Krolyxon
Commit 744d2be

Branches

Refs →

budctlcratesbudctl-guisrcoverview.rs

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