git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitcb0ed2085d07dd39763b57d154a6f1cadd2dd006
parenta476b65ee3
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-09 00:54
refactor: walk linked children by id; dialog tests insert

cce-ui's registry owns its widgets (owning registry, phase 5); its raw-pointer tree accessors are crate-private. The paint walk and the open-menu search find roster slots by id (Slots::find_index) and reach other children through get_widget; the dialog slider tests insert the dialog and lend it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/app.rs    | 10 +++----
 src/main.rs   | 92 +++++++++++++++++++++++++++++------------------------------
 src/render.rs | 18 ++++++------
 src/slots.rs  |  7 ++---
 4 files changed, 60 insertions(+), 67 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 1a9e9a8..91a0674 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -3543,8 +3543,8 @@ impl State {
 
     pub fn viewport_mut(&mut self) -> &mut Viewport3D { self.slots.viewport_mut(&mut self.ui_context) }
 
-    pub fn find_widget_index(&self, target_addr: *const ()) -> Option<usize> {
-        self.slots.find_index(&self.ui_context, target_addr)
+    pub fn find_widget_index(&self, id: cce_ui::widget::WidgetId) -> Option<usize> {
+        self.slots.find_index(&self.ui_context, id)
     }
 
     pub fn has_any_open_menu(&self, idx: usize) -> bool {
@@ -3567,9 +3567,9 @@ impl State {
                 return true;
             }
         }
-        let child_ptrs = self.ui_context.tree.children_ptrs(self.slots.get_dyn(&self.ui_context, idx).base().id());
-        for child_ptr in child_ptrs {
-            if let Some(child_idx) = self.find_widget_index(child_ptr as *const ()) {
+        let child_ids = self.ui_context.tree.child_ids(self.slots.get_dyn(&self.ui_context, idx).base().id());
+        for child_id in child_ids {
+            if let Some(child_idx) = self.find_widget_index(child_id) {
                 if self.has_any_open_menu_impl(child_idx, visited) {
                     return true;
                 }
diff --git a/src/main.rs b/src/main.rs
index 48f7fa4..0ece101 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15439,18 +15439,17 @@ mod tests {
         use crate::dialog::{Control, Dialog, Row, SLIDER_W};
         use cce_ui::widget::{ElementState, MouseButton, WidgetHost};
         let mut ctx = cce_ui::context::UiContext::new();
-        let mut d = Dialog::new();
-        d.set_visible(true);
-        WidgetHost::set_rect(&mut d, 0.0, 0.0, 520.0, 420.0);
-        ctx.register_host(&mut d);
+        let d = ctx.insert(Dialog::new());
+        ctx[d].set_visible(true);
+        WidgetHost::set_rect(&mut ctx[d], 0.0, 0.0, 520.0, 420.0);
         let plain = |i: usize| Row { id: format!("c{i}"), label: format!("Command {i}"), chord: String::new(), control: None, truncate_head: false };
-        d.set_rows(vec![
+        ctx[d].set_rows(vec![
             Row { id: "zoom_level".into(), label: "Zoom".into(), chord: String::new(), control: Some(Control::Slider { value: 100.0, min: 20.0, max: 320.0, dec: 0, step: 10.0, suffix: "%" }), truncate_head: false },
             plain(1),
             plain(2),
         ]);
-        d.set_page(10);
-        d.set_occluding(false);
+        ctx[d].set_page(10);
+        ctx[d].set_occluding(false);
 
         // The first row's rect, as the widget lays it out: the list starts
         // below the query line; the band begins SLIDER_W in from the row's
@@ -15464,43 +15463,43 @@ mod tests {
         // Press at three quarters along the band: the value lands three
         // quarters into the range, and the row is not activated as a pick.
         let px = band_x + band_w * 0.75;
-        assert!(d.mouse_input(MouseButton::Left, ElementState::Pressed, px, row_y, &mut ctx));
-        assert!(d.slider_dragging());
-        let v = d.take_slider_change().map(|(_, v)| v).expect("a press on the band reports a value");
+        assert!(ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, px, row_y, ctx)).unwrap());
+        assert!(ctx[d].slider_dragging());
+        let v = ctx[d].take_slider_change().map(|(_, v)| v).expect("a press on the band reports a value");
         assert!((v - (20.0 + 0.75 * 300.0)).abs() < 3.0, "value {v} is not three quarters of the range");
-        assert_eq!(d.rows[0].slider_value(), Some(v), "the row follows");
-        assert_eq!(d.take_activated(), None, "the band is a control, not a pick");
-        assert_eq!(d.take_slider_change(), None, "reported once");
-        d.mouse_input(MouseButton::Left, ElementState::Released, px, row_y, &mut ctx);
-        assert!(!d.slider_dragging());
+        assert_eq!(ctx[d].rows[0].slider_value(), Some(v), "the row follows");
+        assert_eq!(ctx[d].take_activated(), None, "the band is a control, not a pick");
+        assert_eq!(ctx[d].take_slider_change(), None, "reported once");
+        ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, px, row_y, ctx)).unwrap();
+        assert!(!ctx[d].slider_dragging());
 
         // A press on the row's label end selects it and reports nothing.
-        assert!(d.mouse_input(MouseButton::Left, ElementState::Pressed, 30.0, row_y, &mut ctx));
-        assert_eq!(d.selected, 0);
-        assert!(!d.slider_dragging());
-        assert_eq!(d.take_slider_change(), None);
-        assert_eq!(d.take_activated(), None);
-        d.mouse_input(MouseButton::Left, ElementState::Released, 30.0, row_y, &mut ctx);
+        assert!(ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 30.0, row_y, ctx)).unwrap());
+        assert_eq!(ctx[d].selected, 0);
+        assert!(!ctx[d].slider_dragging());
+        assert_eq!(ctx[d].take_slider_change(), None);
+        assert_eq!(ctx[d].take_activated(), None);
+        ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 30.0, row_y, ctx)).unwrap();
 
         // An ordinary row still picks.
-        assert!(d.mouse_input(MouseButton::Left, ElementState::Pressed, 30.0, row_y + 24.0, &mut ctx));
-        assert_eq!(d.take_activated().as_deref(), Some("c1"));
+        assert!(ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 30.0, row_y + 24.0, ctx)).unwrap());
+        assert_eq!(ctx[d].take_activated().as_deref(), Some("c1"));
 
         // The wheel over the control turns the slider — a notch up is 2% of
         // the range more, as on the toolkit's slider — and over the label
         // end it scrolls the list instead, reporting nothing.
