git.lucas.co / cce-system-interface
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