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