Skip to main content

frust_core/
tree.rs

1//! The retained widget tree, backed by `tree_arena`.
2//!
3//! `tree_arena` gives O(1) access to any node with *simultaneous* mutable
4//! access to a node's value and its children — the proven answer to the
5//! borrow-checker fights a widget tree otherwise causes. All direct use of the
6//! arena is confined to this module; the rest of the crate speaks in
7//! [`WidgetId`]/[`WidgetPod`] terms.
8//!
9//! v0 is single-root (the app has one root widget), but the API is written in
10//! terms of insert-into-list / find so container support drops in
11//! without reshaping this layer.
12//!
13//! # Introspection
14//!
15//! [`WidgetTree::roots`] / [`WidgetTree::children`] / [`WidgetTree::inspect`]
16//! make the arena enumerable for read-only tooling. `tree_arena` 0.2 does expose
17//! `root_ids`/`child_ids`, but both iterate a `HashMap` in **arbitrary** order,
18//! which is useless for an inspector that has to show siblings the way the app
19//! declared them. The tree therefore keeps a parallel insertion-order index
20//! (`root_order`/`child_order`) updated at the two mutation points that exist
21//! ([`WidgetTree::insert_root`] and [`WidgetTree::insert_child`]) and uses it
22//! only to *order* what the arena reports — the arena stays the single source of
23//! truth for membership, so a stale index entry can never surface a dangling id.
24//!
25//! Containers own their children as [`ChildPod`]s rather than as arena nodes
26//! (see that type's docs), so the arena itself holds little more than the root
27//! pod. [`WidgetTree::inspect`] therefore walks *both*: a node's arena children
28//! first, then the pods its widget hands to
29//! [`Widget::visit_children`] — so the snapshot is the real retained hierarchy
30//! rather than the arena's contents. Only `inspect` descends that way;
31//! [`WidgetTree::children`] stays strictly arena-scoped, since it answers "what
32//! does the arena hold under this id".
33//!
34//! The descent needs no cycle check: a pod owns its widget, ownership is a tree,
35//! and the visitor only ever hands out `&ChildPod`s the widget itself owns (see
36//! [`Widget::visit_children`]).
37
38use std::borrow::Cow;
39use std::collections::{HashMap, HashSet};
40
41use kurbo::{Point, Rect, Size};
42use tree_arena::{ArenaMut, ArenaRef, TreeArena};
43
44use crate::view::{ChangeFlags, WidgetId};
45use crate::widget::{ChildPod, Widget};
46
47/// A retained widget plus its layout results and dirty state.
48///
49/// The "pod" wraps the boxed widget with the bookkeeping the framework owns:
50/// where the widget was placed (`origin`), how big it is (`size`), and which
51/// passes are pending (`flags`).
52pub struct WidgetPod {
53    id: WidgetId,
54    widget: Box<dyn Widget>,
55    origin: Point,
56    size: Size,
57    flags: ChangeFlags,
58    /// The concrete widget type's [`core::any::type_name`], captured once at
59    /// construction — a `&'static str` copy, so no per-frame cost.
60    type_name: &'static str,
61    /// An optional human name for tooling, `None` unless something calls
62    /// [`WidgetPod::set_debug_label`].
63    debug_label: Option<Cow<'static, str>>,
64}
65
66impl WidgetPod {
67    /// The `type_name` recorded for a pod built from an already type-erased
68    /// widget, where the concrete type is no longer nameable.
69    pub const ERASED_TYPE_NAME: &'static str = "<erased dyn Widget>";
70
71    /// Wrap a freshly built, already type-erased widget. It starts dirty for
72    /// layout and paint.
73    ///
74    /// The concrete type is gone by the time the box arrives, so
75    /// [`type_name`](WidgetPod::type_name) reports
76    /// [`ERASED_TYPE_NAME`](WidgetPod::ERASED_TYPE_NAME). Prefer
77    /// [`WidgetPod::new_typed`] where the concrete type is still in scope.
78    pub fn new(id: WidgetId, widget: Box<dyn Widget>) -> Self {
79        Self::with_type_name(id, widget, Self::ERASED_TYPE_NAME)
80    }
81
82    /// Wrap a freshly built widget whose concrete type is still in scope,
83    /// recording its [`core::any::type_name`] for introspection.
84    ///
85    /// Pass the widget itself, never an already-boxed `Box<dyn Widget>`: the
86    /// blanket `Widget for Box<dyn Widget>` impl would make that compile while
87    /// recording the box's own type name and double-boxing the widget (which
88    /// breaks the downcast back to the originating view's element type). Use
89    /// [`WidgetPod::new`] for the erased case.
90    pub fn new_typed<W: Widget>(id: WidgetId, widget: W) -> Self {
91        Self::with_type_name(id, Box::new(widget), core::any::type_name::<W>())
92    }
93
94    fn with_type_name(id: WidgetId, widget: Box<dyn Widget>, type_name: &'static str) -> Self {
95        Self {
96            id,
97            widget,
98            origin: Point::ZERO,
99            size: Size::ZERO,
100            flags: ChangeFlags::LAYOUT | ChangeFlags::PAINT,
101            type_name,
102            debug_label: None,
103        }
104    }
105
106    /// This pod's stable identity.
107    pub fn id(&self) -> WidgetId {
108        self.id
109    }
110
111    /// The concrete widget type's name, or
112    /// [`ERASED_TYPE_NAME`](WidgetPod::ERASED_TYPE_NAME) when the pod was built
113    /// from an already-erased box.
114    ///
115    /// Recorded at construction, which is sound here because a root pod's
116    /// element type is fixed by `RenderRoot<State, V>`'s `V` and cannot swap
117    /// under it. A [`ChildPod`], whose widget *can* be swapped by a rebuild,
118    /// asks the live widget instead ([`Widget::type_name`]).
119    ///
120    /// Diagnostic only: `type_name`'s output is not a stable contract across
121    /// compiler versions, so never parse or match on it.
122    pub fn type_name(&self) -> &'static str {
123        self.type_name
124    }
125
126    /// The human name attached for tooling, if any. `None` by default.
127    pub fn debug_label(&self) -> Option<&str> {
128        self.debug_label.as_deref()
129    }
130
131    /// Attach a human name for tooling (an inspector shows it beside the type
132    /// name). Purely descriptive — nothing in the build/layout/paint path reads
133    /// it.
134    pub fn set_debug_label(&mut self, label: impl Into<Cow<'static, str>>) {
135        self.debug_label = Some(label.into());
136    }
137
138    /// Drop any attached debug label.
139    pub fn clear_debug_label(&mut self) {
140        self.debug_label = None;
141    }
142
143    /// Shared access to the boxed widget.
144    pub fn widget(&self) -> &dyn Widget {
145        &*self.widget
146    }
147
148    /// Mutable access to the boxed widget.
149    pub fn widget_mut(&mut self) -> &mut dyn Widget {
150        &mut *self.widget
151    }
152
153    /// The widget's origin in its parent's coordinate space.
154    pub fn origin(&self) -> Point {
155        self.origin
156    }
157
158    /// The widget's resolved size (valid after a layout pass).
159    pub fn size(&self) -> Size {
160        self.size
161    }
162
163    /// Record the geometry produced by a layout pass.
164    pub fn set_layout(&mut self, origin: Point, size: Size) {
165        self.origin = origin;
166        self.size = size;
167    }
168
169    /// The pending pass flags for this pod.
170    pub fn flags(&self) -> ChangeFlags {
171        self.flags
172    }
173
174    /// Merge additional pending flags (e.g. from a rebuild).
175    pub fn merge_flags(&mut self, flags: ChangeFlags) {
176        self.flags |= flags;
177    }
178
179    /// Clear all pending flags (after the corresponding passes have run).
180    pub fn clear_flags(&mut self) {
181        self.flags = ChangeFlags::NONE;
182    }
183}
184
185/// One node of a read-only widget-tree snapshot, as produced by
186/// [`WidgetTree::inspect`].
187///
188/// Plain owned data with no borrow of the tree, so a caller (a devtools service,
189/// a test) can hold or forward it freely. Deliberately renderer-agnostic and
190/// serialization-agnostic: geometry is `kurbo`, and any wire format belongs to
191/// the consumer, not to this crate.
192#[derive(Debug, Clone, PartialEq)]
193pub struct InspectNode {
194    /// The node's stable [`WidgetId`].
195    pub id: WidgetId,
196    /// The parent's id, or `None` for a root.
197    pub parent: Option<WidgetId>,
198    /// The concrete widget type's name — see [`WidgetPod::type_name`].
199    pub type_name: &'static str,
200    /// The pod's optional debug label — see [`WidgetPod::set_debug_label`].
201    pub debug_label: Option<Cow<'static, str>>,
202    /// The widget's border box in **absolute** window coordinates, logical px:
203    /// the pod's recorded layout origin accumulated down from the root, plus its
204    /// recorded size. `Rect::ZERO`-sized until a layout pass has run.
205    pub bounds: Rect,
206    /// This node's children: the arena's own children first (insertion order),
207    /// then the widget's owned [`ChildPod`]s in declaration order.
208    pub children: Vec<WidgetId>,
209    /// Distance from the root (roots are `0`).
210    pub depth: usize,
211}
212
213/// The widget tree: a thin, typed wrapper over `tree_arena::TreeArena`.
214pub struct WidgetTree {
215    arena: TreeArena<WidgetPod>,
216    /// Roots in insertion order. Ordering only — the arena owns membership.
217    root_order: Vec<WidgetId>,
218    /// Per-parent child ids in insertion order. Same rule: ordering only.
219    child_order: HashMap<WidgetId, Vec<WidgetId>>,
220}
221
222impl WidgetTree {
223    /// Create an empty tree.
224    pub fn new() -> Self {
225        Self {
226            arena: TreeArena::new(),
227            root_order: Vec::new(),
228            child_order: HashMap::new(),
229        }
230    }
231
232    /// Insert `pod` as a root of the tree, returning its id.
233    pub fn insert_root(&mut self, pod: WidgetPod) -> WidgetId {
234        let id = pod.id();
235        // `ArenaMutList::insert` takes `impl Into<NodeId>`; `WidgetId -> u64`.
236        self.arena.roots_mut().insert(id.0, pod);
237        self.root_order.push(id);
238        id
239    }
240
241    /// Insert `pod` as a child of `parent`. Returns the child id, or `None` if
242    /// `parent` is not in the tree.
243    ///
244    /// Exercises the tree_arena 0.2.0 `ArenaMut` / `ArenaMutList` handles: a
245    /// node's value and its child list are borrowed disjointly, so this stays
246    /// borrow-checker-clean.
247    pub fn insert_child(&mut self, parent: WidgetId, pod: WidgetPod) -> Option<WidgetId> {
248        let id = pod.id();
249        let mut parent_mut: ArenaMut<'_, WidgetPod> = self.arena.find_mut(parent.0)?;
250        parent_mut.children.insert(id.0, pod);
251        self.child_order.entry(parent).or_default().push(id);
252        Some(id)
253    }
254
255    /// Shared access to the pod with the given id, anywhere in the tree.
256    pub fn pod(&self, id: WidgetId) -> Option<&WidgetPod> {
257        self.arena.find(id.0).map(|node| node.item)
258    }
259
260    /// Mutable access to the pod with the given id, anywhere in the tree.
261    pub fn pod_mut(&mut self, id: WidgetId) -> Option<&mut WidgetPod> {
262        self.arena.find_mut(id.0).map(|node| node.item)
263    }
264
265    /// The tree's root ids, in insertion order.
266    ///
267    /// O(roots). Read-only; nothing here mutates the arena.
268    pub fn roots(&self) -> Vec<WidgetId> {
269        let live: HashSet<u64> = self.arena.root_ids().collect();
270        order_against(&self.root_order, &live)
271    }
272
273    /// The children of `id`, in insertion order — empty if `id` has no children
274    /// or is not in the tree.
275    ///
276    /// O(depth) to find the parent, then O(children). Every id returned is live
277    /// in the arena at call time.
278    pub fn children(&self, id: WidgetId) -> Vec<WidgetId> {
279        match self.arena.find(id.0) {
280            Some(node) => self.ordered_children(id, node),
281            None => Vec::new(),
282        }
283    }
284
285    /// A read-only snapshot of the whole retained tree in pre-order (a node
286    /// precedes its descendants; siblings follow declaration order).
287    ///
288    /// The walk covers **both** child mechanisms: the arena's own children, then
289    /// the [`ChildPod`]s a widget publishes through [`Widget::visit_children`] —
290    /// which is where nearly the whole hierarchy actually lives. A container
291    /// that does not override that seam reads as a leaf.
292    ///
293    /// O(nodes) — one pass, one `HashSet` of sibling ids per arena parent, no
294    /// arena mutation and no bookkeeping left behind (a pod's tooling id is
295    /// assigned once, on its first visit, and reused). Bounds come from what the
296    /// last layout pass recorded on each pod ([`WidgetPod::origin`] /
297    /// [`WidgetPod::size`], [`ChildPod::origin`] / [`ChildPod::size`]),
298    /// accumulated into absolute window coordinates on the way down; call it
299    /// after a layout pass or the rects are all zero-sized.
300    pub fn inspect(&self) -> Vec<InspectNode> {
301        let mut out = Vec::new();
302        let roots = self.arena.roots();
303        for id in self.roots() {
304            if let Some(node) = roots.into_item(id.0) {
305                self.inspect_node(node, id, None, Point::ZERO, 0, &mut out);
306            }
307        }
308        out
309    }
310
311    /// Push `node`'s snapshot, then recurse into its arena children and its
312    /// widget's owned pods. `parent_origin` is the parent's absolute origin; a
313    /// pod's own origin is relative to it.
314    fn inspect_node(
315        &self,
316        node: ArenaRef<'_, WidgetPod>,
317        id: WidgetId,
318        parent: Option<WidgetId>,
319        parent_origin: Point,
320        depth: usize,
321        out: &mut Vec<InspectNode>,
322    ) {
323        let pod = node.item;
324        let origin = parent_origin + pod.origin().to_vec2();
325        let arena_children = self.ordered_children(id, node);
326        // Reserve this node's slot before descending, so pre-order holds and the
327        // `children` list can be filled in from the descent itself.
328        let slot = out.len();
329        out.push(InspectNode {
330            id,
331            parent,
332            type_name: pod.type_name(),
333            debug_label: pod.debug_label.clone(),
334            bounds: Rect::from_origin_size(origin, pod.size()),
335            children: Vec::new(),
336            depth,
337        });
338        let mut children = Vec::with_capacity(arena_children.len());
339        for child_id in arena_children {
340            if let Some(child) = node.children.into_item(child_id.0) {
341                children.push(child_id);
342                self.inspect_node(child, child_id, Some(id), origin, depth + 1, out);
343            }
344        }
345        Self::inspect_pods(pod.widget(), id, origin, depth, &mut children, out);
346        out[slot].children = children;
347    }
348
349    /// Push a snapshot for each of `widget`'s owned [`ChildPod`]s (and, through
350    /// them, the whole subtree below), recording their ids into `children`.
351    fn inspect_pods(
352        widget: &dyn Widget,
353        parent: WidgetId,
354        parent_origin: Point,
355        parent_depth: usize,
356        children: &mut Vec<WidgetId>,
357        out: &mut Vec<InspectNode>,
358    ) {
359        widget.visit_children(&mut |child| {
360            let child_id = child.inspect_id();
361            children.push(child_id);
362            Self::inspect_child(
363                child,
364                child_id,
365                parent,
366                parent_origin,
367                parent_depth + 1,
368                out,
369            );
370        });
371    }
372
373    /// The [`ChildPod`] mirror of [`WidgetTree::inspect_node`]: emit `child`'s
374    /// own node, then descend into whatever it owns.
375    fn inspect_child(
376        child: &ChildPod,
377        id: WidgetId,
378        parent: WidgetId,
379        parent_origin: Point,
380        depth: usize,
381        out: &mut Vec<InspectNode>,
382    ) {
383        let origin = parent_origin + child.origin().to_vec2();
384        let slot = out.len();
385        out.push(InspectNode {
386            id,
387            parent: Some(parent),
388            type_name: child.type_name(),
389            debug_label: child.debug_label_cow(),
390            bounds: Rect::from_origin_size(origin, child.size()),
391            children: Vec::new(),
392            depth,
393        });
394        let mut grandchildren = Vec::new();
395        Self::inspect_pods(child.widget(), id, origin, depth, &mut grandchildren, out);
396        out[slot].children = grandchildren;
397    }
398
399    /// The arena's children of `node`, ordered by the recorded insertion index.
400    fn ordered_children(&self, id: WidgetId, node: ArenaRef<'_, WidgetPod>) -> Vec<WidgetId> {
401        let live: HashSet<u64> = node.child_ids().into_iter().collect();
402        let recorded = self.child_order.get(&id).map_or(&[][..], Vec::as_slice);
403        order_against(recorded, &live)
404    }
405}
406
407/// Order `live` (the arena's authoritative id set) by `recorded` (the insertion
408/// index).
409///
410/// The arena wins on membership: a recorded id the arena no longer holds is
411/// dropped, and a live id the index never saw is appended in id order rather
412/// than lost. That keeps traversal truthful even if a future mutation path
413/// forgets to update the index.
414fn order_against(recorded: &[WidgetId], live: &HashSet<u64>) -> Vec<WidgetId> {
415    let mut ordered: Vec<WidgetId> = recorded
416        .iter()
417        .copied()
418        .filter(|id| live.contains(&id.0))
419        .collect();
420    if ordered.len() != live.len() {
421        let seen: HashSet<u64> = ordered.iter().map(|id| id.0).collect();
422        let mut unrecorded: Vec<u64> = live.difference(&seen).copied().collect();
423        unrecorded.sort_unstable();
424        ordered.extend(unrecorded.into_iter().map(WidgetId));
425    }
426    ordered
427}
428
429impl Default for WidgetTree {
430    fn default() -> Self {
431        Self::new()
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::layout::BoxConstraints;
439    use crate::widget::{LayoutCtx, PaintCtx, PaintScene};
440
441    struct Leaf;
442    impl Widget for Leaf {
443        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
444            bc.max()
445        }
446        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
447    }
448
449    /// A container in the shape every real one has: children owned as
450    /// [`ChildPod`]s, published through [`Widget::visit_children`].
451    struct Container {
452        children: Vec<ChildPod>,
453    }
454
455    impl Container {
456        /// A container over `children`, each placed at `origin` with `size` the
457        /// way a layout pass would have left it.
458        fn new(children: Vec<(Point, Size, Box<dyn Widget>)>) -> Self {
459            Self {
460                children: children
461                    .into_iter()
462                    .map(|(origin, size, widget)| {
463                        let mut pod = ChildPod::new(widget);
464                        pod.set_origin(origin);
465                        // A pod records its size from `layout_child`; these tests
466                        // assert the walk, not layout, so drive it directly.
467                        pod.layout_child(&mut LayoutCtx::new(), &BoxConstraints::new(size, size));
468                        pod
469                    })
470                    .collect(),
471            }
472        }
473    }
474
475    impl Widget for Container {
476        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
477            bc.max()
478        }
479        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
480        fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
481            for child in &self.children {
482                visitor(child);
483            }
484        }
485    }
486
487    fn pod(id: u64) -> WidgetPod {
488        WidgetPod::new(WidgetId(id), Box::new(Leaf))
489    }
490
491    #[test]
492    fn insert_root_and_find() {
493        let mut tree = WidgetTree::new();
494        let id = tree.insert_root(pod(1));
495        assert_eq!(id, WidgetId(1));
496        assert!(tree.pod(id).is_some());
497        assert!(tree.pod_mut(id).is_some());
498        assert!(tree.pod(WidgetId(999)).is_none());
499    }
500
501    #[test]
502    fn insert_child_traverses_arena() {
503        let mut tree = WidgetTree::new();
504        let root = tree.insert_root(pod(1));
505        let child = tree.insert_child(root, pod(2)).expect("child inserted");
506        assert_eq!(child, WidgetId(2));
507        // The child is reachable via the arena's global find (descendant lookup).
508        assert!(tree.pod(child).is_some());
509        // Inserting under a missing parent fails cleanly.
510        assert!(tree.insert_child(WidgetId(42), pod(3)).is_none());
511    }
512
513    #[test]
514    fn roots_and_children_follow_insertion_order() {
515        let mut tree = WidgetTree::new();
516        // Two roots, inserted high id first: recorded order, not id order.
517        let root_b = tree.insert_root(pod(20));
518        let root_a = tree.insert_root(pod(10));
519        assert_eq!(tree.roots(), vec![root_b, root_a]);
520
521        let c2 = tree.insert_child(root_b, pod(22)).unwrap();
522        let c1 = tree.insert_child(root_b, pod(21)).unwrap();
523        let grandchild = tree.insert_child(c2, pod(30)).unwrap();
524        assert_eq!(tree.children(root_b), vec![c2, c1]);
525        assert_eq!(tree.children(c2), vec![grandchild]);
526        // Leaves and unknown ids both report no children, never a dangling id.
527        assert!(tree.children(c1).is_empty());
528        assert!(tree.children(WidgetId(999)).is_empty());
529    }
530
531    #[test]
532    fn pod_records_type_name_and_optional_label() {
533        let mut tree = WidgetTree::new();
534        // The erased constructor cannot name the concrete type.
535        let erased = tree.insert_root(WidgetPod::new(WidgetId(1), Box::new(Leaf)));
536        assert_eq!(
537            tree.pod(erased).unwrap().type_name(),
538            WidgetPod::ERASED_TYPE_NAME
539        );
540
541        let typed = tree
542            .insert_child(erased, WidgetPod::new_typed(WidgetId(2), Leaf))
543            .unwrap();
544        let pod = tree.pod_mut(typed).unwrap();
545        assert!(pod.type_name().ends_with("Leaf"), "{}", pod.type_name());
546        // Labels default to absent, are set on demand, and clear again.
547        assert_eq!(pod.debug_label(), None);
548        pod.set_debug_label("the-leaf");
549        assert_eq!(pod.debug_label(), Some("the-leaf"));
550        pod.clear_debug_label();
551        assert_eq!(pod.debug_label(), None);
552    }
553
554    #[test]
555    fn inspect_walks_pre_order_with_absolute_bounds() {
556        let mut tree = WidgetTree::new();
557        let root = tree.insert_root(WidgetPod::new_typed(WidgetId(1), Leaf));
558        let a = tree
559            .insert_child(root, WidgetPod::new_typed(WidgetId(2), Leaf))
560            .unwrap();
561        let a_child = tree
562            .insert_child(a, WidgetPod::new_typed(WidgetId(3), Leaf))
563            .unwrap();
564        let b = tree
565            .insert_child(root, WidgetPod::new_typed(WidgetId(4), Leaf))
566            .unwrap();
567
568        // Geometry as a layout pass would record it: origins parent-relative.
569        tree.pod_mut(root)
570            .unwrap()
571            .set_layout(Point::ZERO, Size::new(100.0, 100.0));
572        tree.pod_mut(a)
573            .unwrap()
574            .set_layout(Point::new(10.0, 5.0), Size::new(50.0, 40.0));
575        tree.pod_mut(a_child)
576            .unwrap()
577            .set_layout(Point::new(2.0, 3.0), Size::new(10.0, 10.0));
578        tree.pod_mut(b)
579            .unwrap()
580            .set_layout(Point::new(0.0, 60.0), Size::new(20.0, 20.0));
581        tree.pod_mut(a).unwrap().set_debug_label("branch-a");
582
583        let nodes = tree.inspect();
584        // Pre-order: root, a, a's child, b.
585        let ids: Vec<WidgetId> = nodes.iter().map(|n| n.id).collect();
586        assert_eq!(ids, vec![root, a, a_child, b]);
587        assert_eq!(
588            nodes.iter().map(|n| n.depth).collect::<Vec<_>>(),
589            vec![0, 1, 2, 1]
590        );
591        assert_eq!(
592            nodes.iter().map(|n| n.parent).collect::<Vec<_>>(),
593            vec![None, Some(root), Some(a), Some(root)]
594        );
595        assert_eq!(nodes[0].children, vec![a, b]);
596        assert_eq!(nodes[2].children, Vec::new());
597
598        // Bounds accumulate down: a's child sits at 10+2, 5+3.
599        assert_eq!(nodes[1].bounds, Rect::new(10.0, 5.0, 60.0, 45.0));
600        assert_eq!(nodes[2].bounds, Rect::new(12.0, 8.0, 22.0, 18.0));
601
602        // Type names captured, labels default to None.
603        assert!(nodes[0].type_name.ends_with("Leaf"));
604        assert_eq!(nodes[1].debug_label.as_deref(), Some("branch-a"));
605        assert!(nodes[0].debug_label.is_none());
606        assert!(nodes[3].debug_label.is_none());
607    }
608
609    #[test]
610    fn inspect_of_an_unlaid_out_tree_reports_zero_rects() {
611        let mut tree = WidgetTree::new();
612        let root = tree.insert_root(WidgetPod::new_typed(WidgetId(1), Leaf));
613        tree.insert_child(root, WidgetPod::new_typed(WidgetId(2), Leaf))
614            .unwrap();
615        let nodes = tree.inspect();
616        assert_eq!(nodes.len(), 2);
617        assert!(nodes.iter().all(|n| n.bounds == Rect::ZERO));
618    }
619
620    #[test]
621    fn traversal_follows_the_arena_not_the_order_index() {
622        // The order index is ordering only: the arena decides membership. Both
623        // divergence directions are covered, since `WidgetTree` exposes no
624        // removal today and a future one must not be able to leak a stale id.
625        let mut tree = WidgetTree::new();
626        let root = tree.insert_root(pod(1));
627        let kept = tree.insert_child(root, pod(2)).unwrap();
628
629        // An id the index records but the arena never held is dropped.
630        tree.child_order
631            .get_mut(&root)
632            .unwrap()
633            .push(WidgetId(1234));
634        assert_eq!(tree.children(root), vec![kept]);
635        assert_eq!(tree.inspect().len(), 2);
636
637        // An arena child the index never saw is still reported (id order).
638        tree.child_order.remove(&root);
639        assert_eq!(tree.children(root), vec![kept]);
640        tree.root_order.clear();
641        assert_eq!(tree.roots(), vec![root]);
642    }
643
644    #[test]
645    fn inspect_descends_through_child_pods() {
646        // The shape the arena alone cannot see: root -> container -> (leaf,
647        // nested container -> leaf). Only the root is an arena node; everything
648        // below it is a `ChildPod` reached through `visit_children`.
649        let inner = Container::new(vec![(
650            Point::new(1.0, 1.0),
651            Size::new(5.0, 5.0),
652            Box::new(Leaf),
653        )]);
654        let outer = Container::new(vec![
655            (Point::new(10.0, 5.0), Size::new(50.0, 40.0), Box::new(Leaf)),
656            (
657                Point::new(0.0, 60.0),
658                Size::new(20.0, 20.0),
659                Box::new(inner) as Box<dyn Widget>,
660            ),
661        ]);
662
663        let mut tree = WidgetTree::new();
664        let root = tree.insert_root(WidgetPod::new_typed(WidgetId(1), outer));
665        tree.pod_mut(root)
666            .unwrap()
667            .set_layout(Point::new(2.0, 3.0), Size::new(100.0, 100.0));
668
669        let nodes = tree.inspect();
670        assert_eq!(nodes.len(), 4, "root + two pods + one grandchild pod");
671
672        // Pre-order: root, first pod, second pod, the nested pod under it.
673        assert_eq!(
674            nodes.iter().map(|n| n.depth).collect::<Vec<_>>(),
675            vec![0, 1, 1, 2],
676            "a pod's depth continues from its owning node's"
677        );
678        assert_eq!(nodes[0].parent, None);
679        assert_eq!(nodes[1].parent, Some(nodes[0].id));
680        assert_eq!(nodes[2].parent, Some(nodes[0].id));
681        assert_eq!(nodes[3].parent, Some(nodes[2].id));
682        assert_eq!(nodes[0].children, vec![nodes[1].id, nodes[2].id]);
683        assert_eq!(nodes[2].children, vec![nodes[3].id]);
684        assert!(nodes[1].children.is_empty(), "a leaf publishes no children");
685
686        // Bounds accumulate through ChildPod origins, on top of the arena pod's
687        // own absolute origin: root at (2,3); first pod at (2+10, 3+5); the
688        // nested container at (2+0, 3+60); its child at (2+0+1, 3+60+1).
689        assert_eq!(nodes[0].bounds, Rect::new(2.0, 3.0, 102.0, 103.0));
690        assert_eq!(nodes[1].bounds, Rect::new(12.0, 8.0, 62.0, 48.0));
691        assert_eq!(nodes[2].bounds, Rect::new(2.0, 63.0, 22.0, 83.0));
692        assert_eq!(nodes[3].bounds, Rect::new(3.0, 64.0, 8.0, 69.0));
693
694        // Type names: the arena pod's is captured by `new_typed`, a child pod's
695        // by its construction path.
696        assert!(
697            nodes[0].type_name.ends_with("Container"),
698            "{}",
699            nodes[0].type_name
700        );
701        assert!(
702            nodes[1..]
703                .iter()
704                .all(|n| n.type_name.ends_with("Leaf") || n.type_name.ends_with("Container"))
705        );
706
707        // Ids are unique, and every pod id is drawn from the reserved range so
708        // it can never be mistaken for an arena `WidgetId`.
709        let ids: HashSet<u64> = nodes.iter().map(|n| n.id.0).collect();
710        assert_eq!(ids.len(), nodes.len(), "ids are unique across a snapshot");
711        assert!(
712            nodes[1..]
713                .iter()
714                .all(|n| n.id.0 >= crate::widget::ChildPod::INSPECT_ID_BASE)
715        );
716    }
717
718    #[test]
719    fn a_pods_tooling_id_and_label_survive_the_next_walk() {
720        // Selection stability: a devtools client holding an id must still be
721        // holding the same pod on the next frame's snapshot.
722        let mut tree = WidgetTree::new();
723        let root = tree.insert_root(WidgetPod::new_typed(
724            WidgetId(1),
725            Container::new(vec![(Point::ZERO, Size::new(4.0, 4.0), Box::new(Leaf))]),
726        ));
727        let first = tree.inspect();
728        let second = tree.inspect();
729        assert_eq!(first[1].id, second[1].id);
730
731        // A label attached to a child pod reaches the snapshot, and clearing it
732        // takes it back out.
733        let pod = tree.pod_mut(root).unwrap();
734        let container = pod
735            .widget_mut()
736            .downcast_mut::<Container>()
737            .expect("the root widget is the container");
738        container.children[0].set_debug_label("the-child");
739        assert_eq!(tree.inspect()[1].debug_label.as_deref(), Some("the-child"));
740        let container = tree
741            .pod_mut(root)
742            .unwrap()
743            .widget_mut()
744            .downcast_mut::<Container>()
745            .expect("the root widget is the container");
746        container.children[0].clear_debug_label();
747        assert_eq!(tree.inspect()[1].debug_label, None);
748    }
749
750    #[test]
751    fn a_widget_that_ignores_the_seam_reads_as_a_leaf() {
752        // The default impl is free to ignore: a container that never overrides
753        // `visit_children` contributes exactly one node, no matter what it owns.
754        struct Opaque {
755            _child: ChildPod,
756        }
757        impl Widget for Opaque {
758            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
759                bc.max()
760            }
761            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
762        }
763
764        let mut tree = WidgetTree::new();
765        tree.insert_root(WidgetPod::new_typed(
766            WidgetId(1),
767            Opaque {
768                _child: ChildPod::new(Box::new(Leaf)),
769            },
770        ));
771        assert_eq!(tree.inspect().len(), 1);
772    }
773
774    #[test]
775    fn pod_stores_layout_geometry() {
776        let mut tree = WidgetTree::new();
777        let id = tree.insert_root(pod(1));
778        let p = tree.pod_mut(id).unwrap();
779        assert!(p.flags().needs_layout());
780        p.set_layout(Point::new(2.0, 3.0), Size::new(10.0, 20.0));
781        p.clear_flags();
782        assert_eq!(p.origin(), Point::new(2.0, 3.0));
783        assert_eq!(p.size(), Size::new(10.0, 20.0));
784        assert!(p.flags().is_empty());
785    }
786}