-        let before = d.rows[0].slider_value().unwrap();
+        let before = ctx[d].rows[0].slider_value().unwrap();
         let wheel = |x: f32, y: f32| cce_ui::widget::Event::MouseWheel {
             delta: cce_ui::widget::MouseScrollDelta::LineDelta(0.0, 1.0),
             x, y, local_x: x, local_y: y,
         };
         ctx.note_scroll_event();
-        assert!(d.handle_event(&wheel(band_x + 10.0, row_y), &mut ctx));
-        let v = d.take_slider_change().map(|(_, v)| v).expect("a wheel over the band reports a value");
+        assert!(ctx.lend_h(d, |w, ctx| w.handle_event(&wheel(band_x + 10.0, row_y), ctx)).unwrap());
+        let v = ctx[d].take_slider_change().map(|(_, v)| v).expect("a wheel over the band reports a value");
         assert!((v - (before + 0.02 * 300.0)).abs() < 1e-3, "notch up: {before} -> {v}");
         ctx.note_scroll_event();
-        d.handle_event(&wheel(30.0, row_y), &mut ctx);
-        assert_eq!(d.take_slider_change(), None, "over the label the wheel is the list's");
+        ctx.lend_h(d, |w, ctx| w.handle_event(&wheel(30.0, row_y), ctx)).unwrap();
+        assert_eq!(ctx[d].take_slider_change(), None, "over the label the wheel is the list's");
     }
 
     /// The band ends where the key bindings do. The chord column's right edge
