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