GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(context): register_widget is an unsafe fn; register by reference
register_widget(id, ptr) read a raw pointer inside a safe function — the
last two of clippy's not_unsafe_ptr_arg_deref errors. Every app now
registers through register_host(&mut w) (16 crates moved in the same
sweep), so register_widget becomes an unsafe fn whose contract is the one
WidgetTree::register already states.
The toolkit's own paths that hold a borrow moved too: render_widget, the
paginator's sidebar, the tree list's fields, and Adapted::set_parent,
which now takes its parent by reference (every caller passes None). What
still registers a pointer does so in an unsafe block with its SAFETY:
link_parent_child, whose trait objects are not 'static, and the tests
that drive raw pointers on purpose. The demo and cce-relief swap their
`roots() -> [*mut dyn WidgetHost; N]` helpers for a register_roots() that
registers each field by reference — the pattern new apps copy.
All 30 dependents check; the suites of the 16 changed apps pass;
cce-fonts (a filter rebuilding its rows), cce-designer, the demo,
cce-weather and cce-data-editor driven in a shadow behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 13 ++++++---
src/bin/cce-ramp.rs | 8 +++---
src/bin/cce-relief.rs | 54 +++++++++++++++++--------------------
src/context.rs | 41 ++++++++++++++++------------
src/layout.rs | 5 +---
src/main.rs | 30 +++++++++------------
src/scene/painter.rs | 6 +++--
src/widget/container/group.rs | 3 ++-
src/widget/container/menu.rs | 6 ++---
src/widget/container/paginator.rs | 9 +++----
src/widget/container/spreadsheet.rs | 30 ++++++++-------------
src/widget/container/treelist.rs | 23 +++++-----------
src/widget/core.rs | 7 +++--
src/widget/display/graph.rs | 21 +++++----------
src/widget/display/node.rs | 3 +--
src/widget/input/bevel_preview.rs | 4 +--
src/widget/input/button.rs | 7 +++--
src/widget/input/checkbox.rs | 8 +++---
src/widget/input/slider.rs | 10 +++----
src/widget/input/spinbox.rs | 9 +++----
src/widget/model.rs | 33 ++++++++++++-----------
21 files changed, 151 insertions(+), 179 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 6f859a7..9127973 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1580,10 +1580,15 @@ returned by value): the token moves with it. Register from a live borrow before
reads it, as the apps' rebuilds do. The sound end state is a registry that owns its widgets.
The pointer-taking entry points say so:
-- `WidgetTree::register`, `UiContext::set_focused_ptr`, `show_context_menu` and
- `handle_right_click` are `unsafe fn`.
-- `register_widget` stays safe only because 105 app call sites use it. New code calls
- `register_host(&mut w)`.
+- `WidgetTree::register`, `UiContext::register_widget`, `set_focused_ptr`,
+ `show_context_menu` and `handle_right_click` are `unsafe fn`.
+- `Adapted::set_parent` takes its parent by reference.
+- **Register a widget with `register_host(&mut w)`.** Every app moved to it, and so did
+ the toolkit's own paths that hold a borrow (`render_widget`, the paginator, the tree list).
+- `register_widget` remains for the paths that only have a pointer: `link_parent_child`,
+ whose trait objects are not `'static`, and tests that exercise raw pointers.
+- The demo's `register_roots` is the pattern to copy. The `roots()` helpers that returned
+ `[*mut dyn WidgetHost; N]` for a registration loop are gone.
`a_dropped_widget_is_never_handed_out` and `a_clone_has_a_liveness_of_its_own` are the tests.
diff --git a/src/bin/cce-ramp.rs b/src/bin/cce-ramp.rs
index 4c9a08f..fb8d27b 100644
--- a/src/bin/cce-ramp.rs
+++ b/src/bin/cce-ramp.rs
@@ -189,11 +189,9 @@ impl Application for RampPopup {
fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<DisplayList> {
if !self.registered {
self.registered = true;
- let w = self.ramp.as_ptr_mut();
- let id = self.ramp.id();
- self.ui_context.register_widget(id, w);
- self.ui_context.register_widget(self.save_button.id(), self.save_button.as_ptr_mut());
- self.ui_context.register_widget(self.cancel_button.id(), self.cancel_button.as_ptr_mut());
+ self.ui_context.register_host(&mut self.ramp);
+ self.ui_context.register_host(&mut self.save_button);
+ self.ui_context.register_host(&mut self.cancel_button);
}
let size_changed = self.width != size.width as u32
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 6f7c369..3ea70cc 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -1065,28 +1065,30 @@ impl BevelPopup {
]
}
- fn roots(&mut self) -> [*mut (dyn WidgetHost + 'static); 19] {
- [
- self.profile_dropdown.as_ptr_mut(),
- self.edge_dropdown.as_ptr_mut(),
- self.wall.shoulder.as_ptr_mut(),
- self.wall.base.as_ptr_mut(),
- self.wall.bias.as_ptr_mut(),
- self.edge.shoulder.as_ptr_mut(),
- self.edge.base.as_ptr_mut(),
- self.edge.bias.as_ptr_mut(),
- self.depth_slider.as_ptr_mut(),
- self.width_slider.as_ptr_mut(),
- self.height_slider.as_ptr_mut(),
- self.spec_slider.as_ptr_mut(),
- self.shine_slider.as_ptr_mut(),
- self.curv_slider.as_ptr_mut(),
- self.comp_slider.as_ptr_mut(),
- self.refr_slider.as_ptr_mut(),
- self.radius_slider.as_ptr_mut(),
- self.save_button.as_ptr_mut(),
- self.cancel_button.as_ptr_mut(),
- ]
+ /// Register every dispatch root by reference: the registry keeps a pointer to each
+ /// and resolves it only while the widget lives, so register once `self` is at its
+ /// final address (see `display_list`).
+ fn register_roots(&mut self) {
+ let ctx = &mut self.ui_context;
+ ctx.register_host(&mut self.profile_dropdown);
+ ctx.register_host(&mut self.edge_dropdown);
+ ctx.register_host(&mut self.wall.shoulder);
+ ctx.register_host(&mut self.wall.base);
+ ctx.register_host(&mut self.wall.bias);
+ ctx.register_host(&mut self.edge.shoulder);
+ ctx.register_host(&mut self.edge.base);
+ ctx.register_host(&mut self.edge.bias);
+ ctx.register_host(&mut self.depth_slider);
+ ctx.register_host(&mut self.width_slider);
+ ctx.register_host(&mut self.height_slider);
+ ctx.register_host(&mut self.spec_slider);
+ ctx.register_host(&mut self.shine_slider);
+ ctx.register_host(&mut self.curv_slider);
+ ctx.register_host(&mut self.comp_slider);
+ ctx.register_host(&mut self.refr_slider);
+ ctx.register_host(&mut self.radius_slider);
+ ctx.register_host(&mut self.save_button);
+ ctx.register_host(&mut self.cancel_button);
}
/// The pane rung's bound material name, when config binds one — the
@@ -1744,13 +1746,7 @@ impl Application for BevelPopup {
fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<DisplayList> {
if !self.registered {
self.registered = true;
- let self_ptr = self as *mut Self;
- unsafe {
- for w in (*self_ptr).roots() {
- let id = (*w).base().id();
- self.ui_context.register_widget(id, w);
- }
- }
+ self.register_roots();
}
let size_changed = self.width != size.width as u32
diff --git a/src/context.rs b/src/context.rs
index 3f141d5..7fcc1cb 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -774,18 +774,19 @@ impl UiContext {
/// registered again before the next pass reads it.
pub fn register_host(&mut self, w: &mut (dyn WidgetHost + 'static)) {
let id = w.base().id();
- self.register_widget(id, w as *mut (dyn WidgetHost + 'static));
+ // SAFETY: derived from the live borrow we were handed.
+ unsafe { self.register_widget(id, w as *mut (dyn WidgetHost + 'static)) };
}
- /// Register a widget by raw pointer — the legacy form 105 app call sites still use
- /// (`ctx.register_widget(id, w.as_ptr_mut())`). Prefer [`register_host`](Self::register_host).
+ /// Register a widget by raw pointer. Prefer [`register_host`](Self::register_host), which
+ /// takes a reference; this form is for the toolkit's own pointer-routed paths.
+ ///
+ /// # Safety
///
- /// `ptr` must be null or point to a live widget AT THE CALL; it is read here. Taking a raw
- /// pointer in a safe function is unsound in principle (clippy says so) and is kept only until
- /// the apps have moved to `register_host`; a pointer derived from a live borrow just before
- /// the call, which is every call site today, meets the requirement.
- pub fn register_widget(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
- // SAFETY: the documented precondition above.
+ /// `ptr` must be null or point to a live widget at the call; it is read here (see
+ /// [`WidgetTree::register`](crate::scene::tree::WidgetTree::register)).
+ pub unsafe fn register_widget(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
+ // SAFETY: the caller's contract.
unsafe { self.tree.register(id, ptr) };
// A newcomer may itself have a popover rect, so the coverage memo can no
// longer be trusted. Pages that re-register a whole list do it before
@@ -1281,7 +1282,8 @@ mod tests {
WidgetHost::set_rect(&mut slider, 0.0, 0.0, 200.0, 30.0);
let ptr = slider.as_ptr_mut();
let id = slider.base().id();
- ctx.register_widget(id, ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(id, ptr) };
let press = Event::MouseButton {
button: MouseButton::Left,
@@ -1329,13 +1331,14 @@ mod tests {
fn multi_root_press_dispatch_keeps_the_drag_target() {
let mut ctx = UiContext::new();
let mut slider = crate::widget::Slider::new().with_value(0.5);
- let (id, ptr) = (slider.id(), slider.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ let id = slider.id();
+ ctx.register_host(&mut slider);
slider.set_rect(0.0, 0.0, 200.0, 30.0);
let mut other = Block { base: Widget::new_rect(300.0, 300.0, 50.0, 50.0) };
let other_ptr = &mut other as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
let other_id = other.base.id();
- ctx.register_widget(other_id, other_ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(other_id, other_ptr) };
let press = Event::MouseButton {
button: MouseButton::Left,
@@ -1394,7 +1397,8 @@ mod tests {
let mut ctx = UiContext::new();
let mut w = Block { base: Widget::new_rect(10.0, 10.0, 50.0, 50.0) };
let ptr = &mut w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
- ctx.register_widget(w.base.id(), ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(w.base.id(), ptr) };
ctx.rebuild_spatial_grid();
assert!(ctx.drag_allowed_at(200.0, 200.0), "empty surface is draggable");
@@ -1423,7 +1427,8 @@ mod focus_step_tests {
for w in [&mut b as &mut dyn WidgetHost, &mut a, &mut t] {
let (id, ptr) = (w.base().id(), w as *mut dyn WidgetHost);
let ptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(ptr) };
- ctx.register_widget(id, ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(id, ptr) };
}
let (ia, ib, it) = (a.id(), b.id(), t.id());
@@ -1450,7 +1455,8 @@ mod focus_step_tests {
let mut g = crate::widget::Group::new(vec![ia, it]);
let (gid, gptr) = (g.base().id(), &mut g as *mut dyn WidgetHost);
let gptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(gptr) };
- ctx.register_widget(gid, gptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(gid, gptr) };
assert_eq!(ctx.focus_clusters(), vec![vec![ia, it], vec![ib]]);
ctx.set_focused_id(ia);
assert!(ctx.focus_step(false));
@@ -1468,7 +1474,8 @@ mod focus_step_tests {
WidgetHost::set_rect(&mut parked, -1000.0, -1000.0, 1.0, 1.0);
let (pid, pptr) = (parked.base().id(), &mut parked as *mut dyn WidgetHost);
let pptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(pptr) };
- ctx.register_widget(pid, pptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(pid, pptr) };
for _ in 0..4 {
ctx.focus_step(false);
assert!(!ctx.is_focused_id(pid), "the parked plate never takes focus");
diff --git a/src/layout.rs b/src/layout.rs
index cb81482..a404298 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -4220,10 +4220,7 @@ impl RenderTarget for PopoverCollector {
pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
- let id = Some(w.base().id());
- if let Some(w_id) = id {
- ctx.register_widget(w_id, w as *mut T as *mut (dyn WidgetHost + 'static));
- }
+ ctx.register_host(w);
// The flat-host contract, the same block `set_rect` takes: `(x, y)` is the top of
// the detached label and `wh` the block height, label strip included. `layout`
// takes the CONTENT origin and height, so step down by the strip.
diff --git a/src/main.rs b/src/main.rs
index c6455e9..c5dfb34 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -95,16 +95,18 @@ impl DemoApp {
/// The widget roots as pointers, for the one genuinely pointer-consuming path left:
/// registration (the registry stores them). The paint walk takes shared borrows.
- fn roots(&mut self) -> [*mut (dyn WidgetHost + 'static); 7] {
- [
- self.button.as_ptr_mut(),
- self.toggle.as_ptr_mut(),
- self.slider.as_ptr_mut(),
- self.name_box.as_ptr_mut(),
- self.theme_dropdown.as_ptr_mut(),
- self.image_contain.as_ptr_mut(),
- self.image_stretch.as_ptr_mut(),
- ]
+ /// Register every dispatch root by reference: the registry keeps a pointer to each
+ /// and resolves it only while the widget lives, so register once `self` is at its
+ /// final address (see `display_list`).
+ fn register_roots(&mut self) {
+ let ctx = &mut self.ui_context;
+ ctx.register_host(&mut self.button);
+ ctx.register_host(&mut self.toggle);
+ ctx.register_host(&mut self.slider);
+ ctx.register_host(&mut self.name_box);
+ ctx.register_host(&mut self.theme_dropdown);
+ ctx.register_host(&mut self.image_contain);
+ ctx.register_host(&mut self.image_stretch);
}
/// `take_*` plumbing: translate widget changes into app state. Runs after any routed
@@ -231,13 +233,7 @@ impl Application for DemoApp {
// `self` sits at its final address — hence here, not in `new()`.
if !self.widgets_registered {
self.widgets_registered = true;
- let self_ptr = self as *mut Self;
- unsafe {
- for w in (*self_ptr).roots() {
- let id = (*w).base().id();
- self.ui_context.register_widget(id, w);
- }
- }
+ self.register_roots();
}
let size_changed = self.width != size.width as u32
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index 433521d..e5b4392 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -239,7 +239,8 @@ mod tests {
fn reg(ctx: &mut UiContext, w: &mut P) -> (crate::widget::WidgetId, ElemPtr) {
let ptr = &mut *w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
let id = w.base.id();
- ctx.register_widget(id, ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(id, ptr) };
(id, ptr)
}
@@ -375,7 +376,8 @@ mod tests {
w.base.w = 20.0;
w.base.h = 10.0;
let ptr = &mut w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
- ctx.register_widget(w.base.id(), ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(w.base.id(), ptr) };
let list = paint_tree(&ctx, unsafe { &*ptr });
assert!(
diff --git a/src/widget/container/group.rs b/src/widget/container/group.rs
index 42c997a..1e349b9 100644
--- a/src/widget/container/group.rs
+++ b/src/widget/container/group.rs
@@ -319,7 +319,8 @@ mod tests {
fn register(ctx: &mut UiContext, w: &mut dyn WidgetHost) -> WidgetId {
let id = w.base().id();
let ptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(w as *mut dyn WidgetHost) };
- ctx.register_widget(id, ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(id, ptr) };
id
}
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index a9fa30d..2e812ea 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -1219,8 +1219,7 @@ mod tests {
fn menu_open_click_and_controller_roundtrip() {
let mut ctx = UiContext::new();
let mut mb = bar();
- let (id, ptr) = (mb.id(), mb.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut mb);
WidgetHost::set_rect(&mut mb, 0.0, 0.0, 400.0, 24.0);
// Click the "File" strip button (the strip commits selection on release): the dropdown
@@ -1247,8 +1246,7 @@ mod tests {
fn hidden_menubar_reports_no_menu_and_rejects_hits() {
let mut ctx = UiContext::new();
let mut mb = bar();
- let (id, ptr) = (mb.id(), mb.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut mb);
WidgetHost::set_rect(&mut mb, 0.0, 0.0, 400.0, 24.0);
WidgetHost::set_visible(&mut mb, false);
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index 770cf9f..2114ff2 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -161,8 +161,7 @@ impl Layout for Paginator {
}
fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
- let menu_ptr = self.sidebar_menu.as_ptr_mut();
- ctx.register_widget(self.sidebar_menu.id(), menu_ptr);
+ ctx.register_host(&mut self.sidebar_menu);
ctx.link_ids(host_id, self.sidebar_menu.id());
}
}
@@ -293,8 +292,7 @@ mod tests {
fn sidebar_click_switches_page_and_drains_menu_click() {
let mut ctx = UiContext::new();
let mut p = paginator();
- let (id, ptr) = (p.id(), p.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut p);
// Click the second tab (the strip commits selection on release): the selection moves
// and menu_click reports (1, 0) once.
@@ -316,8 +314,7 @@ mod tests {
fn plain_quads_split_like_legacy_and_registration_heals_on_tick() {
let mut ctx = UiContext::new();
let mut p = paginator();
- let (id, ptr) = (p.id(), p.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut p);
// Legacy split: `extra_quads` is the children's chrome only; the sidebar background
// (a rounded rect) lives in `all_rounded_quads` alone (layout-interface draws its
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index f3bf3e8..d92bc6e 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -1225,8 +1225,7 @@ mod tests {
fn horizontal_wheel_integrates_and_decays_through_tick() {
let mut ctx = UiContext::new();
let mut s = wide(6);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
let wheel = Event::MouseWheel {
delta: MouseScrollDelta::LineDelta(-2.0, 0.0),
@@ -1250,7 +1249,8 @@ mod tests {
// A pane whose columns fit ignores horizontal wheels.
let mut fits = wide(2);
let (fid, fptr) = (fits.id(), fits.as_ptr_mut());
- ctx.register_widget(fid, fptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(fid, fptr) };
assert!(!fits.handle_event(&wheel, &mut ctx), "no overflow, wheel passes through");
}
@@ -1258,8 +1258,7 @@ mod tests {
fn wheel_velocity_integrates_and_decays_through_tick() {
let mut ctx = UiContext::new();
let mut s = filled(50);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
// A wheel over the body feeds velocity (negated delta, the ScrollRegion
// convention: a negative line delta scrolls the view down)…
@@ -1289,8 +1288,7 @@ mod tests {
fn scrollbar_drag_and_keys_move_the_scroll() {
let mut ctx = UiContext::new();
let mut s = filled(50);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
// content 1200, viewport 100 -> overflowing, so the host may drag it.
@@ -1365,8 +1363,7 @@ mod tests {
let mut s = Spreadsheet::new();
s.set_visible(true);
WidgetHost::set_rect(&mut s, 0.0, 0.0, 200.0, 124.0);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
// Numeric strings out of lexicographic order: "10" must sort after "9".
let rows = vec![
vec!["10".to_string(), "b".to_string()],
@@ -1425,8 +1422,7 @@ mod tests {
fn rows_select_alone_toggled_and_in_runs() {
let mut ctx = UiContext::new();
let mut s = filled(50);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
// Rows are 24 tall under a 24 header: row k spans 24 + 24k.
let row_y = |k: usize| 24.0 + 24.0 * k as f32 + 12.0;
@@ -1477,8 +1473,7 @@ mod tests {
let mut t = Spreadsheet::new();
t.set_visible(true);
WidgetHost::set_rect(&mut t, 0.0, 0.0, 200.0, 124.0);
- let (id, ptr) = (t.id(), t.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut t);
let rows = vec![
vec!["10".to_string(), "b".to_string()],
vec!["9".to_string(), "c".to_string()],
@@ -1511,8 +1506,7 @@ mod tests {
s.set_visible(true);
let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
WidgetHost::set_rect(&mut s, rect.x, rect.y, rect.width, rect.height);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
let headers: Vec<String> = (0..6).map(|i| format!("c{i}")).collect();
let rows: Vec<Vec<String>> = (0..50).map(|r| (0..6).map(|c| format!("{r}.{c}")).collect()).collect();
SpreadsheetController::set_spreadsheet_data(&mut *s, headers, rows);
@@ -1648,8 +1642,7 @@ mod tests {
let mut s = Spreadsheet::new();
s.set_visible(true);
WidgetHost::set_rect(&mut s, 0.0, 0.0, 200.0, 124.0);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
let n = 1000;
SpreadsheetController::set_spreadsheet_columns(
&mut *s,
@@ -1696,8 +1689,7 @@ mod tests {
let mut s = Spreadsheet::new();
s.set_visible(true);
WidgetHost::set_rect(&mut s, 0.0, 0.0, 200.0, 124.0);
- let (id, ptr) = (s.id(), s.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut s);
SpreadsheetController::set_spreadsheet_data(
&mut *s,
vec!["a".into(), "b".into()],
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index e523fdb..38ffeb2 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -488,9 +488,8 @@ impl TreeList {
self.edit_box.cursor_idx = self.edit_box.text.chars().count();
self.edit_box.select_anchor = Some(0);
- let eb_ptr = self.edit_box.as_ptr_mut();
let eb_id = self.edit_box.base().id();
- ui.register_widget(eb_id, eb_ptr);
+ ui.register_host(&mut self.edit_box);
ui.link_ids(host_id, eb_id);
ui.set_focused(&mut self.edit_box);
@@ -697,22 +696,14 @@ impl Layout for TreeList {
// opened, and the wheel died the same way). Registration alone keeps the ids
// resolvable for focus, coverage, and the spatial grid.
let _ = host_id;
- let sb_ptr = self.search_box.as_ptr_mut();
- let sb_id = self.search_box.base().id();
- ctx.register_widget(sb_id, sb_ptr);
+ ctx.register_host(&mut self.search_box);
- let btn_ptr = self.add_key_btn.as_ptr_mut();
- let btn_id = self.add_key_btn.base().id();
- ctx.register_widget(btn_id, btn_ptr);
+ ctx.register_host(&mut self.add_key_btn);
- let pop_ptr = self.add_key_popover_box.as_ptr_mut();
- let pop_id = self.add_key_popover_box.base().id();
- ctx.register_widget(pop_id, pop_ptr);
+ ctx.register_host(&mut self.add_key_popover_box);
if self.editing_key_idx.is_some() {
- let eb_ptr = self.edit_box.as_ptr_mut();
- let eb_id = self.edit_box.base().id();
- ctx.register_widget(eb_id, eb_ptr);
+ ctx.register_host(&mut self.edit_box);
}
}
}
@@ -1433,7 +1424,7 @@ mod tests {
let mut tree_list = TreeList::new();
tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
- ctx.register_widget(tree_list.base().id(), tree_list.as_ptr_mut());
+ ctx.register_host(&mut tree_list);
ctx.tick(0.016);
ctx.clear_dirty();
@@ -1447,7 +1438,7 @@ mod tests {
let mut ctx = UiContext::new();
let mut tree_list = TreeList::new();
- ctx.register_widget(tree_list.base().id(), tree_list.as_ptr_mut());
+ ctx.register_host(&mut tree_list);
ctx.rebuild_spatial_grid();
let list_top = 52.0;
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 0b522b1..f6a5b0e 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -77,8 +77,11 @@ pub mod focus {
std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(child as *mut dyn WidgetHost)
};
let (p_id, c_id) = (parent.base().id(), child.base().id());
- ctx.register_widget(p_id, parent_ptr);
- ctx.register_widget(c_id, child_ptr);
+ // SAFETY: both derived from the live borrows we were handed.
+ unsafe {
+ ctx.register_widget(p_id, parent_ptr);
+ ctx.register_widget(c_id, child_ptr);
+ }
// The old add_child + set_parent pair, as the tree ops they always were.
ctx.tree.link(p_id, c_id);
ctx.tree.set_parent(c_id, Some(p_id));
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 9a2376d..7af03f9 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -1822,8 +1822,7 @@ mod tests {
fn double_click_survives_the_between_press_node_resync() {
let mut ctx = UiContext::new();
let mut g = two_nodes();
- let (id, ptr) = (g.id(), g.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut g);
// First press on node a, then the host re-syncs (same content),
// then the second press: this is the real event sequence.
@@ -1843,8 +1842,7 @@ mod tests {
fn presses_on_two_nodes_are_not_a_double_click() {
let mut ctx = UiContext::new();
let mut g = two_nodes();
- let (id, ptr) = (g.id(), g.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut g);
assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, &mut ctx));
g.mouse_input(MouseButton::Left, ElementState::Released, 110.0, 120.0, &mut ctx);
@@ -1857,8 +1855,7 @@ mod tests {
fn node_press_selects_arms_drag_and_commit_snaps_to_grid() {
let mut ctx = UiContext::new();
let mut g = two_nodes();
- let (id, ptr) = (g.id(), g.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut g);
// Node a occupies (100, 100, 80, 40). Press its body (away from ports/toggle).
assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, &mut ctx));
@@ -1916,8 +1913,7 @@ mod tests {
};
// alpha above beta, beta reading alpha.
g.set_nodes(&[node("a", "alpha", 0.0, 0.0, ""), node("b", "beta", 0.0, 2.0, "alpha")]);
- let (id, ptr) = (g.id(), g.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut g);
// Drag alpha by its middle onto beta's middle.
assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 140.0, 120.0, &mut ctx));
g.drag_begin(140.0, 120.0);
@@ -1977,8 +1973,7 @@ mod tests {
node("b", "beta", 2.0, 0.0, p("alpha")),
node("c", "gamma", 0.0, 2.0, p("")),
]);
- let (id, ptr) = (g.id(), g.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut g);
// Drag gamma's body onto the wire's horizontal run (cell (1, 0)).
assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 240.0, &mut ctx));
@@ -2216,8 +2211,7 @@ mod tests {
fn port_click_starts_and_completes_a_connection() {
let mut ctx = UiContext::new();
let mut g = two_nodes();
- let (id, ptr) = (g.id(), g.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut g);
// Ports float OUTSIDE the node box (port_center): node a's output
// hangs below the bottom-center of (100,100,80,40), node b's input
@@ -2240,8 +2234,7 @@ mod tests {
fn every_node_parameter_is_a_wire_into_its_own_port() {
let mut ctx = UiContext::new();
let mut g = two_nodes();
- let (id, ptr) = (g.id(), g.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut g);
let wire = |n: &str, v: &str| (n.to_string(), v.to_string(), "node".to_string());
let node = |id: &str, col: f32, row: f32, parameters: Vec<(String, String, String)>, inputs: usize| GraphNode {
id: id.into(),
diff --git a/src/widget/display/node.rs b/src/widget/display/node.rs
index cf96c18..9ea1d45 100644
--- a/src/widget/display/node.rs
+++ b/src/widget/display/node.rs
@@ -262,8 +262,7 @@ mod tests {
fn toggle_click_flips_geom_and_press_starts_drag() {
let mut ctx = UiContext::new();
let mut node = Node::new(100.0, 100.0, 120.0, 40.0, "geo1");
- let (id, ptr) = (node.id(), node.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut node);
// Toggle zone: (100+120-30, 100+11) => 18x18 at (190, 111).
assert!(node.mouse_input(MouseButton::Left, ElementState::Pressed, 195.0, 115.0, &mut ctx));
diff --git a/src/widget/input/bevel_preview.rs b/src/widget/input/bevel_preview.rs
index 9010690..6271582 100644
--- a/src/widget/input/bevel_preview.rs
+++ b/src/widget/input/bevel_preview.rs
@@ -202,8 +202,8 @@ mod tests {
fn click_reports_once() {
let mut ctx = UiContext::new();
let mut p = BevelPreview::new();
- let (id, ptr) = (p.id(), p.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ let id = p.id();
+ ctx.register_host(&mut p);
WidgetHost::set_rect(&mut p, 0.0, 0.0, 125.0, 26.0);
let ev = Event::MouseButton {
button: MouseButton::Left,
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index a831b44..ac1032b 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -757,8 +757,7 @@ mod tests {
use crate::scene::paint::Prim;
let mut ctx = UiContext::new();
let mut b = Button::new(0.0, 0.0, 0.0, 0.0).with_label("Load Images");
- let (id, ptr) = (b.id(), b.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut b);
let size = b.intrinsic_size().unwrap();
WidgetHost::set_rect(&mut b, 10.0, 20.0, size.width, size.height);
@@ -818,8 +817,8 @@ mod tests {
let mut b = Button::new(10.0, 10.0, 80.0, 24.0)
.with_label("Go")
.on_click(move || { fired2.fetch_add(1, std::sync::atomic::Ordering::SeqCst); });
- let (id, ptr) = (b.id(), b.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ let id = b.id();
+ ctx.register_host(&mut b);
// Press in, release in -> click.
assert!(ctx.propagate_event(&press(20.0, 20.0), id));
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 28257a5..b209aa3 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -648,8 +648,8 @@ mod tests {
fn checkbox_click_toggles_and_polls_like_legacy() {
let mut ctx = UiContext::new();
let mut cb = Checkbox::new();
- let (id, ptr) = (cb.id(), cb.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ let id = cb.id();
+ ctx.register_host(&mut cb);
WidgetHost::set_rect(&mut cb, 0.0, 0.0, 20.0, 20.0);
assert!(ctx.propagate_event(&click_at(10.0, 10.0), id), "in-rect click consumed");
@@ -766,8 +766,8 @@ mod tests {
fn toggle_click_glides_the_run_across_its_field() {
let mut ctx = UiContext::new();
let mut t = Toggle::new();
- let (id, ptr) = (t.id(), t.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ let id = t.id();
+ ctx.register_host(&mut t);
WidgetHost::set_rect(&mut t, 0.0, 0.0, 60.0, 30.0);
let rect = Rect { x: 0.0, y: 0.0, width: 60.0, height: 30.0 };
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 8e1829f..e2f016b 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -1126,8 +1126,8 @@ fn probe_slider_bridge() {
fn slider_press_drag_and_wheel() {
let mut ctx = UiContext::new();
let mut sl = Slider::new().with_value(0.5);
- let (id, ptr) = (sl.id(), sl.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ let id = sl.id();
+ ctx.register_host(&mut sl);
WidgetHost::set_rect(&mut sl, 0.0, 0.0, 100.0, 20.0);
// Press on the track grabs the thumb.
@@ -1161,8 +1161,7 @@ fn probe_slider_bridge() {
let notch = |min: f32, max: f32, at: f32| -> f32 {
let mut ctx = UiContext::new();
let mut sl = Slider::new().with_range(min, max);
- let (id, ptr) = (sl.id(), sl.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut sl);
WidgetHost::set_rect(&mut sl, 0.0, 0.0, 200.0, 20.0);
sl.inner_mut().set_scaled_value(at);
let before = sl.inner().get_scaled_value();
@@ -1207,8 +1206,7 @@ fn probe_slider_bridge() {
fn a_slider_hovers_under_the_pointer() {
let mut ctx = UiContext::new();
let mut sl = Slider::new();
- let (id, ptr) = (sl.id(), sl.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut sl);
WidgetHost::set_rect(&mut sl, 0.0, 0.0, 100.0, 20.0);
assert!(!sl.inner().hovered());
assert!(sl.on_cursor_moved(50.0, 10.0, &mut ctx), "entering is a change");
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index c4048dd..dea9da9 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -721,8 +721,7 @@ mod tests {
fn spinbox_button_zones_step_the_value() {
let mut ctx = UiContext::new();
let mut sb = Spinbox::new(0, -100, 100, 1);
- let (id, ptr) = (sb.id(), sb.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut sb);
WidgetHost::set_rect(&mut sb, 10.0, 20.0, 100.0, 26.0);
// Legacy test: click at (75, 33) lands in the decrement zone.
@@ -743,8 +742,7 @@ mod tests {
// FocusOut commit resets it to the stale text.
let mut ctx = UiContext::new();
let mut sb = Spinbox::new(6, 0, 100, 1);
- let (id, ptr) = (sb.id(), sb.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut sb);
WidgetHost::set_rect(&mut sb, 10.0, 20.0, 100.0, 26.0);
sb.begin_edit(true);
assert_eq!(sb.edit_buffer, "6");
@@ -764,8 +762,7 @@ mod tests {
use crate::widget::MouseScrollDelta;
let mut ctx = UiContext::new();
let mut sb = Spinbox::new(10, 0, 100, 5);
- let (id, ptr) = (sb.id(), sb.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut sb);
WidgetHost::set_rect(&mut sb, 10.0, 20.0, 100.0, 26.0);
// One notch up steps up, one notch down steps down — and the wheel is consumed.
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 126eac7..1ef36db 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -904,14 +904,13 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
}
/// Register + (un)link this widget under a parent (off `WidgetHost` in 6bd batch 4).
- pub fn set_parent(&mut self, parent: Option<*mut (dyn WidgetHost + 'static)>, ctx: &mut UiContext) {
+ pub fn set_parent(&mut self, parent: Option<&mut (dyn WidgetHost + 'static)>, ctx: &mut UiContext) {
// Replica of the old WidgetHost default: symmetric tree link.
let id = self.base.id();
- if let Some(p_ptr) = parent {
- let p_id = unsafe { (*p_ptr).base().id() };
- ctx.register_widget(p_id, p_ptr);
- let self_ptr = self.as_ptr_mut();
- ctx.register_widget(id, self_ptr);
+ if let Some(p) = parent {
+ let p_id = p.base().id();
+ ctx.register_host(p);
+ ctx.register_host(self);
ctx.tree.set_parent(id, Some(p_id));
} else {
ctx.tree.set_parent(id, None);
@@ -1818,9 +1817,12 @@ mod tests {
let (root_id, root_ptr) = (root.id(), root.as_ptr_mut());
let (a_id, a_ptr) = (a.id(), a.as_ptr_mut());
let (b_id, b_ptr) = (b.id(), b.as_ptr_mut());
- ctx.register_widget(root_id, root_ptr);
- ctx.register_widget(a_id, a_ptr);
- ctx.register_widget(b_id, b_ptr);
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(root_id, root_ptr) };
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(a_id, a_ptr) };
+ // SAFETY: a test widget, live for the whole test.
+ unsafe { ctx.register_widget(b_id, b_ptr) };
ctx.link_ids(root_id, a_id);
ctx.link_ids(root_id, b_id);
@@ -1893,8 +1895,9 @@ mod tests {
use crate::widget::{ElementState, MouseButton};
let mut ctx = UiContext::new();
let mut w = Box::new(Adapted::new(Clicker { clicks: 0, entered: 0, left: 0 }));
- let (id, ptr) = (w.id(), w.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ let id = w.id();
+ ctx.register_host(&mut *w);
+ let ptr = w.as_ptr_mut();
unsafe { (*ptr).set_rect(10.0, 10.0, 40.0, 20.0) };
let click_at = |x: f32, y: f32| Event::MouseButton {
@@ -1982,8 +1985,8 @@ mod tests {
let mut ctx = UiContext::new();
let mut w = Box::new(Adapted::new(Tag));
- let (id, ptr) = (w.id(), w.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut *w);
+ let ptr = w.as_ptr_mut();
unsafe { (*ptr).set_rect(10.0, 20.0, 100.0, 30.0) };
let list = paint_tree(&ctx, unsafe { &*ptr });
@@ -2022,8 +2025,8 @@ mod tests {
let mut ctx = UiContext::new();
let mut w = Box::new(Adapted::new(Tag).with_label("Name"));
- let (id, ptr) = (w.id(), w.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ ctx.register_host(&mut *w);
+ let ptr = w.as_ptr_mut();
unsafe { (*ptr).set_rect(10.0, 20.0, 100.0, 60.0) };
let list = paint_tree(&ctx, unsafe { &*ptr });