@@ -15512,16 +15511,15 @@ mod tests {
         use crate::dialog::{Control, Dialog, Row, SLIDER_W, TOGGLE_W};
         use cce_ui::widget::{ElementState, MouseButton, WidgetHost};
         let mut ctx = cce_ui::context::UiContext::new();
-        let mut d = Dialog::new();
-        d.set_visible(true);
-        WidgetHost::set_rect(&mut d, 0.0, 0.0, 520.0, 420.0);
-        ctx.register_host(&mut d);
-        d.set_rows(vec![
+        let d = ctx.insert(Dialog::new());
+        ctx[d].set_visible(true);
+        WidgetHost::set_rect(&mut ctx[d], 0.0, 0.0, 520.0, 420.0);
+        ctx[d].set_rows(vec![
             Row { id: "zoom_level".into(), label: "Zoom".into(), chord: String::new(), control: Some(Control::Slider { value: 100.0, min: 20.0, max: 320.0, dec: 0, step: 10.0, suffix: "%" }), truncate_head: false },
             Row { id: "show_grid".into(), label: "Show Grid".into(), chord: "Ctrl+G".into(), control: Some(Control::Toggle(true)), truncate_head: false },
         ]);
-        d.set_page(10);
-        d.set_occluding(false);
+        ctx[d].set_page(10);
+        ctx[d].set_occluding(false);
 
         let row_y = 12.0 + 30.0 + 8.0 + 12.0;
         let row_right = 520.0 - 12.0 - 8.0;
@@ -15530,22 +15528,22 @@ mod tests {
 
         // The band's last pixel is the range's top; the switch column past it
         // is not the band's.
-        assert!(d.mouse_input(MouseButton::Left, ElementState::Pressed, band_right - 1.0, row_y, &mut ctx));
-        assert!(d.slider_dragging(), "the band reaches the chord column's edge");
-        let v = d.take_slider_change().map(|(_, v)| v).expect("a press on the band reports a value");
+        assert!(ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, band_right - 1.0, row_y, ctx)).unwrap());
+        assert!(ctx[d].slider_dragging(), "the band reaches the chord column's edge");
+        let v = ctx[d].take_slider_change().map(|(_, v)| v).expect("a press on the band reports a value");
         assert!((v - 320.0).abs() < 4.0, "the band's end is the range's end, got {v}");
-        d.mouse_input(MouseButton::Left, ElementState::Released, band_right - 1.0, row_y, &mut ctx);
+        ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, band_right - 1.0, row_y, ctx)).unwrap();
 
-        d.mouse_input(MouseButton::Left, ElementState::Pressed, band_right + 4.0, row_y, &mut ctx);
-        assert!(!d.slider_dragging(), "past the chord column the row is not the band");
-        assert_eq!(d.take_slider_change(), None);
-        d.mouse_input(MouseButton::Left, ElementState::Released, band_right + 4.0, row_y, &mut ctx);
+        ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, band_right + 4.0, row_y, ctx)).unwrap();
+        assert!(!ctx[d].slider_dragging(), "past the chord column the row is not the band");
+        assert_eq!(ctx[d].take_slider_change(), None);
+        ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, band_right + 4.0, row_y, ctx)).unwrap();
 
         // The readout lane sits ahead of the band and takes no hold either.
         let lane_x = row_right - SLIDER_W - 60.0 - 8.0;
