Skip to main content

cranpose_ui/widgets/nodes/
layout_node.rs

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