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