-        d.mouse_input(MouseButton::Left, ElementState::Pressed, lane_x + 4.0, row_y, &mut ctx);
-        assert!(!d.slider_dragging(), "the readout is a readout, not a track");
-        assert_eq!(d.take_slider_change(), None);
+        ctx.lend_h(d, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, lane_x + 4.0, row_y, ctx)).unwrap();
+        assert!(!ctx[d].slider_dragging(), "the readout is a readout, not a track");
+        assert_eq!(ctx[d].take_slider_change(), None);
     }
 
     /// A captured pointer hovers no pane. A control lit at the press used to
@@ -15851,7 +15849,7 @@ mod tests {
         assert!(dd_at > dialog_at);
         // And resolvable, which is what the engine's clamp walks: an id the
         // tree has dropped is skipped in silence.
-        assert!(state.ui_context.tree.get_ptr(dd_id).is_some(), "the dropdown is in the widget tree");
+        assert!(state.ui_context.tree.is_registered(dd_id), "the dropdown is in the widget tree");
 
         // Down, Enter: the next unit, the dialog still up.
         state.dialog_key_input(&key_press(Key::Named(NamedKey::ArrowDown)));
diff --git a/src/render.rs b/src/render.rs
index 621ed9a..c150ff8 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -808,13 +808,11 @@ impl State {
         }
 
         // Child elements, then the widget's popover on top of them.
-        for child_ptr in self.ui_context.tree.children_ptrs(w.base().id()) {
-            if let Some(child_idx) = self.find_widget_index(child_ptr as *const ()) {
+        for child_id in self.ui_context.tree.child_ids(w.base().id()) {
+            if let Some(child_idx) = self.find_widget_index(child_id) {
                 self.paint_widget(child_idx, pc, show_cursor, visited, clip, clip_circle);
-            } else {
-                unsafe {
-                    self.paint_element(&*child_ptr, pc, show_cursor, visited, clip, clip_circle);
-                }
+            } else if let Some(child) = self.ui_context.get_widget(child_id) {
+                self.paint_element(child, pc, show_cursor, visited, clip, clip_circle);
             }
         }
 
@@ -839,7 +837,7 @@ impl State {
             return;
         }
 
-        if let Some(idx) = self.find_widget_index(element as *const dyn WidgetHost as *const ()) {
+        if let Some(idx) = self.find_widget_index(element.base().id()) {
             self.paint_widget(idx, pc, show_cursor, visited, clip, clip_circle);
             return;
         }
@@ -847,9 +845,9 @@ impl State {
         append_widget_plate(element, pc);
         element.paint_self(&self.ui_context, pc);
 
-        for child_ptr in self.ui_context.tree.children_ptrs(element.base().id()) {
-            unsafe {
-                self.paint_element(&*child_ptr, pc, show_cursor, visited, clip, clip_circle);
+        for child_id in self.ui_context.tree.child_ids(element.base().id()) {
+            if let Some(child) = self.ui_context.get_widget(child_id) {
+                self.paint_element(child, pc, show_cursor, visited, clip, clip_circle);
             }
         }
     }
diff --git a/src/slots.rs b/src/slots.rs
index 7e48304..e195278 100644
--- a/src/slots.rs
+++ b/src/slots.rs
@@ -127,11 +127,8 @@ widget_roster! {
 impl WidgetSlots {
     /// Roster index of the slot at `target_addr` (a thin widget address — the comparison
     /// never dereferences; callers pass `ptr as *const ()`).
-    pub fn find_index(&self, ui: &UiContext, target_addr: *const ()) -> Option<usize> {
-        (0..WIDGET_COUNT).position(|i| {
-            let w_ptr = self.get_dyn(ui, i) as *const dyn WidgetHost as *const ();
-            w_ptr == target_addr
-        })
+    pub fn find_index(&self, ui: &UiContext, id: cce_ui::widget::WidgetId) -> Option<usize> {
+        (0..WIDGET_COUNT).position(|i| self.get_dyn(ui, i).base().id() == id)
     }
 
     // Roster accessors on CONCRETE types (Phase 6aw, controller decision option 2): each