Krolyxon
Commit 3c3ef17

Branches

Refs →

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.
Author
krolyxon
Date
2026-10-28 01:23 +0530
Parent
a3246e9
Commit
3c3ef1728f1710eb4655083fdb0096b2a4e14a4b

+107 −110

3 files changed

  1. M crates/budctl-gui/src/equalizer.rs +7 −90
  2. M crates/budctl-gui/src/main.rs +14 −9
  3. M crates/budctl-gui/src/overview.rs +86 −11
M crates/budctl-gui/src/equalizer.rs View
@@ -1,7 +1,6 @@
11 //! The Equalizer page: the presets, and the bands of the custom ones.
22
33 use budctl_core::status::{EqBand, EqPreset};
4−use iced::widget::slider::Slider;
54 use iced::widget::{Row, column, container, row, slider, space, text, vertical_slider};
65 use iced::{Alignment, Border, Element, Length};
76
@@ -15,7 +14,7 @@ use crate::{Draft, Item, Marks, Message};
1514 const SAVING: &str = "Saving…";
1615
1716 /// 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.
1918 pub fn items(buds: &Buds) -> Vec<Item> {
2019 let mut items = Vec::new();
2120 if let Some(eq) = &buds.eq {
@@ -24,30 +23,17 @@ pub fn items(buds: &Buds) -> Vec<Item> {
2423 items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz)));
2524 }
2625 }
27− if buds.bass_range.is_some() {
28− items.push(Item::Bass);
29− }
3026 items
3127 }
3228
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> {
4231 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));
4632 let Some(eq) = &buds.eq else {
4733 let none = text("These buds did not list their presets.")
4834 .size(14)
4935 .color(look.p.dim);
50− return column![title, none].push(bass).spacing(12).into();
36+ return column![title, none].spacing(12).into();
5137 };
5238 let current = eq.current.as_deref();
5339 let rows = eq
@@ -84,77 +70,9 @@ pub fn view<'a>(
8470 .fold(column![title, ui::card(look, rows)].spacing(18), |page, preset| {
8571 page.push(bands(look, preset, draft, marks))
8672 })
87− .push(bass)
8873 .into()
8974 }
9075
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−
15876 /// A custom preset's bands, one slider each.
15977 fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> {
16078 let p = look.p;
@@ -230,7 +148,7 @@ fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks:
230148 }
231149
232150 /// `3` → "+3", `0` → "0", `-2` → "-2".
233−fn gain(db: i8) -> String {
151+pub fn gain(db: i8) -> String {
234152 if db == 0 { "0".to_owned() } else { format!("{db:+}") }
235153 }
236154
@@ -250,11 +168,10 @@ mod tests {
250168 #[test]
251169 fn items_follow_the_page() {
252170 let items = items(&buds::demo());
253− assert_eq!(items.len(), 5 + 6 + 1);
171+ assert_eq!(items.len(), 5 + 6);
254172 assert_eq!(items.first(), Some(&Item::Preset(0)));
255173 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)));
258175 assert!(super::items(&buds::unknown()).is_empty());
259176 }
260177
M crates/budctl-gui/src/main.rs View
@@ -1005,7 +1005,7 @@ impl App {
10051005 focus: self.focus(),
10061006 pending: &pending,
10071007 };
1008− overview::view(look, &self.buds, self.anc_level, marks)
1008+ overview::view(look, &self.buds, self.anc_level, self.bass_draft, marks)
10091009 }
10101010 Page::Overview => self.connecting(),
10111011 Page::Equalizer if self.has_reading() => {
@@ -1013,7 +1013,7 @@ impl App {
10131013 focus: self.focus(),
10141014 pending: &pending,
10151015 };
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)
10171017 }
10181018 Page::Gestures if self.has_reading() => {
10191019 let marks = Marks {
@@ -1348,6 +1348,7 @@ mod tests {
13481348 Item::Switch("game"),
13491349 Item::Switch("spatial"),
13501350 Item::Switch("bass-boost"),
1351+ Item::Bass,
13511352 Item::Switch("wind"),
13521353 Item::Switch("dual"),
13531354 Item::Open(Page::Equalizer),
@@ -1491,7 +1492,7 @@ mod tests {
14911492 assert_eq!(app.link, Link::Live);
14921493 assert_eq!(app.buds.name, "realme Buds T310");
14931494 assert!(app.buds.connected);
1494− assert_eq!(app.items().len(), 10);
1495+ assert_eq!(app.items().len(), 11);
14951496 }
14961497
14971498 #[test]
@@ -1594,7 +1595,7 @@ mod tests {
15941595 press(&mut app, "j l");
15951596 assert_eq!(app.buds.status.eq.as_deref(), Some("pure-raw"));
15961597 assert_eq!(app.buds.eq_name(), "Pure raw");
1597− press(&mut app, "G k");
1598+ press(&mut app, "G");
15981599 assert_eq!(app.focus(), Some(Item::Band(4, 16000)));
15991600 // Moving a band saves the custom preset, which also selects it.
16001601 press(&mut app, "l");
@@ -1610,7 +1611,7 @@ mod tests {
16101611 let (mut app, commands) = live();
16111612 let _ = commands.try_recv();
16121613 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");
16141615 let gain = |bands: &[EqBand]| bands.last().map(|band| band.gain);
16151616 assert!(matches!(
16161617 commands.try_recv(),
@@ -1731,8 +1732,8 @@ mod tests {
17311732 #[test]
17321733 fn the_bass_level_by_key() {
17331734 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");
17361737 assert_eq!(app.buds.bass, None, "the buds do not say");
17371738 press(&mut app, "l");
17381739 assert_eq!(app.buds.bass, Some(0), "the first step starts from the middle");
@@ -1815,10 +1816,14 @@ mod tests {
18151816 shots.push(("overview-full".to_owned(), 1240.0, on(Page::Overview, "")));
18161817 shots.push(("overview-cursor".to_owned(), 800.0, on(Page::Overview, "j j j j")));
18171818 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")));
18191820 shots.push(("gestures-cursor".to_owned(), 1000.0, on(Page::Gestures, "j j")));
18201821 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+ ));
18221827 let mut connecting = App::new(palette.clone(), font, None, false);
18231828 connecting.link = Link::Connecting;
18241829 shots.push(("connecting".to_owned(), 800.0, connecting));
M crates/budctl-gui/src/overview.rs View
@@ -1,10 +1,12 @@
11 //! The Overview page: battery, noise control, the switches, and the way to the other pages.
22
33 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};
67
78 use crate::buds::Buds;
9+use crate::equalizer;
810 use crate::icons::{self, Icon, Part, RingColors};
911 use crate::theme::Look;
1012 use crate::ui::{self, Segment};
@@ -19,15 +21,30 @@ pub fn items(buds: &Buds) -> Vec<Item> {
1921 items.push(Item::AncLevel);
2022 }
2123 }
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+ }
2330 items.extend(MORE.map(|(page, _, _)| Item::Open(page)));
2431 items
2532 }
2633
34+/// The switch the bass level shows under.
35+const BASS_BOOST: &str = "bass-boost";
36+
2737 /// Shown instead of an item's subtitle while a write to it is on its way.
2838 const SAVING: &str = "Saving…";
2939
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> {
3148 column![ui::title(look, "Overview"), battery(look, buds)]
3249 .push(
3350 buds.status
@@ -35,7 +52,7 @@ pub fn view<'a>(look: Look, buds: &'a Buds, anc_level: AncLevel, marks: Marks) -
3552 .as_ref()
3653 .map(|now| noise_control(look, &now.mode, anc_level, marks)),
3754 )
38− .push(switches(look, &buds.status.switches, marks))
55+ .push(switches(look, buds, bass, marks))
3956 .push(more(look, buds, marks.focus))
4057 .spacing(18)
4158 .into()
@@ -151,11 +168,11 @@ fn shown(switches: &[Switch]) -> Vec<&Switch> {
151168 shown
152169 }
153170
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> {
155172 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({
159176 let (icon, about) = known(s.key).map_or((Icon::Settings, None), |&(_, icon, about)| (icon, Some(about)));
160177 let saving = marks.pending.contains(&Item::Switch(s.key));
161178 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
168185 Some(Message::Switch(s.key, !s.on)),
169186 marks.focus == Some(Item::Switch(s.key)),
170187 )
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+ }
173193 ui::card(look, rows)
174194 }
175195
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+
176251 /// The rows that open other pages: the page, its icon and title.
177252 const MORE: [(Page, Icon, &str); 3] = [
178253 (Page::Equalizer, Icon::Equalizer, "Equalizer"),