94 lines
3.0 KiB
Rust
94 lines
3.0 KiB
Rust
//! 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<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
|
|
}
|
|
}
|