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