system settings
git clone https://git.lucas.co/cce-system-interface.git
src/app.rs (20.8K)
1 use cce_ui::layout::RenderTarget;
2
3 use crate::pages::audio;
4 use crate::pages::bluetooth;
5 use crate::pages::power;
6 use crate::pages::default_apps;
7 use crate::pages::network;
8 use crate::pages::processes;
9 use crate::pages::services;
10 use crate::pages::system_info;
11 use crate::pages::timers;
12 use crate::pages::storage;
13 use crate::pages::accounts;
14 use crate::pages::packages;
15 use crate::pages::notifications;
16 use crate::pages::browser;
17 use crate::pages::Page;
18
19 pub struct AppState {
20 pub current_page: Page,
21 pub audio: audio::AudioState,
22 pub bluetooth: bluetooth::BluetoothState,
23 pub power: power::PowerState,
24 pub browser: browser::BrowserState,
25 pub default_apps: default_apps::DefaultAppsState,
26 pub network: network::NetworkState,
27 pub processes: processes::ProcessesState,
28 pub services: services::ServicesState,
29 pub system_info: system_info::SystemState,
30 pub timers: timers::TimersState,
31 pub storage: storage::StorageState,
32 pub accounts: accounts::AccountsState,
33 pub packages: packages::PackagesState,
34 pub notifications: notifications::NotificationsState,
35 }
36
37 impl Default for AppState {
38 fn default() -> Self {
39 Self {
40 current_page: Page::ALL[0],
41 audio: audio::AudioState::default(),
42 bluetooth: bluetooth::BluetoothState::default(),
43 power: power::PowerState::default(),
44 browser: browser::BrowserState::default(),
45 default_apps: default_apps::DefaultAppsState::default(),
46 network: network::NetworkState::default(),
47 processes: processes::ProcessesState::default(),
48 services: services::ServicesState::default(),
49 system_info: system_info::SystemState::default(),
50 timers: timers::TimersState::default(),
51 storage: storage::StorageState::default(),
52 accounts: accounts::AccountsState::default_mock(),
53 packages: packages::PackagesState::default(),
54 notifications: notifications::NotificationsState::default(),
55 }
56 }
57 }
58
59 impl AppState {
60 pub fn get_page(&self, page: Page) -> &dyn crate::pages::AppPage {
61 match page {
62 Page::Accounts => &self.accounts,
63 Page::Audio => &self.audio,
64 Page::Bluetooth => &self.bluetooth,
65 Page::Power => &self.power,
66 Page::Browser => &self.browser,
67 Page::DefaultApps => &self.default_apps,
68 Page::Packages => &self.packages,
69 Page::Processes => &self.processes,
70 Page::Services => &self.services,
71 Page::Network => &self.network,
72 Page::Storage => &self.storage,
73 Page::System => &self.system_info,
74 Page::Timers => &self.timers,
75 Page::Notifications => &self.notifications,
76 }
77 }
78
79 pub fn get_page_mut(&mut self, page: Page) -> &mut dyn crate::pages::AppPage {
80 match page {
81 Page::Accounts => &mut self.accounts,
82 Page::Audio => &mut self.audio,
83 Page::Bluetooth => &mut self.bluetooth,
84 Page::Power => &mut self.power,
85 Page::Browser => &mut self.browser,
86 Page::DefaultApps => &mut self.default_apps,
87 Page::Packages => &mut self.packages,
88 Page::Processes => &mut self.processes,
89 Page::Services => &mut self.services,
90 Page::Network => &mut self.network,
91 Page::Storage => &mut self.storage,
92 Page::System => &mut self.system_info,
93 Page::Timers => &mut self.timers,
94 Page::Notifications => &mut self.notifications,
95 }
96 }
97
98 pub fn get_current_page(&self) -> &dyn crate::pages::AppPage {
99 self.get_page(self.current_page)
100 }
101
102 pub fn get_current_page_mut(&mut self) -> &mut dyn crate::pages::AppPage {
103 self.get_page_mut(self.current_page)
104 }
105 }
106
107 #[derive(Debug, Clone)]
108 pub enum AppAction {
109 Exit,
110 Audio(audio::AudioMessage),
111 Browser(browser::BrowserMessage),
112 DefaultApps(default_apps::DefaultAppsMessage),
113 Network(network::NetworkMessage),
114 Bluetooth(bluetooth::BluetoothMessage),
115 Power(power::PowerMessage),
116 Processes(processes::ProcessesMessage),
117 Services(services::ServicesMessage),
118 SystemInfo(system_info::SystemMessage),
119 Timers(timers::TimersMessage),
120 Storage(storage::StorageMessage),
121 Accounts(accounts::AccountsMessage),
122 Packages(packages::PackagesMessage),
123 Notifications(notifications::NotificationsMessage),
124 }
125
126
127 /// A control carve bridged out of a widget's `paint` for the flat path — the
128 /// relief prims the toolkit's controls draw for themselves and the legacy
129 /// `all_quads` stream cannot carry. The page collects them and `display_list`
130 /// re-emits them as real prims into the window plate.
131 #[derive(Clone, Copy, Debug, PartialEq)]
132 pub enum ControlCarve {
133 /// `PaintCtx::inset_plate` — a flush inset face over a boundary seam
134 /// (Dropdown, Button). `color` fills the face; transparent leaves the
135 /// plate below.
136 Plate { x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, color: [f32; 4], tint: Option<[f32; 3]> },
137 /// A step carve straight from the toolkit (TextBox well, a Toggle's well
138 /// and glider) — it already carries its own rect, per-corner radii,
139 /// depth and wall mask, so nothing here re-derives them.
140 Step(cce_ui::layout::ReliefCarve),
141 }
142
143 impl ControlCarve {
144 /// This carve shifted vertically — the page-scroll adjustment the popover
145 /// collector applies when it lifts page carves to the popover layer.
146 pub fn shifted_y(self, dy: f32) -> Self {
147 match self {
148 Self::Plate { x, y, w, h, radius, depth, color, tint } => {
149 Self::Plate { x, y: y + dy, w, h, radius, depth, color, tint }
150 }
151 Self::Step(c) => Self::Step(c.shifted_y(dy)),
152 }
153 }
154 }
155
156 pub struct PageContent {
157 pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
158 pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
159 /// (button, action, clip): clip is the innermost push_clip_rect at emission
160 /// time (page coords) — the renderer clamps the drawn quad, label bounds,
161 /// and the dispatch clone's hit rect to it.
162 pub buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, AppAction, Option<[f32; 4]>)>,
163 /// Section wells claimed via `RenderTarget::section_relief` — the body box
164 /// plus the title tab box, carved into the window plate by display_list as
165 /// recess prims (page coordinates, pre-scroll).
166 pub reliefs: Vec<((f32, f32, f32, f32), Option<(f32, f32, f32, f32)>)>,
167 /// Control carves claimed via the flat-path relief hooks
168 /// (`RenderTarget::inset_plate` for a Dropdown's flush inset chrome,
169 /// `RenderTarget::recess` for a TextBox's well) — (x, y, w, h, radius,
170 /// depth, carve), page coordinates, pre-scroll; display_list re-emits them
171 /// as real relief prims.
172 pub control_reliefs: Vec<ControlCarve>,
173 /// Per carve, `rects.len()` at the moment it was claimed — the carve's
174 /// place in the widget's own emission order. A Dropdown draws its
175 /// hovered-row highlight AFTER the inset plate it claims for the menu
176 /// face; replaying every rect and then every carve put the frosted plate
177 /// over the highlight. The popover layer interleaves on these marks.
178 pub control_relief_marks: Vec<usize>,
179 pub clip_stack: Vec<[f32; 4]>,
180 pub measure_only: bool,
181 }
182
183 impl Default for PageContent {
184 fn default() -> Self {
185 Self {
186 rects: Vec::new(),
187 texts: Vec::new(),
188 buttons: Vec::new(),
189 reliefs: Vec::new(),
190 control_reliefs: Vec::new(),
191 control_relief_marks: Vec::new(),
192 clip_stack: Vec::new(),
193 measure_only: true,
194 }
195 }
196 }
197
198 impl PageContent {
199 pub fn new() -> Self {
200 Self {
201 rects: Vec::new(),
202 texts: Vec::new(),
203 buttons: Vec::new(),
204 reliefs: Vec::new(),
205 control_reliefs: Vec::new(),
206 control_relief_marks: Vec::new(),
207 clip_stack: Vec::new(),
208 measure_only: false,
209 }
210 }
211
212 fn get_clipped_rect(&self, x: f32, y: f32, w: f32, h: f32) -> Option<(f32, f32, f32, f32)> {
213 if let Some(&clip) = self.clip_stack.last() {
214 let cx = clip[0];
215 let cy = clip[1];
216 let cw = clip[2];
217 let ch = clip[3];
218 let rx1 = x.max(cx);
219 let ry1 = y.max(cy);
220 let rx2 = (x + w).min(cx + cw);
221 let ry2 = (y + h).min(cy + ch);
222 if rx1 < rx2 && ry1 < ry2 {
223 Some((rx1, ry1, rx2 - rx1, ry2 - ry1))
224 } else {
225 None
226 }
227 } else {
228 Some((x, y, w, h))
229 }
230 }
231
232 fn get_clipped_bounds(&self, bounds: Option<[f32; 4]>) -> Option<[f32; 4]> {
233 if let Some(&clip) = self.clip_stack.last() {
234 if let Some(b) = bounds {
235 let rx1 = b[0].max(clip[0]);
236 let ry1 = b[1].max(clip[1]);
237 let rx2 = b[2].min(clip[0] + clip[2]);
238 let ry2 = b[3].min(clip[1] + clip[3]);
239 Some([rx1, ry1, rx2.max(rx1), ry2.max(ry1)])
240 } else {
241 Some([clip[0], clip[1], clip[0] + clip[2], clip[1] + clip[3]])
242 }
243 } else {
244 bounds
245 }
246 }
247
248 pub fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
249 if self.measure_only { return; }
250 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
251 self.rects.push((color, cx, cy, cw, ch, 0.0, (true, true, true, true)));
252 }
253 }
254
255 pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
256 if self.measure_only { return; }
257 let cb = self.get_clipped_bounds(None);
258 self.texts.push((content.to_string(), size, x, y, color, None, cb));
259 }
260
261 pub fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
262 if self.measure_only { return; }
263 let cb = self.get_clipped_bounds(None);
264 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), cb));
265 }
266
267 pub fn button(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
268 bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
269 action: AppAction) {
270 if self.measure_only { return; }
271 let btn = cce_ui::widget::Button::new(x, y, w, h)
272 .with_label(label)
273 .with_bg(bg)
274 .with_hover_bg(hover_bg)
275 .with_label_color(label_color);
276 let clip = self.clip_stack.last().copied();
277 self.buttons.push((btn, action, clip));
278 }
279
280 /// A button whose face is a bundled cce-icons glyph instead of a label.
281 ///
282 /// `label` stays as the FALLBACK: `upload_icon` returns `None` when the
283 /// icon set is missing or unparsable, and a control that silently loses
284 /// its face has no affordance left at all — so the button degrades to the
285 /// word rather than to an empty box. `alpha` dims the glyph for a disabled
286 /// control, which is the only state lever an icon has (images carry no
287 /// color).
288 pub fn button_icon(&mut self, icon: &str, label: &str, x: f32, y: f32, w: f32, h: f32,
289 bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
290 alpha: f32, action: AppAction) {
291 if self.measure_only { return; }
292 let mut btn = cce_ui::widget::Button::new(x, y, w, h)
293 .with_label(label)
294 .with_bg(bg)
295 .with_hover_bg(hover_bg)
296 .with_label_color(label_color);
297 // 32px on the longer side: the toolkit caches the upload per (name, px),
298 // so every row's Start button shares one texture.
299 if let Some((id, iw, ih)) = cce_ui::upload_icon(icon, 32) {
300 btn = btn.with_icon(id, iw as f32, ih as f32).with_icon_alpha(alpha);
301 }
302 let clip = self.clip_stack.last().copied();
303 self.buttons.push((btn, action, clip));
304 }
305
306 pub fn button_left(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
307 bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
308 action: AppAction) {
309 if self.measure_only { return; }
310 let btn = cce_ui::widget::Button::new(x, y, w, h)
311 .with_label(label)
312 .with_bg(bg)
313 .with_hover_bg(hover_bg)
314 .with_label_color(label_color)
315 .with_left_align(true);
316 let clip = self.clip_stack.last().copied();
317 self.buttons.push((btn, action, clip));
318 }
319 }
320
321 impl RenderTarget for PageContent {
322 fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
323 if self.measure_only { return; }
324 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
325 self.rects.push((color, cx, cy, cw, ch, 0.0, (true, true, true, true)));
326 }
327 }
328
329 fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
330 if self.measure_only { return; }
331 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
332 self.rects.push((color, cx, cy, cw, ch, radius, (true, true, true, true)));
333 }
334 }
335
336 fn rect_with_radius_corners(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, corners: (bool, bool, bool, bool)) {
337 if self.measure_only { return; }
338 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
339 self.rects.push((color, cx, cy, cw, ch, radius, corners));
340 }
341 }
342
343 fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
344 if self.measure_only { return; }
345 let cb = self.get_clipped_bounds(None);
346 self.texts.push((content.to_string(), size, x, y, color, None, cb));
347 }
348
349 fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
350 if self.measure_only { return; }
351 let cb = self.get_clipped_bounds(None);
352 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), cb));
353 }
354
355 fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
356 if self.measure_only { return; }
357 let cb = self.get_clipped_bounds(bounds);
358 self.texts.push((content.to_string(), size, x, y, color, None, cb));
359 }
360
361 fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
362 if self.measure_only { return; }
363 let cb = self.get_clipped_bounds(bounds);
364 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), cb));
365 }
366
367 fn section_relief_style(&self) -> bool {
368 cce_ui::layout::control_relief()
369 }
370
371 /// The flat-path bridge offers each Dropdown's flush inset trough here;
372 /// carve it for real (display_list turns these into inset plates).
373 fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
374 if self.measure_only {
375 return;
376 }
377 self.control_relief_marks.push(self.rects.len());
378 self.control_reliefs.push(ControlCarve::Plate { x, y, w, h, radius, depth, color, tint: None });
379 }
380 fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: [f32; 3]) {
381 self.control_reliefs.push(ControlCarve::Plate { x, y, w, h, radius, depth, color, tint: Some(tint) });
382 }
383
384 /// The same bridge for the step carves — a DIFFERENT shape, not an inset
385 /// plate at another rect: a trough is a seam about the boundary with the
386 /// face left level, a well drops the whole interior, a boss raises it.
387 /// Collapsing them would give this app controls no other relief host has.
388 fn relief_carve(&mut self, carve: &cce_ui::layout::ReliefCarve) {
389 if self.measure_only {
390 return;
391 }
392 self.control_relief_marks.push(self.rects.len());
393 self.control_reliefs.push(ControlCarve::Step(*carve));
394 }
395
396 fn section_relief(&mut self, f: &cce_ui::layout::SectionFrame) -> bool {
397 // Focused sections keep the legacy green outline (the ctrl-nav feedback);
398 // relief-off styling keeps the outline everywhere.
399 if f.focused || !cce_ui::layout::control_relief() {
400 return false;
401 }
402 if !self.measure_only {
403 self.reliefs.push(((f.x, f.y, f.w, f.h), f.tab));
404 }
405 true
406 }
407
408 fn push_clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
409 if self.measure_only { return; }
410 let clip = if let Some(&parent_clip) = self.clip_stack.last() {
411 let cx = x.max(parent_clip[0]);
412 let cy = y.max(parent_clip[1]);
413 let cw = (x + w).min(parent_clip[0] + parent_clip[2]) - cx;
414 let ch = (y + h).min(parent_clip[1] + parent_clip[3]) - cy;
415 [cx, cy, cw.max(0.0), ch.max(0.0)]
416 } else {
417 [x, y, w, h]
418 };
419 self.clip_stack.push(clip);
420 }
421
422 fn pop_clip_rect(&mut self) {
423 if self.measure_only { return; }
424 self.clip_stack.pop();
425 }
426 }
427
428
429 /// The inset from a section well's rim to the app's own content, on top of
430 /// `section_padding()`. cce-ui's `SectionContext` lays its title tab, its
431 /// grid and a VStack's widgets out `2 * padding + DEFAULT_MARGIN_X` in, and
432 /// `finish()` lands the well's wall `padding + DEFAULT_MARGIN_X` past the
433 /// content — a rect the app places inside a well has to match that number or
434 /// sit off the toolkit's own geometry. A well is this app's pane, so this is
435 /// where the pane rung belongs; TODO(style): become `plate_padding()` once
436 /// SectionContext reads the rung instead of its literal.
437 pub fn section_margin() -> f32 {
438 cce_ui::layout::SectionContext::<PageContent>::DEFAULT_MARGIN_X
439 }
440
441 /// A dim label / bright value pair on one line (the shared details idiom).
442 pub fn section_kv_row(sc: &mut cce_ui::layout::SectionContext<'_, PageContent>, label: &str, value: &str, value_color: [f32; 4]) {
443 let mut y = sc.content_y;
444 if y > sc.content_start_y {
445 y += sc.row_gap;
446 }
447 let lx = sc.ax(section_margin());
448 sc.pc.text(label, lx, y, 12.0, [0.53, 0.53, 0.60, 1.0]);
449 sc.pc.text(value, lx + 130.0, y, 12.0, value_color);
450 sc.content_y = y + 18.0;
451 for h in &mut sc.grid.col_heights {
452 *h = sc.content_y;
453 }
454 }
455
456 /// A hairline separating a well's zones.
457 pub fn section_divider(sc: &mut cce_ui::layout::SectionContext<'_, PageContent>) {
458 let y = sc.content_y + sc.row_gap + 4.0;
459 let x = sc.ax(section_margin());
460 let w = sc.cw - 2.0 * (sc.padding() + section_margin());
461 sc.pc.rect([1.0, 1.0, 1.0, 0.06], x, y, w, 1.0);
462 sc.content_y = y + 5.0;
463 for h in &mut sc.grid.col_heights {
464 *h = sc.content_y;
465 }
466 }
467
468 pub trait SectionContextExt {
469 fn button(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32, bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4], action: AppAction);
470 fn button_left(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32, bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4], action: AppAction);
471 /// [`PageContent::button_icon`] inside a section — `label` is the fallback
472 /// for a missing icon set, `alpha` dims the glyph.
473 #[allow(clippy::too_many_arguments)]
474 fn button_icon(&mut self, icon: &str, label: &str, x: f32, y: f32, w: f32, h: f32, bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4], alpha: f32, action: AppAction);
475 }
476
477 impl<'a> SectionContextExt for cce_ui::layout::SectionContext<'a, PageContent> {
478 fn button(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32, bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4], action: AppAction) {
479 self.pc.button(label, x, y, w, h, bg, hover_bg, label_color, action);
480 self.content_y = self.content_y.max(y + h);
481 for height in &mut self.grid.col_heights {
482 *height = height.max(self.content_y);
483 }
484 }
485
486 fn button_left(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32, bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4], action: AppAction) {
487 self.pc.button_left(label, x, y, w, h, bg, hover_bg, label_color, action);
488 self.content_y = self.content_y.max(y + h);
489 for height in &mut self.grid.col_heights {
490 *height = height.max(self.content_y);
491 }
492 }
493
494 fn button_icon(&mut self, icon: &str, label: &str, x: f32, y: f32, w: f32, h: f32, bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4], alpha: f32, action: AppAction) {
495 self.pc.button_icon(icon, label, x, y, w, h, bg, hover_bg, label_color, alpha, action);
496 self.content_y = self.content_y.max(y + h);
497 for height in &mut self.grid.col_heights {
498 *height = height.max(self.content_y);
499 }
500 }
501 }
502
503
504
505
506