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        let mut new_seen_keys: FxHashSet<u64> = FxHashSet::default();
401
402        for (i, new_child) in new_children.iter().enumerate() {
403            if let Some(key) = new_child.modifier.key {
404                if !new_seen_keys.insert(key) {
405                    panic!(
406                        "reconcile_children: duplicate modifier.key={} in children of node {:?}.\n\
407                         Two sibling views share the same key. Each view passed to a layout \
408                         must have a unique modifier.key. For lazy layouts (LazyColumn, LazyRow, \
409                         etc.), ensure `get_key` returns a unique key for each item by hashing \
410                         the full item identity.",
411                        key, parent_id,
412                    );
413                }
414            }
415            let idx = i as u32;
416            let child_id = if let Some(key) = new_child.modifier.key {
417                // Keyed child: look up by key
418                if let Some(&existing_id) = keyed_children.get(&key) {
419                    used_nodes.insert(existing_id);
420                    self.reconcile_node(
421                        existing_id,
422                        new_child,
423                        Some(parent_id),
424                        child_depth,
425                        idx,
426                        ctx,
427                    )
428                } else {
429                    self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
430                }
431            } else {
432                // Unkeyed child: match by position
433                if unkeyed_index < unkeyed_children.len() {
434                    let existing_id = unkeyed_children[unkeyed_index];
435                    unkeyed_index += 1;
436                    used_nodes.insert(existing_id);
437                    self.reconcile_node(
438                        existing_id,
439                        new_child,
440                        Some(parent_id),
441                        child_depth,
442                        idx,
443                        ctx,
444                    )
445                } else {
446                    self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
447                }
448            };
449
450            new_child_ids.push(child_id);
451
452            if let Some(node) = self.nodes.get(child_id) {
453                new_subtree_hashes.push(node.subtree_hash);
454            }
455        }
456
457        // Mark unused old children for removal
458        for &old_child in &old_children {
459            if !used_nodes.contains(&old_child) {
460                self.mark_for_removal(old_child, ctx);
461            }
462        }
463
464        // Update parent's children list
465        if let Some(parent) = self.nodes.get_mut(parent_id) {
466            parent.children = new_child_ids;
467        }
468
469        new_subtree_hashes
470    }
471
472    /// Create a new node from a View.
473    fn create_node(
474        &mut self,
475        view: &View,
476        parent: Option<NodeId>,
477        depth: u32,
478        index_in_parent: u32,
479        ctx: &mut ReconcileContext,
480    ) -> NodeId {
481        let content_hash = hash_view_content(view);
482
483        // Insert a partial node first
484        let node_id = self.nodes.insert_with_key(|id| {
485            TreeNode::new(
486                id,
487                0,
488                view.kind.clone(),
489                view.modifier.clone(),
490                self.generation,
491            )
492        });
493        ctx.created += 1;
494
495        {
496            let node = self
497                .nodes
498                .get_mut(node_id)
499                .expect("create_node: node just inserted");
500            node.parent = parent;
501            node.depth = depth;
502            node.content_hash = content_hash;
503            node.user_key = view.modifier.key;
504            node.scope_key = view.scope_key.clone();
505        }
506
507        // Now, recursively create children
508        let child_depth = depth + 1;
509        let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
510        let mut child_hashes: Vec<u64> = Vec::with_capacity(view.children.len());
511        let children_to_create: Vec<View> =
512            if let ViewKind::SubcomposeLayout { content } = &view.kind {
513                self.run_subcompose(node_id, content)
514                    .into_iter()
515                    .map(|(_, v)| v)
516                    .collect()
517            } else {
518                view.children.clone()
519            };
520        for (i, child_view) in children_to_create.iter().enumerate() {
521            let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
522            child_ids.push(child_id);
523            child_hashes.push(
524                self.nodes
525                    .get(child_id)
526                    .expect("create_node: child just created")
527                    .subtree_hash,
528            );
529        }
530
531        // Now compute the view_id and subtree_hash, and update the node
532        let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
533        let subtree_hash = hash_subtree(content_hash, &child_hashes);
534
535        let node = self
536            .nodes
537            .get_mut(node_id)
538            .expect("create_node: node just inserted");
539        node.children = child_ids;
540        node.subtree_hash = subtree_hash;
541        node.view_id = view_id;
542
543        self.view_id_map.insert(view_id, node_id);
544        self.dirty.insert(node_id);
545
546        node_id
547    }
548    /// Compute a stable ViewId for a node.
549    fn compute_view_id(
550        &self,
551        view: &View,
552        _node_id: NodeId,
553        parent: Option<NodeId>,
554        index_in_parent: u32,
555    ) -> ViewId {
556        // If the view already has an ID assigned, use it
557        if view.id != 0 {
558            return view.id;
559        }
560
561        // Otherwise compute from parent + index/key
562        let parent_id = parent
563            .and_then(|p| self.nodes.get(p))
564            .map(|n| n.view_id)
565            .unwrap_or(0);
566
567        let salt = view.modifier.key.unwrap_or(index_in_parent as u64);
568
569        // Simple hash combination
570        let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
571        id = id.wrapping_mul(0x9E3779B97F4A7C15);
572        id ^= id >> 30;
573
574        if id == 0 {
575            id = 1;
576        }
577
578        id
579    }
580
581    /// Mark a node and its descendants for removal.
582    fn mark_for_removal(&mut self, node_id: NodeId, ctx: &mut ReconcileContext) {
583        // Gather what we need from the node first so the immutable borrow ends
584        // before we mutate other state.
585        let (view_id, children) = {
586            let node = self.nodes.get(node_id);
587            match node {
588                Some(n) => (n.view_id, n.children.clone()),
589                None => return,
590            }
591        };
592        self.view_id_map.remove(&view_id);
593        self.subcompose_cache.remove(&node_id);
594        for child_id in children.iter() {
595            self.collect_subcompose_cache(child_id);
596        }
597        for child_id in children {
598            self.mark_for_removal(child_id, ctx);
599        }
600        ctx.removed += 1;
601
602        // Mark the node's generation as old so it gets collected
603        if let Some(node) = self.nodes.get_mut(node_id) {
604            node.generation = 0; // Will be collected
605        }
606    }
607
608    /// Remove nodes that weren't updated this generation.
609    fn collect_garbage(&mut self) {
610        let current_gen = self.generation;
611
612        // Find nodes to remove
613        let to_remove: Vec<NodeId> = self
614            .nodes
615            .iter()
616            .filter(|(_, node)| node.generation != current_gen)
617            .map(|(id, _)| id)
618            .collect();
619
620        // Remove them
621        for id in to_remove {
622            if let Some(node) = self.nodes.remove(id) {
623                self.view_id_map.remove(&node.view_id);
624                self.dirty.remove(&id);
625
626                // Track removal for external sync
627                self.removed_ids.push(id);
628            }
629        }
630    }
631
632    /// Set cached layout for a node.
633    pub fn set_layout(
634        &mut self,
635        id: NodeId,
636        rect: Rect,
637        screen_rect: Rect,
638        constraints: LayoutConstraints,
639    ) {
640        if let Some(node) = self.nodes.get_mut(id) {
641            node.layout_cache = Some(LayoutCache {
642                rect,
643                screen_rect,
644                constraints,
645                generation: self.generation,
646            });
647        }
648    }
649
650    /// Iterate over all nodes (parent before children).
651    pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
652        self.nodes.values()
653    }
654
655    /// Iterate over all nodes with their IDs.
656    pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
657        self.nodes.iter()
658    }
659
660    /// Walk the tree from root, calling `f` for each node.
661    /// Returns early if `f` returns false.
662    pub fn walk<F>(&self, mut f: F)
663    where
664        F: FnMut(&TreeNode, u32) -> bool,
665    {
666        if let Some(root_id) = self.root {
667            self.walk_node(root_id, 0, &mut f);
668        }
669    }
670
671    fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
672    where
673        F: FnMut(&TreeNode, u32) -> bool,
674    {
675        if let Some(node) = self.nodes.get(id) {
676            if !f(node, depth) {
677                return;
678            }
679
680            for &child_id in &node.children {
681                self.walk_node(child_id, depth + 1, f);
682            }
683        }
684    }
685
686    /// Get children of a node.
687    pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
688        self.nodes.get(id).map(|n| n.children.as_slice())
689    }
690}
691
692/// Intersect a `SubcomposeScope` with a `Modifier`'s size-related fields.
693/// `Modifier::width` / `Modifier::height` are treated as exact sizes (the
694/// resulting min and max both equal that value). Padding values reduce the
695/// available content area (matching Compose semantics where padding offsets
696/// the child constraints). `fill_max_w` / `fill_max_h` are layout-direction
697/// hints resolved by Taffy and cannot produce a concrete size here; they
698/// are handled by storing the Taffy-computed size after layout.
699fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
700    let mut s = scope;
701    // `size()` sets both min and max to the same value (exact size).
702    if let Some(sz) = modifier.size {
703        s.min_width = s.min_width.max(sz.width);
704        s.max_width = s.max_width.min(sz.width);
705        s.min_height = s.min_height.max(sz.height);
706        s.max_height = s.max_height.min(sz.height);
707    }
708    if let Some(w) = modifier.width {
709        s.min_width = s.min_width.max(w);
710        s.max_width = s.max_width.min(w);
711    }
712    if let Some(h) = modifier.height {
713        s.min_height = s.min_height.max(h);
714        s.max_height = s.max_height.min(h);
715    }
716    if let Some(mw) = modifier.min_width {
717        s.min_width = s.min_width.max(mw);
718    }
719    if let Some(mh) = modifier.min_height {
720        s.min_height = s.min_height.max(mh);
721    }
722    if let Some(mw) = modifier.max_width {
723        s.max_width = s.max_width.min(mw);
724    }
725    if let Some(mh) = modifier.max_height {
726        s.max_height = s.max_height.min(mh);
727    }
728    // Padding reduces the available content area (Compose semantics:
729    // constraints are offset by the padding amount).
730    if let Some(p) = modifier.padding {
731        let total = p * 2.0;
732        s.min_width = (s.min_width - total).max(0.0);
733        s.max_width = (s.max_width - total).max(0.0);
734        s.min_height = (s.min_height - total).max(0.0);
735        s.max_height = (s.max_height - total).max(0.0);
736    }
737    if let Some(pv) = modifier.padding_values {
738        let h_total = pv.left + pv.right;
739        let v_total = pv.top + pv.bottom;
740        s.min_width = (s.min_width - h_total).max(0.0);
741        s.max_width = (s.max_width - h_total).max(0.0);
742        s.min_height = (s.min_height - v_total).max(0.0);
743        s.max_height = (s.max_height - v_total).max(0.0);
744    }
745    s
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use repose_core::{
752        Color, FontStyle, FontWeight, Modifier, SubcomposeScope, TextAlign, TextDecoration, View,
753        ViewKind,
754    };
755    use std::sync::Arc;
756
757    fn text_view(text: &str) -> View {
758        View::new(
759            0,
760            ViewKind::Text {
761                text: text.to_string(),
762                color: Color::WHITE,
763                font_size: 16.0,
764                soft_wrap: true,
765                max_lines: None,
766                overflow: repose_core::TextOverflow::Visible,
767                font_family: None,
768                annotations: None,
769                text_align: TextAlign::Unspecified,
770                font_weight: FontWeight::NORMAL,
771                font_style: FontStyle::Normal,
772                text_decoration: TextDecoration::default(),
773                letter_spacing: 0.0,
774                line_height: 0.0,
775            },
776        )
777    }
778
779    fn box_view() -> View {
780        View::new(0, ViewKind::Box)
781    }
782
783    #[test]
784    fn test_create_tree() {
785        let mut tree = ViewTree::new();
786
787        let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
788
789        tree.update(&root);
790
791        assert_eq!(tree.len(), 3); // box + 2 text
792        assert!(tree.root().is_some());
793    }
794
795    #[test]
796    fn test_unchanged_tree_skips() {
797        let mut tree = ViewTree::new();
798
799        let root = box_view().with_children(vec![text_view("Hello")]);
800
801        tree.update(&root);
802        let gen1 = tree.generation();
803
804        // Same tree
805        tree.update(&root);
806        let gen2 = tree.generation();
807
808        assert_eq!(gen2, gen1 + 1);
809        assert!(tree.stats.skipped_nodes > 0);
810    }
811
812    #[test]
813    fn test_changed_content_reconciles() {
814        let mut tree = ViewTree::new();
815
816        let root1 = box_view().with_children(vec![text_view("Hello")]);
817
818        tree.update(&root1);
819
820        let root2 = box_view().with_children(vec![text_view("Changed")]);
821
822        tree.update(&root2);
823
824        assert!(tree.stats.reconciled_nodes > 0);
825    }
826
827    #[test]
828    fn test_keyed_children_stable() {
829        let mut tree = ViewTree::new();
830
831        // Initial: A, B, C
832        let root1 = box_view().with_children(vec![
833            text_view("A").modifier(Modifier::new().key(1)),
834            text_view("B").modifier(Modifier::new().key(2)),
835            text_view("C").modifier(Modifier::new().key(3)),
836        ]);
837
838        tree.update(&root1);
839
840        // Get B's NodeId
841        let b_view_id = tree
842            .root()
843            .and_then(|r| tree.children(r))
844            .and_then(|c| c.get(1).copied())
845            .and_then(|id| tree.get(id))
846            .map(|n| n.view_id);
847
848        // Reorder: C, A, B
849        let root2 = box_view().with_children(vec![
850            text_view("C").modifier(Modifier::new().key(3)),
851            text_view("A").modifier(Modifier::new().key(1)),
852            text_view("B").modifier(Modifier::new().key(2)),
853        ]);
854
855        tree.update(&root2);
856
857        // B should have same view_id (key-based stability)
858        // Note: Implementation detail - the node may be reused
859        assert_eq!(tree.len(), 4); // Still 4 nodes (box + 3 text)
860    }
861
862    fn subcompose_view<F>(f: F) -> View
863    where
864        F: Fn(&SubcomposeScope) -> View + 'static,
865    {
866        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
867            Arc::new(move |scope| vec![(0, f(scope))]);
868        View {
869            id: 0,
870            kind: ViewKind::SubcomposeLayout { content },
871            modifier: Modifier::default(),
872            children: Vec::new(),
873            scope_key: None,
874            semantics: None,
875        }
876    }
877
878    #[test]
879    fn test_subcompose_invokes_content() {
880        let mut tree = ViewTree::new();
881        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
882        let counter2 = counter.clone();
883
884        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
885            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
886            text_view("from subcompose")
887        })]);
888
889        tree.update(&root);
890
891        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
892        assert_eq!(tree.len(), 3); // box + subcompose + text
893    }
894
895    #[test]
896    fn test_subcompose_receives_scope() {
897        let mut tree = ViewTree::new();
898        let captured = Arc::new(std::sync::Mutex::new(None));
899        let captured2 = captured.clone();
900
901        let root = box_view().with_children(vec![subcompose_view(move |scope| {
902            *captured2.lock().unwrap() = Some(*scope);
903            text_view("hi")
904        })]);
905
906        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 360.0, 0.0, 640.0));
907        tree.update(&root);
908
909        let observed = captured.lock().unwrap().expect("scope captured");
910        assert_eq!(observed.max_width, 360.0);
911        assert_eq!(observed.max_height, 640.0);
912        assert_eq!(observed.min_width, 0.0);
913        assert_eq!(observed.min_height, 0.0);
914    }
915
916    #[test]
917    fn test_subcompose_re_invokes_on_update() {
918        let mut tree = ViewTree::new();
919        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
920        let counter2 = counter.clone();
921
922        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
923            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
924            text_view("hi")
925        })]);
926
927        tree.update(&root);
928        tree.update(&root);
929        tree.update(&root);
930
931        // Closure should run only on the first update; subsequent updates hit
932        // the cache because the scope and content are unchanged.
933        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
934    }
935
936    #[test]
937    fn test_subcompose_reruns_on_scope_change() {
938        let mut tree = ViewTree::new();
939        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
940        let counter2 = counter.clone();
941
942        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
943            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
944            text_view("hi")
945        })]);
946
947        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
948        tree.update(&root);
949        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
950
951        // Same scope: cache hit.
952        tree.update(&root);
953        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
954
955        // Scope changed: closure re-runs.
956        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 200.0, 0.0, 200.0));
957        tree.update(&root);
958        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
959    }
960
961    #[test]
962    fn test_subcompose_reruns_on_content_change() {
963        let mut tree = ViewTree::new();
964        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
965        let c1 = counter.clone();
966
967        let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
968            c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
969            text_view("hi")
970        })]);
971
972        tree.update(&root1);
973        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
974
975        // Same content: cache hit.
976        tree.update(&root1);
977        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
978
979        // Changed modifier: closure re-runs.
980        let c2 = counter.clone();
981        let root2 = box_view().with_children(vec![
982            subcompose_view(move |_scope| {
983                c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
984                text_view("hi")
985            })
986            .modifier(Modifier::new().padding(4.0)),
987        ]);
988
989        tree.update(&root2);
990        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
991    }
992
993    #[test]
994    fn test_subcompose_cache_drops_on_node_removal() {
995        let mut tree = ViewTree::new();
996        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
997
998        // First root has a SubcomposeLayout child.
999        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1000        let c1 = counter.clone();
1001        let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
1002            c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1003            text_view("hi")
1004        })]);
1005
1006        tree.update(&root_with_sub);
1007        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1008
1009        // Swap to a root without the SubcomposeLayout - the old node should be
1010        // garbage-collected and its cache entry dropped.
1011        let root_no_sub = box_view().with_children(vec![text_view("plain")]);
1012        tree.update(&root_no_sub);
1013        assert_eq!(tree.len(), 2);
1014
1015        // Bring the SubcomposeLayout back - it must run the closure again
1016        // because the cache entry was dropped during GC.
1017        let c2 = counter.clone();
1018        let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
1019            c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1020            text_view("hi")
1021        })]);
1022
1023        tree.update(&root_with_sub_again);
1024        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1025    }
1026
1027    fn multi_slot_view<F>(f: F) -> View
1028    where
1029        F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
1030    {
1031        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
1032        View {
1033            id: 0,
1034            kind: ViewKind::SubcomposeLayout { content },
1035            modifier: Modifier::default(),
1036            children: Vec::new(),
1037            scope_key: None,
1038            semantics: None,
1039        }
1040    }
1041
1042    #[test]
1043    fn test_subcompose_multi_slot_produces_multiple_children() {
1044        let mut tree = ViewTree::new();
1045        let root = box_view().with_children(vec![multi_slot_view(|_scope| {
1046            vec![
1047                (0, text_view("a")),
1048                (1, text_view("b")),
1049                (2, text_view("c")),
1050            ]
1051        })]);
1052
1053        tree.update(&root);
1054
1055        // box + subcompose + 3 texts
1056        assert_eq!(tree.len(), 5);
1057        let sub_id = tree
1058            .root()
1059            .and_then(|r| tree.children(r))
1060            .and_then(|c| c.first().copied())
1061            .expect("subcompose node");
1062        let sub_children = tree.children(sub_id).expect("subcompose has children");
1063        assert_eq!(sub_children.len(), 3);
1064    }
1065
1066    #[test]
1067    fn test_subcompose_multi_slot_preserves_identity_across_removal() {
1068        let mut tree = ViewTree::new();
1069
1070        // 3 slots: 0, 1, 2
1071        let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
1072            vec![
1073                (0, text_view("a")),
1074                (1, text_view("b")),
1075                (2, text_view("c")),
1076            ]
1077        })]);
1078        tree.update(&root3);
1079
1080        let sub_id = tree
1081            .root()
1082            .and_then(|r| tree.children(r))
1083            .and_then(|c| c.first().copied())
1084            .expect("subcompose node");
1085        let before = tree.children(sub_id).expect("children").to_vec();
1086        let a_node = before[0];
1087        let b_node = before[1];
1088        let c_node = before[2];
1089
1090        // 2 slots: 0, 2 (middle removed). The Modifier change (padding) forces
1091        // the subcompose cache to invalidate so the new closure runs.
1092        let root2 = box_view().with_children(vec![
1093            multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
1094                .modifier(Modifier::new().padding(4.0)),
1095        ]);
1096        tree.update(&root2);
1097
1098        let after = tree.children(sub_id).expect("children after");
1099        assert_eq!(after.len(), 2);
1100        // Slot 0 (a) and slot 2 (c) should keep their NodeId.
1101        assert_eq!(after[0], a_node);
1102        assert_eq!(after[1], c_node);
1103        // Slot 1 (b) should be gone.
1104        assert!(tree.get(b_node).is_none());
1105    }
1106
1107    #[test]
1108    fn test_subcompose_ancestor_modifier_narrows_scope() {
1109        let mut tree = ViewTree::new();
1110        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1111
1112        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1113        let cap2 = captured.clone();
1114
1115        // SubcomposeLayout inside a Box with width(200.dp) - the closure should
1116        // see max_width == 200.
1117        let sub = multi_slot_view(move |scope| {
1118            *cap2.lock().unwrap() = *scope;
1119            vec![(0, text_view("hi"))]
1120        });
1121        let root = box_view()
1122            .modifier(Modifier::new().width(200.0))
1123            .with_children(vec![sub]);
1124
1125        tree.update(&root);
1126
1127        let observed = *captured.lock().unwrap();
1128        assert_eq!(observed.max_width, 200.0);
1129    }
1130
1131    #[test]
1132    fn test_subcompose_chained_ancestor_constraints_intersect() {
1133        let mut tree = ViewTree::new();
1134        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1135
1136        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1137        let cap2 = captured.clone();
1138
1139        let sub = multi_slot_view(move |scope| {
1140            *cap2.lock().unwrap() = *scope;
1141            vec![(0, text_view("hi"))]
1142        });
1143        // Box(width=400) -> Box(max_width=300) -> SubcomposeLayout.
1144        // The intersection should give max_width = 300.
1145        let root = box_view()
1146            .modifier(Modifier::new().width(400.0))
1147            .with_children(vec![
1148                box_view()
1149                    .modifier(Modifier::new().max_width(300.0))
1150                    .with_children(vec![sub]),
1151            ]);
1152
1153        tree.update(&root);
1154
1155        let observed = *captured.lock().unwrap();
1156        assert_eq!(observed.max_width, 300.0);
1157    }
1158
1159    #[test]
1160    fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
1161        let mut tree = ViewTree::new();
1162        tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1163
1164        let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1165        let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1166        let outer2 = outer_captured.clone();
1167        let inner2 = inner_captured.clone();
1168
1169        // Outer SubcomposeLayout(width=400) hosts an inner SubcomposeLayout.
1170        // The inner closure should observe max_width = 400, not 1000.
1171        let inner = Arc::new(multi_slot_view(move |scope| {
1172            *inner2.lock().unwrap() = *scope;
1173            vec![(0, text_view("inner"))]
1174        }));
1175        let inner_clone = inner.clone();
1176        let outer = multi_slot_view(move |scope| {
1177            *outer2.lock().unwrap() = *scope;
1178            vec![(0, (*inner_clone).clone())]
1179        })
1180        .modifier(Modifier::new().width(400.0));
1181        let root = box_view().with_children(vec![outer]);
1182
1183        tree.update(&root);
1184
1185        let outer_obs = *outer_captured.lock().unwrap();
1186        let inner_obs = *inner_captured.lock().unwrap();
1187        assert_eq!(outer_obs.max_width, 400.0);
1188        assert_eq!(inner_obs.max_width, 400.0);
1189    }
1190}