This commit is contained in:
2026-09-13 19:47:01 +01:00
parent 14c3bb0495
commit ec43d7b57a
12 changed files with 602 additions and 5 deletions
+106
View File
@@ -0,0 +1,106 @@
//! Audio module: the default sink and source as icons, drawn only while
//! something is muted — the signal the quickshell Audio widget gives.
use iced::widget::{container, row, space, text};
use iced::{border, Alignment, Element, Length, Task, Theme};
use common::{AudioNode, BarModule, ModuleEffect, PipewireMsg, PipewireState, Service, Wire};
/// Glyphs exactly as the Quickshell Audio widget draws them: the speaker is a
/// Material `md-volume` icon, the mic a Font Awesome one.
const SINK_ON: &str = "\u{f057e}"; // md-volume_high
const SINK_MUTED: &str = "\u{f0581}"; // md-volume_off
const MIC_ON: &str = "\u{f130}"; // fa-microphone
const MIC_MUTED: &str = "\u{f131}"; // fa-microphone_slash
/// Quickshell's geometry at its 20 px reference bar: a 24 px slot per icon
/// and 4 px between them. Heights and the pill fill the bar through layout,
/// but font size is a fixed px value iced cannot derive from the space it is
/// given — if the bar ever leaves 20 px, scale the two sizes here.
const REF_SLOT_W: f32 = 24.0;
const REF_SPACING: f32 = 4.0;
const REF_RADIUS: f32 = 3.5;
/// The speaker is re-drawn in the mono face, whose ink is 1168/1496 the size
/// of the proportional face quickshell uses, so it is pre-compensated.
const REF_SINK_SIZE: f32 = 20.0 * 1496.0 / 1168.0;
const REF_MIC_SIZE: f32 = 30.0;
pub struct Audio {
state: PipewireState,
}
impl Audio {
pub fn new() -> Self {
Self {
state: PipewireState::default(),
}
}
}
impl BarModule for Audio {
fn id(&self) -> &'static str {
"audio"
}
fn view(&self, _window_id: Option<iced::window::Id>) -> Element<'_, Wire> {
let (sink, source) = (self.state.sink, self.state.source);
if !sink.muted && !source.muted {
// Nothing is muted, so there is nothing to show.
return space::horizontal().into();
}
let icons = row![
icon(glyph(sink, SINK_ON, SINK_MUTED), REF_SINK_SIZE),
icon(glyph(source, MIC_ON, MIC_MUTED), REF_MIC_SIZE),
]
.height(Length::Fill)
.spacing(REF_SPACING);
container(icons)
.height(Length::Fill)
.style(pill)
.into()
}
fn update(&mut self, msg: Wire) -> Task<ModuleEffect> {
if let Some(state) = msg.downcast::<PipewireState>() {
self.state = *state;
}
Task::none()
}
fn services(&self) -> Vec<Service> {
vec![PipewireMsg::State.key()]
}
}
/// One glyph centred in a fixed-width slot that fills the bar's height.
fn icon<'a>(glyph: &'static str, size: f32) -> Element<'a, Wire> {
container(
text(glyph)
.size(size)
.height(Length::Fill)
.align_y(Alignment::Center),
)
.center_x(Length::Fixed(REF_SLOT_W))
.height(Length::Fill)
.into()
}
/// The muted glyph when the node is muted, its normal glyph otherwise.
fn glyph(node: AudioNode, on: &'static str, muted: &'static str) -> &'static str {
if node.muted {
muted
} else {
on
}
}
/// The quickshell pill: rounded, a shade lighter than the bar behind it.
fn pill(theme: &Theme) -> container::Style {
container::Style {
background: Some(theme.extended_palette().background.weak.color.into()),
border: border::rounded(REF_RADIUS),
..container::Style::default()
}
}