Skip to main content

repose_tree/
tree.rs

1//! The main ViewTree structure.
2
3use crate::{
4    hash::{hash_subtree, hash_view_content},
5    node::{LayoutCache, LayoutConstraints, NodeId, TreeNode, TreeStats},
6    reconcile::ReconcileContext,
7};
8use repose_core::{Modifier, Rect, SubcomposeScope, View, ViewId, ViewKind};
9use rustc_hash::{FxHashMap, FxHashSet};
10use slotmap::SlotMap;
11use smallvec::SmallVec;
12use std::sync::Arc;
13
14/// A persistent view tree that supports incremental updates.
15pub struct ViewTree {
16    /// All nodes in the tree.
17    nodes: SlotMap<NodeId, TreeNode>,
18
19    /// The root node.
20    root: Option<NodeId>,
21
22    /// Nodes that need re-layout.
23    dirty: FxHashSet<NodeId>,
24
25    /// Nodes that need re-paint.
26    paint_dirty: FxHashSet<NodeId>,
27
28    /// Current generation (frame counter).
29    generation: u64,
30
31    /// Map from user-facing ViewId to internal NodeId.
32    view_id_map: FxHashMap<ViewId, NodeId>,
33
34    /// Map from user key to NodeId (for stable identity in dynamic lists).
35    key_map: FxHashMap<(NodeId, u64), NodeId>, // (parent, key) -> child
36
37    /// Statistics from the last reconcile operation.
38    pub stats: TreeStats,
39
40    /// Nodes removed during the last update (needed to sync external systems like Taffy).
41    pub removed_ids: Vec<NodeId>,
42
43    /// Root constraints to use when calling a `SubcomposeLayout`'s content
44    /// closure during this frame. Set via [`ViewTree::set_subcompose_scope`]
45    /// before calling [`ViewTree::update`].
46    subcompose_scope: SubcomposeScope,
47
48    /// Cache of (scope, subcomposed slots) for each `SubcomposeLayout` node.
49    /// The closure is re-invoked only when the ancestor-derived scope
50    /// changes or the node's content changes. Each cached slot view has its
51    /// `Modifier::key` overwritten with its slot id.
52    subcompose_cache: FxHashMap<NodeId, (SubcomposeScope, Vec<(u64, View)>)>,
53}
54
55impl Default for ViewTree {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl ViewTree {
62    /// Create a new empty tree.
63    pub fn new() -> Self {
64        Self {
65            nodes: SlotMap::with_key(),
66            root: None,
67            dirty: FxHashSet::default(),
68            paint_dirty: FxHashSet::default(),
69            generation: 0,
70            view_id_map: FxHashMap::default(),
71            key_map: FxHashMap::default(),
72            stats: TreeStats::default(),
73            removed_ids: Vec::new(),
74            subcompose_scope: SubcomposeScope::UNBOUNDED,
75            subcompose_cache: FxHashMap::default(),
76        }
77    }
78
79    /// Set the constraints that will be passed to any `SubcomposeLayout`
80    /// content closures during the next [`update`](Self::update) call.
81    /// The scope is read once per reconcile of a `SubcomposeLayout` node; if
82    /// you need different scopes at different depths, the closure itself is
83    /// responsible for narrowing the values it receives.
84    pub fn set_subcompose_scope(&mut self, scope: SubcomposeScope) {
85        self.subcompose_scope = scope;
86    }
87
88    /// Get the currently-set subcompose scope.
89    pub fn subcompose_scope(&self) -> SubcomposeScope {
90        self.subcompose_scope
91    }
92
93    /// Run a `SubcomposeLayout`'s content closure, returning the cached list
94    /// of `(slot_id, view)` pairs when the scope is unchanged for this node.
95    /// The caller is responsible for ensuring the cache is invalidated (e.g.
96    /// on content change) via [`ViewTree::invalidate_subcompose_cache`].
97    ///
98    /// The scope is computed by walking the node's ancestor chain and
99    /// intersecting the root scope with each ancestor's `Modifier` width /
100    /// height / min / max fields. The SubcomposeLayout's own modifier is
101    /// included as the last intersection.
102    fn run_subcompose(
103        &mut self,
104        node_id: NodeId,
105        content: &Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
106    ) -> Vec<(u64, View)> {
107        let scope = self.compute_scope_for_node(node_id);
108        if let Some((cached_scope, cached_slots)) = self.subcompose_cache.get(&node_id) {
109            if *cached_scope == scope {
110                return cached_slots.clone();
111            }
112        }
113        let mut slots = content(&scope);
114        for (slot_id, view) in slots.iter_mut() {
115            view.modifier.key = Some(*slot_id);
116        }
117        self.subcompose_cache
118            .insert(node_id, (scope, slots.clone()));
119        slots
120    }
121
122    /// Compute the `SubcomposeScope` visible to a `SubcomposeLayout` at
123    /// `node_id`. Starts with the user-set root scope and intersects each
124    /// ancestor's `Modifier` width / height / min / max fields in root-to-leaf
125    /// order. The SubcomposeLayout node itself is included.
126    fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
127        let mut scope = self.subcompose_scope;
128        let mut chain: Vec<NodeId> = Vec::new();
129        let mut current = Some(node_id);
130        while let Some(id) = current {
131            chain.push(id);
132            match self.nodes.get(id) {
133                Some(node) => current = node.parent,
134                None => break,
135            }
136        }
137        chain.reverse();
138        for ancestor_id in chain {
139            if let Some(node) = self.nodes.get(ancestor_id) {
140                scope = intersect_scope_with_modifier(scope, &node.modifier);
141            }
142        }
143        scope
144    }
145
146    /// Drop the cached subcomposed view for a single node. Call this when the
147    /// `SubcomposeLayout`'s modifier or identity changes so the next
148    /// reconciliation re-invokes the closure.
149    pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
150        self.subcompose_cache.remove(&node_id);
151    }
152
153    /// Drop the cached subcomposed views for a list of nodes (used by garbage
154    /// collection).
155    fn drop_subcompose_cache_for(&mut self, ids: &[NodeId]) {
156        for id in ids {
157            self.subcompose_cache.remove(id);
158        }
159    }
160
161    /// Recursively drop cached subcomposed views for a subtree rooted at
162    /// `node_id`. Called when the node is being removed.
163    fn collect_subcompose_cache(&mut self, node_id: &NodeId) {
164        self.subcompose_cache.remove(node_id);
165        let children: Vec<NodeId> = self
166            .nodes
167            .get(*node_id)
168            .map(|n| n.children.iter().copied().collect())
169            .unwrap_or_default();
170        for child in children {
171            self.collect_subcompose_cache(&child);
172        }
173    }
174
175    /// Get the current generation.
176    pub fn generation(&self) -> u64 {
177        self.generation
178    }
179
180    /// Get the root node ID.
181    pub fn root(&self) -> Option<NodeId> {
182        self.root
183    }
184
185    /// Get a node by ID.
186    pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
187        self.nodes.get(id)
188    }
189
190    /// Get a mutable node by ID.
191    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
192        self.nodes.get_mut(id)
193    }
194
195    /// Get a node by ViewId.
196    pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
197        self.view_id_map
198            .get(&view_id)
199            .and_then(|id| self.nodes.get(*id))
200    }
201
202    /// Get the number of nodes in the tree.
203    pub fn len(&self) -> usize {
204        self.nodes.len()
205    }
206
207    /// Check if the tree is empty.
208    pub fn is_empty(&self) -> bool {
209        self.nodes.is_empty()
210    }
211
212    /// Check if a node is marked dirty.
213    pub fn is_dirty(&self, id: NodeId) -> bool {
214        self.dirty.contains(&id)
215    }
216
217    /// Get the set of dirty nodes.
218    pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
219        &self.dirty
220    }
221
222    /// Clear the dirty set (after layout).
223    pub fn clear_dirty(&mut self) {
224        self.dirty.clear();
225    }
226
227    /// Mark a node as needing re-layout.
228    pub fn mark_dirty(&mut self, id: NodeId) {
229        self.dirty.insert(id);
230
231        // Also mark ancestors dirty (layout flows down from root)
232        let mut current = id;
233        while let Some(node) = self.nodes.get(current) {
234            if let Some(parent) = node.parent {
235                self.dirty.insert(parent);
236                current = parent;
237            } else {
238                break;
239            }
240        }
241    }
242
243    /// Update the tree from a new View, performing incremental reconciliation.
244    /// Returns the root NodeId.
245    pub fn update(&mut self, new_root: &View) -> NodeId {
246        self.removed_ids.clear(); // Clear previous frame's removals
247
248        self.generation += 1;
249        self.stats = TreeStats::default();
250
251        let mut ctx = ReconcileContext::new(self.generation);
252
253        let root_id = if let Some(existing_root) = self.root {
254            self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
255        } else {
256            self.create_node(new_root, None, 0, 0, &mut ctx)
257        };
258
259        self.root = Some(root_id);
260
261        // Remove orphaned nodes (nodes not updated this generation)
262        self.collect_garbage();
263
264        // Update stats
265        self.stats.total_nodes = self.nodes.len();
266        self.stats.dirty_nodes = self.dirty.len();
267        self.stats.reconciled_nodes = ctx.reconciled;
268        self.stats.skipped_nodes = ctx.skipped;
269        self.stats.created_nodes = ctx.created;
270        self.stats.removed_nodes = ctx.removed;
271
272        root_id
273    }
274
275    /// Reconcile an existing node with a new View.
276    fn reconcile_node(
277        &mut self,
278        node_id: NodeId,
279        view: &View,
280        parent: Option<NodeId>,
281        depth: u32,
282        index_in_parent: u32,
283        ctx: &mut ReconcileContext,
284    ) -> NodeId {
285        let content_hash = hash_view_content(view);
286
287        let old_hash = self
288            .nodes
289            .get(node_id)
290            .expect("reconcile_node: node not found")
291            .content_hash;
292        let content_changed = old_hash != content_hash;
293
294        if content_changed {
295            self.invalidate_subcompose_cache(node_id);
296        }
297
298        let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
299            let subcomposed = self.run_subcompose(node_id, content);
300            let slot_views: Vec<View> = subcomposed.into_iter().map(|(_, v)| v).collect();
301            self.reconcile_children(node_id, &slot_views, depth, ctx)
302        } else {
303            self.reconcile_children(node_id, &view.children, depth, ctx)
304        };
305
306        let new_subtree_hash = hash_subtree(content_hash, &new_children_hashes);
307
308        let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
309
310        let subtree_changed;
311        {
312            let node = self
313                .nodes
314                .get_mut(node_id)
315                .expect("reconcile_node: node not found");
316
317            // Update parent, depth, generation
318            node.parent = parent;
319            node.depth = depth;
320            node.generation = self.generation;
321
322            // NOTE: fields like on_pointer_down aren't part of the content hash, so can't rely on content_changed to keep them in sync.
323            node.kind = view.kind.clone();
324            node.modifier = view.modifier.clone();
325            node.content_hash = content_hash;
326            node.user_key = view.modifier.key;
327
328            if content_changed {
329                node.invalidate_layout();
330                ctx.reconciled += 1;
331            }
332
333            // Update subtree hash
334            subtree_changed = node.subtree_hash != new_subtree_hash;
335            if subtree_changed {
336                node.subtree_hash = new_subtree_hash;
337            } else if !content_changed {
338                ctx.skipped += 1;
339            }
340
341            // Update view_id
342            node.view_id = view_id;
343        } // Mutable borrow of node ends here
344
345        if subtree_changed {
346            self.mark_dirty(node_id);
347        }
348        self.view_id_map.insert(view_id, node_id);
349
350        node_id
351    }
352    /// Reconcile children of a node.
353    /// Returns the subtree hashes of all children (for computing parent's subtree hash).
354    fn reconcile_children(
355        &mut self,
356        parent_id: NodeId,
357        new_children: &[View],
358        parent_depth: u32,
359        ctx: &mut ReconcileContext,
360    ) -> Vec<u64> {
361        let child_depth = parent_depth + 1;
362
363        // Get current children
364        let old_children: SmallVec<[NodeId; 4]> = self
365            .nodes
366            .get(parent_id)
367            .map(|n| n.children.clone())
368            .unwrap_or_default();
369
370        // Build a map of keyed children for efficient lookup
371        let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
372        let mut unkeyed_children: Vec<NodeId> = Vec::new();
373
374        for &child_id in &old_children {
375            if let Some(node) = self.nodes.get(child_id) {
376                if let Some(key) = node.user_key {
377                    keyed_children.insert(key, child_id);
378                } else {
379                    unkeyed_children.push(child_id);
380                }
381            }
382        }
383
384        let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
385        let mut new_subtree_hashes: Vec<u64> = Vec::with_capacity(new_children.len());
386        let mut unkeyed_index = 0;
387        let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
388
389        for (i, new_child) in new_children.iter().enumerate() {
390            let idx = i as u32;
391            let child_id = if let Some(key) = new_child.modifier.key {
392                // Keyed child: look up by key
393                if let Some(&existing_id) = keyed_children.get(&key) {
394                    used_nodes.insert(existing_id);
395                    self.reconcile_node(
396                        existing_id,
397                        new_child,
398                        Some(parent_id),
399                        child_depth,
400                        idx,
401                        ctx,
402                    )
403                } else {
404                    self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
405                }
406            } else {
407                // Unkeyed child: match by position
408                if unkeyed_index < unkeyed_children.len() {
409                    let existing_id = unkeyed_children[unkeyed_index];
410                    unkeyed_index += 1;
411                    used_nodes.insert(existing_id);
412                    self.reconcile_node(
413                        existing_id,
414                        new_child,
415                        Some(parent_id),
416                        child_depth,
417                        idx,
418                        ctx,
419                    )
420                } else {
421                    self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
422                }
423            };
424
425            new_child_ids.push(child_id);
426
427            if let Some(node) = self.nodes.get(child_id) {
428                new_subtree_hashes.push(node.subtree_hash);
429            }
430        }
431
432        // Mark unused old children for removal
433        for &old_child in &old_children {
434            if !used_nodes.contains(&old_child) {
435                self.mark_for_removal(old_child, ctx);
436            }
437        }
438
439        // Update parent's children list
440        if let Some(parent) = self.nodes.get_mut(parent_id) {
441            parent.children = new_child_ids;
442        }
443
444        new_subtree_hashes
445    }
446
447    /// Create a new node from a View.
448    fn create_node(
449        &mut self,
450        view: &View,
451        parent: Option<NodeId>,
452        depth: u32,
453        index_in_parent: u32,
454        ctx: &mut ReconcileContext,
455    ) -> NodeId {
456        let content_hash = hash_view_content(view);
457
458        // Insert a partial node first
459        let node_id = self.nodes.insert_with_key(|id| {
460            TreeNode::new(
461                id,
462                0,
463                view.kind.clone(),
464                view.modifier.clone(),
465                self.generation,
466            )
467        });
468        ctx.created += 1;
469
470        {
471            let node = self
472                .nodes
473                .get_mut(node_id)
474                .expect("create_node: node just inserted");
475            node.parent = parent;
476            node.depth = depth;
477            node.content_hash = content_hash;
478            node.user_key = view.modifier.key;
479        }
480
481        // Now, recursively create children
482        let child_depth = depth + 1;
483        let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
484        let mut child_hashes: Vec<u64> = Vec::with_capacity(view.children.len());
485        let children_to_create: Vec<View> = if let ViewKind::SubcomposeLayout { content } = &view.kind {
486            self.run_subcompose(node_id, content)
487                .into_iter()
488                .map(|(_, v)| v)
489                .collect()
490        } else {
491            view.children.clone()
492        };
493        for (i, child_view) in children_to_create.iter().enumerate() {
494            let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
495            child_ids.push(child_id);
496            child_hashes.push(
497                self.nodes
498                    .get(child_id)
499                    .expect("create_node: child just created")
500                    .subtree_hash,
501            );
502        }
503
504        // Now compute the view_id and subtree_hash, and update the node
505        let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
506        let subtree_hash = hash_subtree(content_hash, &child_hashes);
507
508        let node = self
509            .nodes
510            .get_mut(node_id)
511            .expect("create_node: node just inserted");
512        node.children = child_ids;
513        node.subtree_hash = subtree_hash;
514        node.view_id = view_id;
515
516        self.view_id_map.insert(view_id, node_id);
517        self.dirty.insert(node_id);
518
519        node_id
520    }
521    /// Compute a stable ViewId for a node.
522    fn compute_view_id(
523        &self,
524        view: &View,
525        node_id: NodeId,
526        parent: Option<NodeId>,
527        index_in_parent: u32,
528    ) -> ViewId {
529        // If the view already has an ID assigned, use it
530        if view.id != 0 {
531            return view.id;
532        }
533
534        // Otherwise compute from parent + index/key
535        let parent_id = parent
536            .and_then(|p| self.nodes.get(p))
537            .map(|n| n.view_id)
538            .unwrap_or(0);
539
540        let salt = view.modifier.key.unwrap_or(index_in_parent as u64);
541
542        // Simple hash combination
543        let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
544        id = id.wrapping_mul(0x9E3779B97F4A7C15);
545        id ^= id >> 30;
546
547        if id == 0 {
548            id = 1;
549        }
550
551        id
552    }
553
554    /// Mark a node and its descendants for removal.
555    fn mark_for_removal(&mut self, node_id: NodeId, ctx: &mut ReconcileContext) {
556        // Gather what we need from the node first so the immutable borrow ends
557        // before we mutate other state.
558        let (view_id, children) = {
559            let node = self.nodes.get(node_id);
560            match node {
561                Some(n) => (n.view_id, n.children.clone()),
562                None => return,
563            }
564        };
565        self.view_id_map.remove(&view_id);
566        self.subcompose_cache.remove(&node_id);
567        for child_id in children.iter() {
568            self.collect_subcompose_cache(child_id);
569        }
570        for child_id in children {
571            self.mark_for_removal(child_id, ctx);
572        }
573        ctx.removed += 1;
574
575        // Mark the node's generation as old so it gets collected
576        if let Some(node) = self.nodes.get_mut(node_id) {
577            node.generation = 0; // Will be collected
578        }
579    }
580
581    /// Remove nodes that weren't updated this generation.
582    fn collect_garbage(&mut self) {
583        let current_gen = self.generation;
584
585        // Find nodes to remove
586        let to_remove: Vec<NodeId> = self
587            .nodes
588            .iter()
589            .filter(|(_, node)| node.generation != current_gen)
590            .map(|(id, _)| id)
591            .collect();
592
593        // Remove them
594        for id in to_remove {
595            if let Some(node) = self.nodes.remove(id) {
596                self.view_id_map.remove(&node.view_id);
597                self.dirty.remove(&id);
598
599                // Track removal for external sync
600                self.removed_ids.push(id);
601            }
602        }
603    }
604
605    /// Set cached layout for a node.
606    pub fn set_layout(
607        &mut self,
608        id: NodeId,
609        rect: Rect,
610        screen_rect: Rect,
611        constraints: LayoutConstraints,
612    ) {
613        if let Some(node) = self.nodes.get_mut(id) {
614            node.layout_cache = Some(LayoutCache {
615                rect,
616                screen_rect,
617                constraints,
618                generation: self.generation,
619            });
620        }
621    }
622
623    /// Iterate over all nodes (parent before children).
624    pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
625        self.nodes.values()
626    }
627
628    /// Iterate over all nodes with their IDs.
629    pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
630        self.nodes.iter()
631    }
632
633    /// Walk the tree from root, calling `f` for each node.
634    /// Returns early if `f` returns false.
635    pub fn walk<F>(&self, mut f: F)
636    where
637        F: FnMut(&TreeNode, u32) -> bool,
638    {
639        if let Some(root_id) = self.root {
640            self.walk_node(root_id, 0, &mut f);
641        }
642    }
643
644    fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
645    where
646        F: FnMut(&TreeNode, u32) -> bool,
647    {
648        if let Some(node) = self.nodes.get(id) {
649            if !f(node, depth) {
650                return;
651            }
652
653            for &child_id in &node.children {
654                self.walk_node(child_id, depth + 1, f);
655            }
656        }
657    }
658
659    /// Get children of a node.
660    pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
661        self.nodes.get(id).map(|n| n.children.as_slice())
662    }
663}
664
665/// Intersect a `SubcomposeScope` with a `Modifier`'s width / height / min /
666/// max fields. `Modifier::width` / `Modifier::height` are treated as exact
667/// sizes (the resulting min and max both equal that value). `fill_max_w` /
668/// `fill_max_h` and `padding` are not consulted.
669fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
670    let mut s = scope;
671    if let Some(w) = modifier.width {
672        s.min_width = s.min_width.max(w);
673        s.max_width = s.max_width.min(w);
674    }
675    if let Some(h) = modifier.height {
676        s.min_height = s.min_height.max(h);
677        s.max_height = s.max_height.min(h);
678    }
679    if let Some(mw) = modifier.min_width {
680        s.min_width = s.min_width.max(mw);
681    }
682    if let Some(mh) = modifier.min_height {
683        s.min_height = s.min_height.max(mh);
684    }
685    if let Some(mw) = modifier.max_width {
686        s.max_width = s.max_width.min(mw);
687    }
688    if let Some(mh) = modifier.max_height {
689        s.max_height = s.max_height.min(mh);
690    }
691    s
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use repose_core::{Color, Modifier, SubcomposeScope, View, ViewKind};
698    use std::sync::Arc;
699
700    fn text_view(text: &str) -> View {
701        View::new(
702            0,
703            ViewKind::Text {
704                text: text.to_string(),
705                color: Color::WHITE,
706                font_size: 16.0,
707                soft_wrap: true,
708                max_lines: None,
709                overflow: repose_core::TextOverflow::Visible,
710                font_family: None,
711                annotations: None,
712            },
713        )
714    }
715
716    fn box_view() -> View {
717        View::new(0, ViewKind::Box)
718    }
719
720    #[test]
721    fn test_create_tree() {
722        let mut tree = ViewTree::new();
723
724        let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
725
726        tree.update(&root);
727
728        assert_eq!(tree.len(), 3); // box + 2 text
729        assert!(tree.root().is_some());
730    }
731
732    #[test]
733    fn test_unchanged_tree_skips() {
734        let mut tree = ViewTree::new();
735
736        let root = box_view().with_children(vec![text_view("Hello")]);
737
738        tree.update(&root);
739        let gen1 = tree.generation();
740
741        // Same tree
742        tree.update(&root);
743        let gen2 = tree.generation();
744
745        assert_eq!(gen2, gen1 + 1);
746        assert!(tree.stats.skipped_nodes > 0);
747    }
748
749    #[test]
750    fn test_changed_content_reconciles() {
751        let mut tree = ViewTree::new();
752
753        let root1 = box_view().with_children(vec![text_view("Hello")]);
754
755        tree.update(&root1);
756
757        let root2 = box_view().with_children(vec![text_view("Changed")]);
758
759        tree.update(&root2);
760
761        assert!(tree.stats.reconciled_nodes > 0);
762    }
763
764    #[test]
765    fn test_keyed_children_stable() {
766        let mut tree = ViewTree::new();
767
768        // Initial: A, B, C
769        let root1 = box_view().with_children(vec![
770            text_view("A").modifier(Modifier::new().key(1)),
771            text_view("B").modifier(Modifier::new().key(2)),
772            text_view("C").modifier(Modifier::new().key(3)),
773        ]);
774
775        tree.update(&root1);
776
777        // Get B's NodeId
778        let b_view_id = tree
779            .root()
780            .and_then(|r| tree.children(r))
781            .and_then(|c| c.get(1).copied())
782            .and_then(|id| tree.get(id))
783            .map(|n| n.view_id);
784
785        // Reorder: C, A, B
786        let root2 = box_view().with_children(vec![
787            text_view("C").modifier(Modifier::new().key(3)),
788            text_view("A").modifier(Modifier::new().key(1)),
789            text_view("B").modifier(Modifier::new().key(2)),
790        ]);
791
792        tree.update(&root2);
793
794        // B should have same view_id (key-based stability)
795        // Note: Implementation detail - the node may be reused
796        assert_eq!(tree.len(), 4); // Still 4 nodes (box + 3 text)
797    }
798
799    fn subcompose_view<F>(f: F) -> View
800    where
801        F: Fn(&SubcomposeScope) -> View + 'static,
802    {
803        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
804            Arc::new(move |scope| vec![(0, f(scope))]);
805        View {
806            id: 0,
807            kind: ViewKind::SubcomposeLayout { content },
808            modifier: Modifier::default(),
809            children: Vec::new(),
810            semantics: None,
811        }
812    }
813
814    #[test]
815    fn test_subcompose_invokes_content() {
816        let mut tree = ViewTree::new();
817        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
818        let counter2 = counter.clone();
819
820        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
821            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
822            text_view("from subcompose")
823        })]);
824
825        tree.update(&root);
826
827        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
828        assert_eq!(tree.len(), 3); // box + subcompose + text
829    }
830
831    #[test]
832    fn test_subcompose_receives_scope() {
833        let mut tree = ViewTree::new();
834        let captured = Arc::new(std::sync::Mutex::new(None));
835        let captured2 = captured.clone();
836
837        let root = box_view().with_children(vec![subcompose_view(move |scope| {
838            *captured2.lock().unwrap() = Some(*scope);
839            text_view("hi")
840        })]);
841
842        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 360.0, 0.0, 640.0));
843        tree.update(&root);
844
845        let observed = captured.lock().unwrap().expect("scope captured");
846        assert_eq!(observed.max_width, 360.0);
847        assert_eq!(observed.max_height, 640.0);
848        assert_eq!(observed.min_width, 0.0);
849        assert_eq!(observed.min_height, 0.0);
850    }
851
852    #[test]
853    fn test_subcompose_re_invokes_on_update() {
854        let mut tree = ViewTree::new();
855        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
856        let counter2 = counter.clone();
857
858        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
859            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
860            text_view("hi")
861        })]);
862
863        tree.update(&root);
864        tree.update(&root);
865        tree.update(&root);
866
867        // Closure should run only on the first update; subsequent updates hit
868        // the cache because the scope and content are unchanged.
869        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
870    }
871
872    #[test]
873    fn test_subcompose_reruns_on_scope_change() {
874        let mut tree = ViewTree::new();
875        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
876        let counter2 = counter.clone();
877
878        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
879            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
880            text_view("hi")
881        })]);
882
883        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
884        tree.update(&root);
885        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
886
887        // Same scope: cache hit.
888        tree.update(&root);
889        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
890
891        // Scope changed: closure re-runs.
892        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 200.0, 0.0, 200.0));
893        tree.update(&root);
894        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
895    }
896
897    #[test]
898    fn test_subcompose_reruns_on_content_change() {
899        let mut tree = ViewTree::new();
900        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
901        let c1 = counter.clone();
902
903        let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
904            c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
905            text_view("hi")
906        })]);
907
908        tree.update(&root1);
909        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
910
911        // Same content: cache hit.
912        tree.update(&root1);
913        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
914
915        // Changed modifier: closure re-runs.
916        let c2 = counter.clone();
917        let root2 = box_view().with_children(vec![subcompose_view(move |_scope| {
918            c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
919            text_view("hi")
920        })
921        .modifier(Modifier::new().padding(4.0))]);
922
923        tree.update(&root2);
924        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
925    }
926
927    #[test]
928    fn test_subcompose_cache_drops_on_node_removal() {
929        let mut tree = ViewTree::new();
930        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
931
932        // First root has a SubcomposeLayout child.
933        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
934        let c1 = counter.clone();
935        let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
936            c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
937            text_view("hi")
938        })]);
939
940        tree.update(&root_with_sub);
941        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
942
943        // Swap to a root without the SubcomposeLayout - the old node should be
944        // garbage-collected and its cache entry dropped.
945        let root_no_sub = box_view().with_children(vec![text_view("plain")]);
946        tree.update(&root_no_sub);
947        assert_eq!(tree.len(), 2);
948
949        // Bring the SubcomposeLayout back - it must run the closure again
950        // because the cache entry was dropped during GC.
951        let c2 = counter.clone();
952        let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
953            c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
954            text_view("hi")
955        })]);
956
957        tree.update(&root_with_sub_again);
958        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
959    }
960
961    fn multi_slot_view<F>(f: F) -> View
962    where
963        F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
964    {
965        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
966        View {
967            id: 0,
968            kind: ViewKind::SubcomposeLayout { content },
969            modifier: Modifier::default(),
970            children: Vec::new(),
971            semantics: None,
972        }
973    }
974
975    #[test]
976    fn test_subcompose_multi_slot_produces_multiple_children() {
977        let mut tree = ViewTree::new();
978        let root = box_view().with_children(vec![multi_slot_view(|_scope| {
979            vec![(0, text_view("a")), (1, text_view("b")), (2, text_view("c"))]
980        })]);
981
982        tree.update(&root);
983
984        // box + subcompose + 3 texts
985        assert_eq!(tree.len(), 5);
986        let sub_id = tree
987            .root()
988            .and_then(|r| tree.children(r))
989            .and_then(|c| c.first().copied())
990            .expect("subcompose node");
991        let sub_children = tree.children(sub_id).expect("subcompose has children");
992        assert_eq!(sub_children.len(), 3);
993    }
994
995    #[test]
996    fn test_subcompose_multi_slot_preserves_identity_across_removal() {
997        let mut tree = ViewTree::new();
998
999        // 3 slots: 0, 1, 2
1000        let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
1001            vec![(0, text_view("a")), (1, text_view("b")), (2, text_view("c"))]
1002        })]);
1003        tree.update(&root3);
1004
1005        let sub_id = tree
1006            .root()
1007            .and_then(|r| tree.children(r))
1008            .and_then(|c| c.first().copied())
1009            .expect("subcompose node");
1010        let before = tree.children(sub_id).expect("children").to_vec();
1011        let a_node = before[0];
1012        let b_node = before[1];
1013        let c_node = before[2];
1014
1015        // 2 slots: 0, 2 (middle removed). The Modifier change (padding) forces
1016        // the subcompose cache to invalidate so the new closure runs.
1017        let root2 = box_view().with_children(vec![multi_slot_view(|_scope| {
1018            vec![(0, text_view("a")), (2, text_view("c"))]
1019        })
1020        .modifier(Modifier::new().padding(4.0))]);
1021        tree.update(&root2);
1022
1023        let after = tree.children(sub_id).expect("children after");
1024        assert_eq!(after.len(), 2);
1025        // Slot 0 (a) and slot 2 (c) should keep their NodeId.
1026        assert_eq!(after[0], a_node);
1027        assert_eq!(after[1], c_node);
1028        // Slot 1 (b) should be gone.
1029        assert!(tree.get(b_node).is_none());
1030    }
1031
1032    #[test]
1033    fn test_subcompose_ancestor_modifier_narrows_scope() {
1034        let mut tree = ViewTree::new();
1035        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1036
1037        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1038        let cap2 = captured.clone();
1039
1040        // SubcomposeLayout inside a Box with width(200.dp) - the closure should
1041        // see max_width == 200.
1042        let sub = multi_slot_view(move |scope| {
1043            *cap2.lock().unwrap() = *scope;
1044            vec![(0, text_view("hi"))]
1045        });
1046        let root = box_view()
1047            .modifier(Modifier::new().width(200.0))
1048            .with_children(vec![sub]);
1049
1050        tree.update(&root);
1051
1052        let observed = *captured.lock().unwrap();
1053        assert_eq!(observed.max_width, 200.0);
1054    }
1055
1056    #[test]
1057    fn test_subcompose_chained_ancestor_constraints_intersect() {
1058        let mut tree = ViewTree::new();
1059        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1060
1061        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1062        let cap2 = captured.clone();
1063
1064        let sub = multi_slot_view(move |scope| {
1065            *cap2.lock().unwrap() = *scope;
1066            vec![(0, text_view("hi"))]
1067        });
1068        // Box(width=400) -> Box(max_width=300) -> SubcomposeLayout.
1069        // The intersection should give max_width = 300.
1070        let root = box_view()
1071            .modifier(Modifier::new().width(400.0))
1072            .with_children(vec![box_view()
1073                .modifier(Modifier::new().max_width(300.0))
1074                .with_children(vec![sub])]);
1075
1076        tree.update(&root);
1077
1078        let observed = *captured.lock().unwrap();
1079        assert_eq!(observed.max_width, 300.0);
1080    }
1081
1082    #[test]
1083    fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
1084        let mut tree = ViewTree::new();
1085        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1086
1087        let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1088        let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1089        let outer2 = outer_captured.clone();
1090        let inner2 = inner_captured.clone();
1091
1092        // Outer SubcomposeLayout(width=400) hosts an inner SubcomposeLayout.
1093        // The inner closure should observe max_width = 400, not 1000.
1094        let inner = Arc::new(multi_slot_view(move |scope| {
1095            *inner2.lock().unwrap() = *scope;
1096            vec![(0, text_view("inner"))]
1097        }));
1098        let inner_clone = inner.clone();
1099        let outer = multi_slot_view(move |scope| {
1100            *outer2.lock().unwrap() = *scope;
1101            vec![(0, (*inner_clone).clone())]
1102        })
1103        .modifier(Modifier::new().width(400.0));
1104        let root = box_view().with_children(vec![outer]);
1105
1106        tree.update(&root);
1107
1108        let outer_obs = *outer_captured.lock().unwrap();
1109        let inner_obs = *inner_captured.lock().unwrap();
1110        assert_eq!(outer_obs.max_width, 400.0);
1111        assert_eq!(inner_obs.max_width, 400.0);
1112    }
1113}