git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitabec18e2fc351e6bae95801beddf6aa886d4c4c3
parent1043c3934a
authorLucas Galante <[email protected]>
date2026-07-13 10:03
refactor(widget)!: children/parent leave WidgetHost — no trait method returns a raw pointer (52->50)

Tree structure is read off ctx.tree (parent_id/parent_ptr/child_ids/
children_ptrs); Adapted's container branch was redundant (Paginator
tree-links its strip every tick). Both navigate_focus twins deleted:
one had zero callers, the other walked a freshly-made EMPTY UiContext
and provably always returned false. serialize.rs keeps the one child its
dummy-ctx lookup could ever surface (Paginator's strip) via concrete
downcast.

Verified: 165 tests + workspace suite; email tab strip paints + Sent
click lands through the tree link; TI page-selector crop byte-identical;
designer /state + panel text intact.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01N4ajhvVZtyEEEus9bodsj3

 docs/rfc-core-rebuild.md        | 26 ++++++++++++++
 src/context.rs                  | 73 ++++---------------------------------
 src/scene/painter.rs            |  2 +-
 src/widget/core.rs              | 80 +++--------------------------------------
 src/widget/display/serialize.rs | 30 +++++++++-------
 src/widget/mod.rs               | 20 +++++------
 src/widget/model.rs             | 11 ------
 7 files changed, 63 insertions(+), 179 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index b9d1ebf..581624e 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -1851,6 +1851,32 @@ Constraint respected: **each crate still builds standalone** — the new core is
        dropdown→Grid relayout lands end-to-end through the id
        router; designer /state serves; demo's four event loops shed
        their unsafe self-alias entirely.
+       FOLLOW-UP (same day): **`children`/`parent` left the trait
+       (~52→50)** — tree structure is read off `ctx.tree`
+       (`parent_id`/`parent_ptr`/`child_ids`/`children_ptrs`); no
+       trait method returns a raw pointer anymore. `Adapted`'s
+       container branch was redundant: Paginator (the one
+       `Layout::container_children` implementor) tree-links its strip
+       every tick via `register_embedded_children`, so the tree
+       serves the walks identically (worst case a first-frame gap
+       before the first tick). Machinery consumers (propagate
+       descent, `find_hovered_scrollable`, the paint walk,
+       `all_quads`/`all_rounded_quads` defaults, designer's render
+       walks, TI's flat-walk parent skip) now read the tree directly
+       — sanctioned transient-pointer class. DEAD CODE FOUND: both
+       `navigate_focus` twins deleted — `UiContext::navigate_focus`
+       had zero callers, and `focus::navigate_focus` (settings'
+       ctrl-nav preamble) resolved parent/children through a
+       freshly-made EMPTY UiContext, so it always returned false
+       (parent has no field-derived form; ctrl+i needed a focused
+       Paginator, which is never focusable). Settings' real ctrl-nav
+       is its own section machinery, unchanged. serialize.rs's
+       dummy-ctx child lookup could only ever surface Paginator's
+       strip — kept via the 6aw concrete downcast. Verified: 165
+       tests + workspace suite; email tab strip paints and a Sent
+       click lands through the tree link; TI page-selector crop
+       byte-identical (no double-draw); designer /state + full panel
+       text intact; settings/files/demo canary-silent.
     5. window_runner render plumbing + remaining `as_ptr` sites; then
        the `Element` + `Adapted` endgame (own design pass).
     Stored-pointer state remaining after slices 1–3, all deliberate:
diff --git a/src/context.rs b/src/context.rs
index 6623141..4e4802c 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -265,7 +265,7 @@ impl UiContext {
             }
 
             let mut handled = false;
-            let mut children = (*root).children(self);
+            let mut children = self.tree.children_ptrs((*root).base().id());
             children.sort_by_key(|&child_ptr| (*child_ptr).z_index());
 
             // Determine if we should record a drag target candidate
@@ -498,72 +498,11 @@ impl UiContext {
         self.focused_widget.is_some()
     }
 
-    pub fn navigate_focus(&mut self, key: &Key, ctrl: bool) -> bool {
-        let ptr = match self.focused_widget.and_then(|id| self.tree.get_ptr(id)) {
-            Some(p) => p,
-            None => return false,
-        };
-
-        unsafe {
-            match (key, ctrl) {
-                (Key::Character(c), true) if c == "u" || c == "U" => {
-                    if let Some(parent_ptr) = (*ptr).parent(self) {
-                        let parent_ref = &mut *parent_ptr;
-                        self.set_focused(parent_ref);
-                        parent_ref.focus();
-                        return true;
-                    }
-                }
-                (Key::Character(c), true) if c == "i" || c == "I" => {
-                    let mut children = (*ptr).children(self);
-                    if !children.is_empty() {
-                        let child_ref = &mut *children[0];
-                        self.set_focused(child_ref);
-                        child_ref.focus();
-                        return true;
-                    }
-                }
-                (Key::Character(c), true) if c == "j" || c == "J" => {
-                    if let Some(parent_ptr) = (*ptr).parent(self) {
-                        let mut siblings = (*parent_ptr).children(self);
-                        let current_idx = siblings.iter().position(|&x| {
-                            let a = x as *mut () as usize;
-                            let b = ptr as *mut () as usize;
-                            a == b
-                        });
-                        if let Some(idx) = current_idx {
-                            let next_idx = (idx + 1) % siblings.len();
-                            let sibling_ref = &mut *siblings[next_idx];
-                            self.set_focused(sibling_ref);
-                            sibling_ref.focus();
-                            return true;
-                        }
-                    }
-                }
-                (Key::Character(c), true) if c == "k" || c == "K" => {
-                    if let Some(parent_ptr) = (*ptr).parent(self) {
-                        let mut siblings = (*parent_ptr).children(self);
-                        let current_idx = siblings.iter().position(|&x| {
-                            let a = x as *mut () as usize;
-                            let b = ptr as *mut () as usize;
-                            a == b
-                        });
-                        if let Some(idx) = current_idx {
-                            let prev_idx = if idx == 0 { siblings.len() - 1 } else { idx - 1 };
-                            let sibling_ref = &mut *siblings[prev_idx];
-                            self.set_focused(sibling_ref);
-                            sibling_ref.focus();
-                            return true;
-                        }
-                    }
-                }
-                _ => {}
-            }
-        }
-        false
-    }
+    // `navigate_focus` (tree-walk ctrl-nav) is DELETED (the plumbing retype): it had
+    // zero callers — its `focus::navigate_focus` twin was the one wired up, and that one
+    // walked an empty dummy context (provably inert). Section-level keyboard nav lives
+    // app-side (settings' focused_section machinery).
 
-    // --- Registry (backed by the generational WidgetTree; see scene/tree.rs) ---
     pub fn register_widget(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
         self.tree.register(id, ptr);
         unsafe {
@@ -871,7 +810,7 @@ impl UiContext {
             if !(*root).hit_test(cx, cy, self) {
                 return None;
             }
-            for child in (*root).children(self).into_iter().rev() {
+            for child in self.tree.children_ptrs((*root).base().id()).into_iter().rev() {
                 if let Some(scrollable) = self.find_hovered_scrollable(child, cx, cy) {
                     return Some(scrollable);
                 }
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index d399954..eec3e18 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -153,7 +153,7 @@ fn paint_node(ui: &UiContext, ptr: ElemPtr, pc: &mut PaintCtx) {
 
         (*ptr).paint_self(ui, pc);
 
-        let children = (*ptr).children(ui);
+        let children = ui.tree.children_ptrs((*ptr).base().id());
         if children.is_empty() {
             return;
         }
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 99153ae..7904164 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -1,4 +1,4 @@
-use crate::widget::{WidgetHost, Key};
+use crate::widget::WidgetHost;
 
 pub mod focus {
     use super::WidgetHost;
@@ -84,80 +84,10 @@ pub mod focus {
         ctx.tree.set_parent(c_id, Some(p_id));
     }
 
-    /// Keyboard tree navigation from the focused widget. `ctx` resolves the focused id to a
-    /// live widget; the parent/children walk itself deliberately keeps the legacy dummy-ctx
-    /// semantics (only `container_children`-style overrides that ignore the ctx ever yielded
-    /// anything here).
-    pub fn navigate_focus(key: &super::Key, ctrl: bool, ctx: &mut crate::context::UiContext) -> bool {
-        FOCUSED_WIDGET.with(|cell| {
-            let ptr = match cell.get().and_then(|id| ctx.tree.get_ptr(id)) {
-                Some(p) => p,
-                None => return false,
-            };
-
-            unsafe {
-                match (key, ctrl) {
-                    (super::Key::Character(c), true) if c == "u" || c == "U" => {
-                        let dummy = crate::context::UiContext::new();
-                        if let Some(parent_ptr) = (*ptr).parent(&dummy) {
-                            let parent_ref = &mut *parent_ptr;
-                            set_focused(parent_ref, Some(&mut *ctx));
-                            parent_ref.focus();
-                            return true;
-                        }
-                    }
-                    (super::Key::Character(c), true) if c == "i" || c == "I" => {
-                        let dummy = crate::context::UiContext::new();
-                        let mut children = (*ptr).children(&dummy);
-                        if !children.is_empty() {
-                            let child_ref = &mut *children[0];
-                            set_focused(child_ref, Some(&mut *ctx));
-                            child_ref.focus();
-                            return true;
-                        }
-                    }
-                    (super::Key::Character(c), true) if c == "j" || c == "J" => {
-                        let dummy = crate::context::UiContext::new();
-                        if let Some(parent_ptr) = (*ptr).parent(&dummy) {
-                            let mut siblings = (*parent_ptr).children(&dummy);
-                            let current_idx = siblings.iter().position(|&x| {
-                                let a = x as *mut () as usize;
-                                let b = ptr as *mut () as usize;
-                                a == b
-                            });
-                            if let Some(idx) = current_idx {
-                                let next_idx = (idx + 1) % siblings.len();
-                                let sibling_ref = &mut *siblings[next_idx];
-                                set_focused(sibling_ref, Some(&mut *ctx));
-                                sibling_ref.focus();
-                                return true;
-                            }
-                        }
-                    }
-                    (super::Key::Character(c), true) if c == "k" || c == "K" => {
-                        let dummy = crate::context::UiContext::new();
-                        if let Some(parent_ptr) = (*ptr).parent(&dummy) {
-                            let mut siblings = (*parent_ptr).children(&dummy);
-                            let current_idx = siblings.iter().position(|&x| {
-                                let a = x as *mut () as usize;
-                                let b = ptr as *mut () as usize;
-                                a == b
-                            });
-                            if let Some(idx) = current_idx {
-                                let prev_idx = if idx == 0 { siblings.len() - 1 } else { idx - 1 };
-                                let sibling_ref = &mut *siblings[prev_idx];
-                                set_focused(sibling_ref, Some(&mut *ctx));
-                                sibling_ref.focus();
-                                return true;
-                            }
-                        }
-                    }
-                    _ => {}
-                }
-            }
-            false
-        })
-    }
+    // `navigate_focus` is DELETED (the plumbing retype): it resolved parent/children
+    // through a freshly-made EMPTY UiContext, so the parent-based arms (ctrl+u/j/k) could
+    // never fire and ctrl+i only fired for a focused container-children widget (Paginator
+    // — never focusable). Its one caller (settings) already runs its own section nav.
 }
 
 pub mod hover_animation {
diff --git a/src/widget/display/serialize.rs b/src/widget/display/serialize.rs
index 31919e7..0bb0a33 100644
--- a/src/widget/display/serialize.rs
+++ b/src/widget/display/serialize.rs
@@ -27,9 +27,15 @@ fn serialize_single_widget(w: &dyn WidgetHost, json: &mut String) {
         type_name, escaped_label, x, y, width, height, focused, hovered, value
     ));
 
-    // Handle children
-    let dummy = crate::context::UiContext::new();
-    let children = w.children(&dummy);
+    // Child handling: there is no ctx here, so tree children were never reachable
+    // (the old lookup ran against a fresh empty UiContext). The one child this path
+    // could ever surface is the field-derived one — Paginator's strip
+    // (`Layout::container_children`) — kept via the 6aw concrete downcast.
+    let children: Vec<&(dyn WidgetHost + 'static)> = w
+        .as_any()
+        .downcast_ref::<Paginator>()
+        .map(|p| vec![&p.sidebar_menu as &(dyn WidgetHost + 'static)])
+        .unwrap_or_default();
     let mut menu_items = Vec::new();
     let mut is_menu_open = false;
     let mut is_vertical = false;
@@ -75,17 +81,15 @@ fn serialize_single_widget(w: &dyn WidgetHost, json: &mut String) {
     } else if !children.is_empty() {
         json.push_str(",\"children\":[");
         let mut first = true;
-        for child_ptr in &children {
-            unsafe {
-                if !(**child_ptr).visible() {
-                    continue;
-                }
-                if !first {
-                    json.push(',');
-                }
-                first = false;
-                serialize_single_widget(&**child_ptr, json);
+        for child in &children {
+            if !child.visible() {
+                continue;
+            }
+            if !first {
+                json.push(',');
             }
+            first = false;
+            serialize_single_widget(*child, json);
         }
         json.push_str("]}");
     } else {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 7edee1f..bead867 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -353,7 +353,7 @@ pub trait WidgetHost {
         let rect = Rect { x, y, width: w, height: h };
         let color = self.color();
 
-        if self.children(ui).is_empty() {
+        if ui.tree.children_ptrs(self.base().id()).is_empty() {
             // Leaf: emit its own rounded quads directly. For an ordinary widget this is just the
             // rounded background; for widgets that override `all_rounded_quads` with custom
             // geometry (e.g. Graph's nodes and edges) it captures that too. No recursion happens
@@ -420,7 +420,7 @@ pub trait WidgetHost {
                 quads.push((x, y, w, h, radius, c, (r1, r2, r3, r4)));
             }
         }
-        for &child_ptr in &self.children(ctx) {
+        for &child_ptr in &ctx.tree.children_ptrs(self.base().id()) {
             let widget = unsafe { &*child_ptr };
             quads.extend(widget.all_rounded_quads(ctx));
         }
@@ -461,18 +461,14 @@ pub trait WidgetHost {
     fn is_child_visible(&self, _child_id: WidgetId) -> bool { true }
     fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
 
-    fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn WidgetHost + 'static)> {
-        ctx.tree.parent_ptr(self.base().id())
-    }
-
-
     // `set_parent`/`add_child` are GONE from the trait (6bd batch 4): linking is a tree
     // operation — concrete callers ride the inherent `Adapted` methods, dyn callers go
-    // through `focus::link_parent_child` or `ctx.tree` directly.
-
-    fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn WidgetHost + 'static)> {
-        ctx.tree.children_ptrs(self.base().id())
-    }
+    // through `focus::link_parent_child` or `ctx.tree` directly. `parent`/`children` are
+    // GONE too (the plumbing retype): tree structure is read off `ctx.tree`
+    // (`parent_id`/`parent_ptr`/`child_ids`/`children_ptrs`) — the trait no longer
+    // proxies it, and no trait method returns a raw pointer. Paginator's field-derived
+    // child (the one `Layout::container_children` implementor) reaches the walks through
+    // the tree link its per-tick `register_embedded_children` maintains.
 
     fn z_index(&self) -> i32 { 0 }
     fn is_scrollable(&self) -> bool { false }
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 1ec5384..0732d59 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1026,13 +1026,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
     // keeps its own pointer Vec via the `Layout` hooks, because `set_rect`-time arrangement
     // has no ctx to reach the tree.
 
-    fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn WidgetHost + 'static)> {
-        if Layout::has_container_children(&self.inner) {
-            return Layout::container_children(&self.inner);
-        }
-        ctx.tree.children_ptrs(self.base.id())
-    }
-
     fn is_child_visible(&self, child_id: WidgetId) -> bool {
         if !Layout::has_container_children(&self.inner) {
             return true;
@@ -1049,10 +1042,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         Layout::z_order(&self.inner)
     }
 
-    fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn WidgetHost + 'static)> {
-        ctx.tree.parent_ptr(self.base.id())
-    }
-
     fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
         Input::set_modifiers(&mut self.inner, ctrl, shift, alt)
     }