Krolyxon
Commit 9dea828

Branches

Refs →

budctlcratesbudctl-guisrcequalizer.rs

288 lines · 9.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, text};
5use iced::{Alignment, Border, Element, Length};
6
7use crate::buds::{self, Buds};
8use crate::dots;
9use crate::icons::Icon;
10use crate::theme::Look;
11use crate::ui;
12use crate::{Draft, Item, Marks, Message};
13
14/// Shown instead of a subtitle while a write is on its way.
15const SAVING: &str = "Saving…";
16
17/// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them: each preset,
18/// with a custom one's pencil and bin; "New preset"; then, while a preset's bands are open,
19/// "Rename", "Done" and its bands.
20pub fn items(buds: &Buds, editing: Option<u8>) -> Vec<Item> {
21 let mut items = Vec::new();
22 let Some(eq) = &buds.eq else {
23 return items;
24 };
25 for preset in &eq.presets {
26 items.push(Item::Preset(preset.id));
27 if preset.custom {
28 items.extend([Item::Edit(preset.id), Item::Delete(preset.id)]);
29 }
30 }
31 items.push(Item::NewPreset);
32 if let Some(preset) = edited(buds, editing) {
33 items.extend([Item::Rename(preset.id), Item::Done]);
34 items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz)));
35 }
36 items
37}
38
39/// The custom preset in use, if one is.
40pub fn in_use(buds: &Buds) -> Option<&EqPreset> {
41 let eq = buds.eq.as_ref()?;
42 let current = eq.current.as_deref()?;
43 eq.presets.iter().find(|p| p.custom && buds::key(p) == current)
44}
45
46/// The custom preset whose bands are open, if any.
47pub fn edited(buds: &Buds, editing: Option<u8>) -> Option<&EqPreset> {
48 buds.preset(editing?).filter(|preset| preset.custom)
49}
50
51/// `editing`: the custom preset whose bands are open, drawn in `opened` (they fade in when
52/// opened); `draft`: bands moved on the screen and not saved on the buds yet.
53pub fn view<'a>(
54 look: Look,
55 opened: Look,
56 buds: &'a Buds,
57 editing: Option<u8>,
58 draft: Option<&'a Draft>,
59 marks: Marks,
60) -> Element<'a, Message> {
61 let p = look.p;
62 let title = ui::title(look, "equalizer · presets, and the bands of yours");
63 let Some(eq) = &buds.eq else {
64 let none = ui::note(look, "These buds did not list their presets.");
65 return column![title, none].spacing(12).into();
66 };
67 let current = eq.current.as_deref();
68 let rows = eq
69 .presets
70 .iter()
71 .map(|preset| {
72 let id = preset.id;
73 let chosen = current == Some(buds::key(preset).as_str());
74 let subtitle = if marks.pending.contains(&Item::Preset(id)) {
75 SAVING
76 } else if preset.custom && editing == Some(id) {
77 "Your preset, open below"
78 } else if preset.custom {
79 "Your preset"
80 } else {
81 "Built in"
82 };
83 let mark: Option<Element<'a, Message>> = chosen.then(|| ui::label(look, "in use", 10.5, p.fg));
84 let trailing: Element<'a, Message> = if preset.custom {
85 row![mark]
86 .push(ui::icon_button(
87 look,
88 Icon::Pencil,
89 marks.focus == Some(Item::Edit(id)),
90 Message::EditPreset(id),
91 ))
92 .push(ui::icon_button(
93 look,
94 Icon::Trash,
95 marks.focus == Some(Item::Delete(id)),
96 Message::DeletePreset(id),
97 ))
98 .spacing(8)
99 .align_y(Alignment::Center)
100 .into()
101 } else {
102 row![mark].into()
103 };
104 ui::list_row(
105 look,
106 Icon::Equalizer,
107 chosen,
108 buds::shown_name(preset),
109 Some(subtitle.to_owned()),
110 trailing,
111 Some(Message::Preset(id)),
112 marks.focus == Some(Item::Preset(id)),
113 )
114 })
115 .collect();
116 let editor: Option<Element<'a, Message>> = match edited(buds, editing) {
117 Some(preset) => Some(bands(opened, preset, draft, marks)),
118 None if eq.presets.iter().any(|p| p.custom) => Some(
119 container(ui::note(
120 look,
121 "Use the pencil on one of your presets to change its bands.",
122 ))
123 .padding([0, 18])
124 .into(),
125 ),
126 None => None,
127 };
128 column![title, ui::card(look, rows), new_preset(look, marks), editor]
129 .spacing(18)
130 .into()
131}
132
133/// The button that creates a custom preset, and what it is doing.
134fn new_preset<'a>(look: Look, marks: Marks) -> Element<'a, Message> {
135 let creating = marks
136 .pending
137 .contains(&Item::NewPreset)
138 .then(|| ui::label(look, "Creating…", 11.0, look.p.live));
139 let about = container(ui::note(look, "A new preset starts flat and is put to use.")).width(Length::Fill);
140 row![about]
141 .push(creating)
142 .push(ui::pill_with(
143 look,
144 "New preset",
145 false,
146 marks.focus == Some(Item::NewPreset),
147 Message::NewPreset,
148 ))
149 .spacing(14)
150 .align_y(Alignment::Center)
151 .padding([0, 18])
152 .into()
153}
154
155/// A custom preset's bands, one slider each.
156fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> {
157 let p = look.p;
158 let id = preset.id;
159 let gains: &[EqBand] = match draft {
160 Some(draft) if draft.preset == id => &draft.bands,
161 _ => &preset.bands,
162 };
163 let (low, high) = preset.range.unwrap_or((-6, 6));
164 let saving = marks
165 .pending
166 .contains(&Item::Preset(id))
167 .then(|| ui::label(look, SAVING, 11.0, p.live));
168 let heading = row![
169 text(preset.name.as_str()).size(20).font(look.semibold()).color(p.fg),
170 container(ui::label(look, &format!("{low:+} to {high:+} dB"), 11.0, p.grey)).width(Length::Fill)
171 ]
172 .push(saving)
173 .push(ui::pill_with(
174 look,
175 "Rename",
176 false,
177 marks.focus == Some(Item::Rename(id)),
178 Message::RenamePreset(id),
179 ))
180 .push(ui::pill_with(
181 look,
182 "Done",
183 false,
184 marks.focus == Some(Item::Done),
185 Message::DoneEditing,
186 ))
187 .spacing(10)
188 .align_y(Alignment::Center);
189 let note = ui::note(
190 look,
191 "Moving a band saves this preset and puts it in use, so you hear it.",
192 );
193 let sliders = gains.iter().fold(Row::new().spacing(6), |sliders, &band| {
194 let focused = marks.focus == Some(Item::Band(id, band.hz));
195 let hz = band.hz;
196 let slider = dots::Slider {
197 low: i16::from(low),
198 high: i16::from(high),
199 value: i16::from(band.gain),
200 vertical: true,
201 change: move |gain, save| Message::Band {
202 preset: id,
203 hz,
204 gain: i8::try_from(gain).unwrap_or(0),
205 save,
206 },
207 release: Message::SaveBands(id),
208 on: p.fill,
209 off: p.s3,
210 motion: look.motion,
211 };
212 let band = column![
213 ui::num(look, &gain(band.gain), 28.0, p.fg),
214 slider.view(190.0),
215 ui::label(look, &frequency(hz), 10.5, p.grey)
216 ]
217 .spacing(12)
218 .align_x(Alignment::Center);
219 sliders.push(
220 container(band)
221 .center_x(Length::Fill)
222 .padding([14, 0])
223 .style(move |_| container::Style {
224 background: focused.then_some(p.s2.into()),
225 border: Border {
226 color: p.fill,
227 width: if focused { 2.0 } else { 0.0 },
228 radius: 24.0.into(),
229 },
230 ..container::Style::default()
231 }),
232 )
233 });
234 ui::panel(look, column![heading, note, sliders].spacing(16).into())
235}
236
237/// `3` → "+3", `0` → "0", `-2` → "-2".
238pub fn gain(db: i8) -> String {
239 if db == 0 { "0".to_owned() } else { format!("{db:+}") }
240}
241
242/// `50` → "50 Hz", `16000` → "16 kHz".
243fn frequency(hz: u16) -> String {
244 if hz >= 1000 && hz.is_multiple_of(1000) {
245 format!("{} kHz", hz / 1000)
246 } else {
247 format!("{hz} Hz")
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn items_follow_the_page() {
257 let buds = buds::demo();
258 let closed = items(&buds, None);
259 assert_eq!(closed.len(), 4 + 3 + 1);
260 assert_eq!(closed.first(), Some(&Item::Preset(0)));
261 assert_eq!(
262 closed.get(4..),
263 Some(&[Item::Preset(4), Item::Edit(4), Item::Delete(4), Item::NewPreset][..])
264 );
265 let open = items(&buds, Some(4));
266 assert_eq!(open.len(), 8 + 2 + 6);
267 assert_eq!(open.get(8..10), Some(&[Item::Rename(4), Item::Done][..]));
268 assert_eq!(open.last(), Some(&Item::Band(4, 16000)));
269 // A built-in preset has no bands to open.
270 assert_eq!(items(&buds, Some(0)).len(), 8);
271 assert!(items(&buds::unknown(), None).is_empty());
272 }
273
274 #[test]
275 fn gains() {
276 assert_eq!(gain(3), "+3");
277 assert_eq!(gain(0), "0");
278 assert_eq!(gain(-6), "-6");
279 }
280
281 #[test]
282 fn frequencies() {
283 assert_eq!(frequency(50), "50 Hz");
284 assert_eq!(frequency(1000), "1 kHz");
285 assert_eq!(frequency(16000), "16 kHz");
286 assert_eq!(frequency(62), "62 Hz");
287 }
288}