Krolyxon
Commit 93bfdf6

Branches

Refs →

budctlcratesbudctl-guisrcequalizer.rs

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