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