budctl3c3ef17
budctl-gui: the bass level goes under bass boost, on the Overview
On the Equalizer page it sat below the custom bands, out of sight. It is now a row with its own slider right under the Bass boost switch, where it is looked for, and the cursor reaches it there. No new dependencies.
+107 −110
3 files changed
- M crates/budctl-gui/src/equalizer.rs +7 −90
- M crates/budctl-gui/src/main.rs +14 −9
- M crates/budctl-gui/src/overview.rs +86 −11
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | //! The Equalizer page: the presets, and the bands of the custom ones. | |
| 2 | 2 | ||
| 3 | 3 | use budctl_core::status::{EqBand, EqPreset}; | |
| 4 | − | use iced::widget::slider::Slider; | |
| 5 | 4 | use iced::widget::{Row, column, container, row, slider, space, text, vertical_slider}; | |
| 6 | 5 | use iced::{Alignment, Border, Element, Length}; | |
| 7 | 6 | ||
| @@ -15,7 +14,7 @@ use crate::{Draft, Item, Marks, Message}; | |||
| 15 | 14 | const SAVING: &str = "Saving…"; | |
| 16 | 15 | ||
| 17 | 16 | /// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them: each preset, | |
| 18 | − | /// then each band of each custom preset, then the bass level. | |
| 17 | + | /// then each band of each custom preset. | |
| 19 | 18 | pub fn items(buds: &Buds) -> Vec<Item> { | |
| 20 | 19 | let mut items = Vec::new(); | |
| 21 | 20 | if let Some(eq) = &buds.eq { | |
| @@ -24,30 +23,17 @@ pub fn items(buds: &Buds) -> Vec<Item> { | |||
| 24 | 23 | items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz))); | |
| 25 | 24 | } | |
| 26 | 25 | } | |
| 27 | − | if buds.bass_range.is_some() { | |
| 28 | − | items.push(Item::Bass); | |
| 29 | − | } | |
| 30 | 26 | items | |
| 31 | 27 | } | |
| 32 | 28 | ||
| 33 | − | /// `draft`: bands moved on the screen and not saved on the buds yet; `bass`: a bass level | |
| 34 | − | /// likewise. | |
| 35 | − | pub fn view<'a>( | |
| 36 | − | look: Look, | |
| 37 | − | buds: &'a Buds, | |
| 38 | − | draft: Option<&'a Draft>, | |
| 39 | − | bass: Option<i8>, | |
| 40 | − | marks: Marks, | |
| 41 | − | ) -> Element<'a, Message> { | |
| 29 | + | /// `draft`: bands moved on the screen and not saved on the buds yet. | |
| 30 | + | pub fn view<'a>(look: Look, buds: &'a Buds, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> { | |
| 42 | 31 | let title = ui::title(look, "Equalizer"); | |
| 43 | − | let bass = buds | |
| 44 | − | .bass_range | |
| 45 | − | .map(|range| bass_level(look, range, bass.or(buds.bass), marks)); | |
| 46 | 32 | let Some(eq) = &buds.eq else { | |
| 47 | 33 | let none = text("These buds did not list their presets.") | |
| 48 | 34 | .size(14) | |
| 49 | 35 | .color(look.p.dim); | |
| 50 | − | return column![title, none].push(bass).spacing(12).into(); | |
| 36 | + | return column![title, none].spacing(12).into(); | |
| 51 | 37 | }; | |
| 52 | 38 | let current = eq.current.as_deref(); | |
| 53 | 39 | let rows = eq | |
| @@ -84,77 +70,9 @@ pub fn view<'a>( | |||
| 84 | 70 | .fold(column![title, ui::card(look, rows)].spacing(18), |page, preset| { | |
| 85 | 71 | page.push(bands(look, preset, draft, marks)) | |
| 86 | 72 | }) | |
| 87 | − | .push(bass) | |
| 88 | 73 | .into() | |
| 89 | 74 | } | |
| 90 | 75 | ||
| 91 | − | /// The bass level: the buds take it but do not report it, so the page shows the last level | |
| 92 | − | /// set from here, if any. | |
| 93 | − | fn bass_level<'a>(look: Look, (low, high): (i8, i8), level: Option<i8>, marks: Marks) -> Element<'a, Message> { | |
| 94 | − | let p = look.p; | |
| 95 | − | let focused = marks.focus == Some(Item::Bass); | |
| 96 | − | let saving = marks | |
| 97 | − | .pending | |
| 98 | − | .contains(&Item::Bass) | |
| 99 | − | .then(|| text(SAVING).size(13).color(p.icon)); | |
| 100 | − | let shown = level.map_or_else(|| "Not set".to_owned(), gain); | |
| 101 | − | let heading = row![ | |
| 102 | − | text("Bass level").size(16).font(look.medium()).color(p.fg), | |
| 103 | − | text(shown) | |
| 104 | − | .size(14) | |
| 105 | − | .font(look.medium()) | |
| 106 | − | .color(p.dim) | |
| 107 | − | .width(Length::Fill) | |
| 108 | − | ] | |
| 109 | − | .push(saving) | |
| 110 | − | .spacing(10) | |
| 111 | − | .align_y(Alignment::Center); | |
| 112 | − | let note = text("The buds do not report their bass level: this is the last one set from here.") | |
| 113 | − | .size(13) | |
| 114 | − | .color(p.dim); | |
| 115 | − | let slider: Slider<'a, i16, Message> = slider( | |
| 116 | − | i16::from(low)..=i16::from(high), | |
| 117 | − | i16::from(level.unwrap_or(0)), | |
| 118 | − | |level| Message::Bass { | |
| 119 | − | level: i8::try_from(level).unwrap_or(0), | |
| 120 | − | save: false, | |
| 121 | − | }, | |
| 122 | − | ) | |
| 123 | − | .on_release(Message::SaveBass) | |
| 124 | − | .style(move |_, _| slider::Style { | |
| 125 | − | rail: slider::Rail { | |
| 126 | − | backgrounds: (p.accent.into(), p.surface_highest.into()), | |
| 127 | − | width: 6.0, | |
| 128 | − | border: Border::default().rounded(3), | |
| 129 | − | }, | |
| 130 | − | handle: slider::Handle { | |
| 131 | − | shape: slider::HandleShape::Circle { radius: 9.0 }, | |
| 132 | − | background: p.fg.into(), | |
| 133 | − | border_width: 2.0, | |
| 134 | − | border_color: p.accent, | |
| 135 | − | }, | |
| 136 | − | }); | |
| 137 | − | let scale = row![ | |
| 138 | − | text(gain(low)).size(12).color(p.dim), | |
| 139 | − | space::horizontal(), | |
| 140 | − | text("0").size(12).color(p.dim), | |
| 141 | − | space::horizontal(), | |
| 142 | − | text(gain(high)).size(12).color(p.dim) | |
| 143 | − | ]; | |
| 144 | − | let content = container(column![heading, note, slider, scale].spacing(12)) | |
| 145 | − | .padding(12) | |
| 146 | − | .style(move |_| container::Style { | |
| 147 | − | background: focused.then_some(p.surface_high.into()), | |
| 148 | − | border: Border { | |
| 149 | − | color: p.accent, | |
| 150 | − | width: if focused { 2.0 } else { 0.0 }, | |
| 151 | − | radius: 12.0.into(), | |
| 152 | − | }, | |
| 153 | − | ..container::Style::default() | |
| 154 | − | }); | |
| 155 | − | ui::panel(look, content.into()) | |
| 156 | − | } | |
| 157 | − | ||
| 158 | 76 | /// A custom preset's bands, one slider each. | |
| 159 | 77 | fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> { | |
| 160 | 78 | let p = look.p; | |
| @@ -230,7 +148,7 @@ fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: | |||
| 230 | 148 | } | |
| 231 | 149 | ||
| 232 | 150 | /// `3` → "+3", `0` → "0", `-2` → "-2". | |
| 233 | − | fn gain(db: i8) -> String { | |
| 151 | + | pub fn gain(db: i8) -> String { | |
| 234 | 152 | if db == 0 { "0".to_owned() } else { format!("{db:+}") } | |
| 235 | 153 | } | |
| 236 | 154 | ||
| @@ -250,11 +168,10 @@ mod tests { | |||
| 250 | 168 | #[test] | |
| 251 | 169 | fn items_follow_the_page() { | |
| 252 | 170 | let items = items(&buds::demo()); | |
| 253 | − | assert_eq!(items.len(), 5 + 6 + 1); | |
| 171 | + | assert_eq!(items.len(), 5 + 6); | |
| 254 | 172 | assert_eq!(items.first(), Some(&Item::Preset(0))); | |
| 255 | 173 | assert_eq!(items.get(5), Some(&Item::Band(4, 50))); | |
| 256 | − | assert_eq!(items.get(10), Some(&Item::Band(4, 16000))); | |
| 257 | − | assert_eq!(items.last(), Some(&Item::Bass)); | |
| 174 | + | assert_eq!(items.last(), Some(&Item::Band(4, 16000))); | |
| 258 | 175 | assert!(super::items(&buds::unknown()).is_empty()); | |
| 259 | 176 | } | |
| 260 | 177 | ||
| @@ -1005,7 +1005,7 @@ impl App { | |||
| 1005 | 1005 | focus: self.focus(), | |
| 1006 | 1006 | pending: &pending, | |
| 1007 | 1007 | }; | |
| 1008 | − | overview::view(look, &self.buds, self.anc_level, marks) | |
| 1008 | + | overview::view(look, &self.buds, self.anc_level, self.bass_draft, marks) | |
| 1009 | 1009 | } | |
| 1010 | 1010 | Page::Overview => self.connecting(), | |
| 1011 | 1011 | Page::Equalizer if self.has_reading() => { | |
| @@ -1013,7 +1013,7 @@ impl App { | |||
| 1013 | 1013 | focus: self.focus(), | |
| 1014 | 1014 | pending: &pending, | |
| 1015 | 1015 | }; | |
| 1016 | − | equalizer::view(look, &self.buds, self.draft.as_ref(), self.bass_draft, marks) | |
| 1016 | + | equalizer::view(look, &self.buds, self.draft.as_ref(), marks) | |
| 1017 | 1017 | } | |
| 1018 | 1018 | Page::Gestures if self.has_reading() => { | |
| 1019 | 1019 | let marks = Marks { | |
| @@ -1348,6 +1348,7 @@ mod tests { | |||
| 1348 | 1348 | Item::Switch("game"), | |
| 1349 | 1349 | Item::Switch("spatial"), | |
| 1350 | 1350 | Item::Switch("bass-boost"), | |
| 1351 | + | Item::Bass, | |
| 1351 | 1352 | Item::Switch("wind"), | |
| 1352 | 1353 | Item::Switch("dual"), | |
| 1353 | 1354 | Item::Open(Page::Equalizer), | |
| @@ -1491,7 +1492,7 @@ mod tests { | |||
| 1491 | 1492 | assert_eq!(app.link, Link::Live); | |
| 1492 | 1493 | assert_eq!(app.buds.name, "realme Buds T310"); | |
| 1493 | 1494 | assert!(app.buds.connected); | |
| 1494 | − | assert_eq!(app.items().len(), 10); | |
| 1495 | + | assert_eq!(app.items().len(), 11); | |
| 1495 | 1496 | } | |
| 1496 | 1497 | ||
| 1497 | 1498 | #[test] | |
| @@ -1594,7 +1595,7 @@ mod tests { | |||
| 1594 | 1595 | press(&mut app, "j l"); | |
| 1595 | 1596 | assert_eq!(app.buds.status.eq.as_deref(), Some("pure-raw")); | |
| 1596 | 1597 | assert_eq!(app.buds.eq_name(), "Pure raw"); | |
| 1597 | − | press(&mut app, "G k"); | |
| 1598 | + | press(&mut app, "G"); | |
| 1598 | 1599 | assert_eq!(app.focus(), Some(Item::Band(4, 16000))); | |
| 1599 | 1600 | // Moving a band saves the custom preset, which also selects it. | |
| 1600 | 1601 | press(&mut app, "l"); | |
| @@ -1610,7 +1611,7 @@ mod tests { | |||
| 1610 | 1611 | let (mut app, commands) = live(); | |
| 1611 | 1612 | let _ = commands.try_recv(); | |
| 1612 | 1613 | let _ = app.update(Message::Worker(Event::Read(Box::new(reading())))); | |
| 1613 | − | press(&mut app, "J G G k l"); | |
| 1614 | + | press(&mut app, "J G G l"); | |
| 1614 | 1615 | let gain = |bands: &[EqBand]| bands.last().map(|band| band.gain); | |
| 1615 | 1616 | assert!(matches!( | |
| 1616 | 1617 | commands.try_recv(), | |
| @@ -1731,8 +1732,8 @@ mod tests { | |||
| 1731 | 1732 | #[test] | |
| 1732 | 1733 | fn the_bass_level_by_key() { | |
| 1733 | 1734 | let mut app = app(); | |
| 1734 | − | press(&mut app, "J G G"); | |
| 1735 | − | assert_eq!(app.focus(), Some(Item::Bass)); | |
| 1735 | + | press(&mut app, "j j j j j j"); | |
| 1736 | + | assert_eq!(app.focus(), Some(Item::Bass), "under bass boost, on the Overview"); | |
| 1736 | 1737 | assert_eq!(app.buds.bass, None, "the buds do not say"); | |
| 1737 | 1738 | press(&mut app, "l"); | |
| 1738 | 1739 | assert_eq!(app.buds.bass, Some(0), "the first step starts from the middle"); | |
| @@ -1815,10 +1816,14 @@ mod tests { | |||
| 1815 | 1816 | shots.push(("overview-full".to_owned(), 1240.0, on(Page::Overview, ""))); | |
| 1816 | 1817 | shots.push(("overview-cursor".to_owned(), 800.0, on(Page::Overview, "j j j j"))); | |
| 1817 | 1818 | shots.push(("keys".to_owned(), 800.0, on(Page::Overview, "?"))); | |
| 1818 | − | shots.push(("equalizer-cursor".to_owned(), 1000.0, on(Page::Equalizer, "G G k k k"))); | |
| 1819 | + | shots.push(("equalizer-cursor".to_owned(), 1000.0, on(Page::Equalizer, "G G k k"))); | |
| 1819 | 1820 | shots.push(("gestures-cursor".to_owned(), 1000.0, on(Page::Gestures, "j j"))); | |
| 1820 | 1821 | shots.push(("find-confirm".to_owned(), 800.0, on(Page::Earbuds, "j Enter"))); | |
| 1821 | − | shots.push(("equalizer-bass".to_owned(), 1300.0, on(Page::Equalizer, "G G l l"))); | |
| 1822 | + | shots.push(( | |
| 1823 | + | "overview-bass".to_owned(), | |
| 1824 | + | 1340.0, | |
| 1825 | + | on(Page::Overview, "j j j j j j l l"), | |
| 1826 | + | )); | |
| 1822 | 1827 | let mut connecting = App::new(palette.clone(), font, None, false); | |
| 1823 | 1828 | connecting.link = Link::Connecting; | |
| 1824 | 1829 | shots.push(("connecting".to_owned(), 800.0, connecting)); | |
| @@ -1,10 +1,12 @@ | |||
| 1 | 1 | //! The Overview page: battery, noise control, the switches, and the way to the other pages. | |
| 2 | 2 | ||
| 3 | 3 | use budctl_core::status::{AncLevel, Charge, NoiseMode, Switch}; | |
| 4 | − | use iced::widget::{column, container, row, text}; | |
| 5 | − | use iced::{Alignment, Element, Length}; | |
| 4 | + | use iced::alignment::Horizontal; | |
| 5 | + | use iced::widget::{column, container, row, slider, text}; | |
| 6 | + | use iced::{Alignment, Border, Element, Length}; | |
| 6 | 7 | ||
| 7 | 8 | use crate::buds::Buds; | |
| 9 | + | use crate::equalizer; | |
| 8 | 10 | use crate::icons::{self, Icon, Part, RingColors}; | |
| 9 | 11 | use crate::theme::Look; | |
| 10 | 12 | use crate::ui::{self, Segment}; | |
| @@ -19,15 +21,30 @@ pub fn items(buds: &Buds) -> Vec<Item> { | |||
| 19 | 21 | items.push(Item::AncLevel); | |
| 20 | 22 | } | |
| 21 | 23 | } | |
| 22 | − | items.extend(shown(&buds.status.switches).into_iter().map(|s| Item::Switch(s.key))); | |
| 24 | + | for switch in shown(&buds.status.switches) { | |
| 25 | + | items.push(Item::Switch(switch.key)); | |
| 26 | + | if switch.key == BASS_BOOST && buds.bass_range.is_some() { | |
| 27 | + | items.push(Item::Bass); | |
| 28 | + | } | |
| 29 | + | } | |
| 23 | 30 | items.extend(MORE.map(|(page, _, _)| Item::Open(page))); | |
| 24 | 31 | items | |
| 25 | 32 | } | |
| 26 | 33 | ||
| 34 | + | /// The switch the bass level shows under. | |
| 35 | + | const BASS_BOOST: &str = "bass-boost"; | |
| 36 | + | ||
| 27 | 37 | /// Shown instead of an item's subtitle while a write to it is on its way. | |
| 28 | 38 | const SAVING: &str = "Saving…"; | |
| 29 | 39 | ||
| 30 | − | pub fn view<'a>(look: Look, buds: &'a Buds, anc_level: AncLevel, marks: Marks) -> Element<'a, Message> { | |
| 40 | + | /// `bass`: a bass level moved on the screen and not set on the buds yet. | |
| 41 | + | pub fn view<'a>( | |
| 42 | + | look: Look, | |
| 43 | + | buds: &'a Buds, | |
| 44 | + | anc_level: AncLevel, | |
| 45 | + | bass: Option<i8>, | |
| 46 | + | marks: Marks, | |
| 47 | + | ) -> Element<'a, Message> { | |
| 31 | 48 | column![ui::title(look, "Overview"), battery(look, buds)] | |
| 32 | 49 | .push( | |
| 33 | 50 | buds.status | |
| @@ -35,7 +52,7 @@ pub fn view<'a>(look: Look, buds: &'a Buds, anc_level: AncLevel, marks: Marks) - | |||
| 35 | 52 | .as_ref() | |
| 36 | 53 | .map(|now| noise_control(look, &now.mode, anc_level, marks)), | |
| 37 | 54 | ) | |
| 38 | − | .push(switches(look, &buds.status.switches, marks)) | |
| 55 | + | .push(switches(look, buds, bass, marks)) | |
| 39 | 56 | .push(more(look, buds, marks.focus)) | |
| 40 | 57 | .spacing(18) | |
| 41 | 58 | .into() | |
| @@ -151,11 +168,11 @@ fn shown(switches: &[Switch]) -> Vec<&Switch> { | |||
| 151 | 168 | shown | |
| 152 | 169 | } | |
| 153 | 170 | ||
| 154 | − | fn switches<'a>(look: Look, switches: &[Switch], marks: Marks) -> Element<'a, Message> { | |
| 171 | + | fn switches<'a>(look: Look, buds: &Buds, bass: Option<i8>, marks: Marks) -> Element<'a, Message> { | |
| 155 | 172 | let known = |key: &str| SWITCHES.iter().find(|(k, _, _)| *k == key); | |
| 156 | − | let rows = shown(switches) | |
| 157 | − | .into_iter() | |
| 158 | − | .map(|s| { | |
| 173 | + | let mut rows = Vec::new(); | |
| 174 | + | for s in shown(&buds.status.switches) { | |
| 175 | + | rows.push({ | |
| 159 | 176 | let (icon, about) = known(s.key).map_or((Icon::Settings, None), |&(_, icon, about)| (icon, Some(about))); | |
| 160 | 177 | let saving = marks.pending.contains(&Item::Switch(s.key)); | |
| 161 | 178 | let subtitle = if saving { Some(SAVING) } else { about }.map(str::to_owned); | |
| @@ -168,11 +185,69 @@ fn switches<'a>(look: Look, switches: &[Switch], marks: Marks) -> Element<'a, Me | |||
| 168 | 185 | Some(Message::Switch(s.key, !s.on)), | |
| 169 | 186 | marks.focus == Some(Item::Switch(s.key)), | |
| 170 | 187 | ) | |
| 171 | − | }) | |
| 172 | − | .collect(); | |
| 188 | + | }); | |
| 189 | + | if let (BASS_BOOST, Some(range)) = (s.key, buds.bass_range) { | |
| 190 | + | rows.push(bass_level(look, range, bass.or(buds.bass), marks)); | |
| 191 | + | } | |
| 192 | + | } | |
| 173 | 193 | ui::card(look, rows) | |
| 174 | 194 | } | |
| 175 | 195 | ||
| 196 | + | /// The bass level: the buds take it but do not report it, so the row shows the last level set | |
| 197 | + | /// from this window, if any. | |
| 198 | + | fn bass_level<'a>(look: Look, (low, high): (i8, i8), level: Option<i8>, marks: Marks) -> Element<'a, Message> { | |
| 199 | + | let p = look.p; | |
| 200 | + | let about = if marks.pending.contains(&Item::Bass) { | |
| 201 | + | SAVING | |
| 202 | + | } else { | |
| 203 | + | "Last set here: the buds do not report it" | |
| 204 | + | }; | |
| 205 | + | let shown = level.map_or_else(|| "Not set".to_owned(), equalizer::gain); | |
| 206 | + | let slider = slider( | |
| 207 | + | i16::from(low)..=i16::from(high), | |
| 208 | + | i16::from(level.unwrap_or(0)), | |
| 209 | + | |level| Message::Bass { | |
| 210 | + | level: i8::try_from(level).unwrap_or(0), | |
| 211 | + | save: false, | |
| 212 | + | }, | |
| 213 | + | ) | |
| 214 | + | .on_release(Message::SaveBass) | |
| 215 | + | .width(200) | |
| 216 | + | .style(move |_, _| slider::Style { | |
| 217 | + | rail: slider::Rail { | |
| 218 | + | backgrounds: (p.accent.into(), p.surface_highest.into()), | |
| 219 | + | width: 6.0, | |
| 220 | + | border: Border::default().rounded(3), | |
| 221 | + | }, | |
| 222 | + | handle: slider::Handle { | |
| 223 | + | shape: slider::HandleShape::Circle { radius: 9.0 }, | |
| 224 | + | background: p.fg.into(), | |
| 225 | + | border_width: 2.0, | |
| 226 | + | border_color: p.accent, | |
| 227 | + | }, | |
| 228 | + | }); | |
| 229 | + | let control = row![ | |
| 230 | + | text(shown) | |
| 231 | + | .size(14) | |
| 232 | + | .font(look.medium()) | |
| 233 | + | .color(p.dim) | |
| 234 | + | .width(64) | |
| 235 | + | .align_x(Horizontal::Right), | |
| 236 | + | slider | |
| 237 | + | ] | |
| 238 | + | .spacing(14) | |
| 239 | + | .align_y(Alignment::Center); | |
| 240 | + | ui::setting( | |
| 241 | + | look, | |
| 242 | + | Icon::Bass, | |
| 243 | + | "Bass level", | |
| 244 | + | Some(about.to_owned()), | |
| 245 | + | control.into(), | |
| 246 | + | None, | |
| 247 | + | marks.focus == Some(Item::Bass), | |
| 248 | + | ) | |
| 249 | + | } | |
| 250 | + | ||
| 176 | 251 | /// The rows that open other pages: the page, its icon and title. | |
| 177 | 252 | const MORE: [(Page, Icon, &str); 3] = [ | |
| 178 | 253 | (Page::Equalizer, Icon::Equalizer, "Equalizer"), | |