system settings
git clone https://git.lucas.co/cce-system-interface.git
src/main.rs (55.5K)
1 use cce_ui::widget::Handle;
2 use cce_ui::widget::hover_animation;
3 use cce_ui::cosmic_text::{Buffer, FontSystem};
4
5 use cce_settings::app::{AppAction, AppState, ControlCarve};
6 use cce_settings::pages::{self, Page};
7 mod input_handler;
8 mod renderer;
9 mod scroll_bar;
10
11 fn make_text_buffer_with_font(
12 fs: &mut FontSystem,
13 text: &str,
14 size: f32,
15 font: Option<&str>,
16 _sans_fallback: &str,
17 _serif_fallback: &str,
18 _mono_fallback: &str,
19 ) -> Buffer {
20 cce_ui::backend::get_text_buffer(fs, text, size, font)
21 }
22
23 #[allow(dead_code)]
24 struct AppWidget {
25 x: f32, y: f32, w: f32, h: f32,
26 color: [f32; 4],
27 hover_color: [f32; 4],
28 hovering: bool,
29 radius: f32,
30 corners: (bool, bool, bool, bool),
31 }
32
33 /// A cce-icons glyph ready to draw: window coordinates (scroll already
34 /// applied), the image's alpha, and the clip it was placed under
35 /// (`[x, y, w, h]`) — an image has no geometry to trim, only a clip.
36 #[derive(Clone, Copy, Debug)]
37 pub(crate) struct PlacedIcon {
38 pub image: u32,
39 pub x: f32,
40 pub y: f32,
41 pub w: f32,
42 pub h: f32,
43 pub alpha: f32,
44 pub clip: Option<[f32; 4]>,
45 }
46
47 fn draw_placed_icon(pc: &mut cce_ui::scene::paint::PaintCtx, icon: &PlacedIcon) {
48 use cce_ui::scene::layout::Rect;
49 let rect = Rect { x: icon.x, y: icon.y, width: icon.w, height: icon.h };
50 match icon.clip {
51 Some([x, y, w, h]) => pc.clip(Rect { x, y, width: w, height: h }, |pc| pc.image(icon.image, rect, icon.alpha)),
52 None => pc.image(icon.image, rect, icon.alpha),
53 }
54 }
55
56 static INITIAL_PAGE_INDEX: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
57
58 struct SystemInterface {
59 app: AppState,
60 font_system: FontSystem,
61 widgets: Vec<AppWidget>,
62 // (content, font_size, x, y, color, font, bounds) — the frame's text, emitted as
63 // display-list Text prims (scroll shift, search dim/highlight, and viewport clamps
64 // already applied by rebuild_layout).
65 texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
66 // Popover + context-menu content, drawn INTO the frame on top of everything (Phase 6t —
67 // no engine xdg popup). Separate from widgets/texts so the wheel fast-path never
68 // scrolls them.
69 popover_widgets: Vec<AppWidget>,
70 popover_texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
71 /// Popover-layer inset plates (window coords, scroll already applied) —
72 /// the dropdown's grown-trigger surface via the inset_plate hook.
73 popover_control_reliefs: Vec<ControlCarve>,
74 /// Per popover carve, the index into `popover_widgets` it precedes
75 /// (`PageContent::control_relief_marks`) — display_list slots each carve
76 /// back between the rects the widget drew before and after it.
77 popover_control_relief_marks: Vec<usize>,
78 page_buttons: Vec<(Handle<cce_ui::widget::Adapted<cce_ui::widget::Button>>, AppAction)>,
79
80 sidebar_width: f32,
81 header_height: f32,
82 status_height: f32,
83
84 cursor_x: f32,
85 cursor_y: f32,
86
87 rx_audio: std::sync::mpsc::Receiver<pages::audio::AudioState>,
88 rx_network: std::sync::mpsc::Receiver<pages::network::NetworkState>,
89 rx_bluetooth: std::sync::mpsc::Receiver<pages::bluetooth::BluetoothState>,
90 rx_power: std::sync::mpsc::Receiver<pages::power::PowerFacts>,
91 pub rx_processes: std::sync::mpsc::Receiver<pages::processes::ProcessesState>,
92 rx_system: std::sync::mpsc::Receiver<pages::system_info::SystemInfo>,
93 rx_storage: std::sync::mpsc::Receiver<pages::storage::StorageInfo>,
94 rx_notifications: std::sync::mpsc::Receiver<pages::notifications::NotificationsConfig>,
95 rx_browser: std::sync::mpsc::Receiver<pages::browser::BrowserConfig>,
96 rx_services: std::sync::mpsc::Receiver<Vec<pages::services::ServiceInfo>>,
97 rx_default_apps: std::sync::mpsc::Receiver<pages::default_apps::DefaultAppsInfo>,
98 rx_timers: std::sync::mpsc::Receiver<Vec<pages::timers::TimerInfo>>,
99 rx_accounts: std::sync::mpsc::Receiver<pages::accounts::AccountsSnapshot>,
100 tx_backup: std::sync::mpsc::Sender<pages::storage::StorageMessage>,
101 rx_backup: std::sync::mpsc::Receiver<pages::storage::StorageMessage>,
102 rx_packages: std::sync::mpsc::Receiver<pages::packages::PackagesState>,
103 tx_update: std::sync::mpsc::Sender<pages::packages::PackagesMessage>,
104 rx_update: std::sync::mpsc::Receiver<pages::packages::PackagesMessage>,
105
106
107 scale_factor: f64,
108 width: u32,
109 height: u32,
110 needs_rebuild: bool,
111 /// The page's DRAWN offset — `page_scroll_motion` glides it (wheel) or
112 /// coasts it (trackpad flick) by shifting the cached geometry in place;
113 /// direct writes (thumb drag, keyboard, search jump, clamp) are adopted
114 /// by the motion on its next step.
115 scroll_y: f32,
116 page_scroll_motion: cce_ui::widget::ScrollMotion,
117 max_scroll_y: f32,
118 scrollable_widgets_start_idx: usize,
119 scrollable_text_items_start_idx: usize,
120 scrollable_buttons_start_idx: usize,
121 last_scroll_y: f32,
122 // SectionContainer DISSOLVED (Phase 6w): section-level keyboard focus is this index
123 // (single-slot with the global widget focus — descending clears it); the per-section
124 // widget groups come from AppPage::section_widgets each time they're needed.
125 focused_section: Option<usize>,
126 // The page the last `rebuild_layout` actually laid out. Registration is a side effect
127 // of the view pass (`render_widget`), and `clear_hierarchy` wipes the registry each
128 // rebuild — so between a page switch and the next rebuild, the NEW page's
129 // `section_widgets()` ids are not registered and every event to them is dropped with
130 // a router warning. `dispatch_page_event` suppresses dispatch across that gap.
131 laid_out_page: Option<Page>,
132 /// The rect of the focused widget at the start of a view pass: the page's
133 /// buttons are per-rebuild clones, so the focused id dies with every
134 /// rebuild and the clone at the same rect takes the focus back (lit as it
135 /// is collected, focused at the pass's tail).
136 refocus_rect: Option<(f32, f32, f32, f32)>,
137 /// What the last rebuild's widgets claimed for text input (an editing
138 /// TextBox, `cce_ui::text_input::capture`). Claimed again every frame:
139 /// a frame that only replays the cached layout paints no widget, and
140 /// would otherwise read as the field closing.
141 text_claim: Option<[f32; 4]>,
142 page_dropdown: Handle<cce_ui::widget::Adapted<cce_ui::widget::input::Dropdown>>,
143 // Switcher + Page DISSOLVED (Phase 6u): the current page is app.current_page, page
144 // scroll is scroll_y/max_scroll_y, and the page scrollbar is this app-owned widget
145 // (rendered into the window assembly, evented directly). content_h feeds it — the
146 // window pass reads last frame's value, exactly as the legacy Page did.
147 page_scroll_bar: Handle<cce_ui::widget::Adapted<crate::scroll_bar::ScrollBar>>,
148 content_h: f32,
149 // Section wells: body box + title tab (page coords, pre-scroll) — carved by
150 // display_list.
151 page_reliefs: Vec<((f32, f32, f32, f32), Option<(f32, f32, f32, f32)>)>,
152 /// Per page carve, the index into `widgets` it precedes — see
153 /// `popover_control_relief_marks`; the page layer interleaves the same way.
154 page_control_relief_marks: Vec<usize>,
155 /// Control troughs from `PageContent::control_reliefs` (page coords,
156 /// pre-scroll) — each carved as a flush inset plate after the section
157 /// wells — with the clip its control was emitted under (`[x, y, w, h]`,
158 /// page coords, pre-scroll). A button's plate needs it: the renderer
159 /// clamps the button's quad to its list, but a carve is drawn whole, so a
160 /// row half-scrolled out of a list left its plate standing past the edge.
161 page_control_reliefs: Vec<(ControlCarve, Option<[f32; 4]>)>,
162 /// The page's glyphs, drawn under the page clip and each its own: a
163 /// button's icon face, and the page's own (`PageContent::icon`: a sort
164 /// chevron, an in-use check). A flat host draws no images at all
165 /// otherwise: `all_quads` carries quads and the text list carries labels,
166 /// and an icon is neither.
167 page_icon_images: Vec<PlacedIcon>,
168 /// Glyphs the popovers drew (the expanded Dropdown's chevron), drawn over
169 /// the popover plates.
170 popover_icon_images: Vec<PlacedIcon>,
171 /// Glyphs of the window chrome (the page dropdown's arrow), window
172 /// coordinates, drawn over the chrome outside the page clip.
173 // root plate container + StatusBar DISSOLVED (Phase 6s): the window plate and the status
174 // bar are emitted as tuples in rebuild_layout.
175 sans_serif_family: String,
176 serif_family: String,
177 monospace_family: String,
178 /// The page on screen, as the page workers see it (`watchers::PageWatch`).
179 current_page_shared: tokio::sync::watch::Sender<u8>,
180 /// A hash of the last snapshot each page worker sent, so an unchanged
181 /// refresh is dropped instead of rebuilding the page (`fresh_snapshot`).
182 seen_snapshots: std::collections::HashMap<&'static str, u64>,
183 sender: calloop::channel::Sender<AppAction>,
184 ui_context: cce_ui::context::UiContext,
185 scroll_logs: Vec<String>,
186 search_open: bool,
187 search_query: String,
188 search_box: Handle<cce_ui::widget::Adapted<cce_ui::widget::input::TextBox>>,
189
190 }
191
192 /// One collected control carve as the real relief prim it stands for.
193 fn emit_control_carve(pc: &mut cce_ui::scene::paint::PaintCtx, carve: ControlCarve) {
194 use cce_ui::scene::layout::Rect;
195 match carve {
196 ControlCarve::Plate { x, y, w, h, radius, depth, color, tint } => match tint {
197 Some(t) => pc.inset_plate_tinted(
198 Rect { x, y, width: w, height: h },
199 (radius, radius, radius, radius),
200 cce_ui::scene::Material::face(color).as_ref(),
201 depth,
202 t,
203 ),
204 None => pc.inset_plate(
205 Rect { x, y, width: w, height: h },
206 (radius, radius, radius, radius),
207 cce_ui::scene::Material::face(color).as_ref(),
208 depth,
209 ),
210 },
211 ControlCarve::Step(c) => pc.carve(&c),
212 }
213 }
214
215 impl cce_ui::engine::Application for SystemInterface {
216 type Message = AppAction;
217
218 fn create(sender: cce_ui::engine::AppSender<Self::Message>) -> Self {
219 // The app keeps calloop's sender; `AppSender` converts into it.
220 let sender: calloop::channel::Sender<Self::Message> = sender.into();
221 cce_ui::scale::set_scale_factor(1.0);
222 // The context owns the widgets; the app keeps their handles.
223 let mut ui_context = cce_ui::context::UiContext::new();
224 let app = AppState::new(&mut ui_context);
225
226 // ── Background refresh channels ──
227 let initial_page_idx = INITIAL_PAGE_INDEX.load(std::sync::atomic::Ordering::SeqCst);
228 let (current_page_shared, page_rx) = tokio::sync::watch::channel(initial_page_idx as u8);
229 let wake: cce_settings::watchers::Wake = {
230 let sender = sender.clone();
231 std::sync::Arc::new(move || {
232 let _ = sender.send(AppAction::Wake);
233 })
234 };
235
236 let (watchers, tx_backup, rx_backup, tx_update, rx_update) =
237 cce_settings::watchers::spawn_all(page_rx, wake);
238
239 let (sans_family, serif_family, monospace_family, _) = cce_ui::layout::read_preferred_fonts();
240
241 let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
242 let page_dropdown = cce_ui::widget::input::Dropdown::new(pages_names, initial_page_idx)
243 .with_open_upward(true)
244 .with_auto_width(true)
245 // The open menu is the page list alone, sat where the trigger was:
246 // the current page already reads blue in the list, so the trigger
247 // band repeating its title under the rows was noise.
248 .with_menu_replaces_trigger(true);
249 let sidebar_width = 0.0f32;
250
251 let mut app_state = app;
252 app_state.current_page = Page::ALL[initial_page_idx];
253
254 let font_system = cce_ui::create_font_system();
255
256 let search_box = ui_context.insert(
257 cce_ui::widget::input::TextBox::new(String::new())
258 .with_placeholder("Search sections & parameters...")
259 .with_draw_bg_border(false),
260 );
261 let mut this = Self {
262 app: app_state,
263 font_system,
264 widgets: Vec::new(),
265 texts: Vec::new(),
266 popover_widgets: Vec::new(),
267 popover_texts: Vec::new(),
268 popover_control_reliefs: Vec::new(),
269 popover_control_relief_marks: Vec::new(),
270 page_buttons: Vec::new(),
271 sidebar_width,
272 header_height: 0.0,
273 // The page switcher at the toolkit's dropdown height, inset 3px
274 // top and bottom: the bar grows with the control, not the reverse.
275 status_height: cce_ui::layout::dropdown_height() + 6.0,
276 cursor_x: 0.0,
277 cursor_y: 0.0,
278 rx_audio: watchers.rx_audio,
279 rx_network: watchers.rx_network,
280 rx_bluetooth: watchers.rx_bluetooth,
281 rx_power: watchers.rx_power,
282 rx_processes: watchers.rx_processes,
283 rx_default_apps: watchers.rx_default_apps,
284 rx_timers: watchers.rx_timers,
285 rx_system: watchers.rx_system,
286 rx_storage: watchers.rx_storage,
287 rx_notifications: watchers.rx_notifications,
288 rx_browser: watchers.rx_browser,
289 rx_services: watchers.rx_services,
290 rx_accounts: watchers.rx_accounts,
291 tx_backup,
292 rx_backup,
293 rx_packages: watchers.rx_packages,
294 tx_update,
295 rx_update,
296
297
298 scale_factor: 1.0,
299 width: 820,
300 height: 680,
301 needs_rebuild: true,
302 scroll_y: 0.0,
303 page_scroll_motion: cce_ui::widget::ScrollMotion::new(),
304 max_scroll_y: 0.0,
305 scrollable_widgets_start_idx: 0,
306 scrollable_text_items_start_idx: 0,
307 scrollable_buttons_start_idx: 0,
308 last_scroll_y: 0.0,
309 focused_section: None,
310 laid_out_page: None,
311 refocus_rect: None,
312 text_claim: None,
313 page_dropdown: ui_context.insert(page_dropdown),
314 page_scroll_bar: ui_context.insert(crate::scroll_bar::ScrollBar::new()),
315 content_h: 0.0,
316 page_reliefs: Vec::new(),
317 page_control_reliefs: Vec::new(),
318 page_control_relief_marks: Vec::new(),
319 page_icon_images: Vec::new(),
320 popover_icon_images: Vec::new(),
321 sans_serif_family: sans_family,
322 serif_family,
323 monospace_family,
324 current_page_shared,
325 seen_snapshots: std::collections::HashMap::new(),
326 sender,
327 ui_context,
328 scroll_logs: Vec::new(),
329 search_open: false,
330 search_query: String::new(),
331 search_box,
332 };
333 this.app.system_info.sender = Some(this.sender.clone());
334
335 this.rebuild_layout(820.0, 680.0);
336 this.needs_rebuild = true;
337 this
338 }
339
340 fn settings(&self) -> cce_ui::engine::WindowSettings {
341 cce_ui::engine::WindowSettings {
342 title: "CCE System Interface".to_string(),
343 app_id: "cce-system-interface".to_string(),
344 width: 820,
345 height: 680,
346 fullscreen: false,
347 min_size: Some((400, 680)),
348 }
349 }
350
351 fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool) {
352 if let AppAction::Exit = msg {
353 *exit = true;
354 return;
355 }
356 // Wakes the loop so `tick` drains the workers' channels; whether
357 // anything is redrawn is `poll_background_updates`' call.
358 if let AppAction::Wake = msg {
359 return;
360 }
361 self.handle_action(&msg);
362 *needs_rebuild = true;
363 self.needs_rebuild = true;
364 }
365
366 fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
367 if self.needs_rebuild {
368 self.rebuild_layout(self.width as f32, self.height as f32);
369 }
370 self.poll_background_updates();
371 if self.tick_internal(dt) {
372 *needs_rebuild = true;
373 }
374 let mut actions = Vec::new();
375 self.propagate_widget_changes(&mut actions);
376 for action in actions {
377 self.handle_action(&action);
378 }
379 if self.needs_rebuild || self.ui_context.is_dirty() {
380 *needs_rebuild = true;
381 self.needs_rebuild = true;
382 }
383 }
384
385 fn display_list(&mut self, size: cce_ui::engine::LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
386 // Phase 6 single paint path: the whole frame — geometry and text — is this one list.
387 // rebuild_layout flattens the UI into self.widgets/self.texts (scroll shift, search
388 // dim/highlight, and viewport clamps already applied).
389 let (width, height) = (size.width, size.height);
390 if self.needs_rebuild || self.ui_context.is_dirty() || self.width != width as u32 || self.height != height as u32 || self.scale_factor != scale {
391 self.width = width as u32;
392 self.height = height as u32;
393 self.scale_factor = scale;
394 cce_ui::scale::set_scale_factor(scale as f32);
395 self.rebuild_layout(width, height);
396 }
397 use cce_ui::scene::layout::Rect;
398 let mut pc = cce_ui::scene::paint::PaintCtx::new();
399
400 // The page scrollbar straddles the window plate (the designer
401 // parameter-pane treatment) and is emitted fresh EVERY frame — never
402 // baked into the rebuilt layout, so the thumb tracks the wheel fast
403 // path's scrolls, which shift cached geometry without a rebuild and
404 // used to leave the bar frozen until scrolling stopped. Sync first:
405 // a thumb drag drives the page, anything else drives the thumb.
406 if self.ui_context[self.page_scroll_bar].dragging {
407 self.scroll_y = self.ui_context[self.page_scroll_bar].scroll_y;
408 } else {
409 self.ui_context[self.page_scroll_bar].scroll_y = self.scroll_y;
410 }
411 let page_bar = {
412 use cce_ui::widget::WidgetHost;
413 let (bx, by, bw, bh) = self.ui_context[self.page_scroll_bar].rect();
414 self.ui_context[self.page_scroll_bar]
415 .layer_quads(Rect { x: bx, y: by, width: bw, height: bh })
416 };
417 // The idle copy: under the translucent window plate, every frame, so
418 // the bar reads as sunk INTO the window rather than gone, and the
419 // plate occludes it from input. Track and thumb are pills (the
420 // designer look). The fore copy fades in over it, below.
421 for &(r, c) in &page_bar {
422 pc.rounded_rect(r, r.width.min(r.height) * 0.5, (true, true, true, true), c);
423 }
424 // The frameless inner lists' idle copies go under the plate too: they
425 // sit on wells carved into it, so this is the plate behind them. In
426 // page coordinates, so shifted by the page scroll and clipped to the
427 // page view as the wells are. Their fore copies are the pages'.
428 {
429 let view = self.page_view(width, height);
430 let scroll_y = self.scroll_y;
431 for list in self.app.get_current_page().frameless_lists() {
432 let mut shown = list.clone();
433 shown.y -= scroll_y;
434 shown.viewport_y -= scroll_y;
435 pc.clip(view, |pc| shown.push_scrollbar_prims(pc));
436 }
437 }
438
439 // One glass slab (data-editor's idiom): the root plate, with the status
440 // bar carved into it as a step — everything else paints on top. The
441 // standard spec (cce-ui `PlateSpec::window`) in this app's own dark
442 // green tint at the DE root opacity: a deliberate deviation from the
443 // DE root colour, and the one thing here that is not the standard.
444 {
445 let mut plate = cce_ui::color::to_linear([0x0a as f32 / 255.0, 0x1a as f32 / 255.0, 0x0e as f32 / 255.0, 1.0]);
446 if plate[3] > 0.001 {
447 plate[3] = cce_ui::color::root_plate_opacity();
448 }
449 pc.plate_spec(
450 &cce_ui::scene::paint::PlateSpec::window(width, height)
451 .with_material(cce_ui::scene::Material::opaque(plate)),
452 );
453 let sb_h = self.status_height;
454 let depth = cce_ui::layout::bar_wall_width().min(sb_h * 0.6);
455 pc.recess_edges(
456 Rect { x: 0.0, y: height - sb_h, width, height: sb_h },
457 (0.0, 0.0, 0.0, 0.0),
458 depth,
459 (true, false, false, false),
460 );
461 }
462
463 // The page's rects and the control carves the flat bridge offered
464 // (a Dropdown's inset plate, a TextBox's well, a Toggle's steps, the
465 // buttons' faces), replayed in the widgets' OWN order — each carve
466 // just before the rect it was claimed ahead of, so a TextBox's
467 // selection highlight and caret land on its well instead of under
468 // its walls. Carves clip to the page viewport like the wells; the
469 // rects were already clamped to it when collected.
470 {
471 let view = self.page_view(width, height);
472 let scroll_y = self.scroll_y;
473 let mut carves = self
474 .page_control_reliefs
475 .iter()
476 .copied()
477 .zip(self.page_control_relief_marks.iter().copied())
478 .peekable();
479 let mut emit_pending = |pc: &mut cce_ui::scene::paint::PaintCtx, upto: usize| {
480 while carves.peek().map_or(false, |&(_, mark)| mark <= upto) {
481 let ((carve, clip), _) = carves.next().unwrap();
482 pc.clip(view, |pc| match clip {
483 Some([x, y, w, h]) => pc.clip(Rect { x, y: y - scroll_y, width: w, height: h }, |pc| {
484 emit_control_carve(pc, carve.shifted_y(-scroll_y))
485 }),
486 None => emit_control_carve(pc, carve.shifted_y(-scroll_y)),
487 });
488 }
489 };
490 for (i, w) in self.widgets.iter().enumerate() {
491 emit_pending(&mut pc, i);
492 let color = if w.hovering { w.hover_color } else { w.color };
493 let rect = Rect { x: w.x, y: w.y, width: w.w, height: w.h };
494 if w.radius > 0.1 {
495 pc.rounded_rect(rect, w.radius, w.corners, color);
496 } else {
497 pc.quad(rect, color);
498 }
499 }
500 emit_pending(&mut pc, usize::MAX);
501 }
502 // The section wells, carved after the page's flat quads so the walls shade
503 // the fills they cross (the designer relief order), clipped to the page
504 // viewport so a scrolled-off well can't shade the status bar or search row.
505 if !self.page_reliefs.is_empty() {
506 let view = self.page_view(width, height);
507 let r = 20.0f32;
508 let scroll_y = self.scroll_y;
509 pc.clip(view, |pc| {
510 for &((cx, cy, cw, ch), tab) in &self.page_reliefs {
511 let cy = cy - scroll_y;
512 let depth = cce_ui::layout::bevel_width().min(ch * 0.2);
513 match tab {
514 Some((tx, ty, tw, th)) => {
515 // The designer union carve: the title tab bottom-open, one
516 // piece owning the whole right run so its corners are real
517 // turns, a left piece carrying the left wall — pieces
518 // extend past their interior seam by `depth` so the walls
519 // crossfade there instead of notching — and the throat's
520 // inside corner rounded by a concave fillet.
521 let ty = ty - scroll_y;
522 let rt = r.min(th * 0.45);
523 let throat_r = tx + tw;
524 let rho = 10.0f32; // designer SECTION_FILLET_R
525 let body_lr = |x_run: f32, pc: &mut cce_ui::scene::paint::PaintCtx| {
526 pc.recess_edges(
527 Rect { x: x_run, y: cy, width: cx + cw - x_run, height: ch },
528 (0.0, r, r, 0.0),
529 depth,
530 (true, true, true, false),
531 );
532 pc.recess_edges(
533 Rect { x: cx, y: cy, width: x_run + depth - cx, height: ch },
534 (0.0, 0.0, 0.0, r),
535 depth,
536 (false, false, true, true),
537 );
538 };
539 if cx + cw > throat_r + 2.0 * rho {
540 // Filleted throat: the tab's right wall ends at the
541 // fillet's vertical tangent, a left-only bridge
542 // carries the left wall across the fillet span.
543 pc.recess_edges(
544 Rect { x: tx, y: ty, width: tw, height: (cy - rho) - ty + depth },
545 (rt, rt, 0.0, 0.0),
546 depth,
547 (true, true, false, true),
548 );
549 pc.recess_edges(
550 Rect { x: tx, y: cy - rho, width: tw, height: rho + depth },
551 (0.0, 0.0, 0.0, 0.0),
552 depth,
553 (false, false, false, true),
554 );
555 body_lr(throat_r + rho - depth, pc);
556 pc.concave_fillet(throat_r + rho, cy - rho, rho, depth, std::f32::consts::FRAC_PI_2, false);
557 } else if cx + cw > throat_r + 0.5 {
558 // Too narrow for the fillet: the plain square throat.
559 pc.recess_edges(
560 Rect { x: tx, y: ty, width: tw, height: (cy - ty) + depth },
561 (rt, rt, 0.0, 0.0),
562 depth,
563 (true, true, false, true),
564 );
565 body_lr(throat_r - depth, pc);
566 } else {
567 // The tab spans the body: no top wall at all.
568 pc.recess_edges(
569 Rect { x: tx, y: ty, width: tw, height: (cy - ty) + depth },
570 (rt, rt, 0.0, 0.0),
571 depth,
572 (true, true, false, true),
573 );
574 pc.recess_edges(
575 Rect { x: cx, y: cy, width: cw, height: ch },
576 (0.0, 0.0, r, r),
577 depth,
578 (false, true, true, true),
579 );
580 }
581 }
582 None => {
583 pc.recess_edges(
584 Rect { x: cx, y: cy, width: cw, height: ch },
585 (r, r, r, r),
586 depth,
587 (true, true, true, true),
588 );
589 }
590 }
591 }
592 });
593 }
594
595 // The page's glyphs — button icon faces and the page's own — over
596 // its quads and carves, under the page viewport and each under the
597 // clip it was placed with, which is what cuts a half-scrolled list
598 // row's glyph at the list edge.
599 if !self.page_icon_images.is_empty() {
600 let view = self.page_view(width, height);
601 pc.clip(view, |pc| {
602 for icon in &self.page_icon_images {
603 draw_placed_icon(pc, icon);
604 }
605 });
606 }
607
608 // The window chrome, as it paints itself: the page dropdown in the status bar
609 // and, while open, the search box in its band.
610 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.ui_context[self.page_dropdown], &mut pc);
611 if self.search_open {
612 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.ui_context[self.search_box], &mut pc);
613 }
614
615 // The page scrollbar's fore copy: over the page content at the
616 // raise's fade, while a scroll or drag holds it up and as it sinks
617 // (popovers still stack above it).
618 let fade = self.ui_context[self.page_scroll_bar].fade();
619 if fade > 0.001 {
620 for &(r, mut c) in &page_bar {
621 c[3] *= fade;
622 pc.rounded_rect(r, r.width.min(r.height) * 0.5, (true, true, true, true), c);
623 }
624 }
625
626 cce_ui::widget::hover_animation::post_render_check();
627 if let Some((qx, qy, qw, qh, qc)) = cce_ui::widget::hover_animation::get_quad() {
628 pc.quad(Rect { x: qx, y: qy - self.scroll_y, width: qw, height: qh }, qc);
629 }
630 // Popover rects and the surfaces claimed through the inset_plate hook
631 // (the dropdown's menu plate), replayed in the widget's OWN order:
632 // each carve goes out just before the rect it was claimed ahead of,
633 // so the hovered-row highlight a Dropdown draws after its plate lands
634 // on top of the frosted face instead of underneath it.
635 {
636 let mut carves = self
637 .popover_control_reliefs
638 .iter()
639 .copied()
640 .zip(self.popover_control_relief_marks.iter().copied())
641 .peekable();
642 for (i, w) in self.popover_widgets.iter().enumerate() {
643 while carves.peek().map_or(false, |&(_, mark)| mark <= i) {
644 let (carve, _) = carves.next().unwrap();
645 emit_control_carve(&mut pc, carve);
646 }
647 let rect = Rect { x: w.x, y: w.y, width: w.w, height: w.h };
648 if w.radius > 0.1 {
649 pc.rounded_rect(rect, w.radius, w.corners, w.color);
650 } else {
651 pc.quad(rect, w.color);
652 }
653 }
654 for (carve, _) in carves {
655 emit_control_carve(&mut pc, carve);
656 }
657 }
658 for icon in &self.popover_icon_images {
659 draw_placed_icon(&mut pc, icon);
660 }
661 // The context menu last, over everything: the toolkit's lit plate,
662 // its rows and its labels in the menu font, with the row marks and
663 // page chevrons as cce-icons glyphs.
664 cce_ui::widget::context_menu::paint_with_labels(&mut pc);
665 for (text, font_size, x, y, col, font, bounds) in self.texts.iter().chain(self.popover_texts.iter()) {
666 pc.text_with(
667 text.clone(),
668 *x,
669 *y,
670 *font_size,
671 [
672 (col[0] * 255.0) as u8,
673 (col[1] * 255.0) as u8,
674 (col[2] * 255.0) as u8,
675 ],
676 font.clone(),
677 *bounds,
678 );
679 }
680 if let Some([x, y, w, h]) = self.text_claim {
681 cce_ui::text_input::claim(x, y, w, h);
682 }
683 Some(pc.finish())
684 }
685
686 fn display_list_text(&self) -> bool {
687 true
688 }
689
690 fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
691 Some(&self.ui_context)
692 }
693
694 // The engine ticks the exposed context each loop — this is what drives the
695 // dropdown expand/contract animation frames.
696 fn ui_context_mut(&mut self) -> Option<&mut cce_ui::context::UiContext> {
697 Some(&mut self.ui_context)
698 }
699
700 // Page widgets are laid out unscrolled and drawn `scroll_y` higher (the
701 // popover replay in `rebuild_layout` subtracts it); the chrome dropdown
702 // is in window coords already.
703 fn popover_offset(&self, id: cce_ui::widget::WidgetId) -> (f32, f32) {
704 if id == self.page_dropdown.id() {
705 (0.0, 0.0)
706 } else {
707 (0.0, -self.scroll_y)
708 }
709 }
710
711 fn clear_color(&self) -> [f32; 4] {
712 [0.039, 0.102, 0.055, 1.0]
713 }
714
715 fn handle_pointer_move(&mut self, pos: cce_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
716 if self.handle_cursor_moved(pos.x, pos.y) {
717 *needs_rebuild = true;
718 }
719 }
720
721 fn handle_mouse_input(&mut self, button: cce_ui::widget::MouseButton, state: cce_ui::widget::ElementState, pos: cce_ui::engine::LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
722 self.cursor_x = pos.x;
723 self.cursor_y = pos.y;
724 if self.handle_mouse_input_internal(button, state) {
725 *needs_rebuild = true;
726 }
727 None
728 }
729
730 fn handle_mouse_wheel(&mut self, delta: &cce_ui::widget::MouseScrollDelta, pos: cce_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
731 if self.handle_mouse_wheel_internal(delta, pos.x, pos.y) {
732 *needs_rebuild = true;
733 }
734 }
735
736 /// Tab walks the page's plates and wells (cce-ui's navigation in plate
737 /// terms); the section chords in input.kdl keep their own walk.
738 fn plate_navigation(&self) -> bool {
739 true
740 }
741
742 /// The geometry is cached until the next rebuild — a moved focus ring needs
743 /// one. The view pass itself carries the focus across the button clones it
744 /// makes (see `refocus_rect`), on every rebuild.
745 fn focus_stepped(&mut self) {
746 self.needs_rebuild = true;
747 }
748
749 fn handle_key_input(&mut self, event: &cce_ui::widget::KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
750 if self.handle_key_input_internal(event) {
751 *needs_rebuild = true;
752 }
753 None
754 }
755 }
756
757 impl SystemInterface {
758 /// The page viewport in window coords: everything under the header and
759 /// above the status bar (and the search row while it is open).
760 fn page_view(&self, width: f32, height: f32) -> cce_ui::scene::layout::Rect {
761 use cce_ui::scene::layout::Rect;
762 Rect {
763 x: self.sidebar_width,
764 y: self.header_height,
765 width: width - self.sidebar_width,
766 height: (height - self.header_height - self.status_height
767 - if self.search_open { 42.0 } else { 0.0 }).max(0.0),
768 }
769 }
770
771
772 fn tick_internal(&mut self, dt: f32) -> bool {
773 let mut needs_redraw = false;
774 if hover_animation::tick(dt) {
775 needs_redraw = true;
776 }
777 // Raise/sink upkeep for the page scrollbar: true while the post-scroll
778 // hold runs (keeps frames coming so the sink actually renders) and on
779 // the raised flip itself. A redraw re-emits the bar at its new depth —
780 // no layout rebuild needed, display_list draws it fresh each frame.
781 if self.ui_context[self.page_scroll_bar].tick_activity(dt) {
782 needs_redraw = true;
783 }
784 // The page's own wheel glide / flick coast: shifts the cached
785 // geometry like the wheel fast path, no rebuild.
786 if self.tick_page_scroll(dt) {
787 needs_redraw = true;
788 }
789 // The current page's inner lists (their glide/coast lives in the
790 // region's tick): a moved list re-lays the page out.
791 if self.app.get_current_page_mut().tick(dt) {
792 needs_redraw = true;
793 self.needs_rebuild = true;
794 }
795 if self.ui_context.tick(dt) {
796 needs_redraw = true;
797 self.needs_rebuild = true;
798 }
799
800 needs_redraw
801 }
802
803 /// Advance the page's wheel glide / flick coast; true while the offset is
804 /// moving, so the frame loop keeps drawing until it settles.
805 fn tick_page_scroll(&mut self, dt: f32) -> bool {
806 use cce_ui::widget::Bounds;
807 self.page_scroll_motion.reconcile(0.0, self.scroll_y);
808 if !self.page_scroll_motion.is_animating() {
809 return false;
810 }
811 let moved = self.page_scroll_motion.tick(dt, Bounds::max(0.0), Bounds::max(self.max_scroll_y));
812 if moved {
813 self.shift_page_to(self.page_scroll_motion.y.pos());
814 }
815 moved || self.page_scroll_motion.is_animating()
816 }
817
818 /// Whether a page worker's snapshot differs from the last one it sent.
819 /// The workers resend the whole state on every period whether or not
820 /// anything moved; applying an identical one changed nothing on screen
821 /// but rebuilt the page and redrew the window — the settings app sat at
822 /// ~3 fps at rest on its default page. Processes is not checked: its
823 /// refresh also settles pending kills (`killing.clear()`), and its CPU
824 /// column moves every time anyway.
825 fn fresh_snapshot<T: std::fmt::Debug>(&mut self, key: &'static str, snapshot: &T) -> bool {
826 use std::hash::{Hash, Hasher};
827 let mut h = std::collections::hash_map::DefaultHasher::new();
828 format!("{snapshot:?}").hash(&mut h);
829 let hash = h.finish();
830 self.seen_snapshots.insert(key, hash) != Some(hash)
831 }
832
833 fn poll_background_updates(&mut self) {
834 use pages::*;
835 while let Ok(s) = self.rx_audio.try_recv() {
836 if !self.fresh_snapshot("audio", &s) {
837 continue;
838 }
839 audio::update(&mut self.app.audio, audio::AudioMessage::Refreshed(s), &mut self.ui_context);
840 if self.app.current_page == Page::Audio {
841 self.needs_rebuild = true;
842 }
843 }
844 while let Ok(s) = self.rx_network.try_recv() {
845 if !self.fresh_snapshot("network", &s) {
846 continue;
847 }
848 network::update(&mut self.app.network, network::NetworkMessage::Refreshed(s), &mut self.ui_context);
849 if self.app.current_page == Page::Network {
850 self.needs_rebuild = true;
851 }
852 }
853 while let Ok(s) = self.rx_timers.try_recv() {
854 if !self.fresh_snapshot("timers", &s) {
855 continue;
856 }
857 pages::timers::update(&mut self.app.timers, pages::timers::TimersMessage::Refreshed(s), &mut self.ui_context);
858 if self.app.current_page == Page::Timers {
859 self.needs_rebuild = true;
860 }
861 }
862 while let Ok(s) = self.rx_bluetooth.try_recv() {
863 if !self.fresh_snapshot("bluetooth", &s) {
864 continue;
865 }
866 pages::bluetooth::update(&mut self.app.bluetooth, pages::bluetooth::BluetoothMessage::Refreshed(s), &mut self.ui_context);
867 if self.app.current_page == Page::Bluetooth {
868 self.needs_rebuild = true;
869 }
870 }
871 while let Ok(s) = self.rx_power.try_recv() {
872 if !self.fresh_snapshot("power", &s) {
873 continue;
874 }
875 pages::power::update(&mut self.app.power, pages::power::PowerMessage::Refreshed(s), &mut self.ui_context);
876 if self.app.current_page == Page::Power {
877 self.needs_rebuild = true;
878 }
879 }
880 while let Ok(s) = self.rx_system.try_recv() {
881 if !self.fresh_snapshot("system", &s) {
882 continue;
883 }
884 system_info::update(&mut self.app.system_info, system_info::SystemMessage::Refreshed(s), &mut self.ui_context);
885 if self.app.current_page == Page::System {
886 self.needs_rebuild = true;
887 }
888 }
889 while let Ok(s) = self.rx_processes.try_recv() {
890 processes::update(&mut self.app.processes, processes::ProcessesMessage::Refreshed(s));
891 if self.app.current_page == Page::Processes {
892 self.needs_rebuild = true;
893 }
894 }
895 while let Ok(s) = self.rx_storage.try_recv() {
896 if !self.fresh_snapshot("storage", &s) {
897 continue;
898 }
899 storage::update(&mut self.app.storage, storage::StorageMessage::Refreshed(s), &mut self.ui_context);
900 if self.app.current_page == Page::Storage {
901 self.needs_rebuild = true;
902 }
903 }
904 while let Ok(s) = self.rx_notifications.try_recv() {
905 if !self.fresh_snapshot("notifications", &s) {
906 continue;
907 }
908 pages::notifications::update(&mut self.app.notifications, pages::notifications::NotificationsMessage::Refreshed(s), &mut self.ui_context);
909 if self.app.current_page == Page::Notifications {
910 self.needs_rebuild = true;
911 }
912 }
913 while let Ok(s) = self.rx_browser.try_recv() {
914 if !self.fresh_snapshot("browser", &s) {
915 continue;
916 }
917 pages::browser::update(&mut self.app.browser, pages::browser::BrowserMessage::Refreshed(s), &mut self.ui_context);
918 if self.app.current_page == Page::Browser {
919 self.needs_rebuild = true;
920 }
921 }
922 while let Ok(s) = self.rx_default_apps.try_recv() {
923 if !self.fresh_snapshot("default_apps", &s) {
924 continue;
925 }
926 pages::default_apps::update(&mut self.app.default_apps, pages::default_apps::DefaultAppsMessage::Refreshed(s), &mut self.ui_context);
927 if self.app.current_page == Page::DefaultApps {
928 self.needs_rebuild = true;
929 }
930 }
931 while let Ok(s) = self.rx_services.try_recv() {
932 if !self.fresh_snapshot("services", &s) {
933 continue;
934 }
935 services::update(&mut self.app.services, services::ServicesMessage::Refreshed(s), &mut self.ui_context);
936 if self.app.current_page == Page::Services {
937 self.needs_rebuild = true;
938 }
939 }
940
941 while let Ok(s) = self.rx_accounts.try_recv() {
942 if !self.fresh_snapshot("accounts", &s) {
943 continue;
944 }
945 accounts::update(&mut self.app.accounts, accounts::AccountsMessage::Refreshed(s), &mut self.ui_context);
946 if self.app.current_page == Page::Accounts {
947 self.needs_rebuild = true;
948 }
949 }
950 while let Ok(m) = self.rx_backup.try_recv() {
951 self.handle_action(&AppAction::Storage(m));
952 if self.app.current_page == Page::Storage {
953 self.needs_rebuild = true;
954 }
955 }
956 while let Ok(s) = self.rx_packages.try_recv() {
957 if !self.fresh_snapshot("packages", &s) {
958 continue;
959 }
960 pages::packages::update(&mut self.app.packages, pages::packages::PackagesMessage::Refreshed(s), &mut self.ui_context);
961 if self.app.current_page == Page::Packages {
962 self.needs_rebuild = true;
963 }
964 }
965 while let Ok(m) = self.rx_update.try_recv() {
966 self.handle_action(&AppAction::Packages(m));
967 if self.app.current_page == Page::Packages {
968 self.needs_rebuild = true;
969 }
970 }
971 }
972
973 fn handle_action(&mut self, action: &AppAction) {
974 use pages::*;
975 // An action may change page state ahead of the system (a Wi-Fi
976 // toggle flips `wifi_enabled` before nmcli answers). If it fails, the
977 // next refresh equals the last one sent — and must still be applied
978 // to put the page right, so forget what was seen.
979 if !matches!(action, AppAction::Exit | AppAction::Wake) {
980 self.seen_snapshots.clear();
981 }
982 if matches!(action, AppAction::Accounts(_)) {
983 accounts::invalidate_keyring_probe();
984 }
985 match action {
986 AppAction::Exit | AppAction::Wake => {}
987 AppAction::Audio(m) => audio::update(&mut self.app.audio, m.clone(), &mut self.ui_context),
988 AppAction::Network(m) => network::update(&mut self.app.network, m.clone(), &mut self.ui_context),
989 AppAction::Bluetooth(m) => pages::bluetooth::update(&mut self.app.bluetooth, m.clone(), &mut self.ui_context),
990 AppAction::Power(m) => pages::power::update(&mut self.app.power, m.clone(), &mut self.ui_context),
991 AppAction::Timers(m) => pages::timers::update(&mut self.app.timers, m.clone(), &mut self.ui_context),
992 AppAction::SystemInfo(m) => system_info::update(&mut self.app.system_info, m.clone(), &mut self.ui_context),
993 AppAction::Processes(m) => processes::update(&mut self.app.processes, m.clone()),
994 AppAction::Services(m) => services::update(&mut self.app.services, m.clone(), &mut self.ui_context),
995 AppAction::DefaultApps(m) => pages::default_apps::update(&mut self.app.default_apps, m.clone(), &mut self.ui_context),
996 AppAction::Notifications(m) => notifications::update(&mut self.app.notifications, m.clone(), &mut self.ui_context),
997 AppAction::Browser(m) => pages::browser::update(&mut self.app.browser, m.clone(), &mut self.ui_context),
998 AppAction::Storage(m) => match m {
999 pages::storage::StorageMessage::StartBackup => {
1000 pages::storage::update(&mut self.app.storage, pages::storage::StorageMessage::StartBackup, &mut self.ui_context);
1001 let tx = self.tx_backup.clone();
1002 let wake = self.sender.clone();
1003 tokio::spawn(async move {
1004 let res = pages::storage::run_backup().await;
1005 let _ = tx.send(pages::storage::StorageMessage::BackupFinished(res));
1006 let _ = wake.send(AppAction::Wake);
1007 });
1008 }
1009 _ => pages::storage::update(&mut self.app.storage, m.clone(), &mut self.ui_context),
1010 },
1011
1012
1013 AppAction::Accounts(m) => match m {
1014 pages::accounts::AccountsMessage::GoogleLoginInit => {
1015 // One listener at a time: port 36137 is fixed, so a second
1016 // flow could only fail to bind and report it as a broken app
1017 // rather than "you already have a login in the browser".
1018 if self.app.accounts.oauth_listener_running {
1019 pages::accounts::update(
1020 &mut self.app.accounts,
1021 pages::accounts::AccountsMessage::StatusMessage(
1022 "A Google sign-in is already waiting on the browser.".to_string(),
1023 ),
1024 &mut self.ui_context,
1025 );
1026 } else {
1027 pages::accounts::update(&mut self.app.accounts, m.clone(), &mut self.ui_context);
1028 let sender = self.sender.clone();
1029 tokio::spawn(async move {
1030 pages::accounts::run_google_login(sender).await;
1031 });
1032 }
1033 }
1034 _ => pages::accounts::update(&mut self.app.accounts, m.clone(), &mut self.ui_context),
1035 },
1036 AppAction::Packages(m) => match m {
1037 pages::packages::PackagesMessage::StartUpdate => {
1038 if self.app.packages.busy() {
1039 return;
1040 }
1041 pages::packages::update(&mut self.app.packages, pages::packages::PackagesMessage::StartUpdate, &mut self.ui_context);
1042 let tx = self.tx_update.clone();
1043 let wake = self.sender.clone();
1044 tokio::spawn(async move {
1045 let res = pages::packages::run_update().await;
1046 let _ = tx.send(pages::packages::PackagesMessage::UpdateFinished(res));
1047 let _ = wake.send(AppAction::Wake);
1048 });
1049 }
1050 pages::packages::PackagesMessage::UpdateFinished(res) => {
1051 pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context);
1052 if res.is_ok() {
1053 let tx = self.tx_update.clone();
1054 let wake = self.sender.clone();
1055 tokio::spawn(async move {
1056 let new_state = pages::packages::fetch_packages_state().await;
1057 let _ = tx.send(pages::packages::PackagesMessage::Refreshed(new_state));
1058 let _ = wake.send(AppAction::Wake);
1059 });
1060 }
1061 }
1062 pages::packages::PackagesMessage::SelectPackage(Some(ref name)) => {
1063 let name_clone = name.clone();
1064 let is_installed = self.app.packages.active_tab == pages::packages::PackageTab::Installed;
1065 pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context);
1066 let tx = self.tx_update.clone();
1067 let wake = self.sender.clone();
1068 tokio::spawn(async move {
1069 let res = pages::packages::fetch_package_info(name_clone.clone(), is_installed).await;
1070 let _ = tx.send(pages::packages::PackagesMessage::InfoFetched(name_clone, res));
1071 let _ = wake.send(AppAction::Wake);
1072 });
1073 }
1074 pages::packages::PackagesMessage::SelectAndScrollPackage(ref name) => {
1075 let name_clone = name.clone();
1076 pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context);
1077 let tx = self.tx_update.clone();
1078 let wake = self.sender.clone();
1079 tokio::spawn(async move {
1080 let res = pages::packages::fetch_package_info(name_clone.clone(), true).await;
1081 let _ = tx.send(pages::packages::PackagesMessage::InfoFetched(name_clone, res));
1082 let _ = wake.send(AppAction::Wake);
1083 });
1084 }
1085 pages::packages::PackagesMessage::PreviewRemoval(ref targets) => {
1086 if !targets.is_empty() && !self.app.packages.busy() {
1087 pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context);
1088 let targets = targets.clone();
1089 let tx = self.tx_update.clone();
1090 let wake = self.sender.clone();
1091 tokio::spawn(async move {
1092 let res = pages::packages::preview_removal(targets.clone()).await;
1093 let _ = tx.send(pages::packages::PackagesMessage::RemovalPreviewed(targets, res));
1094 let _ = wake.send(AppAction::Wake);
1095 });
1096 }
1097 }
1098 pages::packages::PackagesMessage::StartUninstall(ref targets) => {
1099 if !targets.is_empty() && !self.app.packages.busy() {
1100 pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context);
1101 let targets = targets.clone();
1102 let tx = self.tx_update.clone();
1103 let wake = self.sender.clone();
1104 tokio::spawn(async move {
1105 let res = pages::packages::run_uninstall(targets.clone()).await;
1106 let _ = tx.send(pages::packages::PackagesMessage::UninstallFinished(targets, res));
1107 let _ = wake.send(AppAction::Wake);
1108 });
1109 }
1110 }
1111 pages::packages::PackagesMessage::SetInstallReason(ref targets, explicit) => {
1112 if !targets.is_empty() && !self.app.packages.busy() {
1113 pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context);
1114 let (targets, explicit) = (targets.clone(), *explicit);
1115 let tx = self.tx_update.clone();
1116 let wake = self.sender.clone();
1117 tokio::spawn(async move {
1118 let res = pages::packages::run_set_install_reason(targets.clone(), explicit).await;
1119 let _ = tx.send(pages::packages::PackagesMessage::InstallReasonSet(targets, explicit, res));
1120 let _ = wake.send(AppAction::Wake);
1121 });
1122 }
1123 }
1124 // Both outcomes refresh, failure included: the list (and the
1125 // orphan set, which either can change) is pacman's to report.
1126 pages::packages::PackagesMessage::UninstallFinished(..)
1127 | pages::packages::PackagesMessage::InstallReasonSet(..) => {
1128 pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context);
1129 let tx = self.tx_update.clone();
1130 let wake = self.sender.clone();
1131 // The details pane shows the install reason; re-read it.
1132 let reread = self.app.packages.selected_package.clone();
1133 tokio::spawn(async move {
1134 let new_state = pages::packages::fetch_packages_state().await;
1135 let _ = tx.send(pages::packages::PackagesMessage::Refreshed(new_state));
1136 if let Some(name) = reread {
1137 let res = pages::packages::fetch_package_info(name.clone(), true).await;
1138 let _ = tx.send(pages::packages::PackagesMessage::InfoFetched(name, res));
1139 }
1140 let _ = wake.send(AppAction::Wake);
1141 });
1142 }
1143 _ => pages::packages::update(&mut self.app.packages, m.clone(), &mut self.ui_context),
1144 },
1145 }
1146 }
1147
1148 }
1149
1150 fn main() {
1151 // cce-ui reports fatal event-loop errors through `log`; without a logger
1152 // installed they vanish (the cce-terminal connection-death lesson).
1153 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
1154 let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
1155 let _guard = rt.enter();
1156
1157 // One gap everywhere: the wells fill their grid allocations, so the grid
1158 // gap IS the visual section gap. The sections stand on the root plate,
1159 // so it is the root rung's sibling gap — `PageFlow::init` reads the
1160 // toolkit's grid_gap, hence the set here.
1161 cce_ui::layout::set_grid_gap(cce_ui::layout::root_plate_gap());
1162
1163 let mut initial_page = Page::ALL[0];
1164
1165
1166
1167 let args: Vec<String> = std::env::args().collect();
1168 if args.len() > 1 {
1169 let arg = args.last().unwrap().to_lowercase();
1170 for page in Page::ALL {
1171 if page.label().to_lowercase() == arg {
1172 initial_page = page;
1173 break;
1174 }
1175 }
1176 }
1177
1178 let initial_page_idx = Page::ALL.iter().position(|&p| p == initial_page).unwrap_or(0);
1179 INITIAL_PAGE_INDEX.store(initial_page_idx, std::sync::atomic::Ordering::SeqCst);
1180
1181 cce_ui::engine::run::<SystemInterface>();
1182 }
1183
1184 impl Drop for SystemInterface {
1185 fn drop(&mut self) {
1186 if !self.scroll_logs.is_empty() {
1187 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/cce-scroll-debug.log") {
1188 use std::io::Write;
1189 for log in &self.scroll_logs {
1190 let _ = writeln!(file, "{}", log);
1191 }
1192 }
1193 }
1194 }
1195 }