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