Krolyxon
Commit e8c78b9

Branches

Refs →

budctlcratesbudctl-guisrcoverview.rs

204 lines · 7.0 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, text};
5use iced::{Alignment, Element, Length};
6
7use crate::buds::Buds;
8use crate::icons::{self, Icon, Part, RingColors};
9use crate::theme::Look;
10use crate::ui::{self, Segment};
11use crate::{Item, Marks, Message, Page};
12
13/// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them.
14pub fn items(buds: &Buds) -> Vec<Item> {
15 let mut items = Vec::new();
16 if let Some(now) = &buds.status.noise_control {
17 items.push(Item::NoiseMode);
18 if now.mode == NoiseMode::Anc(AncLevel::Deep).name() {
19 items.push(Item::AncLevel);
20 }
21 }
22 items.extend(shown(&buds.status.switches).into_iter().map(|s| Item::Switch(s.key)));
23 items.extend(MORE.map(|(page, _, _)| Item::Open(page)));
24 items
25}
26
27/// Shown instead of an item's subtitle while a write to it is on its way.
28const SAVING: &str = "Saving…";
29
30pub fn view<'a>(look: Look, buds: &'a Buds, anc_level: AncLevel, marks: Marks) -> Element<'a, Message> {
31 column![ui::title(look, "Overview"), battery(look, buds)]
32 .push(
33 buds.status
34 .noise_control
35 .as_ref()
36 .map(|now| noise_control(look, &now.mode, anc_level, marks)),
37 )
38 .push(switches(look, &buds.status.switches, marks))
39 .push(more(look, buds, marks.focus))
40 .spacing(18)
41 .into()
42}
43
44fn battery<'a>(look: Look, buds: &Buds) -> Element<'a, Message> {
45 let p = look.p;
46 let colors = RingColors {
47 track: p.surface_highest,
48 charge: p.accent,
49 part: p.fg,
50 tip: p.dim,
51 cutout: p.surface,
52 };
53 let gauge = |part, label: &'a str, charge: Option<Charge>| -> Element<'a, Message> {
54 let reading = match charge {
55 Some(charge) => format!("{}%", charge.level),
56 None => "—".to_owned(),
57 };
58 let figure = row![text(reading).size(21).font(look.bold()).color(p.fg)]
59 .push(
60 charge
61 .filter(|c| c.charging)
62 .map(|_| icons::icon(Icon::Bolt, 16.0, p.icon)),
63 )
64 .spacing(4)
65 .align_y(Alignment::Center);
66 column![
67 icons::battery_ring(part, charge.map(|c| c.level), 112.0, colors),
68 figure,
69 text(label).size(13).color(p.dim)
70 ]
71 .spacing(6)
72 .align_x(Alignment::Center)
73 .width(Length::Fill)
74 .into()
75 };
76 let battery = buds.status.battery;
77 let gauges = row![
78 gauge(Part::Left, "Left", battery.left),
79 gauge(Part::Case, "Case", battery.case),
80 gauge(Part::Right, "Right", battery.right),
81 ];
82 ui::card(look, vec![container(gauges).padding([22, 8]).into()])
83}
84
85fn noise_control<'a>(look: Look, mode: &str, anc_level: AncLevel, marks: Marks) -> Element<'a, Message> {
86 let focus = marks.focus;
87 let modes = [
88 (Icon::NoiseOff, "Off", NoiseMode::Off),
89 (Icon::Anc, "Noise cancelling", NoiseMode::Anc(anc_level)),
90 (Icon::Transparency, "Transparency", NoiseMode::Transparency),
91 ];
92 let segments = modes
93 .into_iter()
94 .map(|(icon, label, set)| Segment {
95 icon: Some(icon),
96 label,
97 selected: set.name() == mode,
98 on_press: Message::Noise(set),
99 })
100 .collect();
101 let levels: Vec<Segment> = [
102 (AncLevel::Light, "Light"),
103 (AncLevel::Medium, "Medium"),
104 (AncLevel::Deep, "Deep"),
105 ]
106 .into_iter()
107 .map(|(level, label)| Segment {
108 icon: None,
109 label,
110 selected: level == anc_level,
111 on_press: Message::Noise(NoiseMode::Anc(level)),
112 })
113 .collect();
114 let strength = row![
115 text("Strength").size(13.5).color(look.p.dim).width(Length::Fill),
116 container(ui::segmented(look, levels, false, focus == Some(Item::AncLevel))).width(300)
117 ]
118 .align_y(Alignment::Center)
119 .padding([0, 8]);
120 let saving = marks
121 .pending
122 .contains(&Item::NoiseMode)
123 .then(|| container(text(SAVING).size(13).color(look.p.icon)).center_x(Length::Fill));
124 column![ui::segmented(look, segments, true, focus == Some(Item::NoiseMode))]
125 .push(saving)
126 .push((mode == NoiseMode::Anc(anc_level).name()).then_some(strength))
127 .spacing(12)
128 .into()
129}
130
131/// How the page shows the switches it knows, in this order. Others still show, last, with a
132/// plain icon and no subtitle.
133const SWITCHES: [(&str, Icon, &str); 5] = [
134 ("game", Icon::Game, "Lower latency, for games and video"),
135 ("spatial", Icon::Spatial, "Wider, surround-like sound"),
136 ("bass-boost", Icon::Bass, "Stronger low end"),
137 ("wind", Icon::Wind, "Less wind noise outdoors"),
138 ("dual", Icon::Dual, "Stay connected to two devices at once"),
139];
140
141/// The switches in the order the page shows them.
142fn shown(switches: &[Switch]) -> Vec<&Switch> {
143 let rank = |s: &&Switch| {
144 SWITCHES
145 .iter()
146 .position(|(k, _, _)| *k == s.key)
147 .unwrap_or(SWITCHES.len())
148 };
149 let mut shown: Vec<&Switch> = switches.iter().collect();
150 shown.sort_by_key(rank);
151 shown
152}
153
154fn switches<'a>(look: Look, switches: &[Switch], marks: Marks) -> Element<'a, Message> {
155 let known = |key: &str| SWITCHES.iter().find(|(k, _, _)| *k == key);
156 let rows = shown(switches)
157 .into_iter()
158 .map(|s| {
159 let (icon, about) = known(s.key).map_or((Icon::Settings, None), |&(_, icon, about)| (icon, Some(about)));
160 let saving = marks.pending.contains(&Item::Switch(s.key));
161 let subtitle = if saving { Some(SAVING) } else { about }.map(str::to_owned);
162 ui::setting(
163 look,
164 icon,
165 s.label,
166 subtitle,
167 ui::switch(look, s.on),
168 Message::Switch(s.key, !s.on),
169 marks.focus == Some(Item::Switch(s.key)),
170 )
171 })
172 .collect();
173 ui::card(look, rows)
174}
175
176/// The rows that open other pages: the page, its icon and title.
177const MORE: [(Page, Icon, &str); 3] = [
178 (Page::Equalizer, Icon::Equalizer, "Equalizer"),
179 (Page::Gestures, Icon::Gestures, "Gestures"),
180 (Page::Earbuds, Icon::Earbud, "Earbud settings"),
181];
182
183fn more<'a>(look: Look, buds: &Buds, focus: Option<Item>) -> Element<'a, Message> {
184 let rows = MORE
185 .into_iter()
186 .map(|(page, icon, title)| {
187 let subtitle = match page {
188 Page::Equalizer => buds.eq_name(),
189 Page::Gestures => "Taps and holds on each bud".to_owned(),
190 _ => "Find my earbuds, firmware".to_owned(),
191 };
192 ui::setting(
193 look,
194 icon,
195 title,
196 Some(subtitle),
197 ui::chevron(look),
198 Message::Open(page),
199 focus == Some(Item::Open(page)),
200 )
201 })
202 .collect();
203 ui::card(look, rows)
204}