Krolyxon
Commit 66d7a70

Branches

Refs →

budctlcratesbudctl-guisrcoverview.rs

279 lines · 9.2 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::alignment::Horizontal;
5use iced::widget::{column, container, row, slider, text};
6use iced::{Alignment, Border, Element, Length};
7
8use crate::buds::Buds;
9use crate::equalizer;
10use crate::icons::{self, Icon, Part, RingColors};
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![ui::title(look, "Overview"), battery(look, buds)]
49 .push(
50 buds.status
51 .noise_control
52 .as_ref()
53 .map(|now| noise_control(look, &now.mode, anc_level, marks)),
54 )
55 .push(switches(look, buds, bass, marks))
56 .push(more(look, buds, marks.focus))
57 .spacing(18)
58 .into()
59}
60
61fn battery<'a>(look: Look, buds: &Buds) -> Element<'a, Message> {
62 let p = look.p;
63 let colors = RingColors {
64 track: p.surface_highest,
65 charge: p.accent,
66 part: p.fg,
67 tip: p.dim,
68 cutout: p.surface,
69 };
70 let gauge = |part, label: &'a str, charge: Option<Charge>| -> Element<'a, Message> {
71 let reading = match charge {
72 Some(charge) => format!("{}%", charge.level),
73 None => "—".to_owned(),
74 };
75 let figure = row![text(reading).size(21).font(look.bold()).color(p.fg)]
76 .push(
77 charge
78 .filter(|c| c.charging)
79 .map(|_| icons::icon(Icon::Bolt, 16.0, p.icon)),
80 )
81 .spacing(4)
82 .align_y(Alignment::Center);
83 column![
84 icons::battery_ring(part, charge.map(|c| c.level), 112.0, colors),
85 figure,
86 text(label).size(13).color(p.dim)
87 ]
88 .spacing(6)
89 .align_x(Alignment::Center)
90 .width(Length::Fill)
91 .into()
92 };
93 let battery = buds.status.battery;
94 let gauges = row![
95 gauge(Part::Left, "Left", battery.left),
96 gauge(Part::Case, "Case", battery.case),
97 gauge(Part::Right, "Right", battery.right),
98 ];
99 ui::card(look, vec![container(gauges).padding([22, 8]).into()])
100}
101
102fn noise_control<'a>(look: Look, mode: &str, anc_level: AncLevel, marks: Marks) -> Element<'a, Message> {
103 let focus = marks.focus;
104 let modes = [
105 (Icon::NoiseOff, "Off", NoiseMode::Off),
106 (Icon::Anc, "Noise cancelling", NoiseMode::Anc(anc_level)),
107 (Icon::Transparency, "Transparency", NoiseMode::Transparency),
108 ];
109 let segments = modes
110 .into_iter()
111 .map(|(icon, label, set)| Segment {
112 icon: Some(icon),
113 label,
114 selected: set.name() == mode,
115 on_press: Message::Noise(set),
116 })
117 .collect();
118 let levels: Vec<Segment> = [
119 (AncLevel::Light, "Light"),
120 (AncLevel::Medium, "Medium"),
121 (AncLevel::Deep, "Deep"),
122 ]
123 .into_iter()
124 .map(|(level, label)| Segment {
125 icon: None,
126 label,
127 selected: level == anc_level,
128 on_press: Message::Noise(NoiseMode::Anc(level)),
129 })
130 .collect();
131 let strength = row![
132 text("Strength").size(13.5).color(look.p.dim).width(Length::Fill),
133 container(ui::segmented(look, levels, false, focus == Some(Item::AncLevel))).width(300)
134 ]
135 .align_y(Alignment::Center)
136 .padding([0, 8]);
137 let saving = marks
138 .pending
139 .contains(&Item::NoiseMode)
140 .then(|| container(text(SAVING).size(13).color(look.p.icon)).center_x(Length::Fill));
141 column![ui::segmented(look, segments, true, focus == Some(Item::NoiseMode))]
142 .push(saving)
143 .push((mode == NoiseMode::Anc(anc_level).name()).then_some(strength))
144 .spacing(12)
145 .into()
146}
147
148/// How the page shows the switches it knows, in this order. Others still show, last, with a
149/// plain icon and no subtitle.
150const SWITCHES: [(&str, Icon, &str); 5] = [
151 ("game", Icon::Game, "Lower latency, for games and video"),
152 ("spatial", Icon::Spatial, "Wider, surround-like sound"),
153 ("bass-boost", Icon::Bass, "Stronger low end"),
154 ("wind", Icon::Wind, "Less wind noise outdoors"),
155 ("dual", Icon::Dual, "Stay connected to two devices at once"),
156];
157
158/// The switches in the order the page shows them.
159fn shown(switches: &[Switch]) -> Vec<&Switch> {
160 let rank = |s: &&Switch| {
161 SWITCHES
162 .iter()
163 .position(|(k, _, _)| *k == s.key)
164 .unwrap_or(SWITCHES.len())
165 };
166 let mut shown: Vec<&Switch> = switches.iter().collect();
167 shown.sort_by_key(rank);
168 shown
169}
170
171fn switches<'a>(look: Look, buds: &Buds, bass: Option<i8>, marks: Marks) -> Element<'a, Message> {
172 let known = |key: &str| SWITCHES.iter().find(|(k, _, _)| *k == key);
173 let mut rows = Vec::new();
174 for s in shown(&buds.status.switches) {
175 rows.push({
176 let (icon, about) = known(s.key).map_or((Icon::Settings, None), |&(_, icon, about)| (icon, Some(about)));
177 let saving = marks.pending.contains(&Item::Switch(s.key));
178 let subtitle = if saving { Some(SAVING) } else { about }.map(str::to_owned);
179 ui::setting(
180 look,
181 icon,
182 s.label,
183 subtitle,
184 ui::switch(look, s.on),
185 Some(Message::Switch(s.key, !s.on)),
186 marks.focus == Some(Item::Switch(s.key)),
187 )
188 });
189 if let (BASS_BOOST, Some(range)) = (s.key, buds.bass_range) {
190 rows.push(bass_level(look, range, bass.or(buds.bass), marks));
191 }
192 }
193 ui::card(look, rows)
194}
195
196/// The bass level: the buds take it but do not report it, so the row shows the last level set
197/// from this window, if any.
198fn bass_level<'a>(look: Look, (low, high): (i8, i8), level: Option<i8>, marks: Marks) -> Element<'a, Message> {
199 let p = look.p;
200 let about = if marks.pending.contains(&Item::Bass) {
201 SAVING
202 } else {
203 "Last set here: the buds do not report it"
204 };
205 let shown = level.map_or_else(|| "Not set".to_owned(), equalizer::gain);
206 let slider = slider(
207 i16::from(low)..=i16::from(high),
208 i16::from(level.unwrap_or(0)),
209 |level| Message::Bass {
210 level: i8::try_from(level).unwrap_or(0),
211 save: false,
212 },
213 )
214 .on_release(Message::SaveBass)
215 .width(200)
216 .style(move |_, _| slider::Style {
217 rail: slider::Rail {
218 backgrounds: (p.accent.into(), p.surface_highest.into()),
219 width: 6.0,
220 border: Border::default().rounded(3),
221 },
222 handle: slider::Handle {
223 shape: slider::HandleShape::Circle { radius: 9.0 },
224 background: p.fg.into(),
225 border_width: 2.0,
226 border_color: p.accent,
227 },
228 });
229 let control = row![
230 text(shown)
231 .size(14)
232 .font(look.medium())
233 .color(p.dim)
234 .width(64)
235 .align_x(Horizontal::Right),
236 slider
237 ]
238 .spacing(14)
239 .align_y(Alignment::Center);
240 ui::setting(
241 look,
242 Icon::Bass,
243 "Bass level",
244 Some(about.to_owned()),
245 control.into(),
246 None,
247 marks.focus == Some(Item::Bass),
248 )
249}
250
251/// The rows that open other pages: the page, its icon and title.
252const MORE: [(Page, Icon, &str); 3] = [
253 (Page::Equalizer, Icon::Equalizer, "Equalizer"),
254 (Page::Gestures, Icon::Gestures, "Gestures"),
255 (Page::Earbuds, Icon::Earbud, "Earbud settings"),
256];
257
258fn more<'a>(look: Look, buds: &Buds, focus: Option<Item>) -> Element<'a, Message> {
259 let rows = MORE
260 .into_iter()
261 .map(|(page, icon, title)| {
262 let subtitle = match page {
263 Page::Equalizer => buds.eq_name(),
264 Page::Gestures => "Taps and holds on each bud".to_owned(),
265 _ => "Dual connection, find my earbuds, firmware".to_owned(),
266 };
267 ui::setting(
268 look,
269 icon,
270 title,
271 Some(subtitle),
272 ui::chevron(look),
273 Some(Message::Open(page)),
274 focus == Some(Item::Open(page)),
275 )
276 })
277 .collect();
278 ui::card(look, rows)
279}