Skip to main content

cranpose_ui/layout/
mod.rs

1pub mod core;
2pub mod policies;
3
4use std::{
5    cell::{Cell, RefCell},
6    fmt,
7    mem::size_of,
8    rc::Rc,
9};
10
11use cranpose_core::{
12    Applier, ApplierHost, Composer, ConcreteApplierHost, MemoryApplier, Node, NodeError, NodeId,
13    Phase, RuntimeHandle, SlotTable, SlotsHost, SnapshotStateObserver,
14};
15
16use self::core::Measurable;
17use self::core::Placeable;
18#[cfg(test)]
19use self::core::{HorizontalAlignment, VerticalAlignment};
20use crate::modifier::{
21    collect_semantics_from_modifier, DimensionConstraint, EdgeInsets, Modifier, ModifierNodeSlices,
22    ModifierNodeSlicesDebugStats, Point, Rect as GeometryRect, ResolvedModifiers, Size,
23};
24
25use crate::subcompose_layout::SubcomposeLayoutNode;
26use crate::widgets::nodes::{IntrinsicKind, LayoutNode, LayoutNodeCacheHandles, LayoutState};
27use cranpose_foundation::{
28    text::TextRange, InvalidationKind, ModifierNodeContext, NodeCapabilities,
29    SemanticsConfiguration,
30};
31use cranpose_ui_layout::{Constraints, MeasurePolicy, Placement};
32
33/// Runtime context for modifier nodes during measurement.
34///
35/// Unlike `BasicModifierNodeContext`, this context accumulates invalidations
36/// that can be processed after measurement to set dirty flags on the LayoutNode.
37#[derive(Default)]
38pub(crate) struct LayoutNodeContext {
39    invalidations: Vec<InvalidationKind>,
40    update_requested: bool,
41    active_capabilities: Vec<NodeCapabilities>,
42}
43
44impl LayoutNodeContext {
45    pub(crate) fn new() -> Self {
46        Self::default()
47    }
48
49    pub(crate) fn take_invalidations(&mut self) -> Vec<InvalidationKind> {
50        std::mem::take(&mut self.invalidations)
51    }
52}
53
54impl ModifierNodeContext for LayoutNodeContext {
55    fn invalidate(&mut self, kind: InvalidationKind) {
56        if !self.invalidations.contains(&kind) {
57            self.invalidations.push(kind);
58        }
59    }
60
61    fn request_update(&mut self) {
62        self.update_requested = true;
63    }
64
65    fn push_active_capabilities(&mut self, capabilities: NodeCapabilities) {
66        self.active_capabilities.push(capabilities);
67    }
68
69    fn pop_active_capabilities(&mut self) {
70        self.active_capabilities.pop();
71    }
72}
73
74/// Forces all layout caches to be invalidated on the next measure by incrementing the epoch.
75///
76/// # ⚠️ Internal Use Only - NOT Public API
77///
78/// **This function is hidden from public documentation and MUST NOT be called by external code.**
79///
80/// Only `cranpose-app-shell` may call this for rare global events:
81/// - Window/viewport resize
82/// - Global font scale or density changes
83/// - Debug toggles that affect all layout
84///
85/// **This is O(entire app size) - extremely expensive!**
86///
87/// # For Local Changes
88///
89/// **Do NOT use this for scroll, single-node mutations, or any local layout change.**
90/// Instead, use the scoped repass mechanism:
91/// ```text
92/// cranpose_ui::schedule_layout_repass(node_id);
93/// ```
94///
95/// The scoped path bubbles dirty flags without invalidating all caches, giving you O(subtree) instead of O(app).
96#[doc(hidden)]
97pub fn invalidate_all_layout_caches() {
98    crate::render_state::invalidate_layout_cache_epoch();
99}
100
101/// RAII guard that:
102/// - moves the current MemoryApplier into a ConcreteApplierHost
103/// - holds a shared handle to the `SlotTable` used by `LayoutBuilder`
104/// - on Drop, always:
105///   * restores slots into the host from the shared handle
106///   * moves the original MemoryApplier back into the Composition
107///
108/// This makes `measure_layout` panic/Err-safe wrt both the applier and slots.
109/// The key invariant: guard and builder share the same `Rc<RefCell<SlotTable>>`,
110/// so the guard never loses access to the authoritative slots even on panic.
111struct ApplierSlotGuard<'a> {
112    /// The `MemoryApplier` inside the Composition::applier that we must restore into.
113    target: &'a mut MemoryApplier,
114    /// Host that owns the original MemoryApplier while layout is running.
115    host: Rc<ConcreteApplierHost<MemoryApplier>>,
116    /// Shared handle to the slot table. Both the guard and the builder hold a clone.
117    /// On Drop, we write whatever is in this handle back into the applier.
118    slots: Rc<RefCell<SlotTable>>,
119}
120
121impl<'a> ApplierSlotGuard<'a> {
122    /// Creates a new guard:
123    /// - moves the current MemoryApplier out of `target` into a host
124    /// - takes the current slots out of the host and wraps them in a shared handle
125    fn new(target: &'a mut MemoryApplier) -> Self {
126        // Move the original applier into a host; leave `target` with a fresh one
127        let original_applier = std::mem::replace(target, MemoryApplier::new());
128        let host = Rc::new(ConcreteApplierHost::new(original_applier));
129
130        // Take slots from the host into a shared handle
131        let slots = {
132            let mut applier_ref = host.borrow_typed();
133            std::mem::take(applier_ref.slots())
134        };
135        let slots = Rc::new(RefCell::new(slots));
136
137        Self {
138            target,
139            host,
140            slots,
141        }
142    }
143
144    /// Rc to pass into LayoutBuilder::new_with_epoch
145    fn host(&self) -> Rc<ConcreteApplierHost<MemoryApplier>> {
146        Rc::clone(&self.host)
147    }
148
149    /// Returns the shared handle to slots for the builder to use.
150    /// The builder clones this Rc, so both guard and builder share the same slots.
151    fn slots_handle(&self) -> Rc<RefCell<SlotTable>> {
152        Rc::clone(&self.slots)
153    }
154}
155
156impl Drop for ApplierSlotGuard<'_> {
157    fn drop(&mut self) {
158        // 1) Restore slots into the host's MemoryApplier from the shared handle.
159        // This works correctly whether we're on the success path or panic/error path,
160        // because we always have the shared handle.
161        {
162            let mut applier_ref = self.host.borrow_typed();
163            *applier_ref.slots() = std::mem::take(&mut *self.slots.borrow_mut());
164        }
165
166        // 2) Move the original MemoryApplier (with restored/updated slots) back into `target`
167        {
168            let mut applier_ref = self.host.borrow_typed();
169            let original_applier = std::mem::take(&mut *applier_ref);
170            let _ = std::mem::replace(self.target, original_applier);
171        }
172        // No Rc::try_unwrap in Drop → no "panic during panic" risk.
173    }
174}
175
176/// Result of measuring through the modifier node chain.
177struct ModifierChainMeasurement {
178    size: Size,
179    /// Content offset for scroll/inner transforms - NOT padding semantics
180    content_offset: Point,
181    /// Node's own offset (from OffsetNode, affects position in parent)
182    offset: Point,
183}
184
185type LayoutModifierNodeData = (
186    usize,
187    Rc<RefCell<Box<dyn cranpose_foundation::ModifierNode>>>,
188);
189
190struct ScratchVecPool<T> {
191    available: Vec<Vec<T>>,
192}
193
194impl<T> ScratchVecPool<T> {
195    fn acquire(&mut self) -> Vec<T> {
196        self.available.pop().unwrap_or_default()
197    }
198
199    fn release(&mut self, mut values: Vec<T>) {
200        values.clear();
201        self.available.push(values);
202    }
203
204    #[cfg(test)]
205    fn available_count(&self) -> usize {
206        self.available.len()
207    }
208}
209
210impl<T> Default for ScratchVecPool<T> {
211    fn default() -> Self {
212        Self {
213            available: Vec::new(),
214        }
215    }
216}
217
218#[derive(Default)]
219pub(crate) struct FrameLayoutArena {
220    tmp_records: ScratchVecPool<(NodeId, ChildRecord)>,
221    tmp_child_ids: ScratchVecPool<NodeId>,
222    tmp_layout_node_data: ScratchVecPool<LayoutModifierNodeData>,
223    tmp_placements: ScratchVecPool<Placement>,
224}
225
226#[cfg(test)]
227impl FrameLayoutArena {
228    pub(crate) fn available_placement_scratch_count(&self) -> usize {
229        self.tmp_placements.available_count()
230    }
231
232    pub(crate) fn seed_placement_scratch_for_test(&mut self) {
233        self.tmp_placements.release(Vec::with_capacity(1));
234    }
235}
236
237/// Discrete event callback reference produced during semantics extraction.
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct SemanticsCallback {
240    node_id: NodeId,
241}
242
243impl SemanticsCallback {
244    pub fn new(node_id: NodeId) -> Self {
245        Self { node_id }
246    }
247
248    pub fn node_id(&self) -> NodeId {
249        self.node_id
250    }
251}
252
253/// Semantics action exposed to the input system.
254#[derive(Clone, Debug, PartialEq, Eq)]
255pub enum SemanticsAction {
256    Click { handler: SemanticsCallback },
257}
258
259/// Semantic role describing how a node should participate in accessibility and hit testing.
260/// Roles are now derived from SemanticsConfiguration rather than widget types.
261#[derive(Clone, Debug, PartialEq, Eq)]
262pub enum SemanticsRole {
263    /// Generic container or layout node
264    Layout,
265    /// Subcomposition boundary
266    Subcompose,
267    /// Text content derived from the text node semantics payload.
268    Text { value: String },
269    /// Spacer (non-interactive)
270    Spacer,
271    /// Button (derived from is_button semantics flag)
272    Button,
273    /// Unknown or unspecified role
274    Unknown,
275}
276
277/// A single node within the semantics tree.
278#[derive(Clone, Debug, PartialEq, Eq)]
279pub struct SemanticsNode {
280    pub node_id: NodeId,
281    pub role: SemanticsRole,
282    pub actions: Vec<SemanticsAction>,
283    pub children: Vec<SemanticsNode>,
284    pub description: Option<String>,
285    pub editable_text: bool,
286    pub text_selection: Option<TextRange>,
287}
288
289impl SemanticsNode {
290    fn new(
291        node_id: NodeId,
292        role: SemanticsRole,
293        actions: Vec<SemanticsAction>,
294        children: Vec<SemanticsNode>,
295        description: Option<String>,
296        editable_text: bool,
297        text_selection: Option<TextRange>,
298    ) -> Self {
299        Self {
300            node_id,
301            role,
302            actions,
303            children,
304            description,
305            editable_text,
306            text_selection,
307        }
308    }
309}
310
311/// Rooted semantics tree extracted after layout.
312#[derive(Clone, Debug, PartialEq, Eq)]
313pub struct SemanticsTree {
314    root: SemanticsNode,
315}
316
317impl SemanticsTree {
318    fn new(root: SemanticsNode) -> Self {
319        Self { root }
320    }
321
322    pub fn root(&self) -> &SemanticsNode {
323        &self.root
324    }
325}
326
327#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
328pub struct LayoutAllocationDebugStats {
329    pub layout_box_count: usize,
330    pub layout_box_child_count: usize,
331    pub layout_box_child_capacity: usize,
332    pub layout_box_heap_bytes: usize,
333    pub modifier_slice_count: usize,
334    pub modifier_slice_heap_bytes: usize,
335    pub modifier_draw_command_count: usize,
336    pub modifier_draw_command_capacity: usize,
337    pub modifier_pointer_input_count: usize,
338    pub modifier_pointer_input_capacity: usize,
339    pub modifier_click_handler_count: usize,
340    pub modifier_click_handler_capacity: usize,
341    pub modifier_text_content_count: usize,
342    pub modifier_text_style_count: usize,
343    pub modifier_text_layout_options_count: usize,
344    pub modifier_prepared_text_layout_count: usize,
345    pub modifier_graphics_layer_count: usize,
346    pub modifier_graphics_layer_resolver_count: usize,
347    pub semantics_node_count: usize,
348    pub semantics_action_count: usize,
349    pub semantics_action_capacity: usize,
350    pub semantics_child_count: usize,
351    pub semantics_child_capacity: usize,
352    pub semantics_description_count: usize,
353    pub semantics_description_bytes: usize,
354    pub semantics_text_role_bytes: usize,
355    pub semantics_heap_bytes: usize,
356}
357
358impl LayoutAllocationDebugStats {
359    fn add_modifier_slice(&mut self, stats: ModifierNodeSlicesDebugStats) {
360        self.modifier_slice_count += 1;
361        self.modifier_slice_heap_bytes += stats.heap_bytes;
362        self.modifier_draw_command_count += stats.draw_command_count;
363        self.modifier_draw_command_capacity += stats.draw_command_capacity;
364        self.modifier_pointer_input_count += stats.pointer_input_count;
365        self.modifier_pointer_input_capacity += stats.pointer_input_capacity;
366        self.modifier_click_handler_count += stats.click_handler_count;
367        self.modifier_click_handler_capacity += stats.click_handler_capacity;
368        self.modifier_text_content_count += usize::from(stats.has_text_content);
369        self.modifier_text_style_count += usize::from(stats.has_text_style);
370        self.modifier_text_layout_options_count += usize::from(stats.has_text_layout_options);
371        self.modifier_prepared_text_layout_count += usize::from(stats.has_prepared_text_layout);
372        self.modifier_graphics_layer_count += usize::from(stats.has_graphics_layer);
373        self.modifier_graphics_layer_resolver_count +=
374            usize::from(stats.has_graphics_layer_resolver);
375    }
376}
377
378/// Result of running layout for a Compose tree.
379#[derive(Debug, Clone)]
380pub struct LayoutTree {
381    root: LayoutBox,
382}
383
384impl LayoutTree {
385    pub fn new(root: LayoutBox) -> Self {
386        Self { root }
387    }
388
389    pub fn root(&self) -> &LayoutBox {
390        &self.root
391    }
392
393    pub fn root_mut(&mut self) -> &mut LayoutBox {
394        &mut self.root
395    }
396
397    pub fn into_root(self) -> LayoutBox {
398        self.root
399    }
400
401    pub fn debug_allocation_stats(&self) -> LayoutAllocationDebugStats {
402        let mut stats = LayoutAllocationDebugStats::default();
403        record_layout_box_allocation_stats(&self.root, &mut stats);
404        stats
405    }
406}
407
408/// Layout information for a single node.
409#[derive(Debug, Clone)]
410pub struct LayoutBox {
411    pub node_id: NodeId,
412    pub rect: GeometryRect,
413    /// Content offset for scroll/inner transforms (applies to children, NOT this node's position)
414    pub content_offset: Point,
415    pub node_data: LayoutNodeData,
416    pub children: Vec<LayoutBox>,
417}
418
419impl LayoutBox {
420    pub fn new(
421        node_id: NodeId,
422        rect: GeometryRect,
423        content_offset: Point,
424        node_data: LayoutNodeData,
425        children: Vec<LayoutBox>,
426    ) -> Self {
427        Self {
428            node_id,
429            rect,
430            content_offset,
431            node_data,
432            children,
433        }
434    }
435}
436
437/// Snapshot of the data required to render a layout node.
438#[derive(Debug, Clone)]
439pub struct LayoutNodeData {
440    pub modifier: Modifier,
441    pub resolved_modifiers: ResolvedModifiers,
442    pub modifier_slices: Rc<ModifierNodeSlices>,
443    pub kind: LayoutNodeKind,
444}
445
446impl LayoutNodeData {
447    pub fn new(
448        modifier: Modifier,
449        resolved_modifiers: ResolvedModifiers,
450        modifier_slices: Rc<ModifierNodeSlices>,
451        kind: LayoutNodeKind,
452    ) -> Self {
453        Self {
454            modifier,
455            resolved_modifiers,
456            modifier_slices,
457            kind,
458        }
459    }
460
461    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
462        self.resolved_modifiers
463    }
464
465    pub fn modifier_slices(&self) -> &ModifierNodeSlices {
466        &self.modifier_slices
467    }
468}
469
470/// Classification of the node captured inside a [`LayoutBox`].
471///
472/// Note: Text content is no longer represented as a distinct LayoutNodeKind.
473/// Text nodes now use `LayoutNodeKind::Layout` with their content stored in
474/// `modifier_slices.text_content()` via TextModifierNode, following Jetpack
475/// Compose's pattern where text is a modifier node capability.
476#[derive(Clone)]
477pub enum LayoutNodeKind {
478    Layout,
479    Subcompose,
480    Spacer,
481    Button { on_click: Rc<RefCell<dyn FnMut()>> },
482    Unknown,
483}
484
485impl fmt::Debug for LayoutNodeKind {
486    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487        match self {
488            LayoutNodeKind::Layout => f.write_str("Layout"),
489            LayoutNodeKind::Subcompose => f.write_str("Subcompose"),
490            LayoutNodeKind::Spacer => f.write_str("Spacer"),
491            LayoutNodeKind::Button { .. } => f.write_str("Button"),
492            LayoutNodeKind::Unknown => f.write_str("Unknown"),
493        }
494    }
495}
496
497/// Extension trait that equips `MemoryApplier` with layout computation.
498pub trait LayoutEngine {
499    fn compute_layout(&mut self, root: NodeId, max_size: Size) -> Result<LayoutTree, NodeError>;
500}
501
502impl LayoutEngine for MemoryApplier {
503    fn compute_layout(&mut self, root: NodeId, max_size: Size) -> Result<LayoutTree, NodeError> {
504        let measurements = measure_layout(self, root, max_size)?;
505        measurements
506            .into_layout_tree()
507            .ok_or(NodeError::MissingContext {
508                id: root,
509                reason: "layout tree was not requested",
510            })
511    }
512}
513
514/// Result of running the measure pass for a Compose layout tree.
515#[derive(Debug, Clone)]
516pub struct LayoutMeasurements {
517    root: Rc<MeasuredNode>,
518    semantics: Option<SemanticsTree>,
519    layout_tree: Option<LayoutTree>,
520}
521
522impl LayoutMeasurements {
523    fn new(
524        root: Rc<MeasuredNode>,
525        semantics: Option<SemanticsTree>,
526        layout_tree: Option<LayoutTree>,
527    ) -> Self {
528        Self {
529            root,
530            semantics,
531            layout_tree,
532        }
533    }
534
535    /// Returns the measured size of the root node.
536    pub fn root_size(&self) -> Size {
537        self.root.size
538    }
539
540    pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
541        self.semantics.as_ref()
542    }
543
544    pub fn debug_allocation_stats(&self) -> LayoutAllocationDebugStats {
545        let mut stats = self
546            .layout_tree
547            .as_ref()
548            .map(LayoutTree::debug_allocation_stats)
549            .unwrap_or_default();
550        if let Some(semantics) = &self.semantics {
551            record_semantics_allocation_stats(semantics.root(), &mut stats);
552        }
553        stats
554    }
555
556    /// Consumes the measurements and returns the built [`LayoutTree`], if requested.
557    pub fn into_layout_tree(self) -> Option<LayoutTree> {
558        self.layout_tree
559    }
560
561    /// Returns a cloned [`LayoutTree`] for rendering/debug consumers, if requested.
562    pub fn layout_tree(&self) -> Option<LayoutTree> {
563        self.layout_tree.clone()
564    }
565}
566
567/// Builds a semantics tree from an existing [`LayoutTree`].
568///
569/// This is useful for consumers that need semantics on demand without forcing
570/// every layout pass to eagerly allocate a full [`SemanticsTree`].
571pub fn build_semantics_tree_from_layout_tree(layout_tree: &LayoutTree) -> SemanticsTree {
572    SemanticsTree::new(build_semantics_node_from_layout_box(layout_tree.root()))
573}
574
575/// Builds a layout snapshot from retained layout state in the live applier tree.
576///
577/// Renderers use retained node state directly. This function exists for debug,
578/// robot, and tests that need an owned [`LayoutTree`] without forcing every
579/// layout pass to allocate one.
580pub fn build_layout_tree_from_applier(
581    applier: &mut MemoryApplier,
582    root: NodeId,
583) -> Result<Option<LayoutTree>, NodeError> {
584    fn snapshot(
585        applier: &mut MemoryApplier,
586        node_id: NodeId,
587    ) -> Result<Option<(crate::widgets::nodes::layout_node::LayoutState, Vec<NodeId>)>, NodeError>
588    {
589        match applier.with_node::<LayoutNode, _>(node_id, |node| {
590            (node.layout_state(), node.children.clone())
591        }) {
592            Ok(snapshot) => return Ok(Some(snapshot)),
593            Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => {}
594            Err(err) => return Err(err),
595        }
596
597        match applier.with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
598            (node.layout_state(), node.active_children())
599        }) {
600            Ok(snapshot) => Ok(Some(snapshot)),
601            Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => Ok(None),
602            Err(err) => Err(err),
603        }
604    }
605
606    fn place(
607        applier: &mut MemoryApplier,
608        node_id: NodeId,
609        parent_content_origin: Point,
610    ) -> Result<Option<LayoutBox>, NodeError> {
611        let Some((state, child_ids)) = snapshot(applier, node_id)? else {
612            return Ok(None);
613        };
614        if !state.is_placed {
615            return Ok(None);
616        }
617
618        let top_left = Point {
619            x: parent_content_origin.x + state.position.x,
620            y: parent_content_origin.y + state.position.y,
621        };
622        let rect = GeometryRect {
623            x: top_left.x,
624            y: top_left.y,
625            width: state.size.width,
626            height: state.size.height,
627        };
628        let info = runtime_metadata_for(applier, node_id)?;
629        let kind = layout_kind_from_metadata(node_id, &info);
630        let RuntimeNodeMetadata {
631            modifier,
632            resolved_modifiers,
633            modifier_slices,
634            ..
635        } = info;
636        let data = LayoutNodeData::new(modifier, resolved_modifiers, modifier_slices, kind);
637        let child_origin = Point {
638            x: top_left.x + state.content_offset.x,
639            y: top_left.y + state.content_offset.y,
640        };
641        let mut children = Vec::with_capacity(child_ids.len());
642        for child_id in child_ids {
643            if let Some(child) = place(applier, child_id, child_origin)? {
644                children.push(child);
645            }
646        }
647
648        Ok(Some(LayoutBox::new(
649            node_id,
650            rect,
651            state.content_offset,
652            data,
653            children,
654        )))
655    }
656
657    place(applier, root, Point::default()).map(|root| root.map(LayoutTree::new))
658}
659
660/// Builds a semantics snapshot from retained layout state in the live applier tree.
661///
662/// This is the on-demand counterpart to [`build_layout_tree_from_applier`].
663/// It follows the currently placed child set, including subcompose active
664/// children, and clears semantics dirty flags for nodes it visits.
665pub fn build_semantics_tree_from_applier(
666    applier: &mut MemoryApplier,
667    root: NodeId,
668) -> Result<Option<SemanticsTree>, NodeError> {
669    fn node(
670        applier: &mut MemoryApplier,
671        node_id: NodeId,
672    ) -> Result<Option<SemanticsNode>, NodeError> {
673        match applier.with_node::<LayoutNode, _>(node_id, |layout| {
674            let state = layout.layout_state();
675            if !state.is_placed {
676                return None;
677            }
678            let role = role_from_modifier_slices(&layout.modifier_slices_snapshot());
679            let config = layout.semantics_configuration();
680            let children = layout.children.clone();
681            layout.clear_needs_semantics();
682            Some((role, config, children))
683        }) {
684            Ok(Some((role, config, child_ids))) => {
685                let mut children = Vec::with_capacity(child_ids.len());
686                for child_id in child_ids {
687                    if let Some(child) = node(applier, child_id)? {
688                        children.push(child);
689                    }
690                }
691                return Ok(Some(semantics_node_from_parts(
692                    node_id, role, config, children,
693                )));
694            }
695            Ok(None) => return Ok(None),
696            Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => {}
697            Err(err) => return Err(err),
698        }
699
700        match applier.with_node::<SubcomposeLayoutNode, _>(node_id, |subcompose| {
701            let state = subcompose.layout_state();
702            if !state.is_placed {
703                return None;
704            }
705            let config = collect_semantics_from_modifier(&subcompose.modifier());
706            let children = subcompose.active_children();
707            subcompose.clear_needs_semantics();
708            Some((config, children))
709        }) {
710            Ok(Some((config, child_ids))) => {
711                let mut children = Vec::with_capacity(child_ids.len());
712                for child_id in child_ids {
713                    if let Some(child) = node(applier, child_id)? {
714                        children.push(child);
715                    }
716                }
717                Ok(Some(semantics_node_from_parts(
718                    node_id,
719                    SemanticsRole::Subcompose,
720                    config,
721                    children,
722                )))
723            }
724            Ok(None) | Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => {
725                Ok(None)
726            }
727            Err(err) => Err(err),
728        }
729    }
730
731    node(applier, root).map(|root| root.map(SemanticsTree::new))
732}
733
734#[derive(Clone, Copy, Debug, PartialEq, Eq)]
735pub struct MeasureLayoutOptions {
736    pub collect_semantics: bool,
737    pub build_layout_tree: bool,
738}
739
740impl Default for MeasureLayoutOptions {
741    fn default() -> Self {
742        Self {
743            collect_semantics: true,
744            build_layout_tree: true,
745        }
746    }
747}
748
749/// Check if a node or any of its descendants needs measure (selective measure optimization).
750/// This can be used by the app shell to skip layout when the tree is clean.
751///
752/// O(1) check - just looks at root's dirty flag.
753/// Works because all mutation paths bubble dirty flags to root via composer commands.
754///
755/// Returns Result to force caller to handle errors explicitly. No more unwrap_or(true) safety net.
756pub fn tree_needs_layout(applier: &mut dyn Applier, root: NodeId) -> Result<bool, NodeError> {
757    Ok(applier.get_mut(root)?.needs_layout())
758}
759
760/// Check if the root semantics snapshot is dirty.
761///
762/// Semantics invalidations bubble to the root the same way layout invalidations do,
763/// so a root check is sufficient to determine whether the next layout pass needs to
764/// rebuild semantic data even when geometry is otherwise unchanged.
765pub fn tree_needs_semantics(applier: &mut dyn Applier, root: NodeId) -> Result<bool, NodeError> {
766    Ok(applier.get_mut(root)?.needs_semantics())
767}
768
769/// Test helper: bubbles layout dirty flag to root.
770#[cfg(test)]
771pub(crate) fn bubble_layout_dirty(applier: &mut MemoryApplier, node_id: NodeId) {
772    cranpose_core::bubble_layout_dirty(applier as &mut dyn Applier, node_id);
773}
774
775/// Runs the measure phase for the subtree rooted at `root`.
776pub fn measure_layout(
777    applier: &mut MemoryApplier,
778    root: NodeId,
779    max_size: Size,
780) -> Result<LayoutMeasurements, NodeError> {
781    measure_layout_with_options(applier, root, max_size, MeasureLayoutOptions::default())
782}
783
784pub fn measure_layout_with_options(
785    applier: &mut MemoryApplier,
786    root: NodeId,
787    max_size: Size,
788    options: MeasureLayoutOptions,
789) -> Result<LayoutMeasurements, NodeError> {
790    process_pending_layout_repasses(applier, root)?;
791
792    let constraints = Constraints {
793        min_width: 0.0,
794        max_width: max_size.width,
795        min_height: 0.0,
796        max_height: max_size.height,
797    };
798
799    // Selective measure: only increment epoch if something needs MEASURING (not just layout)
800    // O(1) check - just look at root's dirty flag (bubbling ensures correctness)
801    //
802    // CRITICAL: We check needs_MEASURE, not needs_LAYOUT!
803    // - needs_measure: size may change, caches must be invalidated
804    // - needs_layout: position may change but size is cached (e.g., scroll)
805    //
806    // Scroll operations bubble needs_layout to ancestors, but NOT needs_measure.
807    // Using needs_layout here would wipe ALL caches on every scroll frame, causing
808    // O(N) full remeasurement instead of O(changed nodes).
809    let (needs_remeasure, _needs_semantics, cached_epoch) = match applier
810        .with_node::<LayoutNode, _>(root, |node| {
811            (
812                node.needs_measure(), // CORRECT: check needs_measure, not needs_layout
813                node.needs_semantics(),
814                node.cache_handles().epoch(),
815            )
816        }) {
817        Ok(tuple) => tuple,
818        Err(NodeError::TypeMismatch { .. }) => {
819            let node = applier.get_mut(root)?;
820            // Non-LayoutNode roots still expose Node dirty flags.
821            // Use needs_measure here so layout-only subtree repasses can reuse
822            // the existing cache epoch instead of invalidating the whole tree.
823            let measure_dirty = node.needs_measure();
824            let semantics_dirty = node.needs_semantics();
825            (measure_dirty, semantics_dirty, 0)
826        }
827        Err(err) => return Err(err),
828    };
829
830    let epoch = if needs_remeasure {
831        crate::render_state::next_layout_cache_epoch()
832    } else if cached_epoch != 0 {
833        cached_epoch
834    } else {
835        // Fallback when caller root isn't a LayoutNode (e.g. tests using Spacer directly).
836        crate::render_state::current_layout_cache_epoch()
837    };
838
839    // Move the current applier into a host and set up a guard that will
840    // ALWAYS restore:
841    // - the MemoryApplier back into `applier`
842    // - the SlotTable back into that MemoryApplier
843    //
844    // IMPORTANT: Declare the guard *before* the builder so the builder
845    // is dropped first (both on Ok and on unwind).
846    let guard = ApplierSlotGuard::new(applier);
847    let applier_host = guard.host();
848    let slots_handle = guard.slots_handle();
849
850    // Give the builder the shared slots handle - both guard and builder
851    // now share access to the same SlotTable via Rc<RefCell<_>>.
852    let frame_arena = crate::render_state::take_layout_frame_arena();
853    let mut builder = LayoutBuilder::new_with_epoch(
854        Rc::clone(&applier_host),
855        epoch,
856        Rc::clone(&slots_handle),
857        frame_arena,
858    );
859
860    // ---- Measurement -------------------------------------------------------
861    // If measurement fails, the guard will restore slots from the shared handle
862    // on drop - this is safe because the handle always contains valid slots.
863
864    let measured = builder.measure_node(root, normalize_constraints(constraints))?;
865
866    // Root node has no parent to place it, so we must explicitly place it at (0,0).
867    // This ensures is_placed=true, allowing the renderer to traverse the tree.
868    // Handle both LayoutNode and SubcomposeLayoutNode as potential roots.
869    if let Ok(mut applier) = applier_host.try_borrow_typed() {
870        if applier
871            .with_node::<LayoutNode, _>(root, |node| {
872                node.set_position(Point::default());
873            })
874            .is_err()
875        {
876            let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
877                node.set_position(Point::default());
878            });
879        }
880    }
881
882    let (layout_tree, semantics) = {
883        let mut applier_ref = applier_host.borrow_typed();
884        let layout_tree = if options.build_layout_tree {
885            Some(build_layout_tree(&mut applier_ref, &measured)?)
886        } else {
887            None
888        };
889        let semantics = if options.collect_semantics {
890            let semantics_tree = if let Some(layout_tree) = layout_tree.as_ref() {
891                clear_semantics_dirty_flags(&mut applier_ref, &measured)?;
892                build_semantics_tree_from_layout_tree(layout_tree)
893            } else {
894                build_semantics_tree_from_live_nodes(&mut applier_ref, &measured)?
895            };
896            Some(semantics_tree)
897        } else {
898            None
899        };
900        (layout_tree, semantics)
901    };
902
903    // Drop builder before guard - slots are already in the shared handle.
904    // Guard's Drop will write them back to the applier.
905    drop(builder);
906
907    // DO NOT manually unwrap `applier_host` or replace `applier` here.
908    // `ApplierSlotGuard::drop` will restore everything when this function returns.
909
910    Ok(LayoutMeasurements::new(measured, semantics, layout_tree))
911}
912
913fn process_pending_layout_repasses(
914    applier: &mut MemoryApplier,
915    root: NodeId,
916) -> Result<(), NodeError> {
917    for node_id in crate::render_state::take_modifier_slice_repass_nodes() {
918        if let Ok(node) = applier.get_mut(node_id) {
919            let any = node.as_any_mut();
920            if let Some(layout) = any.downcast_mut::<crate::widgets::nodes::LayoutNode>() {
921                layout.mark_modifier_slices_dirty();
922            } else if let Some(subcompose) =
923                any.downcast_mut::<crate::subcompose_layout::SubcomposeLayoutNode>()
924            {
925                subcompose.mark_modifier_slices_dirty();
926            }
927        }
928    }
929    let repass_nodes = crate::take_layout_repass_nodes();
930    if repass_nodes.is_empty() {
931        return Ok(());
932    }
933    for node_id in repass_nodes {
934        cranpose_core::bubble_layout_dirty(applier as &mut dyn Applier, node_id);
935    }
936    applier.get_mut(root)?.mark_needs_layout();
937    Ok(())
938}
939
940struct LayoutBuilder {
941    state: Rc<RefCell<LayoutBuilderState>>,
942}
943
944impl LayoutBuilder {
945    fn new_with_epoch(
946        applier: Rc<ConcreteApplierHost<MemoryApplier>>,
947        epoch: u64,
948        slots: Rc<RefCell<SlotTable>>,
949        frame_arena: FrameLayoutArena,
950    ) -> Self {
951        Self {
952            state: Rc::new(RefCell::new(LayoutBuilderState::new_with_epoch(
953                applier,
954                epoch,
955                slots,
956                frame_arena,
957            ))),
958        }
959    }
960
961    fn measure_node(
962        &mut self,
963        node_id: NodeId,
964        constraints: Constraints,
965    ) -> Result<Rc<MeasuredNode>, NodeError> {
966        LayoutBuilderState::measure_node(Rc::clone(&self.state), node_id, constraints)
967    }
968
969    fn set_runtime_handle(&mut self, handle: Option<RuntimeHandle>) {
970        self.state.borrow_mut().runtime_handle = handle;
971    }
972}
973
974impl Drop for LayoutBuilder {
975    fn drop(&mut self) {
976        if Rc::strong_count(&self.state) != 1 {
977            return;
978        }
979        let Ok(mut state) = self.state.try_borrow_mut() else {
980            return;
981        };
982        crate::render_state::replace_layout_frame_arena(std::mem::take(&mut state.frame_arena));
983    }
984}
985
986struct LayoutBuilderState {
987    applier: Rc<ConcreteApplierHost<MemoryApplier>>,
988    runtime_handle: Option<RuntimeHandle>,
989    /// Shared handle to the slot table. This is shared with ApplierSlotGuard
990    /// to ensure panic-safety: even if we panic, the guard can restore slots.
991    slots: Rc<RefCell<SlotTable>>,
992    cache_epoch: u64,
993    frame_arena: FrameLayoutArena,
994}
995
996struct LayoutRuntimeFrameBindingCleanup {
997    state: Rc<RefCell<LayoutRuntimeState>>,
998}
999
1000impl LayoutRuntimeFrameBindingCleanup {
1001    fn new(state: Rc<RefCell<LayoutRuntimeState>>) -> Self {
1002        Self { state }
1003    }
1004}
1005
1006impl Drop for LayoutRuntimeFrameBindingCleanup {
1007    fn drop(&mut self) {
1008        self.state.borrow().clear_frame_bindings();
1009    }
1010}
1011
1012impl LayoutBuilderState {
1013    fn new_with_epoch(
1014        applier: Rc<ConcreteApplierHost<MemoryApplier>>,
1015        epoch: u64,
1016        slots: Rc<RefCell<SlotTable>>,
1017        frame_arena: FrameLayoutArena,
1018    ) -> Self {
1019        let runtime_handle = applier.borrow_typed().runtime_handle();
1020
1021        Self {
1022            applier,
1023            runtime_handle,
1024            slots,
1025            cache_epoch: epoch,
1026            frame_arena,
1027        }
1028    }
1029
1030    fn try_with_applier_result<R>(
1031        state_rc: &Rc<RefCell<Self>>,
1032        f: impl FnOnce(&mut MemoryApplier) -> Result<R, NodeError>,
1033    ) -> Option<Result<R, NodeError>> {
1034        let host = {
1035            let state = state_rc.borrow();
1036            Rc::clone(&state.applier)
1037        };
1038
1039        // Try to borrow - if already borrowed (nested call), return None
1040        let Ok(mut applier) = host.try_borrow_typed() else {
1041            return None;
1042        };
1043
1044        Some(f(&mut applier))
1045    }
1046
1047    fn with_applier_result<R>(
1048        state_rc: &Rc<RefCell<Self>>,
1049        f: impl FnOnce(&mut MemoryApplier) -> Result<R, NodeError>,
1050    ) -> Result<R, NodeError> {
1051        Self::try_with_applier_result(state_rc, f).unwrap_or_else(|| {
1052            Err(NodeError::MissingContext {
1053                id: NodeId::default(),
1054                reason: "applier already borrowed",
1055            })
1056        })
1057    }
1058
1059    /// Clears the is_placed flag for a node at the start of measurement.
1060    /// This ensures nodes that drop out of placement won't render with stale geometry.
1061    fn clear_node_placed(state_rc: &Rc<RefCell<Self>>, node_id: NodeId) {
1062        let host = {
1063            let state = state_rc.borrow();
1064            Rc::clone(&state.applier)
1065        };
1066        let Ok(mut applier) = host.try_borrow_typed() else {
1067            return;
1068        };
1069        // Try LayoutNode first, then SubcomposeLayoutNode
1070        if applier
1071            .with_node::<LayoutNode, _>(node_id, |node| {
1072                node.clear_placed();
1073            })
1074            .is_err()
1075        {
1076            let _ = applier.with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1077                node.clear_placed();
1078            });
1079        }
1080    }
1081
1082    fn measure_node(
1083        state_rc: Rc<RefCell<Self>>,
1084        node_id: NodeId,
1085        constraints: Constraints,
1086    ) -> Result<Rc<MeasuredNode>, NodeError> {
1087        // Clear is_placed at the start of measurement.
1088        // Nodes that are placed will have is_placed set to true via Placeable::place().
1089        // Nodes that drop out of placement (not placed this pass) will remain is_placed=false.
1090        Self::clear_node_placed(&state_rc, node_id);
1091
1092        // Try SubcomposeLayoutNode first
1093        if let Some(subcompose) =
1094            Self::try_measure_subcompose(Rc::clone(&state_rc), node_id, constraints)?
1095        {
1096            return Ok(subcompose);
1097        }
1098
1099        // Try LayoutNode (the primary modern path)
1100        if let Some(result) = Self::try_with_applier_result(&state_rc, |applier| {
1101            match applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
1102                LayoutNodeSnapshot::from_layout_node(layout_node)
1103            }) {
1104                Ok(snapshot) => Ok(Some(snapshot)),
1105                Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => Ok(None),
1106                Err(err) => Err(err),
1107            }
1108        }) {
1109            // Applier was available, process the result
1110            if let Some(snapshot) = result? {
1111                return Self::measure_layout_node(
1112                    Rc::clone(&state_rc),
1113                    node_id,
1114                    snapshot,
1115                    constraints,
1116                );
1117            }
1118        }
1119        // If applier was busy (None) or snapshot was None, fall through to fallback
1120
1121        // No alternate fallbacks - all widgets use LayoutNode or SubcomposeLayoutNode
1122        // If we reach here, it's an unknown node type (shouldn't happen in normal use)
1123        Ok(Rc::new(MeasuredNode::new(
1124            node_id,
1125            Size::default(),
1126            Point { x: 0.0, y: 0.0 },
1127            Point::default(), // No content offset for fallback nodes
1128            Vec::new(),
1129        )))
1130    }
1131
1132    fn try_measure_subcompose(
1133        state_rc: Rc<RefCell<Self>>,
1134        node_id: NodeId,
1135        constraints: Constraints,
1136    ) -> Result<Option<Rc<MeasuredNode>>, NodeError> {
1137        let applier_host = {
1138            let state = state_rc.borrow();
1139            Rc::clone(&state.applier)
1140        };
1141
1142        let (node_handle, resolved_modifiers) = {
1143            // Try to borrow - if already borrowed (nested measurement), return None
1144            let Ok(mut applier) = applier_host.try_borrow_typed() else {
1145                return Ok(None);
1146            };
1147            let node = match applier.get_mut(node_id) {
1148                Ok(node) => node,
1149                Err(NodeError::Missing { .. }) => return Ok(None),
1150                Err(err) => return Err(err),
1151            };
1152            let any = node.as_any_mut();
1153            if let Some(subcompose) =
1154                any.downcast_mut::<crate::subcompose_layout::SubcomposeLayoutNode>()
1155            {
1156                let handle = subcompose.handle();
1157                let resolved_modifiers = handle.resolved_modifiers();
1158                (handle, resolved_modifiers)
1159            } else {
1160                return Ok(None);
1161            }
1162        };
1163
1164        let runtime_handle = {
1165            let mut state = state_rc.borrow_mut();
1166            if state.runtime_handle.is_none() {
1167                // Try to borrow - if already borrowed, we can't get runtime handle
1168                if let Ok(applier) = applier_host.try_borrow_typed() {
1169                    state.runtime_handle = applier.runtime_handle();
1170                }
1171            }
1172            state
1173                .runtime_handle
1174                .clone()
1175                .ok_or(NodeError::MissingContext {
1176                    id: node_id,
1177                    reason: "runtime handle required for subcomposition",
1178                })?
1179        };
1180
1181        let props = resolved_modifiers.layout_properties();
1182        let padding = resolved_modifiers.padding();
1183        let offset = resolved_modifiers.offset();
1184        let mut inner_constraints = normalize_constraints(subtract_padding(constraints, padding));
1185
1186        if let DimensionConstraint::Points(width) = props.width() {
1187            let constrained_width = width - padding.horizontal_sum();
1188            inner_constraints.max_width = inner_constraints.max_width.min(constrained_width);
1189            inner_constraints.min_width = inner_constraints.min_width.min(constrained_width);
1190        }
1191        if let DimensionConstraint::Points(height) = props.height() {
1192            let constrained_height = height - padding.vertical_sum();
1193            inner_constraints.max_height = inner_constraints.max_height.min(constrained_height);
1194            inner_constraints.min_height = inner_constraints.min_height.min(constrained_height);
1195        }
1196
1197        let mut slots_guard = SlotsGuard::take(Rc::clone(&state_rc));
1198        let slots_host = slots_guard.host();
1199        let applier_host_dyn: Rc<dyn ApplierHost> = applier_host.clone();
1200        let observer = SnapshotStateObserver::new(|callback| callback());
1201        let composer = Composer::new(
1202            Rc::clone(&slots_host),
1203            applier_host_dyn,
1204            runtime_handle.clone(),
1205            observer,
1206            Some(node_id),
1207        );
1208        composer.enter_phase(Phase::Measure);
1209
1210        let state_rc_clone = Rc::clone(&state_rc);
1211        let measure_error = RefCell::new(None);
1212        let state_rc_for_subcompose = Rc::clone(&state_rc_clone);
1213        let error_for_subcompose = &measure_error;
1214        let measured_children = node_handle.measured_children_scratch();
1215        let measured_children_for_subcompose = Rc::clone(&measured_children);
1216
1217        let measure_result = node_handle.measure(
1218            &composer,
1219            node_id,
1220            inner_constraints,
1221            Box::new(
1222                move |child_id: NodeId, child_constraints: Constraints| -> Size {
1223                    match Self::measure_node(
1224                        Rc::clone(&state_rc_for_subcompose),
1225                        child_id,
1226                        child_constraints,
1227                    ) {
1228                        Ok(measured) => {
1229                            measured_children_for_subcompose
1230                                .borrow_mut()
1231                                .insert(child_id, Rc::clone(&measured));
1232                            measured.size
1233                        }
1234                        Err(err) => {
1235                            let mut slot = error_for_subcompose.borrow_mut();
1236                            if slot.is_none() {
1237                                *slot = Some(err);
1238                            }
1239                            Size::default()
1240                        }
1241                    }
1242                },
1243            ),
1244            &measure_error,
1245        )?;
1246        drop(composer);
1247        slots_guard.restore(slots_host.into_table()?);
1248
1249        if let Some(err) = measure_error.borrow_mut().take() {
1250            return Err(err);
1251        }
1252
1253        // NOTE: Children are now managed by the composer via insert_child commands
1254        // (from parent_stack initialization with root). set_active_children is no longer used.
1255
1256        let cranpose_ui_layout::MeasureResult {
1257            size: measured_size,
1258            placements,
1259        } = measure_result;
1260
1261        let mut width = measured_size.width + padding.horizontal_sum();
1262        let mut height = measured_size.height + padding.vertical_sum();
1263
1264        width = resolve_dimension(
1265            width,
1266            props.width(),
1267            props.min_width(),
1268            props.max_width(),
1269            constraints.min_width,
1270            constraints.max_width,
1271        );
1272        height = resolve_dimension(
1273            height,
1274            props.height(),
1275            props.min_height(),
1276            props.max_height(),
1277            constraints.min_height,
1278            constraints.max_height,
1279        );
1280
1281        let mut children = Vec::with_capacity(placements.len());
1282        let mut measured_children_by_id = measured_children.borrow_mut();
1283
1284        // Update the SubcomposeLayoutNode's size (position will be set by parent's placement)
1285        if let Ok(mut applier) = applier_host.try_borrow_typed() {
1286            let _ = applier.with_node::<SubcomposeLayoutNode, _>(node_id, |parent_node| {
1287                parent_node.set_measured_size(Size { width, height });
1288                parent_node.clear_needs_measure();
1289                parent_node.clear_needs_layout();
1290            });
1291        }
1292
1293        for placement in &placements {
1294            let child = if let Some(measured) = measured_children_by_id.remove(&placement.node_id) {
1295                measured
1296            } else {
1297                // Policies may place subcomposed children without calling `measure()` first
1298                // (for example, when they only need a slot's rendered content). Keep the
1299                // existing fallback for that case, but preserve the policy-time measurement
1300                // whenever it exists so we don't silently remeasure lazy items with the
1301                // container's tighter constraints.
1302                Self::measure_node(Rc::clone(&state_rc), placement.node_id, inner_constraints)?
1303            };
1304            let position = Point {
1305                x: padding.left + placement.x,
1306                y: padding.top + placement.y,
1307            };
1308
1309            // Critical: Update the child LayoutNode's retained state.
1310            // Standard layouts do this via Placeable::place(), but SubcomposeLayout logic
1311            // bypasses Placeables and returns raw Placements.
1312            if let Ok(mut applier) = applier_host.try_borrow_typed() {
1313                let _ = applier.with_node::<LayoutNode, _>(placement.node_id, |node| {
1314                    node.set_position(position);
1315                });
1316            }
1317
1318            children.push(MeasuredChild {
1319                node: child,
1320                offset: position,
1321            });
1322        }
1323
1324        // Update the SubcomposeLayoutNode's active children for rendering
1325        node_handle.set_active_children(children.iter().map(|c| c.node.node_id));
1326        node_handle.recycle_placement_scratch(placements);
1327
1328        Ok(Some(Rc::new(MeasuredNode::new(
1329            node_id,
1330            Size { width, height },
1331            offset,
1332            Point::default(), // Subcompose nodes: content_offset handled by child layout
1333            children,
1334        ))))
1335    }
1336    /// Measures through the layout modifier coordinator chain using reconciled modifier nodes.
1337    /// Iterates through LayoutModifierNode instances from the ModifierNodeChain and calls
1338    /// their measure() methods through the retained coordinator chain.
1339    ///
1340    /// Always succeeds, measuring either directly or through retained layout modifier nodes.
1341    ///
1342    fn measure_through_modifier_chain(
1343        state_rc: &Rc<RefCell<Self>>,
1344        node_id: NodeId,
1345        runtime_state: &mut LayoutRuntimeState,
1346        measure_policy: &Rc<dyn MeasurePolicy>,
1347        constraints: Constraints,
1348        layout_node_data: &mut Vec<LayoutModifierNodeData>,
1349        placements: &mut Vec<Placement>,
1350    ) -> ModifierChainMeasurement {
1351        use cranpose_foundation::NodeCapabilities;
1352
1353        // Collect layout node information from the modifier chain
1354        layout_node_data.clear();
1355        let mut offset = Point::default();
1356
1357        {
1358            let state = state_rc.borrow();
1359            let mut applier = state.applier.borrow_typed();
1360
1361            let _ = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
1362                let chain_handle = layout_node.modifier_chain();
1363
1364                if !chain_handle.has_layout_nodes() {
1365                    return;
1366                }
1367
1368                // Collect indices and node Rc clones for layout modifier nodes
1369                chain_handle.chain().for_each_forward_matching(
1370                    NodeCapabilities::LAYOUT,
1371                    |node_ref| {
1372                        if let Some(index) = node_ref.entry_index() {
1373                            // Get the Rc clone for this node
1374                            if let Some(node_rc) = chain_handle.chain().get_node_rc(index) {
1375                                layout_node_data.push((index, Rc::clone(&node_rc)));
1376                            }
1377
1378                            // Extract offset from OffsetNode for the node's own position
1379                            // The coordinator chain handles placement_offset (for children),
1380                            // but the node's offset affects where IT is positioned in the parent
1381                            node_ref.with_node(|node| {
1382                                if let Some(offset_node) =
1383                                    node.as_any()
1384                                        .downcast_ref::<crate::modifier_nodes::OffsetNode>()
1385                                {
1386                                    let delta = offset_node.offset();
1387                                    offset.x += delta.x;
1388                                    offset.y += delta.y;
1389                                }
1390                            });
1391                        }
1392                    },
1393                );
1394            });
1395        }
1396
1397        // Fast path: if there are no layout modifiers, measure directly without the
1398        // retained coordinator chain frame.
1399        if layout_node_data.is_empty() {
1400            let final_size = measure_policy.measure_into(
1401                runtime_state.child_measurables(),
1402                constraints,
1403                placements,
1404            );
1405
1406            return ModifierChainMeasurement {
1407                size: final_size,
1408                content_offset: Point::default(),
1409                offset,
1410            };
1411        }
1412
1413        runtime_state.reconcile_coordinator_chain(layout_node_data.as_slice());
1414        let frame = CoordinatorFrame::new(
1415            measure_policy,
1416            runtime_state.child_measurables(),
1417            placements,
1418        );
1419
1420        // Measure through the complete coordinator chain
1421        let placeable = runtime_state
1422            .coordinator_chain()
1423            .measure_from(0, &frame, constraints);
1424        let final_size = Size {
1425            width: placeable.width(),
1426            height: placeable.height(),
1427        };
1428
1429        // Get accumulated content offset from the placeable (computed during measure)
1430        let content_offset = placeable.content_offset();
1431        let all_placement_offset = Point {
1432            x: content_offset.0,
1433            y: content_offset.1,
1434        };
1435
1436        // The content_offset for scroll/inner transforms is the accumulated placement offset
1437        // MINUS the node's own offset (which affects its position in the parent, not content position).
1438        // This properly separates: node position (offset) vs inner content position (content_offset).
1439        let content_offset = Point {
1440            x: all_placement_offset.x - offset.x,
1441            y: all_placement_offset.y - offset.y,
1442        };
1443
1444        // offset was already extracted from OffsetNode above
1445
1446        // Process any invalidations requested during measurement
1447        let invalidations = frame.take_invalidations();
1448        if !invalidations.is_empty() {
1449            // Mark the LayoutNode as needing the appropriate passes
1450            Self::with_applier_result(state_rc, |applier| {
1451                applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
1452                    for kind in invalidations {
1453                        match kind {
1454                            InvalidationKind::Layout => layout_node.mark_needs_measure(),
1455                            InvalidationKind::Draw => layout_node.mark_needs_redraw(),
1456                            InvalidationKind::Semantics => layout_node.mark_needs_semantics(),
1457                            InvalidationKind::PointerInput => layout_node.mark_needs_pointer_pass(),
1458                            InvalidationKind::Focus => layout_node.mark_needs_focus_sync(),
1459                        }
1460                    }
1461                })
1462            })
1463            .ok();
1464        }
1465
1466        ModifierChainMeasurement {
1467            size: final_size,
1468            content_offset,
1469            offset,
1470        }
1471    }
1472
1473    fn layout_child_measure_data(
1474        applier: &mut MemoryApplier,
1475        child_id: NodeId,
1476    ) -> Result<Option<LayoutChildMeasureData>, NodeError> {
1477        match applier.with_node::<LayoutNode, _>(child_id, |n| LayoutChildMeasureData {
1478            cache: n.cache_handles(),
1479            layout_state: Some(n.layout_state_handle()),
1480            needs_layout: n.needs_layout(),
1481            needs_measure: n.needs_measure(),
1482        }) {
1483            Ok(data) => Ok(Some(data)),
1484            Err(NodeError::TypeMismatch { .. }) => {
1485                match applier.with_node::<SubcomposeLayoutNode, _>(child_id, |n| {
1486                    LayoutChildMeasureData {
1487                        cache: LayoutNodeCacheHandles::default(),
1488                        layout_state: None,
1489                        needs_layout: n.needs_layout(),
1490                        needs_measure: n.needs_measure(),
1491                    }
1492                }) {
1493                    Ok(data) => Ok(Some(data)),
1494                    Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => {
1495                        Ok(None)
1496                    }
1497                    Err(err) => Err(err),
1498                }
1499            }
1500            Err(NodeError::Missing { .. }) => Ok(None),
1501            Err(err) => Err(err),
1502        }
1503    }
1504
1505    fn measure_layout_node(
1506        state_rc: Rc<RefCell<Self>>,
1507        node_id: NodeId,
1508        snapshot: LayoutNodeSnapshot,
1509        constraints: Constraints,
1510    ) -> Result<Rc<MeasuredNode>, NodeError> {
1511        let cache_epoch = {
1512            let state = state_rc.borrow();
1513            state.cache_epoch
1514        };
1515        let LayoutNodeSnapshot {
1516            measure_policy,
1517            cache,
1518            layout_runtime_state,
1519            needs_layout,
1520            needs_measure,
1521        } = snapshot;
1522        cache.activate(cache_epoch);
1523
1524        if needs_measure {
1525            // Node has needs_measure=true
1526        }
1527
1528        // Only check cache when the node is fully clean.
1529        // needs_layout=true means either the node itself or one of its descendants
1530        // must be revisited even if the node's own measured size can stay cached.
1531        if !needs_measure && !needs_layout {
1532            // Check cache for current constraints
1533            if let Some(cached) = cache.get_measurement(constraints) {
1534                // Clear dirty flag after successful cache hit
1535                Self::with_applier_result(&state_rc, |applier| {
1536                    applier.with_node::<LayoutNode, _>(node_id, |node| {
1537                        node.clear_needs_measure();
1538                        node.clear_needs_layout();
1539                    })
1540                })
1541                .ok();
1542                return Ok(cached);
1543            }
1544        }
1545
1546        let (runtime_handle, applier_host) = {
1547            let state = state_rc.borrow();
1548            (state.runtime_handle.clone(), Rc::clone(&state.applier))
1549        };
1550
1551        let measure_handle = LayoutMeasureHandle::new(Rc::clone(&state_rc));
1552        let error = Rc::new(RefCell::new(None));
1553        let mut pools = VecPools::acquire(Rc::clone(&state_rc));
1554        let (records, child_ids, layout_node_data, placements) = pools.parts();
1555
1556        applier_host
1557            .borrow_typed()
1558            .with_node::<LayoutNode, _>(node_id, |node| {
1559                child_ids.extend_from_slice(&node.children);
1560            })?;
1561
1562        let mut valid_child_count = 0;
1563        for index in 0..child_ids.len() {
1564            let child_id = child_ids[index];
1565            let child_exists = {
1566                let mut applier = applier_host.borrow_typed();
1567                Self::layout_child_measure_data(&mut applier, child_id)?.is_some()
1568            };
1569            if child_exists {
1570                child_ids[valid_child_count] = child_id;
1571                valid_child_count += 1;
1572            }
1573        }
1574        child_ids.truncate(valid_child_count);
1575
1576        let _frame_binding_cleanup =
1577            LayoutRuntimeFrameBindingCleanup::new(Rc::clone(&layout_runtime_state));
1578
1579        {
1580            let mut runtime_state = layout_runtime_state.borrow_mut();
1581            runtime_state.reconcile_child_measurables(child_ids.as_slice());
1582
1583            for (index, &child_id) in child_ids.iter().enumerate() {
1584                let data = {
1585                    let mut applier = applier_host.borrow_typed();
1586                    Self::layout_child_measure_data(&mut applier, child_id)?
1587                };
1588                let Some(data) = data else {
1589                    continue;
1590                };
1591
1592                let child_state = runtime_state.child_state(index);
1593                child_state.configure(LayoutChildMeasureConfig {
1594                    applier: Rc::clone(&applier_host),
1595                    node_id: child_id,
1596                    error: Rc::clone(&error),
1597                    runtime_handle: runtime_handle.clone(),
1598                    cache: data.cache,
1599                    cache_epoch,
1600                    force_remeasure: data.needs_layout || data.needs_measure,
1601                    measure_handle: Some(measure_handle.clone()),
1602                    layout_state: data.layout_state,
1603                });
1604                records.push((child_id, ChildRecord { state: child_state }));
1605            }
1606        }
1607
1608        let chain_constraints = constraints;
1609
1610        let modifier_chain_result = {
1611            let mut runtime_state = layout_runtime_state.borrow_mut();
1612            Self::measure_through_modifier_chain(
1613                &state_rc,
1614                node_id,
1615                &mut runtime_state,
1616                &measure_policy,
1617                chain_constraints,
1618                layout_node_data,
1619                placements,
1620            )
1621        };
1622
1623        // Modifier chain always succeeds - use the node-driven measurement.
1624        let (width, height, content_offset, offset) = {
1625            let result = modifier_chain_result;
1626            // The size is already correct from the modifier chain (modifiers like SizeNode
1627            // have already enforced their constraints), so we use it directly.
1628            if let Some(err) = error.borrow_mut().take() {
1629                return Err(err);
1630            }
1631
1632            (
1633                result.size.width,
1634                result.size.height,
1635                result.content_offset,
1636                result.offset,
1637            )
1638        };
1639
1640        let mut measured_children = Vec::with_capacity(records.len());
1641        for (child_id, record) in records.iter() {
1642            if let Some(measured) = record.state.take_measured() {
1643                let base_position = placements
1644                    .iter()
1645                    .find(|placement| placement.node_id == *child_id)
1646                    .map(|placement| Point {
1647                        x: placement.x,
1648                        y: placement.y,
1649                    })
1650                    .or_else(|| record.state.last_position())
1651                    .unwrap_or(Point { x: 0.0, y: 0.0 });
1652                // Apply content_offset (from scroll/transforms) to child positioning
1653                let position = Point {
1654                    x: content_offset.x + base_position.x,
1655                    y: content_offset.y + base_position.y,
1656                };
1657                measured_children.push(MeasuredChild {
1658                    node: measured,
1659                    offset: position,
1660                });
1661            }
1662        }
1663
1664        let measured = Rc::new(MeasuredNode::new(
1665            node_id,
1666            Size { width, height },
1667            offset,
1668            content_offset,
1669            measured_children,
1670        ));
1671
1672        cache.store_measurement(constraints, Rc::clone(&measured));
1673
1674        // Clear dirty flags and update derived state
1675        Self::with_applier_result(&state_rc, |applier| {
1676            applier.with_node::<LayoutNode, _>(node_id, |node| {
1677                node.clear_needs_measure();
1678                node.clear_needs_layout();
1679                node.set_measured_size(Size { width, height });
1680                node.set_content_offset(content_offset);
1681            })
1682        })
1683        .ok();
1684
1685        Ok(measured)
1686    }
1687}
1688
1689struct LayoutChildMeasureData {
1690    cache: LayoutNodeCacheHandles,
1691    layout_state: Option<Rc<RefCell<LayoutState>>>,
1692    needs_layout: bool,
1693    needs_measure: bool,
1694}
1695
1696/// Snapshot of a LayoutNode's data for measuring.
1697/// This is a temporary copy used during the measure phase, not a live node.
1698///
1699/// Note: We capture `needs_measure` here because it's checked during measure to enable
1700/// selective measure optimization at the individual node level. Even if the tree is partially
1701/// dirty (some nodes changed), clean nodes can skip measure and use cached results.
1702struct LayoutNodeSnapshot {
1703    measure_policy: Rc<dyn MeasurePolicy>,
1704    cache: LayoutNodeCacheHandles,
1705    layout_runtime_state: Rc<RefCell<LayoutRuntimeState>>,
1706    needs_layout: bool,
1707    /// Whether this specific node needs to be measured (vs using cached measurement)
1708    needs_measure: bool,
1709}
1710
1711impl LayoutNodeSnapshot {
1712    fn from_layout_node(node: &LayoutNode) -> Self {
1713        Self {
1714            measure_policy: Rc::clone(&node.measure_policy),
1715            cache: node.cache_handles(),
1716            layout_runtime_state: node.layout_runtime_state_handle(),
1717            needs_layout: node.needs_layout(),
1718            needs_measure: node.needs_measure(),
1719        }
1720    }
1721}
1722
1723// Helper types for accessing subsets of LayoutBuilderState
1724struct VecPools {
1725    state: Rc<RefCell<LayoutBuilderState>>,
1726    records: Vec<(NodeId, ChildRecord)>,
1727    child_ids: Vec<NodeId>,
1728    layout_node_data: Vec<LayoutModifierNodeData>,
1729    placements: Vec<Placement>,
1730}
1731
1732impl VecPools {
1733    fn acquire(state: Rc<RefCell<LayoutBuilderState>>) -> Self {
1734        let (records, child_ids, layout_node_data, placements) = {
1735            let mut state_mut = state.borrow_mut();
1736            (
1737                state_mut.frame_arena.tmp_records.acquire(),
1738                state_mut.frame_arena.tmp_child_ids.acquire(),
1739                state_mut.frame_arena.tmp_layout_node_data.acquire(),
1740                state_mut.frame_arena.tmp_placements.acquire(),
1741            )
1742        };
1743        Self {
1744            state,
1745            records,
1746            child_ids,
1747            layout_node_data,
1748            placements,
1749        }
1750    }
1751
1752    #[allow(clippy::type_complexity)] // Returns internal Vec references for layout operations
1753    fn parts(
1754        &mut self,
1755    ) -> (
1756        &mut Vec<(NodeId, ChildRecord)>,
1757        &mut Vec<NodeId>,
1758        &mut Vec<LayoutModifierNodeData>,
1759        &mut Vec<Placement>,
1760    ) {
1761        (
1762            &mut self.records,
1763            &mut self.child_ids,
1764            &mut self.layout_node_data,
1765            &mut self.placements,
1766        )
1767    }
1768}
1769
1770impl Drop for VecPools {
1771    fn drop(&mut self) {
1772        let mut state = self.state.borrow_mut();
1773        state
1774            .frame_arena
1775            .tmp_records
1776            .release(std::mem::take(&mut self.records));
1777        state
1778            .frame_arena
1779            .tmp_child_ids
1780            .release(std::mem::take(&mut self.child_ids));
1781        state
1782            .frame_arena
1783            .tmp_layout_node_data
1784            .release(std::mem::take(&mut self.layout_node_data));
1785        state
1786            .frame_arena
1787            .tmp_placements
1788            .release(std::mem::take(&mut self.placements));
1789    }
1790}
1791
1792struct SlotsGuard {
1793    state: Rc<RefCell<LayoutBuilderState>>,
1794    slots: Option<SlotTable>,
1795}
1796
1797impl SlotsGuard {
1798    fn take(state: Rc<RefCell<LayoutBuilderState>>) -> Self {
1799        let slots = {
1800            let state_ref = state.borrow();
1801            let mut slots_ref = state_ref.slots.borrow_mut();
1802            std::mem::take(&mut *slots_ref)
1803        };
1804        Self {
1805            state,
1806            slots: Some(slots),
1807        }
1808    }
1809
1810    fn host(&mut self) -> Rc<SlotsHost> {
1811        let slots = self.slots.take().unwrap_or_default();
1812        Rc::new(SlotsHost::new(slots))
1813    }
1814
1815    fn restore(&mut self, slots: SlotTable) {
1816        debug_assert!(self.slots.is_none());
1817        self.slots = Some(slots);
1818    }
1819}
1820
1821impl Drop for SlotsGuard {
1822    fn drop(&mut self) {
1823        if let Some(slots) = self.slots.take() {
1824            let state_ref = self.state.borrow();
1825            *state_ref.slots.borrow_mut() = slots;
1826        }
1827    }
1828}
1829
1830#[derive(Clone)]
1831struct LayoutMeasureHandle {
1832    state: Rc<RefCell<LayoutBuilderState>>,
1833}
1834
1835impl LayoutMeasureHandle {
1836    fn new(state: Rc<RefCell<LayoutBuilderState>>) -> Self {
1837        Self { state }
1838    }
1839
1840    fn measure(
1841        &self,
1842        node_id: NodeId,
1843        constraints: Constraints,
1844    ) -> Result<Rc<MeasuredNode>, NodeError> {
1845        LayoutBuilderState::measure_node(Rc::clone(&self.state), node_id, constraints)
1846    }
1847}
1848
1849#[derive(Debug, Clone)]
1850pub(crate) struct MeasuredNode {
1851    node_id: NodeId,
1852    size: Size,
1853    /// Node's position offset relative to parent (from OffsetNode etc.)
1854    offset: Point,
1855    /// Content offset for scroll/inner transforms (NOT node position)
1856    content_offset: Point,
1857    children: Vec<MeasuredChild>,
1858}
1859
1860impl MeasuredNode {
1861    fn new(
1862        node_id: NodeId,
1863        size: Size,
1864        offset: Point,
1865        content_offset: Point,
1866        children: Vec<MeasuredChild>,
1867    ) -> Self {
1868        Self {
1869            node_id,
1870            size,
1871            offset,
1872            content_offset,
1873            children,
1874        }
1875    }
1876}
1877
1878#[derive(Debug, Clone)]
1879struct MeasuredChild {
1880    node: Rc<MeasuredNode>,
1881    offset: Point,
1882}
1883
1884struct ChildRecord {
1885    state: Rc<LayoutChildMeasureState>,
1886}
1887
1888struct CoordinatorFrame<'a> {
1889    measure_policy: &'a Rc<dyn MeasurePolicy>,
1890    measurables: &'a [Box<dyn Measurable>],
1891    placements: RefCell<&'a mut Vec<Placement>>,
1892    context: RefCell<LayoutNodeContext>,
1893}
1894
1895impl<'a> CoordinatorFrame<'a> {
1896    fn new(
1897        measure_policy: &'a Rc<dyn MeasurePolicy>,
1898        measurables: &'a [Box<dyn Measurable>],
1899        placements: &'a mut Vec<Placement>,
1900    ) -> Self {
1901        Self {
1902            measure_policy,
1903            measurables,
1904            placements: RefCell::new(placements),
1905            context: RefCell::new(LayoutNodeContext::new()),
1906        }
1907    }
1908
1909    fn take_invalidations(&self) -> Vec<InvalidationKind> {
1910        self.context.borrow_mut().take_invalidations()
1911    }
1912}
1913
1914struct CoordinatorLink<'chain, 'frame_ref, 'frame_data> {
1915    chain: &'chain CoordinatorChain,
1916    frame: &'frame_ref CoordinatorFrame<'frame_data>,
1917    index: usize,
1918}
1919
1920impl Measurable for CoordinatorLink<'_, '_, '_> {
1921    fn measure(&self, constraints: Constraints) -> Placeable {
1922        self.chain.measure_from(self.index, self.frame, constraints)
1923    }
1924
1925    fn min_intrinsic_width(&self, height: f32) -> f32 {
1926        self.chain
1927            .min_intrinsic_width_from(self.index, self.frame, height)
1928    }
1929
1930    fn max_intrinsic_width(&self, height: f32) -> f32 {
1931        self.chain
1932            .max_intrinsic_width_from(self.index, self.frame, height)
1933    }
1934
1935    fn min_intrinsic_height(&self, width: f32) -> f32 {
1936        self.chain
1937            .min_intrinsic_height_from(self.index, self.frame, width)
1938    }
1939
1940    fn max_intrinsic_height(&self, width: f32) -> f32 {
1941        self.chain
1942            .max_intrinsic_height_from(self.index, self.frame, width)
1943    }
1944}
1945
1946struct CoordinatorNode {
1947    modifier_index: usize,
1948    node: Rc<RefCell<Box<dyn cranpose_foundation::ModifierNode>>>,
1949    measured_size: Cell<Size>,
1950    accumulated_offset: Cell<Point>,
1951}
1952
1953impl CoordinatorNode {
1954    fn new(
1955        modifier_index: usize,
1956        node: Rc<RefCell<Box<dyn cranpose_foundation::ModifierNode>>>,
1957    ) -> Self {
1958        Self {
1959            modifier_index,
1960            node,
1961            measured_size: Cell::new(Size::default()),
1962            accumulated_offset: Cell::new(Point::default()),
1963        }
1964    }
1965
1966    fn matches(
1967        &self,
1968        modifier_index: usize,
1969        node: &Rc<RefCell<Box<dyn cranpose_foundation::ModifierNode>>>,
1970    ) -> bool {
1971        self.modifier_index == modifier_index && Rc::ptr_eq(&self.node, node)
1972    }
1973
1974    #[cfg(test)]
1975    fn ptr(&self) -> usize {
1976        Rc::as_ptr(&self.node) as *const () as usize
1977    }
1978}
1979
1980#[derive(Default)]
1981struct CoordinatorChain {
1982    nodes: Vec<CoordinatorNode>,
1983}
1984
1985impl CoordinatorChain {
1986    fn reconcile(&mut self, layout_node_data: &[LayoutModifierNodeData]) {
1987        if self.matches(layout_node_data) {
1988            return;
1989        }
1990
1991        let mut previous_nodes = std::mem::take(&mut self.nodes);
1992        self.nodes.reserve(layout_node_data.len());
1993
1994        for (modifier_index, node) in layout_node_data.iter() {
1995            if let Some(position) = previous_nodes
1996                .iter()
1997                .position(|candidate| candidate.matches(*modifier_index, node))
1998            {
1999                self.nodes.push(previous_nodes.swap_remove(position));
2000            } else {
2001                self.nodes
2002                    .push(CoordinatorNode::new(*modifier_index, Rc::clone(node)));
2003            }
2004        }
2005    }
2006
2007    fn matches(&self, layout_node_data: &[LayoutModifierNodeData]) -> bool {
2008        self.nodes.len() == layout_node_data.len()
2009            && self
2010                .nodes
2011                .iter()
2012                .zip(layout_node_data.iter())
2013                .all(|(node, (modifier_index, node_rc))| node.matches(*modifier_index, node_rc))
2014    }
2015
2016    fn measure_from(
2017        &self,
2018        index: usize,
2019        frame: &CoordinatorFrame<'_>,
2020        constraints: Constraints,
2021    ) -> Placeable {
2022        let Some(node) = self.nodes.get(index) else {
2023            let mut placements = frame.placements.borrow_mut();
2024            let size =
2025                frame
2026                    .measure_policy
2027                    .measure_into(frame.measurables, constraints, &mut placements);
2028            return Placeable::value(size.width, size.height, NodeId::default());
2029        };
2030
2031        let wrapped = CoordinatorLink {
2032            chain: self,
2033            frame,
2034            index: index + 1,
2035        };
2036        let node_borrow = node.node.borrow();
2037
2038        let Some(layout_node) = node_borrow.as_layout_node() else {
2039            let placeable = wrapped.measure(constraints);
2040            let child_accumulated = self.total_content_offset_from(index + 1);
2041            node.accumulated_offset.set(child_accumulated);
2042            return Placeable::value_with_offset(
2043                placeable.width(),
2044                placeable.height(),
2045                NodeId::default(),
2046                (child_accumulated.x, child_accumulated.y),
2047            );
2048        };
2049
2050        let result = match frame.context.try_borrow_mut() {
2051            Ok(mut context) => layout_node.measure(&mut *context, &wrapped, constraints),
2052            Err(_) => {
2053                let mut temp = LayoutNodeContext::new();
2054                let result = layout_node.measure(&mut temp, &wrapped, constraints);
2055                if let Ok(mut context) = frame.context.try_borrow_mut() {
2056                    for kind in temp.take_invalidations() {
2057                        context.invalidate(kind);
2058                    }
2059                }
2060                result
2061            }
2062        };
2063
2064        node.measured_size.set(result.size);
2065        let local_offset = Point {
2066            x: result.placement_offset_x,
2067            y: result.placement_offset_y,
2068        };
2069        let child_accumulated = self.total_content_offset_from(index + 1);
2070        let accumulated = Point {
2071            x: local_offset.x + child_accumulated.x,
2072            y: local_offset.y + child_accumulated.y,
2073        };
2074        node.accumulated_offset.set(accumulated);
2075
2076        Placeable::value_with_offset(
2077            result.size.width,
2078            result.size.height,
2079            NodeId::default(),
2080            (accumulated.x, accumulated.y),
2081        )
2082    }
2083
2084    fn min_intrinsic_width_from(
2085        &self,
2086        index: usize,
2087        frame: &CoordinatorFrame<'_>,
2088        height: f32,
2089    ) -> f32 {
2090        let Some(node) = self.nodes.get(index) else {
2091            return frame
2092                .measure_policy
2093                .min_intrinsic_width(frame.measurables, height);
2094        };
2095        let wrapped = CoordinatorLink {
2096            chain: self,
2097            frame,
2098            index: index + 1,
2099        };
2100        let node_borrow = node.node.borrow();
2101        node_borrow
2102            .as_layout_node()
2103            .map(|layout_node| layout_node.min_intrinsic_width(&wrapped, height))
2104            .unwrap_or_else(|| wrapped.min_intrinsic_width(height))
2105    }
2106
2107    fn max_intrinsic_width_from(
2108        &self,
2109        index: usize,
2110        frame: &CoordinatorFrame<'_>,
2111        height: f32,
2112    ) -> f32 {
2113        let Some(node) = self.nodes.get(index) else {
2114            return frame
2115                .measure_policy
2116                .max_intrinsic_width(frame.measurables, height);
2117        };
2118        let wrapped = CoordinatorLink {
2119            chain: self,
2120            frame,
2121            index: index + 1,
2122        };
2123        let node_borrow = node.node.borrow();
2124        node_borrow
2125            .as_layout_node()
2126            .map(|layout_node| layout_node.max_intrinsic_width(&wrapped, height))
2127            .unwrap_or_else(|| wrapped.max_intrinsic_width(height))
2128    }
2129
2130    fn min_intrinsic_height_from(
2131        &self,
2132        index: usize,
2133        frame: &CoordinatorFrame<'_>,
2134        width: f32,
2135    ) -> f32 {
2136        let Some(node) = self.nodes.get(index) else {
2137            return frame
2138                .measure_policy
2139                .min_intrinsic_height(frame.measurables, width);
2140        };
2141        let wrapped = CoordinatorLink {
2142            chain: self,
2143            frame,
2144            index: index + 1,
2145        };
2146        let node_borrow = node.node.borrow();
2147        node_borrow
2148            .as_layout_node()
2149            .map(|layout_node| layout_node.min_intrinsic_height(&wrapped, width))
2150            .unwrap_or_else(|| wrapped.min_intrinsic_height(width))
2151    }
2152
2153    fn max_intrinsic_height_from(
2154        &self,
2155        index: usize,
2156        frame: &CoordinatorFrame<'_>,
2157        width: f32,
2158    ) -> f32 {
2159        let Some(node) = self.nodes.get(index) else {
2160            return frame
2161                .measure_policy
2162                .max_intrinsic_height(frame.measurables, width);
2163        };
2164        let wrapped = CoordinatorLink {
2165            chain: self,
2166            frame,
2167            index: index + 1,
2168        };
2169        let node_borrow = node.node.borrow();
2170        node_borrow
2171            .as_layout_node()
2172            .map(|layout_node| layout_node.max_intrinsic_height(&wrapped, width))
2173            .unwrap_or_else(|| wrapped.max_intrinsic_height(width))
2174    }
2175
2176    fn total_content_offset_from(&self, index: usize) -> Point {
2177        self.nodes
2178            .get(index)
2179            .map(|node| node.accumulated_offset.get())
2180            .unwrap_or_default()
2181    }
2182
2183    #[cfg(test)]
2184    fn debug_ptrs(&self) -> Vec<usize> {
2185        self.nodes.iter().map(CoordinatorNode::ptr).collect()
2186    }
2187}
2188
2189#[derive(Default)]
2190pub(crate) struct LayoutRuntimeState {
2191    child_ids: Vec<NodeId>,
2192    child_states: Vec<Rc<LayoutChildMeasureState>>,
2193    child_measurables: Vec<Box<dyn Measurable>>,
2194    coordinator_chain: CoordinatorChain,
2195}
2196
2197impl LayoutRuntimeState {
2198    fn reconcile_child_measurables(&mut self, child_ids: &[NodeId]) {
2199        if self.child_ids == child_ids {
2200            return;
2201        }
2202
2203        let mut previous_ids = std::mem::take(&mut self.child_ids);
2204        let mut previous_states = std::mem::take(&mut self.child_states);
2205        let mut previous_measurables = std::mem::take(&mut self.child_measurables);
2206
2207        self.child_ids.reserve(child_ids.len());
2208        self.child_states.reserve(child_ids.len());
2209        self.child_measurables.reserve(child_ids.len());
2210
2211        for &child_id in child_ids {
2212            if let Some(position) = previous_ids.iter().position(|&id| id == child_id) {
2213                self.child_ids.push(previous_ids.swap_remove(position));
2214                self.child_states
2215                    .push(previous_states.swap_remove(position));
2216                self.child_measurables
2217                    .push(previous_measurables.swap_remove(position));
2218            } else {
2219                let state = LayoutChildMeasureState::new(child_id);
2220                self.child_ids.push(child_id);
2221                self.child_states.push(Rc::clone(&state));
2222                self.child_measurables
2223                    .push(Box::new(LayoutChildMeasurable::new(state)));
2224            }
2225        }
2226    }
2227
2228    fn child_state(&self, index: usize) -> Rc<LayoutChildMeasureState> {
2229        Rc::clone(&self.child_states[index])
2230    }
2231
2232    fn child_measurables(&self) -> &[Box<dyn Measurable>] {
2233        self.child_measurables.as_slice()
2234    }
2235
2236    fn reconcile_coordinator_chain(&mut self, layout_node_data: &[LayoutModifierNodeData]) {
2237        self.coordinator_chain.reconcile(layout_node_data);
2238    }
2239
2240    fn coordinator_chain(&self) -> &CoordinatorChain {
2241        &self.coordinator_chain
2242    }
2243
2244    fn clear_frame_bindings(&self) {
2245        for child_state in &self.child_states {
2246            child_state.clear_frame_bindings();
2247        }
2248    }
2249
2250    #[cfg(test)]
2251    pub(crate) fn debug_stats(&self) -> LayoutRuntimeDebugStats {
2252        LayoutRuntimeDebugStats {
2253            child_ids: self.child_ids.clone(),
2254            child_state_ptrs: self
2255                .child_states
2256                .iter()
2257                .map(|state| Rc::as_ptr(state) as *const () as usize)
2258                .collect(),
2259            child_measurable_ptrs: self
2260                .child_measurables
2261                .iter()
2262                .map(|measurable| {
2263                    measurable.as_ref() as *const dyn Measurable as *const () as usize
2264                })
2265                .collect(),
2266            child_measurable_count: self.child_measurables.len(),
2267            coordinator_node_ptrs: self.coordinator_chain.debug_ptrs(),
2268            coordinator_node_count: self.coordinator_chain.nodes.len(),
2269        }
2270    }
2271}
2272
2273#[cfg(test)]
2274#[derive(Debug, Clone, PartialEq, Eq)]
2275pub(crate) struct LayoutRuntimeDebugStats {
2276    pub(crate) child_ids: Vec<NodeId>,
2277    pub(crate) child_state_ptrs: Vec<usize>,
2278    pub(crate) child_measurable_ptrs: Vec<usize>,
2279    pub(crate) child_measurable_count: usize,
2280    pub(crate) coordinator_node_ptrs: Vec<usize>,
2281    pub(crate) coordinator_node_count: usize,
2282}
2283
2284struct LayoutChildMeasureConfig {
2285    applier: Rc<ConcreteApplierHost<MemoryApplier>>,
2286    node_id: NodeId,
2287    error: Rc<RefCell<Option<NodeError>>>,
2288    runtime_handle: Option<RuntimeHandle>,
2289    cache: LayoutNodeCacheHandles,
2290    cache_epoch: u64,
2291    force_remeasure: bool,
2292    measure_handle: Option<LayoutMeasureHandle>,
2293    layout_state: Option<Rc<RefCell<LayoutState>>>,
2294}
2295
2296struct LayoutChildMeasureState {
2297    applier: RefCell<Option<Rc<ConcreteApplierHost<MemoryApplier>>>>,
2298    node_id: Cell<NodeId>,
2299    measured: RefCell<Option<Rc<MeasuredNode>>>,
2300    last_position: Cell<Option<Point>>,
2301    error: RefCell<Option<Rc<RefCell<Option<NodeError>>>>>,
2302    runtime_handle: RefCell<Option<RuntimeHandle>>,
2303    cache: RefCell<LayoutNodeCacheHandles>,
2304    cache_epoch: Cell<u64>,
2305    force_remeasure: Cell<bool>,
2306    measure_handle: RefCell<Option<LayoutMeasureHandle>>,
2307    layout_state: RefCell<Option<Rc<RefCell<LayoutState>>>>,
2308}
2309
2310impl LayoutChildMeasureState {
2311    fn new(node_id: NodeId) -> Rc<Self> {
2312        Rc::new(Self {
2313            applier: RefCell::new(None),
2314            node_id: Cell::new(node_id),
2315            measured: RefCell::new(None),
2316            last_position: Cell::new(None),
2317            error: RefCell::new(None),
2318            runtime_handle: RefCell::new(None),
2319            cache: RefCell::new(LayoutNodeCacheHandles::default()),
2320            cache_epoch: Cell::new(0),
2321            force_remeasure: Cell::new(true),
2322            measure_handle: RefCell::new(None),
2323            layout_state: RefCell::new(None),
2324        })
2325    }
2326
2327    fn configure(&self, config: LayoutChildMeasureConfig) {
2328        config.cache.activate(config.cache_epoch);
2329        *self.applier.borrow_mut() = Some(config.applier);
2330        self.node_id.set(config.node_id);
2331        self.measured.borrow_mut().take();
2332        self.last_position.set(None);
2333        *self.error.borrow_mut() = Some(config.error);
2334        *self.runtime_handle.borrow_mut() = config.runtime_handle;
2335        *self.cache.borrow_mut() = config.cache;
2336        self.cache_epoch.set(config.cache_epoch);
2337        self.force_remeasure.set(config.force_remeasure);
2338        *self.measure_handle.borrow_mut() = config.measure_handle;
2339        *self.layout_state.borrow_mut() = config.layout_state;
2340    }
2341
2342    fn clear_frame_bindings(&self) {
2343        self.measured.borrow_mut().take();
2344        *self.applier.borrow_mut() = None;
2345        *self.error.borrow_mut() = None;
2346        *self.runtime_handle.borrow_mut() = None;
2347        *self.measure_handle.borrow_mut() = None;
2348        *self.layout_state.borrow_mut() = None;
2349    }
2350
2351    fn node_id(&self) -> NodeId {
2352        self.node_id.get()
2353    }
2354
2355    fn cache(&self) -> LayoutNodeCacheHandles {
2356        self.cache.borrow().clone()
2357    }
2358
2359    fn applier(&self) -> Option<Rc<ConcreteApplierHost<MemoryApplier>>> {
2360        self.applier.borrow().clone()
2361    }
2362
2363    fn layout_state(&self) -> Option<Rc<RefCell<LayoutState>>> {
2364        self.layout_state.borrow().clone()
2365    }
2366
2367    fn take_measured(&self) -> Option<Rc<MeasuredNode>> {
2368        self.measured.borrow_mut().take()
2369    }
2370
2371    fn last_position(&self) -> Option<Point> {
2372        self.last_position.get()
2373    }
2374
2375    fn set_last_position(&self, position: Point) {
2376        self.last_position.set(Some(position));
2377    }
2378
2379    fn set_measured(&self, measured: Option<Rc<MeasuredNode>>) {
2380        *self.measured.borrow_mut() = measured;
2381    }
2382
2383    fn record_error(&self, err: NodeError) {
2384        let Some(error) = self.error.borrow().clone() else {
2385            return;
2386        };
2387        let mut slot = error.borrow_mut();
2388        if slot.is_none() {
2389            *slot = Some(err);
2390        }
2391    }
2392
2393    fn perform_measure(&self, constraints: Constraints) -> Result<Rc<MeasuredNode>, NodeError> {
2394        let node_id = self.node_id();
2395        if let Some(handle) = self.measure_handle.borrow().clone() {
2396            return handle.measure(node_id, constraints);
2397        }
2398        let applier = self.applier().ok_or(NodeError::MissingContext {
2399            id: node_id,
2400            reason: "layout child applier not configured",
2401        })?;
2402        measure_node_with_host(
2403            applier,
2404            self.runtime_handle.borrow().clone(),
2405            node_id,
2406            constraints,
2407            self.cache_epoch.get(),
2408        )
2409    }
2410
2411    fn intrinsic_measure(&self, constraints: Constraints) -> Option<Rc<MeasuredNode>> {
2412        let cache = self.cache();
2413        cache.activate(self.cache_epoch.get());
2414        if !self.force_remeasure.get() {
2415            if let Some(cached) = cache.get_measurement(constraints) {
2416                return Some(cached);
2417            }
2418        }
2419
2420        match self.perform_measure(constraints) {
2421            Ok(measured) => {
2422                self.force_remeasure.set(false);
2423                cache.store_measurement(constraints, Rc::clone(&measured));
2424                Some(measured)
2425            }
2426            Err(err) => {
2427                self.record_error(err);
2428                None
2429            }
2430        }
2431    }
2432}
2433
2434struct LayoutChildMeasurable {
2435    state: Rc<LayoutChildMeasureState>,
2436}
2437
2438impl LayoutChildMeasurable {
2439    fn new(state: Rc<LayoutChildMeasureState>) -> Self {
2440        Self { state }
2441    }
2442}
2443
2444impl Measurable for LayoutChildMeasurable {
2445    fn measure(&self, constraints: Constraints) -> Placeable {
2446        let state = &self.state;
2447        let cache = state.cache();
2448        cache.activate(state.cache_epoch.get());
2449        let measured_size;
2450        if !state.force_remeasure.get() {
2451            if let Some(cached) = cache.get_measurement(constraints) {
2452                measured_size = cached.size;
2453                state.set_measured(Some(Rc::clone(&cached)));
2454            } else {
2455                match state.perform_measure(constraints) {
2456                    Ok(measured) => {
2457                        state.force_remeasure.set(false);
2458                        measured_size = measured.size;
2459                        cache.store_measurement(constraints, Rc::clone(&measured));
2460                        state.set_measured(Some(measured));
2461                    }
2462                    Err(err) => {
2463                        state.record_error(err);
2464                        state.set_measured(None);
2465                        measured_size = Size {
2466                            width: 0.0,
2467                            height: 0.0,
2468                        };
2469                    }
2470                }
2471            }
2472        } else {
2473            match state.perform_measure(constraints) {
2474                Ok(measured) => {
2475                    state.force_remeasure.set(false);
2476                    measured_size = measured.size;
2477                    cache.store_measurement(constraints, Rc::clone(&measured));
2478                    state.set_measured(Some(measured));
2479                }
2480                Err(err) => {
2481                    state.record_error(err);
2482                    state.set_measured(None);
2483                    measured_size = Size {
2484                        width: 0.0,
2485                        height: 0.0,
2486                    };
2487                }
2488            }
2489        }
2490
2491        if let Some(layout_state) = state.layout_state() {
2492            let mut layout_state = layout_state.borrow_mut();
2493            layout_state.size = measured_size;
2494            layout_state.measurement_constraints = constraints;
2495        } else if let Some(applier) = state.applier() {
2496            let Ok(mut applier) = applier.try_borrow_typed() else {
2497                return Placeable::value(
2498                    measured_size.width,
2499                    measured_size.height,
2500                    state.node_id(),
2501                );
2502            };
2503            let _ = applier.with_node::<LayoutNode, _>(state.node_id(), |node| {
2504                node.set_measured_size(measured_size);
2505                node.set_measurement_constraints(constraints);
2506            });
2507        }
2508
2509        let state = Rc::clone(&self.state);
2510        let applier = state.applier();
2511        let node_id = state.node_id();
2512        let layout_state = state.layout_state();
2513
2514        let place_fn = Rc::new(move |x: f32, y: f32| {
2515            let internal_offset = state
2516                .measured
2517                .borrow()
2518                .as_ref()
2519                .map(|m| m.offset)
2520                .unwrap_or_default();
2521
2522            let position = Point {
2523                x: x + internal_offset.x,
2524                y: y + internal_offset.y,
2525            };
2526            state.set_last_position(position);
2527
2528            if let Some(layout_state) = &layout_state {
2529                let mut layout_state = layout_state.borrow_mut();
2530                layout_state.position = position;
2531                layout_state.is_placed = true;
2532            } else if let Some(applier) = &applier {
2533                let Ok(mut applier) = applier.try_borrow_typed() else {
2534                    return;
2535                };
2536                if applier
2537                    .with_node::<LayoutNode, _>(node_id, |node| {
2538                        node.set_position(position);
2539                    })
2540                    .is_err()
2541                {
2542                    let _ = applier.with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
2543                        node.set_position(position);
2544                    });
2545                }
2546            }
2547        });
2548
2549        Placeable::with_place_fn(measured_size.width, measured_size.height, node_id, place_fn)
2550    }
2551
2552    fn min_intrinsic_width(&self, height: f32) -> f32 {
2553        let kind = IntrinsicKind::MinWidth(height);
2554        let cache = self.state.cache();
2555        cache.activate(self.state.cache_epoch.get());
2556        if !self.state.force_remeasure.get() {
2557            if let Some(value) = cache.get_intrinsic(&kind) {
2558                return value;
2559            }
2560        }
2561        let constraints = Constraints {
2562            min_width: 0.0,
2563            max_width: f32::INFINITY,
2564            min_height: height,
2565            max_height: height,
2566        };
2567        if let Some(node) = self.state.intrinsic_measure(constraints) {
2568            let value = node.size.width;
2569            cache.store_intrinsic(kind, value);
2570            value
2571        } else {
2572            0.0
2573        }
2574    }
2575
2576    fn max_intrinsic_width(&self, height: f32) -> f32 {
2577        let kind = IntrinsicKind::MaxWidth(height);
2578        let cache = self.state.cache();
2579        cache.activate(self.state.cache_epoch.get());
2580        if !self.state.force_remeasure.get() {
2581            if let Some(value) = cache.get_intrinsic(&kind) {
2582                return value;
2583            }
2584        }
2585        let constraints = Constraints {
2586            min_width: 0.0,
2587            max_width: f32::INFINITY,
2588            min_height: 0.0,
2589            max_height: height,
2590        };
2591        if let Some(node) = self.state.intrinsic_measure(constraints) {
2592            let value = node.size.width;
2593            cache.store_intrinsic(kind, value);
2594            value
2595        } else {
2596            0.0
2597        }
2598    }
2599
2600    fn min_intrinsic_height(&self, width: f32) -> f32 {
2601        let kind = IntrinsicKind::MinHeight(width);
2602        let cache = self.state.cache();
2603        cache.activate(self.state.cache_epoch.get());
2604        if !self.state.force_remeasure.get() {
2605            if let Some(value) = cache.get_intrinsic(&kind) {
2606                return value;
2607            }
2608        }
2609        let constraints = Constraints {
2610            min_width: width,
2611            max_width: width,
2612            min_height: 0.0,
2613            max_height: f32::INFINITY,
2614        };
2615        if let Some(node) = self.state.intrinsic_measure(constraints) {
2616            let value = node.size.height;
2617            cache.store_intrinsic(kind, value);
2618            value
2619        } else {
2620            0.0
2621        }
2622    }
2623
2624    fn max_intrinsic_height(&self, width: f32) -> f32 {
2625        let kind = IntrinsicKind::MaxHeight(width);
2626        let cache = self.state.cache();
2627        cache.activate(self.state.cache_epoch.get());
2628        if !self.state.force_remeasure.get() {
2629            if let Some(value) = cache.get_intrinsic(&kind) {
2630                return value;
2631            }
2632        }
2633        let constraints = Constraints {
2634            min_width: 0.0,
2635            max_width: width,
2636            min_height: 0.0,
2637            max_height: f32::INFINITY,
2638        };
2639        if let Some(node) = self.state.intrinsic_measure(constraints) {
2640            let value = node.size.height;
2641            cache.store_intrinsic(kind, value);
2642            value
2643        } else {
2644            0.0
2645        }
2646    }
2647
2648    fn flex_parent_data(&self) -> Option<cranpose_ui_layout::FlexParentData> {
2649        let applier = self.state.applier()?;
2650        let node_id = self.state.node_id();
2651        let Ok(mut applier) = applier.try_borrow_typed() else {
2652            return None;
2653        };
2654
2655        applier
2656            .with_node::<LayoutNode, _>(node_id, |layout_node| {
2657                let props = layout_node.resolved_modifiers().layout_properties();
2658                props.weight().map(|weight_data| {
2659                    cranpose_ui_layout::FlexParentData::new(weight_data.weight, weight_data.fill)
2660                })
2661            })
2662            .ok()
2663            .flatten()
2664    }
2665}
2666
2667fn measure_node_with_host(
2668    applier: Rc<ConcreteApplierHost<MemoryApplier>>,
2669    runtime_handle: Option<RuntimeHandle>,
2670    node_id: NodeId,
2671    constraints: Constraints,
2672    epoch: u64,
2673) -> Result<Rc<MeasuredNode>, NodeError> {
2674    let runtime_handle = match runtime_handle {
2675        Some(handle) => Some(handle),
2676        None => applier.borrow_typed().runtime_handle(),
2677    };
2678    let mut builder = LayoutBuilder::new_with_epoch(
2679        applier,
2680        epoch,
2681        Rc::new(RefCell::new(SlotTable::default())),
2682        FrameLayoutArena::default(),
2683    );
2684    builder.set_runtime_handle(runtime_handle);
2685    builder.measure_node(node_id, constraints)
2686}
2687
2688#[derive(Clone)]
2689struct RuntimeNodeMetadata {
2690    modifier: Modifier,
2691    resolved_modifiers: ResolvedModifiers,
2692    modifier_slices: Rc<ModifierNodeSlices>,
2693    role: SemanticsRole,
2694    button_handler: Option<Rc<RefCell<dyn FnMut()>>>,
2695}
2696
2697impl Default for RuntimeNodeMetadata {
2698    fn default() -> Self {
2699        Self {
2700            modifier: Modifier::empty(),
2701            resolved_modifiers: ResolvedModifiers::default(),
2702            modifier_slices: Rc::default(),
2703            role: SemanticsRole::Unknown,
2704            button_handler: None,
2705        }
2706    }
2707}
2708
2709fn role_from_modifier_slices(modifier_slices: &ModifierNodeSlices) -> SemanticsRole {
2710    modifier_slices
2711        .text_content()
2712        .map(|text| SemanticsRole::Text {
2713            value: text.to_string(),
2714        })
2715        .unwrap_or(SemanticsRole::Layout)
2716}
2717
2718fn runtime_metadata_for(
2719    applier: &mut MemoryApplier,
2720    node_id: NodeId,
2721) -> Result<RuntimeNodeMetadata, NodeError> {
2722    // Try LayoutNode (the primary modern path)
2723    // IMPORTANT: We use with_node (reference) instead of try_clone because cloning
2724    // LayoutNode creates a NEW ModifierChainHandle with NEW nodes and NEW handlers,
2725    // which would lose gesture state like press_position.
2726    if let Ok(meta) = applier.with_node::<LayoutNode, _>(node_id, |layout| {
2727        let modifier = layout.modifier.clone();
2728        let resolved_modifiers = layout.resolved_modifiers();
2729        let modifier_slices = layout.modifier_slices_snapshot();
2730        let role = role_from_modifier_slices(&modifier_slices);
2731
2732        RuntimeNodeMetadata {
2733            modifier,
2734            resolved_modifiers,
2735            modifier_slices,
2736            role,
2737            button_handler: None,
2738        }
2739    }) {
2740        return Ok(meta);
2741    }
2742
2743    // Try SubcomposeLayoutNode
2744    if let Ok((modifier, resolved_modifiers, modifier_slices)) = applier
2745        .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
2746            (
2747                node.modifier(),
2748                node.resolved_modifiers(),
2749                node.modifier_slices_snapshot(),
2750            )
2751        })
2752    {
2753        return Ok(RuntimeNodeMetadata {
2754            modifier,
2755            resolved_modifiers,
2756            modifier_slices,
2757            role: SemanticsRole::Subcompose,
2758            button_handler: None,
2759        });
2760    }
2761    Ok(RuntimeNodeMetadata::default())
2762}
2763
2764fn clear_semantics_dirty_flags(
2765    applier: &mut MemoryApplier,
2766    node: &MeasuredNode,
2767) -> Result<(), NodeError> {
2768    match applier.with_node::<LayoutNode, _>(node.node_id, |layout| {
2769        layout.clear_needs_semantics();
2770    }) {
2771        Ok(()) => {}
2772        Err(NodeError::Missing { .. }) => {}
2773        Err(NodeError::TypeMismatch { .. }) => {
2774            match applier.with_node::<SubcomposeLayoutNode, _>(node.node_id, |subcompose| {
2775                subcompose.clear_needs_semantics();
2776            }) {
2777                Ok(()) | Err(NodeError::Missing { .. }) | Err(NodeError::TypeMismatch { .. }) => {}
2778                Err(err) => return Err(err),
2779            }
2780        }
2781        Err(err) => return Err(err),
2782    }
2783
2784    for child in &node.children {
2785        clear_semantics_dirty_flags(applier, &child.node)?;
2786    }
2787
2788    Ok(())
2789}
2790
2791fn build_semantics_tree_from_live_nodes(
2792    applier: &mut MemoryApplier,
2793    node: &MeasuredNode,
2794) -> Result<SemanticsTree, NodeError> {
2795    Ok(SemanticsTree::new(build_semantics_node_from_live_nodes(
2796        applier, node,
2797    )?))
2798}
2799
2800fn semantics_node_from_parts(
2801    node_id: NodeId,
2802    mut role: SemanticsRole,
2803    config: Option<SemanticsConfiguration>,
2804    children: Vec<SemanticsNode>,
2805) -> SemanticsNode {
2806    let mut actions = Vec::new();
2807    let mut description = None;
2808    let mut editable_text = false;
2809    let mut text_selection = None;
2810
2811    if let Some(config) = config {
2812        if config.is_button {
2813            role = SemanticsRole::Button;
2814        }
2815        if config.is_clickable {
2816            actions.push(SemanticsAction::Click {
2817                handler: SemanticsCallback::new(node_id),
2818            });
2819        }
2820        description = config.content_description;
2821        editable_text = config.is_editable_text;
2822        text_selection = config.text_selection;
2823    }
2824
2825    SemanticsNode::new(
2826        node_id,
2827        role,
2828        actions,
2829        children,
2830        description,
2831        editable_text,
2832        text_selection,
2833    )
2834}
2835
2836fn build_semantics_node_from_live_nodes(
2837    applier: &mut MemoryApplier,
2838    node: &MeasuredNode,
2839) -> Result<SemanticsNode, NodeError> {
2840    let (role, config) = match applier.with_node::<LayoutNode, _>(node.node_id, |layout| {
2841        let role = role_from_modifier_slices(&layout.modifier_slices_snapshot());
2842        let config = layout.semantics_configuration();
2843        layout.clear_needs_semantics();
2844        (role, config)
2845    }) {
2846        Ok(data) => data,
2847        Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => {
2848            match applier.with_node::<SubcomposeLayoutNode, _>(node.node_id, |subcompose| {
2849                subcompose.clear_needs_semantics();
2850                (
2851                    SemanticsRole::Subcompose,
2852                    collect_semantics_from_modifier(&subcompose.modifier()),
2853                )
2854            }) {
2855                Ok(data) => data,
2856                Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => {
2857                    (SemanticsRole::Unknown, None)
2858                }
2859                Err(err) => return Err(err),
2860            }
2861        }
2862        Err(err) => return Err(err),
2863    };
2864
2865    let mut children = Vec::with_capacity(node.children.len());
2866    for child in &node.children {
2867        children.push(build_semantics_node_from_live_nodes(applier, &child.node)?);
2868    }
2869
2870    Ok(semantics_node_from_parts(
2871        node.node_id,
2872        role,
2873        config,
2874        children,
2875    ))
2876}
2877
2878fn record_semantics_allocation_stats(node: &SemanticsNode, stats: &mut LayoutAllocationDebugStats) {
2879    stats.semantics_node_count += 1;
2880    stats.semantics_action_count += node.actions.len();
2881    stats.semantics_action_capacity += node.actions.capacity();
2882    stats.semantics_child_count += node.children.len();
2883    stats.semantics_child_capacity += node.children.capacity();
2884    stats.semantics_heap_bytes += node.actions.capacity() * size_of::<SemanticsAction>();
2885    stats.semantics_heap_bytes += node.children.capacity() * size_of::<SemanticsNode>();
2886
2887    if let Some(description) = &node.description {
2888        stats.semantics_description_count += 1;
2889        stats.semantics_description_bytes += description.capacity();
2890        stats.semantics_heap_bytes += description.capacity();
2891    }
2892    if let SemanticsRole::Text { value } = &node.role {
2893        stats.semantics_text_role_bytes += value.capacity();
2894        stats.semantics_heap_bytes += value.capacity();
2895    }
2896
2897    for child in &node.children {
2898        record_semantics_allocation_stats(child, stats);
2899    }
2900}
2901
2902fn record_layout_box_allocation_stats(
2903    layout_box: &LayoutBox,
2904    stats: &mut LayoutAllocationDebugStats,
2905) {
2906    stats.layout_box_count += 1;
2907    stats.layout_box_child_count += layout_box.children.len();
2908    stats.layout_box_child_capacity += layout_box.children.capacity();
2909    stats.layout_box_heap_bytes += layout_box.children.capacity() * size_of::<LayoutBox>();
2910    stats.add_modifier_slice(layout_box.node_data.modifier_slices().debug_stats());
2911
2912    for child in &layout_box.children {
2913        record_layout_box_allocation_stats(child, stats);
2914    }
2915}
2916
2917fn build_layout_tree(
2918    applier: &mut MemoryApplier,
2919    node: &MeasuredNode,
2920) -> Result<LayoutTree, NodeError> {
2921    fn place(
2922        applier: &mut MemoryApplier,
2923        node: &MeasuredNode,
2924        origin: Point,
2925    ) -> Result<LayoutBox, NodeError> {
2926        // Include the node's own offset (from OffsetNode) in its position
2927        let top_left = Point {
2928            x: origin.x + node.offset.x,
2929            y: origin.y + node.offset.y,
2930        };
2931        let rect = GeometryRect {
2932            x: top_left.x,
2933            y: top_left.y,
2934            width: node.size.width,
2935            height: node.size.height,
2936        };
2937        let info = runtime_metadata_for(applier, node.node_id)?;
2938        let kind = layout_kind_from_metadata(node.node_id, &info);
2939        let RuntimeNodeMetadata {
2940            modifier,
2941            resolved_modifiers,
2942            modifier_slices,
2943            ..
2944        } = info;
2945        let data = LayoutNodeData::new(modifier, resolved_modifiers, modifier_slices, kind);
2946        let mut children = Vec::with_capacity(node.children.len());
2947        for child in &node.children {
2948            let child_origin = Point {
2949                x: top_left.x + child.offset.x,
2950                y: top_left.y + child.offset.y,
2951            };
2952            children.push(place(applier, &child.node, child_origin)?);
2953        }
2954        Ok(LayoutBox::new(
2955            node.node_id,
2956            rect,
2957            node.content_offset,
2958            data,
2959            children,
2960        ))
2961    }
2962
2963    Ok(LayoutTree::new(place(
2964        applier,
2965        node,
2966        Point { x: 0.0, y: 0.0 },
2967    )?))
2968}
2969
2970fn semantics_role_from_layout_box(layout_box: &LayoutBox) -> SemanticsRole {
2971    match &layout_box.node_data.kind {
2972        LayoutNodeKind::Subcompose => SemanticsRole::Subcompose,
2973        LayoutNodeKind::Spacer => SemanticsRole::Spacer,
2974        LayoutNodeKind::Unknown => SemanticsRole::Unknown,
2975        LayoutNodeKind::Button { .. } => SemanticsRole::Button,
2976        LayoutNodeKind::Layout => layout_box
2977            .node_data
2978            .modifier_slices()
2979            .text_content()
2980            .map(|text| SemanticsRole::Text {
2981                value: text.to_string(),
2982            })
2983            .unwrap_or(SemanticsRole::Layout),
2984    }
2985}
2986
2987fn build_semantics_node_from_layout_box(layout_box: &LayoutBox) -> SemanticsNode {
2988    let children = layout_box
2989        .children
2990        .iter()
2991        .map(build_semantics_node_from_layout_box)
2992        .collect();
2993
2994    semantics_node_from_parts(
2995        layout_box.node_id,
2996        semantics_role_from_layout_box(layout_box),
2997        collect_semantics_from_modifier(&layout_box.node_data.modifier),
2998        children,
2999    )
3000}
3001
3002fn layout_kind_from_metadata(_node_id: NodeId, info: &RuntimeNodeMetadata) -> LayoutNodeKind {
3003    match &info.role {
3004        SemanticsRole::Layout => LayoutNodeKind::Layout,
3005        SemanticsRole::Subcompose => LayoutNodeKind::Subcompose,
3006        SemanticsRole::Text { .. } => {
3007            // Text content is now handled via TextModifierNode in the modifier chain
3008            // and collected in modifier_slices.text_content(). LayoutNodeKind should
3009            // reflect the layout policy (EmptyMeasurePolicy), not the content type.
3010            LayoutNodeKind::Layout
3011        }
3012        SemanticsRole::Spacer => LayoutNodeKind::Spacer,
3013        SemanticsRole::Button => {
3014            let handler = info
3015                .button_handler
3016                .as_ref()
3017                .cloned()
3018                .unwrap_or_else(|| Rc::new(RefCell::new(|| {})));
3019            LayoutNodeKind::Button { on_click: handler }
3020        }
3021        SemanticsRole::Unknown => LayoutNodeKind::Unknown,
3022    }
3023}
3024
3025fn subtract_padding(constraints: Constraints, padding: EdgeInsets) -> Constraints {
3026    let horizontal = padding.horizontal_sum();
3027    let vertical = padding.vertical_sum();
3028    let min_width = (constraints.min_width - horizontal).max(0.0);
3029    let mut max_width = constraints.max_width;
3030    if max_width.is_finite() {
3031        max_width = (max_width - horizontal).max(0.0);
3032    }
3033    let min_height = (constraints.min_height - vertical).max(0.0);
3034    let mut max_height = constraints.max_height;
3035    if max_height.is_finite() {
3036        max_height = (max_height - vertical).max(0.0);
3037    }
3038    normalize_constraints(Constraints {
3039        min_width,
3040        max_width,
3041        min_height,
3042        max_height,
3043    })
3044}
3045
3046#[cfg(test)]
3047pub(crate) fn align_horizontal(alignment: HorizontalAlignment, available: f32, child: f32) -> f32 {
3048    match alignment {
3049        HorizontalAlignment::Start => 0.0,
3050        HorizontalAlignment::CenterHorizontally => ((available - child) / 2.0).max(0.0),
3051        HorizontalAlignment::End => (available - child).max(0.0),
3052    }
3053}
3054
3055#[cfg(test)]
3056pub(crate) fn align_vertical(alignment: VerticalAlignment, available: f32, child: f32) -> f32 {
3057    match alignment {
3058        VerticalAlignment::Top => 0.0,
3059        VerticalAlignment::CenterVertically => ((available - child) / 2.0).max(0.0),
3060        VerticalAlignment::Bottom => (available - child).max(0.0),
3061    }
3062}
3063
3064fn resolve_dimension(
3065    base: f32,
3066    explicit: DimensionConstraint,
3067    min_override: Option<f32>,
3068    max_override: Option<f32>,
3069    min_limit: f32,
3070    max_limit: f32,
3071) -> f32 {
3072    let mut min_bound = min_limit;
3073    if let Some(min_value) = min_override {
3074        min_bound = min_bound.max(min_value);
3075    }
3076
3077    let mut max_bound = if max_limit.is_finite() {
3078        max_limit
3079    } else {
3080        max_override.unwrap_or(max_limit)
3081    };
3082    if let Some(max_value) = max_override {
3083        if max_bound.is_finite() {
3084            max_bound = max_bound.min(max_value);
3085        } else {
3086            max_bound = max_value;
3087        }
3088    }
3089    if max_bound < min_bound {
3090        max_bound = min_bound;
3091    }
3092
3093    let mut size = match explicit {
3094        DimensionConstraint::Points(points) => points,
3095        DimensionConstraint::Fraction(fraction) => {
3096            if max_limit.is_finite() {
3097                max_limit * fraction.clamp(0.0, 1.0)
3098            } else {
3099                base
3100            }
3101        }
3102        DimensionConstraint::Unspecified => base,
3103        // Intrinsic sizing is resolved at a higher level where we have access to children.
3104        // At this point we just use the base size as a fallback.
3105        DimensionConstraint::Intrinsic(_) => base,
3106    };
3107
3108    size = clamp_dimension(size, min_bound, max_bound);
3109    size = clamp_dimension(size, min_limit, max_limit);
3110    size.max(0.0)
3111}
3112
3113fn clamp_dimension(value: f32, min: f32, max: f32) -> f32 {
3114    let mut result = value.max(min);
3115    if max.is_finite() {
3116        result = result.min(max);
3117    }
3118    result
3119}
3120
3121fn normalize_constraints(mut constraints: Constraints) -> Constraints {
3122    if constraints.max_width < constraints.min_width {
3123        constraints.max_width = constraints.min_width;
3124    }
3125    if constraints.max_height < constraints.min_height {
3126        constraints.max_height = constraints.min_height;
3127    }
3128    constraints
3129}
3130
3131#[cfg(test)]
3132#[path = "tests/layout_tests.rs"]
3133mod tests;