GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(context): the registry never hands out a dropped widget's pointer
UiContext's tree holds raw pointers to widgets the app owns, and an app
that dropped a registered widget without unregistering it (a rebuilt Vec
of rows) left a dangling pointer the next registry sweep dereferenced.
Every widget base now carries a Liveness token; each tree entry keeps a
Weak watch on it, and every accessor resolves a pointer only while its
widget exists. A clone gets a token of its own. A widget MOVED while
registered is still not caught (the token moves with it); the sound end
state is a registry that owns its widgets.
The pointer-taking entry points say what they require:
WidgetTree::register, set_focused_ptr, show_context_menu and
handle_right_click are unsafe fn. register_widget stays safe for its 105
app call sites; register_host(&mut w) is the safe form for new code.
Adapted::add_child, called by nothing, is gone, and set_parent no longer
registers a *mut derived from &self.
a_dropped_widget_is_never_handed_out and a_clone_has_a_liveness_of_its_own
are the tests. All 29 dependent apps check unchanged but cce-gallery (one
call moved into its unsafe block); cce-system-interface and cce-files
driven in a shadow (focus, a filter rebuilding its rows, popovers, both
context-menu paths, navigation) behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 20 ++++++
src/context.rs | 61 +++++++++++++----
src/scene/tree.rs | 138 +++++++++++++++++++++++++++++----------
src/widget/container/treelist.rs | 15 +++--
src/widget/core.rs | 46 +++++++++++++
src/widget/model.rs | 28 ++------
6 files changed, 234 insertions(+), 74 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 1f57aa5..6f859a7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1567,6 +1567,26 @@ are GONE from the trait — events route through `handle_event`, and apps drain
through the concrete inherent `Adapted<W>` methods. See the RFC's blueprint notes before
adding anything to this trait.
+### The registry holds pointers, and knows when they die
+
+`UiContext`'s tree (`scene::tree::WidgetTree`) does not own its widgets: the app does, and
+registers raw pointers to them. Each widget's `Widget` base carries a `Liveness` token, and every
+entry keeps a watch on it; every accessor (`get_ptr`, `children_ptrs`, `iter_registered`, …)
+resolves a pointer only while its widget exists. A widget dropped without `unregister_widget`
+reads back as absent instead of as freed memory. A clone gets a token of its own.
+
+What it does NOT catch is a widget MOVED while registered (a `Vec` that reallocated, a struct
+returned by value): the token moves with it. Register from a live borrow before the pass that
+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)`.
+
+`a_dropped_widget_is_never_handed_out` and `a_clone_has_a_liveness_of_its_own` are the tests.
+
**Runtime verification matters here.** Several scene changes are "compiles + tests pass; runtime
verification pending" per the RFC — the headless tests can't catch paint/event regressions. When
changing scene wiring, `cargo run` a real client (cce-files, cce-designer, cce-graph,
diff --git a/src/context.rs b/src/context.rs
index de16d7a..3f141d5 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -495,19 +495,24 @@ impl UiContext {
let new_ptr = unsafe {
std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(w as *mut dyn WidgetHost)
};
- self.tree.register(id, new_ptr);
+ // SAFETY: derived from the live borrow we were handed.
+ unsafe { self.tree.register(id, new_ptr) };
self.set_focused_id(id);
}
- /// Transitional pointer form (TreeList focuses its adapter via `EventCtx::host_ptr`). The
- /// pointer must be live at the call — it is only used to derive the id and refresh the
- /// registry, never stored.
- pub fn set_focused_ptr(&mut self, new_ptr: *mut (dyn WidgetHost + 'static)) {
+ /// Transitional pointer form (TreeList focuses its adapter via `EventCtx::host_ptr`).
+ ///
+ /// # Safety
+ ///
+ /// `new_ptr` must be null or point to a live widget at the call. It is read to derive the
+ /// id and refresh the registry (see [`WidgetTree::register`](crate::scene::tree::WidgetTree::register)).
+ pub unsafe fn set_focused_ptr(&mut self, new_ptr: *mut (dyn WidgetHost + 'static)) {
if new_ptr.is_null() {
return;
}
+ // SAFETY: the caller's contract.
let id = unsafe { (*new_ptr).base().id() };
- self.tree.register(id, new_ptr);
+ unsafe { self.tree.register(id, new_ptr) };
self.set_focused_id(id);
}
@@ -761,8 +766,27 @@ impl UiContext {
// walked an empty dummy context (provably inert). Section-level keyboard nav lives
// app-side (settings' focused_section machinery).
+ /// Register a widget the app owns, by reference: the safe form of
+ /// [`register_widget`](Self::register_widget). `ctx.register_host(&mut self.button)`.
+ ///
+ /// The registry keeps a pointer to the widget and resolves it only while the widget has not
+ /// been dropped (see `widget::core::Liveness`); a widget MOVED after registering must be
+ /// 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));
+ }
+
+ /// 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).
+ ///
+ /// `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)) {
- self.tree.register(id, ptr);
+ // SAFETY: the documented precondition above.
+ 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
// dispatching, so the memo is rebuilt once and then serves every root.
@@ -895,7 +919,8 @@ impl UiContext {
/// pointer we are handed (the occlusion walks resolve the stored id through the tree).
pub fn register_popover(&mut self, w: &mut (dyn WidgetHost + 'static)) {
let id = w.base().id();
- self.tree.register(id, w as *mut (dyn WidgetHost + 'static));
+ // SAFETY: derived from the live borrow we were handed.
+ unsafe { self.tree.register(id, w as *mut (dyn WidgetHost + 'static)) };
if !self.active_popovers.contains(&id) {
self.active_popovers.push(id);
}
@@ -999,16 +1024,27 @@ impl UiContext {
crate::widget::context_menu::is_visible()
}
- pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, header_count: usize, target: *mut (dyn WidgetHost + 'static)) {
+ /// Open the shared context menu on `target`.
+ ///
+ /// # Safety
+ ///
+ /// `target` must be null or point to a live widget at the call.
+ pub unsafe fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, header_count: usize, target: *mut (dyn WidgetHost + 'static)) {
if target.is_null() {
return;
}
+ // SAFETY: the caller's contract.
let id = unsafe { (*target).base().id() };
- self.tree.register(id, target);
+ unsafe { self.tree.register(id, target) };
crate::widget::context_menu::show(x, y, options, header_count, id);
}
- pub fn handle_right_click(&mut self, target: *mut (dyn WidgetHost + 'static), px: f32, py: f32) {
+ /// Open the shared config context menu for a right-click on `target`.
+ ///
+ /// # Safety
+ ///
+ /// `target` must be null or point to a live widget at the call.
+ pub unsafe fn handle_right_click(&mut self, target: *mut (dyn WidgetHost + 'static), px: f32, py: f32) {
if target.is_null() {
return;
}
@@ -1092,7 +1128,8 @@ impl UiContext {
let scroll_y = crate::widget::hover_animation::get_scroll_offset();
let adjusted_py = py - scroll_y;
- self.show_context_menu(px, adjusted_py, options, header_count, target);
+ // SAFETY: the caller's contract, passed on.
+ unsafe { self.show_context_menu(px, adjusted_py, options, header_count, target) };
}
pub fn hide_context_menu(&mut self) {
diff --git a/src/scene/tree.rs b/src/scene/tree.rs
index ae1d538..bf05ffb 100644
--- a/src/scene/tree.rs
+++ b/src/scene/tree.rs
@@ -22,30 +22,42 @@
//! apps (paint recursion and event propagation both read `children`). See
//! `docs/rfc-core-rebuild.md` Phase 1b.
//!
-//! Nothing here is wired into `UiContext` yet; this is the tested drop-in the swap will use.
+//! `UiContext` keeps its tree here (`UiContext::tree`).
+//!
+//! ## What a pointer here is worth
+//!
+//! The tree does not own its widgets; the app does, and registers raw pointers to them. Each
+//! entry also keeps a watch on the widget's liveness token (`widget::core::Liveness`), and
+//! every accessor resolves a pointer only while that token exists — so a widget dropped
+//! without being unregistered reads back as absent rather than as a pointer to freed memory.
+//! A widget MOVED while registered is not caught (its token moves with it); see `Liveness`.
use std::collections::HashMap;
+use std::sync::Weak;
use crate::scene::arena::{Arena, NodeId};
use crate::widget::{WidgetHost, WidgetId};
-/// One arena node's payload: the widget's stable id plus its live pointer. The pointer is `None`
-/// for a node that has been *linked* into the tree (as a parent/child) but not yet *registered*
-/// with a real widget — mirroring the legacy maps, where a `layout_tree` link can precede the
-/// `widget_registry` entry. (`*mut dyn WidgetHost` is a fat pointer, so `Option` is the natural
-/// "absent" representation — there is no thin null to use as a sentinel.)
-#[derive(Clone, Copy)]
+/// One arena node's payload: the widget's stable id, its pointer, and a watch on the widget's
+/// liveness token. The pointer is `None` for a node that has been *linked* into the tree (as a
+/// parent/child) but not yet *registered* with a real widget — mirroring the legacy maps, where a
+/// `layout_tree` link can precede the `widget_registry` entry. (`*mut dyn WidgetHost` is a fat
+/// pointer, so `Option` is the natural "absent" representation — there is no thin null to use as
+/// a sentinel.) `alive` is `None` exactly when there is no non-null pointer to watch.
+#[derive(Clone)]
struct Entry {
id: WidgetId,
ptr: Option<*mut (dyn WidgetHost + 'static)>,
+ alive: Option<Weak<()>>,
}
-/// Resolve an entry's pointer to a usable, non-null pointer (skipping link-only and null-data
-/// pointers exactly as the legacy `filter_map` over the registry did).
+/// Resolve an entry's pointer to a usable one: non-null (skipping link-only and null-data
+/// pointers exactly as the legacy `filter_map` over the registry did) and naming a widget that
+/// has not been dropped since it was registered.
#[inline]
fn live_ptr(entry: &Entry) -> Option<*mut (dyn WidgetHost + 'static)> {
- match entry.ptr {
- Some(p) if !p.is_null() => Some(p),
+ match (entry.ptr, &entry.alive) {
+ (Some(p), Some(alive)) if !p.is_null() && alive.strong_count() > 0 => Some(p),
_ => None,
}
}
@@ -85,7 +97,7 @@ impl WidgetTree {
return node;
}
}
- let node = self.arena.insert(Entry { id, ptr: None });
+ let node = self.arena.insert(Entry { id, ptr: None, alive: None });
self.by_id.insert(id, node);
node
}
@@ -93,10 +105,20 @@ impl WidgetTree {
/// Register (or overwrite) the live pointer for `id`. Mirrors `register_widget`'s
/// insert-overwrite semantics. Registering a `null` pointer is allowed (the node exists but
/// resolves to `None`), matching the legacy behavior where a link can precede registration.
- pub fn register(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
+ ///
+ /// # Safety
+ ///
+ /// `ptr` must be null or point to a live widget at the call: it is read once here, to take
+ /// a watch on the widget's liveness token. After the call the tree resolves the pointer only
+ /// while that widget has not been dropped.
+ pub unsafe fn register(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
+ // SAFETY: the caller's contract — null, or a live widget.
+ let alive = unsafe { ptr.as_ref() }.map(|w| w.base().live.watch());
let node = self.ensure_node(id);
// `ensure_node` guarantees the node exists.
- self.arena.value_mut(node).unwrap().ptr = Some(ptr);
+ let entry = self.arena.value_mut(node).unwrap();
+ entry.ptr = Some(ptr);
+ entry.alive = alive;
}
/// Make `child` a child of `parent` (deduped, reparenting from any previous parent). Mirrors
@@ -258,7 +280,7 @@ mod tests {
let mut tree = WidgetTree::new();
let (id, ptr) = w.make(1);
assert_eq!(tree.get_ptr(id), None, "unknown id resolves to None");
- tree.register(id, ptr);
+ unsafe { tree.register(id, ptr) };
assert_eq!(tree.get_ptr(id), Some(ptr));
assert!(tree.is_registered(id));
}
@@ -270,8 +292,8 @@ mod tests {
let id = WidgetId(1);
let (_, p1) = w.make(1);
let (_, p2) = w.make(2);
- tree.register(id, p1);
- tree.register(id, p2); // same id, new pointer
+ unsafe { tree.register(id, p1) };
+ unsafe { tree.register(id, p2) }; // same id, new pointer
assert_eq!(tree.get_ptr(id), Some(p2));
assert_eq!(tree.len(), 1, "overwrite must not create a second node");
}
@@ -282,8 +304,8 @@ mod tests {
let mut tree = WidgetTree::new();
let (p, pp) = w.make(1);
let (c, cp) = w.make(2);
- tree.register(p, pp);
- tree.register(c, cp);
+ unsafe { tree.register(p, pp) };
+ unsafe { tree.register(c, cp) };
tree.link(p, c);
tree.link(p, c); // duplicate link is a no-op
@@ -299,9 +321,9 @@ mod tests {
let (a, ap) = w.make(1);
let (b, bp) = w.make(2);
let (c, cp) = w.make(3);
- tree.register(a, ap);
- tree.register(b, bp);
- tree.register(c, cp);
+ unsafe { tree.register(a, ap) };
+ unsafe { tree.register(b, bp) };
+ unsafe { tree.register(c, cp) };
tree.link(a, c);
assert_eq!(tree.child_ids(a), vec![c]);
@@ -318,7 +340,7 @@ mod tests {
let mut w = Widgets::new();
let mut tree = WidgetTree::new();
let (p, pp) = w.make(1);
- tree.register(p, pp);
+ unsafe { tree.register(p, pp) };
let child = WidgetId(2);
tree.link(p, child); // child not registered yet
@@ -326,7 +348,7 @@ mod tests {
assert!(tree.children_ptrs(p).is_empty(), "link-only child has no pointer yet");
let (_, cp) = w.make(2);
- tree.register(child, cp);
+ unsafe { tree.register(child, cp) };
assert_eq!(tree.children_ptrs(p), vec![cp], "now resolvable");
}
@@ -338,8 +360,8 @@ mod tests {
let mut tree = WidgetTree::new();
let (p, pp) = w.make(1);
let (c, cp) = w.make(2);
- tree.register(p, pp);
- tree.register(c, cp);
+ unsafe { tree.register(p, pp) };
+ unsafe { tree.register(c, cp) };
tree.link(p, c);
tree.set_parent(c, None);
@@ -355,9 +377,9 @@ mod tests {
let (p, pp) = w.make(1);
let (c1, c1p) = w.make(2);
let (c2, c2p) = w.make(3);
- tree.register(p, pp);
- tree.register(c1, c1p);
- tree.register(c2, c2p);
+ unsafe { tree.register(p, pp) };
+ unsafe { tree.register(c1, c1p) };
+ unsafe { tree.register(c2, c2p) };
tree.link(p, c1);
tree.link(p, c2);
@@ -373,8 +395,8 @@ mod tests {
let mut tree = WidgetTree::new();
let (p, pp) = w.make(1);
let (c, cp) = w.make(2);
- tree.register(p, pp);
- tree.register(c, cp);
+ unsafe { tree.register(p, pp) };
+ unsafe { tree.register(c, cp) };
tree.link(p, c);
tree.clear_all();
@@ -391,8 +413,8 @@ mod tests {
let mut tree = WidgetTree::new();
let (p, pp) = w.make(1);
let (c, cp) = w.make(2);
- tree.register(p, pp);
- tree.register(c, cp);
+ unsafe { tree.register(p, pp) };
+ unsafe { tree.register(c, cp) };
tree.link(p, c);
tree.remove(p); // removes p and its subtree (c)
@@ -406,10 +428,58 @@ mod tests {
let mut w = Widgets::new();
let mut tree = WidgetTree::new();
let (p, pp) = w.make(1);
- tree.register(p, pp);
+ unsafe { tree.register(p, pp) };
tree.link(p, WidgetId(99)); // link-only, null pointer
let seen: Vec<WidgetId> = tree.iter_registered().map(|(id, _)| id).collect();
assert_eq!(seen, vec![p], "link-only (null) node is not yielded");
}
+
+ #[test]
+ fn a_dropped_widget_is_never_handed_out() {
+ // The app rebuilt its rows and forgot to unregister the old ones: every accessor must
+ // read the dropped widgets as absent, never as a pointer into freed memory.
+ let mut w = Widgets::new();
+ let mut tree = WidgetTree::new();
+ let (p, pp) = w.make(1);
+ let (c, cp) = w.make(2);
+ unsafe { tree.register(p, pp) };
+ unsafe { tree.register(c, cp) };
+ tree.link(p, c);
+ assert_eq!(tree.children_ptrs(p), vec![cp]);
+
+ w.boxes.remove(1); // drop the child, still registered and linked
+ assert_eq!(tree.get_ptr(c), None);
+ assert!(!tree.is_registered(c));
+ assert!(tree.children_ptrs(p).is_empty());
+ assert_eq!(tree.child_ids(p), vec![c], "the link itself is kept, as for a link-only child");
+ let seen: Vec<WidgetId> = tree.iter_registered().map(|(id, _)| id).collect();
+ assert_eq!(seen, vec![p]);
+
+ w.boxes.clear(); // and the parent
+ assert_eq!(tree.get_ptr(p), None);
+ assert_eq!(tree.parent_ptr(c), None);
+ assert_eq!(tree.iter_registered().count(), 0);
+ }
+
+ #[test]
+ fn a_clone_has_a_liveness_of_its_own() {
+ // A clone shares its original's id (the id cell is copied) but is a different widget:
+ // registering it and dropping the original must leave it resolvable, and a clone must
+ // not keep a dropped original resolvable either.
+ let mut w = Widgets::new();
+ let mut tree = WidgetTree::new();
+ let (id, original) = w.make(1);
+ let copy = Box::new(Marker { base: w.boxes[0].base.clone(), tag: 2 });
+ unsafe { tree.register(id, original) };
+ w.boxes.clear();
+ assert_eq!(tree.get_ptr(id), None, "the clone does not keep the original alive");
+
+ let mut copy = copy;
+ let copy_ptr: *mut (dyn WidgetHost + 'static) = &mut *copy;
+ unsafe { tree.register(id, copy_ptr) };
+ assert_eq!(tree.get_ptr(id), Some(copy_ptr));
+ drop(copy);
+ assert_eq!(tree.get_ptr(id), None);
+ }
}
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 1ff19d0..e523fdb 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -453,7 +453,8 @@ impl TreeList {
if button == MouseButton::Left && state == ElementState::Pressed && !covered {
let on_scrollbar = self.scroll_box.hit_test_scrollbar(px, py) || self.scroll_box.scrollbar_dragging;
if !on_scrollbar && px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
- if let Some(h) = host { ui.set_focused_ptr(h); }
+ // SAFETY: `host` is this widget's own adapter, live while its event is routed.
+ if let Some(h) = host { unsafe { ui.set_focused_ptr(h) }; }
let relative_y = py - list_top + self.scroll_box.scroll_y;
let row_idx = (relative_y / self.item_height) as usize;
if row_idx < self.items.len() {
@@ -530,7 +531,8 @@ impl TreeList {
if button == MouseButton::Right && state == ElementState::Pressed && !covered {
if px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
- if let Some(h) = host { ui.set_focused_ptr(h); }
+ // SAFETY: `host` is this widget's own adapter, live while its event is routed.
+ if let Some(h) = host { unsafe { ui.set_focused_ptr(h) }; }
let relative_y = py - list_top + self.scroll_box.scroll_y;
let row_idx = (relative_y / self.item_height) as usize;
if row_idx < self.items.len() {
@@ -549,7 +551,8 @@ impl TreeList {
options.push("Collapse All".to_string());
let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
- if let Some(h) = host { ui.show_context_menu(px, py - scroll_offset, options, 1, h); }
+ // SAFETY: as above — our own adapter, live while its event is routed.
+ if let Some(h) = host { unsafe { ui.show_context_menu(px, py - scroll_offset, options, 1, h) }; }
changed = true;
}
TreeElement::Leaf { original_idx, ref path, ref name, indent, ref val } => {
@@ -569,7 +572,8 @@ impl TreeList {
"Delete".to_string(),
];
let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
- if let Some(h) = host { ui.show_context_menu(px, py - scroll_offset, options, 1, h); }
+ // SAFETY: as above — our own adapter, live while its event is routed.
+ if let Some(h) = host { unsafe { ui.show_context_menu(px, py - scroll_offset, options, 1, h) }; }
changed = true;
}
}
@@ -1176,7 +1180,8 @@ impl Input for TreeList {
let eb_id = self.edit_box.base().id();
ui.unlink_child(host_id, eb_id);
ui.unregister_widget(eb_id);
- if let Some(h) = host { ui.set_focused_ptr(h); }
+ // SAFETY: `host` is this widget's own adapter, live while its event is routed.
+ if let Some(h) = host { unsafe { ui.set_focused_ptr(h) }; }
changed = true;
}
}
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 09d6bcb..0b522b1 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -1954,6 +1954,50 @@ pub struct Widget {
pub dirty: bool,
pub config_file: Option<String>,
pub config_key: Option<String>,
+ /// Lives exactly as long as this base: the registry holds a watch on it and will
+ /// not hand out the widget's pointer once it is gone. See [`Liveness`].
+ pub(crate) live: Liveness,
+}
+
+/// A token owned by a widget's [`Widget`] base, watched by the [`UiContext`] registry
+/// (`scene::tree::WidgetTree`).
+///
+/// The registry holds raw pointers to widgets the APP owns, so an app that drops a
+/// widget without unregistering it (a rebuilt `Vec` of rows — the case
+/// `UiContext::unregister_widget` warns about) used to leave a dangling pointer that
+/// the next registry sweep dereferenced. The registry now keeps a [`Weak`] to this
+/// token beside each pointer and resolves the pointer only while the token is alive,
+/// so a dropped widget reads back as unregistered instead.
+///
+/// It does NOT catch a widget that was MOVED while registered (a `Vec` that
+/// reallocated, a struct returned by value): the token moves with it, and the stored
+/// pointer still names the old address. Registering from a live borrow just before the
+/// pass that uses it, as every app's rebuild does, is what keeps that case sound until
+/// the registry owns its widgets.
+///
+/// A clone is a different widget at a different address, so it gets a fresh token —
+/// not a share of the original's, which would keep a dropped original "alive".
+///
+/// [`UiContext`]: crate::context::UiContext
+/// [`Weak`]: std::sync::Weak
+#[derive(Debug)]
+pub(crate) struct Liveness(std::sync::Arc<()>);
+
+impl Liveness {
+ pub(crate) fn new() -> Self {
+ Liveness(std::sync::Arc::new(()))
+ }
+
+ /// A watch that reports whether this token still exists.
+ pub(crate) fn watch(&self) -> std::sync::Weak<()> {
+ std::sync::Arc::downgrade(&self.0)
+ }
+}
+
+impl Clone for Liveness {
+ fn clone(&self) -> Self {
+ Liveness::new()
+ }
}
impl Widget {
@@ -1972,6 +2016,7 @@ impl Widget {
dirty: true,
config_file: None,
config_key: None,
+ live: Liveness::new(),
}
}
@@ -1990,6 +2035,7 @@ impl Widget {
dirty: true,
config_file: None,
config_key: None,
+ live: Liveness::new(),
}
}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 30f614f..126eac7 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -370,7 +370,8 @@ impl EventCtx<'_> {
/// `on_event`, not after it. No-op outside a routed path (no ctx or no self pointer).
pub fn open_context_menu(&mut self, px: f32, py: f32) {
if let (Some(ptr), Some(ui)) = (self.self_ptr, self.ui.as_deref_mut()) {
- ui.handle_right_click(ptr, px, py);
+ // SAFETY: `self_ptr` is the routed widget's own adapter, live for the event.
+ unsafe { ui.handle_right_click(ptr, px, py) };
}
}
@@ -902,26 +903,6 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
}
}
- /// Register + link a child under this widget (off `WidgetHost` in 6bd batch 4; dyn callers
- /// went to `focus::link_parent_child`/tree ops).
- pub fn add_child(&mut self, child: *mut (dyn WidgetHost + 'static), ctx: &mut UiContext) {
- // The old WidgetHost default's tree link…
- let c_id = unsafe { (*child).base().id() };
- let p_id = self.base.id();
- let self_ptr = self.as_ptr();
- ctx.register_widget(p_id, self_ptr);
- ctx.register_widget(c_id, child);
- ctx.tree.link(p_id, c_id);
- // …plus, for containers, the legacy container extra: parent the child back (Layer,
- // Switcher) — the symmetric tree link the child's own set_parent used to make.
- if Layout::has_container_children(&self.inner) {
- let self_ptr = self.as_ptr_mut();
- ctx.register_widget(self.base.id(), self_ptr);
- ctx.register_widget(c_id, child);
- ctx.tree.set_parent(c_id, Some(self.base.id()));
- }
- }
-
/// 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) {
// Replica of the old WidgetHost default: symmetric tree link.
@@ -929,7 +910,7 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
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();
+ let self_ptr = self.as_ptr_mut();
ctx.register_widget(id, self_ptr);
ctx.tree.set_parent(id, Some(p_id));
} else {
@@ -1640,7 +1621,8 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
..
} if Input::opens_context_menu(&self.inner) => {
if self.hit_test(*px, *py, ctx) {
- ctx.handle_right_click(self_ptr, *px, *py);
+ // SAFETY: `self_ptr` is this adapter, derived from `&mut self` above.
+ unsafe { ctx.handle_right_click(self_ptr, *px, *py) };
return true;
}
false