system settings
git clone https://git.lucas.co/cce-system-interface.git
feat: Switcher + Page DISSOLVED (Phase 6u) — sections are the dispatch roots
The page tree's top two layers are gone. The current page was always
app.current_page (the Switcher's active index was derived); page scroll
was already scroll_y/max_scroll_y; what remained load-bearing was Page's
scrollbar child, its out-of-bounds event gate, keyboard scrolling, and
the propagate root. All app-owned now:
- dispatch_page_event replicates Page's routing: the out-of-bounds gate
(screen-space, scrollbar-drag bypass), then the legacy child order
reversed — scrollbar first with Page's y-unshift, then the app-held
section-container clones last-to-first. PointerMove visits all roots;
other events stop at the first handler.
- The page scrollbar is an app field, placed exactly as Page::layout
did and updated with LAST frame's content height for the window pass
(the legacy pass also ran before the frame's content was measured);
its quads collect into the window assembly's plain slot (before the
plate wash) where the switcher subtree used to emit them.
- Scroll keys (arrows/PgUp/PgDn/Home/End over the page viewport) and
the ctrl-nav entry/section-cycling move into the key handler; the
entry focuses section 0 (network's WiFi highlight ORs that in where
it used root focus).
- System-page special cases deleted: scroll is the same manual path as
every other page.
System page: the widget-tree render path SEGFAULTED at launch on the
pre-6u baseline (exit 139 — the one-time raw-pointer section/label tree,
the exact use-after-free class the RFC core rebuild exists to kill), so
this flips it to the immediate-mode view that already existed in the
file but was never wired to the AppPage impl. Its CPU/GPU labels become
sec.text lines (the grid-column widget placement overlapped them), the
governor InfoBoxes advance the section cursor, gpu_strings lands on
state, and its two menus link into the app-held clone sections exactly
like every other page (the state-owned section tree and one-time widget
graveyard are no longer linked).
A/B (audio): WINDOW_PC tuple stream byte-identical; pixels AE=0.
Live-verified: spinbox +/-, right-click context menu, page dropdown
switching, page wheel + fallback (fast path), System gov dropdown
open/select + page scroll (page was previously uninspectable),
notifications bell dropdown, fonts textbox focus. Pre-existing and
unchanged: List/ScrollBox inner wheel is dead (its dissolution step
owns that); the scrollbar track's right half sits in the compositor's
8px edge-resize zone; an instant synthetic click's track-jump is wiped
by the release-frame sync (humans get a rebuild while dragging, same
as legacy). Not headlessly drivable: keyboard scroll/search/focus-nav,
held drags (slider, scrollbar thumb).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MjP3pGQEDLkJbV5WEmYBe
src/input_handler.rs | 221 +++++++++++++++++++++++++++++++--------------
src/main.rs | 51 ++---------
src/pages/accounts.rs | 3 -
src/pages/audio.rs | 3 -
src/pages/fonts.rs | 4 -
src/pages/mod.rs | 3 +-
src/pages/network.rs | 10 +-
src/pages/notifications.rs | 4 -
src/pages/packages.rs | 4 -
src/pages/processes.rs | 3 -
src/pages/storage.rs | 8 +-
src/pages/system_info.rs | 163 +++++++++------------------------
src/renderer.rs | 63 ++++++-------
13 files changed, 237 insertions(+), 303 deletions(-)
diff --git a/src/input_handler.rs b/src/input_handler.rs
index f6cf58b..7421d8e 100644
--- a/src/input_handler.rs
+++ b/src/input_handler.rs
@@ -36,9 +36,6 @@ impl SystemInterface {
if self.page_dropdown.cursor_moved(lx_no_scroll, ly_no_scroll, &mut self.ui_context) {
changed = true;
}
- if self.switcher.cursor_moved(lx_no_scroll, ly_no_scroll, &mut self.ui_context) {
- changed = true;
- }
// Drag updates are high-priority overrides
let mut drag_handled = false;
@@ -53,10 +50,8 @@ impl SystemInterface {
if !drag_handled {
let event = cce_ui::widget::Event::PointerMove { x: lx, y: ly, local_x: lx, local_y: ly };
- if let Some(root) = self.get_page_root_widget() {
- if self.ui_context.propagate_event(&event, root) {
- changed = true;
- }
+ if self.dispatch_page_event(&event) {
+ changed = true;
}
}
@@ -114,7 +109,6 @@ impl SystemInterface {
}
}
- let mut handled = false;
if self.page_dropdown.mouse_input(button, state, lx_no_scroll, ly_no_scroll, &mut self.ui_context) {
if self.page_dropdown.take_change() {
let idx = self.page_dropdown.selected;
@@ -128,12 +122,6 @@ impl SystemInterface {
}
}
self.needs_rebuild = true;
- handled = true;
- } else if self.switcher.mouse_input(button, state, lx_no_scroll, ly_no_scroll, &mut self.ui_context) {
- self.needs_rebuild = true;
- handled = true;
- }
- if handled {
return true;
}
@@ -170,9 +158,7 @@ impl SystemInterface {
let lx = self.cursor_x / s;
let ly = self.cursor_y / s + self.scroll_y;
let event = cce_ui::widget::Event::MouseButton { button, state, x: lx, y: ly, local_x: lx, local_y: ly };
- if let Some(root) = self.get_page_root_widget() {
- self.ui_context.propagate_event(&event, root);
- }
+ self.dispatch_page_event(&event);
if state == cce_ui::widget::ElementState::Pressed {
self.app.get_current_page_mut().handle_pointer_down(lx, ly, &mut self.ui_context);
@@ -208,33 +194,10 @@ impl SystemInterface {
let event = cce_ui::widget::Event::MouseWheel { delta: delta.clone(), x: lx, y: ly, local_x: lx, local_y: ly };
- let mut handled = false;
- if let Some(root) = self.get_page_root_widget() {
- if self.app.current_page == Page::System {
- if self.ui_context.propagate_event(&event, root) {
- handled = true;
- }
- } else {
- unsafe {
- for child in (*root).children(&self.ui_context).into_iter().rev() {
- let (cx, cy, _, _) = (*child).rect();
- let mut local_adjusted = event.clone();
- match &mut local_adjusted {
- cce_ui::widget::Event::MouseWheel { local_x, local_y, .. } => {
- *local_x -= cx;
- *local_y -= cy;
- }
- _ => {}
- }
- let adjusted_event = (*root).transform_event_for_child(child, local_adjusted, &self.ui_context);
- if self.ui_context.propagate_event(&adjusted_event, child) {
- handled = true;
- break;
- }
- }
- }
- }
- }
+ // Page dissolved (6u): one dispatch path for every page — scrollbar, then
+ // sections (inner ScrollBoxes take the wheel first), then the manual page
+ // scroll below as the fallback, exactly as the non-System pages worked.
+ let handled = self.dispatch_page_event(&event);
let mut actions = Vec::new();
self.propagate_widget_changes(&mut actions);
@@ -281,9 +244,74 @@ impl SystemInterface {
false
}
- pub(crate) fn get_page_root_widget(&mut self) -> Option<*mut (dyn cce_ui::widget::Element + 'static)> {
- let page_idx = Page::ALL.iter().position(|&p| p == self.app.current_page).unwrap_or(0);
- Some(self.pages[page_idx].as_ptr_mut())
+ /// The current page's event-dispatch roots (Phase 6u — the Page widget is dissolved):
+ /// the app-held section-container clones every page links its widgets under.
+ pub(crate) fn page_dispatch_roots(&mut self) -> Vec<*mut (dyn cce_ui::widget::Element + 'static)> {
+ self.page_sec_containers.iter_mut().map(|s| s.as_ptr_mut()).collect()
+ }
+
+ /// Replicates the dissolved Page's event routing: the out-of-bounds gate (events whose
+ /// screen position is outside the page viewport never reach page widgets, unless the
+ /// scrollbar is mid-drag), then the legacy child order reversed — the scrollbar first
+ /// (with Page's y-unshift, its coords are screen-space while the event carries the
+ /// scroll offset), then the sections last-to-first. PointerMove visits everything
+ /// (hover bookkeeping); other events stop at the first handler.
+ pub(crate) fn dispatch_page_event(&mut self, event: &cce_ui::widget::Event) -> bool {
+ use cce_ui::widget::Event;
+ let is_pointer_event = matches!(
+ event,
+ Event::PointerMove { .. } | Event::MouseButton { .. } | Event::MouseWheel { .. }
+ );
+ if is_pointer_event && !self.page_scroll_bar.dragging {
+ if let Event::PointerMove { x, y, .. }
+ | Event::MouseButton { x, y, .. }
+ | Event::MouseWheel { x, y, .. } = event
+ {
+ let rx = self.sidebar_width;
+ let ry = self.header_height;
+ let rw = self.width as f32 - self.sidebar_width;
+ let mut rh = self.height as f32 - self.header_height - self.status_height;
+ if self.search_open {
+ rh -= 42.0;
+ }
+ let screen_y = *y - self.scroll_y;
+ if *x < rx || *x > rx + rw || screen_y < ry || screen_y > ry + rh {
+ return false;
+ }
+ }
+ }
+
+ let is_pointer_move = matches!(event, Event::PointerMove { .. });
+ let mut handled = false;
+
+ if self.page_scroll_bar.content_h > self.page_scroll_bar.viewport_h {
+ let mut sb_event = event.clone();
+ if let Event::PointerMove { y, local_y, .. }
+ | Event::MouseButton { y, local_y, .. }
+ | Event::MouseWheel { y, local_y, .. } = &mut sb_event
+ {
+ *y -= self.scroll_y;
+ *local_y -= self.scroll_y;
+ }
+ let sb_ptr = self.page_scroll_bar.as_ptr_mut();
+ if self.ui_context.propagate_event(&sb_event, sb_ptr) {
+ if !is_pointer_move {
+ return true;
+ }
+ handled = true;
+ }
+ }
+
+ let roots = self.page_dispatch_roots();
+ for root in roots.into_iter().rev() {
+ if self.ui_context.propagate_event(event, root) {
+ if !is_pointer_move {
+ return true;
+ }
+ handled = true;
+ }
+ }
+ handled
}
pub(crate) fn handle_key_input_internal(&mut self, event: &cce_ui::widget::KeyEvent) -> bool {
@@ -346,15 +374,46 @@ impl SystemInterface {
self.needs_rebuild = true;
return true;
}
+ // Sections are parentless with the Page dissolved (6u), so the
+ // parent-pointer walk can't cycle BETWEEN them — do it app-side.
+ if let cce_ui::widget::Key::Character(c) = &event.logical_key {
+ let forward = c == "j" || c == "J";
+ let backward = c == "k" || c == "K";
+ if forward || backward {
+ let mut roots = self.page_dispatch_roots();
+ let focused_idx = roots.iter().position(|&r| unsafe {
+ cce_ui::widget::focus::is_focused(&*r)
+ });
+ if let (Some(idx), true) = (focused_idx, !roots.is_empty()) {
+ let next = if forward {
+ (idx + 1) % roots.len()
+ } else if idx == 0 {
+ roots.len() - 1
+ } else {
+ idx - 1
+ };
+ unsafe {
+ let sec = &mut *roots[next];
+ cce_ui::widget::focus::set_focused(sec);
+ sec.focus();
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ }
} else {
- if let Some(root_ptr) = self.get_page_root_widget() {
+ // Entry point: the dissolved page root used to take focus here; focus
+ // the first section instead.
+ let roots = self.page_dispatch_roots();
+ if let Some(&first) = roots.first() {
unsafe {
- let root_ref = &mut *root_ptr;
- cce_ui::widget::focus::set_focused(root_ref);
- root_ref.focus();
- self.needs_rebuild = true;
- return true;
+ let sec = &mut *first;
+ cce_ui::widget::focus::set_focused(sec);
+ sec.focus();
}
+ self.needs_rebuild = true;
+ return true;
}
}
}
@@ -362,25 +421,47 @@ impl SystemInterface {
let event_wrapper = cce_ui::widget::Event::KeyInput(event.clone());
let mut key_handled = false;
- if let Some(root) = self.get_page_root_widget() {
- let page_idx = Page::ALL.iter().position(|&p| p == self.app.current_page).unwrap_or(0);
- let old_page_scroll = self.pages[page_idx].scroll_y;
-
- if self.ui_context.propagate_event(&event_wrapper, root) {
- let mut actions = Vec::new();
- self.propagate_widget_changes(&mut actions);
- for a in actions {
- self.handle_action(&a);
- }
- self.needs_rebuild = true;
- key_handled = true;
+ if self.dispatch_page_event(&event_wrapper) {
+ let mut actions = Vec::new();
+ self.propagate_widget_changes(&mut actions);
+ for a in actions {
+ self.handle_action(&a);
}
+ self.needs_rebuild = true;
+ key_handled = true;
+ }
- let new_page_scroll = self.pages[page_idx].scroll_y;
- if (new_page_scroll - old_page_scroll).abs() > 0.01 {
- self.scroll_y = new_page_scroll;
- self.needs_rebuild = true;
- key_handled = true;
+ // The dissolved Page's keyboard scrolling: when nothing in the page tree took the
+ // key and the cursor is over the page viewport, scroll keys move the page.
+ if !key_handled && event.state == cce_ui::widget::ElementState::Pressed && self.max_scroll_y > 0.0 {
+ let over_page = {
+ let ry = self.header_height;
+ let mut rh = self.height as f32 - self.header_height - self.status_height;
+ if self.search_open {
+ rh -= 42.0;
+ }
+ self.cursor_x >= self.sidebar_width
+ && self.cursor_y >= ry
+ && self.cursor_y <= ry + rh
+ };
+ if over_page {
+ use cce_ui::widget::{Key, NamedKey};
+ let viewport_h = self.height as f32 - self.header_height - self.status_height
+ - if self.search_open { 42.0 } else { 0.0 };
+ let old_scroll = self.scroll_y;
+ match &event.logical_key {
+ Key::Named(NamedKey::ArrowDown) => self.scroll_y = (self.scroll_y + 24.0).min(self.max_scroll_y),
+ Key::Named(NamedKey::ArrowUp) => self.scroll_y = (self.scroll_y - 24.0).max(0.0),
+ Key::Named(NamedKey::PageDown) => self.scroll_y = (self.scroll_y + viewport_h).min(self.max_scroll_y),
+ Key::Named(NamedKey::PageUp) => self.scroll_y = (self.scroll_y - viewport_h).max(0.0),
+ Key::Named(NamedKey::Home) => self.scroll_y = 0.0,
+ Key::Named(NamedKey::End) => self.scroll_y = self.max_scroll_y,
+ _ => {}
+ }
+ if (self.scroll_y - old_scroll).abs() > 0.01 {
+ self.needs_rebuild = true;
+ key_handled = true;
+ }
}
}
diff --git a/src/main.rs b/src/main.rs
index 993f320..2b00342 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,4 @@
-use cce_ui::widget::{hover_animation, Element};
+use cce_ui::widget::hover_animation;
use glyphon::{Buffer, FontSystem};
use cce_settings::app::{AppAction, AppState};
@@ -80,8 +80,12 @@ struct SystemInterface {
last_scroll_y: f32,
page_sec_containers: Vec<cce_ui::widget::SectionContainer>,
page_dropdown: cce_ui::widget::Adapted<cce_ui::widget::input::Dropdown>,
- switcher: cce_ui::widget::Adapted<cce_ui::widget::Switcher>,
- pages: Vec<cce_ui::widget::Page>,
+ // Switcher + Page DISSOLVED (Phase 6u): the current page is app.current_page, page
+ // scroll is scroll_y/max_scroll_y, and the page scrollbar is this app-owned widget
+ // (rendered into the window assembly, evented directly). content_h feeds it — the
+ // window pass reads last frame's value, exactly as the legacy Page did.
+ page_scroll_bar: cce_ui::widget::ScrollBar,
+ content_h: f32,
// Root Backplate + StatusBar DISSOLVED (Phase 6s): the window plate and the status
// bar are emitted as tuples in rebuild_layout; this is the bar's text.
status_text: String,
@@ -123,23 +127,6 @@ impl cce_ui::engine::Application for SystemInterface {
.with_auto_width(true);
let sidebar_width = 0.0f32;
- let switcher = cce_ui::widget::Switcher::new(sidebar_width, 0.0, 820.0 - sidebar_width, 680.0);
- let mut pages = Vec::new();
- for page in Page::ALL.iter() {
- let mut page_widget = cce_ui::widget::Page::new(sidebar_width, 0.0, 820.0 - sidebar_width, 680.0)
- .with_label(page.label());
- if *page == Page::System {
- page_widget.layout = Box::new(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 320.0,
- gap: 20.0,
- padding_x: 10.0,
- padding_y: 10.0,
- grid: None,
- });
- }
- pages.push(page_widget);
- }
-
let mut app_state = app;
app_state.current_page = Page::ALL[initial_page_idx];
@@ -186,8 +173,8 @@ impl cce_ui::engine::Application for SystemInterface {
last_scroll_y: 0.0,
page_sec_containers: Vec::new(),
page_dropdown,
- switcher,
- pages,
+ page_scroll_bar: cce_ui::widget::ScrollBar::new(),
+ content_h: 0.0,
status_text: String::new(),
sans_serif_family: sans_family,
serif_family,
@@ -204,12 +191,6 @@ impl cce_ui::engine::Application for SystemInterface {
};
this.app.system_info.sender = Some(this.sender.clone());
- for page in &mut this.pages {
- this.switcher.add_child(page.as_ptr(), &mut this.ui_context);
- }
-
- // Root Backplate dissolved: switcher/dropdown register parentless via
- // render_widget in rebuild_layout.
this.update_status_text();
this.rebuild_layout(820.0, 680.0);
@@ -359,20 +340,6 @@ impl cce_ui::engine::Application for SystemInterface {
impl SystemInterface {
-fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f32, f32, f32, f32)>, ctx: &cce_ui::context::UiContext) {
- if let Some(rect) = w.popover_rect() {
- popovers.push(rect);
- }
- for child_ptr in w.children(ctx) {
- unsafe {
- if let Some(child) = child_ptr.as_ref() {
- Self::collect_popover_rects(child, popovers, ctx);
- }
- }
- }
-}
-
-
fn tick_internal(&mut self, dt: f32) -> bool {
let mut needs_redraw = false;
if hover_animation::tick(dt) {
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index c6fab77..f9f2643 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -863,12 +863,9 @@ impl crate::pages::AppPage for AccountsState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
if self.editing_oauth_creds {
cce_ui::widget::link_parent_child(&mut sec_containers[1], &mut self.oauth_client_id_box, ctx);
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index cc2e53b..4ff7733 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -451,12 +451,9 @@ impl crate::pages::AppPage for AudioState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
for (i, sink) in self.sinks.iter().enumerate() {
if sink.active {
diff --git a/src/pages/fonts.rs b/src/pages/fonts.rs
index 69c8749..87a692f 100644
--- a/src/pages/fonts.rs
+++ b/src/pages/fonts.rs
@@ -138,13 +138,9 @@ impl AppPage for FontsState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- for sec in sec_containers.iter_mut() {
- cce_ui::widget::link_parent_child(page_root, sec, ctx);
- }
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.sans_box, ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.serif_box, ctx);
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index c95cec3..a6c9bc7 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -63,9 +63,10 @@ pub trait AppPage {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer>;
+ /// Wire the page's widgets under the section containers (Phase 6u: the Page widget is
+ /// dissolved — the app-held section clones are the top-level dispatch/focus roots).
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
);
diff --git a/src/pages/network.rs b/src/pages/network.rs
index d7a6ba8..fabe943 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -549,12 +549,9 @@ impl crate::pages::AppPage for NetworkState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.wifi_toggle, ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.wifi_list_box.scroll_box, ctx);
@@ -568,11 +565,14 @@ impl crate::pages::AppPage for NetworkState {
cw: f32,
ch: f32,
root_focused: bool,
- _sec_focused: &[bool],
+ sec_focused: &[bool],
layout: &mut dyn LayoutStrategy,
ctx: &mut cce_ui::context::UiContext,
) -> crate::app::PageContent {
- view(self, cx, cy, cw, ch, root_focused, layout, ctx)
+ // Page root dissolved (6u): the ctrl-nav entry focuses section 0 now, which used to
+ // be expressed as root focus here.
+ let focused = root_focused || sec_focused.first().copied().unwrap_or(false);
+ view(self, cx, cy, cw, ch, focused, layout, ctx)
}
fn propagate_widget_changes(&mut self, actions: &mut Vec<crate::app::AppAction>) {
diff --git a/src/pages/notifications.rs b/src/pages/notifications.rs
index 2d9c09f..8697698 100644
--- a/src/pages/notifications.rs
+++ b/src/pages/notifications.rs
@@ -209,13 +209,9 @@ impl AppPage for NotificationsState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- for sec in sec_containers.iter_mut() {
- cce_ui::widget::link_parent_child(page_root, sec, ctx);
- }
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.enable_toggle, ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.bell_menu, ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.duration_spinbox, ctx);
diff --git a/src/pages/packages.rs b/src/pages/packages.rs
index cd5899b..083e135 100644
--- a/src/pages/packages.rs
+++ b/src/pages/packages.rs
@@ -750,13 +750,9 @@ impl crate::pages::AppPage for PackagesState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[2], ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.search_box, ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.installed_list_box.scroll_box, ctx);
diff --git a/src/pages/processes.rs b/src/pages/processes.rs
index ad8a97b..7a6477c 100644
--- a/src/pages/processes.rs
+++ b/src/pages/processes.rs
@@ -514,12 +514,9 @@ impl crate::pages::AppPage for ProcessesState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.cpu_list_box.scroll_box, ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[1], &mut self.services_search_box, ctx);
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index a426722..96a8034 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -345,13 +345,9 @@ impl crate::pages::AppPage for StorageState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
- sec_containers: &mut [cce_ui::widget::SectionContainer],
- ctx: &mut cce_ui::context::UiContext,
+ _sec_containers: &mut [cce_ui::widget::SectionContainer],
+ _ctx: &mut cce_ui::context::UiContext,
) {
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
- cce_ui::widget::link_parent_child(page_root, &mut sec_containers[2], ctx);
}
fn view(
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index 6f13720..98af427 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -34,6 +34,7 @@ pub struct SystemState {
pub cpu_usage: f32,
pub cpu_cores: u32,
pub gpus: Vec<String>,
+ pub gpu_strings: Vec<String>,
pub cpu_label: cce_ui::widget::Adapted<cce_ui::widget::Label>,
pub cpu_usage_label: cce_ui::widget::Adapted<cce_ui::widget::Label>,
pub cpu_temp_label: cce_ui::widget::Adapted<cce_ui::widget::Label>,
@@ -102,6 +103,7 @@ impl Default for SystemState {
cpu_usage: 0.0,
cpu_cores: 0,
gpus: Vec::new(),
+ gpu_strings: Vec::new(),
cpu_label: Label::new("CPU Info"),
cpu_usage_label: Label::new("CPU Usage"),
cpu_temp_label: Label::new("CPU Temp"),
@@ -600,14 +602,12 @@ pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
if !state.loaded {
sec.text("Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
} else {
- // CPU Info Label
- sec.widget(&mut state.cpu_label, 12.0, sec.cw - 24.0, 26.0, ctx);
-
- // CPU Usage Label
- sec.widget(&mut state.cpu_usage_label, 12.0, sec.cw - 24.0, 26.0, ctx);
-
- // CPU Temp Label
- sec.widget(&mut state.cpu_temp_label, 12.0, sec.cw - 24.0, 26.0, ctx);
+ // Same strings the old Label widgets carried, stacked vertically (the
+ // grid-column widget placement overlapped them at narrow widths).
+ sec.text(&format!("CPU {} ({} cores)", state.cpu_model, state.cpu_cores), 12.0, 0.0, 12.0, TEXT_FG);
+ sec.text(&format!("Usage {:.0}%", state.cpu_usage), 12.0, 0.0, 12.0, TEXT_FG);
+ let cpu_temp_text = read_cpu_temp().map(|t| format!("Temp {:.0}°C", t)).unwrap_or_else(|| "Temp N/A".to_string());
+ sec.text(&cpu_temp_text, 12.0, 0.0, 12.0, TEXT_FG);
}
});
@@ -616,8 +616,8 @@ pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
if !state.loaded {
sec_gpu.text("Loading GPU models...", 12.0, 0.0, 12.0, TEXT_FG);
} else {
- for gpu_lbl in state.gpu_labels.iter_mut() {
- sec_gpu.widget(gpu_lbl, 12.0, sec_gpu.cw - 24.0, 26.0, ctx);
+ for gpu_text in state.gpu_strings.iter() {
+ sec_gpu.text(gpu_text, 12.0, 0.0, 12.0, TEXT_FG);
}
}
});
@@ -652,6 +652,11 @@ pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
let info_h = 80.0;
let info_y = sec_gov.ay();
render_widget(sec_gov.pc, &mut info_box, rx + 12.0, info_y, sec_gov.cw - 24.0, info_h, ctx);
+ // Advance the section cursor past the hand-placed box.
+ sec_gov.content_y = sec_gov.content_y.max(info_y + info_h);
+ for h in &mut sec_gov.grid.col_heights {
+ *h = h.max(sec_gov.content_y);
+ }
}
});
@@ -685,6 +690,11 @@ pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
let info_h = 80.0;
let info_y = sec_gpow.ay();
render_widget(sec_gpow.pc, &mut info_box, rx + 12.0, info_y, sec_gpow.cw - 24.0, info_h, ctx);
+ // Advance the section cursor past the hand-placed box.
+ sec_gpow.content_y = sec_gpow.content_y.max(info_y + info_h);
+ for h in &mut sec_gpow.grid.col_heights {
+ *h = h.max(sec_gpow.content_y);
+ }
}
});
@@ -745,6 +755,7 @@ pub fn update(state: &mut SystemState, msg: SystemMessage, ctx: &mut cce_ui::con
state.cpu_usage = new.cpu_usage;
state.cpu_cores = new.cpu_cores;
state.gpus = new.gpus;
+ state.gpu_strings = new.gpu_strings.clone();
state.battery = new.battery;
state.on_ac = new.on_ac;
@@ -904,124 +915,34 @@ impl crate::pages::AppPage for SystemState {
fn link_children(
&mut self,
- page_root: &mut dyn cce_ui::widget::Element,
- _sec_containers: &mut [cce_ui::widget::SectionContainer],
+ sec_containers: &mut [cce_ui::widget::SectionContainer],
ctx: &mut cce_ui::context::UiContext,
) {
- if !self.initialized {
- self.initialized = true;
-
- // Clear page root children to prevent duplicates
- page_root.clear_children(ctx);
-
- // Bind click callbacks to actions
- if let Some(ref tx) = self.sender {
- let tx1 = tx.clone();
- self.suspend_btn = self.suspend_btn.clone().on_click(move || {
- let _ = tx1.send(AppAction::SystemInfo(SystemMessage::Suspend));
- });
-
- let tx2 = tx.clone();
- self.hibernate_btn = self.hibernate_btn.clone().on_click(move || {
- let _ = tx2.send(AppAction::SystemInfo(SystemMessage::Hibernate));
- });
-
- let tx3 = tx.clone();
- self.reboot_btn = self.reboot_btn.clone().on_click(move || {
- let _ = tx3.send(AppAction::SystemInfo(SystemMessage::Reboot));
- });
-
- let tx4 = tx.clone();
- self.poweroff_btn = self.poweroff_btn.clone().on_click(move || {
- let _ = tx4.send(AppAction::SystemInfo(SystemMessage::PowerOff));
- });
-
- let tx5 = tx.clone();
- self.force_shutdown_btn = self.force_shutdown_btn.clone().on_click(move || {
- let _ = tx5.send(AppAction::SystemInfo(SystemMessage::ForceShutdown));
- });
- }
-
- // Set up initial rect sizes for info boxes
- self.cpu_info_box.set_rect(0.0, 0.0, 0.0, 80.0);
- self.gpu_info_box.set_rect(0.0, 0.0, 0.0, 80.0);
-
- // Build layout tree:
- // ── 1. System Section ──
- self.sec_system.add_child(self.hostname_label.as_ptr_mut(), ctx);
- self.sec_system.add_child(self.uptime_label.as_ptr_mut(), ctx);
-
- // ── 2. Actions Section ──
- self.actions_row.add_child(self.suspend_btn.as_ptr_mut(), ctx);
- self.actions_row.add_child(self.hibernate_btn.as_ptr_mut(), ctx);
- self.actions_row.add_child(self.reboot_btn.as_ptr_mut(), ctx);
- self.actions_row.add_child(self.poweroff_btn.as_ptr_mut(), ctx);
-
- self.sec_actions.add_child(self.actions_row.as_ptr_mut(), ctx);
- self.sec_actions.add_child(self.force_shutdown_btn.as_ptr_mut(), ctx);
-
- // ── 3. CPU Section ──
- self.sec_cpu.add_child(self.cpu_label.as_ptr_mut(), ctx);
- self.sec_cpu.add_child(self.cpu_usage_label.as_ptr_mut(), ctx);
- self.sec_cpu.add_child(self.cpu_temp_label.as_ptr_mut(), ctx);
-
- // ── 4. GPU Section ──
- for gpu_lbl in &mut self.gpu_labels {
- self.sec_gpu.add_child(gpu_lbl.as_ptr_mut(), ctx);
- }
-
- // ── 5. CPU Governor Section ──
- self.sec_cpu_gov.add_child(self.cpu_gov_menu.as_ptr_mut(), ctx);
- self.sec_cpu_gov.add_child(self.cpu_info_box.as_ptr_mut(), ctx);
-
- // ── 6. GPU Power Section ──
- self.sec_gpu_gov.add_child(self.gpu_gov_menu.as_ptr_mut(), ctx);
- self.sec_gpu_gov.add_child(self.gpu_info_box.as_ptr_mut(), ctx);
-
- // ── 7. Battery Section ──
- self.sec_battery.add_child(self.battery_label_pct.as_ptr_mut(), ctx);
- self.sec_battery.add_child(self.battery_label_state.as_ptr_mut(), ctx);
- self.sec_battery.add_child(self.battery_label_time.as_ptr_mut(), ctx);
- self.sec_battery.add_child(self.battery_label_details.as_ptr_mut(), ctx);
- self.sec_battery.add_child(self.battery_label_ac.as_ptr_mut(), ctx);
-
- // Mount SectionContainers onto the Page
- page_root.add_child(self.sec_system.as_ptr_mut(), ctx);
- page_root.add_child(self.sec_actions.as_ptr_mut(), ctx);
- page_root.add_child(self.sec_cpu.as_ptr_mut(), ctx);
- page_root.add_child(self.sec_gpu.as_ptr_mut(), ctx);
- page_root.add_child(self.sec_cpu_gov.as_ptr_mut(), ctx);
- page_root.add_child(self.sec_gpu_gov.as_ptr_mut(), ctx);
- page_root.add_child(self.sec_battery.as_ptr_mut(), ctx);
- }
-
- // Link parent-child for focus context
- cce_ui::widget::link_parent_child(page_root, &mut self.sec_system, ctx);
- cce_ui::widget::link_parent_child(page_root, &mut self.sec_actions, ctx);
- cce_ui::widget::link_parent_child(page_root, &mut self.sec_cpu, ctx);
- cce_ui::widget::link_parent_child(page_root, &mut self.sec_gpu, ctx);
- cce_ui::widget::link_parent_child(page_root, &mut self.sec_cpu_gov, ctx);
- cce_ui::widget::link_parent_child(page_root, &mut self.sec_gpu_gov, ctx);
- cce_ui::widget::link_parent_child(page_root, &mut self.sec_battery, ctx);
-
- cce_ui::widget::link_parent_child(&mut self.sec_cpu_gov, &mut self.cpu_gov_menu, ctx);
- cce_ui::widget::link_parent_child(&mut self.sec_cpu_gov, &mut self.cpu_info_box, ctx);
- cce_ui::widget::link_parent_child(&mut self.sec_gpu_gov, &mut self.gpu_gov_menu, ctx);
- cce_ui::widget::link_parent_child(&mut self.sec_gpu_gov, &mut self.gpu_info_box, ctx);
+ // Phase 6u: System renders through the immediate view like every other page;
+ // only its two menus need event dispatch/focus, linked into the app-held clone
+ // sections exactly as the other pages do (the old one-time widget tree — labels,
+ // buttons, state-owned sections — is dead; the view emits text/buttons directly).
+ cce_ui::widget::link_parent_child(&mut sec_containers[4], &mut self.cpu_gov_menu, ctx);
+ cce_ui::widget::link_parent_child(&mut sec_containers[5], &mut self.gpu_gov_menu, ctx);
}
fn view(
&mut self,
- _cx: f32,
- _cy: f32,
- _cw: f32,
- _ch: f32,
- _root_focused: bool,
- _sec_focused: &[bool],
- _layout: &mut dyn LayoutStrategy,
- _ctx: &mut cce_ui::context::UiContext,
+ cx: f32,
+ cy: f32,
+ cw: f32,
+ ch: f32,
+ root_focused: bool,
+ sec_focused: &[bool],
+ layout: &mut dyn LayoutStrategy,
+ ctx: &mut cce_ui::context::UiContext,
) -> crate::app::PageContent {
- crate::app::PageContent::new()
+ // Phase 6u: System used to render through the WIDGET TREE (the only page that
+ // did) — its content reached the frame via the root aggregate walking
+ // Switcher → Page(AdaptiveGridLayout) → sections → widgets. With that chain
+ // dissolved, the page renders through the same immediate-mode view as every
+ // other page (this free `view` predates the flip; it was never wired up).
+ view(self, cx, cy, cw, ch, root_focused, sec_focused, layout, ctx)
}
fn propagate_widget_changes(&mut self, actions: &mut Vec<crate::app::AppAction>) {
diff --git a/src/renderer.rs b/src/renderer.rs
index 56a34c5..1def5fb 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -85,18 +85,8 @@ impl SystemInterface {
pub(crate) fn rebuild_layout(&mut self, sw: f32, sh: f32) {
self.ui_context.clear_hierarchy();
- self.switcher.clear_children(&mut self.ui_context);
- for page in &mut self.pages {
- self.switcher.add_child(page.as_ptr(), &mut self.ui_context);
- }
-
- let page_idx = Page::ALL.iter().position(|&p| p == self.app.current_page).unwrap_or(0);
-
- // ── Rebuild Element Focus Hierarchy ──
- for page in &mut self.pages {
- page.clear_children(&mut self.ui_context);
- }
- let page_root = &mut self.pages[page_idx];
+ // ── Rebuild Element Focus Hierarchy (Switcher + Page dissolved, Phase 6u:
+ // sections are the top-level dispatch/focus roots) ──
for c in &mut self.page_sec_containers {
c.clear_children(&mut self.ui_context);
c.set_parent(None, &mut self.ui_context);
@@ -113,7 +103,7 @@ impl SystemInterface {
let active_page = self.app.get_current_page_mut();
self.page_sec_containers = active_page.get_section_containers();
- active_page.link_children(page_root, &mut self.page_sec_containers, &mut self.ui_context);
+ active_page.link_children(&mut self.page_sec_containers, &mut self.ui_context);
self.sidebar_width = 0.0;
self.header_height = 0.0; // No CSD Titlebar
@@ -146,7 +136,6 @@ impl SystemInterface {
let page_idx = Page::ALL.iter().position(|&p| p == self.app.current_page).unwrap_or(0);
self.page_dropdown.selected = page_idx;
- self.switcher.set_active_index(Some(page_idx));
// Root Backplate DISSOLVED (Phase 6s): top-level widgets stay parentless
// (render_widget registers them); the window plate, the root aggregate's
@@ -173,7 +162,19 @@ impl SystemInterface {
} else {
logical_sh - self.header_height - self.status_height
};
- cce_ui::layout::render_widget(&mut dummy_pc, &mut self.switcher, self.sidebar_width, self.header_height, logical_sw - self.sidebar_width, switcher_h, &mut self.ui_context);
+ // The page scrollbar (the only geometry the dissolved Page subtree ever emitted):
+ // placed exactly as Page::layout did, updated with LAST frame's content height —
+ // the legacy window pass also ran before this frame's content was measured.
+ self.page_scroll_bar.set_rect(
+ self.sidebar_width + (logical_sw - self.sidebar_width) - 6.0 - 2.0,
+ self.header_height + 4.0,
+ 6.0,
+ switcher_h - 8.0,
+ );
+ if self.page_scroll_bar.dragging {
+ self.scroll_y = self.page_scroll_bar.scroll_y;
+ }
+ self.page_scroll_bar.update(self.scroll_y, self.content_h, switcher_h);
// Assemble the window exactly as the legacy `render_widget(root Backplate)`
// aggregate did: every child plain quad (clipped to the window, with the root's
@@ -190,7 +191,7 @@ impl SystemInterface {
let mut rounded: Vec<RectTuple> = Vec::new();
let mut wtexts: Vec<TextTuple> = Vec::new();
- collect_window_child(&self.switcher, &self.ui_context, logical_sw, logical_sh, plate_radius, &mut plain, &mut rounded, &mut wtexts);
+ collect_window_child(&self.page_scroll_bar, &self.ui_context, logical_sw, logical_sh, plate_radius, &mut plain, &mut rounded, &mut wtexts);
// The dissolved status bar's slot in the child order.
let sb_y = logical_sh - self.status_height;
@@ -227,13 +228,6 @@ impl SystemInterface {
window_pc.texts.extend(wtexts);
}
- let page_idx = Page::ALL.iter().position(|&p| p == self.app.current_page).unwrap_or(0);
- let active_page_widget = &self.pages[page_idx];
- if self.app.current_page == Page::System {
- self.scroll_y = active_page_widget.scroll_y;
- self.max_scroll_y = (active_page_widget.content_h - active_page_widget.base.base.h).max(0.0);
- }
-
let mut search_pc = PageContent::new();
if self.search_open {
search_pc.rects.push((
@@ -298,7 +292,7 @@ impl SystemInterface {
- if self.search_open && !self.search_query.is_empty() && !self.pages[page_idx].scroll_bar.dragging {
+ if self.search_open && !self.search_query.is_empty() && !self.page_scroll_bar.dragging {
let query_lower = self.search_query.to_lowercase();
let mut first_match_y = None;
for (t, _, _, y, _, _, _) in &pc.texts {
@@ -324,9 +318,6 @@ impl SystemInterface {
}
}
- let mut popovers = Vec::new();
- Self::collect_popover_rects(&self.pages[page_idx], &mut popovers, &self.ui_context);
-
let mut max_y = 0.0f32;
for (_, _, y, _, h, _, _) in &pc.rects {
max_y = max_y.max(y + h);
@@ -348,15 +339,13 @@ impl SystemInterface {
}
}
- let page_root = &mut self.pages[page_idx];
- if page_root.scroll_bar.dragging {
- self.scroll_y = page_root.scroll_bar.scroll_y;
+ if self.page_scroll_bar.dragging {
+ self.scroll_y = self.page_scroll_bar.scroll_y;
} else {
- page_root.scroll_y = self.scroll_y;
- page_root.scroll_bar.scroll_y = self.scroll_y;
+ self.page_scroll_bar.scroll_y = self.scroll_y;
}
- page_root.content_h = max_y;
- page_root.scroll_bar.update(self.scroll_y, max_y, lch);
+ self.content_h = max_y;
+ self.page_scroll_bar.update(self.scroll_y, max_y, lch);
let scroll_offset_y = self.scroll_y;
@@ -675,12 +664,12 @@ impl SystemInterface {
let cw = (cw - 2.0 * margin).max(1.0);
let ch = (ch - 2.0 * margin).max(1.0);
let mut layout = AdaptiveGrid::new(260.0, 20.0);
- let page_idx = Page::ALL.iter().position(|&p| p == self.app.current_page).unwrap_or(0);
- let root_focused = cce_ui::widget::focus::is_focused(&self.pages[page_idx]);
+ // Page root dissolved (6u): the ctrl-nav entry focuses section 0, so root focus is
+ // permanently false; views that highlighted on it OR in their first section's bool.
let sec_focused: Vec<bool> = self.page_sec_containers.iter()
.map(|c| cce_ui::widget::focus::is_focused(c))
.collect();
- self.app.get_current_page_mut().view(cx, cy, cw, ch, root_focused, &sec_focused, &mut layout, &mut self.ui_context)
+ self.app.get_current_page_mut().view(cx, cy, cw, ch, false, &sec_focused, &mut layout, &mut self.ui_context)
}
}