Krolyxon
Commit 1978b2f

Branches

Refs →

budctlcratesbudctl-guisrcgestures.rs

256 lines · 8.1 KB · Rust

1//! The Gestures page: what each tap and hold does on each bud, and what a hold steps through.
2
3use std::fmt;
4
5use budctl_core::status::TouchControls;
6use iced::widget::overlay::menu;
7use iced::widget::{column, pick_list, text};
8use iced::{Border, Element, Shadow};
9
10use crate::buds::{self, Buds};
11use crate::icons::Icon;
12use crate::theme::Look;
13use crate::ui;
14use crate::{Item, Marks, Message, TouchDraft};
15
16/// The buds, as the page shows them.
17const SIDES: [(&str, &str); 2] = [("left", "Left bud"), ("right", "Right bud")];
18
19/// Shown instead of a subtitle while a write is on its way.
20const SAVING: &str = "Saving…";
21
22/// One gesture of one bud, and the actions it can be given.
23pub struct Row<'a> {
24 pub side: &'static str,
25 pub gesture: &'a str,
26 pub actions: &'a [String],
27}
28
29/// The page's gesture rows, in order: each bud's gestures, in the order the buds offer them.
30/// [`Item::Gesture`] counts in this list.
31pub fn rows(touch: &TouchControls) -> Vec<Row<'_>> {
32 SIDES
33 .iter()
34 .flat_map(|&(side, _)| {
35 touch.choices.iter().map(move |choice| Row {
36 side,
37 gesture: &choice.gesture,
38 actions: &choice.actions,
39 })
40 })
41 .collect()
42}
43
44/// What the keyboard cursor can rest on, top to bottom: the gestures, then the hold-cycle modes.
45pub fn items(buds: &Buds) -> Vec<Item> {
46 let Some(touch) = &buds.touch else {
47 return Vec::new();
48 };
49 let count = |n: usize| (0..n).filter_map(|i| u8::try_from(i).ok());
50 count(rows(touch).len())
51 .map(Item::Gesture)
52 .chain(count(touch.cycle_modes.len()).map(Item::Cycle))
53 .collect()
54}
55
56/// `draft`: an action picked on the screen and not on the buds yet.
57pub fn view<'a>(look: Look, buds: &'a Buds, draft: Option<&'a TouchDraft>, marks: Marks) -> Element<'a, Message> {
58 let p = look.p;
59 let title = ui::title(look, "Gestures");
60 let Some(touch) = &buds.touch else {
61 let none = text("These buds did not report their touch controls.")
62 .size(14)
63 .color(p.dim);
64 return column![title, none].spacing(12).into();
65 };
66 let rows = rows(touch);
67 let mut page = column![title].spacing(18);
68 for (side, heading) in SIDES {
69 let gestures = rows
70 .iter()
71 .enumerate()
72 .filter(|(_, row)| row.side == side)
73 .filter_map(|(n, row)| Some((u8::try_from(n).ok()?, row)))
74 .map(|(n, row)| {
75 let shown = draft
76 .filter(|draft| draft.row == n)
77 .map(|draft| draft.action.as_str())
78 .or_else(|| buds.action(side, row.gesture))
79 .unwrap_or("none");
80 let options: Vec<Choice> = row.actions.iter().map(|a| Choice::new(a)).collect();
81 let picker = pick_list(options, Some(Choice::new(shown)), move |choice: Choice| {
82 Message::Action {
83 row: n,
84 action: choice.key,
85 save: true,
86 }
87 })
88 .width(220)
89 .padding([8, 12])
90 .text_size(14)
91 .style(move |_, _| pick_list::Style {
92 text_color: p.fg,
93 placeholder_color: p.dim,
94 handle_color: p.dim,
95 background: p.surface_high.into(),
96 border: Border {
97 color: p.line,
98 width: 1.0,
99 radius: 10.0.into(),
100 },
101 })
102 .menu_style(move |_| menu::Style {
103 background: p.surface_high.into(),
104 border: Border {
105 color: p.line,
106 width: 1.0,
107 radius: 10.0.into(),
108 },
109 text_color: p.fg,
110 selected_text_color: p.accent_fg,
111 selected_background: p.accent.into(),
112 shadow: Shadow::default(),
113 });
114 let saving = marks.pending.contains(&Item::Gesture(n)).then(|| SAVING.to_owned());
115 ui::setting(
116 look,
117 gesture_icon(row.gesture),
118 gesture_name(row.gesture),
119 saving,
120 picker.into(),
121 None,
122 marks.focus == Some(Item::Gesture(n)),
123 )
124 })
125 .collect();
126 page = page.push(column![ui::heading(look, heading), ui::card(look, gestures)].spacing(10));
127 }
128 if let (Some(cycle), false) = (&touch.hold_cycle, touch.cycle_modes.is_empty()) {
129 let modes = touch
130 .cycle_modes
131 .iter()
132 .enumerate()
133 .filter_map(|(n, mode)| Some((u8::try_from(n).ok()?, mode)))
134 .map(|(n, mode)| {
135 let on = cycle.contains(mode);
136 let about = if marks.pending.contains(&Item::Cycle(n)) {
137 Some(SAVING.to_owned())
138 } else if on && cycle.len() <= 2 {
139 Some("A hold needs two modes to step through".to_owned())
140 } else {
141 None
142 };
143 ui::setting(
144 look,
145 mode_icon(mode),
146 mode_name(mode),
147 about,
148 ui::switch(look, on),
149 Some(Message::Cycle(n, !on)),
150 marks.focus == Some(Item::Cycle(n)),
151 )
152 })
153 .collect();
154 page = page.push(column![ui::heading(look, "A hold steps through"), ui::card(look, modes)].spacing(10));
155 }
156 page.into()
157}
158
159/// An action, for the dropdown: its name on the buds, and how it reads.
160#[derive(Debug, Clone, PartialEq)]
161struct Choice {
162 key: String,
163 label: String,
164}
165
166impl Choice {
167 fn new(key: &str) -> Self {
168 Choice {
169 key: key.to_owned(),
170 label: action_name(key),
171 }
172 }
173}
174
175impl fmt::Display for Choice {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 f.write_str(&self.label)
178 }
179}
180
181fn gesture_name(gesture: &str) -> String {
182 match gesture {
183 "hold" => "Touch and hold".to_owned(),
184 other => buds::words(other),
185 }
186}
187
188fn gesture_icon(gesture: &str) -> Icon {
189 match gesture {
190 "hold" => Icon::Earbud,
191 _ => Icon::Gestures,
192 }
193}
194
195pub fn action_name(action: &str) -> String {
196 match action {
197 "none" => "Nothing",
198 "play-pause" => "Play / pause",
199 "next" => "Next track",
200 "previous" => "Previous track",
201 "voice-assistant" => "Voice assistant",
202 "noise-cycle" => "Noise control",
203 other => return buds::words(other),
204 }
205 .to_owned()
206}
207
208fn mode_name(mode: &str) -> String {
209 match mode {
210 "anc" => "Noise cancelling".to_owned(),
211 other => buds::words(other),
212 }
213}
214
215fn mode_icon(mode: &str) -> Icon {
216 match mode {
217 "off" => Icon::NoiseOff,
218 "anc" => Icon::Anc,
219 _ => Icon::Transparency,
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn rows_go_bud_by_bud() {
229 let buds = buds::demo();
230 let touch = buds.touch.as_ref().unwrap();
231 let rows = rows(touch);
232 let names: Vec<(&str, &str)> = rows.iter().map(|r| (r.side, r.gesture)).collect();
233 assert_eq!(
234 names,
235 [
236 ("left", "double-tap"),
237 ("left", "triple-tap"),
238 ("left", "hold"),
239 ("right", "double-tap"),
240 ("right", "triple-tap"),
241 ("right", "hold"),
242 ]
243 );
244 let items = items(&buds);
245 assert_eq!(items.len(), 6 + 3);
246 assert_eq!(items.last(), Some(&Item::Cycle(2)));
247 }
248
249 #[test]
250 fn names_read_well() {
251 assert_eq!(action_name("play-pause"), "Play / pause");
252 assert_eq!(action_name("volume-up"), "Volume up");
253 assert_eq!(gesture_name("triple-tap"), "Triple tap");
254 assert_eq!(mode_name("anc"), "Noise cancelling");
255 }
256}