GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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)
}