budctl6dcfe3c
budctl-gui: the bass level, on the Equalizer page
The buds take a bass level but never report it (W6a), so the card says
so and shows the level last set from the window ("Not set" before
that). A drag sets it once, on release; h/l step it, the first step
from the middle. A level moved while one is on its way goes out when
that is answered, so the latest level wins.
Core: Session::bass_range() declares the levels set_bass_level takes,
when the buds have one (the T310: -5 to +5, if its bitmap allows the
write), so front ends show the setting only for buds that declare it.
The worker reads it with the rest and gained Command::Bass.
No new dependencies.
+236 −18
6 files changed
- M crates/budctl-core/src/devices/mod.rs +6 −0
- M crates/budctl-core/src/devices/realme_t310/mod.rs +10 −1
- M crates/budctl-gui/src/buds.rs +8 −0
- M crates/budctl-gui/src/equalizer.rs +95 −12
- M crates/budctl-gui/src/main.rs +100 −4
- M crates/budctl-gui/src/worker.rs +17 −1
| @@ -78,6 +78,12 @@ pub trait Session: Send { | |||
| 78 | 78 | ))) | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | + | /// The lowest and the highest level [`Session::set_bass_level`] takes, when the buds have a | |
| 82 | + | /// bass level to set. | |
| 83 | + | fn bass_range(&self) -> Option<(i8, i8)> { | |
| 84 | + | None | |
| 85 | + | } | |
| 86 | + | ||
| 81 | 87 | /// Set the bass level. Only the ack can be checked when the buds cannot report the level. | |
| 82 | 88 | fn set_bass_level(&mut self, _level: i8) -> Result<()> { | |
| 83 | 89 | Err(Error::Unsupported( | |
| @@ -379,6 +379,13 @@ impl Session for T310Session { | |||
| 379 | 379 | } | |
| 380 | 380 | ||
| 381 | 381 | /// Ack-only: the T310 never answers the bass-level read. Spec: realme_t310.md#bass-write | |
| 382 | + | /// Spec: realme_t310.md#bass-write | |
| 383 | + | fn bass_range(&self) -> Option<(i8, i8)> { | |
| 384 | + | self.commands | |
| 385 | + | .allows(codec::BASS_LEVEL_WRITE) | |
| 386 | + | .then_some(codec::BASS_RANGE) | |
| 387 | + | } | |
| 388 | + | ||
| 382 | 389 | fn set_bass_level(&mut self, level: i8) -> Result<()> { | |
| 383 | 390 | let payload = codec::bass_write_payload(level)?; | |
| 384 | 391 | if !self.commands.allows(codec::BASS_LEVEL_WRITE) { | |
| @@ -1180,7 +1187,9 @@ mod tests { | |||
| 1180 | 1187 | let mut steps = handshake(); | |
| 1181 | 1188 | steps.extend([tx(0x041B, 0x05, "FB 05 FE"), rx(0x841B, 0x05, "00")]); | |
| 1182 | 1189 | let script = Script::new(steps); | |
| 1183 | − | open(&script).unwrap().set_bass_level(-2).unwrap(); | |
| 1190 | + | let mut session = open(&script).unwrap(); | |
| 1191 | + | assert_eq!(session.bass_range(), Some((-5, 5))); | |
| 1192 | + | session.set_bass_level(-2).unwrap(); | |
| 1184 | 1193 | script.assert_done(); | |
| 1185 | 1194 | } | |
| 1186 | 1195 | ||
| @@ -14,6 +14,10 @@ pub struct Buds { | |||
| 14 | 14 | pub eq: Option<EqPresets>, | |
| 15 | 15 | /// The touch controls, when the buds report them. | |
| 16 | 16 | pub touch: Option<TouchControls>, | |
| 17 | + | /// The levels the bass level takes, when the buds have one to set. | |
| 18 | + | pub bass_range: Option<(i8, i8)>, | |
| 19 | + | /// The bass level last set from this window. The buds do not report it. | |
| 20 | + | pub bass: Option<i8>, | |
| 17 | 21 | } | |
| 18 | 22 | ||
| 19 | 23 | impl Buds { | |
| @@ -161,6 +165,8 @@ pub fn demo() -> Buds { | |||
| 161 | 165 | cycle_modes: names(&["off", "transparency", "anc"]), | |
| 162 | 166 | warnings: Vec::new(), | |
| 163 | 167 | }), | |
| 168 | + | bass_range: Some((-5, 5)), | |
| 169 | + | bass: None, | |
| 164 | 170 | } | |
| 165 | 171 | } | |
| 166 | 172 | ||
| @@ -172,6 +178,8 @@ pub fn unknown() -> Buds { | |||
| 172 | 178 | status: Status::default(), | |
| 173 | 179 | eq: None, | |
| 174 | 180 | touch: None, | |
| 181 | + | bass_range: None, | |
| 182 | + | bass: None, | |
| 175 | 183 | } | |
| 176 | 184 | } | |
| 177 | 185 | ||
| @@ -1,6 +1,7 @@ | |||
| 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; | |
| 4 | 5 | use iced::widget::{Row, column, container, row, slider, space, text, vertical_slider}; | |
| 5 | 6 | use iced::{Alignment, Border, Element, Length}; | |
| 6 | 7 | ||
| @@ -14,26 +15,39 @@ use crate::{Draft, Item, Marks, Message}; | |||
| 14 | 15 | const SAVING: &str = "Saving…"; | |
| 15 | 16 | ||
| 16 | 17 | /// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them: each preset, | |
| 17 | − | /// then each band of each custom preset. | |
| 18 | + | /// then each band of each custom preset, then the bass level. | |
| 18 | 19 | pub fn items(buds: &Buds) -> Vec<Item> { | |
| 19 | − | let Some(eq) = &buds.eq else { | |
| 20 | − | return Vec::new(); | |
| 21 | − | }; | |
| 22 | − | let mut items: Vec<Item> = eq.presets.iter().map(|p| Item::Preset(p.id)).collect(); | |
| 23 | − | for preset in eq.presets.iter().filter(|p| p.custom) { | |
| 24 | − | items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz))); | |
| 20 | + | let mut items = Vec::new(); | |
| 21 | + | if let Some(eq) = &buds.eq { | |
| 22 | + | items.extend(eq.presets.iter().map(|p| Item::Preset(p.id))); | |
| 23 | + | for preset in eq.presets.iter().filter(|p| p.custom) { | |
| 24 | + | items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz))); | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | if buds.bass_range.is_some() { | |
| 28 | + | items.push(Item::Bass); | |
| 25 | 29 | } | |
| 26 | 30 | items | |
| 27 | 31 | } | |
| 28 | 32 | ||
| 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> { | |
| 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> { | |
| 31 | 42 | 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)); | |
| 32 | 46 | let Some(eq) = &buds.eq else { | |
| 33 | 47 | let none = text("These buds did not list their presets.") | |
| 34 | 48 | .size(14) | |
| 35 | 49 | .color(look.p.dim); | |
| 36 | − | return column![title, none].spacing(12).into(); | |
| 50 | + | return column![title, none].push(bass).spacing(12).into(); | |
| 37 | 51 | }; | |
| 38 | 52 | let current = eq.current.as_deref(); | |
| 39 | 53 | let rows = eq | |
| @@ -70,9 +84,77 @@ pub fn view<'a>(look: Look, buds: &'a Buds, draft: Option<&'a Draft>, marks: Mar | |||
| 70 | 84 | .fold(column![title, ui::card(look, rows)].spacing(18), |page, preset| { | |
| 71 | 85 | page.push(bands(look, preset, draft, marks)) | |
| 72 | 86 | }) | |
| 87 | + | .push(bass) | |
| 73 | 88 | .into() | |
| 74 | 89 | } | |
| 75 | 90 | ||
| 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 | + | ||
| 76 | 158 | /// A custom preset's bands, one slider each. | |
| 77 | 159 | fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> { | |
| 78 | 160 | let p = look.p; | |
| @@ -168,10 +250,11 @@ mod tests { | |||
| 168 | 250 | #[test] | |
| 169 | 251 | fn items_follow_the_page() { | |
| 170 | 252 | let items = items(&buds::demo()); | |
| 171 | − | assert_eq!(items.len(), 5 + 6); | |
| 253 | + | assert_eq!(items.len(), 5 + 6 + 1); | |
| 172 | 254 | assert_eq!(items.first(), Some(&Item::Preset(0))); | |
| 173 | 255 | assert_eq!(items.get(5), Some(&Item::Band(4, 50))); | |
| 174 | − | assert_eq!(items.last(), Some(&Item::Band(4, 16000))); | |
| 256 | + | assert_eq!(items.get(10), Some(&Item::Band(4, 16000))); | |
| 257 | + | assert_eq!(items.last(), Some(&Item::Bass)); | |
| 175 | 258 | assert!(super::items(&buds::unknown()).is_empty()); | |
| 176 | 259 | } | |
| 177 | 260 | ||
| @@ -190,6 +190,8 @@ enum Item { | |||
| 190 | 190 | Cycle(u8), | |
| 191 | 191 | /// Find my earbuds. | |
| 192 | 192 | Find, | |
| 193 | + | /// The bass level. | |
| 194 | + | Bass, | |
| 193 | 195 | } | |
| 194 | 196 | ||
| 195 | 197 | /// What a page needs besides the buds: the item under the keyboard cursor, if it shows, and the | |
| @@ -259,6 +261,13 @@ enum Message { | |||
| 259 | 261 | Find, | |
| 260 | 262 | /// The answer to "Ring the buds?". | |
| 261 | 263 | ConfirmFind(bool), | |
| 264 | + | /// Move the bass level. With `save`, set it on the buds right away, else when | |
| 265 | + | /// [`Message::SaveBass`] comes (the mouse lets go of the slider). | |
| 266 | + | Bass { | |
| 267 | + | level: i8, | |
| 268 | + | save: bool, | |
| 269 | + | }, | |
| 270 | + | SaveBass, | |
| 262 | 271 | ShowHelp, | |
| 263 | 272 | /// Read the buds again. | |
| 264 | 273 | Refresh, | |
| @@ -300,6 +309,8 @@ struct App { | |||
| 300 | 309 | draft: Option<Draft>, | |
| 301 | 310 | /// A gesture's action picked and not written yet. | |
| 302 | 311 | touch_draft: Option<TouchDraft>, | |
| 312 | + | /// A bass level moved and not set yet. | |
| 313 | + | bass_draft: Option<i8>, | |
| 303 | 314 | /// The buds were told to ring and not yet to stop. | |
| 304 | 315 | ringing: bool, | |
| 305 | 316 | /// "Ring the buds?" is open. | |
| @@ -339,6 +350,7 @@ impl App { | |||
| 339 | 350 | read_at: None, | |
| 340 | 351 | draft: None, | |
| 341 | 352 | touch_draft: None, | |
| 353 | + | bass_draft: None, | |
| 342 | 354 | ringing: false, | |
| 343 | 355 | confirm_find: false, | |
| 344 | 356 | palette_problem: None, | |
| @@ -376,6 +388,14 @@ impl App { | |||
| 376 | 388 | } | |
| 377 | 389 | } | |
| 378 | 390 | Message::ShowHelp => self.help = true, | |
| 391 | + | Message::Bass { level, save } => { | |
| 392 | + | let (low, high) = self.buds.bass_range.unwrap_or((i8::MIN, i8::MAX)); | |
| 393 | + | self.bass_draft = Some(level.clamp(low, high)); | |
| 394 | + | if save { | |
| 395 | + | self.save_bass(); | |
| 396 | + | } | |
| 397 | + | } | |
| 398 | + | Message::SaveBass => self.save_bass(), | |
| 379 | 399 | Message::Refresh => self.refresh(), | |
| 380 | 400 | Message::Worker(event) => self.answer(event), | |
| 381 | 401 | Message::Focused => { | |
| @@ -427,6 +447,10 @@ impl App { | |||
| 427 | 447 | } | |
| 428 | 448 | } | |
| 429 | 449 | Command::Find(ring) => self.ringing = ring, | |
| 450 | + | Command::Bass(level) => { | |
| 451 | + | self.buds.bass = Some(level); | |
| 452 | + | self.bass_draft = None; | |
| 453 | + | } | |
| 430 | 454 | Command::CustomEq(_, bands) => { | |
| 431 | 455 | if let Some(preset) = self.buds.preset(item_preset(item)) { | |
| 432 | 456 | let mut preset = preset.clone(); | |
| @@ -535,6 +559,13 @@ impl App { | |||
| 535 | 559 | self.save_touch(row); | |
| 536 | 560 | } | |
| 537 | 561 | } | |
| 562 | + | Event::Bass(level) => { | |
| 563 | + | self.in_flight.pop_front(); | |
| 564 | + | self.buds.bass = Some(level); | |
| 565 | + | self.link = Link::Live; | |
| 566 | + | // A level moved while this one was on its way goes out now. | |
| 567 | + | self.save_bass(); | |
| 568 | + | } | |
| 538 | 569 | Event::Ringing(ring) => { | |
| 539 | 570 | self.in_flight.pop_front(); | |
| 540 | 571 | self.ringing = ring; | |
| @@ -556,6 +587,7 @@ impl App { | |||
| 556 | 587 | Some(Some(Item::Gesture(row))) if self.touch_draft.as_ref().is_some_and(|d| d.row == row) => { | |
| 557 | 588 | self.touch_draft = None; | |
| 558 | 589 | } | |
| 590 | + | Some(Some(Item::Bass)) => self.bass_draft = None, | |
| 559 | 591 | _ => {} | |
| 560 | 592 | } | |
| 561 | 593 | if kind == Kind::NotFound { | |
| @@ -569,6 +601,7 @@ impl App { | |||
| 569 | 601 | fn apply_reading(&mut self, reading: Reading) { | |
| 570 | 602 | self.buds.eq = reading.eq; | |
| 571 | 603 | self.buds.touch = reading.touch; | |
| 604 | + | self.buds.bass_range = reading.bass_range; | |
| 572 | 605 | if let Some(level) = reading | |
| 573 | 606 | .status | |
| 574 | 607 | .noise_control | |
| @@ -701,6 +734,19 @@ impl App { | |||
| 701 | 734 | } | |
| 702 | 735 | } | |
| 703 | 736 | ||
| 737 | + | /// Sets the bass level moved on the screen. If a level is on its way, this one goes out | |
| 738 | + | /// when that is answered: the latest level wins. | |
| 739 | + | fn save_bass(&mut self) { | |
| 740 | + | let Some(level) = self.bass_draft else { | |
| 741 | + | return; | |
| 742 | + | }; | |
| 743 | + | if self.buds.bass == Some(level) { | |
| 744 | + | self.bass_draft = None; | |
| 745 | + | } else if !self.pending().contains(&Item::Bass) { | |
| 746 | + | self.write(Item::Bass, Command::Bass(level)); | |
| 747 | + | } | |
| 748 | + | } | |
| 749 | + | ||
| 704 | 750 | /// The items with a write on its way. | |
| 705 | 751 | fn pending(&self) -> Vec<Item> { | |
| 706 | 752 | self.in_flight.iter().flatten().copied().collect() | |
| @@ -870,6 +916,18 @@ impl App { | |||
| 870 | 916 | }) | |
| 871 | 917 | } | |
| 872 | 918 | Item::Find => (action != Action::Left).then_some(Message::Find), | |
| 919 | + | Item::Bass => { | |
| 920 | + | let (low, high) = self.buds.bass_range?; | |
| 921 | + | let now = self.bass_draft.or(self.buds.bass); | |
| 922 | + | let to = match (action, now) { | |
| 923 | + | // Not set yet: the first step starts from the middle. | |
| 924 | + | (Action::Left | Action::Right, None) => 0, | |
| 925 | + | (Action::Left, Some(level)) => level.checked_sub(1)?.max(low), | |
| 926 | + | (Action::Right, Some(level)) => level.checked_add(1)?.min(high), | |
| 927 | + | _ => return None, | |
| 928 | + | }; | |
| 929 | + | (Some(to) != now).then_some(Message::Bass { level: to, save: true }) | |
| 930 | + | } | |
| 873 | 931 | Item::Cycle(mode) => { | |
| 874 | 932 | let touch = self.buds.touch.as_ref()?; | |
| 875 | 933 | let on = touch | |
| @@ -951,7 +1009,7 @@ impl App { | |||
| 951 | 1009 | focus: self.focus(), | |
| 952 | 1010 | pending: &pending, | |
| 953 | 1011 | }; | |
| 954 | − | equalizer::view(look, &self.buds, self.draft.as_ref(), marks) | |
| 1012 | + | equalizer::view(look, &self.buds, self.draft.as_ref(), self.bass_draft, marks) | |
| 955 | 1013 | } | |
| 956 | 1014 | Page::Gestures if self.has_reading() => { | |
| 957 | 1015 | let marks = Marks { | |
| @@ -1230,6 +1288,7 @@ mod tests { | |||
| 1230 | 1288 | status: buds::demo().status, | |
| 1231 | 1289 | eq: buds::demo().eq, | |
| 1232 | 1290 | touch: buds::demo().touch, | |
| 1291 | + | bass_range: buds::demo().bass_range, | |
| 1233 | 1292 | } | |
| 1234 | 1293 | } | |
| 1235 | 1294 | ||
| @@ -1514,7 +1573,7 @@ mod tests { | |||
| 1514 | 1573 | press(&mut app, "j l"); | |
| 1515 | 1574 | assert_eq!(app.buds.status.eq.as_deref(), Some("pure-raw")); | |
| 1516 | 1575 | assert_eq!(app.buds.eq_name(), "Pure raw"); | |
| 1517 | − | press(&mut app, "G"); | |
| 1576 | + | press(&mut app, "G k"); | |
| 1518 | 1577 | assert_eq!(app.focus(), Some(Item::Band(4, 16000))); | |
| 1519 | 1578 | // Moving a band saves the custom preset, which also selects it. | |
| 1520 | 1579 | press(&mut app, "l"); | |
| @@ -1530,7 +1589,7 @@ mod tests { | |||
| 1530 | 1589 | let (mut app, commands) = live(); | |
| 1531 | 1590 | let _ = commands.try_recv(); | |
| 1532 | 1591 | let _ = app.update(Message::Worker(Event::Read(Box::new(reading())))); | |
| 1533 | − | press(&mut app, "J G G l"); | |
| 1592 | + | press(&mut app, "J G G k l"); | |
| 1534 | 1593 | let gain = |bands: &[EqBand]| bands.last().map(|band| band.gain); | |
| 1535 | 1594 | assert!(matches!( | |
| 1536 | 1595 | commands.try_recv(), | |
| @@ -1648,6 +1707,42 @@ mod tests { | |||
| 1648 | 1707 | assert!(matches!(commands.try_recv(), Ok(Command::Find(false)))); | |
| 1649 | 1708 | } | |
| 1650 | 1709 | ||
| 1710 | + | #[test] | |
| 1711 | + | fn the_bass_level_by_key() { | |
| 1712 | + | let mut app = app(); | |
| 1713 | + | press(&mut app, "J G G"); | |
| 1714 | + | assert_eq!(app.focus(), Some(Item::Bass)); | |
| 1715 | + | assert_eq!(app.buds.bass, None, "the buds do not say"); | |
| 1716 | + | press(&mut app, "l"); | |
| 1717 | + | assert_eq!(app.buds.bass, Some(0), "the first step starts from the middle"); | |
| 1718 | + | press(&mut app, "l l"); | |
| 1719 | + | assert_eq!(app.buds.bass, Some(2)); | |
| 1720 | + | press(&mut app, "h h h h h h h h"); | |
| 1721 | + | assert_eq!(app.buds.bass, Some(-5), "it stops at the bottom"); | |
| 1722 | + | } | |
| 1723 | + | ||
| 1724 | + | #[test] | |
| 1725 | + | fn bass_moves_wait_for_the_level_before_them() { | |
| 1726 | + | let (mut app, commands) = live(); | |
| 1727 | + | let _ = commands.try_recv(); | |
| 1728 | + | let _ = app.update(Message::Worker(Event::Read(Box::new(reading())))); | |
| 1729 | + | let _ = app.update(Message::Bass { level: 2, save: false }); | |
| 1730 | + | assert!( | |
| 1731 | + | commands.try_recv().is_err(), | |
| 1732 | + | "a drag sends nothing until the mouse lets go" | |
| 1733 | + | ); | |
| 1734 | + | let _ = app.update(Message::SaveBass); | |
| 1735 | + | assert!(matches!(commands.try_recv(), Ok(Command::Bass(2)))); | |
| 1736 | + | let _ = app.update(Message::Bass { level: 4, save: true }); | |
| 1737 | + | assert!(commands.try_recv().is_err()); | |
| 1738 | + | let _ = app.update(Message::Worker(Event::Bass(2))); | |
| 1739 | + | assert!(matches!(commands.try_recv(), Ok(Command::Bass(4)))); | |
| 1740 | + | let _ = app.update(Message::Worker(Event::Bass(4))); | |
| 1741 | + | assert_eq!(app.buds.bass, Some(4)); | |
| 1742 | + | assert_eq!(app.bass_draft, None); | |
| 1743 | + | assert!(commands.try_recv().is_err()); | |
| 1744 | + | } | |
| 1745 | + | ||
| 1651 | 1746 | #[test] | |
| 1652 | 1747 | fn steps() { | |
| 1653 | 1748 | let abc = ['a', 'b', 'c']; | |
| @@ -1699,9 +1794,10 @@ mod tests { | |||
| 1699 | 1794 | shots.push(("overview-full".to_owned(), 1240.0, on(Page::Overview, ""))); | |
| 1700 | 1795 | shots.push(("overview-cursor".to_owned(), 800.0, on(Page::Overview, "j j j j"))); | |
| 1701 | 1796 | shots.push(("keys".to_owned(), 800.0, on(Page::Overview, "?"))); | |
| 1702 | − | shots.push(("equalizer-cursor".to_owned(), 1000.0, on(Page::Equalizer, "G G k k"))); | |
| 1797 | + | shots.push(("equalizer-cursor".to_owned(), 1000.0, on(Page::Equalizer, "G G k k k"))); | |
| 1703 | 1798 | shots.push(("gestures-cursor".to_owned(), 1000.0, on(Page::Gestures, "j j"))); | |
| 1704 | 1799 | shots.push(("find-confirm".to_owned(), 800.0, on(Page::Earbuds, "j Enter"))); | |
| 1800 | + | shots.push(("equalizer-bass".to_owned(), 1300.0, on(Page::Equalizer, "G G l l"))); | |
| 1705 | 1801 | let mut connecting = App::new(palette.clone(), font, None, false); | |
| 1706 | 1802 | connecting.link = Link::Connecting; | |
| 1707 | 1803 | shots.push(("connecting".to_owned(), 800.0, connecting)); | |
| @@ -46,6 +46,8 @@ pub enum Command { | |||
| 46 | 46 | HoldCycle(Vec<String>), | |
| 47 | 47 | /// Start (`true`) or stop ringing the buds. | |
| 48 | 48 | Find(bool), | |
| 49 | + | /// Set the bass level. | |
| 50 | + | Bass(i8), | |
| 49 | 51 | } | |
| 50 | 52 | ||
| 51 | 53 | #[derive(Debug, Clone)] | |
| @@ -70,6 +72,8 @@ pub enum Event { | |||
| 70 | 72 | HoldCycle(Vec<String>), | |
| 71 | 73 | /// The answer to [`Command::Find`]: whether the buds were told to ring. | |
| 72 | 74 | Ringing(bool), | |
| 75 | + | /// The answer to [`Command::Bass`]: the level the buds took. | |
| 76 | + | Bass(i8), | |
| 73 | 77 | /// The answer to a command that failed. The session is closed; the next command opens a | |
| 74 | 78 | /// new one. | |
| 75 | 79 | Failed { kind: Kind, message: String }, | |
| @@ -84,6 +88,8 @@ pub struct Reading { | |||
| 84 | 88 | pub eq: Option<EqPresets>, | |
| 85 | 89 | /// The touch controls, when the buds report them. | |
| 86 | 90 | pub touch: Option<TouchControls>, | |
| 91 | + | /// The levels the bass level takes, when the buds have one to set. | |
| 92 | + | pub bass_range: Option<(i8, i8)>, | |
| 87 | 93 | } | |
| 88 | 94 | ||
| 89 | 95 | /// The worker, as a stream of events for an iced subscription. `device` picks the buds, as | |
| @@ -207,6 +213,10 @@ impl Worker { | |||
| 207 | 213 | .session() | |
| 208 | 214 | .and_then(|session| session.set_touch(&side, &gesture, &action, false)) | |
| 209 | 215 | .map(|change| Event::Touch(change.gestures)), | |
| 216 | + | Command::Bass(level) => self | |
| 217 | + | .session() | |
| 218 | + | .and_then(|session| session.set_bass_level(level)) | |
| 219 | + | .map(|()| Event::Bass(level)), | |
| 210 | 220 | Command::Find(ring) => self | |
| 211 | 221 | .session() | |
| 212 | 222 | .and_then(|session| session.find(ring)) | |
| @@ -235,7 +245,13 @@ impl Worker { | |||
| 235 | 245 | let status = session.status()?; | |
| 236 | 246 | let eq = session.eq_presets().ok(); | |
| 237 | 247 | let touch = session.touch_controls().ok(); | |
| 238 | − | Ok(Reading { status, eq, touch }) | |
| 248 | + | let bass_range = session.bass_range(); | |
| 249 | + | Ok(Reading { | |
| 250 | + | status, | |
| 251 | + | eq, | |
| 252 | + | touch, | |
| 253 | + | bass_range, | |
| 254 | + | }) | |
| 239 | 255 | } | |
| 240 | 256 | ||
| 241 | 257 | /// The open session, opening one first if there is none. | |