Skip to main content

cranpose_ui/widgets/nodes/
layout_node.rs

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