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