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            && *cached_scope == scope {
110                return cached_slots.clone();
111            }
112        let mut slots = content(&scope);
113        for (slot_id, view) in slots.iter_mut() {
114            view.modifier.key = Some(*slot_id);
115        }
116        self.subcompose_cache
117            .insert(node_id, (scope, slots.clone()));
118        slots
119    }
120
121    /// Compute the `SubcomposeScope` visible to a `SubcomposeLayout` at
122    /// `node_id`. Starts with the user-set root scope and intersects each
123    /// ancestor's `Modifier` width / height / min / max fields in root-to-leaf
124    /// order. The SubcomposeLayout node itself is included.
125    fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
126        let mut scope = self.subcompose_scope;
127        let mut chain: Vec<NodeId> = Vec::new();
128        let mut current = Some(node_id);
129        while let Some(id) = current {
130            chain.push(id);
131            match self.nodes.get(id) {
132                Some(node) => current = node.parent,
133                None => break,
134            }
135        }
136        chain.reverse();
137        for ancestor_id in chain {
138            if let Some(node) = self.nodes.get(ancestor_id) {
139                scope = intersect_scope_with_modifier(scope, &node.modifier);
140            }
141        }
142        scope
143    }
144
145    /// Drop the cached subcomposed view for a single node. Call this when the
146    /// `SubcomposeLayout`'s modifier or identity changes so the next
147    /// reconciliation re-invokes the closure.
148    pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
149        self.subcompose_cache.remove(&node_id);
150    }
151
152    /// Drop the cached subcomposed views for a list of nodes (used by garbage
153    /// collection).
154    fn drop_subcompose_cache_for(&mut self, ids: &[NodeId]) {
155        for id in ids {
156            self.subcompose_cache.remove(id);
157        }
158    }
159
160    /// Recursively drop cached subcomposed views for a subtree rooted at
161    /// `node_id`. Called when the node is being removed.
162    fn collect_subcompose_cache(&mut self, node_id: &NodeId) {
163        self.subcompose_cache.remove(node_id);
164        let children: Vec<NodeId> = self
165            .nodes
166            .get(*node_id)
167            .map(|n| n.children.iter().copied().collect())
168            .unwrap_or_default();
169        for child in children {
170            self.collect_subcompose_cache(&child);
171        }
172    }
173
174    /// Get the current generation.
175    pub fn generation(&self) -> u64 {
176        self.generation
177    }
178
179    /// Get the root node ID.
180    pub fn root(&self) -> Option<NodeId> {
181        self.root
182    }
183
184    /// Get a node by ID.
185    pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
186        self.nodes.get(id)
187    }
188
189    /// Get a mutable node by ID.
190    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
191        self.nodes.get_mut(id)
192    }
193
194    /// Get a node by ViewId.
195    pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
196        self.view_id_map
197            .get(&view_id)
198            .and_then(|id| self.nodes.get(*id))
199    }
200
201    /// Get the number of nodes in the tree.
202    pub fn len(&self) -> usize {
203        self.nodes.len()
204    }
205
206    /// Check if the tree is empty.
207    pub fn is_empty(&self) -> bool {
208        self.nodes.is_empty()
209    }
210
211    /// Check if a node is marked dirty.
212    pub fn is_dirty(&self, id: NodeId) -> bool {
213        self.dirty.contains(&id)
214    }
215
216    /// Get the set of dirty nodes.
217    pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
218        &self.dirty
219    }
220
221    /// Clear the dirty set (after layout).
222    pub fn clear_dirty(&mut self) {
223        self.dirty.clear();
224    }
225
226    /// Mark a node as needing re-layout.
227    pub fn mark_dirty(&mut self, id: NodeId) {
228        self.dirty.insert(id);
229
230        // Also mark ancestors dirty (layout flows down from root)
231        let mut current = id;
232        while let Some(node) = self.nodes.get(current) {
233            if let Some(parent) = node.parent {
234                self.dirty.insert(parent);
235                current = parent;
236            } else {
237                break;
238            }
239        }
240    }
241
242    /// Update the tree from a new View, performing incremental reconciliation.
243    /// Returns the root NodeId.
244    pub fn update(&mut self, new_root: &View) -> NodeId {
245        self.removed_ids.clear(); // Clear previous frame's removals
246
247        self.generation += 1;
248        self.stats = TreeStats::default();
249
250        let mut ctx = ReconcileContext::new(self.generation);
251
252        let root_id = if let Some(existing_root) = self.root {
253            self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
254        } else {
255            self.create_node(new_root, None, 0, 0, &mut ctx)
256        };
257
258        self.root = Some(root_id);
259
260        // Remove orphaned nodes (nodes not updated this generation)
261        self.collect_garbage();
262
263        // Update stats
264        self.stats.total_nodes = self.nodes.len();
265        self.stats.dirty_nodes = self.dirty.len();
266        self.stats.reconciled_nodes = ctx.reconciled;
267        self.stats.skipped_nodes = ctx.skipped;
268        self.stats.created_nodes = ctx.created;
269        self.stats.removed_nodes = ctx.removed;
270
271        root_id
272    }
273
274    /// Reconcile an existing node with a new View.
275    fn reconcile_node(
276        &mut self,
277        node_id: NodeId,
278        view: &View,
279        parent: Option<NodeId>,
280        depth: u32,
281        index_in_parent: u32,
282        ctx: &mut ReconcileContext,
283    ) -> NodeId {
284        let content_hash = hash_view_content(view);
285
286        let old_hash = self
287            .nodes
288            .get(node_id)
289            .expect("reconcile_node: node not found")
290            .content_hash;
291        let content_changed = old_hash != content_hash;
292
293        if content_changed {
294            self.invalidate_subcompose_cache(node_id);
295        }
296
297        let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
298            let subcomposed = self.run_subcompose(node_id, content);
299            let slot_views: Vec<View> = subcomposed.into_iter().map(|(_, v)| v).collect();
300            self.reconcile_children(node_id, &slot_views, depth, ctx)
301        } else {
302            self.reconcile_children(node_id, &view.children, depth, ctx)
303        };
304
305        let new_subtree_hash = hash_subtree(content_hash, &new_children_hashes);
306
307        let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
308
309        let subtree_changed;
310        {
311            let node = self
312                .nodes
313                .get_mut(node_id)
314                .expect("reconcile_node: node not found");
315
316            // Update parent, depth, generation
317            node.parent = parent;
318            node.depth = depth;
319            node.generation = self.generation;
320
321            // 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.
322            node.kind = view.kind.clone();
323            node.modifier = view.modifier.clone();
324            node.content_hash = content_hash;
325            node.user_key = view.modifier.key;
326
327            if content_changed {
328                node.invalidate_layout();
329                ctx.reconciled += 1;
330            }
331
332            // Update subtree hash
333            subtree_changed = node.subtree_hash != new_subtree_hash;
334            if subtree_changed {
335                node.subtree_hash = new_subtree_hash;
336            } else if !content_changed {
337                ctx.skipped += 1;
338            }
339
340            // Update view_id
341            node.view_id = view_id;
342        } // Mutable borrow of node ends here
343
344        if subtree_changed {
345            self.mark_dirty(node_id);
346        }
347        self.view_id_map.insert(view_id, node_id);
348
349        node_id
350    }
351    /// Reconcile children of a node.
352    /// Returns the subtree hashes of all children (for computing parent's subtree hash).
353    fn reconcile_children(
354        &mut self,
355        parent_id: NodeId,
356        new_children: &[View],
357        parent_depth: u32,
358        ctx: &mut ReconcileContext,
359    ) -> Vec<u64> {
360        let child_depth = parent_depth + 1;
361
362        // Get current children
363        let old_children: SmallVec<[NodeId; 4]> = self
364            .nodes
365            .get(parent_id)
366            .map(|n| n.children.clone())
367            .unwrap_or_default();
368
369        // Build a map of keyed children for efficient lookup
370        let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
371        let mut unkeyed_children: Vec<NodeId> = Vec::new();
372
373        for &child_id in &old_children {
374            if let Some(node) = self.nodes.get(child_id) {
375                if let Some(key) = node.user_key {
376                    keyed_children.insert(key, child_id);
377                } else {
378                    unkeyed_children.push(child_id);
379                }
380            }
381        }
382
383        let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
384        let mut new_subtree_hashes: Vec<u64> = Vec::with_capacity(new_children.len());
385        let mut unkeyed_index = 0;
386        let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
387
388        for (i, new_child) in new_children.iter().enumerate() {
389            let idx = i as u32;
390            let child_id = if let Some(key) = new_child.modifier.key {
391                // Keyed child: look up by key
392                if let Some(&existing_id) = keyed_children.get(&key) {
393                    used_nodes.insert(existing_id);
394                    self.reconcile_node(
395                        existing_id,
396                        new_child,
397                        Some(parent_id),
398                        child_depth,
399                        idx,
400                        ctx,
401                    )
402                } else {
403                    self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
404                }
405            } else {
406                // Unkeyed child: match by position
407                if unkeyed_index < unkeyed_children.len() {
408                    let existing_id = unkeyed_children[unkeyed_index];
409                    unkeyed_index += 1;
410                    used_nodes.insert(existing_id);
411                    self.reconcile_node(
412                        existing_id,
413                        new_child,
414                        Some(parent_id),
415                        child_depth,
416                        idx,
417                        ctx,
418                    )
419                } else {
420                    self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
421                }
422            };
423
424            new_child_ids.push(child_id);
425
426            if let Some(node) = self.nodes.get(child_id) {
427                new_subtree_hashes.push(node.subtree_hash);
428            }
429        }
430
431        // Mark unused old children for removal
432        for &old_child in &old_children {
433            if !used_nodes.contains(&old_child) {
434                self.mark_for_removal(old_child, ctx);
435            }
436        }
437
438        // Update parent's children list
439        if let Some(parent) = self.nodes.get_mut(parent_id) {
440            parent.children = new_child_ids;
441        }
442
443        new_subtree_hashes
444    }
445
446    /// Create a new node from a View.
447    fn create_node(
448        &mut self,
449        view: &View,
450        parent: Option<NodeId>,
451        depth: u32,
452        index_in_parent: u32,
453        ctx: &mut ReconcileContext,
454    ) -> NodeId {
455        let content_hash = hash_view_content(view);
456
457        // Insert a partial node first
458        let node_id = self.nodes.insert_with_key(|id| {
459            TreeNode::new(
460                id,
461                0,
462                view.kind.clone(),
463                view.modifier.clone(),
464                self.generation,
465            )
466        });
467        ctx.created += 1;
468
469        {
470            let node = self
471                .nodes
472                .get_mut(node_id)
473                .expect("create_node: node just inserted");
474            node.parent = parent;
475            node.depth = depth;
476            node.content_hash = content_hash;
477            node.user_key = view.modifier.key;
478        }
479
480        // Now, recursively create children
481        let child_depth = depth + 1;
482        let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
483        let mut child_hashes: Vec<u64> = Vec::with_capacity(view.children.len());
484        let children_to_create: Vec<View> =
485            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![
918            subcompose_view(move |_scope| {
919                c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
920                text_view("hi")
921            })
922            .modifier(Modifier::new().padding(4.0)),
923        ]);
924
925        tree.update(&root2);
926        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
927    }
928
929    #[test]
930    fn test_subcompose_cache_drops_on_node_removal() {
931        let mut tree = ViewTree::new();
932        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
933
934        // First root has a SubcomposeLayout child.
935        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
936        let c1 = counter.clone();
937        let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
938            c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
939            text_view("hi")
940        })]);
941
942        tree.update(&root_with_sub);
943        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
944
945        // Swap to a root without the SubcomposeLayout - the old node should be
946        // garbage-collected and its cache entry dropped.
947        let root_no_sub = box_view().with_children(vec![text_view("plain")]);
948        tree.update(&root_no_sub);
949        assert_eq!(tree.len(), 2);
950
951        // Bring the SubcomposeLayout back - it must run the closure again
952        // because the cache entry was dropped during GC.
953        let c2 = counter.clone();
954        let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
955            c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
956            text_view("hi")
957        })]);
958
959        tree.update(&root_with_sub_again);
960        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
961    }
962
963    fn multi_slot_view<F>(f: F) -> View
964    where
965        F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
966    {
967        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
968        View {
969            id: 0,
970            kind: ViewKind::SubcomposeLayout { content },
971            modifier: Modifier::default(),
972            children: Vec::new(),
973            semantics: None,
974        }
975    }
976
977    #[test]
978    fn test_subcompose_multi_slot_produces_multiple_children() {
979        let mut tree = ViewTree::new();
980        let root = box_view().with_children(vec![multi_slot_view(|_scope| {
981            vec![
982                (0, text_view("a")),
983                (1, text_view("b")),
984                (2, text_view("c")),
985            ]
986        })]);
987
988        tree.update(&root);
989
990        // box + subcompose + 3 texts
991        assert_eq!(tree.len(), 5);
992        let sub_id = tree
993            .root()
994            .and_then(|r| tree.children(r))
995            .and_then(|c| c.first().copied())
996            .expect("subcompose node");
997        let sub_children = tree.children(sub_id).expect("subcompose has children");
998        assert_eq!(sub_children.len(), 3);
999    }
1000
1001    #[test]
1002    fn test_subcompose_multi_slot_preserves_identity_across_removal() {
1003        let mut tree = ViewTree::new();
1004
1005        // 3 slots: 0, 1, 2
1006        let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
1007            vec![
1008                (0, text_view("a")),
1009                (1, text_view("b")),
1010                (2, text_view("c")),
1011            ]
1012        })]);
1013        tree.update(&root3);
1014
1015        let sub_id = tree
1016            .root()
1017            .and_then(|r| tree.children(r))
1018            .and_then(|c| c.first().copied())
1019            .expect("subcompose node");
1020        let before = tree.children(sub_id).expect("children").to_vec();
1021        let a_node = before[0];
1022        let b_node = before[1];
1023        let c_node = before[2];
1024
1025        // 2 slots: 0, 2 (middle removed). The Modifier change (padding) forces
1026        // the subcompose cache to invalidate so the new closure runs.
1027        let root2 = box_view().with_children(vec![
1028            multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
1029                .modifier(Modifier::new().padding(4.0)),
1030        ]);
1031        tree.update(&root2);
1032
1033        let after = tree.children(sub_id).expect("children after");
1034        assert_eq!(after.len(), 2);
1035        // Slot 0 (a) and slot 2 (c) should keep their NodeId.
1036        assert_eq!(after[0], a_node);
1037        assert_eq!(after[1], c_node);
1038        // Slot 1 (b) should be gone.
1039        assert!(tree.get(b_node).is_none());
1040    }
1041
1042    #[test]
1043    fn test_subcompose_ancestor_modifier_narrows_scope() {
1044        let mut tree = ViewTree::new();
1045        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1046
1047        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1048        let cap2 = captured.clone();
1049
1050        // SubcomposeLayout inside a Box with width(200.dp) - the closure should
1051        // see max_width == 200.
1052        let sub = multi_slot_view(move |scope| {
1053            *cap2.lock().unwrap() = *scope;
1054            vec![(0, text_view("hi"))]
1055        });
1056        let root = box_view()
1057            .modifier(Modifier::new().width(200.0))
1058            .with_children(vec![sub]);
1059
1060        tree.update(&root);
1061
1062        let observed = *captured.lock().unwrap();
1063        assert_eq!(observed.max_width, 200.0);
1064    }
1065
1066    #[test]
1067    fn test_subcompose_chained_ancestor_constraints_intersect() {
1068        let mut tree = ViewTree::new();
1069        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1070
1071        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1072        let cap2 = captured.clone();
1073
1074        let sub = multi_slot_view(move |scope| {
1075            *cap2.lock().unwrap() = *scope;
1076            vec![(0, text_view("hi"))]
1077        });
1078        // Box(width=400) -> Box(max_width=300) -> SubcomposeLayout.
1079        // The intersection should give max_width = 300.
1080        let root = box_view()
1081            .modifier(Modifier::new().width(400.0))
1082            .with_children(vec![
1083                box_view()
1084                    .modifier(Modifier::new().max_width(300.0))
1085                    .with_children(vec![sub]),
1086            ]);
1087
1088        tree.update(&root);
1089
1090        let observed = *captured.lock().unwrap();
1091        assert_eq!(observed.max_width, 300.0);
1092    }
1093
1094    #[test]
1095    fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
1096        let mut tree = ViewTree::new();
1097        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1098
1099        let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1100        let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1101        let outer2 = outer_captured.clone();
1102        let inner2 = inner_captured.clone();
1103
1104        // Outer SubcomposeLayout(width=400) hosts an inner SubcomposeLayout.
1105        // The inner closure should observe max_width = 400, not 1000.
1106        let inner = Arc::new(multi_slot_view(move |scope| {
1107            *inner2.lock().unwrap() = *scope;
1108            vec![(0, text_view("inner"))]
1109        }));
1110        let inner_clone = inner.clone();
1111        let outer = multi_slot_view(move |scope| {
1112            *outer2.lock().unwrap() = *scope;
1113            vec![(0, (*inner_clone).clone())]
1114        })
1115        .modifier(Modifier::new().width(400.0));
1116        let root = box_view().with_children(vec![outer]);
1117
1118        tree.update(&root);
1119
1120        let outer_obs = *outer_captured.lock().unwrap();
1121        let inner_obs = *inner_captured.lock().unwrap();
1122        assert_eq!(outer_obs.max_width, 400.0);
1123        assert_eq!(inner_obs.max_width, 400.0);
1124    }
1125}