git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

src/pages/mod.rs (11.9K)

  1 pub mod audio;
  2 pub mod bluetooth;
  3 pub mod browser;
  4 pub mod default_apps;
  5 pub mod network;
  6 pub mod storage;
  7 pub mod system_info;
  8 pub mod timers;
  9 pub mod processes;
 10 pub mod services;
 11 pub mod accounts;
 12 pub mod packages;
 13 pub mod notifications;
 14 pub mod power;
 15 
 16 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 17 pub enum Page {
 18     Accounts,
 19     Audio,
 20     Bluetooth,
 21     Browser,
 22     DefaultApps,
 23     Network,
 24     Notifications,
 25     Power,
 26     Storage,
 27     System,
 28     Timers,
 29     Processes,
 30     Services,
 31     Packages,
 32 }
 33 
 34 impl Page {
 35     pub const ALL: [Page; 14] = [
 36         Page::Accounts,
 37         Page::Audio,
 38         Page::Bluetooth,
 39         Page::Browser,
 40         Page::DefaultApps,
 41         Page::Network,
 42         Page::Notifications,
 43         Page::Packages,
 44         Page::Power,
 45         Page::Processes,
 46         Page::Services,
 47         Page::Storage,
 48         Page::System,
 49         Page::Timers,
 50     ];
 51 
 52     pub fn label(self) -> &'static str {
 53         match self {
 54             Page::Accounts => "Accounts",
 55             Page::Audio => "Audio",
 56             Page::Bluetooth => "Bluetooth",
 57             Page::Browser => "Browser",
 58             Page::DefaultApps => "Default Apps",
 59             Page::Network => "Network",
 60             Page::Notifications => "Notifications",
 61             Page::Power => "Power",
 62             Page::Storage => "Storage",
 63             Page::System => "System",
 64             Page::Timers => "Timers",
 65             Page::Processes => "Processes",
 66             Page::Services => "Services",
 67             Page::Packages => "Packages",
 68         }
 69     }
 70 
 71     pub fn icon(self) -> &'static str {
 72         ""
 73     }
 74 
 75     pub fn index(self) -> usize {
 76         Page::ALL.iter().position(|&p| p == self).unwrap()
 77     }
 78 }
 79 
 80 pub trait AppPage {
 81     /// Per-section event/nav widget groups (Phase 6w: SectionContainer dissolved). One
 82     /// inner Vec per section — same count and order as the old section containers (the
 83     /// outer length drives the `sec_focused` flags) — holding the widgets that used to
 84     /// hang under that section's container, in the old link order. The widgets dispatch
 85     /// directly as propagate roots and the groups drive the app-side ctrl-nav.
 86     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>>;
 87 
 88     fn view(
 89         &mut self,
 90         cx: f32,
 91         cy: f32,
 92         cw: f32,
 93         ch: f32,
 94         root_focused: bool,
 95         sec_focused: &[bool],
 96         layout: &mut cce_ui::layout::PageFlow,
 97         ctx: &mut cce_ui::context::UiContext,
 98     ) -> crate::app::PageContent;
 99 
100     fn propagate_widget_changes(&mut self, actions: &mut Vec<crate::app::AppAction>, ctx: &mut cce_ui::context::UiContext);
101 
102     fn handle_pointer_move(
103         &mut self,
104         _lx: f32,
105         _ly: f32,
106         _actions: &mut Vec<crate::app::AppAction>,
107         _ctx: &mut cce_ui::context::UiContext,
108     ) -> bool {
109         false
110     }
111 
112     fn handle_pointer_down(&mut self, _lx: f32, _ly: f32, _ctx: &mut cce_ui::context::UiContext) -> bool {
113         false
114     }
115 
116     fn handle_pointer_up(&mut self, _ctx: &mut cce_ui::context::UiContext) -> bool {
117         false
118     }
119 
120     /// Extra top-level event-dispatch roots beyond the section containers: the per-row
121     /// widgets that used to hang under a `List`'s ScrollBox (dissolved — the rows now
122     /// dispatch directly; `Adapted` hit-gates presses/wheel so misses fall through).
123     fn extra_dispatch_roots(&mut self) -> Vec<cce_ui::widget::WidgetId> {
124         Vec::new()
125     }
126 
127     /// Refresh the extra dispatch roots' registrations (id-rooted router: roots resolve
128     /// through the registry). The row Vecs are rebuilt on data refresh, so pages
129     /// re-register the current allocations right before each dispatch — the same
130     /// liveness contract the pointer-rooted dispatch had. Default no-op.
131     fn register_extra_dispatch_roots(&mut self, _ctx: &mut cce_ui::context::UiContext) {}
132 
133     /// The dissolved inner lists' wheel (`ScrollBox::mouse_wheel`, hit-scoped). Runs after
134     /// the widget dispatch and before the manual whole-page scroll fallback — the legacy
135     /// "inner ScrollBoxes take the wheel first" order.
136     fn handle_mouse_wheel(&mut self, _delta: &cce_ui::widget::MouseScrollDelta, _lx: f32, _ly: f32) -> bool {
137         false
138     }
139 
140     /// The dissolved inner lists' hover/focus-scoped keyboard scrolling
141     /// (`ScrollBox::keyboard_input`). Runs before the whole-page scroll-key fallback.
142     fn handle_key_input(&mut self, _event: &cce_ui::widget::KeyEvent) -> bool {
143         false
144     }
145 
146     /// Per-frame upkeep for the dissolved inner lists. A `ScrollRegion`'s
147     /// wheel only moves its target; its `tick` is what glides (wheel) or
148     /// coasts (trackpad flick) the drawn offset there — a page hosting one
149     /// must pump it here or the list freezes after the first notch. True =
150     /// the page's geometry changed (the host re-lays it out).
151     fn tick(&mut self, _dt: f32) -> bool {
152         false
153     }
154 
155     /// The page's FRAMELESS inner lists, as laid out this frame. Their
156     /// scrollbars ride their centre lines behind the plate: a framed list
157     /// lays its idle copy under its own bg (`ScrollRegion::push_prims`), but
158     /// a frameless one sits straight on a well carved into the root plate,
159     /// so the host draws its idle copy before the root plate
160     /// (`push_scrollbar_prims`). The fore copy is the page's, after its rows
161     /// (`push_scrollbar_fore`). Default: none.
162     fn frameless_lists(&self) -> Vec<&cce_ui::widget::ScrollRegion> {
163         Vec::new()
164     }
165 }
166 
167 
168 
169 #[cfg(test)]
170 mod list_clip_tests {
171     use super::*;
172     use cce_ui::widget::ScrollRegion;
173 
174     fn render(page: &mut dyn AppPage, ui: &mut cce_ui::context::UiContext) -> crate::app::PageContent {
175         let mut layout = cce_ui::layout::PageFlow::new();
176         page.view(10.0, 20.0, 820.0, 640.0, false, &[false; 4], &mut layout, ui)
177     }
178 
179     /// A list sits inside its section's clip exactly when the clip it
180     /// pushes for its rows survives whole. Clips intersect, so a list
181     /// sticking out past the section's content box — laid out at
182     /// `left + margin` — gets its row clip narrowed to that box, the cut
183     /// that took the edges off the framed lists and the first letter off
184     /// the Processes summary. Row texts carry the clip in their bounds
185     /// (`[x0, y0, x1, y1]`), row buttons beside them (`[x, y, w, h]`).
186     fn assert_list_inside(name: &str, pc: &crate::app::PageContent, list: &ScrollRegion) {
187         let text_clips = pc.texts.iter().filter_map(|t| t.6);
188         let button_clips = pc.buttons.iter().filter_map(|b| b.2).map(|[x, y, w, h]| [x, y, x + w, y + h]);
189         let widest = text_clips
190             .chain(button_clips)
191             .filter(|b| b[1] >= list.y - 0.5 && b[3] <= list.y + list.h + 0.5)
192             .max_by(|a, b| (a[2] - a[0]).total_cmp(&(b[2] - b[0])))
193             .unwrap_or_else(|| panic!("{name}: no row drawn under the list's clip"));
194         let (l, r) = (list.x, list.x + list.w);
195         assert!(
196             (widest[0] - l).abs() < 0.5 && (widest[2] - r).abs() < 0.5,
197             "{name}: rows clip to [{}, {}], the list spans [{l}, {r}]",
198             widest[0],
199             widest[2]
200         );
201     }
202 
203     /// Every inner list's scrollbar is the DE's one design: it rides the
204     /// list's centre line, behind the plate until a scroll raises it
205     /// (sinking IS centring for a `ScrollRegion`). The frameless ones are
206     /// handed to the host, whose idle copy goes under the root plate they
207     /// are carved into; a framed one lays its own under its bg.
208     #[test]
209     fn every_inner_list_rides_its_centre_line_behind_the_plate() {
210         let mut ui = cce_ui::context::UiContext::new();
211         let app = crate::app::AppState::new(&mut ui);
212         let lists: [(&str, &ScrollRegion); 7] = [
213             ("accounts", &app.accounts.list),
214             ("network", &app.network.wifi_list),
215             ("packages installed", &app.packages.installed_list),
216             ("packages updates", &app.packages.updates_list),
217             ("processes", &app.processes.cpu_list),
218             ("services", &app.services.list),
219             ("timers", &app.timers.list),
220         ];
221         for (name, list) in lists {
222             assert!(list.sink_behind, "{name}: its bar sinks behind the plate");
223         }
224         for page in [&app.services as &dyn AppPage, &app.timers, &app.processes, &app.packages] {
225             assert_eq!(page.frameless_lists().len(), 1, "a frameless list is handed to the host");
226         }
227         for page in [&app.accounts as &dyn AppPage, &app.network] {
228             assert!(page.frameless_lists().is_empty(), "a framed list lays its own idle copy");
229         }
230     }
231 
232     #[test]
233     fn services_list_inside_its_section() {
234         let mut ui = cce_ui::context::UiContext::new();
235         let mut s = services::ServicesState::new(&mut ui);
236         s.loaded = true;
237         s.services = (0..5)
238             .map(|i| services::ServiceInfo {
239                 name: format!("s{i}.service"),
240                 description: "d".into(),
241                 active_state: "active".into(),
242                 sub_state: "running".into(),
243                 is_system: true,
244             })
245             .collect();
246         let pc = render(&mut s, &mut ui);
247         assert_list_inside("services", &pc, &s.list);
248     }
249 
250     #[test]
251     fn timers_list_inside_its_section() {
252         let mut ui = cce_ui::context::UiContext::new();
253         let mut s = timers::TimersState::new(&mut ui);
254         s.loaded = true;
255         s.timers = (0..5)
256             .map(|i| timers::TimerInfo {
257                 unit: format!("t{i}.timer"),
258                 activates: format!("t{i}.service"),
259                 next_usec: None,
260                 last_usec: None,
261                 active: true,
262                 file_state: "enabled".into(),
263                 is_system: true,
264                 editable: false,
265             })
266             .collect();
267         let pc = render(&mut s, &mut ui);
268         assert_list_inside("timers", &pc, &s.list);
269     }
270 
271     #[test]
272     fn packages_list_inside_its_section() {
273         let mut ui = cce_ui::context::UiContext::new();
274         let mut s = packages::PackagesState::new(&mut ui);
275         s.loaded = true;
276         s.installed = (0..5)
277             .map(|i| packages::PackageInfo { name: format!("p{i}"), version: "1.0".into(), ..Default::default() })
278             .collect();
279         let pc = render(&mut s, &mut ui);
280         assert_list_inside("packages", &pc, &s.installed_list);
281     }
282 
283     #[test]
284     fn network_list_inside_its_section() {
285         let mut ui = cce_ui::context::UiContext::new();
286         let mut s = network::NetworkState::new(&mut ui);
287         s.loaded = true;
288         s.wifi_enabled = true;
289         s.available = (0..3)
290             .map(|i| network::WifiNetwork { ssid: format!("net{i}"), signal: 50, secured: true, in_use: i == 0 })
291             .collect();
292         let pc = render(&mut s, &mut ui);
293         assert_list_inside("network", &pc, &s.wifi_list);
294     }
295 
296     #[test]
297     fn accounts_list_inside_its_section() {
298         let mut ui = cce_ui::context::UiContext::new();
299         let mut s = accounts::AccountsState::new(&mut ui);
300         s.loaded = true;
301         s.accounts = vec![serde_json::from_str(
302             r#"{"email":"a@example.org","imap":"imap.example.org:993","smtp":"smtp.example.org:465","is_default":true,"password":""}"#,
303         )
304         .unwrap()];
305         let pc = render(&mut s, &mut ui);
306         assert_list_inside("accounts", &pc, &s.list);
307     }
308 
309     #[test]
310     fn processes_list_inside_its_section() {
311         let mut ui = cce_ui::context::UiContext::new();
312         let mut s = processes::ProcessesState { loaded: true, ..Default::default() };
313         s.processes = (0..5)
314             .map(|i| processes::ProcessRow {
315                 pid: i.to_string(),
316                 cpu: "1.0".into(),
317                 mem_pct: "1.0".into(),
318                 rss_kb: 1024,
319                 command: "p".into(),
320                 watts: None,
321                 wakeups: None,
322             })
323             .collect();
324         let pc = render(&mut s, &mut ui);
325         assert_list_inside("processes", &pc, &s.cpu_list);
326     }
327 }