GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/scene/tree.rs (16.1K)
1 //! `WidgetTree` — `UiContext`'s widget tree (`UiContext::tree`): the widgets themselves and
2 //! their parent/child links, in one generational [`Arena`] keyed through a
3 //! `WidgetId → NodeId` index, so the context's `WidgetId`-based API (`link_ids`,
4 //! `clear_hierarchy`, …) and the apps' handles name nodes by id. Links are **symmetric** by
5 //! construction: `set_parent` / `unlink` update both ends. (The two `HashMap`s it replaced —
6 //! an id → pointer registry and a parents/children pair kept in step by hand — were sometimes
7 //! left asymmetric; `docs/rfc-core-rebuild.md` Phase 1b.)
8 //!
9 //! ## The tree owns its widgets
10 //!
11 //! Every widget in the tree was moved into it (`UiContext::insert`) and lives in an allocation
12 //! the tree keeps, held as a raw ROOT (never a `Box` across accesses: a `Box` is a unique
13 //! pointer to the language, and every reborrow through it would invalidate the references the
14 //! context hands out). Every access — the app's through a handle, the context's dispatch —
15 //! derives from that root, and a widget out on loan (`UiContext::lend`) resolves to nothing,
16 //! so no two `&mut`s to one widget ever coexist. Until 2026-10-08 the tree held raw pointers
17 //! to widgets the APP owned, each watched by a liveness token; that path is gone
18 //! (`docs/rfc-owning-registry.md`, phase 5).
19 use std::any::TypeId;
20 use std::collections::HashMap;
21 use std::ptr::NonNull;
22
23 use crate::scene::arena::{Arena, NodeId};
24 use crate::widget::{WidgetHost, WidgetId};
25
26 /// One arena node's payload: the widget's stable id and, once it is inserted, the widget. A
27 /// node can be LINKED (as a parent or child) before its widget is inserted, as an app may
28 /// link ids before it builds the widgets; such a node resolves to nothing.
29 struct Entry {
30 id: WidgetId,
31 slot: Option<Slot>,
32 /// Out on loan (`UiContext::lend`): while set, nothing in the tree resolves the widget,
33 /// so a re-entrant reach for it is a miss rather than a second `&mut`.
34 lent: bool,
35 }
36
37 /// A widget the tree owns: its allocation, held as the raw root every access derives from,
38 /// and its type for typed access. Dropping the slot drops the widget.
39 struct Slot {
40 root: NonNull<dyn WidgetHost>,
41 type_id: TypeId,
42 }
43
44 impl Drop for Slot {
45 fn drop(&mut self) {
46 // SAFETY: `root` was made by `Box::into_raw` in `insert_owned` and is freed only here,
47 // once (`take_owned` forgets the slot it frees itself).
48 unsafe { drop(Box::from_raw(self.root.as_ptr())) };
49 }
50 }
51
52 /// The entry's widget, unless there is none yet or it is out on loan.
53 #[inline]
54 fn live_ptr(entry: &Entry) -> Option<*mut (dyn WidgetHost + 'static)> {
55 if entry.lent {
56 return None;
57 }
58 entry.slot.as_ref().map(|s| s.root.as_ptr())
59 }
60
61 /// The consolidated, generational widget tree. See the module docs.
62 pub struct WidgetTree {
63 arena: Arena<Entry>,
64 by_id: HashMap<WidgetId, NodeId>,
65 }
66
67 impl Default for WidgetTree {
68 fn default() -> Self {
69 Self::new()
70 }
71 }
72
73 impl WidgetTree {
74 pub fn new() -> Self {
75 WidgetTree { arena: Arena::new(), by_id: HashMap::new() }
76 }
77
78 /// Number of nodes known to the tree (inserted or link-only).
79 pub fn len(&self) -> usize {
80 self.arena.len()
81 }
82
83 pub fn is_empty(&self) -> bool {
84 self.arena.is_empty()
85 }
86
87 /// Get (or lazily create) the arena node for `id`. A freshly created node has no widget
88 /// until [`insert_owned`](WidgetTree::insert_owned) supplies one. Re-creates the node if a
89 /// stale `by_id` entry points at a removed slot.
90 fn ensure_node(&mut self, id: WidgetId) -> NodeId {
91 if let Some(&node) = self.by_id.get(&id) {
92 if self.arena.contains(node) {
93 return node;
94 }
95 }
96 let node = self.arena.insert(Entry { id, slot: None, lent: false });
97 self.by_id.insert(id, node);
98 node
99 }
100
101 /// Make `child` a child of `parent` (deduped, reparenting from any previous parent). Keeps
102 /// both ends of the edge consistent. No-op (rather than panic) if the link would form a
103 /// cycle.
104 pub fn link(&mut self, parent: WidgetId, child: WidgetId) {
105 let parent_node = self.ensure_node(parent);
106 let child_node = self.ensure_node(child);
107 if parent_node == child_node || self.arena.is_ancestor(child_node, parent_node) {
108 return;
109 }
110 self.arena.append_child(parent_node, child_node);
111 }
112
113 /// Set or clear `child`'s parent. `Some(p)` links symmetrically (as [`link`](WidgetTree::link));
114 /// `None` detaches `child` from its current parent.
115 pub fn set_parent(&mut self, child: WidgetId, parent: Option<WidgetId>) {
116 match parent {
117 Some(p) => self.link(p, child),
118 None => {
119 if let Some(&node) = self.by_id.get(&child) {
120 self.arena.detach(node);
121 }
122 }
123 }
124 }
125
126 /// Remove `child` from `parent` if it is currently a child of it.
127 pub fn unlink(&mut self, parent: WidgetId, child: WidgetId) {
128 if let (Some(&child_node), Some(&parent_node)) =
129 (self.by_id.get(&child), self.by_id.get(&parent))
130 {
131 if self.arena.parent(child_node) == Some(parent_node) {
132 self.arena.detach(child_node);
133 }
134 }
135 }
136
137 /// Detach all of `parent`'s children, leaving them as roots. Non-recursive.
138 pub fn clear_children(&mut self, parent: WidgetId) {
139 if let Some(&parent_node) = self.by_id.get(&parent) {
140 let children: Vec<NodeId> = self.arena.children(parent_node).to_vec();
141 for child in children {
142 self.arena.detach(child);
143 }
144 }
145 }
146
147 /// Drop every link, and every node that is only linked: the widgets stay, unlinked, as
148 /// roots — an app that rebuilds its links every frame does not hand its widgets back by
149 /// doing so.
150 pub fn clear_all(&mut self) {
151 let nodes: Vec<NodeId> = self.by_id.values().copied().collect();
152 for &node in &nodes {
153 if self.arena.contains(node) {
154 self.arena.detach(node);
155 }
156 }
157 for node in nodes {
158 let keep = self.arena.value(node).is_some_and(|e| e.slot.is_some());
159 if !keep && self.arena.contains(node) {
160 let id = self.arena.value(node).map(|e| e.id);
161 self.arena.remove_subtree(node);
162 if let Some(id) = id {
163 self.by_id.remove(&id);
164 }
165 }
166 }
167 self.by_id.retain(|_, n| self.arena.contains(*n));
168 }
169
170 /// Take ownership of `widget`: it moves into an allocation the tree keeps, under its own
171 /// id (keeping any links made to that id already), and is dropped when it is taken back
172 /// or the tree is. Returns the id.
173 pub fn insert_owned<W: WidgetHost + 'static>(&mut self, widget: W) -> WidgetId {
174 let id = widget.base().id();
175 let raw: *mut (dyn WidgetHost + 'static) = Box::into_raw(Box::new(widget));
176 // SAFETY: `Box::into_raw` never returns null.
177 let root = unsafe { NonNull::new_unchecked(raw) };
178 let node = self.ensure_node(id);
179 let entry = self.arena.value_mut(node).unwrap();
180 // Two widgets with one id are a clone of a widget whose id was already drawn (the id
181 // cell is copied): the second would replace — and drop — the first.
182 debug_assert!(entry.slot.is_none(), "insert: {id:?} is already in the context (a clone of a widget that is?)");
183 entry.lent = false;
184 entry.slot = Some(Slot { root, type_id: TypeId::of::<W>() });
185 id
186 }
187
188 /// The tree's widget `id` as a `W`: its root, when it is there, it is a `W`, and it is
189 /// not out on loan.
190 pub fn owned_root<W: WidgetHost + 'static>(&self, id: WidgetId) -> Option<NonNull<W>> {
191 let entry = self.arena.value(*self.by_id.get(&id)?)?;
192 let slot = entry.slot.as_ref()?;
193 if entry.lent || slot.type_id != TypeId::of::<W>() {
194 return None;
195 }
196 Some(slot.root.cast::<W>())
197 }
198
199 /// Give the tree's widget `id` back by value. Its node goes; its children stay, as roots.
200 pub fn take_owned<W: WidgetHost + 'static>(&mut self, id: WidgetId) -> Option<W> {
201 let node = *self.by_id.get(&id)?;
202 let entry = self.arena.value_mut(node)?;
203 if entry.lent || entry.slot.as_ref()?.type_id != TypeId::of::<W>() {
204 return None;
205 }
206 let slot = std::mem::ManuallyDrop::new(entry.slot.take()?);
207 // SAFETY: the slot's root was made by `Box::into_raw` of a `W` (its type id says so);
208 // the slot is forgotten (ManuallyDrop) so it is freed only here, by the `Box` the
209 // widget is moved out of.
210 let widget = unsafe { *Box::from_raw(slot.root.cast::<W>().as_ptr()) };
211 self.clear_children(id);
212 self.arena.remove_subtree(node);
213 self.by_id.remove(&id);
214 Some(widget)
215 }
216
217 /// Mark `id` lent (or back). Returns whether it was free to lend: false for an unknown id,
218 /// a link-only one, or one already out.
219 pub fn set_lent(&mut self, id: WidgetId, lent: bool) -> bool {
220 let Some(&node) = self.by_id.get(&id) else { return false };
221 let Some(entry) = self.arena.value_mut(node) else { return false };
222 if lent && (entry.lent || entry.slot.is_none()) {
223 return false;
224 }
225 entry.lent = lent;
226 true
227 }
228
229 /// Whether `id` names a widget the tree holds and has not lent out.
230 pub fn is_registered(&self, id: WidgetId) -> bool {
231 self.get_ptr(id).is_some()
232 }
233
234 /// The widget `id` names, or `None` if unknown, link-only or out on loan. For the
235 /// context, which hands out references derived from it under its own borrow rules.
236 pub(crate) fn get_ptr(&self, id: WidgetId) -> Option<*mut (dyn WidgetHost + 'static)> {
237 let node = *self.by_id.get(&id)?;
238 live_ptr(self.arena.value(node)?)
239 }
240
241 /// `id`'s parent id, if any.
242 pub fn parent_id(&self, id: WidgetId) -> Option<WidgetId> {
243 let node = *self.by_id.get(&id)?;
244 let parent = self.arena.parent(node)?;
245 Some(self.arena.value(parent)?.id)
246 }
247
248 /// `id`'s child ids in order (including link-only children not yet inserted).
249 pub fn child_ids(&self, id: WidgetId) -> Vec<WidgetId> {
250 let Some(&node) = self.by_id.get(&id) else { return Vec::new() };
251 self.arena.children(node).iter().filter_map(|&c| self.arena.value(c).map(|e| e.id)).collect()
252 }
253
254 /// `id`'s children in order, skipping any that is link-only or out on loan.
255 pub(crate) fn children_ptrs(&self, id: WidgetId) -> Vec<*mut (dyn WidgetHost + 'static)> {
256 let Some(&node) = self.by_id.get(&id) else { return Vec::new() };
257 self.arena
258 .children(node)
259 .iter()
260 .filter_map(|&c| live_ptr(self.arena.value(c)?))
261 .collect()
262 }
263
264 /// Every widget the tree holds and has not lent out, for the passes that sweep the whole
265 /// registry (`clear_dirty`, `rebuild_spatial_grid`, the focus walk).
266 pub(crate) fn iter_registered(&self) -> impl Iterator<Item = (WidgetId, *mut (dyn WidgetHost + 'static))> + '_ {
267 self.by_id.values().filter_map(move |&node| {
268 let entry = self.arena.value(node)?;
269 live_ptr(entry).map(|p| (entry.id, p))
270 })
271 }
272
273 /// The ids of every widget the tree holds and has not lent out, in no particular order.
274 pub fn registered_ids(&self) -> Vec<WidgetId> {
275 self.iter_registered().map(|(id, _)| id).collect()
276 }
277 }
278
279 #[cfg(test)]
280 mod tests {
281 use super::*;
282
283 /// A minimal widget whose drops are counted.
284 struct Marker {
285 base: crate::widget::Widget,
286 drops: std::rc::Rc<std::cell::Cell<u32>>,
287 }
288 impl Drop for Marker {
289 fn drop(&mut self) {
290 self.drops.set(self.drops.get() + 1);
291 }
292 }
293 impl WidgetHost for Marker {
294 crate::impl_widget_base!(Marker);
295 }
296
297 fn marker(drops: &std::rc::Rc<std::cell::Cell<u32>>) -> Marker {
298 Marker { base: crate::widget::Widget::new(), drops: drops.clone() }
299 }
300
301 fn tree_of(n: usize) -> (WidgetTree, Vec<WidgetId>, std::rc::Rc<std::cell::Cell<u32>>) {
302 let drops = std::rc::Rc::new(std::cell::Cell::new(0));
303 let mut tree = WidgetTree::new();
304 let ids = (0..n).map(|_| tree.insert_owned(marker(&drops))).collect();
305 (tree, ids, drops)
306 }
307
308 #[test]
309 fn insert_and_resolve() {
310 let (tree, ids, _) = tree_of(1);
311 assert!(tree.is_registered(ids[0]));
312 assert!(tree.owned_root::<Marker>(ids[0]).is_some());
313 assert!(tree.owned_root::<crate::widget::Adapted<crate::widget::Slider>>(ids[0]).is_none(), "typed by what was inserted");
314 assert!(!tree.is_registered(WidgetId(usize::MAX)), "unknown id resolves to nothing");
315 }
316
317 #[test]
318 fn link_is_symmetric_and_deduped() {
319 let (mut tree, ids, _) = tree_of(2);
320 let (p, c) = (ids[0], ids[1]);
321 tree.link(p, c);
322 tree.link(p, c); // duplicate link is a no-op
323 assert_eq!(tree.parent_id(c), Some(p));
324 assert_eq!(tree.child_ids(p), vec![c]);
325 assert_eq!(tree.children_ptrs(p), vec![tree.get_ptr(c).unwrap()]);
326 }
327
328 #[test]
329 fn reparenting_removes_from_old_parent() {
330 let (mut tree, ids, _) = tree_of(3);
331 let (a, b, c) = (ids[0], ids[1], ids[2]);
332 tree.link(a, c);
333 tree.link(b, c);
334 assert!(tree.child_ids(a).is_empty(), "old parent drops the child");
335 assert_eq!(tree.child_ids(b), vec![c]);
336 assert_eq!(tree.parent_id(c), Some(b));
337 tree.link(c, b);
338 assert_eq!(tree.parent_id(b), None, "a link that would close a cycle is refused");
339 }
340
341 #[test]
342 fn a_link_can_come_before_the_widget() {
343 let (mut tree, ids, drops) = tree_of(1);
344 let p = ids[0];
345 let w = marker(&drops);
346 let child = w.base.id();
347 tree.link(p, child);
348 assert_eq!(tree.child_ids(p), vec![child]);
349 assert!(tree.children_ptrs(p).is_empty(), "a link-only child resolves to nothing");
350 tree.insert_owned(w);
351 assert_eq!(tree.child_ids(p), vec![child], "inserting keeps the link");
352 assert_eq!(tree.children_ptrs(p).len(), 1);
353 }
354
355 #[test]
356 fn detaching_and_clearing_keep_the_widgets() {
357 let (mut tree, ids, drops) = tree_of(3);
358 let (p, c1, c2) = (ids[0], ids[1], ids[2]);
359 tree.link(p, c1);
360 tree.link(p, c2);
361 tree.set_parent(c1, None);
362 assert_eq!(tree.child_ids(p), vec![c2], "a symmetric detach");
363 tree.clear_children(p);
364 assert!(tree.child_ids(p).is_empty() && tree.parent_id(c2).is_none());
365 tree.link(p, c1);
366 tree.link(p, WidgetId(usize::MAX)); // link-only
367 tree.clear_all();
368 assert_eq!(tree.len(), 3, "the link-only node goes, the widgets stay");
369 assert!(ids.iter().all(|&id| tree.is_registered(id) && tree.parent_id(id).is_none()));
370 assert_eq!(drops.get(), 0);
371 }
372
373 #[test]
374 fn a_widget_taken_back_leaves_its_children() {
375 let (mut tree, ids, drops) = tree_of(2);
376 let (p, c) = (ids[0], ids[1]);
377 tree.link(p, c);
378 let back = tree.take_owned::<Marker>(p).expect("given back");
379 assert_eq!(back.base.id(), p);
380 assert!(!tree.is_registered(p) && tree.take_owned::<Marker>(p).is_none());
381 assert!(tree.is_registered(c) && tree.parent_id(c).is_none(), "the child stays, a root");
382 assert_eq!(drops.get(), 0);
383 drop(back);
384 drop(tree);
385 assert_eq!(drops.get(), 2, "the tree drops what it holds");
386 }
387
388 #[test]
389 fn a_lent_widget_resolves_to_nothing() {
390 let (mut tree, ids, _) = tree_of(2);
391 let (p, c) = (ids[0], ids[1]);
392 tree.link(p, c);
393 assert!(tree.set_lent(c, true));
394 assert!(!tree.set_lent(c, true), "not lent twice");
395 assert!(tree.get_ptr(c).is_none() && tree.children_ptrs(p).is_empty());
396 assert!(tree.owned_root::<Marker>(c).is_none() && tree.take_owned::<Marker>(c).is_none());
397 assert_eq!(tree.registered_ids(), vec![p]);
398 tree.set_lent(c, false);
399 assert!(tree.is_registered(c));
400 assert!(!tree.set_lent(WidgetId(usize::MAX), true), "nothing to lend");
401 }
402 }