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