Krolyxon
Commit 56c56d4

Branches

Refs →

budctlcratesbudctl-guisrcequalizer.rs

229 lines · 7.5 KB · Rust

1//! The Equalizer page: the presets, and the bands of the custom ones.
2
3use budctl_core::status::{EqBand, EqPreset};
4use iced::widget::{Row, column, container, row, slider, space, text, vertical_slider};
5use iced::{Alignment, Border, Element, Length};
6
7use crate::buds::{self, Buds};
8use crate::icons::{self, Icon};
9use crate::theme::Look;
10use crate::ui;
11use crate::{Draft, Item, Marks, Message};
12
13/// Shown instead of a subtitle while a write is on its way.
14const SAVING: &str = "Saving…";
15
16/// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them: each preset,
17/// "New preset", then for each custom preset "Rename" and its bands.
18pub fn items(buds: &Buds) -> Vec<Item> {
19 let mut items = Vec::new();
20 if let Some(eq) = &buds.eq {
21 items.extend(eq.presets.iter().map(|p| Item::Preset(p.id)));
22 items.push(Item::NewPreset);
23 for preset in eq.presets.iter().filter(|p| p.custom) {
24 items.push(Item::Rename(preset.id));
25 items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz)));
26 }
27 }
28 items
29}
30
31/// `draft`: bands moved on the screen and not saved on the buds yet.
32pub fn view<'a>(look: Look, buds: &'a Buds, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> {
33 let title = ui::title(look, "Equalizer");
34 let Some(eq) = &buds.eq else {
35 let none = text("These buds did not list their presets.")
36 .size(14)
37 .color(look.p.dim);
38 return column![title, none].spacing(12).into();
39 };
40 let current = eq.current.as_deref();
41 let rows = eq
42 .presets
43 .iter()
44 .map(|preset| {
45 let chosen = current == Some(buds::key(preset).as_str());
46 let subtitle = if marks.pending.contains(&Item::Preset(preset.id)) {
47 SAVING
48 } else if preset.custom {
49 "Your preset"
50 } else {
51 "Built in"
52 };
53 let mark: Element<'a, Message> = if chosen {
54 icons::icon(Icon::Check, 20.0, look.p.icon)
55 } else {
56 space().width(20).into()
57 };
58 ui::setting(
59 look,
60 Icon::Equalizer,
61 buds::shown_name(preset),
62 Some(subtitle.to_owned()),
63 mark,
64 Some(Message::Preset(preset.id)),
65 marks.focus == Some(Item::Preset(preset.id)),
66 )
67 })
68 .collect();
69 eq.presets
70 .iter()
71 .filter(|p| p.custom)
72 .fold(
73 column![title, ui::card(look, rows), new_preset(look, marks)].spacing(18),
74 |page, preset| page.push(bands(look, preset, draft, marks)),
75 )
76 .into()
77}
78
79/// The button that creates a custom preset, and what it is doing.
80fn new_preset<'a>(look: Look, marks: Marks) -> Element<'a, Message> {
81 let creating = marks
82 .pending
83 .contains(&Item::NewPreset)
84 .then(|| text("Creating…").size(13).color(look.p.icon));
85 let about = text("A new preset starts flat, with the bands of your other presets, and is put to use.")
86 .size(13)
87 .color(look.p.dim)
88 .width(Length::Fill);
89 row![about]
90 .push(creating)
91 .push(ui::pill_with(
92 look,
93 "New preset",
94 false,
95 marks.focus == Some(Item::NewPreset),
96 Message::NewPreset,
97 ))
98 .spacing(14)
99 .align_y(Alignment::Center)
100 .padding([0, 8])
101 .into()
102}
103
104/// A custom preset's bands, one slider each.
105fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> {
106 let p = look.p;
107 let id = preset.id;
108 let gains: &[EqBand] = match draft {
109 Some(draft) if draft.preset == id => &draft.bands,
110 _ => &preset.bands,
111 };
112 let (low, high) = preset.range.unwrap_or((-6, 6));
113 let saving = marks
114 .pending
115 .contains(&Item::Preset(id))
116 .then(|| text(SAVING).size(13).color(p.icon));
117 let heading = row![
118 text(preset.name.as_str()).size(16).font(look.medium()).color(p.fg),
119 text(format!("{low:+} to {high:+} dB"))
120 .size(13)
121 .color(p.dim)
122 .width(Length::Fill)
123 ]
124 .push(saving)
125 .push(ui::pill_with(
126 look,
127 "Rename",
128 false,
129 marks.focus == Some(Item::Rename(id)),
130 Message::RenamePreset(id),
131 ))
132 .spacing(10)
133 .align_y(Alignment::Center);
134 let sliders = gains.iter().fold(Row::new().spacing(6), |sliders, &band| {
135 let focused = marks.focus == Some(Item::Band(id, band.hz));
136 let hz = band.hz;
137 let slider = vertical_slider(i16::from(low)..=i16::from(high), i16::from(band.gain), move |gain| {
138 Message::Band {
139 preset: id,
140 hz,
141 gain: i8::try_from(gain).unwrap_or(0),
142 save: false,
143 }
144 })
145 .on_release(Message::SaveBands(id))
146 .height(170)
147 .style(move |_, _| slider::Style {
148 rail: slider::Rail {
149 backgrounds: (p.accent.into(), p.surface_highest.into()),
150 width: 6.0,
151 border: Border::default().rounded(3),
152 },
153 handle: slider::Handle {
154 shape: slider::HandleShape::Circle { radius: 9.0 },
155 background: p.fg.into(),
156 border_width: 2.0,
157 border_color: p.accent,
158 },
159 });
160 let band = column![
161 text(gain(band.gain)).size(14).font(look.medium()).color(p.fg),
162 slider,
163 text(frequency(hz)).size(12).color(p.dim)
164 ]
165 .spacing(10)
166 .align_x(Alignment::Center);
167 sliders.push(
168 container(band)
169 .center_x(Length::Fill)
170 .padding([12, 0])
171 .style(move |_| container::Style {
172 background: focused.then_some(p.surface_high.into()),
173 border: Border {
174 color: p.accent,
175 width: if focused { 2.0 } else { 0.0 },
176 radius: 12.0.into(),
177 },
178 ..container::Style::default()
179 }),
180 )
181 });
182 ui::panel(look, column![heading, sliders].spacing(16).into())
183}
184
185/// `3` → "+3", `0` → "0", `-2` → "-2".
186pub fn gain(db: i8) -> String {
187 if db == 0 { "0".to_owned() } else { format!("{db:+}") }
188}
189
190/// `50` → "50 Hz", `16000` → "16 kHz".
191fn frequency(hz: u16) -> String {
192 if hz >= 1000 && hz.is_multiple_of(1000) {
193 format!("{} kHz", hz / 1000)
194 } else {
195 format!("{hz} Hz")
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn items_follow_the_page() {
205 let items = items(&buds::demo());
206 assert_eq!(items.len(), 5 + 1 + 1 + 6);
207 assert_eq!(items.first(), Some(&Item::Preset(0)));
208 assert_eq!(items.get(5), Some(&Item::NewPreset));
209 assert_eq!(items.get(6), Some(&Item::Rename(4)));
210 assert_eq!(items.get(7), Some(&Item::Band(4, 50)));
211 assert_eq!(items.last(), Some(&Item::Band(4, 16000)));
212 assert!(super::items(&buds::unknown()).is_empty());
213 }
214
215 #[test]
216 fn gains() {
217 assert_eq!(gain(3), "+3");
218 assert_eq!(gain(0), "0");
219 assert_eq!(gain(-6), "-6");
220 }
221
222 #[test]
223 fn frequencies() {
224 assert_eq!(frequency(50), "50 Hz");
225 assert_eq!(frequency(1000), "1 kHz");
226 assert_eq!(frequency(16000), "16 kHz");
227 assert_eq!(frequency(62), "62 Hz");
228 }
229}