budctl4276e2b
budctl-gui: a pencil and a bin on each custom preset's row
Opening a preset's bands no longer needs it selected: the pencil on its row opens them below the list (Rename and Done on that card), and the bin deletes it after a yes. A click on the row still selects it, as for built-in presets. Since the buds save bands only with the command that also selects the preset (W11b), the open card says that moving a band puts the preset in use. A new preset opens its bands at once. The cursor reaches the pencil and the bin right after their row. No new dependencies.
+180 −55
4 files changed
- M crates/budctl-gui/src/equalizer.rs +89 −43
- M crates/budctl-gui/src/icons.rs +13 −0
- M crates/budctl-gui/src/main.rs +59 −12
- M crates/budctl-gui/src/ui.rs +19 −0
| @@ -14,34 +14,52 @@ use crate::{Draft, Item, Marks, Message}; | |||
| 14 | 14 | const SAVING: &str = "Saving…"; | |
| 15 | 15 | ||
| 16 | 16 | /// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them: each preset, | |
| 17 | − | /// "New preset", then, when a custom preset is in use, "Rename", "Delete" and its bands. | |
| 18 | − | pub fn items(buds: &Buds) -> Vec<Item> { | |
| 17 | + | /// with a custom one's pencil and bin; "New preset"; then, while a preset's bands are open, | |
| 18 | + | /// "Rename", "Done" and its bands. | |
| 19 | + | pub fn items(buds: &Buds, editing: Option<u8>) -> Vec<Item> { | |
| 19 | 20 | let mut items = Vec::new(); | |
| 20 | − | if let Some(eq) = &buds.eq { | |
| 21 | − | items.extend(eq.presets.iter().map(|p| Item::Preset(p.id))); | |
| 22 | − | items.push(Item::NewPreset); | |
| 23 | − | if let Some(preset) = in_use(buds) { | |
| 24 | − | items.extend([Item::Rename(preset.id), Item::Delete(preset.id)]); | |
| 25 | − | items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz))); | |
| 21 | + | let Some(eq) = &buds.eq else { | |
| 22 | + | return items; | |
| 23 | + | }; | |
| 24 | + | for preset in &eq.presets { | |
| 25 | + | items.push(Item::Preset(preset.id)); | |
| 26 | + | if preset.custom { | |
| 27 | + | items.extend([Item::Edit(preset.id), Item::Delete(preset.id)]); | |
| 26 | 28 | } | |
| 27 | 29 | } | |
| 30 | + | items.push(Item::NewPreset); | |
| 31 | + | if let Some(preset) = edited(buds, editing) { | |
| 32 | + | items.extend([Item::Rename(preset.id), Item::Done]); | |
| 33 | + | items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz))); | |
| 34 | + | } | |
| 28 | 35 | items | |
| 29 | 36 | } | |
| 30 | 37 | ||
| 31 | − | /// The custom preset in use, if one is: only its bands can be changed. | |
| 38 | + | /// The custom preset in use, if one is. | |
| 32 | 39 | pub fn in_use(buds: &Buds) -> Option<&EqPreset> { | |
| 33 | 40 | let eq = buds.eq.as_ref()?; | |
| 34 | 41 | let current = eq.current.as_deref()?; | |
| 35 | 42 | eq.presets.iter().find(|p| p.custom && buds::key(p) == current) | |
| 36 | 43 | } | |
| 37 | 44 | ||
| 38 | − | /// `draft`: bands moved on the screen and not saved on the buds yet. | |
| 39 | − | pub fn view<'a>(look: Look, buds: &'a Buds, draft: Option<&'a Draft>, marks: Marks) -> Element<'a, Message> { | |
| 45 | + | /// The custom preset whose bands are open, if any. | |
| 46 | + | pub fn edited(buds: &Buds, editing: Option<u8>) -> Option<&EqPreset> { | |
| 47 | + | buds.preset(editing?).filter(|preset| preset.custom) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | /// `editing`: the custom preset whose bands are open; `draft`: bands moved on the screen and not | |
| 51 | + | /// saved on the buds yet. | |
| 52 | + | pub fn view<'a>( | |
| 53 | + | look: Look, | |
| 54 | + | buds: &'a Buds, | |
| 55 | + | editing: Option<u8>, | |
| 56 | + | draft: Option<&'a Draft>, | |
| 57 | + | marks: Marks, | |
| 58 | + | ) -> Element<'a, Message> { | |
| 59 | + | let p = look.p; | |
| 40 | 60 | let title = ui::title(look, "Equalizer"); | |
| 41 | 61 | let Some(eq) = &buds.eq else { | |
| 42 | − | let none = text("These buds did not list their presets.") | |
| 43 | − | .size(14) | |
| 44 | − | .color(look.p.dim); | |
| 62 | + | let none = text("These buds did not list their presets.").size(14).color(p.dim); | |
| 45 | 63 | return column![title, none].spacing(12).into(); | |
| 46 | 64 | }; | |
| 47 | 65 | let current = eq.current.as_deref(); | |
| @@ -49,38 +67,61 @@ pub fn view<'a>(look: Look, buds: &'a Buds, draft: Option<&'a Draft>, marks: Mar | |||
| 49 | 67 | .presets | |
| 50 | 68 | .iter() | |
| 51 | 69 | .map(|preset| { | |
| 70 | + | let id = preset.id; | |
| 52 | 71 | let chosen = current == Some(buds::key(preset).as_str()); | |
| 53 | − | let subtitle = if marks.pending.contains(&Item::Preset(preset.id)) { | |
| 72 | + | let subtitle = if marks.pending.contains(&Item::Preset(id)) { | |
| 54 | 73 | SAVING | |
| 74 | + | } else if preset.custom && editing == Some(id) { | |
| 75 | + | "Your preset, open below" | |
| 55 | 76 | } else if preset.custom { | |
| 56 | 77 | "Your preset" | |
| 57 | 78 | } else { | |
| 58 | 79 | "Built in" | |
| 59 | 80 | }; | |
| 60 | 81 | let mark: Element<'a, Message> = if chosen { | |
| 61 | − | icons::icon(Icon::Check, 20.0, look.p.icon) | |
| 82 | + | icons::icon(Icon::Check, 20.0, p.icon) | |
| 62 | 83 | } else { | |
| 63 | 84 | space().width(20).into() | |
| 64 | 85 | }; | |
| 86 | + | let trailing: Element<'a, Message> = if preset.custom { | |
| 87 | + | row![ | |
| 88 | + | mark, | |
| 89 | + | ui::icon_button( | |
| 90 | + | look, | |
| 91 | + | Icon::Pencil, | |
| 92 | + | marks.focus == Some(Item::Edit(id)), | |
| 93 | + | Message::EditPreset(id) | |
| 94 | + | ), | |
| 95 | + | ui::icon_button( | |
| 96 | + | look, | |
| 97 | + | Icon::Trash, | |
| 98 | + | marks.focus == Some(Item::Delete(id)), | |
| 99 | + | Message::DeletePreset(id) | |
| 100 | + | ), | |
| 101 | + | ] | |
| 102 | + | .spacing(6) | |
| 103 | + | .align_y(Alignment::Center) | |
| 104 | + | .into() | |
| 105 | + | } else { | |
| 106 | + | mark | |
| 107 | + | }; | |
| 65 | 108 | ui::setting( | |
| 66 | 109 | look, | |
| 67 | 110 | Icon::Equalizer, | |
| 68 | 111 | buds::shown_name(preset), | |
| 69 | 112 | Some(subtitle.to_owned()), | |
| 70 | − | mark, | |
| 71 | − | Some(Message::Preset(preset.id)), | |
| 72 | − | marks.focus == Some(Item::Preset(preset.id)), | |
| 113 | + | trailing, | |
| 114 | + | Some(Message::Preset(id)), | |
| 115 | + | marks.focus == Some(Item::Preset(id)), | |
| 73 | 116 | ) | |
| 74 | 117 | }) | |
| 75 | 118 | .collect(); | |
| 76 | − | let editor = match in_use(buds) { | |
| 119 | + | let editor: Element<'a, Message> = match edited(buds, editing) { | |
| 77 | 120 | Some(preset) => bands(look, preset, draft, marks), | |
| 78 | − | None => { | |
| 79 | − | let note = text("Built-in presets cannot be changed. Pick one of your presets to change its bands.") | |
| 80 | − | .size(14) | |
| 81 | − | .color(look.p.dim); | |
| 82 | − | ui::panel(look, note.into()) | |
| 83 | − | } | |
| 121 | + | None => text("Use the pencil on one of your presets to change its bands.") | |
| 122 | + | .size(13) | |
| 123 | + | .color(p.dim) | |
| 124 | + | .into(), | |
| 84 | 125 | }; | |
| 85 | 126 | column![title, ui::card(look, rows), new_preset(look, marks), editor] | |
| 86 | 127 | .spacing(18) | |
| @@ -142,13 +183,16 @@ fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: | |||
| 142 | 183 | )) | |
| 143 | 184 | .push(ui::pill_with( | |
| 144 | 185 | look, | |
| 145 | − | "Delete", | |
| 186 | + | "Done", | |
| 146 | 187 | false, | |
| 147 | − | marks.focus == Some(Item::Delete(id)), | |
| 148 | − | Message::DeletePreset(id), | |
| 188 | + | marks.focus == Some(Item::Done), | |
| 189 | + | Message::DoneEditing, | |
| 149 | 190 | )) | |
| 150 | 191 | .spacing(10) | |
| 151 | 192 | .align_y(Alignment::Center); | |
| 193 | + | let note = text("Moving a band saves this preset and puts it in use, so you hear it.") | |
| 194 | + | .size(13) | |
| 195 | + | .color(p.dim); | |
| 152 | 196 | let sliders = gains.iter().fold(Row::new().spacing(6), |sliders, &band| { | |
| 153 | 197 | let focused = marks.focus == Some(Item::Band(id, band.hz)); | |
| 154 | 198 | let hz = band.hz; | |
| @@ -197,7 +241,7 @@ fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks: | |||
| 197 | 241 | }), | |
| 198 | 242 | ) | |
| 199 | 243 | }); | |
| 200 | − | ui::panel(look, column![heading, sliders].spacing(16).into()) | |
| 244 | + | ui::panel(look, column![heading, note, sliders].spacing(14).into()) | |
| 201 | 245 | } | |
| 202 | 246 | ||
| 203 | 247 | /// `3` → "+3", `0` → "0", `-2` → "-2". | |
| @@ -220,19 +264,21 @@ mod tests { | |||
| 220 | 264 | ||
| 221 | 265 | #[test] | |
| 222 | 266 | fn items_follow_the_page() { | |
| 223 | − | let items = items(&buds::demo()); | |
| 224 | − | assert_eq!(items.len(), 5 + 1 + 2 + 6); | |
| 225 | − | assert_eq!(items.first(), Some(&Item::Preset(0))); | |
| 226 | − | assert_eq!(items.get(5), Some(&Item::NewPreset)); | |
| 227 | − | assert_eq!(items.get(6), Some(&Item::Rename(4))); | |
| 228 | − | assert_eq!(items.get(7), Some(&Item::Delete(4))); | |
| 229 | − | assert_eq!(items.get(8), Some(&Item::Band(4, 50))); | |
| 230 | − | assert_eq!(items.last(), Some(&Item::Band(4, 16000))); | |
| 231 | − | // With a built-in preset in use, there is nothing to edit. | |
| 232 | − | let mut buds = buds::demo(); | |
| 233 | − | buds.set_eq("pure-bass".to_owned()); | |
| 234 | − | assert_eq!(super::items(&buds).last(), Some(&Item::NewPreset)); | |
| 235 | − | assert!(super::items(&buds::unknown()).is_empty()); | |
| 267 | + | let buds = buds::demo(); | |
| 268 | + | let closed = items(&buds, None); | |
| 269 | + | assert_eq!(closed.len(), 4 + 3 + 1); | |
| 270 | + | assert_eq!(closed.first(), Some(&Item::Preset(0))); | |
| 271 | + | assert_eq!( | |
| 272 | + | closed.get(4..), | |
| 273 | + | Some(&[Item::Preset(4), Item::Edit(4), Item::Delete(4), Item::NewPreset][..]) | |
| 274 | + | ); | |
| 275 | + | let open = items(&buds, Some(4)); | |
| 276 | + | assert_eq!(open.len(), 8 + 2 + 6); | |
| 277 | + | assert_eq!(open.get(8..10), Some(&[Item::Rename(4), Item::Done][..])); | |
| 278 | + | assert_eq!(open.last(), Some(&Item::Band(4, 16000))); | |
| 279 | + | // A built-in preset has no bands to open. | |
| 280 | + | assert_eq!(items(&buds, Some(0)).len(), 8); | |
| 281 | + | assert!(items(&buds::unknown(), None).is_empty()); | |
| 236 | 282 | } | |
| 237 | 283 | ||
| 238 | 284 | #[test] | |
| @@ -28,6 +28,8 @@ pub enum Icon { | |||
| 28 | 28 | Info, | |
| 29 | 29 | Chip, | |
| 30 | 30 | Keys, | |
| 31 | + | Pencil, | |
| 32 | + | Trash, | |
| 31 | 33 | } | |
| 32 | 34 | ||
| 33 | 35 | /// `icon` at `size` pixels, in `color`. | |
| @@ -256,6 +258,17 @@ fn draw_icon(pen: &mut Pen, icon: Icon) { | |||
| 256 | 258 | pen.line(from, to); | |
| 257 | 259 | } | |
| 258 | 260 | } | |
| 261 | + | Icon::Pencil => { | |
| 262 | + | pen.polyline(&[(4.5, 19.5), (5.5, 15.0), (15.5, 5.0), (19.0, 8.5), (9.0, 18.5)], true); | |
| 263 | + | pen.line((13.0, 7.5), (16.5, 11.0)); | |
| 264 | + | } | |
| 265 | + | Icon::Trash => { | |
| 266 | + | pen.line((4.0, 6.5), (20.0, 6.5)); | |
| 267 | + | pen.polyline(&[(9.0, 6.5), (9.5, 3.5), (14.5, 3.5), (15.0, 6.5)], false); | |
| 268 | + | pen.polyline(&[(6.0, 6.5), (7.0, 20.5), (17.0, 20.5), (18.0, 6.5)], false); | |
| 269 | + | pen.line((10.0, 10.5), (10.0, 16.5)); | |
| 270 | + | pen.line((14.0, 10.5), (14.0, 16.5)); | |
| 271 | + | } | |
| 259 | 272 | Icon::Info => { | |
| 260 | 273 | pen.circle(12.0, 12.0, 9.0); | |
| 261 | 274 | pen.line((12.0, 11.0), (12.0, 16.5)); | |
| @@ -198,8 +198,12 @@ enum Item { | |||
| 198 | 198 | NewPreset, | |
| 199 | 199 | /// "Rename", on a custom EQ preset's card. | |
| 200 | 200 | Rename(u8), | |
| 201 | − | /// "Delete", on a custom EQ preset's card. | |
| 201 | + | /// The bin on a custom EQ preset's row. | |
| 202 | 202 | Delete(u8), | |
| 203 | + | /// The pencil on a custom EQ preset's row: open its bands. | |
| 204 | + | Edit(u8), | |
| 205 | + | /// "Done", on the open bands. | |
| 206 | + | Done, | |
| 203 | 207 | } | |
| 204 | 208 | ||
| 205 | 209 | /// The dialog that asks for a preset's name. | |
| @@ -299,6 +303,10 @@ enum Message { | |||
| 299 | 303 | NameInput(String), | |
| 300 | 304 | SubmitName, | |
| 301 | 305 | CancelName, | |
| 306 | + | /// Open a custom preset's (by its id) bands, or close them if they are open. | |
| 307 | + | EditPreset(u8), | |
| 308 | + | /// Close the open bands. | |
| 309 | + | DoneEditing, | |
| 302 | 310 | /// Delete a custom preset (by its id), after a confirmation. | |
| 303 | 311 | DeletePreset(u8), | |
| 304 | 312 | /// The answer to "Delete the preset?". | |
| @@ -356,6 +364,8 @@ struct App { | |||
| 356 | 364 | prompt: Option<NamePrompt>, | |
| 357 | 365 | /// "Delete the preset?" is open, for this preset. | |
| 358 | 366 | confirm_delete: Option<u8>, | |
| 367 | + | /// The custom preset whose bands are open on the Equalizer page. | |
| 368 | + | editing: Option<u8>, | |
| 359 | 369 | } | |
| 360 | 370 | ||
| 361 | 371 | impl App { | |
| @@ -395,6 +405,7 @@ impl App { | |||
| 395 | 405 | palette_problem: None, | |
| 396 | 406 | prompt: None, | |
| 397 | 407 | confirm_delete: None, | |
| 408 | + | editing: None, | |
| 398 | 409 | }; | |
| 399 | 410 | app.refresh_palette(); | |
| 400 | 411 | app | |
| @@ -450,12 +461,19 @@ impl App { | |||
| 450 | 461 | } | |
| 451 | 462 | Message::SubmitName => self.submit_name(), | |
| 452 | 463 | Message::CancelName => self.prompt = None, | |
| 464 | + | Message::EditPreset(id) => { | |
| 465 | + | self.editing = if self.editing == Some(id) { None } else { Some(id) }; | |
| 466 | + | } | |
| 467 | + | Message::DoneEditing => self.editing = None, | |
| 453 | 468 | Message::DeletePreset(id) => self.confirm_delete = Some(id), | |
| 454 | 469 | Message::ConfirmDelete(yes) => { | |
| 455 | 470 | if let (true, Some(id)) = (yes, self.confirm_delete.take()) | |
| 456 | 471 | && let Some(preset) = self.buds.preset(id) | |
| 457 | 472 | { | |
| 458 | 473 | let command = Command::DeleteEq(buds::key(preset)); | |
| 474 | + | if self.editing == Some(id) { | |
| 475 | + | self.editing = None; | |
| 476 | + | } | |
| 459 | 477 | self.write(Item::Preset(id), command); | |
| 460 | 478 | } | |
| 461 | 479 | self.confirm_delete = None; | |
| @@ -530,6 +548,7 @@ impl App { | |||
| 530 | 548 | let model = customs().next().cloned(); | |
| 531 | 549 | let id = customs().map(|p| p.id).max().map_or(4, |id| id.saturating_add(1)); | |
| 532 | 550 | if let Some(model) = model { | |
| 551 | + | self.editing = Some(id); | |
| 533 | 552 | self.buds.set_custom(EqPreset { | |
| 534 | 553 | name, | |
| 535 | 554 | id, | |
| @@ -664,9 +683,13 @@ impl App { | |||
| 664 | 683 | } | |
| 665 | 684 | } | |
| 666 | 685 | Event::Presets(eq) => { | |
| 667 | − | self.in_flight.pop_front(); | |
| 686 | + | let done = self.in_flight.pop_front(); | |
| 668 | 687 | self.buds.set_presets(eq); | |
| 669 | 688 | self.link = Link::Live; | |
| 689 | + | // A new preset opens its bands: it is the one in use. | |
| 690 | + | if done == Some(Some(Item::NewPreset)) { | |
| 691 | + | self.editing = equalizer::in_use(&self.buds).map(|preset| preset.id); | |
| 692 | + | } | |
| 670 | 693 | } | |
| 671 | 694 | Event::Bass(level) => { | |
| 672 | 695 | self.in_flight.pop_front(); | |
| @@ -981,7 +1004,7 @@ impl App { | |||
| 981 | 1004 | fn items(&self) -> Vec<Item> { | |
| 982 | 1005 | match self.page { | |
| 983 | 1006 | Page::Overview if self.has_reading() => overview::items(&self.buds), | |
| 984 | − | Page::Equalizer if self.has_reading() => equalizer::items(&self.buds), | |
| 1007 | + | Page::Equalizer if self.has_reading() => equalizer::items(&self.buds, self.editing), | |
| 985 | 1008 | Page::Gestures if self.has_reading() => gestures::items(&self.buds), | |
| 986 | 1009 | Page::Earbuds if self.has_reading() => earbuds::items(), | |
| 987 | 1010 | _ => Vec::new(), | |
| @@ -1093,6 +1116,8 @@ impl App { | |||
| 1093 | 1116 | Item::NewPreset => (action != Action::Left).then_some(Message::NewPreset), | |
| 1094 | 1117 | Item::Rename(id) => (action != Action::Left).then_some(Message::RenamePreset(id)), | |
| 1095 | 1118 | Item::Delete(id) => (action != Action::Left).then_some(Message::DeletePreset(id)), | |
| 1119 | + | Item::Edit(id) => (action != Action::Left).then_some(Message::EditPreset(id)), | |
| 1120 | + | Item::Done => (action != Action::Left).then_some(Message::DoneEditing), | |
| 1096 | 1121 | Item::Bass => { | |
| 1097 | 1122 | let (low, high) = self.buds.bass_range?; | |
| 1098 | 1123 | let now = self.bass_draft.or(self.buds.bass); | |
| @@ -1186,7 +1211,7 @@ impl App { | |||
| 1186 | 1211 | focus: self.focus(), | |
| 1187 | 1212 | pending: &pending, | |
| 1188 | 1213 | }; | |
| 1189 | − | equalizer::view(look, &self.buds, self.draft.as_ref(), marks) | |
| 1214 | + | equalizer::view(look, &self.buds, self.editing, self.draft.as_ref(), marks) | |
| 1190 | 1215 | } | |
| 1191 | 1216 | Page::Gestures if self.has_reading() => { | |
| 1192 | 1217 | let marks = Marks { | |
| @@ -1855,12 +1880,19 @@ mod tests { | |||
| 1855 | 1880 | press(&mut app, "j l"); | |
| 1856 | 1881 | assert_eq!(app.buds.status.eq.as_deref(), Some("pure-raw")); | |
| 1857 | 1882 | assert_eq!(app.buds.eq_name(), "Pure raw"); | |
| 1858 | − | // Back to Custom1, the fifth preset: its bands come back. | |
| 1859 | − | press(&mut app, "j j j j l"); | |
| 1860 | − | assert_eq!(app.buds.status.eq.as_deref(), Some("custom-4")); | |
| 1883 | + | // Custom1's pencil, after the four built-in presets and Custom1's row, opens its bands. | |
| 1884 | + | press(&mut app, "j j j j j Enter"); | |
| 1885 | + | assert_eq!(app.editing, Some(4)); | |
| 1886 | + | assert_eq!( | |
| 1887 | + | app.buds.status.eq.as_deref(), | |
| 1888 | + | Some("pure-raw"), | |
| 1889 | + | "opening selects nothing" | |
| 1890 | + | ); | |
| 1861 | 1891 | press(&mut app, "G"); | |
| 1862 | 1892 | assert_eq!(app.focus(), Some(Item::Band(4, 16000))); | |
| 1893 | + | // Moving a band saves the preset, which puts it in use. | |
| 1863 | 1894 | press(&mut app, "l"); | |
| 1895 | + | assert_eq!(app.buds.status.eq.as_deref(), Some("custom-4")); | |
| 1864 | 1896 | let last = |app: &App| app.buds.preset(4).unwrap().bands.last().unwrap().gain; | |
| 1865 | 1897 | assert_eq!(last(&app), 3); | |
| 1866 | 1898 | press(&mut app, "l l l l l"); | |
| @@ -1872,6 +1904,7 @@ mod tests { | |||
| 1872 | 1904 | let (mut app, commands) = live(); | |
| 1873 | 1905 | let _ = commands.try_recv(); | |
| 1874 | 1906 | let _ = app.update(Message::Worker(Event::Read(Box::new(reading())))); | |
| 1907 | + | let _ = app.update(Message::EditPreset(4)); | |
| 1875 | 1908 | press(&mut app, "J G G l"); | |
| 1876 | 1909 | let gain = |bands: &[EqBand]| bands.last().map(|band| band.gain); | |
| 1877 | 1910 | assert!(matches!( | |
| @@ -2050,6 +2083,7 @@ mod tests { | |||
| 2050 | 2083 | .find(|(_, name)| *name == "Gym") | |
| 2051 | 2084 | .map(|(id, _)| id) | |
| 2052 | 2085 | .unwrap(); | |
| 2086 | + | assert_eq!(app.editing, Some(gym), "and its bands open"); | |
| 2053 | 2087 | ||
| 2054 | 2088 | let _ = app.update(Message::RenamePreset(gym)); | |
| 2055 | 2089 | assert_eq!(app.prompt.as_ref().map(|p| p.text.as_str()), Some("Gym")); | |
| @@ -2080,6 +2114,7 @@ mod tests { | |||
| 2080 | 2114 | presets.current = Some("custom-5".to_owned()); | |
| 2081 | 2115 | let _ = app.update(Message::Worker(Event::Presets(presets))); | |
| 2082 | 2116 | assert_eq!(app.buds.eq_name(), "Gym"); | |
| 2117 | + | assert_eq!(app.editing, Some(5)); | |
| 2083 | 2118 | assert!(app.pending().is_empty()); | |
| 2084 | 2119 | ||
| 2085 | 2120 | let _ = app.update(Message::RenamePreset(4)); | |
| @@ -2114,11 +2149,19 @@ mod tests { | |||
| 2114 | 2149 | } | |
| 2115 | 2150 | ||
| 2116 | 2151 | #[test] | |
| 2117 | − | fn only_the_preset_in_use_shows_its_bands() { | |
| 2152 | + | fn the_pencil_opens_the_bands_and_done_closes_them() { | |
| 2118 | 2153 | let mut app = app(); | |
| 2119 | − | press(&mut app, "J j l"); | |
| 2120 | − | assert_eq!(app.buds.status.eq.as_deref(), Some("pure-raw")); | |
| 2121 | − | assert!(!app.items().iter().any(|item| matches!(item, Item::Band(..)))); | |
| 2154 | + | press(&mut app, "J"); | |
| 2155 | + | let bands = |app: &App| app.items().iter().any(|item| matches!(item, Item::Band(..))); | |
| 2156 | + | assert!(!bands(&app)); | |
| 2157 | + | let _ = app.update(Message::EditPreset(4)); | |
| 2158 | + | assert!(bands(&app)); | |
| 2159 | + | let _ = app.update(Message::DoneEditing); | |
| 2160 | + | assert!(!bands(&app)); | |
| 2161 | + | // The pencil again closes them too. | |
| 2162 | + | let _ = app.update(Message::EditPreset(4)); | |
| 2163 | + | let _ = app.update(Message::EditPreset(4)); | |
| 2164 | + | assert!(!bands(&app)); | |
| 2122 | 2165 | } | |
| 2123 | 2166 | ||
| 2124 | 2167 | #[test] | |
| @@ -2172,7 +2215,11 @@ mod tests { | |||
| 2172 | 2215 | shots.push(("overview-full".to_owned(), 1240.0, on(Page::Overview, ""))); | |
| 2173 | 2216 | shots.push(("overview-cursor".to_owned(), 800.0, on(Page::Overview, "j j j j"))); | |
| 2174 | 2217 | shots.push(("keys".to_owned(), 800.0, on(Page::Overview, "?"))); | |
| 2175 | − | shots.push(("equalizer-cursor".to_owned(), 1000.0, on(Page::Equalizer, "G G k k"))); | |
| 2218 | + | let mut editing = on(Page::Equalizer, ""); | |
| 2219 | + | let _ = editing.update(Message::EditPreset(4)); | |
| 2220 | + | press(&mut editing, "G G k k"); | |
| 2221 | + | shots.push(("equalizer-cursor".to_owned(), 1100.0, editing)); | |
| 2222 | + | shots.push(("equalizer-pencil".to_owned(), 800.0, on(Page::Equalizer, "j j j j j j"))); | |
| 2176 | 2223 | shots.push(("gestures-cursor".to_owned(), 1000.0, on(Page::Gestures, "j j"))); | |
| 2177 | 2224 | shots.push(("find-confirm".to_owned(), 800.0, on(Page::Earbuds, "j Enter"))); | |
| 2178 | 2225 | shots.push(( | |
| @@ -270,6 +270,25 @@ pub fn pill_with<'a>( | |||
| 270 | 270 | .into() | |
| 271 | 271 | } | |
| 272 | 272 | ||
| 273 | + | /// A round button with just an icon, like the pencil on a preset's row. | |
| 274 | + | pub fn icon_button<'a>(look: Look, icon: Icon, focused: bool, on_press: Message) -> Element<'a, Message> { | |
| 275 | + | let p = look.p; | |
| 276 | + | button(icons::icon(icon, 19.0, if focused { p.fg } else { p.dim })) | |
| 277 | + | .on_press(on_press) | |
| 278 | + | .padding(8) | |
| 279 | + | .style(move |_, status| button::Style { | |
| 280 | + | background: (focused || hovered(status)).then_some(p.surface_highest.into()), | |
| 281 | + | text_color: p.fg, | |
| 282 | + | border: Border { | |
| 283 | + | color: p.accent, | |
| 284 | + | width: if focused { 2.0 } else { 0.0 }, | |
| 285 | + | radius: 999.0.into(), | |
| 286 | + | }, | |
| 287 | + | ..button::Style::default() | |
| 288 | + | }) | |
| 289 | + | .into() | |
| 290 | + | } | |
| 291 | + | ||
| 273 | 292 | /// A wide button with an outline, like "Disconnect". | |
| 274 | 293 | pub fn outline_button<'a>(look: Look, label: &'a str, on_press: Message) -> Element<'a, Message> { | |
| 275 | 294 | let p = look.p; | |