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