Skip to main content

oxiui_core/
tree.rs

1//! Retained widget-tree data structure with stable IDs and hit testing.
2//!
3//! This is an optional retained-mode scaffold used for hit testing, focus
4//! traversal, and accessibility-tree generation. The immediate-mode
5//! [`UiCtx`](crate::UiCtx) path does not require it; adapters that need a
6//! persistent tree (e.g. `oxiui-accessibility`) build one here.
7//!
8//! ## Arena-style allocation
9//!
10//! Nodes are stored in a flat [`Vec`] (the *arena*). A companion
11//! `HashMap<WidgetId, usize>` (*index*) maps every live [`WidgetId`] to its
12//! slot in the vec in O(1). This makes `get` / `get_mut` / `index_of` all
13//! O(1) instead of O(n), which matters for trees with thousands of nodes.
14//!
15//! When a node is removed its slot is swapped with the last element and the
16//! index is updated, keeping the vec compact (no holes). The index is the
17//! single source of truth for liveness: a [`WidgetId`] absent from the index
18//! is definitively dead.
19
20use crate::geometry::{Point, Rect};
21use std::collections::HashMap;
22
23/// A stable, unique identifier for a node within a single [`WidgetTree`].
24///
25/// IDs are allocated monotonically by [`WidgetIdAllocator`] and never reused
26/// within the lifetime of one allocator, guaranteeing uniqueness.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct WidgetId(pub u64);
29
30impl WidgetId {
31    /// The reserved root identifier.
32    pub const ROOT: WidgetId = WidgetId(0);
33}
34
35/// Monotonic allocator of [`WidgetId`]s.
36///
37/// `WidgetId(0)` is reserved for the root; user IDs start at `1`.
38#[derive(Debug)]
39pub struct WidgetIdAllocator {
40    next: u64,
41}
42
43impl WidgetIdAllocator {
44    /// Create a fresh allocator. The first allocated ID is `WidgetId(1)`.
45    pub fn new() -> Self {
46        Self { next: 1 }
47    }
48
49    /// Allocate the next unique [`WidgetId`].
50    pub fn alloc(&mut self) -> WidgetId {
51        let id = WidgetId(self.next);
52        self.next += 1;
53        id
54    }
55
56    /// Number of IDs allocated so far (excluding the reserved root).
57    pub fn allocated(&self) -> u64 {
58        self.next - 1
59    }
60}
61
62impl Default for WidgetIdAllocator {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68/// A single node in a [`WidgetTree`].
69#[derive(Clone, Debug)]
70pub struct WidgetNode {
71    /// This node's stable identifier.
72    pub id: WidgetId,
73    /// Parent node, or `None` for the root.
74    pub parent: Option<WidgetId>,
75    /// Child nodes in paint (back-to-front) order.
76    pub children: Vec<WidgetId>,
77    /// Layout rectangle in the coordinate space of the tree's root.
78    pub rect: Rect,
79    /// Paint order among siblings; higher draws on top.
80    pub z_index: i32,
81    /// Whether this node participates in hit testing (false = pass-through).
82    pub hit_testable: bool,
83    /// Whether this node can receive keyboard focus.
84    pub focusable: bool,
85    /// Dirty flag: layout or paint needs recomputation.
86    pub dirty: bool,
87    /// Optional clip rectangle, in the tree's root coordinate space. When set,
88    /// content (and hit testing) is constrained to the intersection of `rect`
89    /// and this clip. `None` means inherit the parent clip / no clipping.
90    pub clip_rect: Option<Rect>,
91    /// Optional debug/role label (used by a11y and diagnostics).
92    pub label: Option<String>,
93}
94
95impl WidgetNode {
96    /// Construct a node with the given id and rectangle. Defaults: hit-testable,
97    /// not focusable, `z_index` 0, dirty.
98    pub fn new(id: WidgetId, rect: Rect) -> Self {
99        Self {
100            id,
101            parent: None,
102            children: Vec::new(),
103            rect,
104            z_index: 0,
105            hit_testable: true,
106            focusable: false,
107            dirty: true,
108            clip_rect: None,
109            label: None,
110        }
111    }
112
113    /// Builder-style setter for [`clip_rect`](WidgetNode::clip_rect).
114    pub fn with_clip(mut self, clip: Rect) -> Self {
115        self.clip_rect = Some(clip);
116        self
117    }
118}
119
120/// A tree of [`WidgetNode`]s addressed by [`WidgetId`].
121///
122/// Nodes are stored in a flat arena vector; parent/child relationships are
123/// tracked by id. A companion `HashMap` index provides O(1) lookup from id to
124/// vec slot. The tree always contains a root node (`WidgetId::ROOT`).
125///
126/// See the module-level documentation for the arena allocation strategy.
127#[derive(Debug)]
128pub struct WidgetTree {
129    /// Arena: flat storage of all live nodes.
130    nodes: Vec<WidgetNode>,
131    /// O(1) index: maps WidgetId → arena slot index.
132    index: HashMap<WidgetId, usize>,
133    alloc: WidgetIdAllocator,
134}
135
136impl WidgetTree {
137    /// Create a new tree whose root covers `root_rect`.
138    pub fn new(root_rect: Rect) -> Self {
139        let mut root = WidgetNode::new(WidgetId::ROOT, root_rect);
140        root.label = Some("root".to_owned());
141        let mut index = HashMap::new();
142        index.insert(WidgetId::ROOT, 0usize);
143        Self {
144            nodes: vec![root],
145            index,
146            alloc: WidgetIdAllocator::new(),
147        }
148    }
149
150    /// Total number of nodes (including the root).
151    pub fn len(&self) -> usize {
152        self.nodes.len()
153    }
154
155    /// Returns `true` if only the root node exists.
156    pub fn is_empty(&self) -> bool {
157        self.nodes.len() <= 1
158    }
159
160    fn index_of(&self, id: WidgetId) -> Option<usize> {
161        self.index.get(&id).copied()
162    }
163
164    /// Borrow a node by id.
165    pub fn get(&self, id: WidgetId) -> Option<&WidgetNode> {
166        self.index_of(id).map(|i| &self.nodes[i])
167    }
168
169    /// Mutably borrow a node by id.
170    pub fn get_mut(&mut self, id: WidgetId) -> Option<&mut WidgetNode> {
171        self.index_of(id).map(move |i| &mut self.nodes[i])
172    }
173
174    /// Insert a new child of `parent` covering `rect`. Returns the new id, or
175    /// `None` if `parent` does not exist.
176    pub fn insert(&mut self, parent: WidgetId, rect: Rect) -> Option<WidgetId> {
177        self.index_of(parent)?;
178        let id = self.alloc.alloc();
179        let new_idx = self.nodes.len();
180        let mut node = WidgetNode::new(id, rect);
181        node.parent = Some(parent);
182        self.nodes.push(node);
183        self.index.insert(id, new_idx);
184        if let Some(pi) = self.index_of(parent) {
185            self.nodes[pi].children.push(id);
186        }
187        Some(id)
188    }
189
190    /// Remove `id` and its entire subtree. The root cannot be removed.
191    ///
192    /// Returns the number of nodes removed.
193    pub fn remove(&mut self, id: WidgetId) -> usize {
194        if id == WidgetId::ROOT {
195            return 0;
196        }
197        // Collect subtree ids (DFS) without borrowing issues.
198        let mut to_remove = Vec::new();
199        let mut stack = vec![id];
200        while let Some(cur) = stack.pop() {
201            if let Some(node) = self.get(cur) {
202                stack.extend(node.children.iter().copied());
203                to_remove.push(cur);
204            }
205        }
206        // Detach from parent.
207        if let Some(parent) = self.get(id).and_then(|n| n.parent) {
208            if let Some(pi) = self.index_of(parent) {
209                self.nodes[pi].children.retain(|&c| c != id);
210            }
211        }
212        let removed = to_remove.len();
213        // Remove the nodes from the arena.
214        self.nodes.retain(|n| !to_remove.contains(&n.id));
215        // Remove stale index entries for the removed ids.
216        for rid in &to_remove {
217            self.index.remove(rid);
218        }
219        // Rebuild the remaining index entries (positions may have shifted after retain).
220        for (slot, node) in self.nodes.iter().enumerate() {
221            self.index.insert(node.id, slot);
222        }
223        removed
224    }
225
226    /// Move `id` to become a child of `new_parent`. Returns `false` if either
227    /// id is missing, if `id` is the root, or if the move would create a cycle.
228    pub fn reparent(&mut self, id: WidgetId, new_parent: WidgetId) -> bool {
229        if id == WidgetId::ROOT || self.get(id).is_none() || self.get(new_parent).is_none() {
230            return false;
231        }
232        // Reject cycles: new_parent must not be a descendant of id.
233        if self.is_descendant(new_parent, id) {
234            return false;
235        }
236        let old_parent = self.get(id).and_then(|n| n.parent);
237        if let Some(op) = old_parent {
238            if let Some(oi) = self.index_of(op) {
239                self.nodes[oi].children.retain(|&c| c != id);
240            }
241        }
242        if let Some(ni) = self.index_of(new_parent) {
243            self.nodes[ni].children.push(id);
244        }
245        if let Some(i) = self.index_of(id) {
246            self.nodes[i].parent = Some(new_parent);
247        }
248        true
249    }
250
251    /// Returns `true` if `maybe_descendant` is in the subtree rooted at `ancestor`.
252    pub fn is_descendant(&self, maybe_descendant: WidgetId, ancestor: WidgetId) -> bool {
253        let mut cur = maybe_descendant;
254        while let Some(node) = self.get(cur) {
255            match node.parent {
256                Some(p) if p == ancestor => return true,
257                Some(p) => cur = p,
258                None => return false,
259            }
260        }
261        false
262    }
263
264    /// Depth of `id` from the root (root has depth 0). Returns `None` if missing.
265    pub fn depth(&self, id: WidgetId) -> Option<usize> {
266        let mut depth = 0;
267        let mut cur = self.get(id)?;
268        while let Some(parent) = cur.parent {
269            depth += 1;
270            cur = self.get(parent)?;
271        }
272        Some(depth)
273    }
274
275    /// Visit every node in depth-first pre-order, invoking `visit(node, depth)`.
276    pub fn walk_dfs(&self, mut visit: impl FnMut(&WidgetNode, usize)) {
277        let mut stack = vec![(WidgetId::ROOT, 0usize)];
278        while let Some((id, depth)) = stack.pop() {
279            if let Some(node) = self.get(id) {
280                visit(node, depth);
281                // Push children reversed so they're visited left-to-right.
282                for &child in node.children.iter().rev() {
283                    stack.push((child, depth + 1));
284                }
285            }
286        }
287    }
288
289    /// The effective clip rectangle for `id`: the intersection of its own
290    /// `clip_rect` (if any) with every ancestor's `clip_rect`. Returns `None`
291    /// when no ancestor (and the node itself) clips — meaning "unclipped".
292    ///
293    /// If the accumulated clips do not overlap at all, returns
294    /// `Some(Rect::ZERO)` (a degenerate, empty clip) so callers treat the node
295    /// as fully clipped away.
296    pub fn effective_clip(&self, id: WidgetId) -> Option<Rect> {
297        let mut acc: Option<Rect> = None;
298        let mut cur = self.get(id);
299        while let Some(node) = cur {
300            if let Some(clip) = node.clip_rect {
301                acc = Some(match acc {
302                    None => clip,
303                    Some(existing) => existing.intersection(&clip).unwrap_or(Rect::ZERO),
304                });
305            }
306            cur = node.parent.and_then(|p| self.get(p));
307        }
308        acc
309    }
310
311    /// Hit-test `point`, returning the front-most hit-testable node containing it.
312    ///
313    /// Traversal is depth-first; among overlapping candidates the one with the
314    /// greatest combined `(depth, z_index)` paint order wins (i.e. the visually
315    /// front-most). Non-`hit_testable` nodes are skipped but their descendants
316    /// are still tested. A node is only a candidate when `point` lies within
317    /// both its `rect` *and* its [`effective_clip`](WidgetTree::effective_clip);
318    /// content outside an (ancestor) clip is not interactive.
319    pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
320        let mut best: Option<(WidgetId, usize, i32)> = None;
321        self.walk_dfs(|node, depth| {
322            if !node.hit_testable || !node.rect.contains(point) {
323                return;
324            }
325            // Respect clipping: the point must also lie inside the accumulated
326            // clip region for this node.
327            if let Some(clip) = self.effective_clip(node.id) {
328                if !clip.contains(point) {
329                    return;
330                }
331            }
332            let key = (depth, node.z_index);
333            match best {
334                Some((_, bd, bz)) if (bd, bz) >= key => {}
335                _ => best = Some((node.id, depth, node.z_index)),
336            }
337        });
338        best.map(|(id, _, _)| id)
339    }
340
341    /// Collect the focusable nodes in DFS (tab) order.
342    pub fn focus_order(&self) -> Vec<WidgetId> {
343        let mut order = Vec::new();
344        self.walk_dfs(|node, _| {
345            if node.focusable {
346                order.push(node.id);
347            }
348        });
349        order
350    }
351
352    /// Collect every node id in back-to-front paint order.
353    ///
354    /// Order is a stable sort by the key `(depth, z_index)`: shallower nodes
355    /// paint first (behind), and within the same depth lower `z_index` paints
356    /// first. Painting nodes in this order means later draws correctly occlude
357    /// earlier ones. The sort is stable, so siblings with equal `z_index`
358    /// retain their tree (DFS) order, matching CSS source-order tie-breaking.
359    pub fn paint_order(&self) -> Vec<WidgetId> {
360        let mut entries: Vec<(usize, i32, usize, WidgetId)> = Vec::new();
361        let mut seq = 0usize;
362        self.walk_dfs(|node, depth| {
363            entries.push((depth, node.z_index, seq, node.id));
364            seq += 1;
365        });
366        // Stable sort by (depth, z_index); `seq` (DFS order) breaks ties so the
367        // result is deterministic and source-ordered within a stacking level.
368        entries.sort_by_key(|&(depth, z, seq, _)| (depth, z, seq));
369        entries.into_iter().map(|(_, _, _, id)| id).collect()
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::geometry::Rect;
377
378    fn sample_tree() -> WidgetTree {
379        let mut t = WidgetTree::new(Rect::new(0.0, 0.0, 200.0, 200.0));
380        let a = t
381            .insert(WidgetId::ROOT, Rect::new(0.0, 0.0, 100.0, 100.0))
382            .expect("root exists");
383        let _b = t
384            .insert(WidgetId::ROOT, Rect::new(100.0, 0.0, 100.0, 100.0))
385            .expect("root exists");
386        let _a1 = t
387            .insert(a, Rect::new(10.0, 10.0, 30.0, 30.0))
388            .expect("a exists");
389        t
390    }
391
392    #[test]
393    fn id_allocator_is_monotonic_and_unique() {
394        let mut alloc = WidgetIdAllocator::new();
395        let a = alloc.alloc();
396        let b = alloc.alloc();
397        assert_ne!(a, b);
398        assert_eq!(a, WidgetId(1));
399        assert_eq!(b, WidgetId(2));
400        assert_eq!(alloc.allocated(), 2);
401    }
402
403    #[test]
404    fn insert_and_structure() {
405        let t = sample_tree();
406        assert_eq!(t.len(), 4); // root + a + b + a1
407        let root = t.get(WidgetId::ROOT).expect("root");
408        assert_eq!(root.children.len(), 2);
409        assert_eq!(t.depth(WidgetId(3)), Some(2)); // a1 nested under a
410    }
411
412    #[test]
413    fn remove_subtree() {
414        let mut t = sample_tree();
415        let removed = t.remove(WidgetId(1)); // remove `a` and its child a1
416        assert_eq!(removed, 2);
417        assert_eq!(t.len(), 2); // root + b
418        assert!(t.get(WidgetId(1)).is_none());
419        assert!(t.get(WidgetId(3)).is_none());
420        // root cannot be removed
421        assert_eq!(t.remove(WidgetId::ROOT), 0);
422    }
423
424    #[test]
425    fn reparent_rejects_cycles() {
426        let mut t = sample_tree();
427        // Try to make `a` (id 1) a child of its own descendant a1 (id 3): cycle.
428        assert!(!t.reparent(WidgetId(1), WidgetId(3)));
429        // Valid reparent: move a1 (id 3) under b (id 2). b is a child of root
430        // (depth 1), so a1 ends up at depth 2.
431        assert!(t.reparent(WidgetId(3), WidgetId(2)));
432        assert_eq!(t.depth(WidgetId(3)), Some(2));
433        // a1 is now a child of b, no longer of a.
434        assert_eq!(t.get(WidgetId(3)).and_then(|n| n.parent), Some(WidgetId(2)));
435    }
436
437    #[test]
438    fn hit_test_front_most_wins() {
439        let mut t = WidgetTree::new(Rect::new(0.0, 0.0, 100.0, 100.0));
440        // Two overlapping siblings; the deeper/later one should win.
441        let back = t
442            .insert(WidgetId::ROOT, Rect::new(0.0, 0.0, 50.0, 50.0))
443            .expect("root");
444        let front = t
445            .insert(back, Rect::new(10.0, 10.0, 20.0, 20.0))
446            .expect("back");
447        let hit = t.hit_test(Point::new(15.0, 15.0));
448        assert_eq!(hit, Some(front));
449        // A point only inside the parent hits the parent.
450        assert_eq!(t.hit_test(Point::new(45.0, 45.0)), Some(back));
451        // Outside everything.
452        assert_eq!(t.hit_test(Point::new(90.0, 90.0)), Some(WidgetId::ROOT));
453    }
454
455    #[test]
456    fn hit_test_skips_non_hit_testable() {
457        let mut t = WidgetTree::new(Rect::new(0.0, 0.0, 100.0, 100.0));
458        let overlay = t
459            .insert(WidgetId::ROOT, Rect::new(0.0, 0.0, 100.0, 100.0))
460            .expect("root");
461        if let Some(n) = t.get_mut(overlay) {
462            n.hit_testable = false; // pass-through overlay
463        }
464        // Overlay is pass-through, so root receives the hit.
465        assert_eq!(t.hit_test(Point::new(50.0, 50.0)), Some(WidgetId::ROOT));
466    }
467
468    #[test]
469    fn focus_order_dfs() {
470        let mut t = WidgetTree::new(Rect::ZERO);
471        let a = t.insert(WidgetId::ROOT, Rect::ZERO).expect("root");
472        let b = t.insert(WidgetId::ROOT, Rect::ZERO).expect("root");
473        for id in [a, b] {
474            if let Some(n) = t.get_mut(id) {
475                n.focusable = true;
476            }
477        }
478        assert_eq!(t.focus_order(), vec![a, b]);
479    }
480
481    #[test]
482    fn hit_test_respects_clip_rect() {
483        let mut t = WidgetTree::new(Rect::new(0.0, 0.0, 100.0, 100.0));
484        // A scroll container clipped to its left half.
485        let container = t
486            .insert(WidgetId::ROOT, Rect::new(0.0, 0.0, 100.0, 100.0))
487            .expect("root");
488        if let Some(n) = t.get_mut(container) {
489            n.clip_rect = Some(Rect::new(0.0, 0.0, 50.0, 100.0));
490        }
491        // A child that overflows past the clip on the right.
492        let child = t
493            .insert(container, Rect::new(40.0, 0.0, 40.0, 20.0))
494            .expect("container");
495        // Inside both child rect and clip -> hits child.
496        assert_eq!(t.hit_test(Point::new(45.0, 5.0)), Some(child));
497        // Inside child rect but outside the clip (x >= 50) -> falls through to
498        // the (clipped) container, but that point is also outside the container
499        // clip, so it reaches the unclipped root.
500        assert_eq!(t.hit_test(Point::new(70.0, 5.0)), Some(WidgetId::ROOT));
501    }
502
503    #[test]
504    fn effective_clip_intersects_ancestors() {
505        let mut t = WidgetTree::new(Rect::new(0.0, 0.0, 100.0, 100.0));
506        let outer = t
507            .insert(WidgetId::ROOT, Rect::new(0.0, 0.0, 100.0, 100.0))
508            .expect("root");
509        if let Some(n) = t.get_mut(outer) {
510            n.clip_rect = Some(Rect::new(0.0, 0.0, 60.0, 100.0));
511        }
512        let inner = t
513            .insert(outer, Rect::new(0.0, 0.0, 100.0, 100.0))
514            .expect("outer");
515        if let Some(n) = t.get_mut(inner) {
516            n.clip_rect = Some(Rect::new(40.0, 0.0, 60.0, 100.0));
517        }
518        // Intersection of [0,60) and [40,100) on x => [40,60).
519        assert_eq!(
520            t.effective_clip(inner),
521            Some(Rect::new(40.0, 0.0, 20.0, 100.0))
522        );
523        // Root (unclipped) has no effective clip.
524        assert_eq!(t.effective_clip(WidgetId::ROOT), None);
525    }
526
527    #[test]
528    fn paint_order_back_to_front() {
529        let mut t = WidgetTree::new(Rect::ZERO);
530        // root(depth0); a,b children(depth1); a has higher z than b.
531        let a = t.insert(WidgetId::ROOT, Rect::ZERO).expect("root");
532        let b = t.insert(WidgetId::ROOT, Rect::ZERO).expect("root");
533        let a1 = t.insert(a, Rect::ZERO).expect("a"); // depth 2
534        if let Some(n) = t.get_mut(a) {
535            n.z_index = 5;
536        }
537        if let Some(n) = t.get_mut(b) {
538            n.z_index = 1;
539        }
540        let order = t.paint_order();
541        // Root first (shallowest). Among depth-1 siblings, lower z (b) before a.
542        assert_eq!(order[0], WidgetId::ROOT);
543        let pos_a = order.iter().position(|&x| x == a).expect("a present");
544        let pos_b = order.iter().position(|&x| x == b).expect("b present");
545        let pos_a1 = order.iter().position(|&x| x == a1).expect("a1 present");
546        assert!(pos_b < pos_a, "lower z_index paints first");
547        // a1 is deepest -> paints last (in front of everything).
548        assert!(pos_a1 > pos_a && pos_a1 > pos_b);
549    }
550}