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 }