Krolyxon
Commit b9e5c00

Branches

Refs →

budctlcratesbudctl-guisrcoverview.rs

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