Skip to main content

repose_tree/
tree.rs

1//! The main ViewTree structure.
2
3use crate::{
4    hash::{SubtreeHashBuilder, SubtreeHashes, ViewHashes, hash_view_facets},
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::rc::Rc;
14use std::sync::Arc;
15
16/// A persistent view tree that supports incremental updates.
17pub struct ViewTree {
18    /// All nodes in the tree.
19    nodes: SlotMap<NodeId, TreeNode>,
20
21    /// The root node.
22    root: Option<NodeId>,
23
24    /// Nodes that need re-layout.
25    dirty: FxHashSet<NodeId>,
26    dirty_propagated: FxHashSet<NodeId>,
27
28    paint_dirty: FxHashSet<NodeId>,
29    paint_dirty_propagated: FxHashSet<NodeId>,
30
31    /// Current generation (frame counter).
32    generation: u64,
33
34    /// Map from user-facing ViewId to internal NodeId.
35    view_id_map: FxHashMap<ViewId, NodeId>,
36
37    duplicate_view_ids: FxHashSet<ViewId>,
38
39    /// Statistics from the last reconcile operation.
40    pub stats: TreeStats,
41
42    /// Nodes removed during the last update (needed to sync external systems like Taffy).
43    pub removed_ids: Vec<NodeId>,
44
45    pending_removals: Vec<NodeId>,
46    removal_seen: FxHashSet<NodeId>,
47
48    /// Root constraints to use when calling a `SubcomposeLayout`'s content
49    /// closure during this frame. Set via [`ViewTree::set_subcompose_scope`]
50    /// before calling [`ViewTree::update`].
51    subcompose_scope: SubcomposeScope,
52
53    /// Cache of (scope, subcomposed slots) for each `SubcomposeLayout` node.
54    /// The closure is re-invoked only when the ancestor-derived scope
55    /// changes or the node's content changes. Each cached slot view has its
56    /// `Modifier::key` overwritten with its slot id.
57    subcompose_cache: FxHashMap<NodeId, (SubcomposeScope, Arc<Vec<View>>)>,
58    /// Per-frame re-invocation counts for oscillation damping
59    /// (node -> (generation, count)).
60    subcompose_runs: FxHashMap<NodeId, (u64, u8, SubcomposeScope)>,
61
62    scope_invalidation_seen: FxHashSet<NodeId>,
63}
64
65impl Default for ViewTree {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71fn rc_identity_changed<T: ?Sized>(old: &Option<Rc<T>>, new: &Option<Rc<T>>) -> bool {
72    match (old, new) {
73        (Some(old), Some(new)) => !Rc::ptr_eq(old, new),
74        (None, None) => false,
75        _ => true,
76    }
77}
78
79fn arc_identity_changed<T: ?Sized>(old: &Option<Arc<T>>, new: &Option<Arc<T>>) -> bool {
80    match (old, new) {
81        (Some(old), Some(new)) => !Arc::ptr_eq(old, new),
82        (None, None) => false,
83        _ => true,
84    }
85}
86
87#[derive(Clone, Copy, Default)]
88struct IdentityChange {
89    paint: bool,
90    layout: bool,
91    subcompose: bool,
92}
93
94impl IdentityChange {
95    fn merge(&mut self, other: Self) {
96        self.paint |= other.paint;
97        self.layout |= other.layout;
98        self.subcompose |= other.subcompose;
99    }
100}
101
102fn focus_requester_changed(
103    old: &Option<repose_core::FocusRequester>,
104    new: &Option<repose_core::FocusRequester>,
105) -> bool {
106    match (old, new) {
107        (Some(old), Some(new)) => !Rc::ptr_eq(&old.target, &new.target),
108        (None, None) => false,
109        _ => true,
110    }
111}
112
113fn interaction_source_changed(
114    old: &Option<repose_core::InteractionSource>,
115    new: &Option<repose_core::InteractionSource>,
116) -> bool {
117    match (old, new) {
118        (Some(old), Some(new)) => old.stable_id() != new.stable_id(),
119        (None, None) => false,
120        _ => true,
121    }
122}
123
124fn scroll_axis_identity_changed(
125    old: &repose_core::scroll::ScrollAxisBinding,
126    new: &repose_core::scroll::ScrollAxisBinding,
127) -> bool {
128    rc_identity_changed(&old.on_scroll, &new.on_scroll)
129        || rc_identity_changed(&old.set_viewport_main, &new.set_viewport_main)
130        || rc_identity_changed(&old.set_content_main, &new.set_content_main)
131        || rc_identity_changed(&old.get_offset_main, &new.get_offset_main)
132        || rc_identity_changed(&old.set_offset_main, &new.set_offset_main)
133        || rc_identity_changed(&old.tick, &new.tick)
134        || rc_identity_changed(&old.set_nested_scroll_parent, &new.set_nested_scroll_parent)
135}
136
137fn scroll_identity_changed(
138    old: &Option<repose_core::ScrollBinding>,
139    new: &Option<repose_core::ScrollBinding>,
140) -> bool {
141    match (old, new) {
142        (
143            Some(repose_core::ScrollBinding::Vertical(old)),
144            Some(repose_core::ScrollBinding::Vertical(new)),
145        ) => scroll_axis_identity_changed(old, new),
146        (
147            Some(repose_core::ScrollBinding::Horizontal(old)),
148            Some(repose_core::ScrollBinding::Horizontal(new)),
149        ) => scroll_axis_identity_changed(old, new),
150        (
151            Some(repose_core::ScrollBinding::Both(old)),
152            Some(repose_core::ScrollBinding::Both(new)),
153        ) => {
154            rc_identity_changed(&old.on_scroll, &new.on_scroll)
155                || rc_identity_changed(&old.set_viewport_width, &new.set_viewport_width)
156                || rc_identity_changed(&old.set_viewport_height, &new.set_viewport_height)
157                || rc_identity_changed(&old.set_content_width, &new.set_content_width)
158                || rc_identity_changed(&old.set_content_height, &new.set_content_height)
159                || rc_identity_changed(&old.get_offset_xy, &new.get_offset_xy)
160                || rc_identity_changed(&old.set_offset_xy, &new.set_offset_xy)
161                || rc_identity_changed(&old.tick, &new.tick)
162                || rc_identity_changed(&old.set_nested_scroll_parent, &new.set_nested_scroll_parent)
163        }
164        (None, None) => false,
165        _ => true,
166    }
167}
168
169fn cursor_identity_changed(
170    old: &Option<repose_core::CursorIcon>,
171    new: &Option<repose_core::CursorIcon>,
172) -> bool {
173    match (old, new) {
174        (
175            Some(repose_core::CursorIcon::Custom(old)),
176            Some(repose_core::CursorIcon::Custom(new)),
177        ) => !std::sync::Arc::ptr_eq(old, new) || old != new,
178        (None, None) => false,
179        _ => true,
180    }
181}
182
183fn text_input_identity_change(
184    old: Option<&repose_core::TextInputConfig>,
185    new: Option<&repose_core::TextInputConfig>,
186) -> IdentityChange {
187    let (Some(old), Some(new)) = (old, new) else {
188        return IdentityChange {
189            paint: old.is_some() != new.is_some(),
190            layout: old.is_some() != new.is_some(),
191            subcompose: false,
192        };
193    };
194    let mut change = IdentityChange::default();
195    if rc_identity_changed(&old.on_change, &new.on_change)
196        || rc_identity_changed(&old.on_submit, &new.on_submit)
197        || rc_identity_changed(&old.on_text_layout, &new.on_text_layout)
198        || rc_identity_changed(&old.focus_tracker, &new.focus_tracker)
199        || interaction_source_changed(&old.interaction_source, &new.interaction_source)
200    {
201        change.paint = true;
202    }
203    if rc_identity_changed(&old.visual_transformation, &new.visual_transformation) {
204        change.paint = true;
205        change.layout = true;
206    }
207    match (&old.keyboard_actions, &new.keyboard_actions) {
208        (Some(old), Some(new)) => {
209            if rc_identity_changed(&old.on_done, &new.on_done)
210                || rc_identity_changed(&old.on_go, &new.on_go)
211                || rc_identity_changed(&old.on_next, &new.on_next)
212                || rc_identity_changed(&old.on_previous, &new.on_previous)
213                || rc_identity_changed(&old.on_search, &new.on_search)
214                || rc_identity_changed(&old.on_send, &new.on_send)
215            {
216                change.paint = true;
217            }
218        }
219        (None, None) => {}
220        _ => change.paint = true,
221    }
222    change
223}
224
225fn modifier_identity_change(old: &Modifier, new: &Modifier) -> IdentityChange {
226    let mut change = IdentityChange::default();
227    macro_rules! paint_changed {
228        ($($field:ident),+ $(,)?) => {
229            if false $(|| rc_identity_changed(&old.$field, &new.$field))+ {
230                change.paint = true;
231            }
232        };
233    }
234    macro_rules! layout_changed {
235        ($($field:ident),+ $(,)?) => {
236            if false $(|| rc_identity_changed(&old.$field, &new.$field))+ {
237                change.layout = true;
238            }
239        };
240    }
241    paint_changed!(
242        on_scroll,
243        on_pointer_down,
244        on_pointer_move,
245        on_pointer_up,
246        on_pointer_cancel,
247        on_pointer_enter,
248        on_pointer_leave,
249        on_click,
250        on_double_click,
251        on_long_click,
252        on_globally_positioned,
253        on_size_changed,
254        on_key_event,
255        on_preview_key_event,
256        on_drag_start,
257        on_drag_end,
258        on_drag_enter,
259        on_drag_over,
260        on_drag_leave,
261        on_drop,
262        on_action,
263        painter,
264        indication,
265        drag_preview,
266        on_focus_changed,
267    );
268    layout_changed!(layout);
269    if arc_identity_changed(&old.paint_callback, &new.paint_callback) {
270        change.paint = true;
271    }
272    if focus_requester_changed(&old.focus_requester, &new.focus_requester)
273        || interaction_source_changed(&old.interaction_source, &new.interaction_source)
274    {
275        change.paint = true;
276    }
277    change.merge(text_input_identity_change(
278        old.text_input.as_ref(),
279        new.text_input.as_ref(),
280    ));
281    match (&old.nested_scroll_connection, &new.nested_scroll_connection) {
282        (Some(old), Some(new)) => {
283            if rc_identity_changed(&old.on_pre_scroll, &new.on_pre_scroll)
284                || rc_identity_changed(&old.on_post_scroll, &new.on_post_scroll)
285                || rc_identity_changed(&old.on_pre_fling, &new.on_pre_fling)
286                || rc_identity_changed(&old.on_post_fling, &new.on_post_fling)
287            {
288                change.paint = true;
289            }
290        }
291        (None, None) => {}
292        _ => change.paint = true,
293    }
294    if scroll_identity_changed(&old.scroll, &new.scroll)
295        || cursor_identity_changed(&old.cursor, &new.cursor)
296        || old.scrollbar_style != new.scrollbar_style
297    {
298        change.paint = true;
299    }
300    change
301}
302
303fn view_identity_change(node: &TreeNode, view: &View) -> IdentityChange {
304    let mut change = modifier_identity_change(&node.modifier, &view.modifier);
305    if let (
306        ViewKind::SubcomposeLayout { content: old },
307        ViewKind::SubcomposeLayout { content: new },
308    ) = (&node.kind, &view.kind)
309        && !Arc::ptr_eq(old, new)
310    {
311        change.paint = true;
312        change.layout = true;
313        change.subcompose = true;
314    }
315    change
316}
317
318fn semantics_equal(
319    old: Option<&repose_core::Semantics>,
320    new: Option<&repose_core::Semantics>,
321) -> bool {
322    match (old, new) {
323        (Some(old), Some(new)) => {
324            old.role == new.role
325                && old.label == new.label
326                && old.focused == new.focused
327                && old.enabled == new.enabled
328                && old.selectable_group == new.selectable_group
329                && old.checked == new.checked
330                && old.selected == new.selected
331                && old.value == new.value
332        }
333        (None, None) => true,
334        _ => false,
335    }
336}
337
338fn view_kind_equal(old: &ViewKind, new: &ViewKind) -> bool {
339    match (old, new) {
340        (
341            ViewKind::SubcomposeLayout { content: old },
342            ViewKind::SubcomposeLayout { content: new },
343        ) => Arc::ptr_eq(old, new),
344        (
345            ViewKind::Text {
346                text: old_text,
347                color: old_color,
348                font_size: old_font_size,
349                soft_wrap: old_soft_wrap,
350                max_lines: old_max_lines,
351                overflow: old_overflow,
352                font_family: old_font_family,
353                annotations: old_annotations,
354                text_align: old_text_align,
355                font_weight: old_font_weight,
356                font_style: old_font_style,
357                text_decoration: old_text_decoration,
358                letter_spacing: old_letter_spacing,
359                line_height: old_line_height,
360                url: old_url,
361                font_variation_settings: old_font_variation_settings,
362                draw_style: old_draw_style,
363            },
364            ViewKind::Text {
365                text: new_text,
366                color: new_color,
367                font_size: new_font_size,
368                soft_wrap: new_soft_wrap,
369                max_lines: new_max_lines,
370                overflow: new_overflow,
371                font_family: new_font_family,
372                annotations: new_annotations,
373                text_align: new_text_align,
374                font_weight: new_font_weight,
375                font_style: new_font_style,
376                text_decoration: new_text_decoration,
377                letter_spacing: new_letter_spacing,
378                line_height: new_line_height,
379                url: new_url,
380                font_variation_settings: new_font_variation_settings,
381                draw_style: new_draw_style,
382            },
383        ) => {
384            old_text == new_text
385                && old_color == new_color
386                && old_font_size == new_font_size
387                && old_soft_wrap == new_soft_wrap
388                && old_max_lines == new_max_lines
389                && old_overflow == new_overflow
390                && old_font_family == new_font_family
391                && old_annotations == new_annotations
392                && old_text_align == new_text_align
393                && old_font_weight == new_font_weight
394                && old_font_style == new_font_style
395                && old_text_decoration == new_text_decoration
396                && old_letter_spacing == new_letter_spacing
397                && old_line_height == new_line_height
398                && old_url == new_url
399                && old_font_variation_settings == new_font_variation_settings
400                && old_draw_style == new_draw_style
401        }
402        (
403            ViewKind::Image {
404                handle: old_handle,
405                tint: old_tint,
406                fit: old_fit,
407                filter: old_filter,
408                source_rect: old_source_rect,
409                alignment: old_alignment,
410            },
411            ViewKind::Image {
412                handle: new_handle,
413                tint: new_tint,
414                fit: new_fit,
415                filter: new_filter,
416                source_rect: new_source_rect,
417                alignment: new_alignment,
418            },
419        ) => {
420            old_handle == new_handle
421                && old_tint == new_tint
422                && old_fit == new_fit
423                && old_filter == new_filter
424                && old_source_rect == new_source_rect
425                && old_alignment == new_alignment
426        }
427        _ => std::mem::discriminant(old) == std::mem::discriminant(new),
428    }
429}
430
431fn next_dedup_key(
432    original: u64,
433    index: usize,
434    parent_id: NodeId,
435    new_seen_keys: &mut FxHashSet<u64>,
436    keyed_children: &FxHashMap<u64, NodeId>,
437) -> u64 {
438    let mut candidate = (original.wrapping_mul(0x9E3779B97F4A7C15)
439        ^ (index as u64).wrapping_add(0xBF58476D1CE4E5B9))
440    .wrapping_add(parent_id.data().as_ffi());
441    loop {
442        if candidate != original
443            && !new_seen_keys.contains(&candidate)
444            && !keyed_children.contains_key(&candidate)
445        {
446            new_seen_keys.insert(candidate);
447            return candidate;
448        }
449        candidate = candidate.wrapping_add(1);
450        if candidate == 0 {
451            candidate = 1;
452        }
453    }
454}
455
456impl ViewTree {
457    /// Create a new empty tree.
458    pub fn new() -> Self {
459        Self {
460            nodes: SlotMap::with_key(),
461            root: None,
462            dirty: FxHashSet::default(),
463            dirty_propagated: FxHashSet::default(),
464            paint_dirty: FxHashSet::default(),
465            paint_dirty_propagated: FxHashSet::default(),
466            generation: 0,
467            view_id_map: FxHashMap::default(),
468            duplicate_view_ids: FxHashSet::default(),
469            stats: TreeStats::default(),
470            removed_ids: Vec::new(),
471            pending_removals: Vec::new(),
472            removal_seen: FxHashSet::default(),
473            subcompose_scope: SubcomposeScope::UNBOUNDED,
474            subcompose_cache: FxHashMap::default(),
475            subcompose_runs: FxHashMap::default(),
476            scope_invalidation_seen: FxHashSet::default(),
477        }
478    }
479
480    /// Set the constraints that will be passed to any `SubcomposeLayout`
481    /// content closures during the next [`update`](Self::update) call.
482    /// The scope is read once per reconcile of a `SubcomposeLayout` node. If
483    /// you need different scopes at different depths, the closure itself is
484    /// responsible for narrowing the values it receives.
485    pub fn set_subcompose_scope(&mut self, scope: SubcomposeScope) {
486        self.subcompose_scope = SubcomposeScope::new(
487            scope.min_width,
488            scope.max_width,
489            scope.min_height,
490            scope.max_height,
491        );
492    }
493
494    /// Get the currently-set subcompose scope.
495    pub fn subcompose_scope(&self) -> SubcomposeScope {
496        self.subcompose_scope
497    }
498
499    /// Run a `SubcomposeLayout`'s content closure, returning the cached views
500    /// when the scope is unchanged for this node.
501    /// The caller is responsible for ensuring the cache is invalidated (e.g.
502    /// on content change) via [`ViewTree::invalidate_subcompose_cache`].
503    ///
504    /// The scope is computed by walking the node's ancestor chain and
505    /// intersecting the root scope with each ancestor's `Modifier` width /
506    /// height / min / max fields. The SubcomposeLayout's own modifier is
507    /// included as the last intersection.
508    fn run_subcompose(
509        &mut self,
510        node_id: NodeId,
511        content: &Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
512    ) -> Arc<Vec<View>> {
513        let raw_scope = self.compute_scope_for_node(node_id);
514        let scope = SubcomposeScope::new(
515            raw_scope.min_width,
516            raw_scope.max_width,
517            raw_scope.min_height,
518            raw_scope.max_height,
519        );
520        let cached = self
521            .subcompose_cache
522            .get(&node_id)
523            .map(|(cached_scope, cached_slots)| (*cached_scope, Arc::clone(cached_slots)));
524        if let Some((cached_scope, cached_slots)) = cached {
525            let previous_scope = self
526                .subcompose_runs
527                .get(&node_id)
528                .map(|(_, _, previous_scope)| *previous_scope);
529            let scope_changed =
530                previous_scope.is_some_and(|previous_scope| previous_scope != scope);
531            if cached_scope == scope && !scope_changed {
532                self.subcompose_runs.remove(&node_id);
533                return cached_slots;
534            }
535            let cur_gen = self.generation;
536            let streak = match self.subcompose_runs.get(&node_id) {
537                Some((last_gen, count, _)) if *last_gen == cur_gen => count.saturating_add(1),
538                Some((last_gen, count, _)) if last_gen.wrapping_add(1) == cur_gen => {
539                    count.saturating_add(1)
540                }
541                _ => 1,
542            };
543            self.subcompose_runs
544                .insert(node_id, (cur_gen, streak, scope));
545            if streak >= 4 {
546                if streak == 4 {
547                    log::warn!(
548                        "SubcomposeLayout {:?} oscillating; holding cached result",
549                        node_id
550                    );
551                }
552                return cached_slots;
553            }
554            if cached_scope == scope {
555                return cached_slots;
556            }
557        } else {
558            self.subcompose_runs.remove(&node_id);
559        }
560        let slots = content(&scope);
561        let scope_key = format!("subcompose_{:?}", node_id);
562        let mut views = Vec::with_capacity(slots.len());
563        for (slot_id, mut view) in slots {
564            view.modifier.key = Some(slot_id);
565            view.scope_key = Some(scope_key.clone());
566            view.modifier.repaint_boundary = true;
567            views.push(view);
568        }
569        let views = Arc::new(views);
570        let old_cache = self
571            .subcompose_cache
572            .insert(node_id, (scope, Arc::clone(&views)));
573        drop(old_cache);
574        views
575    }
576
577    /// Compute the `SubcomposeScope` visible to a `SubcomposeLayout` at
578    /// `node_id`. Starts with the user-set root scope and intersects each
579    /// ancestor's `Modifier` width / height / min / max / padding fields in
580    /// root-to-leaf order. Then narrows using the SubcomposeLayout's own
581    /// cached Taffy-computed size (from the previous frame's layout pass)
582    /// if available, so `fill_max_width` / `fill_max_height` and other
583    /// parent-dependent sizes are reflected in the scope after the first frame.
584    fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
585        let mut scope = self.subcompose_scope;
586        let mut chain: SmallVec<[NodeId; 16]> = SmallVec::new();
587        let mut current = Some(node_id);
588        while let Some(id) = current {
589            chain.push(id);
590            match self.nodes.get(id) {
591                Some(node) => current = node.parent,
592                None => break,
593            }
594        }
595        chain.reverse();
596        for ancestor_id in chain {
597            if let Some(node) = self.nodes.get(ancestor_id) {
598                scope = intersect_scope_with_modifier(scope, &node.modifier);
599                if let Some(cache) = &node.layout_cache {
600                    let w = cache.rect.w;
601                    if w >= 0.0 && w.is_finite() {
602                        scope.max_width = scope.max_width.min(repose_core::Dp(w));
603                    }
604                    let h = cache.rect.h;
605                    if h >= 0.0 && h.is_finite() {
606                        scope.max_height = scope.max_height.min(repose_core::Dp(h));
607                    }
608                }
609            }
610        }
611        scope
612    }
613
614    /// Drop the cached subcomposed view for a single node. Call this when the
615    /// `SubcomposeLayout`'s modifier or identity changes so the next
616    /// reconciliation re-invokes the closure.
617    pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
618        let removed = self.subcompose_cache.remove(&node_id);
619        drop(removed);
620        self.subcompose_runs.remove(&node_id);
621    }
622
623    /// Get the current generation.
624    pub fn generation(&self) -> u64 {
625        self.generation
626    }
627
628    /// Get the root node ID.
629    pub fn root(&self) -> Option<NodeId> {
630        self.root
631    }
632
633    /// Get a node by ID.
634    pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
635        self.nodes.get(id)
636    }
637
638    /// Get a mutable node by ID.
639    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
640        self.nodes.get_mut(id)
641    }
642
643    /// Get a node by ViewId.
644    pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
645        self.view_id_map
646            .get(&view_id)
647            .and_then(|id| self.nodes.get(*id))
648    }
649
650    /// Get the number of nodes in the tree.
651    pub fn len(&self) -> usize {
652        self.nodes.len()
653    }
654
655    /// Check if the tree is empty.
656    pub fn is_empty(&self) -> bool {
657        self.nodes.is_empty()
658    }
659
660    /// Check if a node is marked dirty.
661    pub fn is_dirty(&self, id: NodeId) -> bool {
662        self.dirty.contains(&id)
663    }
664
665    /// Get the set of dirty nodes.
666    pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
667        &self.dirty
668    }
669
670    /// Clear the dirty set (after layout).
671    pub fn clear_dirty(&mut self) {
672        self.dirty.clear();
673        self.dirty_propagated.clear();
674    }
675
676    pub fn paint_dirty_nodes(&self) -> &FxHashSet<NodeId> {
677        &self.paint_dirty
678    }
679
680    pub fn clear_paint_dirty(&mut self) {
681        self.paint_dirty.clear();
682        self.paint_dirty_propagated.clear();
683    }
684
685    fn mark_paint_dirty(&mut self, node_id: NodeId) {
686        let mut current = Some(node_id);
687        while let Some(id) = current {
688            if !self.paint_dirty_propagated.insert(id) {
689                break;
690            }
691            self.paint_dirty.insert(id);
692            current = self.nodes.get(id).and_then(|node| node.parent);
693        }
694    }
695
696    fn mark_scope_descendants_dirty(&mut self, id: NodeId) {
697        if !self.scope_invalidation_seen.insert(id) {
698            return;
699        }
700        let mut stack: SmallVec<[NodeId; 16]> = SmallVec::new();
701        stack.push(id);
702        while let Some(current) = stack.pop() {
703            let Some(node) = self.nodes.get(current) else {
704                continue;
705            };
706            for &child in &node.children {
707                if self
708                    .nodes
709                    .get(child)
710                    .is_some_and(|child| child.scope_key.is_some())
711                {
712                    self.dirty.insert(child);
713                }
714                stack.push(child);
715            }
716        }
717    }
718
719    /// Mark a node as needing re-layout.
720    pub fn mark_dirty(&mut self, id: NodeId) {
721        let mut current = Some(id);
722        while let Some(current_id) = current {
723            if !self.dirty_propagated.insert(current_id) {
724                break;
725            }
726            self.dirty.insert(current_id);
727            current = self.nodes.get(current_id).and_then(|node| node.parent);
728        }
729
730        let needs_scope_invalidate = self.nodes.get(id).is_some_and(|node| {
731            node.modifier.transform.is_some()
732                || node.modifier.alpha.is_some()
733                || node.modifier.graphics_layer.is_some()
734        });
735        if needs_scope_invalidate {
736            self.mark_scope_descendants_dirty(id);
737        }
738    }
739
740    /// Update the tree from a new View, performing incremental reconciliation.
741    /// Returns the root NodeId.
742    pub fn update(&mut self, new_root: &View) -> NodeId {
743        self.removed_ids.clear();
744        self.pending_removals.clear();
745        self.removal_seen.clear();
746        self.paint_dirty.clear();
747        self.paint_dirty_propagated.clear();
748        self.dirty_propagated.clear();
749        self.scope_invalidation_seen.clear();
750
751        self.generation += 1;
752        self.stats = TreeStats::default();
753
754        let mut ctx = ReconcileContext::new(self.generation);
755
756        let root_id = if let Some(existing_root) = self.root {
757            self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
758        } else {
759            self.create_node(new_root, None, 0, 0, &mut ctx)
760        };
761
762        self.root = Some(root_id);
763        self.collect_explicit_removals(&mut ctx);
764
765        self.stats.total_nodes = self.nodes.len();
766        self.stats.dirty_nodes = self.dirty.len();
767        self.stats.reconciled_nodes = ctx.reconciled;
768        self.stats.skipped_nodes = ctx.skipped;
769        self.stats.created_nodes = ctx.created;
770        self.stats.removed_nodes = ctx.removed;
771
772        root_id
773    }
774
775    /// Reconcile an existing node with a new View.
776    fn reconcile_node(
777        &mut self,
778        node_id: NodeId,
779        view: &View,
780        parent: Option<NodeId>,
781        depth: u32,
782        index_in_parent: u32,
783        ctx: &mut ReconcileContext,
784    ) -> NodeId {
785        let hashes = hash_view_facets(view);
786        let Some(node) = self.nodes.get(node_id) else {
787            log::error!(
788                "reconcile_node: node {:?} not found - creating fresh",
789                node_id
790            );
791            return self.create_node(view, parent, depth, index_in_parent, ctx);
792        };
793
794        let old_hashes = ViewHashes {
795            layout: node.layout_hash,
796            measurement: node.measurement_hash,
797            paint: node.paint_hash,
798            semantics: node.semantics_hash,
799        };
800        let identity = view_identity_change(node, view);
801        let old_subtree = SubtreeHashes {
802            layout: node.layout_subtree_hash,
803            measurement: node.measurement_subtree_hash,
804            paint: node.paint_subtree_hash,
805            semantics: node.semantics_subtree_hash,
806        };
807        let old_subtree_hash = node.subtree_hash;
808        let old_scope_key = node.scope_key.clone();
809        let old_view_id = node.view_id;
810        let old_is_subcompose = matches!(&node.kind, ViewKind::SubcomposeLayout { .. });
811        let new_is_subcompose = matches!(&view.kind, ViewKind::SubcomposeLayout { .. });
812        let hashes_equal = old_hashes == hashes;
813        let content_hash = hashes.combined();
814        let content_changed = !hashes_equal;
815        let view_semantics_changed =
816            !hashes_equal && !semantics_equal(node.semantics.as_ref(), view.semantics.as_ref());
817        let modifier_semantics_changed = !hashes_equal
818            && !semantics_equal(
819                node.modifier.semantics.as_ref(),
820                view.modifier.semantics.as_ref(),
821            );
822        let scope_effect_changed = node.modifier.transform != view.modifier.transform
823            || node.modifier.alpha != view.modifier.alpha
824            || node.modifier.graphics_layer != view.modifier.graphics_layer;
825        let kind_changed = !view_kind_equal(&node.kind, &view.kind);
826        let modifier_changed = old_hashes.layout != hashes.layout
827            || old_hashes.measurement != hashes.measurement
828            || old_hashes.paint != hashes.paint
829            || modifier_semantics_changed
830            || identity.paint
831            || identity.layout;
832        let scope_changed = old_scope_key.as_deref() != view.scope_key.as_deref();
833        let node_layout_changed = old_hashes.layout != hashes.layout
834            || old_hashes.measurement != hashes.measurement
835            || identity.layout
836            || identity.subcompose
837            || old_is_subcompose != new_is_subcompose;
838        let subcompose_changed = identity.subcompose
839            || (new_is_subcompose
840                && (old_hashes.layout != hashes.layout
841                    || old_hashes.measurement != hashes.measurement
842                    || identity.layout))
843            || old_is_subcompose != new_is_subcompose;
844        if subcompose_changed {
845            self.invalidate_subcompose_cache(node_id);
846        }
847
848        let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
849        self.assign_view_id(
850            node_id,
851            old_view_id,
852            view_id,
853            view.id == 0,
854            "reconcile_node",
855        );
856
857        {
858            let Some(node) = self.nodes.get_mut(node_id) else {
859                log::error!("reconcile_node: node {:?} vanished mid-reconcile", node_id);
860                return node_id;
861            };
862            node.parent = parent;
863            node.depth = depth;
864            node.generation = self.generation;
865            if kind_changed {
866                node.kind = view.kind.clone();
867            }
868            if modifier_changed {
869                node.modifier = view.modifier.clone();
870            }
871            if view_semantics_changed {
872                node.semantics = view.semantics.clone();
873            }
874            if scope_changed {
875                node.scope_key = view.scope_key.clone();
876            }
877            node.user_key = view.modifier.key;
878            if node_layout_changed {
879                node.layout_cache = None;
880            }
881        }
882
883        let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
884            let subcomposed = self.run_subcompose(node_id, content);
885            self.reconcile_children(node_id, subcomposed.as_slice(), depth, ctx)
886        } else {
887            self.reconcile_children(node_id, &view.children, depth, ctx)
888        };
889        let new_subtree = new_children_hashes.finish(hashes);
890        let new_subtree_hash = new_subtree.combined();
891        let layout_subtree_changed = old_subtree.layout != new_subtree.layout
892            || old_subtree.measurement != new_subtree.measurement
893            || identity.layout
894            || identity.subcompose;
895        let paint_subtree_changed =
896            old_subtree.paint != new_subtree.paint || identity.paint || identity.subcompose;
897        let semantics_subtree_changed = old_subtree.semantics != new_subtree.semantics;
898        let subtree_changed = old_subtree_hash != new_subtree_hash || identity.subcompose;
899
900        {
901            let Some(node) = self.nodes.get_mut(node_id) else {
902                log::error!("reconcile_node: node {:?} vanished mid-reconcile", node_id);
903                return node_id;
904            };
905            if layout_subtree_changed {
906                node.layout_cache = None;
907            }
908            node.content_hash = content_hash;
909            node.layout_hash = hashes.layout;
910            node.measurement_hash = hashes.measurement;
911            node.paint_hash = hashes.paint;
912            node.semantics_hash = hashes.semantics;
913            node.subtree_hash = new_subtree_hash;
914            node.layout_subtree_hash = new_subtree.layout;
915            node.measurement_subtree_hash = new_subtree.measurement;
916            node.paint_subtree_hash = new_subtree.paint;
917            node.semantics_subtree_hash = new_subtree.semantics;
918
919            if content_changed || identity.paint || identity.layout || identity.subcompose {
920                ctx.reconciled += 1;
921            } else if !subtree_changed {
922                ctx.skipped += 1;
923            }
924        }
925
926        if paint_subtree_changed || semantics_subtree_changed {
927            self.mark_paint_dirty(node_id);
928        }
929        if layout_subtree_changed {
930            self.mark_dirty(node_id);
931        }
932        if scope_effect_changed {
933            self.mark_scope_descendants_dirty(node_id);
934        }
935
936        node_id
937    }
938    fn node_subtree_hashes(&self, node_id: NodeId) -> SubtreeHashes {
939        self.nodes
940            .get(node_id)
941            .map_or_default(|node| SubtreeHashes {
942                layout: node.layout_subtree_hash,
943                measurement: node.measurement_subtree_hash,
944                paint: node.paint_subtree_hash,
945                semantics: node.semantics_subtree_hash,
946            })
947    }
948
949    /// Reconcile children of a node.
950    /// Returns the subtree hashes of all children (for computing parent's subtree hash).
951    fn reconcile_children(
952        &mut self,
953        parent_id: NodeId,
954        new_children: &[View],
955        parent_depth: u32,
956        ctx: &mut ReconcileContext,
957    ) -> SubtreeHashBuilder {
958        let child_depth = parent_depth + 1;
959        let old_children: SmallVec<[NodeId; 4]> = self
960            .nodes
961            .get_mut(parent_id)
962            .map(|node| std::mem::take(&mut node.children))
963            .unwrap_or_default();
964
965        let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
966        let mut unkeyed_children: SmallVec<[NodeId; 4]> = SmallVec::new();
967        for &child_id in &old_children {
968            if let Some(node) = self.nodes.get(child_id) {
969                match node.user_key {
970                    Some(key) => {
971                        keyed_children.entry(key).or_insert(child_id);
972                    }
973                    None => unkeyed_children.push(child_id),
974                }
975            }
976        }
977
978        let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
979        let mut child_hashes = SubtreeHashBuilder::new(new_children.len());
980        let mut unkeyed_index = 0;
981        let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
982        let mut new_seen_keys: FxHashSet<u64> = FxHashSet::default();
983
984        for (i, new_child) in new_children.iter().enumerate() {
985            let mut owned_child: Option<View>;
986            let child_ref = if let Some(key) = new_child.modifier.key {
987                if new_seen_keys.insert(key) {
988                    new_child
989                } else {
990                    log::error!(
991                        "reconcile_children: duplicate modifier.key={} in children of node {:?} - deduplicating (suffixing). Ensure get_key returns unique keys.",
992                        key,
993                        parent_id
994                    );
995                    let salt =
996                        next_dedup_key(key, i, parent_id, &mut new_seen_keys, &keyed_children);
997                    owned_child = Some(new_child.clone());
998                    let child = owned_child.as_mut().expect("owned child inserted");
999                    child.modifier.key = Some(salt);
1000                    child
1001                }
1002            } else {
1003                new_child
1004            };
1005
1006            let is_subcompose = matches!(&child_ref.kind, ViewKind::SubcomposeLayout { .. });
1007            let child_id = if let Some(key) = child_ref.modifier.key {
1008                if let Some(&existing_id) = keyed_children.get(&key) {
1009                    if used_nodes.insert(existing_id) {
1010                        self.reconcile_node(
1011                            existing_id,
1012                            child_ref,
1013                            Some(parent_id),
1014                            child_depth,
1015                            i as u32,
1016                            ctx,
1017                        )
1018                    } else {
1019                        let salt =
1020                            next_dedup_key(key, i, parent_id, &mut new_seen_keys, &keyed_children);
1021                        let mut fresh = child_ref.clone();
1022                        fresh.modifier.key = Some(salt);
1023                        self.create_node(&fresh, Some(parent_id), child_depth, i as u32, ctx)
1024                    }
1025                } else if is_subcompose && unkeyed_index < unkeyed_children.len() {
1026                    let existing_id = unkeyed_children[unkeyed_index];
1027                    unkeyed_index += 1;
1028                    if used_nodes.insert(existing_id) {
1029                        self.reconcile_node(
1030                            existing_id,
1031                            child_ref,
1032                            Some(parent_id),
1033                            child_depth,
1034                            i as u32,
1035                            ctx,
1036                        )
1037                    } else {
1038                        self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1039                    }
1040                } else {
1041                    self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1042                }
1043            } else if let Some(&existing_id) = unkeyed_children.get(unkeyed_index) {
1044                unkeyed_index += 1;
1045                if used_nodes.insert(existing_id) {
1046                    self.reconcile_node(
1047                        existing_id,
1048                        child_ref,
1049                        Some(parent_id),
1050                        child_depth,
1051                        i as u32,
1052                        ctx,
1053                    )
1054                } else {
1055                    self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1056                }
1057            } else {
1058                self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1059            };
1060
1061            new_child_ids.push(child_id);
1062            child_hashes.push(self.node_subtree_hashes(child_id));
1063        }
1064
1065        for &old_child in &old_children {
1066            if !used_nodes.contains(&old_child) {
1067                self.mark_for_removal(old_child);
1068            }
1069        }
1070
1071        if let Some(parent) = self.nodes.get_mut(parent_id) {
1072            parent.children = new_child_ids;
1073        }
1074
1075        child_hashes
1076    }
1077
1078    /// Create a new node from a View.
1079    fn create_node(
1080        &mut self,
1081        view: &View,
1082        parent: Option<NodeId>,
1083        depth: u32,
1084        index_in_parent: u32,
1085        ctx: &mut ReconcileContext,
1086    ) -> NodeId {
1087        let hashes = hash_view_facets(view);
1088        let content_hash = hashes.combined();
1089
1090        let node_id = self.nodes.insert_with_key(|id| {
1091            TreeNode::new(
1092                id,
1093                0,
1094                view.kind.clone(),
1095                view.modifier.clone(),
1096                self.generation,
1097            )
1098        });
1099        ctx.created += 1;
1100
1101        {
1102            let node = self
1103                .nodes
1104                .get_mut(node_id)
1105                .expect("create_node: node just inserted");
1106            node.parent = parent;
1107            node.depth = depth;
1108            node.semantics = view.semantics.clone();
1109            node.content_hash = content_hash;
1110            node.layout_hash = hashes.layout;
1111            node.measurement_hash = hashes.measurement;
1112            node.paint_hash = hashes.paint;
1113            node.semantics_hash = hashes.semantics;
1114            node.user_key = view.modifier.key;
1115            node.scope_key = view.scope_key.clone();
1116        }
1117
1118        let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
1119        self.assign_view_id(node_id, 0, view_id, view.id == 0, "create_node");
1120
1121        let child_depth = depth + 1;
1122        let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
1123        let subcomposed = if let ViewKind::SubcomposeLayout { content } = &view.kind {
1124            Some(self.run_subcompose(node_id, content))
1125        } else {
1126            None
1127        };
1128        let children_to_create = subcomposed
1129            .as_ref()
1130            .map_or(view.children.as_slice(), |views| views.as_slice());
1131        let mut child_hashes = SubtreeHashBuilder::new(children_to_create.len());
1132        for (i, child_view) in children_to_create.iter().enumerate() {
1133            let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
1134            child_ids.push(child_id);
1135            child_hashes.push(self.node_subtree_hashes(child_id));
1136        }
1137
1138        let subtree = child_hashes.finish(hashes);
1139        let subtree_hash = subtree.combined();
1140
1141        let node = self
1142            .nodes
1143            .get_mut(node_id)
1144            .expect("create_node: node just inserted");
1145        node.children = child_ids;
1146        node.subtree_hash = subtree_hash;
1147        node.layout_subtree_hash = subtree.layout;
1148        node.measurement_subtree_hash = subtree.measurement;
1149        node.paint_subtree_hash = subtree.paint;
1150        node.semantics_subtree_hash = subtree.semantics;
1151        self.dirty.insert(node_id);
1152
1153        node_id
1154    }
1155    /// Compute a stable ViewId for a node.
1156    fn compute_view_id(
1157        &self,
1158        view: &View,
1159        _node_id: NodeId,
1160        parent: Option<NodeId>,
1161        index_in_parent: u32,
1162    ) -> ViewId {
1163        if view.id != 0 {
1164            return view.id;
1165        }
1166
1167        let parent_id = parent
1168            .and_then(|p| self.nodes.get(p))
1169            .map(|n| n.view_id)
1170            .unwrap_or(0);
1171
1172        let salt = match view.modifier.key {
1173            Some(key) => key.wrapping_mul(2).wrapping_add(1),
1174            None => (index_in_parent as u64).wrapping_mul(2),
1175        };
1176
1177        let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
1178        id = id.wrapping_mul(0x9E3779B97F4A7C15);
1179        id ^= id >> 30;
1180
1181        if id == 0 {
1182            id = 1;
1183        }
1184
1185        id
1186    }
1187
1188    fn repair_view_id(&mut self, view_id: ViewId) {
1189        let previous = self.view_id_map.get(&view_id).copied();
1190        let mut owner = None;
1191        let mut count = 0usize;
1192        for (node_id, node) in self.nodes.iter() {
1193            if node.generation != 0 && node.view_id == view_id {
1194                count += 1;
1195                if owner.is_none() {
1196                    owner = Some(node_id);
1197                }
1198            }
1199        }
1200        let owner = previous
1201            .filter(|node_id| {
1202                self.nodes
1203                    .get(*node_id)
1204                    .is_some_and(|node| node.generation != 0 && node.view_id == view_id)
1205            })
1206            .or(owner);
1207        if let Some(owner) = owner {
1208            self.view_id_map.insert(view_id, owner);
1209        } else {
1210            self.view_id_map.remove(&view_id);
1211        }
1212        if count > 1 {
1213            self.duplicate_view_ids.insert(view_id);
1214        } else {
1215            self.duplicate_view_ids.remove(&view_id);
1216        }
1217    }
1218
1219    fn rebuild_view_id_map(&mut self) {
1220        let previous = std::mem::take(&mut self.view_id_map);
1221        self.duplicate_view_ids.clear();
1222        let mut counts: FxHashMap<ViewId, (NodeId, usize)> = FxHashMap::default();
1223        for (node_id, node) in self.nodes.iter() {
1224            if node.generation == 0 {
1225                continue;
1226            }
1227            let entry = counts.entry(node.view_id).or_insert((node_id, 0));
1228            entry.1 += 1;
1229        }
1230        for (view_id, (first_node, count)) in counts {
1231            let owner = previous
1232                .get(&view_id)
1233                .copied()
1234                .filter(|node_id| {
1235                    self.nodes
1236                        .get(*node_id)
1237                        .is_some_and(|node| node.generation != 0 && node.view_id == view_id)
1238                })
1239                .unwrap_or(first_node);
1240            self.view_id_map.insert(view_id, owner);
1241            if count > 1 {
1242                self.duplicate_view_ids.insert(view_id);
1243            }
1244        }
1245    }
1246
1247    fn assign_view_id(
1248        &mut self,
1249        node_id: NodeId,
1250        old_view_id: ViewId,
1251        requested_view_id: ViewId,
1252        generated: bool,
1253        phase: &str,
1254    ) {
1255        let view_id = if generated {
1256            self.available_generated_view_id(node_id, requested_view_id)
1257        } else {
1258            requested_view_id
1259        };
1260
1261        let old_owner =
1262            old_view_id != 0 && self.view_id_map.get(&old_view_id).copied() == Some(node_id);
1263        if old_owner {
1264            self.view_id_map.remove(&old_view_id);
1265        }
1266
1267        if let Some(existing) = self.view_id_map.get(&view_id).copied()
1268            && existing != node_id
1269        {
1270            self.duplicate_view_ids.insert(view_id);
1271            log::error!(
1272                "{}: duplicate View.id {}; keeping the first node {:?}",
1273                phase,
1274                view_id,
1275                existing
1276            );
1277        } else {
1278            self.view_id_map.insert(view_id, node_id);
1279        }
1280
1281        if let Some(node) = self.nodes.get_mut(node_id) {
1282            node.view_id = view_id;
1283        }
1284        if old_owner {
1285            self.repair_view_id(old_view_id);
1286        }
1287    }
1288
1289    fn available_generated_view_id(&self, node_id: NodeId, requested: ViewId) -> ViewId {
1290        if self
1291            .view_id_map
1292            .get(&requested)
1293            .map_or(true, |existing| *existing == node_id)
1294        {
1295            return requested;
1296        }
1297
1298        let mut candidate = requested;
1299        loop {
1300            candidate = candidate.wrapping_add(1);
1301            if candidate == 0 {
1302                candidate = 1;
1303            }
1304            if self
1305                .view_id_map
1306                .get(&candidate)
1307                .map_or(true, |existing| *existing == node_id)
1308            {
1309                return candidate;
1310            }
1311        }
1312    }
1313
1314    /// Mark a node and its descendants for removal.
1315    fn mark_for_removal(&mut self, node_id: NodeId) {
1316        if !self.removal_seen.insert(node_id) {
1317            return;
1318        }
1319        let Some((view_id, children)) = self.nodes.get_mut(node_id).map(|node| {
1320            node.generation = 0;
1321            let children = std::mem::take(&mut node.children);
1322            (node.view_id, children)
1323        }) else {
1324            return;
1325        };
1326        self.pending_removals.push(node_id);
1327        if self.view_id_map.get(&view_id).copied() == Some(node_id) {
1328            self.view_id_map.remove(&view_id);
1329            self.repair_view_id(view_id);
1330        }
1331        self.dirty.remove(&node_id);
1332        self.dirty_propagated.remove(&node_id);
1333        self.paint_dirty.remove(&node_id);
1334        self.paint_dirty_propagated.remove(&node_id);
1335        self.scope_invalidation_seen.remove(&node_id);
1336        let removed_cache = self.subcompose_cache.remove(&node_id);
1337        drop(removed_cache);
1338        self.subcompose_runs.remove(&node_id);
1339        for child_id in children {
1340            self.mark_for_removal(child_id);
1341        }
1342    }
1343
1344    fn collect_explicit_removals(&mut self, ctx: &mut ReconcileContext) {
1345        let pending = std::mem::take(&mut self.pending_removals);
1346        for node_id in pending {
1347            if self.nodes.remove(node_id).is_none() {
1348                continue;
1349            }
1350            ctx.removed += 1;
1351            self.removed_ids.push(node_id);
1352        }
1353        self.removal_seen.clear();
1354        self.rebuild_view_id_map();
1355    }
1356
1357    /// Set cached layout for a node.
1358    pub fn set_layout(
1359        &mut self,
1360        id: NodeId,
1361        rect: Rect,
1362        screen_rect: Rect,
1363        constraints: LayoutConstraints,
1364    ) {
1365        if let Some(node) = self.nodes.get_mut(id) {
1366            node.layout_cache = Some(LayoutCache {
1367                rect,
1368                screen_rect,
1369                constraints,
1370                generation: self.generation,
1371            });
1372        }
1373    }
1374
1375    /// Iterate over all nodes (parent before children).
1376    pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
1377        self.nodes.values()
1378    }
1379
1380    /// Iterate over all nodes with their IDs.
1381    pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
1382        self.nodes.iter()
1383    }
1384
1385    /// Walk the tree from root, calling `f` for each node.
1386    /// Returns early if `f` returns false.
1387    pub fn walk<F>(&self, mut f: F)
1388    where
1389        F: FnMut(&TreeNode, u32) -> bool,
1390    {
1391        if let Some(root_id) = self.root {
1392            self.walk_node(root_id, 0, &mut f);
1393        }
1394    }
1395
1396    fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
1397    where
1398        F: FnMut(&TreeNode, u32) -> bool,
1399    {
1400        if let Some(node) = self.nodes.get(id) {
1401            if !f(node, depth) {
1402                return;
1403            }
1404
1405            for &child_id in &node.children {
1406                self.walk_node(child_id, depth + 1, f);
1407            }
1408        }
1409    }
1410
1411    /// Get children of a node.
1412    pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
1413        self.nodes.get(id).map(|n| n.children.as_slice())
1414    }
1415}
1416
1417/// Intersect a `SubcomposeScope` with a `Modifier`'s size-related fields.
1418/// `Modifier::width` / `Modifier::height` are treated as exact sizes (the
1419/// resulting min and max both equal that value). Padding values reduce the
1420/// available content area (matching Compose semantics where padding offsets
1421/// the child constraints). `fill_max_w` / `fill_max_h` are layout-direction
1422/// hints resolved by Taffy and cannot produce a concrete size here. They
1423/// are handled by storing the Taffy-computed size after layout.
1424fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
1425    let mut s = scope;
1426    // `size()` sets both min and max to the same value (exact size).
1427    if let Some(sz) = modifier.size {
1428        s.min_width = s.min_width.max(sz.width);
1429        s.max_width = s.max_width.min(sz.width);
1430        s.min_height = s.min_height.max(sz.height);
1431        s.max_height = s.max_height.min(sz.height);
1432    }
1433    if let Some(w) = modifier.width {
1434        s.min_width = s.min_width.max(w);
1435        s.max_width = s.max_width.min(w);
1436    }
1437    if let Some(h) = modifier.height {
1438        s.min_height = s.min_height.max(h);
1439        s.max_height = s.max_height.min(h);
1440    }
1441    if let Some(mw) = modifier.min_width {
1442        s.min_width = s.min_width.max(mw);
1443    }
1444    if let Some(mh) = modifier.min_height {
1445        s.min_height = s.min_height.max(mh);
1446    }
1447    if let Some(mw) = modifier.max_width {
1448        s.max_width = s.max_width.min(mw);
1449    }
1450    if let Some(mh) = modifier.max_height {
1451        s.max_height = s.max_height.min(mh);
1452    }
1453    // Padding reduces the available content area (Compose semantics:
1454    // constraints are offset by the padding amount).
1455    if let Some(p) = modifier.padding {
1456        let total = p * 2.0;
1457        s.min_width = (s.min_width - total).max(repose_core::Dp::ZERO);
1458        s.max_width = (s.max_width - total).max(repose_core::Dp::ZERO);
1459        s.min_height = (s.min_height - total).max(repose_core::Dp::ZERO);
1460        s.max_height = (s.max_height - total).max(repose_core::Dp::ZERO);
1461    }
1462    if let Some(pv) = modifier.padding_values {
1463        let h_total = pv.left + pv.right;
1464        let v_total = pv.top + pv.bottom;
1465        s.min_width = (s.min_width - h_total).max(repose_core::Dp::ZERO);
1466        s.max_width = (s.max_width - h_total).max(repose_core::Dp::ZERO);
1467        s.min_height = (s.min_height - v_total).max(repose_core::Dp::ZERO);
1468        s.max_height = (s.max_height - v_total).max(repose_core::Dp::ZERO);
1469    }
1470    if let Some(required) = modifier.required_size {
1471        s.min_width = required.width.max(repose_core::Dp::ZERO);
1472        s.max_width = s.min_width;
1473        s.min_height = required.height.max(repose_core::Dp::ZERO);
1474        s.max_height = s.min_height;
1475    }
1476    if let Some(value) = modifier.required_min_width {
1477        s.min_width = value.max(repose_core::Dp::ZERO);
1478        if s.min_width > s.max_width {
1479            s.max_width = s.min_width;
1480        }
1481    }
1482    if let Some(value) = modifier.required_max_width {
1483        s.max_width = value.max(repose_core::Dp::ZERO);
1484        if s.min_width > s.max_width {
1485            s.min_width = s.max_width;
1486        }
1487    }
1488    if let Some(value) = modifier.required_min_height {
1489        s.min_height = value.max(repose_core::Dp::ZERO);
1490        if s.min_height > s.max_height {
1491            s.max_height = s.min_height;
1492        }
1493    }
1494    if let Some(value) = modifier.required_max_height {
1495        s.max_height = value.max(repose_core::Dp::ZERO);
1496        if s.min_height > s.max_height {
1497            s.min_height = s.max_height;
1498        }
1499    }
1500    if s.min_width > s.max_width {
1501        let coerced = s.max_width.max(repose_core::Dp::ZERO);
1502        s.min_width = coerced;
1503        s.max_width = coerced;
1504    }
1505    if s.min_height > s.max_height {
1506        let coerced = s.max_height.max(repose_core::Dp::ZERO);
1507        s.min_height = coerced;
1508        s.max_height = coerced;
1509    }
1510    SubcomposeScope::new(s.min_width, s.max_width, s.min_height, s.max_height)
1511}
1512
1513#[cfg(test)]
1514mod tests {
1515    use super::*;
1516    use repose_core::{
1517        Color, Dp, DrawStyle, FontStyle, FontWeight, Modifier, Sp, SubcomposeScope, TextAlign,
1518        TextDecoration, UnitExt, View, ViewKind,
1519    };
1520    use std::rc::Rc;
1521    use std::sync::Arc;
1522
1523    fn text_view(text: &str) -> View {
1524        View::new(
1525            0,
1526            ViewKind::Text {
1527                text: text.to_string(),
1528                color: Color::WHITE,
1529                font_size: 16.0.sp(),
1530                soft_wrap: true,
1531                max_lines: None,
1532                overflow: repose_core::TextOverflow::Visible,
1533                font_family: None,
1534                annotations: None,
1535                text_align: TextAlign::Unspecified,
1536                font_weight: FontWeight::NORMAL,
1537                font_style: FontStyle::Normal,
1538                text_decoration: TextDecoration::default(),
1539                letter_spacing: Sp::ZERO,
1540                line_height: Sp::ZERO,
1541                url: None,
1542                font_variation_settings: None,
1543                draw_style: DrawStyle::Fill,
1544            },
1545        )
1546    }
1547
1548    fn box_view() -> View {
1549        View::new(0, ViewKind::Box)
1550    }
1551
1552    #[test]
1553    fn test_create_tree() {
1554        let mut tree = ViewTree::new();
1555
1556        let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
1557
1558        tree.update(&root);
1559
1560        assert_eq!(tree.len(), 3); // box + 2 text
1561        assert!(tree.root().is_some());
1562    }
1563
1564    #[test]
1565    fn test_unchanged_tree_skips() {
1566        let mut tree = ViewTree::new();
1567
1568        let root = box_view().with_children(vec![text_view("Hello")]);
1569
1570        tree.update(&root);
1571        let gen1 = tree.generation();
1572
1573        tree.update(&root);
1574        let gen2 = tree.generation();
1575
1576        assert_eq!(gen2, gen1 + 1);
1577        assert!(tree.stats.skipped_nodes > 0);
1578    }
1579
1580    #[test]
1581    fn test_changed_content_reconciles() {
1582        let mut tree = ViewTree::new();
1583
1584        let root1 = box_view().with_children(vec![text_view("Hello")]);
1585
1586        tree.update(&root1);
1587
1588        let root2 = box_view().with_children(vec![text_view("Changed")]);
1589
1590        tree.update(&root2);
1591
1592        assert!(tree.stats.reconciled_nodes > 0);
1593    }
1594
1595    #[test]
1596    fn generated_view_ids_are_unique_and_stable() {
1597        let mut tree = ViewTree::new();
1598        let branch = |text: &str| box_view().with_children(vec![text_view(text)]);
1599        let root = box_view().with_children(vec![
1600            text_view("keyed").modifier(Modifier::new().key(1)),
1601            text_view("unkeyed"),
1602            branch("left"),
1603            branch("right"),
1604        ]);
1605
1606        tree.update(&root);
1607        let first: Vec<(NodeId, u64)> = tree
1608            .iter_with_ids()
1609            .map(|(id, node)| (id, node.view_id))
1610            .collect();
1611        let unique: std::collections::HashSet<u64> = first.iter().map(|(_, id)| *id).collect();
1612        assert_eq!(unique.len(), first.len());
1613        for (node_id, view_id) in &first {
1614            assert_eq!(
1615                tree.get_by_view_id(*view_id).map(|node| node.id),
1616                Some(*node_id)
1617            );
1618        }
1619
1620        tree.update(&root);
1621        let second: Vec<(NodeId, u64)> = tree
1622            .iter_with_ids()
1623            .map(|(id, node)| (id, node.view_id))
1624            .collect();
1625        assert_eq!(first, second);
1626    }
1627
1628    #[test]
1629    fn duplicate_explicit_ids_keep_first_lookup_stable() {
1630        let mut tree = ViewTree::new();
1631        let mut first = text_view("first").modifier(Modifier::new().key(1));
1632        let mut second = text_view("second").modifier(Modifier::new().key(2));
1633        first.id = 42;
1634        second.id = 42;
1635        let root = box_view().with_children(vec![first.clone(), second.clone()]);
1636        let root_id = tree.update(&root);
1637        let first_id = tree.children(root_id).unwrap()[0];
1638        assert_eq!(tree.get_by_view_id(42).map(|node| node.id), Some(first_id));
1639        let root = box_view().with_children(vec![second]);
1640        tree.update(&root);
1641        let second_id = tree.children(tree.root().unwrap()).unwrap()[0];
1642        assert_eq!(tree.get_by_view_id(42).map(|node| node.id), Some(second_id));
1643    }
1644
1645    #[test]
1646    fn test_keyed_children_stable() {
1647        let mut tree = ViewTree::new();
1648
1649        // Initial: A, B, C
1650        let root1 = box_view().with_children(vec![
1651            text_view("A").modifier(Modifier::new().key(1)),
1652            text_view("B").modifier(Modifier::new().key(2)),
1653            text_view("C").modifier(Modifier::new().key(3)),
1654        ]);
1655
1656        tree.update(&root1);
1657
1658        let _b_view_id = tree
1659            .root()
1660            .and_then(|r| tree.children(r))
1661            .and_then(|c| c.get(1).copied())
1662            .and_then(|id| tree.get(id))
1663            .map(|n| n.view_id);
1664
1665        // Reorder: C, A, B
1666        let root2 = box_view().with_children(vec![
1667            text_view("C").modifier(Modifier::new().key(3)),
1668            text_view("A").modifier(Modifier::new().key(1)),
1669            text_view("B").modifier(Modifier::new().key(2)),
1670        ]);
1671
1672        tree.update(&root2);
1673
1674        // B should have same view_id (key-based stability)
1675        // Note: Implementation detail - the node may be reused
1676        assert_eq!(tree.len(), 4); // Still 4 nodes (box + 3 text)
1677    }
1678
1679    fn subcompose_view<F>(f: F) -> View
1680    where
1681        F: Fn(&SubcomposeScope) -> View + 'static,
1682    {
1683        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
1684            Arc::new(move |scope| vec![(0, f(scope))]);
1685        View {
1686            id: 0,
1687            kind: ViewKind::SubcomposeLayout { content },
1688            modifier: Modifier::default(),
1689            children: Vec::new(),
1690            scope_key: None,
1691            semantics: None,
1692        }
1693    }
1694
1695    #[test]
1696    fn callback_identity_updates_handlers_without_layout_reset() {
1697        let mut tree = ViewTree::new();
1698        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1699        let content_calls = calls.clone();
1700        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(move |_| {
1701            content_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1702            vec![(0, text_view("callback"))]
1703        });
1704        let make = |callback: Rc<dyn Fn()>| {
1705            let callback = callback.clone();
1706            View {
1707                id: 0,
1708                kind: ViewKind::SubcomposeLayout {
1709                    content: content.clone(),
1710                },
1711                modifier: Modifier::new().on_click(move || callback()),
1712                children: Vec::new(),
1713                scope_key: None,
1714                semantics: None,
1715            }
1716        };
1717        let root = make(Rc::new(|| {}));
1718        let root_id = tree.update(&root);
1719        tree.set_layout(
1720            root_id,
1721            Rect {
1722                x: 0.0,
1723                y: 0.0,
1724                w: 100.0,
1725                h: 100.0,
1726            },
1727            Rect {
1728                x: 0.0,
1729                y: 0.0,
1730                w: 100.0,
1731                h: 100.0,
1732            },
1733            crate::LayoutConstraints::default(),
1734        );
1735        let root = make(Rc::new(|| {}));
1736        tree.update(&root);
1737        let calls_before = calls.load(std::sync::atomic::Ordering::SeqCst);
1738        tree.update(&root);
1739        assert!(tree.get(root_id).unwrap().has_valid_layout());
1740        assert_eq!(
1741            calls.load(std::sync::atomic::Ordering::SeqCst),
1742            calls_before
1743        );
1744        let calls_before = calls.load(std::sync::atomic::Ordering::SeqCst);
1745        let root = make(Rc::new(|| {}));
1746        tree.update(&root);
1747        assert!(tree.get(root_id).unwrap().has_valid_layout());
1748        assert_eq!(
1749            calls.load(std::sync::atomic::Ordering::SeqCst),
1750            calls_before
1751        );
1752        assert!(tree.paint_dirty_nodes().contains(&root_id));
1753    }
1754
1755    #[test]
1756    fn test_subcompose_invokes_content() {
1757        let mut tree = ViewTree::new();
1758        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1759        let counter2 = counter.clone();
1760
1761        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
1762            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1763            text_view("from subcompose")
1764        })]);
1765
1766        tree.update(&root);
1767
1768        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1769        assert_eq!(tree.len(), 3); // box + subcompose + text
1770    }
1771
1772    #[test]
1773    fn test_subcompose_receives_scope() {
1774        let mut tree = ViewTree::new();
1775        let captured = Arc::new(std::sync::Mutex::new(None));
1776        let captured2 = captured.clone();
1777
1778        let root = box_view().with_children(vec![subcompose_view(move |scope| {
1779            *captured2.lock().unwrap() = Some(*scope);
1780            text_view("hi")
1781        })]);
1782
1783        tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(360.0), Dp(0.0), Dp(640.0)));
1784        tree.update(&root);
1785
1786        let observed = captured.lock().unwrap().expect("scope captured");
1787        assert_eq!(observed.max_width, Dp(360.0));
1788        assert_eq!(observed.max_height, Dp(640.0));
1789        assert_eq!(observed.min_width, Dp(0.0));
1790        assert_eq!(observed.min_height, Dp(0.0));
1791    }
1792
1793    #[test]
1794    fn zero_subcompose_bounds_are_real() {
1795        let mut tree = ViewTree::new();
1796        let captured = Arc::new(std::sync::Mutex::new(None));
1797        let captured2 = captured.clone();
1798        let root = box_view().with_children(vec![subcompose_view(move |scope| {
1799            *captured2.lock().unwrap() = Some(*scope);
1800            text_view("zero")
1801        })]);
1802        tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(0.0), Dp(0.0), Dp(0.0)));
1803        tree.update(&root);
1804        let scope = captured.lock().unwrap().unwrap();
1805        assert_eq!(scope.min_width, Dp(0.0));
1806        assert_eq!(scope.max_width, Dp(0.0));
1807        assert_eq!(scope.min_height, Dp(0.0));
1808        assert_eq!(scope.max_height, Dp(0.0));
1809    }
1810
1811    #[test]
1812    fn test_subcompose_re_invokes_on_update() {
1813        let mut tree = ViewTree::new();
1814        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1815        let counter2 = counter.clone();
1816
1817        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
1818            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1819            text_view("hi")
1820        })]);
1821
1822        tree.update(&root);
1823        tree.update(&root);
1824        tree.update(&root);
1825
1826        // Closure should run only on the first update; subsequent updates hit
1827        // the cache because the scope and content are unchanged.
1828        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1829    }
1830
1831    #[test]
1832    fn test_subcompose_reruns_on_scope_change() {
1833        let mut tree = ViewTree::new();
1834        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1835        let counter2 = counter.clone();
1836
1837        let root = box_view().with_children(vec![subcompose_view(move |_scope| {
1838            counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1839            text_view("hi")
1840        })]);
1841
1842        tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
1843        tree.update(&root);
1844        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1845
1846        // Same scope: cache hit.
1847        tree.update(&root);
1848        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1849
1850        // Scope changed: closure re-runs.
1851        tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(200.0), Dp(0.0), Dp(200.0)));
1852        tree.update(&root);
1853        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1854    }
1855
1856    #[test]
1857    fn test_subcompose_reruns_on_content_change() {
1858        let mut tree = ViewTree::new();
1859        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1860        let c1 = counter.clone();
1861
1862        let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
1863            c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1864            text_view("hi")
1865        })]);
1866
1867        tree.update(&root1);
1868        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1869
1870        // Same content: cache hit.
1871        tree.update(&root1);
1872        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1873
1874        // Changed modifier: closure re-runs.
1875        let c2 = counter.clone();
1876        let root2 = box_view().with_children(vec![
1877            subcompose_view(move |_scope| {
1878                c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1879                text_view("hi")
1880            })
1881            .modifier(Modifier::new().padding(4.0.dp())),
1882        ]);
1883
1884        tree.update(&root2);
1885        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1886    }
1887
1888    #[test]
1889    fn test_subcompose_cache_drops_on_node_removal() {
1890        let mut tree = ViewTree::new();
1891        tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
1892
1893        // First root has a SubcomposeLayout child.
1894        let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1895        let c1 = counter.clone();
1896        let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
1897            c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1898            text_view("hi")
1899        })]);
1900
1901        tree.update(&root_with_sub);
1902        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1903
1904        // Swap to a root without the SubcomposeLayout - the old node should be
1905        // garbage-collected and its cache entry dropped.
1906        let root_no_sub = box_view().with_children(vec![text_view("plain")]);
1907        tree.update(&root_no_sub);
1908        assert_eq!(tree.len(), 2);
1909
1910        // Bring the SubcomposeLayout back - it must run the closure again
1911        // because the cache entry was dropped during GC.
1912        let c2 = counter.clone();
1913        let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
1914            c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1915            text_view("hi")
1916        })]);
1917
1918        tree.update(&root_with_sub_again);
1919        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1920    }
1921
1922    fn multi_slot_view<F>(f: F) -> View
1923    where
1924        F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
1925    {
1926        let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
1927        View {
1928            id: 0,
1929            kind: ViewKind::SubcomposeLayout { content },
1930            modifier: Modifier::default(),
1931            children: Vec::new(),
1932            scope_key: None,
1933            semantics: None,
1934        }
1935    }
1936
1937    #[test]
1938    fn test_subcompose_multi_slot_produces_multiple_children() {
1939        let mut tree = ViewTree::new();
1940        let root = box_view().with_children(vec![multi_slot_view(|_scope| {
1941            vec![
1942                (0, text_view("a")),
1943                (1, text_view("b")),
1944                (2, text_view("c")),
1945            ]
1946        })]);
1947
1948        tree.update(&root);
1949
1950        // box + subcompose + 3 texts
1951        assert_eq!(tree.len(), 5);
1952        let sub_id = tree
1953            .root()
1954            .and_then(|r| tree.children(r))
1955            .and_then(|c| c.first().copied())
1956            .expect("subcompose node");
1957        let sub_children = tree.children(sub_id).expect("subcompose has children");
1958        assert_eq!(sub_children.len(), 3);
1959    }
1960
1961    #[test]
1962    fn test_subcompose_multi_slot_preserves_identity_across_removal() {
1963        let mut tree = ViewTree::new();
1964
1965        // 3 slots: 0, 1, 2
1966        let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
1967            vec![
1968                (0, text_view("a")),
1969                (1, text_view("b")),
1970                (2, text_view("c")),
1971            ]
1972        })]);
1973        tree.update(&root3);
1974
1975        let sub_id = tree
1976            .root()
1977            .and_then(|r| tree.children(r))
1978            .and_then(|c| c.first().copied())
1979            .expect("subcompose node");
1980        let before = tree.children(sub_id).expect("children").to_vec();
1981        let a_node = before[0];
1982        let b_node = before[1];
1983        let c_node = before[2];
1984
1985        // 2 slots: 0, 2 (middle removed). The Modifier change (padding) forces
1986        // the subcompose cache to invalidate so the new closure runs.
1987        let root2 = box_view().with_children(vec![
1988            multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
1989                .modifier(Modifier::new().padding(4.0.dp())),
1990        ]);
1991        tree.update(&root2);
1992
1993        let after = tree.children(sub_id).expect("children after");
1994        assert_eq!(after.len(), 2);
1995        // Slot 0 (a) and slot 2 (c) should keep their NodeId.
1996        assert_eq!(after[0], a_node);
1997        assert_eq!(after[1], c_node);
1998        // Slot 1 (b) should be gone.
1999        assert!(tree.get(b_node).is_none());
2000    }
2001
2002    #[test]
2003    fn test_subcompose_ancestor_modifier_narrows_scope() {
2004        let mut tree = ViewTree::new();
2005        tree.set_subcompose_scope(SubcomposeScope::new(
2006            Dp(0.0),
2007            Dp(1000.0),
2008            Dp(0.0),
2009            Dp(1000.0),
2010        ));
2011
2012        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2013        let cap2 = captured.clone();
2014
2015        // SubcomposeLayout inside a Box with width(200.dp) - the closure should
2016        // see max_width == 200.
2017        let sub = multi_slot_view(move |scope| {
2018            *cap2.lock().unwrap() = *scope;
2019            vec![(0, text_view("hi"))]
2020        });
2021        let root = box_view()
2022            .modifier(Modifier::new().width(200.0.dp()))
2023            .with_children(vec![sub]);
2024
2025        tree.update(&root);
2026
2027        let observed = *captured.lock().unwrap();
2028        assert_eq!(observed.max_width, Dp(200.0));
2029    }
2030
2031    #[test]
2032    fn test_subcompose_chained_ancestor_constraints_intersect() {
2033        let mut tree = ViewTree::new();
2034        tree.set_subcompose_scope(SubcomposeScope::new(
2035            Dp(0.0),
2036            Dp(1000.0),
2037            Dp(0.0),
2038            Dp(1000.0),
2039        ));
2040
2041        let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2042        let cap2 = captured.clone();
2043
2044        let sub = multi_slot_view(move |scope| {
2045            *cap2.lock().unwrap() = *scope;
2046            vec![(0, text_view("hi"))]
2047        });
2048        // Box(width=400) -> Box(max_width=300) -> SubcomposeLayout.
2049        // The intersection should give max_width = 300.
2050        let root = box_view()
2051            .modifier(Modifier::new().width(400.0.dp()))
2052            .with_children(vec![
2053                box_view()
2054                    .modifier(Modifier::new().max_width(300.0.dp()))
2055                    .with_children(vec![sub]),
2056            ]);
2057
2058        tree.update(&root);
2059
2060        let observed = *captured.lock().unwrap();
2061        assert_eq!(observed.max_width, Dp(300.0));
2062    }
2063
2064    #[test]
2065    // Test-only: the view is shared across closures on one thread; Arc is structural.
2066    #[allow(clippy::arc_with_non_send_sync)]
2067    fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
2068        let mut tree = ViewTree::new();
2069        tree.set_subcompose_scope(SubcomposeScope::new(
2070            Dp(0.0),
2071            Dp(1000.0),
2072            Dp(0.0),
2073            Dp(1000.0),
2074        ));
2075
2076        let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2077        let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2078        let outer2 = outer_captured.clone();
2079        let inner2 = inner_captured.clone();
2080
2081        // Outer SubcomposeLayout(width=400) hosts an inner SubcomposeLayout.
2082        // The inner closure should observe max_width = 400, not 1000.
2083        let inner = Arc::new(multi_slot_view(move |scope| {
2084            *inner2.lock().unwrap() = *scope;
2085            vec![(0, text_view("inner"))]
2086        }));
2087        let inner_clone = inner.clone();
2088        let outer = multi_slot_view(move |scope| {
2089            *outer2.lock().unwrap() = *scope;
2090            vec![(0, (*inner_clone).clone())]
2091        })
2092        .modifier(Modifier::new().width(400.0.dp()));
2093        let root = box_view().with_children(vec![outer]);
2094
2095        tree.update(&root);
2096
2097        let outer_obs = *outer_captured.lock().unwrap();
2098        let inner_obs = *inner_captured.lock().unwrap();
2099        assert_eq!(outer_obs.max_width, Dp(400.0));
2100        assert_eq!(inner_obs.max_width, Dp(400.0));
2101    }
2102}