Skip to main content

cranpose_ui/widgets/nodes/
layout_node.rs

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