Krolyxon
Commit 7c15b41

Branches

Refs →

budctlcratesbudctl-guisrcoverview.rs

255 lines · 8.7 KB · Rust

1//! The Overview page: battery, noise control, the switches, and the way to the other pages.
2
3use budctl_core::status::{AncLevel, Charge, NoiseMode, Switch};
4use iced::widget::{column, container, row, stack};
5use iced::{Alignment, Element, Length};
6
7use crate::buds::Buds;
8use crate::dots;
9use crate::equalizer;
10use crate::icons::{self, Icon};
11use crate::theme::Look;
12use crate::ui::{self, Segment};
13use crate::{Item, Marks, Message, Page};
14
15/// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them.
16pub fn items(buds: &Buds) -> Vec<Item> {
17 let mut items = Vec::new();
18 if let Some(now) = &buds.status.noise_control {
19 items.push(Item::NoiseMode);
20 if now.mode == NoiseMode::Anc(AncLevel::Deep).name() {
21 items.push(Item::AncLevel);
22 }
23 }
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 }
30 items.extend(MORE.map(|(page, _, _)| Item::Open(page)));
31 items
32}
33
34/// The switch the bass level shows under.
35const BASS_BOOST: &str = "bass-boost";
36
37/// Shown instead of an item's subtitle while a write to it is on its way.
38const SAVING: &str = "Saving…";
39
40/// `bass`: a bass level moved on the screen and not set on the buds yet.
41pub 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> {
48 column![
49 ui::title(look, "overview · battery, noise control, sound"),
50 battery(look, buds)
51 ]
52 .push(
53 buds.status
54 .noise_control
55 .as_ref()
56 .map(|now| noise_control(look, &now.mode, anc_level, marks)),
57 )
58 .push(switches(look, buds, bass, marks))
59 .push(more(look, buds, marks.focus))
60 .spacing(18)
61 .into()
62}
63
64fn battery<'a>(look: Look, buds: &Buds) -> Element<'a, Message> {
65 let p = look.p;
66 let gauge = |label: &str, charge: Option<Charge>| -> Element<'a, Message> {
67 let figure = match charge {
68 Some(charge) => ui::num(look, &charge.level.to_string(), 44.0, p.fg),
69 None => ui::num(look, "--", 44.0, p.mute),
70 };
71 let ring = stack![
72 dots::ring(charge.map(|c| f32::from(c.level) / 100.0), 150.0, p.fill, p.s3),
73 container(figure).center(150),
74 ];
75 let charging = charge.is_some_and(|c| c.charging);
76 let caption = row![ui::label(look, label, 11.0, p.grey)]
77 .push(charging.then(|| icons::icon(Icon::Bolt, 14.0, p.live)))
78 .push(charging.then(|| ui::label(look, "charging", 11.0, p.live)))
79 .spacing(6)
80 .align_y(Alignment::Center);
81 column![ring, caption]
82 .spacing(14)
83 .align_x(Alignment::Center)
84 .width(Length::Fill)
85 .into()
86 };
87 let battery = buds.status.battery;
88 let gauges = row![
89 gauge("left", battery.left),
90 gauge("case", battery.case),
91 gauge("right", battery.right),
92 ];
93 ui::card(look, vec![container(gauges).padding([26, 8]).into()])
94}
95
96fn noise_control<'a>(look: Look, mode: &str, anc_level: AncLevel, marks: Marks) -> Element<'a, Message> {
97 let focus = marks.focus;
98 let modes = [
99 (Icon::NoiseOff, "Off", NoiseMode::Off),
100 (Icon::Anc, "Noise cancelling", NoiseMode::Anc(anc_level)),
101 (Icon::Transparency, "Transparency", NoiseMode::Transparency),
102 ];
103 let segments = modes
104 .into_iter()
105 .map(|(icon, label, set)| Segment {
106 icon: Some(icon),
107 label,
108 selected: set.name() == mode,
109 on_press: Message::Noise(set),
110 })
111 .collect();
112 let levels: Vec<Segment> = [
113 (AncLevel::Light, "Light"),
114 (AncLevel::Medium, "Medium"),
115 (AncLevel::Deep, "Deep"),
116 ]
117 .into_iter()
118 .map(|(level, label)| Segment {
119 icon: None,
120 label,
121 selected: level == anc_level,
122 on_press: Message::Noise(NoiseMode::Anc(level)),
123 })
124 .collect();
125 let strength = row![
126 container(ui::label(look, "strength", 11.0, look.p.grey)).width(Length::Fill),
127 container(ui::segmented(look, levels, false, focus == Some(Item::AncLevel))).width(340)
128 ]
129 .align_y(Alignment::Center)
130 .padding([0, 18]);
131 let saving = marks
132 .pending
133 .contains(&Item::NoiseMode)
134 .then(|| container(ui::label(look, SAVING, 11.0, look.p.live)).center_x(Length::Fill));
135 column![ui::segmented(look, segments, true, focus == Some(Item::NoiseMode))]
136 .push(saving)
137 .push((mode == NoiseMode::Anc(anc_level).name()).then_some(strength))
138 .spacing(12)
139 .into()
140}
141
142/// How the page shows the switches it knows, in this order. Others still show, last, with a
143/// plain icon and no subtitle.
144const SWITCHES: [(&str, Icon, &str); 5] = [
145 ("game", Icon::Game, "Lower latency, for games and video"),
146 ("spatial", Icon::Spatial, "Wider, surround-like sound"),
147 ("bass-boost", Icon::Bass, "Stronger low end"),
148 ("wind", Icon::Wind, "Less wind noise outdoors"),
149 ("dual", Icon::Dual, "Stay connected to two devices at once"),
150];
151
152/// The switches in the order the page shows them.
153fn shown(switches: &[Switch]) -> Vec<&Switch> {
154 let rank = |s: &&Switch| {
155 SWITCHES
156 .iter()
157 .position(|(k, _, _)| *k == s.key)
158 .unwrap_or(SWITCHES.len())
159 };
160 let mut shown: Vec<&Switch> = switches.iter().collect();
161 shown.sort_by_key(rank);
162 shown
163}
164
165fn switches<'a>(look: Look, buds: &Buds, bass: Option<i8>, marks: Marks) -> Element<'a, Message> {
166 let known = |key: &str| SWITCHES.iter().find(|(k, _, _)| *k == key);
167 let mut rows = Vec::new();
168 for s in shown(&buds.status.switches) {
169 rows.push({
170 let (icon, about) = known(s.key).map_or((Icon::Settings, None), |&(_, icon, about)| (icon, Some(about)));
171 let saving = marks.pending.contains(&Item::Switch(s.key));
172 let subtitle = if saving { Some(SAVING) } else { about }.map(str::to_owned);
173 ui::setting(
174 look,
175 icon,
176 s.label,
177 subtitle,
178 ui::switch(look, s.on),
179 Some(Message::Switch(s.key, !s.on)),
180 marks.focus == Some(Item::Switch(s.key)),
181 )
182 });
183 if let (BASS_BOOST, Some(range)) = (s.key, buds.bass_range) {
184 rows.push(bass_level(look, range, bass.or(buds.bass), marks));
185 }
186 }
187 ui::card(look, rows)
188}
189
190/// The bass level: the buds take it but do not report it, so the row shows the last level set
191/// from this window, if any.
192fn bass_level<'a>(look: Look, (low, high): (i8, i8), level: Option<i8>, marks: Marks) -> Element<'a, Message> {
193 let p = look.p;
194 let about = if marks.pending.contains(&Item::Bass) {
195 SAVING
196 } else {
197 "Last set here: the buds do not report it"
198 };
199 let shown = level.map_or_else(|| "not set".to_owned(), equalizer::gain);
200 let slider = dots::Slider {
201 low: i16::from(low),
202 high: i16::from(high),
203 value: i16::from(level.unwrap_or(0)),
204 vertical: false,
205 change: |level, save| Message::Bass {
206 level: i8::try_from(level).unwrap_or(0),
207 save,
208 },
209 release: Message::SaveBass,
210 on: p.fill,
211 off: p.s3,
212 };
213 let control = row![container(ui::value(look, &shown)).align_right(76), slider.view(200.0)]
214 .spacing(14)
215 .align_y(Alignment::Center);
216 ui::setting(
217 look,
218 Icon::Bass,
219 "Bass level",
220 Some(about.to_owned()),
221 control.into(),
222 None,
223 marks.focus == Some(Item::Bass),
224 )
225}
226
227/// The rows that open other pages: the page, its icon and title.
228const MORE: [(Page, Icon, &str); 3] = [
229 (Page::Equalizer, Icon::Equalizer, "Equalizer"),
230 (Page::Gestures, Icon::Gestures, "Gestures"),
231 (Page::Earbuds, Icon::Earbud, "Earbud settings"),
232];
233
234fn more<'a>(look: Look, buds: &Buds, focus: Option<Item>) -> Element<'a, Message> {
235 let rows = MORE
236 .into_iter()
237 .map(|(page, icon, title)| {
238 let subtitle = match page {
239 Page::Equalizer => buds.eq_name(),
240 Page::Gestures => "Taps and holds on each bud".to_owned(),
241 _ => "Dual connection, find my earbuds, firmware".to_owned(),
242 };
243 ui::setting(
244 look,
245 icon,
246 title,
247 Some(subtitle),
248 ui::chevron(look),
249 Some(Message::Open(page)),
250 focus == Some(Item::Open(page)),
251 )
252 })
253 .collect();
254 ui::card(look, rows)
255}