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