//! 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::{Alignment, Element, Length, Task}; use common::{pill, 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; /// 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) -> 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 { if let Some(state) = msg.downcast::() { self.state = *state; } Task::none() } fn services(&self) -> Vec { 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 } }