Skip to main content

cranpose_ui/layout/
mod.rs

1// WIP: Layout system infrastructure - many helper types not yet fully wired up
2
3pub mod coordinator;
4pub mod core;
5pub mod policies;
6
7use cranpose_core::collections::map::Entry;
8use cranpose_core::collections::map::HashMap;
9use std::{
10    cell::RefCell,
11    fmt,
12    rc::Rc,
13    sync::atomic::{AtomicU64, Ordering},
14};
15
16use cranpose_core::{
17    Applier, ApplierHost, Composer, ConcreteApplierHost, MemoryApplier, NodeError, NodeId, Phase,
18    RuntimeHandle, SlotBackend, SlotsHost, SnapshotStateObserver,
19};
20
21use self::coordinator::NodeCoordinator;
22use self::core::Measurable;
23use self::core::Placeable;
24#[cfg(test)]
25use self::core::{HorizontalAlignment, VerticalAlignment};
26use crate::modifier::{
27    collect_semantics_from_modifier, collect_slices_from_modifier, DimensionConstraint, EdgeInsets,
28    Modifier, ModifierNodeSlices, Point, Rect as GeometryRect, ResolvedModifiers, Size,
29};
30
31use crate::subcompose_layout::SubcomposeLayoutNode;
32use crate::widgets::nodes::{IntrinsicKind, LayoutNode, LayoutNodeCacheHandles};
33use cranpose_foundation::InvalidationKind;
34use cranpose_foundation::ModifierNodeContext;
35use cranpose_foundation::{NodeCapabilities, SemanticsConfiguration};
36use cranpose_ui_layout::{Constraints, MeasurePolicy, MeasureResult};
37
38/// Runtime context for modifier nodes during measurement.
39///
40/// Unlike `BasicModifierNodeContext`, this context accumulates invalidations
41/// that can be processed after measurement to set dirty flags on the LayoutNode.
42#[derive(Default)]
43pub(crate) struct LayoutNodeContext {
44    invalidations: Vec<InvalidationKind>,
45    update_requested: bool,
46    active_capabilities: Vec<NodeCapabilities>,
47}
48
49impl LayoutNodeContext {
50    pub(crate) fn new() -> Self {
51        Self::default()
52    }
53
54    pub(crate) fn take_invalidations(&mut self) -> Vec<InvalidationKind> {
55        std::mem::take(&mut self.invalidations)
56    }
57}
58
59impl ModifierNodeContext for LayoutNodeContext {
60    fn invalidate(&mut self, kind: InvalidationKind) {
61        if !self.invalidations.contains(&kind) {
62            self.invalidations.push(kind);
63        }
64    }
65
66    fn request_update(&mut self) {
67        self.update_requested = true;
68    }
69
70    fn push_active_capabilities(&mut self, capabilities: NodeCapabilities) {
71        self.active_capabilities.push(capabilities);
72    }
73
74    fn pop_active_capabilities(&mut self) {
75        self.active_capabilities.pop();
76    }
77}
78
79static NEXT_CACHE_EPOCH: AtomicU64 = AtomicU64::new(1);
80
81/// Forces all layout caches to be invalidated on the next measure by incrementing the epoch.
82///
83/// # ⚠️ Internal Use Only - NOT Public API
84///
85/// **This function is hidden from public documentation and MUST NOT be called by external code.**
86///
87/// Only `cranpose-app-shell` may call this for rare global events:
88/// - Window/viewport resize
89/// - Global font scale or density changes
90/// - Debug toggles that affect all layout
91///
92/// **This is O(entire app size) - extremely expensive!**
93///
94/// # For Local Changes
95///
96/// **Do NOT use this for scroll, single-node mutations, or any local layout change.**
97/// Instead, use the scoped repass mechanism:
98/// ```text
99/// cranpose_ui::schedule_layout_repass(node_id);
100/// ```
101///
102/// The scoped path bubbles dirty flags without invalidating all caches, giving you O(subtree) instead of O(app).
103#[doc(hidden)]
104pub fn invalidate_all_layout_caches() {
105    NEXT_CACHE_EPOCH.fetch_add(1, Ordering::Relaxed);
106}
107
108/// RAII guard that:
109/// - moves the current MemoryApplier into a ConcreteApplierHost
110/// - holds a shared handle to the SlotBackend used by LayoutBuilder
111/// - on Drop, always:
112///   * restores slots into the host from the shared handle
113///   * moves the original MemoryApplier back into the Composition
114///
115/// This makes `measure_layout` panic/Err-safe wrt both the applier and slots.
116/// The key invariant: guard and builder share the same `Rc<RefCell<SlotBackend>>`,
117/// so the guard never loses access to the authoritative slots even on panic.
118struct ApplierSlotGuard<'a> {
119    /// The `MemoryApplier` inside the Composition::applier that we must restore into.
120    target: &'a mut MemoryApplier,
121    /// Host that owns the original MemoryApplier while layout is running.
122    host: Rc<ConcreteApplierHost<MemoryApplier>>,
123    /// Shared handle to the slot table. Both the guard and the builder hold a clone.
124    /// On Drop, we write whatever is in this handle back into the applier.
125    slots: Rc<RefCell<SlotBackend>>,
126}
127
128impl<'a> ApplierSlotGuard<'a> {
129    /// Creates a new guard:
130    /// - moves the current MemoryApplier out of `target` into a host
131    /// - takes the current slots out of the host and wraps them in a shared handle
132    fn new(target: &'a mut MemoryApplier) -> Self {
133        // Move the original applier into a host; leave `target` with a fresh one
134        let original_applier = std::mem::replace(target, MemoryApplier::new());
135        let host = Rc::new(ConcreteApplierHost::new(original_applier));
136
137        // Take slots from the host into a shared handle
138        let slots = {
139            let mut applier_ref = host.borrow_typed();
140            std::mem::take(applier_ref.slots())
141        };
142        let slots = Rc::new(RefCell::new(slots));
143
144        Self {
145            target,
146            host,
147            slots,
148        }
149    }
150
151    /// Rc to pass into LayoutBuilder::new_with_epoch
152    fn host(&self) -> Rc<ConcreteApplierHost<MemoryApplier>> {
153        Rc::clone(&self.host)
154    }
155
156    /// Returns the shared handle to slots for the builder to use.
157    /// The builder clones this Rc, so both guard and builder share the same slots.
158    fn slots_handle(&self) -> Rc<RefCell<SlotBackend>> {
159        Rc::clone(&self.slots)
160    }
161}
162
163impl Drop for ApplierSlotGuard<'_> {
164    fn drop(&mut self) {
165        // 1) Restore slots into the host's MemoryApplier from the shared handle.
166        // This works correctly whether we're on the success path or panic/error path,
167        // because we always have the shared handle.
168        {
169            let mut applier_ref = self.host.borrow_typed();
170            *applier_ref.slots() = std::mem::take(&mut *self.slots.borrow_mut());
171        }
172
173        // 2) Move the original MemoryApplier (with restored/updated slots) back into `target`
174        {
175            let mut applier_ref = self.host.borrow_typed();
176            let original_applier = std::mem::take(&mut *applier_ref);
177            let _ = std::mem::replace(self.target, original_applier);
178        }
179        // No Rc::try_unwrap in Drop → no "panic during panic" risk.
180    }
181}
182
183/// Result of measuring through the modifier node chain.
184struct ModifierChainMeasurement {
185    result: MeasureResult,
186    /// Content offset for scroll/inner transforms - NOT padding semantics
187    content_offset: Point,
188    /// Node's own offset (from OffsetNode, affects position in parent)
189    offset: Point,
190}
191
192/// Discrete event callback reference produced during semantics extraction.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct SemanticsCallback {
195    node_id: NodeId,
196}
197
198impl SemanticsCallback {
199    pub fn new(node_id: NodeId) -> Self {
200        Self { node_id }
201    }
202
203    pub fn node_id(&self) -> NodeId {
204        self.node_id
205    }
206}
207
208/// Semantics action exposed to the input system.
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub enum SemanticsAction {
211    Click { handler: SemanticsCallback },
212}
213
214/// Semantic role describing how a node should participate in accessibility and hit testing.
215/// Roles are now derived from SemanticsConfiguration rather than widget types.
216#[derive(Clone, Debug, PartialEq, Eq)]
217pub enum SemanticsRole {
218    /// Generic container or layout node
219    Layout,
220    /// Subcomposition boundary
221    Subcompose,
222    /// Text content (derived from TextNode for backward compatibility)
223    Text { value: String },
224    /// Spacer (non-interactive)
225    Spacer,
226    /// Button (derived from is_button semantics flag)
227    Button,
228    /// Unknown or unspecified role
229    Unknown,
230}
231
232/// A single node within the semantics tree.
233#[derive(Clone, Debug)]
234pub struct SemanticsNode {
235    pub node_id: NodeId,
236    pub role: SemanticsRole,
237    pub actions: Vec<SemanticsAction>,
238    pub children: Vec<SemanticsNode>,
239    pub description: Option<String>,
240}
241
242impl SemanticsNode {
243    fn new(
244        node_id: NodeId,
245        role: SemanticsRole,
246        actions: Vec<SemanticsAction>,
247        children: Vec<SemanticsNode>,
248        description: Option<String>,
249    ) -> Self {
250        Self {
251            node_id,
252            role,
253            actions,
254            children,
255            description,
256        }
257    }
258}
259
260/// Rooted semantics tree extracted after layout.
261#[derive(Clone, Debug)]
262pub struct SemanticsTree {
263    root: SemanticsNode,
264}
265
266impl SemanticsTree {
267    fn new(root: SemanticsNode) -> Self {
268        Self { root }
269    }
270
271    pub fn root(&self) -> &SemanticsNode {
272        &self.root
273    }
274}
275
276/// Caches semantics configurations for layout nodes, similar to Jetpack Compose's SemanticsOwner.
277/// This enables lazy semantics tree construction and efficient invalidation.
278#[derive(Default)]
279pub struct SemanticsOwner {
280    configurations: RefCell<HashMap<NodeId, Option<SemanticsConfiguration>>>,
281}
282
283impl SemanticsOwner {
284    pub fn new() -> Self {
285        Self {
286            configurations: RefCell::new(HashMap::default()),
287        }
288    }
289
290    /// Returns the cached configuration for the given node, computing it if necessary.
291    pub fn get_or_compute(
292        &self,
293        node_id: NodeId,
294        applier: &mut MemoryApplier,
295    ) -> Option<SemanticsConfiguration> {
296        // Check cache first
297        if let Some(cached) = self.configurations.borrow().get(&node_id) {
298            return cached.clone();
299        }
300
301        // Compute and cache
302        let config = compute_semantics_for_node(applier, node_id);
303        self.configurations
304            .borrow_mut()
305            .insert(node_id, config.clone());
306        config
307    }
308}
309
310/// Result of running layout for a Compose tree.
311#[derive(Debug, Clone)]
312pub struct LayoutTree {
313    root: LayoutBox,
314}
315
316impl LayoutTree {
317    pub fn new(root: LayoutBox) -> Self {
318        Self { root }
319    }
320
321    pub fn root(&self) -> &LayoutBox {
322        &self.root
323    }
324
325    pub fn root_mut(&mut self) -> &mut LayoutBox {
326        &mut self.root
327    }
328
329    pub fn into_root(self) -> LayoutBox {
330        self.root
331    }
332}
333
334/// Layout information for a single node.
335#[derive(Debug, Clone)]
336pub struct LayoutBox {
337    pub node_id: NodeId,
338    pub rect: GeometryRect,
339    /// Content offset for scroll/inner transforms (applies to children, NOT this node's position)
340    pub content_offset: Point,
341    pub node_data: LayoutNodeData,
342    pub children: Vec<LayoutBox>,
343}
344
345impl LayoutBox {
346    pub fn new(
347        node_id: NodeId,
348        rect: GeometryRect,
349        content_offset: Point,
350        node_data: LayoutNodeData,
351        children: Vec<LayoutBox>,
352    ) -> Self {
353        Self {
354            node_id,
355            rect,
356            content_offset,
357            node_data,
358            children,
359        }
360    }
361}
362
363/// Snapshot of the data required to render a layout node.
364#[derive(Debug, Clone)]
365pub struct LayoutNodeData {
366    pub modifier: Modifier,
367    pub resolved_modifiers: ResolvedModifiers,
368    pub modifier_slices: Rc<ModifierNodeSlices>,
369    pub kind: LayoutNodeKind,
370}
371
372impl LayoutNodeData {
373    pub fn new(
374        modifier: Modifier,
375        resolved_modifiers: ResolvedModifiers,
376        modifier_slices: Rc<ModifierNodeSlices>,
377        kind: LayoutNodeKind,
378    ) -> Self {
379        Self {
380            modifier,
381            resolved_modifiers,
382            modifier_slices,
383            kind,
384        }
385    }
386
387    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
388        self.resolved_modifiers
389    }
390
391    pub fn modifier_slices(&self) -> &ModifierNodeSlices {
392        &self.modifier_slices
393    }
394}
395
396/// Classification of the node captured inside a [`LayoutBox`].
397///
398/// Note: Text content is no longer represented as a distinct LayoutNodeKind.
399/// Text nodes now use `LayoutNodeKind::Layout` with their content stored in
400/// `modifier_slices.text_content()` via TextModifierNode, following Jetpack
401/// Compose's pattern where text is a modifier node capability.
402#[derive(Clone)]
403pub enum LayoutNodeKind {
404    Layout,
405    Subcompose,
406    Spacer,
407    Button { on_click: Rc<RefCell<dyn FnMut()>> },
408    Unknown,
409}
410
411impl fmt::Debug for LayoutNodeKind {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        match self {
414            LayoutNodeKind::Layout => f.write_str("Layout"),
415            LayoutNodeKind::Subcompose => f.write_str("Subcompose"),
416            LayoutNodeKind::Spacer => f.write_str("Spacer"),
417            LayoutNodeKind::Button { .. } => f.write_str("Button"),
418            LayoutNodeKind::Unknown => f.write_str("Unknown"),
419        }
420    }
421}
422
423/// Extension trait that equips `MemoryApplier` with layout computation.
424pub trait LayoutEngine {
425    fn compute_layout(&mut self, root: NodeId, max_size: Size) -> Result<LayoutTree, NodeError>;
426}
427
428impl LayoutEngine for MemoryApplier {
429    fn compute_layout(&mut self, root: NodeId, max_size: Size) -> Result<LayoutTree, NodeError> {
430        let measurements = measure_layout(self, root, max_size)?;
431        Ok(measurements.into_layout_tree())
432    }
433}
434
435/// Result of running the measure pass for a Compose layout tree.
436#[derive(Debug, Clone)]
437pub struct LayoutMeasurements {
438    root: Rc<MeasuredNode>,
439    semantics: SemanticsTree,
440    layout_tree: LayoutTree,
441}
442
443impl LayoutMeasurements {
444    fn new(root: Rc<MeasuredNode>, semantics: SemanticsTree, layout_tree: LayoutTree) -> Self {
445        Self {
446            root,
447            semantics,
448            layout_tree,
449        }
450    }
451
452    /// Returns the measured size of the root node.
453    pub fn root_size(&self) -> Size {
454        self.root.size
455    }
456
457    pub fn semantics_tree(&self) -> &SemanticsTree {
458        &self.semantics
459    }
460
461    /// Consumes the measurements and produces a [`LayoutTree`].
462    pub fn into_layout_tree(self) -> LayoutTree {
463        self.layout_tree
464    }
465
466    /// Returns a borrowed [`LayoutTree`] for rendering.
467    pub fn layout_tree(&self) -> LayoutTree {
468        self.layout_tree.clone()
469    }
470}
471
472/// Check if a node or any of its descendants needs measure (selective measure optimization).
473/// This can be used by the app shell to skip layout when the tree is clean.
474///
475/// O(1) check - just looks at root's dirty flag.
476/// Works because all mutation paths bubble dirty flags to root via composer commands.
477///
478/// Returns Result to force caller to handle errors explicitly. No more unwrap_or(true) safety net.
479pub fn tree_needs_layout(applier: &mut dyn Applier, root: NodeId) -> Result<bool, NodeError> {
480    // Just check root - bubbling ensures it's dirty if any descendant is dirty
481    let node = applier.get_mut(root)?;
482    let layout_node =
483        node.as_any_mut()
484            .downcast_mut::<LayoutNode>()
485            .ok_or(NodeError::TypeMismatch {
486                id: root,
487                expected: std::any::type_name::<LayoutNode>(),
488            })?;
489    Ok(layout_node.needs_layout())
490}
491
492/// Test helper: bubbles layout dirty flag to root.
493#[cfg(test)]
494pub(crate) fn bubble_layout_dirty(applier: &mut MemoryApplier, node_id: NodeId) {
495    cranpose_core::bubble_layout_dirty(applier as &mut dyn Applier, node_id);
496}
497
498/// Runs the measure phase for the subtree rooted at `root`.
499pub fn measure_layout(
500    applier: &mut MemoryApplier,
501    root: NodeId,
502    max_size: Size,
503) -> Result<LayoutMeasurements, NodeError> {
504    let constraints = Constraints {
505        min_width: 0.0,
506        max_width: max_size.width,
507        min_height: 0.0,
508        max_height: max_size.height,
509    };
510
511    // Selective measure: only increment epoch if something needs MEASURING (not just layout)
512    // O(1) check - just look at root's dirty flag (bubbling ensures correctness)
513    //
514    // CRITICAL: We check needs_MEASURE, not needs_LAYOUT!
515    // - needs_measure: size may change, caches must be invalidated
516    // - needs_layout: position may change but size is cached (e.g., scroll)
517    //
518    // Scroll operations bubble needs_layout to ancestors, but NOT needs_measure.
519    // Using needs_layout here would wipe ALL caches on every scroll frame, causing
520    // O(N) full remeasurement instead of O(changed nodes).
521    let (needs_remeasure, _needs_semantics, cached_epoch) = match applier
522        .with_node::<LayoutNode, _>(root, |node| {
523            (
524                node.needs_measure(), // CORRECT: check needs_measure, not needs_layout
525                node.needs_semantics(),
526                node.cache_handles().epoch(),
527            )
528        }) {
529        Ok(tuple) => tuple,
530        Err(NodeError::TypeMismatch { .. }) => {
531            let node = applier.get_mut(root)?;
532            // For non-LayoutNode roots, check needs_layout as fallback
533            let measure_dirty = node.needs_layout();
534            let semantics_dirty = node.needs_semantics();
535            (measure_dirty, semantics_dirty, 0)
536        }
537        Err(err) => return Err(err),
538    };
539
540    let epoch = if needs_remeasure {
541        NEXT_CACHE_EPOCH.fetch_add(1, Ordering::Relaxed)
542    } else if cached_epoch != 0 {
543        cached_epoch
544    } else {
545        // Fallback when caller root isn't a LayoutNode (e.g. tests using Spacer directly).
546        NEXT_CACHE_EPOCH.load(Ordering::Relaxed)
547    };
548
549    // Move the current applier into a host and set up a guard that will
550    // ALWAYS restore:
551    // - the MemoryApplier back into `applier`
552    // - the SlotBackend back into that MemoryApplier
553    //
554    // IMPORTANT: Declare the guard *before* the builder so the builder
555    // is dropped first (both on Ok and on unwind).
556    let guard = ApplierSlotGuard::new(applier);
557    let applier_host = guard.host();
558    let slots_handle = guard.slots_handle();
559
560    // Give the builder the shared slots handle - both guard and builder
561    // now share access to the same SlotBackend via Rc<RefCell<_>>.
562    let mut builder =
563        LayoutBuilder::new_with_epoch(Rc::clone(&applier_host), epoch, Rc::clone(&slots_handle));
564
565    // ---- Measurement -------------------------------------------------------
566    // If measurement fails, the guard will restore slots from the shared handle
567    // on drop - this is safe because the handle always contains valid slots.
568
569    let measured = builder.measure_node(root, normalize_constraints(constraints))?;
570
571    // Root node has no parent to place it, so we must explicitly place it at (0,0).
572    // This ensures is_placed=true, allowing the renderer to traverse the tree.
573    // Handle both LayoutNode and SubcomposeLayoutNode as potential roots.
574    if let Ok(mut applier) = applier_host.try_borrow_typed() {
575        if applier
576            .with_node::<LayoutNode, _>(root, |node| {
577                node.set_position(Point::default());
578            })
579            .is_err()
580        {
581            let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
582                node.set_position(Point::default());
583            });
584        }
585    }
586
587    // ---- Metadata ----------------------------------------------------------
588    let metadata = {
589        let mut applier_ref = applier_host.borrow_typed();
590        collect_runtime_metadata(&mut applier_ref, &measured)?
591    };
592
593    // ---- Semantics snapshot ------------------------------------------------
594    let semantics_snapshot = {
595        let mut applier_ref = applier_host.borrow_typed();
596        collect_semantics_snapshot(&mut applier_ref, &measured)?
597    };
598
599    // Drop builder before guard - slots are already in the shared handle.
600    // Guard's Drop will write them back to the applier.
601    drop(builder);
602
603    // DO NOT manually unwrap `applier_host` or replace `applier` here.
604    // `ApplierSlotGuard::drop` will restore everything when this function returns.
605
606    // Build semantics and layout trees from `measured` + metadata + snapshot
607    let semantics_root = build_semantics_node(&measured, &metadata, &semantics_snapshot);
608    let semantics = SemanticsTree::new(semantics_root);
609    let layout_tree = build_layout_tree_from_metadata(&measured, &metadata);
610
611    Ok(LayoutMeasurements::new(measured, semantics, layout_tree))
612}
613
614struct LayoutBuilder {
615    state: Rc<RefCell<LayoutBuilderState>>,
616}
617
618impl LayoutBuilder {
619    fn new_with_epoch(
620        applier: Rc<ConcreteApplierHost<MemoryApplier>>,
621        epoch: u64,
622        slots: Rc<RefCell<SlotBackend>>,
623    ) -> Self {
624        Self {
625            state: Rc::new(RefCell::new(LayoutBuilderState::new_with_epoch(
626                applier, epoch, slots,
627            ))),
628        }
629    }
630
631    fn measure_node(
632        &mut self,
633        node_id: NodeId,
634        constraints: Constraints,
635    ) -> Result<Rc<MeasuredNode>, NodeError> {
636        LayoutBuilderState::measure_node(Rc::clone(&self.state), node_id, constraints)
637    }
638
639    fn set_runtime_handle(&mut self, handle: Option<RuntimeHandle>) {
640        self.state.borrow_mut().runtime_handle = handle;
641    }
642}
643
644struct LayoutBuilderState {
645    applier: Rc<ConcreteApplierHost<MemoryApplier>>,
646    runtime_handle: Option<RuntimeHandle>,
647    /// Shared handle to the slot table. This is shared with ApplierSlotGuard
648    /// to ensure panic-safety: even if we panic, the guard can restore slots.
649    slots: Rc<RefCell<SlotBackend>>,
650    cache_epoch: u64,
651    tmp_measurables: Vec<Box<dyn Measurable>>,
652    tmp_records: Vec<(NodeId, ChildRecord)>,
653}
654
655impl LayoutBuilderState {
656    fn new_with_epoch(
657        applier: Rc<ConcreteApplierHost<MemoryApplier>>,
658        epoch: u64,
659        slots: Rc<RefCell<SlotBackend>>,
660    ) -> Self {
661        let runtime_handle = applier.borrow_typed().runtime_handle();
662
663        Self {
664            applier,
665            runtime_handle,
666            slots,
667            cache_epoch: epoch,
668            tmp_measurables: Vec::new(),
669            tmp_records: Vec::new(),
670        }
671    }
672
673    fn try_with_applier_result<R>(
674        state_rc: &Rc<RefCell<Self>>,
675        f: impl FnOnce(&mut MemoryApplier) -> Result<R, NodeError>,
676    ) -> Option<Result<R, NodeError>> {
677        let host = {
678            let state = state_rc.borrow();
679            Rc::clone(&state.applier)
680        };
681
682        // Try to borrow - if already borrowed (nested call), return None
683        let Ok(mut applier) = host.try_borrow_typed() else {
684            return None;
685        };
686
687        Some(f(&mut applier))
688    }
689
690    fn with_applier_result<R>(
691        state_rc: &Rc<RefCell<Self>>,
692        f: impl FnOnce(&mut MemoryApplier) -> Result<R, NodeError>,
693    ) -> Result<R, NodeError> {
694        Self::try_with_applier_result(state_rc, f).unwrap_or_else(|| {
695            Err(NodeError::MissingContext {
696                id: NodeId::default(),
697                reason: "applier already borrowed",
698            })
699        })
700    }
701
702    /// Clears the is_placed flag for a node at the start of measurement.
703    /// This ensures nodes that drop out of placement won't render with stale geometry.
704    fn clear_node_placed(state_rc: &Rc<RefCell<Self>>, node_id: NodeId) {
705        let host = {
706            let state = state_rc.borrow();
707            Rc::clone(&state.applier)
708        };
709        let Ok(mut applier) = host.try_borrow_typed() else {
710            return;
711        };
712        // Try LayoutNode first, then SubcomposeLayoutNode
713        if applier
714            .with_node::<LayoutNode, _>(node_id, |node| {
715                node.clear_placed();
716            })
717            .is_err()
718        {
719            let _ = applier.with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
720                node.clear_placed();
721            });
722        }
723    }
724
725    fn measure_node(
726        state_rc: Rc<RefCell<Self>>,
727        node_id: NodeId,
728        constraints: Constraints,
729    ) -> Result<Rc<MeasuredNode>, NodeError> {
730        // Clear is_placed at the start of measurement.
731        // Nodes that are placed will have is_placed set to true via Placeable::place().
732        // Nodes that drop out of placement (not placed this pass) will remain is_placed=false.
733        Self::clear_node_placed(&state_rc, node_id);
734
735        // Try SubcomposeLayoutNode first
736        if let Some(subcompose) =
737            Self::try_measure_subcompose(Rc::clone(&state_rc), node_id, constraints)?
738        {
739            return Ok(subcompose);
740        }
741
742        // Try LayoutNode (the primary modern path)
743        if let Some(result) = Self::try_with_applier_result(&state_rc, |applier| {
744            match applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
745                LayoutNodeSnapshot::from_layout_node(layout_node)
746            }) {
747                Ok(snapshot) => Ok(Some(snapshot)),
748                Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => Ok(None),
749                Err(err) => Err(err),
750            }
751        }) {
752            // Applier was available, process the result
753            if let Some(snapshot) = result? {
754                return Self::measure_layout_node(
755                    Rc::clone(&state_rc),
756                    node_id,
757                    snapshot,
758                    constraints,
759                );
760            }
761        }
762        // If applier was busy (None) or snapshot was None, fall through to fallback
763
764        // No legacy fallbacks - all widgets now use LayoutNode or SubcomposeLayoutNode
765        // If we reach here, it's an unknown node type (shouldn't happen in normal use)
766        Ok(Rc::new(MeasuredNode::new(
767            node_id,
768            Size::default(),
769            Point { x: 0.0, y: 0.0 },
770            Point::default(), // No content offset for fallback nodes
771            Vec::new(),
772        )))
773    }
774
775    fn try_measure_subcompose(
776        state_rc: Rc<RefCell<Self>>,
777        node_id: NodeId,
778        constraints: Constraints,
779    ) -> Result<Option<Rc<MeasuredNode>>, NodeError> {
780        let applier_host = {
781            let state = state_rc.borrow();
782            Rc::clone(&state.applier)
783        };
784
785        let (node_handle, resolved_modifiers) = {
786            // Try to borrow - if already borrowed (nested measurement), return None
787            let Ok(mut applier) = applier_host.try_borrow_typed() else {
788                return Ok(None);
789            };
790            let node = match applier.get_mut(node_id) {
791                Ok(node) => node,
792                Err(NodeError::Missing { .. }) => return Ok(None),
793                Err(err) => return Err(err),
794            };
795            let any = node.as_any_mut();
796            if let Some(subcompose) =
797                any.downcast_mut::<crate::subcompose_layout::SubcomposeLayoutNode>()
798            {
799                let handle = subcompose.handle();
800                let resolved_modifiers = handle.resolved_modifiers();
801                (handle, resolved_modifiers)
802            } else {
803                return Ok(None);
804            }
805        };
806
807        let runtime_handle = {
808            let mut state = state_rc.borrow_mut();
809            if state.runtime_handle.is_none() {
810                // Try to borrow - if already borrowed, we can't get runtime handle
811                if let Ok(applier) = applier_host.try_borrow_typed() {
812                    state.runtime_handle = applier.runtime_handle();
813                }
814            }
815            state
816                .runtime_handle
817                .clone()
818                .ok_or(NodeError::MissingContext {
819                    id: node_id,
820                    reason: "runtime handle required for subcomposition",
821                })?
822        };
823
824        let props = resolved_modifiers.layout_properties();
825        let padding = resolved_modifiers.padding();
826        let offset = resolved_modifiers.offset();
827        let mut inner_constraints = normalize_constraints(subtract_padding(constraints, padding));
828
829        if let DimensionConstraint::Points(width) = props.width() {
830            let constrained_width = width - padding.horizontal_sum();
831            inner_constraints.max_width = inner_constraints.max_width.min(constrained_width);
832            inner_constraints.min_width = inner_constraints.min_width.min(constrained_width);
833        }
834        if let DimensionConstraint::Points(height) = props.height() {
835            let constrained_height = height - padding.vertical_sum();
836            inner_constraints.max_height = inner_constraints.max_height.min(constrained_height);
837            inner_constraints.min_height = inner_constraints.min_height.min(constrained_height);
838        }
839
840        let mut slots_guard = SlotsGuard::take(Rc::clone(&state_rc));
841        let slots_host = slots_guard.host();
842        let applier_host_dyn: Rc<dyn ApplierHost> = applier_host.clone();
843        let observer = SnapshotStateObserver::new(|callback| callback());
844        let composer = Composer::new(
845            Rc::clone(&slots_host),
846            applier_host_dyn,
847            runtime_handle.clone(),
848            observer,
849            Some(node_id),
850        );
851        composer.enter_phase(Phase::Measure);
852
853        let state_rc_clone = Rc::clone(&state_rc);
854        let measure_error: Rc<RefCell<Option<NodeError>>> = Rc::new(RefCell::new(None));
855        let error_for_measurer = Rc::clone(&measure_error);
856        let measurer = Box::new(
857            move |child_id: NodeId, child_constraints: Constraints| -> Size {
858                match Self::measure_node(Rc::clone(&state_rc_clone), child_id, child_constraints) {
859                    Ok(measured) => measured.size,
860                    Err(err) => {
861                        let mut slot = error_for_measurer.borrow_mut();
862                        if slot.is_none() {
863                            *slot = Some(err);
864                        }
865                        Size::default()
866                    }
867                }
868            },
869        );
870
871        let measure_result = node_handle.measure(
872            &composer,
873            node_id,
874            inner_constraints,
875            measurer,
876            Rc::clone(&measure_error),
877        )?;
878
879        slots_guard.restore(slots_host.take());
880
881        if let Some(err) = measure_error.borrow_mut().take() {
882            return Err(err);
883        }
884
885        // NOTE: Children are now managed by the composer via insert_child commands
886        // (from parent_stack initialization with root). set_active_children is no longer used.
887
888        let mut width = measure_result.size.width + padding.horizontal_sum();
889        let mut height = measure_result.size.height + padding.vertical_sum();
890
891        width = resolve_dimension(
892            width,
893            props.width(),
894            props.min_width(),
895            props.max_width(),
896            constraints.min_width,
897            constraints.max_width,
898        );
899        height = resolve_dimension(
900            height,
901            props.height(),
902            props.min_height(),
903            props.max_height(),
904            constraints.min_height,
905            constraints.max_height,
906        );
907
908        let mut children = Vec::new();
909
910        // Update the SubcomposeLayoutNode's size (position will be set by parent's placement)
911        if let Ok(mut applier) = applier_host.try_borrow_typed() {
912            let _ = applier.with_node::<SubcomposeLayoutNode, _>(node_id, |parent_node| {
913                parent_node.set_measured_size(Size { width, height });
914            });
915        }
916
917        for placement in measure_result.placements {
918            let child =
919                Self::measure_node(Rc::clone(&state_rc), placement.node_id, inner_constraints)?;
920            let position = Point {
921                x: padding.left + placement.x,
922                y: padding.top + placement.y,
923            };
924
925            // Critical: Update the child LayoutNode's retained state.
926            // Standard layouts do this via Placeable::place(), but SubcomposeLayout logic
927            // bypasses Placeables and returns raw Placements.
928            if let Ok(mut applier) = applier_host.try_borrow_typed() {
929                let _ = applier.with_node::<LayoutNode, _>(placement.node_id, |node| {
930                    node.set_position(position);
931                });
932            }
933
934            children.push(MeasuredChild {
935                node: child,
936                offset: position,
937            });
938        }
939
940        // Update the SubcomposeLayoutNode's active children for rendering
941        node_handle.set_active_children(children.iter().map(|c| c.node.node_id));
942
943        Ok(Some(Rc::new(MeasuredNode::new(
944            node_id,
945            Size { width, height },
946            offset,
947            Point::default(), // Subcompose nodes: content_offset handled by child layout
948            children,
949        ))))
950    }
951    /// Measures through the layout modifier coordinator chain using reconciled modifier nodes.
952    /// Iterates through LayoutModifierNode instances from the ModifierNodeChain and calls
953    /// their measure() methods, mirroring Jetpack Compose's LayoutModifierNodeCoordinator pattern.
954    ///
955    /// Always succeeds, building a coordinator chain (possibly just InnerCoordinator) to measure.
956    ///
957    fn measure_through_modifier_chain(
958        state_rc: &Rc<RefCell<Self>>,
959        node_id: NodeId,
960        measurables: &[Box<dyn Measurable>],
961        measure_policy: &Rc<dyn MeasurePolicy>,
962        constraints: Constraints,
963    ) -> ModifierChainMeasurement {
964        use cranpose_foundation::NodeCapabilities;
965
966        // Collect layout node information from the modifier chain
967        #[allow(clippy::type_complexity)]
968        // Tuple of (index, boxed trait object) is reasonable for modifier nodes
969        let mut layout_node_data: Vec<(
970            usize,
971            Rc<RefCell<Box<dyn cranpose_foundation::ModifierNode>>>,
972        )> = Vec::new();
973        let mut offset = Point::default();
974
975        {
976            let state = state_rc.borrow();
977            let mut applier = state.applier.borrow_typed();
978
979            let _ = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
980                let chain_handle = layout_node.modifier_chain();
981
982                if !chain_handle.has_layout_nodes() {
983                    return;
984                }
985
986                // Collect indices and node Rc clones for layout modifier nodes
987                chain_handle.chain().for_each_forward_matching(
988                    NodeCapabilities::LAYOUT,
989                    |node_ref| {
990                        if let Some(index) = node_ref.entry_index() {
991                            // Get the Rc clone for this node
992                            if let Some(node_rc) = chain_handle.chain().get_node_rc(index) {
993                                layout_node_data.push((index, Rc::clone(&node_rc)));
994                            }
995
996                            // Extract offset from OffsetNode for the node's own position
997                            // The coordinator chain handles placement_offset (for children),
998                            // but the node's offset affects where IT is positioned in the parent
999                            node_ref.with_node(|node| {
1000                                if let Some(offset_node) =
1001                                    node.as_any()
1002                                        .downcast_ref::<crate::modifier_nodes::OffsetNode>()
1003                                {
1004                                    let delta = offset_node.offset();
1005                                    offset.x += delta.x;
1006                                    offset.y += delta.y;
1007                                }
1008                            });
1009                        }
1010                    },
1011                );
1012            });
1013        }
1014
1015        // Fast path: if there are no layout modifiers, measure directly without coordinator chain.
1016        // This saves 3 allocations (shared_context, policy_result, InnerCoordinator box).
1017        if layout_node_data.is_empty() {
1018            let result = measure_policy.measure(measurables, constraints);
1019            let final_size = result.size;
1020            let placements = result.placements;
1021
1022            return ModifierChainMeasurement {
1023                result: MeasureResult {
1024                    size: final_size,
1025                    placements,
1026                },
1027                content_offset: Point::default(),
1028                offset,
1029            };
1030        }
1031
1032        // Slow path: build coordinator chain for layout modifiers
1033        // Reverse order: rightmost modifier is measured first (innermost), leftmost is outer
1034        layout_node_data.reverse();
1035
1036        // Create a shared context for this measurement pass to track invalidations
1037        let shared_context = Rc::new(RefCell::new(LayoutNodeContext::new()));
1038
1039        // Create the inner coordinator that wraps the measure policy
1040        let policy_result = Rc::new(RefCell::new(None));
1041        let inner_coordinator: Box<dyn NodeCoordinator + '_> =
1042            Box::new(coordinator::InnerCoordinator::new(
1043                Rc::clone(measure_policy),
1044                measurables,
1045                Rc::clone(&policy_result),
1046            ));
1047
1048        // Wrap each layout modifier node in a coordinator, building the chain
1049        let mut current_coordinator = inner_coordinator;
1050        for (_, node_rc) in layout_node_data {
1051            current_coordinator = Box::new(coordinator::LayoutModifierCoordinator::new(
1052                node_rc,
1053                current_coordinator,
1054                Rc::clone(&shared_context),
1055            ));
1056        }
1057
1058        // Measure through the complete coordinator chain
1059        let placeable = current_coordinator.measure(constraints);
1060        let final_size = Size {
1061            width: placeable.width(),
1062            height: placeable.height(),
1063        };
1064
1065        // Get accumulated content offset from the placeable (computed during measure)
1066        let content_offset = placeable.content_offset();
1067        let all_placement_offset = Point {
1068            x: content_offset.0,
1069            y: content_offset.1,
1070        };
1071
1072        // The content_offset for scroll/inner transforms is the accumulated placement offset
1073        // MINUS the node's own offset (which affects its position in the parent, not content position).
1074        // This properly separates: node position (offset) vs inner content position (content_offset).
1075        let content_offset = Point {
1076            x: all_placement_offset.x - offset.x,
1077            y: all_placement_offset.y - offset.y,
1078        };
1079
1080        // offset was already extracted from OffsetNode above
1081
1082        let placements = policy_result
1083            .borrow_mut()
1084            .take()
1085            .map(|result| result.placements)
1086            .unwrap_or_default();
1087
1088        // Process any invalidations requested during measurement
1089        let invalidations = shared_context.borrow_mut().take_invalidations();
1090        if !invalidations.is_empty() {
1091            // Mark the LayoutNode as needing the appropriate passes
1092            Self::with_applier_result(state_rc, |applier| {
1093                applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
1094                    for kind in invalidations {
1095                        match kind {
1096                            InvalidationKind::Layout => layout_node.mark_needs_measure(),
1097                            InvalidationKind::Draw => layout_node.mark_needs_redraw(),
1098                            InvalidationKind::Semantics => layout_node.mark_needs_semantics(),
1099                            InvalidationKind::PointerInput => layout_node.mark_needs_pointer_pass(),
1100                            InvalidationKind::Focus => layout_node.mark_needs_focus_sync(),
1101                        }
1102                    }
1103                })
1104            })
1105            .ok();
1106        }
1107
1108        ModifierChainMeasurement {
1109            result: MeasureResult {
1110                size: final_size,
1111                placements,
1112            },
1113            content_offset,
1114            offset,
1115        }
1116    }
1117
1118    fn measure_layout_node(
1119        state_rc: Rc<RefCell<Self>>,
1120        node_id: NodeId,
1121        snapshot: LayoutNodeSnapshot,
1122        constraints: Constraints,
1123    ) -> Result<Rc<MeasuredNode>, NodeError> {
1124        let cache_epoch = {
1125            let state = state_rc.borrow();
1126            state.cache_epoch
1127        };
1128        let LayoutNodeSnapshot {
1129            resolved_modifiers,
1130            measure_policy,
1131            children,
1132            cache,
1133            needs_measure,
1134        } = snapshot;
1135        cache.activate(cache_epoch);
1136        let layout_props = resolved_modifiers.layout_properties();
1137
1138        if needs_measure {
1139            // Node has needs_measure=true
1140        }
1141
1142        // Only check cache if not marked as needing measure.
1143        // When needs_measure=true, we MUST re-run measure() even if constraints match,
1144        // because something else changed (e.g., scroll offset, modifier state).
1145        if !needs_measure {
1146            // Check cache for current constraints
1147            if let Some(cached) = cache.get_measurement(constraints) {
1148                // Clear dirty flag after successful cache hit
1149                Self::with_applier_result(&state_rc, |applier| {
1150                    applier.with_node::<LayoutNode, _>(node_id, |node| {
1151                        node.clear_needs_measure();
1152                        node.clear_needs_layout();
1153                    })
1154                })
1155                .ok();
1156                return Ok(cached);
1157            }
1158        }
1159
1160        let (runtime_handle, applier_host) = {
1161            let state = state_rc.borrow();
1162            (state.runtime_handle.clone(), Rc::clone(&state.applier))
1163        };
1164
1165        let measure_handle = LayoutMeasureHandle::new(Rc::clone(&state_rc));
1166        let error = Rc::new(RefCell::new(None));
1167        let mut pools = VecPools::acquire(Rc::clone(&state_rc));
1168        let (measurables, records) = pools.parts();
1169
1170        for &child_id in children.iter() {
1171            let measured = Rc::new(RefCell::new(None));
1172            let position = Rc::new(RefCell::new(None));
1173
1174            let data = {
1175                let mut applier = applier_host.borrow_typed();
1176                match applier.with_node::<LayoutNode, _>(child_id, |n| {
1177                    (n.cache_handles(), n.layout_state_handle())
1178                }) {
1179                    Ok((cache, state)) => Some((cache, Some(state))),
1180                    Err(NodeError::TypeMismatch { .. }) => {
1181                        Some((LayoutNodeCacheHandles::default(), None))
1182                    }
1183                    Err(NodeError::Missing { .. }) => None,
1184                    Err(err) => return Err(err),
1185                }
1186            };
1187
1188            let Some((cache_handles, layout_state)) = data else {
1189                continue;
1190            };
1191
1192            cache_handles.activate(cache_epoch);
1193
1194            records.push((
1195                child_id,
1196                ChildRecord {
1197                    measured: Rc::clone(&measured),
1198                    last_position: Rc::clone(&position),
1199                },
1200            ));
1201            measurables.push(Box::new(LayoutChildMeasurable::new(
1202                Rc::clone(&applier_host),
1203                child_id,
1204                measured,
1205                position,
1206                Rc::clone(&error),
1207                runtime_handle.clone(),
1208                cache_handles,
1209                cache_epoch,
1210                Some(measure_handle.clone()),
1211                layout_state,
1212            )));
1213        }
1214
1215        // Try to measure through the modifier node chain first.
1216        // If a node participates in weight-based sizing, never loosen constraints;
1217        // otherwise, use unbounded max on unspecified dimensions to support wrap-content
1218        // behavior without fill modifiers forcing parent size.
1219        let has_weight = layout_props
1220            .weight()
1221            .map(|weight| weight.weight > 0.0)
1222            .unwrap_or(false);
1223        let chain_constraints = if has_weight {
1224            constraints
1225        } else {
1226            Constraints {
1227                min_width: constraints.min_width,
1228                max_width: if matches!(layout_props.width(), DimensionConstraint::Unspecified) {
1229                    f32::INFINITY
1230                } else {
1231                    constraints.max_width
1232                },
1233                min_height: constraints.min_height,
1234                max_height: if matches!(layout_props.height(), DimensionConstraint::Unspecified) {
1235                    f32::INFINITY
1236                } else {
1237                    constraints.max_height
1238                },
1239            }
1240        };
1241
1242        let mut modifier_chain_result = Self::measure_through_modifier_chain(
1243            &state_rc,
1244            node_id,
1245            measurables.as_slice(),
1246            &measure_policy,
1247            chain_constraints,
1248        );
1249
1250        if (chain_constraints.max_width != constraints.max_width
1251            || chain_constraints.max_height != constraints.max_height)
1252            && ((constraints.max_width.is_finite()
1253                && modifier_chain_result.result.size.width > constraints.max_width)
1254                || (constraints.max_height.is_finite()
1255                    && modifier_chain_result.result.size.height > constraints.max_height))
1256        {
1257            modifier_chain_result = Self::measure_through_modifier_chain(
1258                &state_rc,
1259                node_id,
1260                measurables.as_slice(),
1261                &measure_policy,
1262                constraints,
1263            );
1264        }
1265
1266        // Modifier chain always succeeds - use the node-driven measurement.
1267        let (width, height, policy_result, content_offset, offset) = {
1268            let result = modifier_chain_result;
1269            // The size is already correct from the modifier chain (modifiers like SizeNode
1270            // have already enforced their constraints), so we use it directly.
1271            if let Some(err) = error.borrow_mut().take() {
1272                return Err(err);
1273            }
1274
1275            (
1276                result.result.size.width,
1277                result.result.size.height,
1278                result.result,
1279                result.content_offset,
1280                result.offset,
1281            )
1282        };
1283
1284        let mut measured_children = Vec::new();
1285        for &child_id in children.iter() {
1286            if let Some((_, record)) = records.iter().find(|(id, _)| *id == child_id) {
1287                if let Some(measured) = record.measured.borrow_mut().take() {
1288                    let base_position = policy_result
1289                        .placements
1290                        .iter()
1291                        .find(|placement| placement.node_id == child_id)
1292                        .map(|placement| Point {
1293                            x: placement.x,
1294                            y: placement.y,
1295                        })
1296                        .or_else(|| record.last_position.borrow().as_ref().copied())
1297                        .unwrap_or(Point { x: 0.0, y: 0.0 });
1298                    // Apply content_offset (from scroll/transforms) to child positioning
1299                    let position = Point {
1300                        x: content_offset.x + base_position.x,
1301                        y: content_offset.y + base_position.y,
1302                    };
1303                    measured_children.push(MeasuredChild {
1304                        node: measured,
1305                        offset: position,
1306                    });
1307                }
1308            }
1309        }
1310
1311        let measured = Rc::new(MeasuredNode::new(
1312            node_id,
1313            Size { width, height },
1314            offset,
1315            content_offset,
1316            measured_children,
1317        ));
1318
1319        cache.store_measurement(constraints, Rc::clone(&measured));
1320
1321        // Clear dirty flags and update derived state
1322        Self::with_applier_result(&state_rc, |applier| {
1323            applier.with_node::<LayoutNode, _>(node_id, |node| {
1324                node.clear_needs_measure();
1325                node.clear_needs_layout();
1326                node.set_measured_size(Size { width, height });
1327                node.set_content_offset(content_offset);
1328            })
1329        })
1330        .ok();
1331
1332        Ok(measured)
1333    }
1334}
1335
1336/// Snapshot of a LayoutNode's data for measuring.
1337/// This is a temporary copy used during the measure phase, not a live node.
1338///
1339/// Note: We capture `needs_measure` here because it's checked during measure to enable
1340/// selective measure optimization at the individual node level. Even if the tree is partially
1341/// dirty (some nodes changed), clean nodes can skip measure and use cached results.
1342struct LayoutNodeSnapshot {
1343    resolved_modifiers: ResolvedModifiers,
1344    measure_policy: Rc<dyn MeasurePolicy>,
1345    children: Vec<NodeId>,
1346    cache: LayoutNodeCacheHandles,
1347    /// Whether this specific node needs to be measured (vs using cached measurement)
1348    needs_measure: bool,
1349}
1350
1351impl LayoutNodeSnapshot {
1352    fn from_layout_node(node: &LayoutNode) -> Self {
1353        Self {
1354            resolved_modifiers: node.resolved_modifiers(),
1355            measure_policy: Rc::clone(&node.measure_policy),
1356            children: node.children.iter().copied().collect(),
1357            cache: node.cache_handles(),
1358            needs_measure: node.needs_measure(),
1359        }
1360    }
1361}
1362
1363// Helper types for accessing subsets of LayoutBuilderState
1364struct VecPools {
1365    state: Rc<RefCell<LayoutBuilderState>>,
1366    measurables: Option<Vec<Box<dyn Measurable>>>,
1367    records: Option<Vec<(NodeId, ChildRecord)>>,
1368}
1369
1370impl VecPools {
1371    fn acquire(state: Rc<RefCell<LayoutBuilderState>>) -> Self {
1372        let measurables = {
1373            let mut state_mut = state.borrow_mut();
1374            std::mem::take(&mut state_mut.tmp_measurables)
1375        };
1376        let records = {
1377            let mut state_mut = state.borrow_mut();
1378            std::mem::take(&mut state_mut.tmp_records)
1379        };
1380        Self {
1381            state,
1382            measurables: Some(measurables),
1383            records: Some(records),
1384        }
1385    }
1386
1387    #[allow(clippy::type_complexity)] // Returns internal Vec references for layout operations
1388    fn parts(
1389        &mut self,
1390    ) -> (
1391        &mut Vec<Box<dyn Measurable>>,
1392        &mut Vec<(NodeId, ChildRecord)>,
1393    ) {
1394        let measurables = self
1395            .measurables
1396            .as_mut()
1397            .expect("measurables already returned");
1398        let records = self.records.as_mut().expect("records already returned");
1399        (measurables, records)
1400    }
1401}
1402
1403impl Drop for VecPools {
1404    fn drop(&mut self) {
1405        let mut state = self.state.borrow_mut();
1406        if let Some(mut measurables) = self.measurables.take() {
1407            measurables.clear();
1408            state.tmp_measurables = measurables;
1409        }
1410        if let Some(mut records) = self.records.take() {
1411            records.clear();
1412            state.tmp_records = records;
1413        }
1414    }
1415}
1416
1417struct SlotsGuard {
1418    state: Rc<RefCell<LayoutBuilderState>>,
1419    slots: Option<SlotBackend>,
1420}
1421
1422impl SlotsGuard {
1423    fn take(state: Rc<RefCell<LayoutBuilderState>>) -> Self {
1424        let slots = {
1425            let state_ref = state.borrow();
1426            let mut slots_ref = state_ref.slots.borrow_mut();
1427            std::mem::take(&mut *slots_ref)
1428        };
1429        Self {
1430            state,
1431            slots: Some(slots),
1432        }
1433    }
1434
1435    fn host(&mut self) -> Rc<SlotsHost> {
1436        let slots = self.slots.take().unwrap_or_default();
1437        Rc::new(SlotsHost::new(slots))
1438    }
1439
1440    fn restore(&mut self, slots: SlotBackend) {
1441        debug_assert!(self.slots.is_none());
1442        self.slots = Some(slots);
1443    }
1444}
1445
1446impl Drop for SlotsGuard {
1447    fn drop(&mut self) {
1448        if let Some(slots) = self.slots.take() {
1449            let state_ref = self.state.borrow();
1450            *state_ref.slots.borrow_mut() = slots;
1451        }
1452    }
1453}
1454
1455#[derive(Clone)]
1456struct LayoutMeasureHandle {
1457    state: Rc<RefCell<LayoutBuilderState>>,
1458}
1459
1460impl LayoutMeasureHandle {
1461    fn new(state: Rc<RefCell<LayoutBuilderState>>) -> Self {
1462        Self { state }
1463    }
1464
1465    fn measure(
1466        &self,
1467        node_id: NodeId,
1468        constraints: Constraints,
1469    ) -> Result<Rc<MeasuredNode>, NodeError> {
1470        LayoutBuilderState::measure_node(Rc::clone(&self.state), node_id, constraints)
1471    }
1472}
1473
1474#[derive(Debug, Clone)]
1475pub(crate) struct MeasuredNode {
1476    node_id: NodeId,
1477    size: Size,
1478    /// Node's position offset relative to parent (from OffsetNode etc.)
1479    offset: Point,
1480    /// Content offset for scroll/inner transforms (NOT node position)
1481    content_offset: Point,
1482    children: Vec<MeasuredChild>,
1483}
1484
1485impl MeasuredNode {
1486    fn new(
1487        node_id: NodeId,
1488        size: Size,
1489        offset: Point,
1490        content_offset: Point,
1491        children: Vec<MeasuredChild>,
1492    ) -> Self {
1493        Self {
1494            node_id,
1495            size,
1496            offset,
1497            content_offset,
1498            children,
1499        }
1500    }
1501}
1502
1503#[derive(Debug, Clone)]
1504struct MeasuredChild {
1505    node: Rc<MeasuredNode>,
1506    offset: Point,
1507}
1508
1509struct ChildRecord {
1510    measured: Rc<RefCell<Option<Rc<MeasuredNode>>>>,
1511    last_position: Rc<RefCell<Option<Point>>>,
1512}
1513
1514struct LayoutChildMeasurable {
1515    applier: Rc<ConcreteApplierHost<MemoryApplier>>,
1516    node_id: NodeId,
1517    measured: Rc<RefCell<Option<Rc<MeasuredNode>>>>,
1518    last_position: Rc<RefCell<Option<Point>>>,
1519    error: Rc<RefCell<Option<NodeError>>>,
1520    runtime_handle: Option<RuntimeHandle>,
1521    cache: LayoutNodeCacheHandles,
1522    cache_epoch: u64,
1523    measure_handle: Option<LayoutMeasureHandle>,
1524    layout_state: Option<Rc<RefCell<crate::widgets::nodes::layout_node::LayoutState>>>,
1525}
1526
1527impl LayoutChildMeasurable {
1528    #[allow(clippy::too_many_arguments)] // Constructor needs all layout state for child measurement
1529    fn new(
1530        applier: Rc<ConcreteApplierHost<MemoryApplier>>,
1531        node_id: NodeId,
1532        measured: Rc<RefCell<Option<Rc<MeasuredNode>>>>,
1533        last_position: Rc<RefCell<Option<Point>>>,
1534        error: Rc<RefCell<Option<NodeError>>>,
1535        runtime_handle: Option<RuntimeHandle>,
1536        cache: LayoutNodeCacheHandles,
1537        cache_epoch: u64,
1538        measure_handle: Option<LayoutMeasureHandle>,
1539        layout_state: Option<Rc<RefCell<crate::widgets::nodes::layout_node::LayoutState>>>,
1540    ) -> Self {
1541        cache.activate(cache_epoch);
1542        Self {
1543            applier,
1544            node_id,
1545            measured,
1546            last_position,
1547            error,
1548            runtime_handle,
1549            cache,
1550            cache_epoch,
1551            measure_handle,
1552            layout_state,
1553        }
1554    }
1555
1556    fn record_error(&self, err: NodeError) {
1557        let mut slot = self.error.borrow_mut();
1558        if slot.is_none() {
1559            *slot = Some(err);
1560        }
1561    }
1562
1563    fn perform_measure(&self, constraints: Constraints) -> Result<Rc<MeasuredNode>, NodeError> {
1564        if let Some(handle) = &self.measure_handle {
1565            handle.measure(self.node_id, constraints)
1566        } else {
1567            measure_node_with_host(
1568                Rc::clone(&self.applier),
1569                self.runtime_handle.clone(),
1570                self.node_id,
1571                constraints,
1572                self.cache_epoch,
1573            )
1574        }
1575    }
1576
1577    fn intrinsic_measure(&self, constraints: Constraints) -> Option<Rc<MeasuredNode>> {
1578        self.cache.activate(self.cache_epoch);
1579        if let Some(cached) = self.cache.get_measurement(constraints) {
1580            return Some(cached);
1581        }
1582
1583        match self.perform_measure(constraints) {
1584            Ok(measured) => {
1585                self.cache
1586                    .store_measurement(constraints, Rc::clone(&measured));
1587                Some(measured)
1588            }
1589            Err(err) => {
1590                self.record_error(err);
1591                None
1592            }
1593        }
1594    }
1595}
1596
1597impl Measurable for LayoutChildMeasurable {
1598    fn measure(&self, constraints: Constraints) -> Box<dyn Placeable> {
1599        self.cache.activate(self.cache_epoch);
1600        let measured_size;
1601        if let Some(cached) = self.cache.get_measurement(constraints) {
1602            measured_size = cached.size;
1603            *self.measured.borrow_mut() = Some(Rc::clone(&cached));
1604        } else {
1605            match self.perform_measure(constraints) {
1606                Ok(measured) => {
1607                    measured_size = measured.size;
1608                    self.cache
1609                        .store_measurement(constraints, Rc::clone(&measured));
1610                    *self.measured.borrow_mut() = Some(measured);
1611                }
1612                Err(err) => {
1613                    self.record_error(err);
1614                    self.measured.borrow_mut().take();
1615                    measured_size = Size {
1616                        width: 0.0,
1617                        height: 0.0,
1618                    };
1619                }
1620            }
1621        }
1622
1623        // Update retained LayoutNode state with measured size (new architecture).
1624        // PRIORITIZE direct handle to avoid Applier borrow conflicts during layout!
1625        if let Some(state) = &self.layout_state {
1626            let mut state = state.borrow_mut();
1627            state.size = measured_size;
1628            state.measurement_constraints = constraints;
1629        } else if let Ok(mut applier) = self.applier.try_borrow_typed() {
1630            let _ = applier.with_node::<LayoutNode, _>(self.node_id, |node| {
1631                node.set_measured_size(measured_size);
1632                node.set_measurement_constraints(constraints);
1633            });
1634        }
1635
1636        Box::new(LayoutChildPlaceable::new(
1637            Rc::clone(&self.applier),
1638            self.node_id,
1639            Rc::clone(&self.measured),
1640            Rc::clone(&self.last_position),
1641            self.layout_state.clone(),
1642        ))
1643    }
1644
1645    fn min_intrinsic_width(&self, height: f32) -> f32 {
1646        let kind = IntrinsicKind::MinWidth(height);
1647        self.cache.activate(self.cache_epoch);
1648        if let Some(value) = self.cache.get_intrinsic(&kind) {
1649            return value;
1650        }
1651        let constraints = Constraints {
1652            min_width: 0.0,
1653            max_width: f32::INFINITY,
1654            min_height: height,
1655            max_height: height,
1656        };
1657        if let Some(node) = self.intrinsic_measure(constraints) {
1658            let value = node.size.width;
1659            self.cache.store_intrinsic(kind, value);
1660            value
1661        } else {
1662            0.0
1663        }
1664    }
1665
1666    fn max_intrinsic_width(&self, height: f32) -> f32 {
1667        let kind = IntrinsicKind::MaxWidth(height);
1668        self.cache.activate(self.cache_epoch);
1669        if let Some(value) = self.cache.get_intrinsic(&kind) {
1670            return value;
1671        }
1672        let constraints = Constraints {
1673            min_width: 0.0,
1674            max_width: f32::INFINITY,
1675            min_height: 0.0,
1676            max_height: height,
1677        };
1678        if let Some(node) = self.intrinsic_measure(constraints) {
1679            let value = node.size.width;
1680            self.cache.store_intrinsic(kind, value);
1681            value
1682        } else {
1683            0.0
1684        }
1685    }
1686
1687    fn min_intrinsic_height(&self, width: f32) -> f32 {
1688        let kind = IntrinsicKind::MinHeight(width);
1689        self.cache.activate(self.cache_epoch);
1690        if let Some(value) = self.cache.get_intrinsic(&kind) {
1691            return value;
1692        }
1693        let constraints = Constraints {
1694            min_width: width,
1695            max_width: width,
1696            min_height: 0.0,
1697            max_height: f32::INFINITY,
1698        };
1699        if let Some(node) = self.intrinsic_measure(constraints) {
1700            let value = node.size.height;
1701            self.cache.store_intrinsic(kind, value);
1702            value
1703        } else {
1704            0.0
1705        }
1706    }
1707
1708    fn max_intrinsic_height(&self, width: f32) -> f32 {
1709        let kind = IntrinsicKind::MaxHeight(width);
1710        self.cache.activate(self.cache_epoch);
1711        if let Some(value) = self.cache.get_intrinsic(&kind) {
1712            return value;
1713        }
1714        let constraints = Constraints {
1715            min_width: 0.0,
1716            max_width: width,
1717            min_height: 0.0,
1718            max_height: f32::INFINITY,
1719        };
1720        if let Some(node) = self.intrinsic_measure(constraints) {
1721            let value = node.size.height;
1722            self.cache.store_intrinsic(kind, value);
1723            value
1724        } else {
1725            0.0
1726        }
1727    }
1728
1729    fn flex_parent_data(&self) -> Option<cranpose_ui_layout::FlexParentData> {
1730        // Try to borrow the applier - if it's already borrowed (nested measurement), return None.
1731        // This is safe because parent data doesn't change during measurement.
1732        let Ok(mut applier) = self.applier.try_borrow_typed() else {
1733            return None;
1734        };
1735
1736        applier
1737            .with_node::<LayoutNode, _>(self.node_id, |layout_node| {
1738                let props = layout_node.resolved_modifiers().layout_properties();
1739                props.weight().map(|weight_data| {
1740                    cranpose_ui_layout::FlexParentData::new(weight_data.weight, weight_data.fill)
1741                })
1742            })
1743            .ok()
1744            .flatten()
1745    }
1746}
1747
1748struct LayoutChildPlaceable {
1749    applier: Rc<ConcreteApplierHost<MemoryApplier>>,
1750    node_id: NodeId,
1751    measured: Rc<RefCell<Option<Rc<MeasuredNode>>>>,
1752    last_position: Rc<RefCell<Option<Point>>>,
1753    layout_state: Option<Rc<RefCell<crate::widgets::nodes::layout_node::LayoutState>>>,
1754}
1755
1756impl LayoutChildPlaceable {
1757    fn new(
1758        applier: Rc<ConcreteApplierHost<MemoryApplier>>,
1759        node_id: NodeId,
1760        measured: Rc<RefCell<Option<Rc<MeasuredNode>>>>,
1761        last_position: Rc<RefCell<Option<Point>>>,
1762        layout_state: Option<Rc<RefCell<crate::widgets::nodes::layout_node::LayoutState>>>,
1763    ) -> Self {
1764        Self {
1765            applier,
1766            node_id,
1767            measured,
1768            last_position,
1769            layout_state,
1770        }
1771    }
1772}
1773
1774impl Placeable for LayoutChildPlaceable {
1775    fn place(&self, x: f32, y: f32) {
1776        // Retrieve the node's own offset (from modifiers like offset(), padding(), etc.)
1777        // This must be added to the placement position (x, y) provided by the parent.
1778        let internal_offset = self
1779            .measured
1780            .borrow()
1781            .as_ref()
1782            .map(|m| m.offset)
1783            .unwrap_or_default();
1784
1785        let position = Point {
1786            x: x + internal_offset.x,
1787            y: y + internal_offset.y,
1788        };
1789        // Update transient storage (for backwards compatibility during transition)
1790        *self.last_position.borrow_mut() = Some(position);
1791
1792        // Update retained LayoutNode state (the new architecture)
1793        // PRIORITIZE direct handle to avoid Applier borrow conflicts during layout!
1794        if let Some(state) = &self.layout_state {
1795            let mut state = state.borrow_mut();
1796            state.position = position;
1797            state.is_placed = true;
1798        } else if let Ok(mut applier) = self.applier.try_borrow_typed() {
1799            // Try LayoutNode first, then SubcomposeLayoutNode
1800            if applier
1801                .with_node::<LayoutNode, _>(self.node_id, |node| {
1802                    node.set_position(position);
1803                })
1804                .is_err()
1805            {
1806                let _ = applier.with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
1807                    node.set_position(position);
1808                });
1809            }
1810        }
1811    }
1812
1813    fn width(&self) -> f32 {
1814        self.measured
1815            .borrow()
1816            .as_ref()
1817            .map(|node| node.size.width)
1818            .unwrap_or(0.0)
1819    }
1820
1821    fn height(&self) -> f32 {
1822        self.measured
1823            .borrow()
1824            .as_ref()
1825            .map(|node| node.size.height)
1826            .unwrap_or(0.0)
1827    }
1828
1829    fn node_id(&self) -> NodeId {
1830        self.node_id
1831    }
1832}
1833
1834fn measure_node_with_host(
1835    applier: Rc<ConcreteApplierHost<MemoryApplier>>,
1836    runtime_handle: Option<RuntimeHandle>,
1837    node_id: NodeId,
1838    constraints: Constraints,
1839    epoch: u64,
1840) -> Result<Rc<MeasuredNode>, NodeError> {
1841    let runtime_handle = match runtime_handle {
1842        Some(handle) => Some(handle),
1843        None => applier.borrow_typed().runtime_handle(),
1844    };
1845    let mut builder = LayoutBuilder::new_with_epoch(
1846        applier,
1847        epoch,
1848        Rc::new(RefCell::new(SlotBackend::default())),
1849    );
1850    builder.set_runtime_handle(runtime_handle);
1851    builder.measure_node(node_id, constraints)
1852}
1853
1854#[derive(Clone)]
1855struct RuntimeNodeMetadata {
1856    modifier: Modifier,
1857    resolved_modifiers: ResolvedModifiers,
1858    modifier_slices: Rc<ModifierNodeSlices>,
1859    role: SemanticsRole,
1860    button_handler: Option<Rc<RefCell<dyn FnMut()>>>,
1861}
1862
1863impl Default for RuntimeNodeMetadata {
1864    fn default() -> Self {
1865        Self {
1866            modifier: Modifier::empty(),
1867            resolved_modifiers: ResolvedModifiers::default(),
1868            modifier_slices: Rc::default(),
1869            role: SemanticsRole::Unknown,
1870            button_handler: None,
1871        }
1872    }
1873}
1874
1875fn collect_runtime_metadata(
1876    applier: &mut MemoryApplier,
1877    node: &MeasuredNode,
1878) -> Result<HashMap<NodeId, RuntimeNodeMetadata>, NodeError> {
1879    let mut map = HashMap::default();
1880    collect_runtime_metadata_inner(applier, node, &mut map)?;
1881    Ok(map)
1882}
1883
1884/// Collects semantics configurations for all nodes in the measured tree using the SemanticsOwner cache.
1885fn collect_semantics_with_owner(
1886    applier: &mut MemoryApplier,
1887    node: &MeasuredNode,
1888    owner: &SemanticsOwner,
1889) -> Result<(), NodeError> {
1890    // Compute and cache configuration for this node
1891    owner.get_or_compute(node.node_id, applier);
1892
1893    // Recurse to children
1894    for child in &node.children {
1895        collect_semantics_with_owner(applier, &child.node, owner)?;
1896    }
1897    Ok(())
1898}
1899
1900fn collect_semantics_snapshot(
1901    applier: &mut MemoryApplier,
1902    node: &MeasuredNode,
1903) -> Result<HashMap<NodeId, Option<SemanticsConfiguration>>, NodeError> {
1904    let owner = SemanticsOwner::new();
1905    collect_semantics_with_owner(applier, node, &owner)?;
1906
1907    // Extract all cached configurations into a map
1908    let mut map = HashMap::default();
1909    extract_configurations_recursive(node, &owner, &mut map);
1910    Ok(map)
1911}
1912
1913fn extract_configurations_recursive(
1914    node: &MeasuredNode,
1915    owner: &SemanticsOwner,
1916    map: &mut HashMap<NodeId, Option<SemanticsConfiguration>>,
1917) {
1918    if let Some(config) = owner.configurations.borrow().get(&node.node_id) {
1919        map.insert(node.node_id, config.clone());
1920    }
1921    for child in &node.children {
1922        extract_configurations_recursive(&child.node, owner, map);
1923    }
1924}
1925
1926fn collect_runtime_metadata_inner(
1927    applier: &mut MemoryApplier,
1928    node: &MeasuredNode,
1929    map: &mut HashMap<NodeId, RuntimeNodeMetadata>,
1930) -> Result<(), NodeError> {
1931    if let Entry::Vacant(entry) = map.entry(node.node_id) {
1932        let meta = runtime_metadata_for(applier, node.node_id)?;
1933        entry.insert(meta);
1934    }
1935    for child in &node.children {
1936        collect_runtime_metadata_inner(applier, &child.node, map)?;
1937    }
1938    Ok(())
1939}
1940
1941/// Extracts text content from a LayoutNode's modifier chain.
1942///
1943/// Searches the modifier chain for a TextModifierNode and returns its text content.
1944/// This replaces the old approach of checking measure_policy.text_content().
1945///
1946/// We extract text from the semantics configuration, which TextModifierNode
1947/// populates via its SemanticsNode implementation.
1948fn extract_text_from_layout_node(layout: &LayoutNode) -> Option<String> {
1949    // Use the semantics configuration which collects data from all SemanticsNode instances
1950    // in the modifier chain, including TextModifierNode
1951    layout
1952        .semantics_configuration()
1953        .and_then(|config| config.content_description)
1954}
1955
1956fn runtime_metadata_for(
1957    applier: &mut MemoryApplier,
1958    node_id: NodeId,
1959) -> Result<RuntimeNodeMetadata, NodeError> {
1960    // Try LayoutNode (the primary modern path)
1961    // IMPORTANT: We use with_node (reference) instead of try_clone because cloning
1962    // LayoutNode creates a NEW ModifierChainHandle with NEW nodes and NEW handlers,
1963    // which would lose gesture state like press_position.
1964    if let Ok(meta) = applier.with_node::<LayoutNode, _>(node_id, |layout| {
1965        // Extract text content from the modifier chain instead of measure policy
1966        let role = if let Some(text) = extract_text_from_layout_node(layout) {
1967            SemanticsRole::Text { value: text }
1968        } else {
1969            SemanticsRole::Layout
1970        };
1971
1972        RuntimeNodeMetadata {
1973            modifier: layout.modifier.clone(),
1974            resolved_modifiers: layout.resolved_modifiers(),
1975            modifier_slices: layout.modifier_slices_snapshot(),
1976            role,
1977            button_handler: None,
1978        }
1979    }) {
1980        return Ok(meta);
1981    }
1982
1983    // Try SubcomposeLayoutNode
1984    if let Ok((modifier, resolved_modifiers)) = applier
1985        .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1986            (node.modifier(), node.resolved_modifiers())
1987        })
1988    {
1989        // SubcomposeLayoutNode doesn't cache slices yet, so we still allocate here.
1990        // TODO: Optimize SubcomposeLayoutNode to cache slices too.
1991        let modifier_slices = Rc::new(collect_slices_from_modifier(&modifier));
1992        return Ok(RuntimeNodeMetadata {
1993            modifier,
1994            resolved_modifiers,
1995            modifier_slices,
1996            role: SemanticsRole::Subcompose,
1997            button_handler: None,
1998        });
1999    }
2000    Ok(RuntimeNodeMetadata::default())
2001}
2002
2003/// Computes semantics configuration for a node by reading from its modifier chain.
2004/// This is the primary entry point for extracting semantics from nodes, replacing
2005/// the widget-specific fallbacks with pure modifier-node traversal.
2006fn compute_semantics_for_node(
2007    applier: &mut MemoryApplier,
2008    node_id: NodeId,
2009) -> Option<SemanticsConfiguration> {
2010    // Try LayoutNode (the primary modern path)
2011    match applier.with_node::<LayoutNode, _>(node_id, |layout| {
2012        let config = layout.semantics_configuration();
2013        layout.clear_needs_semantics();
2014        config
2015    }) {
2016        Ok(config) => return config,
2017        Err(NodeError::TypeMismatch { .. }) | Err(NodeError::Missing { .. }) => {}
2018        Err(_) => return None,
2019    }
2020
2021    // Try SubcomposeLayoutNode
2022    if let Ok(modifier) =
2023        applier.with_node::<SubcomposeLayoutNode, _>(node_id, |node| node.modifier())
2024    {
2025        return collect_semantics_from_modifier(&modifier);
2026    }
2027
2028    None
2029}
2030
2031/// Builds a semantics node from measured tree data and semantics configurations.
2032/// Roles and actions are now derived entirely from SemanticsConfiguration, with
2033/// metadata consulted only for legacy widget type information.
2034fn build_semantics_node(
2035    node: &MeasuredNode,
2036    metadata: &HashMap<NodeId, RuntimeNodeMetadata>,
2037    semantics: &HashMap<NodeId, Option<SemanticsConfiguration>>,
2038) -> SemanticsNode {
2039    let info = metadata.get(&node.node_id).cloned().unwrap_or_default();
2040
2041    // Start with the widget-derived role as a fallback
2042    let mut role = info.role.clone();
2043    let mut actions = Vec::new();
2044    let mut description = None;
2045
2046    // Override with semantics configuration if present
2047    if let Some(config) = semantics.get(&node.node_id).cloned().flatten() {
2048        // Role synthesis: prefer semantics flags over widget type
2049        if config.is_button {
2050            role = SemanticsRole::Button;
2051        }
2052
2053        // Action synthesis: create click action if node is clickable
2054        if config.is_clickable {
2055            actions.push(SemanticsAction::Click {
2056                handler: SemanticsCallback::new(node.node_id),
2057            });
2058        }
2059
2060        // Description from configuration
2061        if let Some(desc) = config.content_description {
2062            description = Some(desc);
2063        }
2064    }
2065
2066    let children = node
2067        .children
2068        .iter()
2069        .map(|child| build_semantics_node(&child.node, metadata, semantics))
2070        .collect();
2071
2072    SemanticsNode::new(node.node_id, role, actions, children, description)
2073}
2074
2075fn build_layout_tree_from_metadata(
2076    node: &MeasuredNode,
2077    metadata: &HashMap<NodeId, RuntimeNodeMetadata>,
2078) -> LayoutTree {
2079    fn place(
2080        node: &MeasuredNode,
2081        origin: Point,
2082        metadata: &HashMap<NodeId, RuntimeNodeMetadata>,
2083    ) -> LayoutBox {
2084        // Include the node's own offset (from OffsetNode) in its position
2085        let top_left = Point {
2086            x: origin.x + node.offset.x,
2087            y: origin.y + node.offset.y,
2088        };
2089        let rect = GeometryRect {
2090            x: top_left.x,
2091            y: top_left.y,
2092            width: node.size.width,
2093            height: node.size.height,
2094        };
2095        let info = metadata.get(&node.node_id).cloned().unwrap_or_default();
2096        let kind = layout_kind_from_metadata(node.node_id, &info);
2097        let data = LayoutNodeData::new(
2098            info.modifier.clone(),
2099            info.resolved_modifiers,
2100            info.modifier_slices.clone(),
2101            kind,
2102        );
2103        let children = node
2104            .children
2105            .iter()
2106            .map(|child| {
2107                let child_origin = Point {
2108                    x: top_left.x + child.offset.x,
2109                    y: top_left.y + child.offset.y,
2110                };
2111                place(&child.node, child_origin, metadata)
2112            })
2113            .collect();
2114        LayoutBox::new(node.node_id, rect, node.content_offset, data, children)
2115    }
2116
2117    LayoutTree::new(place(node, Point { x: 0.0, y: 0.0 }, metadata))
2118}
2119
2120fn layout_kind_from_metadata(_node_id: NodeId, info: &RuntimeNodeMetadata) -> LayoutNodeKind {
2121    match &info.role {
2122        SemanticsRole::Layout => LayoutNodeKind::Layout,
2123        SemanticsRole::Subcompose => LayoutNodeKind::Subcompose,
2124        SemanticsRole::Text { .. } => {
2125            // Text content is now handled via TextModifierNode in the modifier chain
2126            // and collected in modifier_slices.text_content(). LayoutNodeKind should
2127            // reflect the layout policy (EmptyMeasurePolicy), not the content type.
2128            LayoutNodeKind::Layout
2129        }
2130        SemanticsRole::Spacer => LayoutNodeKind::Spacer,
2131        SemanticsRole::Button => {
2132            let handler = info
2133                .button_handler
2134                .as_ref()
2135                .cloned()
2136                .unwrap_or_else(|| Rc::new(RefCell::new(|| {})));
2137            LayoutNodeKind::Button { on_click: handler }
2138        }
2139        SemanticsRole::Unknown => LayoutNodeKind::Unknown,
2140    }
2141}
2142
2143fn subtract_padding(constraints: Constraints, padding: EdgeInsets) -> Constraints {
2144    let horizontal = padding.horizontal_sum();
2145    let vertical = padding.vertical_sum();
2146    let min_width = (constraints.min_width - horizontal).max(0.0);
2147    let mut max_width = constraints.max_width;
2148    if max_width.is_finite() {
2149        max_width = (max_width - horizontal).max(0.0);
2150    }
2151    let min_height = (constraints.min_height - vertical).max(0.0);
2152    let mut max_height = constraints.max_height;
2153    if max_height.is_finite() {
2154        max_height = (max_height - vertical).max(0.0);
2155    }
2156    normalize_constraints(Constraints {
2157        min_width,
2158        max_width,
2159        min_height,
2160        max_height,
2161    })
2162}
2163
2164#[cfg(test)]
2165pub(crate) fn align_horizontal(alignment: HorizontalAlignment, available: f32, child: f32) -> f32 {
2166    match alignment {
2167        HorizontalAlignment::Start => 0.0,
2168        HorizontalAlignment::CenterHorizontally => ((available - child) / 2.0).max(0.0),
2169        HorizontalAlignment::End => (available - child).max(0.0),
2170    }
2171}
2172
2173#[cfg(test)]
2174pub(crate) fn align_vertical(alignment: VerticalAlignment, available: f32, child: f32) -> f32 {
2175    match alignment {
2176        VerticalAlignment::Top => 0.0,
2177        VerticalAlignment::CenterVertically => ((available - child) / 2.0).max(0.0),
2178        VerticalAlignment::Bottom => (available - child).max(0.0),
2179    }
2180}
2181
2182fn resolve_dimension(
2183    base: f32,
2184    explicit: DimensionConstraint,
2185    min_override: Option<f32>,
2186    max_override: Option<f32>,
2187    min_limit: f32,
2188    max_limit: f32,
2189) -> f32 {
2190    let mut min_bound = min_limit;
2191    if let Some(min_value) = min_override {
2192        min_bound = min_bound.max(min_value);
2193    }
2194
2195    let mut max_bound = if max_limit.is_finite() {
2196        max_limit
2197    } else {
2198        max_override.unwrap_or(max_limit)
2199    };
2200    if let Some(max_value) = max_override {
2201        if max_bound.is_finite() {
2202            max_bound = max_bound.min(max_value);
2203        } else {
2204            max_bound = max_value;
2205        }
2206    }
2207    if max_bound < min_bound {
2208        max_bound = min_bound;
2209    }
2210
2211    let mut size = match explicit {
2212        DimensionConstraint::Points(points) => points,
2213        DimensionConstraint::Fraction(fraction) => {
2214            if max_limit.is_finite() {
2215                max_limit * fraction.clamp(0.0, 1.0)
2216            } else {
2217                base
2218            }
2219        }
2220        DimensionConstraint::Unspecified => base,
2221        // Intrinsic sizing is resolved at a higher level where we have access to children.
2222        // At this point we just use the base size as a fallback.
2223        DimensionConstraint::Intrinsic(_) => base,
2224    };
2225
2226    size = clamp_dimension(size, min_bound, max_bound);
2227    size = clamp_dimension(size, min_limit, max_limit);
2228    size.max(0.0)
2229}
2230
2231fn clamp_dimension(value: f32, min: f32, max: f32) -> f32 {
2232    let mut result = value.max(min);
2233    if max.is_finite() {
2234        result = result.min(max);
2235    }
2236    result
2237}
2238
2239fn normalize_constraints(mut constraints: Constraints) -> Constraints {
2240    if constraints.max_width < constraints.min_width {
2241        constraints.max_width = constraints.min_width;
2242    }
2243    if constraints.max_height < constraints.min_height {
2244        constraints.max_height = constraints.min_height;
2245    }
2246    constraints
2247}
2248
2249#[cfg(test)]
2250#[path = "tests/layout_tests.rs"]
2251mod tests;