Skip to main content

cranpose_ui/widgets/nodes/
layout_node.rs

1#[cfg(test)]
2use crate::layout::LayoutRuntimeDebugStats;
3use crate::{
4    layout::{LayoutRuntimeState, MeasuredNode},
5    modifier::{
6        Modifier, ModifierChainHandle, ModifierLocalSource, ModifierLocalToken,
7        ModifierLocalsHandle, ModifierNodeSlices, Point, ResolvedModifierLocal, ResolvedModifiers,
8        Size,
9    },
10};
11use cranpose_core::{Node, NodeId};
12use cranpose_foundation::{
13    InvalidationKind, ModifierInvalidation, NodeCapabilities, SemanticsConfiguration,
14};
15use cranpose_ui_layout::{Constraints, MeasurePolicy};
16use std::any::TypeId;
17use std::cell::{Cell, RefCell};
18use std::collections::HashMap;
19use std::hash::{Hash, Hasher};
20use std::rc::Rc;
21
22#[derive(Clone, Copy)]
23enum LayoutInvalidationDispatchDiag {
24    Disabled,
25    All,
26    Node(NodeId),
27}
28
29fn layout_invalidation_dispatch_diag() -> LayoutInvalidationDispatchDiag {
30    static MODE: std::sync::OnceLock<LayoutInvalidationDispatchDiag> = std::sync::OnceLock::new();
31    *MODE.get_or_init(|| {
32        let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_INVALIDATION_DISPATCH_DIAG") else {
33            return LayoutInvalidationDispatchDiag::Disabled;
34        };
35        if value == "all" {
36            return LayoutInvalidationDispatchDiag::All;
37        }
38        value
39            .to_string_lossy()
40            .parse::<NodeId>()
41            .map(LayoutInvalidationDispatchDiag::Node)
42            .unwrap_or(LayoutInvalidationDispatchDiag::Disabled)
43    })
44}
45
46fn log_layout_invalidation_dispatch(
47    id: NodeId,
48    invalidation: &ModifierInvalidation,
49    curr_caps: NodeCapabilities,
50    prev_caps: NodeCapabilities,
51    modifier: &Modifier,
52) {
53    let enabled = match layout_invalidation_dispatch_diag() {
54        LayoutInvalidationDispatchDiag::Disabled => false,
55        LayoutInvalidationDispatchDiag::All => true,
56        LayoutInvalidationDispatchDiag::Node(target) => target == id,
57    };
58    if enabled {
59        log::warn!(
60            "[layout-invalidation-dispatch] node={} invalidation={:?} curr_caps={:?} prev_caps={:?} modifier={}",
61            id,
62            invalidation,
63            curr_caps,
64            prev_caps,
65            modifier
66        );
67    }
68}
69
70/// Retained layout state for a LayoutNode.
71/// This mirrors Jetpack Compose's approach where each node stores its own
72/// measured size and placed position, eliminating the need for per-frame
73/// LayoutTree reconstruction.
74#[derive(Clone, Debug)]
75pub struct LayoutState {
76    /// The measured size of this node (width, height).
77    pub size: Size,
78    /// Position relative to parent's content origin.
79    pub position: Point,
80    /// True if this node has been placed in the current layout pass.
81    pub is_placed: bool,
82    /// The constraints used for the last measurement.
83    pub measurement_constraints: Constraints,
84    /// Offset of the content box relative to the node origin (e.g. due to padding).
85    pub content_offset: Point,
86}
87
88impl Default for LayoutState {
89    fn default() -> Self {
90        Self {
91            size: Size::default(),
92            position: Point::default(),
93            is_placed: false,
94            measurement_constraints: Constraints {
95                min_width: 0.0,
96                max_width: f32::INFINITY,
97                min_height: 0.0,
98                max_height: f32::INFINITY,
99            },
100            content_offset: Point::default(),
101        }
102    }
103}
104
105#[derive(Clone)]
106struct MeasurementCacheEntry {
107    constraints: Constraints,
108    measured: Rc<MeasuredNode>,
109}
110
111#[derive(Clone, Copy, Debug)]
112pub enum IntrinsicKind {
113    MinWidth(f32),
114    MaxWidth(f32),
115    MinHeight(f32),
116    MaxHeight(f32),
117}
118
119impl IntrinsicKind {
120    fn discriminant(&self) -> u8 {
121        match self {
122            IntrinsicKind::MinWidth(_) => 0,
123            IntrinsicKind::MaxWidth(_) => 1,
124            IntrinsicKind::MinHeight(_) => 2,
125            IntrinsicKind::MaxHeight(_) => 3,
126        }
127    }
128
129    fn value_bits(&self) -> u32 {
130        match self {
131            IntrinsicKind::MinWidth(value)
132            | IntrinsicKind::MaxWidth(value)
133            | IntrinsicKind::MinHeight(value)
134            | IntrinsicKind::MaxHeight(value) => value.to_bits(),
135        }
136    }
137}
138
139impl PartialEq for IntrinsicKind {
140    fn eq(&self, other: &Self) -> bool {
141        self.discriminant() == other.discriminant() && self.value_bits() == other.value_bits()
142    }
143}
144
145impl Eq for IntrinsicKind {}
146
147impl Hash for IntrinsicKind {
148    fn hash<H: Hasher>(&self, state: &mut H) {
149        self.discriminant().hash(state);
150        self.value_bits().hash(state);
151    }
152}
153
154#[derive(Default)]
155struct NodeCacheState {
156    epoch: u64,
157    measurements: Vec<MeasurementCacheEntry>,
158    intrinsics: Vec<(IntrinsicKind, f32)>,
159}
160
161#[derive(Clone, Default)]
162pub(crate) struct LayoutNodeCacheHandles {
163    state: Rc<RefCell<NodeCacheState>>,
164}
165
166impl LayoutNodeCacheHandles {
167    pub(crate) fn clear(&self) {
168        let mut state = self.state.borrow_mut();
169        state.measurements.clear();
170        state.intrinsics.clear();
171        state.epoch = 0;
172    }
173
174    pub(crate) fn activate(&self, epoch: u64) {
175        let mut state = self.state.borrow_mut();
176        if state.epoch != epoch {
177            state.measurements.clear();
178            state.intrinsics.clear();
179            state.epoch = epoch;
180        }
181    }
182
183    pub(crate) fn epoch(&self) -> u64 {
184        self.state.borrow().epoch
185    }
186
187    pub(crate) fn get_measurement(&self, constraints: Constraints) -> Option<Rc<MeasuredNode>> {
188        let state = self.state.borrow();
189        state
190            .measurements
191            .iter()
192            .find(|entry| entry.constraints == constraints)
193            .map(|entry| Rc::clone(&entry.measured))
194    }
195
196    pub(crate) fn store_measurement(&self, constraints: Constraints, measured: Rc<MeasuredNode>) {
197        let mut state = self.state.borrow_mut();
198        if let Some(entry) = state
199            .measurements
200            .iter_mut()
201            .find(|entry| entry.constraints == constraints)
202        {
203            entry.measured = measured;
204        } else {
205            state.measurements.push(MeasurementCacheEntry {
206                constraints,
207                measured,
208            });
209        }
210    }
211
212    pub(crate) fn get_intrinsic(&self, kind: &IntrinsicKind) -> Option<f32> {
213        let state = self.state.borrow();
214        state
215            .intrinsics
216            .iter()
217            .find(|(stored_kind, _)| stored_kind == kind)
218            .map(|(_, value)| *value)
219    }
220
221    pub(crate) fn store_intrinsic(&self, kind: IntrinsicKind, value: f32) {
222        let mut state = self.state.borrow_mut();
223        if let Some((_, existing)) = state
224            .intrinsics
225            .iter_mut()
226            .find(|(stored_kind, _)| stored_kind == &kind)
227        {
228            *existing = value;
229        } else {
230            state.intrinsics.push((kind, value));
231        }
232    }
233}
234
235pub struct LayoutNode {
236    pub modifier: Modifier,
237    modifier_chain: ModifierChainHandle,
238    resolved_modifiers: ResolvedModifiers,
239    modifier_capabilities: NodeCapabilities,
240    modifier_child_capabilities: NodeCapabilities,
241    pub measure_policy: Rc<dyn MeasurePolicy>,
242    /// The device pixel grid this node was composed against.
243    ///
244    /// Measurement runs after composition and so cannot read a composition
245    /// local; the grid is captured here while the composition that owns this
246    /// node is still running, which is what lets a subtree be measured on a
247    /// grid of its own rather than on whatever the shell holds.
248    density: crate::density::Density,
249    /// The actual children of this node (folded view - includes virtual nodes as-is)
250    pub children: Vec<NodeId>,
251    cache: LayoutNodeCacheHandles,
252    // Dirty flags for selective measure/layout/render
253    needs_measure: Cell<bool>,
254    needs_layout: Cell<bool>,
255    needs_semantics: Cell<bool>,
256    needs_redraw: Cell<bool>,
257    needs_pointer_pass: Cell<bool>,
258    needs_focus_sync: Cell<bool>,
259    /// Parent for dirty flag bubbling (skips virtual nodes)
260    parent: Cell<Option<NodeId>>,
261    /// Direct parent in the tree (may be virtual)
262    folded_parent: Cell<Option<NodeId>>,
263    // Node's own ID (set by applier after creation)
264    id: Cell<Option<NodeId>>,
265    owner_context_id: Cell<Option<crate::render_state::AppContextId>>,
266    debug_modifiers: Cell<bool>,
267    /// Virtual node flag - virtual nodes are transparent containers for subcomposition
268    /// Their children are flattened into the parent's children list for measurement
269    is_virtual: bool,
270    /// Count of virtual children (for lazy unfolded children computation)
271    virtual_children_count: Cell<usize>,
272
273    modifier_slices_snapshot: RefCell<Rc<ModifierNodeSlices>>,
274    modifier_slices_dirty: Cell<bool>,
275
276    /// Retained layout state (size, position) for this node.
277    /// Updated by measure/place and read by renderer.
278    /// Wrapped in Rc to ensure state is shared across clones (e.g. SubcomposeLayout usage).
279    layout_state: Rc<RefCell<LayoutState>>,
280    layout_runtime_state: Rc<RefCell<LayoutRuntimeState>>,
281}
282
283pub(crate) const RECYCLED_LAYOUT_NODE_POOL_LIMIT: usize = 128;
284
285thread_local! {
286    static EMPTY_MEASURE_POLICY: Rc<dyn MeasurePolicy> =
287        Rc::new(crate::layout::policies::EmptyMeasurePolicy);
288}
289
290fn empty_measure_policy() -> Rc<dyn MeasurePolicy> {
291    EMPTY_MEASURE_POLICY.with(Rc::clone)
292}
293
294impl LayoutNode {
295    pub fn new(modifier: Modifier, measure_policy: Rc<dyn MeasurePolicy>) -> Self {
296        Self::new_with_virtual(modifier, measure_policy, false)
297    }
298
299    /// Create a virtual LayoutNode for subcomposition slot containers.
300    /// Virtual nodes are transparent - their children are flattened into parent's children list.
301    pub fn new_virtual() -> Self {
302        Self::new_with_virtual(Modifier::empty(), empty_measure_policy(), true)
303    }
304
305    fn new_recycled_shell(is_virtual: bool) -> Self {
306        let mut shell =
307            Self::new_with_virtual(Modifier::empty(), empty_measure_policy(), is_virtual);
308        shell.needs_measure.set(false);
309        shell.needs_layout.set(false);
310        shell.needs_semantics.set(false);
311        shell.needs_redraw.set(false);
312        shell.needs_pointer_pass.set(false);
313        shell.needs_focus_sync.set(false);
314        shell.parent.set(None);
315        shell.folded_parent.set(None);
316        shell.id.set(None);
317        shell.owner_context_id.set(None);
318        shell.debug_modifiers.set(false);
319        shell.virtual_children_count.set(0);
320        shell.cache = LayoutNodeCacheHandles::default();
321        shell.modifier_slices_snapshot = RefCell::new(Rc::default());
322        shell.modifier_slices_dirty = Cell::new(true);
323        shell.layout_state = Rc::new(RefCell::new(LayoutState::default()));
324        shell.layout_runtime_state = Rc::new(RefCell::new(LayoutRuntimeState::default()));
325        shell
326    }
327
328    fn new_with_virtual(
329        modifier: Modifier,
330        measure_policy: Rc<dyn MeasurePolicy>,
331        is_virtual: bool,
332    ) -> Self {
333        let mut node = Self {
334            modifier,
335            modifier_chain: ModifierChainHandle::new(),
336            resolved_modifiers: ResolvedModifiers::default(),
337            modifier_capabilities: NodeCapabilities::default(),
338            modifier_child_capabilities: NodeCapabilities::default(),
339            measure_policy,
340            density: crate::density::Density::default(),
341            children: Vec::new(),
342            cache: LayoutNodeCacheHandles::default(),
343            needs_measure: Cell::new(true), // New nodes need initial measure
344            needs_layout: Cell::new(true),  // New nodes need initial layout
345            needs_semantics: Cell::new(true), // Semantics snapshot needs initial build
346            needs_redraw: Cell::new(true),  // First render should draw the node
347            needs_pointer_pass: Cell::new(false),
348            needs_focus_sync: Cell::new(false),
349            parent: Cell::new(None),        // Non-virtual parent for bubbling
350            folded_parent: Cell::new(None), // Direct parent (may be virtual)
351            id: Cell::new(None),            // ID set by applier after creation
352            owner_context_id: Cell::new(None),
353            debug_modifiers: Cell::new(false),
354            is_virtual,
355            virtual_children_count: Cell::new(0),
356            modifier_slices_snapshot: RefCell::new(Rc::default()),
357            modifier_slices_dirty: Cell::new(true),
358            layout_state: Rc::new(RefCell::new(LayoutState::default())),
359            layout_runtime_state: Rc::new(RefCell::new(LayoutRuntimeState::default())),
360        };
361        node.sync_modifier_chain();
362        node
363    }
364
365    pub fn set_modifier(&mut self, modifier: Modifier) {
366        // Always sync the modifier chain because element equality is used for node
367        // matching/reuse, not for skipping updates. Closures may capture updated
368        // state values that need to be passed to nodes even when the Modifier
369        // compares as equal. This matches Jetpack Compose where update() is always
370        // called on matched nodes.
371        let modifier_changed = !self.modifier.structural_eq(&modifier);
372        self.modifier = modifier;
373        self.sync_modifier_chain();
374        if modifier_changed {
375            self.cache.clear();
376            self.request_semantics_update();
377        }
378    }
379
380    fn sync_modifier_chain(&mut self) {
381        let prev_caps = self.modifier_capabilities;
382        let start_parent = self.parent();
383        let mut resolver = move |token: &ModifierLocalToken| {
384            resolve_modifier_local_from_parent_chain(start_parent, token)
385        };
386        self.modifier_chain
387            .set_debug_logging(self.debug_modifiers.get());
388        self.modifier_chain.set_node_id(self.id.get());
389        let modifier_local_invalidations = self
390            .modifier_chain
391            .update_with_resolver(&self.modifier, &mut resolver);
392        self.resolved_modifiers = self.modifier_chain.resolved_modifiers();
393        self.modifier_capabilities = self.modifier_chain.capabilities();
394        self.modifier_child_capabilities = self.modifier_chain.aggregate_child_capabilities();
395
396        self.update_modifier_slices_cache();
397
398        let mut invalidations = self.modifier_chain.take_invalidations();
399        invalidations.extend(modifier_local_invalidations);
400        self.dispatch_modifier_invalidations_with_prev(&invalidations, prev_caps);
401        self.refresh_registry_state();
402    }
403
404    fn update_modifier_slices_cache(&self) {
405        use crate::modifier::collect_modifier_slices_into;
406
407        let mut snapshot = self.modifier_slices_snapshot.borrow_mut();
408        collect_modifier_slices_into(self.modifier_chain.chain(), Rc::make_mut(&mut snapshot));
409        self.modifier_slices_dirty.set(false);
410    }
411
412    pub(crate) fn mark_modifier_slices_dirty(&self) {
413        self.modifier_slices_dirty.set(true);
414    }
415
416    #[cfg(test)]
417    fn dispatch_modifier_invalidations(&self, invalidations: &[ModifierInvalidation]) {
418        self.dispatch_modifier_invalidations_with_prev(invalidations, NodeCapabilities::empty());
419    }
420
421    fn dispatch_modifier_invalidations_with_prev(
422        &self,
423        invalidations: &[ModifierInvalidation],
424        prev_caps: NodeCapabilities,
425    ) {
426        let curr_caps = self.modifier_capabilities;
427        for invalidation in invalidations {
428            self.modifier_slices_dirty.set(true);
429            let has_capability =
430                |capability| curr_caps.contains(capability) || prev_caps.contains(capability);
431            match invalidation.kind() {
432                InvalidationKind::Layout => {
433                    if has_capability(NodeCapabilities::LAYOUT) {
434                        self.mark_needs_measure();
435                        if let Some(id) = self.id.get() {
436                            log_layout_invalidation_dispatch(
437                                id,
438                                invalidation,
439                                curr_caps,
440                                prev_caps,
441                                &self.modifier,
442                            );
443                            let inside_composition =
444                                cranpose_core::composer_context::try_with_composer(|_| ())
445                                    .is_some();
446                            if inside_composition {
447                                cranpose_core::bubble_measure_dirty_in_composer(id);
448                            } else {
449                                crate::schedule_layout_repass(id);
450                            }
451                        }
452                    }
453                }
454                InvalidationKind::Draw => {
455                    if has_capability(NodeCapabilities::DRAW)
456                        || invalidation.capabilities().contains(NodeCapabilities::DRAW)
457                    {
458                        self.mark_needs_redraw();
459                    }
460                }
461                InvalidationKind::PointerInput => {
462                    if has_capability(NodeCapabilities::POINTER_INPUT) {
463                        self.mark_needs_pointer_pass();
464                        crate::request_pointer_invalidation();
465                        // Schedule pointer repass for this node
466                        if let Some(id) = self.id.get() {
467                            crate::schedule_pointer_repass(id);
468                        }
469                    }
470                }
471                InvalidationKind::Semantics => {
472                    self.request_semantics_update();
473                }
474                InvalidationKind::Focus => {
475                    if has_capability(NodeCapabilities::FOCUS) {
476                        self.mark_needs_focus_sync();
477                        crate::request_focus_invalidation();
478                        // Schedule focus invalidation for this node
479                        if let Some(id) = self.id.get() {
480                            crate::schedule_focus_invalidation(id);
481                        }
482                    }
483                }
484            }
485        }
486    }
487
488    /// The grid this node was composed against.
489    pub fn density(&self) -> crate::density::Density {
490        self.density
491    }
492
493    /// Records the grid the composition provided, re-measuring if it moved.
494    pub fn set_density(&mut self, density: crate::density::Density) {
495        if self.density != density {
496            self.density = density;
497            self.cache.clear();
498            self.mark_needs_measure();
499        }
500    }
501
502    pub fn set_measure_policy(&mut self, policy: Rc<dyn MeasurePolicy>) {
503        // Only mark dirty if policy actually changed (pointer comparison)
504        if !Rc::ptr_eq(&self.measure_policy, &policy) {
505            self.measure_policy = policy;
506            self.cache.clear();
507            self.mark_needs_measure();
508            if let Some(id) = self.id.get() {
509                cranpose_core::bubble_measure_dirty_in_composer(id);
510            }
511        }
512    }
513
514    /// Mark this node as needing measure. Also marks it as needing layout.
515    pub fn mark_needs_measure(&self) {
516        self.needs_measure.set(true);
517        self.needs_layout.set(true);
518    }
519
520    /// Mark this node as needing layout (but not necessarily measure).
521    pub fn mark_needs_layout(&self) {
522        self.needs_layout.set(true);
523    }
524
525    /// Mark this node as needing redraw without forcing measure/layout.
526    pub fn mark_needs_redraw(&self) {
527        self.needs_redraw.set(true);
528        if let Some(id) = self.id.get() {
529            crate::schedule_draw_repass(id);
530        }
531        crate::request_render_invalidation();
532    }
533
534    /// Check if this node needs measure.
535    pub fn needs_measure(&self) -> bool {
536        self.needs_measure.get()
537    }
538
539    /// Check if this node needs layout.
540    pub fn needs_layout(&self) -> bool {
541        self.needs_layout.get()
542    }
543
544    /// Mark this node as needing semantics recomputation.
545    pub fn mark_needs_semantics(&self) {
546        self.needs_semantics.set(true);
547    }
548
549    /// Clear the semantics dirty flag after rebuilding semantics.
550    pub(crate) fn clear_needs_semantics(&self) {
551        self.needs_semantics.set(false);
552    }
553
554    /// Returns true when semantics need to be recomputed.
555    pub fn needs_semantics(&self) -> bool {
556        self.needs_semantics.get()
557    }
558
559    /// Returns true when this node requested a redraw since the last render pass.
560    pub fn needs_redraw(&self) -> bool {
561        self.needs_redraw.get()
562    }
563
564    pub fn clear_needs_redraw(&self) {
565        self.needs_redraw.set(false);
566    }
567
568    fn request_semantics_update(&self) {
569        let already_dirty = self.needs_semantics.replace(true);
570        if already_dirty {
571            return;
572        }
573
574        if let Some(id) = self.id.get() {
575            cranpose_core::queue_semantics_invalidation(id);
576        }
577    }
578
579    /// Clear the measure dirty flag after measuring.
580    pub(crate) fn clear_needs_measure(&self) {
581        self.needs_measure.set(false);
582    }
583
584    /// Clear the layout dirty flag after laying out.
585    pub(crate) fn clear_needs_layout(&self) {
586        self.needs_layout.set(false);
587    }
588
589    /// Marks this node as needing a fresh pointer-input pass.
590    pub fn mark_needs_pointer_pass(&self) {
591        self.needs_pointer_pass.set(true);
592    }
593
594    /// Returns true when pointer-input state needs to be recomputed.
595    pub fn needs_pointer_pass(&self) -> bool {
596        self.needs_pointer_pass.get()
597    }
598
599    /// Clears the pointer-input dirty flag after hosts service it.
600    pub fn clear_needs_pointer_pass(&self) {
601        self.needs_pointer_pass.set(false);
602    }
603
604    /// Marks this node as needing a focus synchronization.
605    pub fn mark_needs_focus_sync(&self) {
606        self.needs_focus_sync.set(true);
607    }
608
609    /// Returns true when focus state needs to be synchronized.
610    pub fn needs_focus_sync(&self) -> bool {
611        self.needs_focus_sync.get()
612    }
613
614    /// Clears the focus dirty flag after the focus manager processes it.
615    pub fn clear_needs_focus_sync(&self) {
616        self.needs_focus_sync.set(false);
617    }
618
619    /// Set this node's ID (called by applier after creation).
620    pub fn set_node_id(&mut self, id: NodeId) {
621        if let Some(existing) = self.id.replace(Some(id)) {
622            if let Some(owner_context_id) = self.owner_context_id.take() {
623                unregister_layout_node(owner_context_id, existing);
624            }
625        }
626        let owner_context_id = register_layout_node(id, self);
627        self.owner_context_id.set(Some(owner_context_id));
628        self.refresh_registry_state();
629
630        // Propagate the ID to the modifier chain. This triggers a lifecycle update
631        // for nodes that depend on the node ID for invalidation (e.g., ScrollNode).
632        self.modifier_chain.set_node_id(Some(id));
633        let invalidations = self.modifier_chain.take_invalidations();
634        self.dispatch_modifier_invalidations_with_prev(&invalidations, NodeCapabilities::empty());
635        self.update_modifier_slices_cache();
636    }
637
638    /// Get this node's ID.
639    pub fn node_id(&self) -> Option<NodeId> {
640        self.id.get()
641    }
642
643    /// Set this node's parent (called when node is added as child).
644    /// Sets both folded_parent (direct) and parent (first non-virtual ancestor for bubbling).
645    pub fn set_parent(&self, parent: NodeId) {
646        self.folded_parent.set(Some(parent));
647        // For now, parent = folded_parent. Virtual parent skipping requires applier access.
648        // The actual virtual-skipping happens in bubble_measure_dirty via applier traversal.
649        self.parent.set(Some(parent));
650        self.refresh_registry_state();
651    }
652
653    /// Clear this node's parent (called when node is removed from parent).
654    pub fn clear_parent(&self) {
655        self.folded_parent.set(None);
656        self.parent.set(None);
657        self.refresh_registry_state();
658    }
659
660    /// Get this node's parent for dirty flag bubbling (may skip virtual nodes).
661    pub fn parent(&self) -> Option<NodeId> {
662        self.parent.get()
663    }
664
665    /// Get this node's direct parent (may be a virtual node).
666    pub fn folded_parent(&self) -> Option<NodeId> {
667        self.folded_parent.get()
668    }
669
670    /// Returns true if this is a virtual node (transparent container for subcomposition).
671    pub fn is_virtual(&self) -> bool {
672        self.is_virtual
673    }
674
675    pub(crate) fn cache_handles(&self) -> LayoutNodeCacheHandles {
676        self.cache.clone()
677    }
678
679    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
680        self.resolved_modifiers
681    }
682
683    pub fn modifier_capabilities(&self) -> NodeCapabilities {
684        self.modifier_capabilities
685    }
686
687    pub fn modifier_child_capabilities(&self) -> NodeCapabilities {
688        self.modifier_child_capabilities
689    }
690
691    pub fn set_debug_modifiers(&mut self, enabled: bool) {
692        self.debug_modifiers.set(enabled);
693        self.modifier_chain.set_debug_logging(enabled);
694    }
695
696    pub fn debug_modifiers_enabled(&self) -> bool {
697        self.debug_modifiers.get()
698    }
699
700    pub fn modifier_locals_handle(&self) -> ModifierLocalsHandle {
701        self.modifier_chain.modifier_locals_handle()
702    }
703
704    pub fn has_layout_modifier_nodes(&self) -> bool {
705        self.modifier_capabilities
706            .contains(NodeCapabilities::LAYOUT)
707    }
708
709    pub fn has_draw_modifier_nodes(&self) -> bool {
710        self.modifier_capabilities.contains(NodeCapabilities::DRAW)
711    }
712
713    pub fn has_pointer_input_modifier_nodes(&self) -> bool {
714        self.modifier_capabilities
715            .contains(NodeCapabilities::POINTER_INPUT)
716    }
717
718    pub fn has_semantics_modifier_nodes(&self) -> bool {
719        self.modifier_capabilities
720            .contains(NodeCapabilities::SEMANTICS)
721    }
722
723    pub fn has_focus_modifier_nodes(&self) -> bool {
724        self.modifier_capabilities.contains(NodeCapabilities::FOCUS)
725    }
726
727    fn refresh_registry_state(&self) {
728        if let (Some(id), Some(owner_context_id)) = (self.id.get(), self.owner_context_id.get()) {
729            let parent = self.parent();
730            let capabilities = self.modifier_child_capabilities();
731            let modifier_locals = self.modifier_locals_handle();
732            let _ = crate::render_state::with_layout_node_registry_by_app_context(
733                owner_context_id,
734                |registry| {
735                    registry.update_entry(id, parent, capabilities, modifier_locals);
736                },
737            );
738        }
739    }
740
741    pub fn modifier_slices_snapshot(&self) -> Rc<ModifierNodeSlices> {
742        if self.modifier_slices_dirty.get() {
743            self.update_modifier_slices_cache();
744        }
745        self.modifier_slices_snapshot.borrow().clone()
746    }
747
748    // ═══════════════════════════════════════════════════════════════════════
749    // Retained Layout State API
750    // ═══════════════════════════════════════════════════════════════════════
751
752    /// Returns a clone of the current layout state.
753    pub fn layout_state(&self) -> LayoutState {
754        self.layout_state.borrow().clone()
755    }
756
757    /// Returns the measured size of this node.
758    pub fn measured_size(&self) -> Size {
759        self.layout_state.borrow().size
760    }
761
762    /// Returns the position of this node relative to its parent.
763    pub fn position(&self) -> Point {
764        self.layout_state.borrow().position
765    }
766
767    /// Returns true if this node has been placed in the current layout pass.
768    pub fn is_placed(&self) -> bool {
769        self.layout_state.borrow().is_placed
770    }
771
772    /// Updates the measured size of this node. Called during measurement.
773    pub fn set_measured_size(&self, size: Size) {
774        let mut state = self.layout_state.borrow_mut();
775        state.size = size;
776    }
777
778    /// Updates the position of this node. Called during placement.
779    pub fn set_position(&self, position: Point) {
780        let mut state = self.layout_state.borrow_mut();
781        state.position = position;
782        state.is_placed = true;
783    }
784
785    /// Records the constraints used for measurement. Used for relayout optimization.
786    pub fn set_measurement_constraints(&self, constraints: Constraints) {
787        self.layout_state.borrow_mut().measurement_constraints = constraints;
788    }
789
790    /// Records the content offset (e.g. from padding).
791    pub fn set_content_offset(&self, offset: Point) {
792        self.layout_state.borrow_mut().content_offset = offset;
793    }
794
795    /// Clears the is_placed flag. Called at the start of a layout pass.
796    pub fn clear_placed(&self) {
797        self.layout_state.borrow_mut().is_placed = false;
798    }
799
800    pub fn semantics_configuration(&self) -> Option<SemanticsConfiguration> {
801        crate::modifier::collect_semantics_from_chain(self.modifier_chain.chain())
802    }
803
804    /// Returns a reference to the modifier chain for layout/draw pipeline integration.
805    pub(crate) fn modifier_chain(&self) -> &ModifierChainHandle {
806        &self.modifier_chain
807    }
808
809    /// Access the text field modifier node (if present) with a mutable callback.
810    ///
811    /// This is used for keyboard event dispatch to text fields.
812    /// Returns `None` if no text field modifier is found in the chain.
813    pub fn with_text_field_modifier_mut<R>(
814        &mut self,
815        f: impl FnMut(&mut crate::TextFieldModifierNode) -> R,
816    ) -> Option<R> {
817        self.modifier_chain.with_text_field_modifier_mut(f)
818    }
819
820    /// Returns a handle to the shared layout state.
821    /// Used by layout system to update state without borrowing the Applier.
822    pub fn layout_state_handle(&self) -> Rc<RefCell<LayoutState>> {
823        self.layout_state.clone()
824    }
825
826    pub(crate) fn layout_runtime_state_handle(&self) -> Rc<RefCell<LayoutRuntimeState>> {
827        self.layout_runtime_state.clone()
828    }
829
830    #[cfg(test)]
831    pub(crate) fn layout_runtime_debug_stats(&self) -> LayoutRuntimeDebugStats {
832        self.layout_runtime_state.borrow().debug_stats()
833    }
834}
835impl Clone for LayoutNode {
836    fn clone(&self) -> Self {
837        let mut node = Self {
838            modifier: self.modifier.clone(),
839            modifier_chain: ModifierChainHandle::new(),
840            resolved_modifiers: ResolvedModifiers::default(),
841            modifier_capabilities: self.modifier_capabilities,
842            modifier_child_capabilities: self.modifier_child_capabilities,
843            measure_policy: self.measure_policy.clone(),
844            density: self.density,
845            children: self.children.clone(),
846            cache: self.cache.clone(),
847            needs_measure: Cell::new(self.needs_measure.get()),
848            needs_layout: Cell::new(self.needs_layout.get()),
849            needs_semantics: Cell::new(self.needs_semantics.get()),
850            needs_redraw: Cell::new(self.needs_redraw.get()),
851            needs_pointer_pass: Cell::new(self.needs_pointer_pass.get()),
852            needs_focus_sync: Cell::new(self.needs_focus_sync.get()),
853            parent: Cell::new(self.parent.get()),
854            folded_parent: Cell::new(self.folded_parent.get()),
855            id: Cell::new(None),
856            owner_context_id: Cell::new(None),
857            debug_modifiers: Cell::new(self.debug_modifiers.get()),
858            is_virtual: self.is_virtual,
859            virtual_children_count: Cell::new(self.virtual_children_count.get()),
860            modifier_slices_snapshot: RefCell::new(Rc::default()),
861            modifier_slices_dirty: Cell::new(true),
862            // Share the same layout state across clones
863            layout_state: self.layout_state.clone(),
864            layout_runtime_state: self.layout_runtime_state.clone(),
865        };
866        node.sync_modifier_chain();
867        node
868    }
869}
870
871impl Node for LayoutNode {
872    fn mount(&mut self) {
873        let (chain, mut context) = self.modifier_chain.chain_and_context_mut();
874        chain.repair_chain();
875        chain.attach_nodes(&mut *context);
876    }
877
878    fn unmount(&mut self) {
879        self.modifier_chain.chain_mut().detach_nodes();
880    }
881
882    fn set_node_id(&mut self, id: NodeId) {
883        // Delegate to inherent method to ensure proper registration and chain updates
884        LayoutNode::set_node_id(self, id);
885    }
886
887    fn insert_child(&mut self, child: NodeId) {
888        if self.children.contains(&child) {
889            return;
890        }
891        if is_virtual_node(child) {
892            let count = self.virtual_children_count.get();
893            self.virtual_children_count.set(count + 1);
894        }
895        self.children.push(child);
896        self.cache.clear();
897        self.mark_needs_measure();
898    }
899
900    fn remove_child(&mut self, child: NodeId) {
901        let before = self.children.len();
902        self.children.retain(|&id| id != child);
903        if self.children.len() < before {
904            if is_virtual_node(child) {
905                let count = self.virtual_children_count.get();
906                if count > 0 {
907                    self.virtual_children_count.set(count - 1);
908                }
909            }
910            self.cache.clear();
911            self.mark_needs_measure();
912        }
913    }
914
915    fn move_child(&mut self, from: usize, to: usize) {
916        if from == to || from >= self.children.len() {
917            return;
918        }
919        let child = self.children.remove(from);
920        let target = to.min(self.children.len());
921        self.children.insert(target, child);
922        self.cache.clear();
923        self.mark_needs_measure();
924    }
925
926    fn update_children(&mut self, children: &[NodeId]) {
927        self.children.clear();
928        self.children.extend_from_slice(children);
929        self.cache.clear();
930        self.mark_needs_measure();
931    }
932
933    fn children(&self) -> Vec<NodeId> {
934        self.children.clone()
935    }
936
937    fn collect_children_into(&self, out: &mut smallvec::SmallVec<[NodeId; 8]>) {
938        out.clear();
939        out.extend(self.children.iter().copied());
940    }
941
942    fn on_attached_to_parent(&mut self, parent: NodeId) {
943        self.set_parent(parent);
944    }
945
946    fn on_removed_from_parent(&mut self) {
947        self.clear_parent();
948    }
949
950    fn parent(&self) -> Option<NodeId> {
951        self.parent.get()
952    }
953
954    fn mark_needs_layout(&self) {
955        self.needs_layout.set(true);
956    }
957
958    fn needs_layout(&self) -> bool {
959        self.needs_layout.get()
960    }
961
962    fn mark_needs_measure(&self) {
963        self.needs_measure.set(true);
964        self.needs_layout.set(true);
965    }
966
967    fn needs_measure(&self) -> bool {
968        self.needs_measure.get()
969    }
970
971    fn mark_needs_semantics(&self) {
972        self.needs_semantics.set(true);
973    }
974
975    fn needs_semantics(&self) -> bool {
976        self.needs_semantics.get()
977    }
978
979    /// Minimal parent setter for dirty flag bubbling.
980    /// Only sets the parent Cell without triggering registry updates.
981    /// This is used during SubcomposeLayout measurement where we need parent
982    /// pointers for bubble_measure_dirty but don't want full attachment side effects.
983    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
984        self.parent.set(Some(parent));
985    }
986
987    fn recycle_key(&self) -> Option<TypeId> {
988        Some(TypeId::of::<Self>())
989    }
990
991    fn recycle_pool_limit(&self) -> Option<usize> {
992        Some(RECYCLED_LAYOUT_NODE_POOL_LIMIT)
993    }
994
995    fn prepare_for_recycle(&mut self) {
996        *self = Self::new_recycled_shell(self.is_virtual);
997    }
998
999    fn rehouse_for_recycle(&self) -> Option<Box<dyn cranpose_core::Node>> {
1000        Some(Box::new(Self::new_recycled_shell(self.is_virtual)))
1001    }
1002
1003    fn rehouse_for_live_compaction(&mut self) -> Option<Box<dyn cranpose_core::Node>> {
1004        let mut previous = std::mem::replace(self, Self::new_recycled_shell(self.is_virtual));
1005        let node_id = previous.id.replace(None);
1006        let parent = previous.parent.get();
1007        let folded_parent = previous.folded_parent.get();
1008        let debug_modifiers = previous.debug_modifiers.get();
1009        let needs_measure = previous.needs_measure.get();
1010        let needs_layout = previous.needs_layout.get();
1011        let needs_semantics = previous.needs_semantics.get();
1012        let needs_redraw = previous.needs_redraw.get();
1013        let needs_pointer_pass = previous.needs_pointer_pass.get();
1014        let needs_focus_sync = previous.needs_focus_sync.get();
1015        let virtual_children_count = previous.virtual_children_count.get();
1016        let children = previous.children.to_vec();
1017        let modifier = previous.modifier.rehouse_for_live_compaction();
1018        let measure_policy = previous.measure_policy.clone();
1019        let layout_state = previous.layout_state.clone();
1020        let layout_runtime_state = previous.layout_runtime_state.clone();
1021
1022        previous.modifier_chain.chain_mut().detach_nodes();
1023
1024        let mut compact = Self::new_with_virtual(modifier, measure_policy, previous.is_virtual);
1025        compact.children = children;
1026        compact.parent.set(parent);
1027        compact.folded_parent.set(folded_parent);
1028        compact.id.set(node_id);
1029        compact.debug_modifiers.set(debug_modifiers);
1030        compact.needs_measure.set(needs_measure);
1031        compact.needs_layout.set(needs_layout);
1032        compact.needs_semantics.set(needs_semantics);
1033        compact.needs_redraw.set(needs_redraw);
1034        compact.needs_pointer_pass.set(needs_pointer_pass);
1035        compact.needs_focus_sync.set(needs_focus_sync);
1036        compact.virtual_children_count.set(virtual_children_count);
1037        compact.layout_state = layout_state;
1038        compact.layout_runtime_state = layout_runtime_state;
1039        compact.sync_modifier_chain();
1040        if let Some(id) = node_id {
1041            let owner_context_id = register_layout_node(id, &compact);
1042            compact.owner_context_id.set(Some(owner_context_id));
1043        }
1044
1045        Some(Box::new(compact))
1046    }
1047}
1048
1049impl Drop for LayoutNode {
1050    fn drop(&mut self) {
1051        if let (Some(id), Some(owner_context_id)) = (self.id.get(), self.owner_context_id.get()) {
1052            unregister_layout_node(owner_context_id, id);
1053        }
1054    }
1055}
1056
1057const MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY: usize = 128;
1058const VIRTUAL_NODE_ID_START: NodeId = 0xC0000000;
1059
1060#[cfg(test)]
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1062struct LayoutNodeRegistryDebugStats {
1063    len: usize,
1064    capacity: usize,
1065}
1066
1067struct LayoutNodeRegistryEntry {
1068    parent: Option<NodeId>,
1069    modifier_child_capabilities: NodeCapabilities,
1070    modifier_locals: ModifierLocalsHandle,
1071    is_virtual: bool,
1072}
1073
1074pub(crate) struct LayoutNodeRegistryState {
1075    entries: RefCell<HashMap<NodeId, LayoutNodeRegistryEntry>>,
1076    virtual_node_id_counter: Cell<NodeId>,
1077}
1078
1079impl LayoutNodeRegistryState {
1080    pub(crate) fn new() -> Self {
1081        Self {
1082            entries: RefCell::new(HashMap::new()),
1083            virtual_node_id_counter: Cell::new(VIRTUAL_NODE_ID_START),
1084        }
1085    }
1086
1087    fn register(&self, id: NodeId, node: &LayoutNode) {
1088        self.entries.borrow_mut().insert(
1089            id,
1090            LayoutNodeRegistryEntry {
1091                parent: node.parent(),
1092                modifier_child_capabilities: node.modifier_child_capabilities(),
1093                modifier_locals: node.modifier_locals_handle(),
1094                is_virtual: node.is_virtual(),
1095            },
1096        );
1097    }
1098
1099    fn unregister(&self, id: NodeId) {
1100        let mut entries = self.entries.borrow_mut();
1101        entries.remove(&id);
1102        let should_shrink = (entries.len() <= MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY
1103            && entries.capacity() > MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY)
1104            || entries.capacity()
1105                > entries
1106                    .len()
1107                    .max(MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY)
1108                    .saturating_mul(4);
1109        if should_shrink {
1110            let retained = entries
1111                .len()
1112                .max(MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY);
1113            let mut rebuilt = HashMap::new();
1114            rebuilt.reserve(retained);
1115            rebuilt.extend(entries.drain());
1116            *entries = rebuilt;
1117        }
1118    }
1119
1120    fn update_entry(
1121        &self,
1122        id: NodeId,
1123        parent: Option<NodeId>,
1124        modifier_child_capabilities: NodeCapabilities,
1125        modifier_locals: ModifierLocalsHandle,
1126    ) {
1127        if let Some(entry) = self.entries.borrow_mut().get_mut(&id) {
1128            entry.parent = parent;
1129            entry.modifier_child_capabilities = modifier_child_capabilities;
1130            entry.modifier_locals = modifier_locals;
1131        }
1132    }
1133
1134    #[cfg(test)]
1135    fn stats(&self) -> LayoutNodeRegistryDebugStats {
1136        let entries = self.entries.borrow();
1137        LayoutNodeRegistryDebugStats {
1138            len: entries.len(),
1139            capacity: entries.capacity(),
1140        }
1141    }
1142
1143    fn is_virtual_node(&self, id: NodeId) -> bool {
1144        self.entries
1145            .borrow()
1146            .get(&id)
1147            .map(|entry| entry.is_virtual)
1148            .unwrap_or(false)
1149    }
1150
1151    fn allocate_virtual_node_id(&self) -> NodeId {
1152        let id = self.virtual_node_id_counter.get();
1153        self.virtual_node_id_counter.set(id.wrapping_add(1));
1154        id
1155    }
1156
1157    fn resolve_modifier_local_from_parent_chain(
1158        &self,
1159        start: Option<NodeId>,
1160        token: &ModifierLocalToken,
1161    ) -> Option<ResolvedModifierLocal> {
1162        let mut current = start;
1163        while let Some(parent_id) = current {
1164            let (next_parent, resolved) = {
1165                let entries = self.entries.borrow();
1166                if let Some(entry) = entries.get(&parent_id) {
1167                    let resolved = if entry
1168                        .modifier_child_capabilities
1169                        .contains(NodeCapabilities::MODIFIER_LOCALS)
1170                    {
1171                        entry
1172                            .modifier_locals
1173                            .borrow()
1174                            .resolve(token)
1175                            .map(|value| value.with_source(ModifierLocalSource::Ancestor))
1176                    } else {
1177                        None
1178                    };
1179                    (entry.parent, resolved)
1180                } else {
1181                    (None, None)
1182                }
1183            };
1184            if let Some(value) = resolved {
1185                return Some(value);
1186            }
1187            current = next_parent;
1188        }
1189        None
1190    }
1191}
1192
1193pub(crate) fn register_layout_node(
1194    id: NodeId,
1195    node: &LayoutNode,
1196) -> crate::render_state::AppContextId {
1197    let owner_context_id = crate::render_state::current_app_context_id();
1198    let _ = crate::render_state::with_layout_node_registry_by_app_context(
1199        owner_context_id,
1200        |registry| {
1201            registry.register(id, node);
1202        },
1203    );
1204    owner_context_id
1205}
1206
1207pub(crate) fn unregister_layout_node(
1208    owner_context_id: crate::render_state::AppContextId,
1209    id: NodeId,
1210) {
1211    let _ = crate::render_state::with_layout_node_registry_by_app_context(
1212        owner_context_id,
1213        |registry| {
1214            registry.unregister(id);
1215        },
1216    );
1217}
1218
1219#[cfg(test)]
1220fn layout_node_registry_stats() -> LayoutNodeRegistryDebugStats {
1221    crate::render_state::with_layout_node_registry(|registry| registry.stats())
1222}
1223
1224pub(crate) fn is_virtual_node(id: NodeId) -> bool {
1225    crate::render_state::with_layout_node_registry(|registry| registry.is_virtual_node(id))
1226}
1227
1228pub(crate) fn allocate_virtual_node_id() -> NodeId {
1229    crate::render_state::with_layout_node_registry(|registry| registry.allocate_virtual_node_id())
1230}
1231
1232fn resolve_modifier_local_from_parent_chain(
1233    start: Option<NodeId>,
1234    token: &ModifierLocalToken,
1235) -> Option<ResolvedModifierLocal> {
1236    crate::render_state::with_layout_node_registry(|registry| {
1237        registry.resolve_modifier_local_from_parent_chain(start, token)
1238    })
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243    use super::*;
1244    use cranpose_ui_graphics::Size as GeometrySize;
1245    use cranpose_ui_layout::{Measurable, MeasureResult, MeasureScope};
1246    use std::rc::Rc;
1247
1248    #[derive(Default)]
1249    struct TestMeasurePolicy;
1250
1251    impl MeasurePolicy for TestMeasurePolicy {
1252        fn measure(
1253            &self,
1254            _scope: &dyn MeasureScope,
1255            _measurables: &[Box<dyn Measurable>],
1256            _constraints: Constraints,
1257        ) -> MeasureResult {
1258            MeasureResult::new(
1259                GeometrySize {
1260                    width: 0.0,
1261                    height: 0.0,
1262                },
1263                Vec::new(),
1264            )
1265        }
1266
1267        fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
1268            0.0
1269        }
1270
1271        fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
1272            0.0
1273        }
1274
1275        fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
1276            0.0
1277        }
1278
1279        fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
1280            0.0
1281        }
1282    }
1283
1284    fn fresh_node() -> LayoutNode {
1285        LayoutNode::new(Modifier::empty(), Rc::new(TestMeasurePolicy))
1286    }
1287
1288    #[test]
1289    fn modifier_slices_cache_reuses_unique_snapshot_allocation() {
1290        let _app_context = crate::render_state::app_context_test_scope();
1291        let mut node = fresh_node();
1292        let snapshot = node.modifier_slices_snapshot();
1293        let snapshot_ptr = Rc::as_ptr(&snapshot);
1294        drop(snapshot);
1295
1296        node.set_modifier(Modifier::empty().padding(4.0));
1297
1298        let updated = node.modifier_slices_snapshot();
1299        assert_eq!(Rc::as_ptr(&updated), snapshot_ptr);
1300    }
1301
1302    #[test]
1303    fn modifier_slices_cache_preserves_live_snapshot_isolation() {
1304        let _app_context = crate::render_state::app_context_test_scope();
1305        let mut node = fresh_node();
1306        let old_snapshot = node.modifier_slices_snapshot();
1307        let old_snapshot_ptr = Rc::as_ptr(&old_snapshot);
1308
1309        node.set_modifier(Modifier::empty().padding(4.0));
1310
1311        let updated = node.modifier_slices_snapshot();
1312        assert_ne!(Rc::as_ptr(&updated), old_snapshot_ptr);
1313        assert_eq!(old_snapshot.draw_commands().len(), 0);
1314    }
1315
1316    #[test]
1317    fn layout_node_registry_retains_warm_capacity_after_large_cleanup() {
1318        let _app_context = crate::render_state::app_context_test_scope();
1319        let app_context = crate::render_state::AppContext::new_with_density(1.0);
1320        app_context.enter(|| {
1321            let nodes: Vec<_> = (0..2048)
1322                .map(|_| {
1323                    let id = allocate_virtual_node_id();
1324                    let node = fresh_node();
1325                    let owner_context_id = register_layout_node(id, &node);
1326                    (id, owner_context_id, node)
1327                })
1328                .collect();
1329
1330            for (id, owner_context_id, _) in &nodes {
1331                unregister_layout_node(*owner_context_id, *id);
1332            }
1333
1334            let stats = layout_node_registry_stats();
1335            assert_eq!(stats.len, 0);
1336            assert!(
1337                (MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY
1338                    ..=MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY.saturating_mul(2))
1339                    .contains(&stats.capacity),
1340                "registry warm capacity {} fell outside expected retained range {}..={}",
1341                stats.capacity,
1342                MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY,
1343                MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY.saturating_mul(2),
1344            );
1345        });
1346    }
1347
1348    #[test]
1349    fn layout_node_registry_is_scoped_by_app_context() {
1350        let _app_context = crate::render_state::app_context_test_scope();
1351        let first = crate::render_state::AppContext::new_with_density(1.0);
1352        let second = crate::render_state::AppContext::new_with_density(1.0);
1353
1354        let first_id = first.enter(allocate_virtual_node_id);
1355        let second_id = second.enter(allocate_virtual_node_id);
1356
1357        assert_eq!(first_id, VIRTUAL_NODE_ID_START);
1358        assert_eq!(second_id, VIRTUAL_NODE_ID_START);
1359
1360        let virtual_node = LayoutNode::new_virtual();
1361        let regular_node = fresh_node();
1362
1363        first.enter(|| {
1364            register_layout_node(first_id, &virtual_node);
1365            register_layout_node(101, &regular_node);
1366            assert!(is_virtual_node(first_id));
1367            assert_eq!(layout_node_registry_stats().len, 2);
1368        });
1369
1370        second.enter(|| {
1371            assert!(!is_virtual_node(first_id));
1372            assert_eq!(layout_node_registry_stats().len, 0);
1373            assert_eq!(allocate_virtual_node_id(), VIRTUAL_NODE_ID_START + 1);
1374        });
1375
1376        first.enter(|| {
1377            let owner_context_id = crate::render_state::current_app_context_id();
1378            unregister_layout_node(owner_context_id, first_id);
1379            unregister_layout_node(owner_context_id, 101);
1380            assert_eq!(layout_node_registry_stats().len, 0);
1381        });
1382    }
1383
1384    fn invalidation(kind: InvalidationKind) -> ModifierInvalidation {
1385        ModifierInvalidation::new(kind, NodeCapabilities::for_invalidation(kind))
1386    }
1387
1388    #[test]
1389    fn layout_invalidation_requires_layout_capability() {
1390        let _app_context = crate::render_state::app_context_test_scope();
1391        let mut node = fresh_node();
1392        node.clear_needs_measure();
1393        node.clear_needs_layout();
1394        node.modifier_capabilities = NodeCapabilities::DRAW;
1395        node.modifier_child_capabilities = node.modifier_capabilities;
1396
1397        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Layout)]);
1398
1399        assert!(!node.needs_measure());
1400        assert!(!node.needs_layout());
1401    }
1402
1403    #[test]
1404    fn semantics_configuration_reflects_modifier_state() {
1405        let _app_context = crate::render_state::app_context_test_scope();
1406        let mut node = fresh_node();
1407        node.set_modifier(Modifier::empty().semantics(|config| {
1408            config.content_description = Some("greeting".into());
1409            config.is_clickable = true;
1410        }));
1411
1412        let config = node
1413            .semantics_configuration()
1414            .expect("expected semantics configuration");
1415        assert_eq!(config.content_description.as_deref(), Some("greeting"));
1416        assert!(config.is_clickable);
1417    }
1418
1419    #[test]
1420    fn layout_invalidation_marks_flags_when_capability_present() {
1421        let _app_context = crate::render_state::app_context_test_scope();
1422        let _guard = crate::render_state::render_state_test_guard();
1423        crate::reset_render_state_for_tests();
1424        let mut node = fresh_node();
1425        node.id.set(Some(11));
1426        node.clear_needs_measure();
1427        node.clear_needs_layout();
1428        node.modifier_capabilities = NodeCapabilities::LAYOUT;
1429        node.modifier_child_capabilities = node.modifier_capabilities;
1430
1431        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Layout)]);
1432
1433        assert!(node.needs_measure());
1434        assert!(node.needs_layout());
1435        assert_eq!(crate::take_layout_repass_nodes(), vec![11]);
1436        assert!(crate::take_layout_invalidation());
1437    }
1438
1439    #[test]
1440    fn layout_invalidation_skips_repass_while_composing() {
1441        let _app_context = crate::render_state::app_context_test_scope();
1442        let _guard = crate::render_state::render_state_test_guard();
1443        crate::reset_render_state_for_tests();
1444
1445        let node = Rc::new(RefCell::new(fresh_node()));
1446        {
1447            let mut node = node.borrow_mut();
1448            node.id.set(Some(17));
1449            node.clear_needs_measure();
1450            node.clear_needs_layout();
1451            node.modifier_capabilities = NodeCapabilities::LAYOUT;
1452            node.modifier_child_capabilities = node.modifier_capabilities;
1453        }
1454
1455        let node_for_composition = Rc::clone(&node);
1456        let _composition = crate::run_test_composition(move || {
1457            node_for_composition
1458                .borrow()
1459                .dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Layout)]);
1460        });
1461
1462        let node = node.borrow();
1463        assert!(node.needs_measure());
1464        assert!(node.needs_layout());
1465        assert!(crate::take_layout_repass_nodes().is_empty());
1466        assert!(!crate::take_layout_invalidation());
1467    }
1468
1469    #[test]
1470    fn draw_invalidation_marks_redraw_flag_when_capable() {
1471        let _app_context = crate::render_state::app_context_test_scope();
1472        let mut node = fresh_node();
1473        node.clear_needs_measure();
1474        node.clear_needs_layout();
1475        node.modifier_capabilities = NodeCapabilities::DRAW;
1476        node.modifier_child_capabilities = node.modifier_capabilities;
1477
1478        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Draw)]);
1479
1480        assert!(node.needs_redraw());
1481        assert!(!node.needs_layout());
1482    }
1483
1484    #[test]
1485    fn draw_invalidation_capability_marks_redraw_for_layout_modifier_update() {
1486        let _app_context = crate::render_state::app_context_test_scope();
1487        let mut node = fresh_node();
1488        node.clear_needs_measure();
1489        node.clear_needs_layout();
1490        node.clear_needs_redraw();
1491        node.modifier_capabilities = NodeCapabilities::LAYOUT;
1492        node.modifier_child_capabilities = node.modifier_capabilities;
1493
1494        node.dispatch_modifier_invalidations(&[ModifierInvalidation::new(
1495            InvalidationKind::Draw,
1496            NodeCapabilities::DRAW,
1497        )]);
1498
1499        assert!(node.needs_redraw());
1500        assert!(!node.needs_measure());
1501        assert!(!node.needs_layout());
1502    }
1503
1504    #[test]
1505    fn semantics_invalidation_sets_semantics_flag_only() {
1506        let _app_context = crate::render_state::app_context_test_scope();
1507        let mut node = fresh_node();
1508        node.clear_needs_measure();
1509        node.clear_needs_layout();
1510        node.clear_needs_semantics();
1511        node.modifier_capabilities = NodeCapabilities::SEMANTICS;
1512        node.modifier_child_capabilities = node.modifier_capabilities;
1513
1514        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Semantics)]);
1515
1516        assert!(node.needs_semantics());
1517        assert!(!node.needs_measure());
1518        assert!(!node.needs_layout());
1519    }
1520
1521    #[test]
1522    fn pointer_invalidation_requires_pointer_capability() {
1523        let _app_context = crate::render_state::app_context_test_scope();
1524        let mut node = fresh_node();
1525        node.clear_needs_pointer_pass();
1526        node.modifier_capabilities = NodeCapabilities::DRAW;
1527        node.modifier_child_capabilities = node.modifier_capabilities;
1528        // Note: We don't assert on global take_pointer_invalidation() because
1529        // it's shared across tests running in parallel and causes flakiness.
1530        // The node's local state is sufficient to verify correct dispatch behavior.
1531
1532        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::PointerInput)]);
1533
1534        assert!(!node.needs_pointer_pass());
1535    }
1536
1537    #[test]
1538    fn pointer_invalidation_marks_flag_and_requests_queue() {
1539        let _app_context = crate::render_state::app_context_test_scope();
1540        let mut node = fresh_node();
1541        node.clear_needs_pointer_pass();
1542        node.modifier_capabilities = NodeCapabilities::POINTER_INPUT;
1543        node.modifier_child_capabilities = node.modifier_capabilities;
1544        // Note: We don't assert on global take_pointer_invalidation() because
1545        // it's shared across tests running in parallel and causes flakiness.
1546        // The node's local state is sufficient to verify correct dispatch behavior.
1547
1548        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::PointerInput)]);
1549
1550        assert!(node.needs_pointer_pass());
1551    }
1552
1553    #[test]
1554    fn focus_invalidation_requires_focus_capability() {
1555        let _app_context = crate::render_state::app_context_test_scope();
1556        let mut node = fresh_node();
1557        node.clear_needs_focus_sync();
1558        node.modifier_capabilities = NodeCapabilities::DRAW;
1559        node.modifier_child_capabilities = node.modifier_capabilities;
1560        crate::take_focus_invalidation();
1561
1562        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Focus)]);
1563
1564        assert!(!node.needs_focus_sync());
1565        assert!(!crate::take_focus_invalidation());
1566    }
1567
1568    #[test]
1569    fn focus_invalidation_marks_flag_and_requests_queue() {
1570        let _app_context = crate::render_state::app_context_test_scope();
1571        let mut node = fresh_node();
1572        node.clear_needs_focus_sync();
1573        node.modifier_capabilities = NodeCapabilities::FOCUS;
1574        node.modifier_child_capabilities = node.modifier_capabilities;
1575        crate::take_focus_invalidation();
1576
1577        node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Focus)]);
1578
1579        assert!(node.needs_focus_sync());
1580        assert!(crate::take_focus_invalidation());
1581    }
1582
1583    #[test]
1584    fn set_modifier_marks_semantics_dirty() {
1585        let _app_context = crate::render_state::app_context_test_scope();
1586        let mut node = fresh_node();
1587        node.clear_needs_semantics();
1588        node.set_modifier(Modifier::empty().semantics(|config| {
1589            config.is_clickable = true;
1590        }));
1591
1592        assert!(node.needs_semantics());
1593    }
1594
1595    #[test]
1596    fn modifier_child_capabilities_reflect_chain_head() {
1597        let _app_context = crate::render_state::app_context_test_scope();
1598        let mut node = fresh_node();
1599        node.set_modifier(Modifier::empty().padding(4.0));
1600        assert!(
1601            node.modifier_child_capabilities()
1602                .contains(NodeCapabilities::LAYOUT),
1603            "padding should introduce layout capability"
1604        );
1605    }
1606}