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