Skip to main content

day_core/
layout.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! The layout engine (DESIGN.md §7): parent-proposes/child-chooses with a proposal-keyed
5//! measure cache. Layout impls are ours or user-provided; they never run reactive user code,
6//! so the engine holds the single tree borrow for the whole pass.
7
8use std::rc::Rc;
9
10day_reactive::tls_slots! {
11    layout;
12    /// Nodes already warned about (see the report site below): once per node, not per
13    /// frame. A rebuilt page gets a fresh node and may report again, which is fine.
14    static REPORTED: std::cell::RefCell<std::collections::HashSet<RNode>> =
15        std::cell::RefCell::new(std::collections::HashSet::new());
16}
17
18use day_spec::*;
19
20use crate::tree::{Flex, RNode, Tree, TreeOps};
21
22/// Open layout protocol (§7.2). `children` are the node's direct children; group nodes
23/// (`when`/`each` anchors) are layout-transparent: stacks expand them inline.
24pub trait Layout: 'static {
25    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size;
26    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect);
27    /// Where this node's own first text baseline falls, measured from the top of a box of
28    /// `size` (docs/baseline.md). `None` (the default) means the node has no baseline to
29    /// offer, so a baseline-aligned parent falls back to box alignment for it.
30    ///
31    /// A container answers on behalf of its content: a column reports its first
32    /// baseline-bearing child's, shifted by where that child sits. Leaves never reach here;
33    /// [`LayoutOps::baseline_of`] asks the toolkit for those.
34    fn baseline(&self, _cx: &mut dyn LayoutOps, _children: &[RNode], _size: Size) -> Option<f64> {
35        None
36    }
37}
38
39/// The engine surface visible to `Layout` implementations.
40pub trait LayoutOps {
41    fn measure_child(&mut self, child: RNode, p: Proposal) -> Size;
42    fn place_child(&mut self, child: RNode, rect: Rect);
43    /// Place a child whose on-screen frame is native-owned (nav pages in splitter panes /
44    /// nav-controller views): never direction-mirrored, because the toolkit positions it.
45    fn place_child_native(&mut self, child: RNode, rect: Rect) {
46        self.place_child(child, rect);
47    }
48    fn flex_of(&self, child: RNode) -> Flex;
49    fn children_of(&self, node: RNode) -> Vec<RNode>;
50    /// Native intrinsic measurement of the current node (leaves).
51    fn measure_leaf(&mut self, p: Proposal) -> Size;
52    /// A child's first text baseline at `size`, from the top of its box (docs/baseline.md):
53    /// the toolkit answers for a native leaf, the child's own [`Layout::baseline`] for a
54    /// container. `None` ⇒ no baseline to align to.
55    fn baseline_of(&mut self, child: RNode, size: Size) -> Option<f64>;
56    /// Report scroll content size for the current node (§7.6).
57    fn set_scroll_content(&mut self, content: Size);
58    /// The room the current scroll node's scroll bar takes from its content area while the
59    /// content overflows (`Toolkit::scroll_bar_inset`); 0 where bars overlay the content.
60    fn scroll_bar_inset(&mut self, _vertical: bool) -> f64 {
61        0.0
62    }
63    /// Report that the current node's children outgrew the bounds its `place()` was given
64    /// (`needed` vs `available` main-axis points). A diagnostic hook, not a layout input:
65    /// the engine logs it once per node in debug builds and ignores it in release, and
66    /// placement proceeds unchanged either way. Scroll containers never report, since
67    /// content larger than the viewport is their normal state.
68    fn report_overflow(&mut self, _needed: f64, _available: f64) {}
69}
70
71pub struct EngineCx<'a, B: Toolkit> {
72    pub(crate) tree: &'a mut Tree<B>,
73    pub(crate) offset: Point,
74    pub(crate) current: RNode,
75    /// The bounds the current node's `place()` was given: the mirroring axis for RTL.
76    pub(crate) parent_size: Size,
77}
78
79impl<B: Toolkit> LayoutOps for EngineCx<'_, B> {
80    fn measure_child(&mut self, child: RNode, p: Proposal) -> Size {
81        measure_node(self.tree, child, p)
82    }
83    fn place_child(&mut self, child: RNode, rect: Rect) {
84        // RTL (docs/localization): layouts compute LTR ("leading" = left); under a
85        // right-to-left locale every horizontal placement mirrors around the parent's
86        // width, so leading means right everywhere (rows reverse, padding swaps sides,
87        // alignment flips) without any layout impl knowing about direction. Leaf content
88        // (canvas drawing, text runs) is not mirrored; native text handles RTL itself.
89        let rect = if crate::layout_direction() == day_geometry::LayoutDirection::Rtl {
90            Rect::new(
91                self.parent_size.width - rect.origin.x - rect.size.width,
92                rect.origin.y,
93                rect.size.width,
94                rect.size.height,
95            )
96        } else {
97            rect
98        };
99        place_node(self.tree, child, rect, self.offset, false);
100    }
101    fn place_child_native(&mut self, child: RNode, rect: Rect) {
102        place_node(self.tree, child, rect, self.offset, false);
103    }
104    fn flex_of(&self, child: RNode) -> Flex {
105        self.tree.node(child).map(|n| n.flex).unwrap_or_default()
106    }
107    fn children_of(&self, node: RNode) -> Vec<RNode> {
108        self.tree
109            .node(node)
110            .map(|n| n.children.clone())
111            .unwrap_or_default()
112    }
113    fn measure_leaf(&mut self, p: Proposal) -> Size {
114        let Some(n) = self.tree.node(self.current) else {
115            return Size::ZERO;
116        };
117        let kind = n.kind;
118        let Some(h) = n.handle.clone() else {
119            return Size::ZERO;
120        };
121        self.tree.toolkit.measure(&h, kind, p)
122    }
123    fn baseline_of(&mut self, child: RNode, size: Size) -> Option<f64> {
124        baseline_node(self.tree, child, size)
125    }
126    fn set_scroll_content(&mut self, content: Size) {
127        let current = self.current;
128        let Some(n) = self.tree.node_mut(current) else {
129            return;
130        };
131        // Cache for scroll_to_target (§7.6): edge targets need content-minus-viewport math.
132        n.scroll_content = Some(content);
133        let Some(h) = n.handle.clone() else { return };
134        self.tree.toolkit.set_scroll_content(&h, content);
135    }
136    fn scroll_bar_inset(&mut self, vertical: bool) -> f64 {
137        let Some(h) = self.tree.node(self.current).and_then(|n| n.handle.clone()) else {
138            return 0.0;
139        };
140        self.tree.toolkit.scroll_bar_inset(&h, vertical).max(0.0)
141    }
142
143    #[cfg(debug_assertions)]
144    fn report_overflow(&mut self, needed: f64, available: f64) {
145        // Once per node, not per frame: layout re-runs constantly, and this is meant as a
146        // greppable hint, not a firehose. A rebuilt page gets a fresh node and may report
147        // again, which is fine.
148        if REPORTED.with(|r| !r.borrow_mut().insert(self.current)) {
149            return;
150        }
151        // Name the container by the dayscript ids in reach, because an id is the one thing a
152        // developer can grep straight to the source. Ids usually sit a wrapper or two below
153        // the overflowing stack (`.id` tags the piece, decorators wrap it), so this walks the
154        // subtree depth-first and keeps the first few it meets.
155        let mut ids: Vec<String> = Vec::new();
156        let mut stack = vec![self.current];
157        while let Some(node) = stack.pop() {
158            if ids.len() >= 6 {
159                break;
160            }
161            if let Some(n) = self.tree.node(node) {
162                ids.extend(n.id.clone());
163                stack.extend(n.children.iter().rev());
164            }
165        }
166        let who = if ids.is_empty() {
167            String::from("no .id in reach")
168        } else {
169            ids.join(", ")
170        };
171        log::warn!(
172            "day layout: children overflow their container by {:.0}pt ({needed:.0} needed, \
173             {available:.0} available; {who}) — give the row a fit policy: \
174             .fit(RowFit::Wrap/Scroll) or adaptive(..) (docs/size-classes.md)",
175            needed - available,
176        );
177    }
178}
179
180pub(crate) fn measure_node<B: Toolkit>(tree: &mut Tree<B>, node: RNode, p: Proposal) -> Size {
181    let key = p.cache_key();
182    let (layout, children) = {
183        let Some(n) = tree.node(node) else {
184            return Size::ZERO;
185        };
186        if !n.needs_measure
187            && let Some(&(_, s)) = n.cache.iter().find(|(k, _)| *k == key)
188        {
189            return s;
190        }
191        (n.layout.clone(), n.children.clone())
192    };
193    let mut cx = EngineCx {
194        tree,
195        offset: Point::ZERO,
196        current: node,
197        parent_size: Size::ZERO, // placement never happens during measure
198    };
199    let size = layout.measure(&mut cx, &children, p);
200    if let Some(n) = tree.node_mut(node) {
201        n.needs_measure = false;
202        if n.cache.len() >= 4 {
203            n.cache.clear();
204        }
205        n.cache.push((key, size));
206    }
207    size
208}
209
210/// A node's first text baseline at `size`, from the top of its box (docs/baseline.md). A native
211/// leaf asks the toolkit; anything else asks its own `Layout`, which answers for its content.
212///
213/// Cached per size and invalidated with the measure cache, so a baseline-aligned row costs one
214/// toolkit call per child per measure generation rather than one per layout pass.
215pub(crate) fn baseline_node<B: Toolkit>(
216    tree: &mut Tree<B>,
217    node: RNode,
218    size: Size,
219) -> Option<f64> {
220    let key = Proposal::exact(size).cache_key();
221    let (layout, children, handle, kind) = {
222        let n = tree.node(node)?;
223        if !n.needs_measure
224            && let Some((k, b)) = n.baseline_cache
225            && k == key
226        {
227            return b;
228        }
229        (
230            n.layout.clone(),
231            n.children.clone(),
232            n.handle.clone(),
233            n.kind,
234        )
235    };
236    // A realized leaf's baseline is the toolkit's to report; a container's is its layout's.
237    let baseline = match handle {
238        Some(h) if children.is_empty() => tree.toolkit.first_baseline(&h, kind, size),
239        _ => {
240            let mut cx = EngineCx {
241                tree,
242                offset: Point::ZERO,
243                current: node,
244                parent_size: Size::ZERO,
245            };
246            layout.baseline(&mut cx, &children, size)
247        }
248    };
249    if let Some(n) = tree.node_mut(node) {
250        n.baseline_cache = Some((key, baseline));
251    }
252    baseline
253}
254
255/// `rect` is in the parent node's coordinates; `offset` is the parent's origin in the nearest
256/// native ancestor's coordinates (§7.1, accumulated through layout-only nodes).
257pub(crate) fn place_node<B: Toolkit>(
258    tree: &mut Tree<B>,
259    node: RNode,
260    rect: Rect,
261    offset: Point,
262    is_root: bool,
263) {
264    let abs = Rect {
265        origin: rect.origin.offset(offset.x, offset.y),
266        size: rect.size,
267    };
268    let (layout, children, has_handle) = {
269        let Some(n) = tree.node(node) else { return };
270        (n.layout.clone(), n.children.clone(), n.handle.is_some())
271    };
272    // A cover's frame is the backend's (docs/cover.md): presented, it is re-homed over the
273    // window and follows the window's resizing; parked, it is hidden. Where it sits in
274    // the tree it measures zero (`CoverLayout`), so applying the parent's placement (a
275    // zero-size rect at the parent's center) would collapse a presented cover to a line
276    // the moment anything re-lays the parent out. A window resize did exactly that on
277    // macOS (Games Fair, 2026-09-11): the game vanished into a one-pixel strip over the home
278    // page. Its content is still placed below, at the size the backend reported.
279    let native_frame = tree
280        .node(node)
281        .map(|n| n.kind == day_spec::kinds::COVER)
282        .unwrap_or(false);
283    let child_offset = if has_handle {
284        if !is_root && !native_frame {
285            let changed = tree
286                .node(node)
287                .map(|n| {
288                    n.last_native_frame
289                        .map(|f| !f.approx_eq(&abs, 0.25))
290                        .unwrap_or(true)
291                })
292                .unwrap_or(false);
293            if changed {
294                let h = tree.node(node).and_then(|n| n.handle.clone());
295                if let Some(h) = h {
296                    let anim = tree.resolve_anim(node);
297                    tree.toolkit.set_frame(&h, abs, anim.as_ref());
298                }
299                if tree
300                    .node(node)
301                    .map(|n| n.kind == day_spec::kinds::CANVAS)
302                    .unwrap_or(false)
303                {
304                    // Queue-only (§8.3): canvases re-record against the new size after layout.
305                    crate::tree::enqueue_event(
306                        crate::tree::rnode_to_id(node),
307                        day_spec::Event::FrameChanged(abs.size),
308                    );
309                }
310            }
311        }
312        Point::ZERO
313    } else {
314        abs.origin
315    };
316    // A list whose width changes re-lays its active cells in this pass: the native table
317    // resizes the physical cell views, but each cell's day content keeps the old width's
318    // placement until laid out again (a trailing control would sit clipped after a
319    // narrowing). It runs synchronously because interactive live-resize runs inside a
320    // native tracking loop where deferred main-thread work may not drain until the drag
321    // ends.
322    let relayout_cells = tree
323        .node(node)
324        .map(|n| {
325            n.kind == day_spec::kinds::LIST
326                && n.last_native_frame
327                    .map(|f| (f.size.width - abs.size.width).abs() > 0.25)
328                    .unwrap_or(false)
329        })
330        .unwrap_or(false);
331    if let Some(n) = tree.node_mut(node) {
332        n.last_native_frame = Some(abs);
333        n.needs_layout = false;
334    }
335    if relayout_cells {
336        for key in tree.list_cell_keys(node) {
337            tree.list_layout_cell(node, key);
338        }
339    }
340    let mut cx = EngineCx {
341        tree,
342        offset: child_offset,
343        current: node,
344        parent_size: rect.size,
345    };
346    layout.place(&mut cx, &children, Rect::from_size(rect.size));
347}
348
349// ---------------------------------------------------------------------------
350// Built-in layouts
351// ---------------------------------------------------------------------------
352
353/// A single-child wrapper's baseline is its child's, moved by where the child sits inside it
354/// (docs/baseline.md).
355///
356/// Every wrapper below places its one child at its own top-left, so `dy` is 0 for all of them
357/// but padding. Without this a decorated piece reports no baseline, and since decorators are
358/// invisible in the source (`.width(90)` on a label is still "a label" to the reader), a row
359/// would silently center the very children the author asked to align. That is exactly what
360/// happened to the showcase's first baseline demo.
361fn wrapper_baseline(
362    cx: &mut dyn LayoutOps,
363    children: &[RNode],
364    child_size: Size,
365    dy: f64,
366) -> Option<f64> {
367    let &c = children.first()?;
368    Some(cx.baseline_of(c, child_size)? + dy)
369}
370
371/// Single-child pass-through (root, wrappers, group fallback): top-leading.
372pub struct PassThrough;
373
374impl Layout for PassThrough {
375    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
376        match children.first() {
377            Some(&c) => cx.measure_child(c, p),
378            None => Size::ZERO,
379        }
380    }
381    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
382        if let Some(&c) = children.first() {
383            let s = cx.measure_child(c, Proposal::exact(bounds.size));
384            cx.place_child(c, Rect::from_size(s));
385        }
386    }
387}
388
389/// A recycled tree cell's anchor layout (docs/tree.md): row content hugs its own height, so
390/// center it in the cell's fixed row height; [`PassThrough`] pins it to the top, which
391/// reads as a misaligned row whenever the content is shorter than the row (a 16pt label in
392/// a 28pt row sat 6pt high). Width still fills; a content taller than the row clamps to it.
393pub struct CellCenter;
394
395impl Layout for CellCenter {
396    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
397        match children.first() {
398            Some(&c) => cx.measure_child(c, p),
399            None => Size::ZERO,
400        }
401    }
402    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
403        if let Some(&c) = children.first() {
404            let s = cx.measure_child(c, Proposal::exact(bounds.size));
405            let y = ((bounds.size.height - s.height) / 2.0).max(0.0);
406            cx.place_child(
407                c,
408                Rect::new(0.0, y, s.width, s.height.min(bounds.size.height)),
409            );
410        }
411    }
412    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
413        PassThrough::forward(cx, children, size)
414    }
415}
416
417/// A list cell's anchor layout (docs/list.md "Row metrics"): the row sits inside the platform's
418/// row margins and the cell is at least the platform's row height, content centered in it.
419/// With zero metrics it is [`PassThrough`].
420pub struct CellMetrics(pub day_spec::RowMetrics);
421
422impl CellMetrics {
423    /// The metrics for `child`. The platform's own whole row keeps the list's side margins
424    /// (they line up with the separators) and sizes its height itself.
425    fn for_child(&self, cx: &dyn LayoutOps, child: RNode) -> day_spec::RowMetrics {
426        // Through the single-child wrappers a row's decorators and builder add around it.
427        let mut node = child;
428        for _ in 0..8 {
429            if cx.flex_of(node).whole_row {
430                let i = self.0.insets;
431                return day_spec::RowMetrics {
432                    insets: day_spec::Insets {
433                        top: 0.0,
434                        leading: i.leading,
435                        bottom: 0.0,
436                        trailing: i.trailing,
437                    },
438                    min_height: 0.0,
439                };
440            }
441            match cx.children_of(node)[..] {
442                [only] => node = only,
443                _ => break,
444            }
445        }
446        self.0
447    }
448}
449
450impl Layout for CellMetrics {
451    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
452        let Some(&c) = children.first() else {
453            return Size::ZERO;
454        };
455        let m = self.for_child(cx, c);
456        let i = m.insets;
457        let (dx, dy) = (i.leading + i.trailing, i.top + i.bottom);
458        let s = cx.measure_child(
459            c,
460            Proposal::new(
461                p.width.map(|w| (w - dx).max(0.0)),
462                p.height.map(|h| (h - dy).max(0.0)),
463            ),
464        );
465        Size::new(s.width + dx, (s.height + dy).max(m.min_height))
466    }
467    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
468        let Some(&c) = children.first() else {
469            return;
470        };
471        let i = self.for_child(cx, c).insets;
472        let inner = Size::new(
473            (bounds.size.width - i.leading - i.trailing).max(0.0),
474            (bounds.size.height - i.top - i.bottom).max(0.0),
475        );
476        let s = cx.measure_child(c, Proposal::new(Some(inner.width), None));
477        // Inside the margins where the row has room for them; a fixed-height row too short for
478        // them (`RowHeight::Uniform`) centers its content in the whole row instead of clipping.
479        let (y, h) = if s.height <= inner.height {
480            (i.top + (inner.height - s.height) / 2.0, s.height)
481        } else {
482            let h = s.height.min(bounds.size.height);
483            (((bounds.size.height - h) / 2.0).max(0.0), h)
484        };
485        cx.place_child(c, Rect::new(i.leading, y, inner.width, h));
486    }
487    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
488        PassThrough::forward(cx, children, size)
489    }
490}
491
492impl PassThrough {
493    /// Shared by every wrapper whose child measures at the wrapper's own size.
494    fn forward(cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
495        let &c = children.first()?;
496        let cs = cx.measure_child(c, Proposal::exact(size));
497        wrapper_baseline(cx, children, cs, 0.0)
498    }
499}
500
501/// Paint/clip wrapper (`.background`, `.corner_radius`, the animatable layers): like
502/// [`PassThrough`] for measurement, but the granted rect flows to the child verbatim at place
503/// time. These containers exist to paint or clip the area the parent granted, so a grow
504/// stretch above them must reach the painted surface (the grid-cell card case) instead of
505/// being re-hugged at every wrapper. Under a parent that places at measured size (every
506/// stack, grid rigid cell, and overlay) bounds equal the measure and nothing changes.
507pub struct FillThrough;
508
509impl Layout for FillThrough {
510    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
511        match children.first() {
512            Some(&c) => cx.measure_child(c, p),
513            None => Size::ZERO,
514        }
515    }
516    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
517        if let Some(&c) = children.first() {
518            cx.place_child(c, Rect::from_size(bounds.size));
519        }
520    }
521    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
522        wrapper_baseline(cx, children, size, 0.0)
523    }
524}
525
526/// Native leaf: measurement delegates to the toolkit.
527pub struct LeafLayout;
528
529impl Layout for LeafLayout {
530    fn measure(&self, cx: &mut dyn LayoutOps, _children: &[RNode], p: Proposal) -> Size {
531        cx.measure_leaf(p)
532    }
533    fn place(&self, _cx: &mut dyn LayoutOps, _children: &[RNode], _bounds: Rect) {}
534}
535
536#[derive(Clone, Copy, PartialEq, Eq)]
537pub enum Axis {
538    Vertical,
539    Horizontal,
540}
541
542#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
543pub enum CrossAlign {
544    Leading,
545    #[default]
546    Center,
547    Trailing,
548    /// Align children on their first text baseline rather than on their boxes
549    /// (docs/baseline.md). Horizontal rows only: a column's children are stacked along the
550    /// axis a baseline lives on, so there is nothing to align. A child whose toolkit reports no
551    /// baseline is centered, so a row of text and an image still looks right.
552    FirstBaseline,
553}
554
555impl CrossAlign {
556    /// Placement fraction of the free space: 0 = leading, 0.5 = center, 1 = trailing.
557    fn fraction(self) -> f64 {
558        match self {
559            CrossAlign::Leading => 0.0,
560            // No baseline to align to at this level (a column, or every child reported None):
561            // centering is the fallback the variant promises.
562            CrossAlign::Center | CrossAlign::FirstBaseline => 0.5,
563            CrossAlign::Trailing => 1.0,
564        }
565    }
566}
567
568/// Column/row negotiation (§7.2): rigid children first, remaining main-axis space divided
569/// among flexible children; `spacer()` is maximally flexible; group anchors expand inline.
570pub struct StackLayout {
571    pub axis: Axis,
572    pub spacing: f64,
573    pub align: CrossAlign,
574}
575
576impl StackLayout {
577    fn main(&self, s: Size) -> f64 {
578        match self.axis {
579            Axis::Vertical => s.height,
580            Axis::Horizontal => s.width,
581        }
582    }
583    fn cross(&self, s: Size) -> f64 {
584        match self.axis {
585            Axis::Vertical => s.width,
586            Axis::Horizontal => s.height,
587        }
588    }
589    fn mk(&self, main: f64, cross: f64) -> Size {
590        match self.axis {
591            Axis::Vertical => Size::new(cross, main),
592            Axis::Horizontal => Size::new(main, cross),
593        }
594    }
595    fn split(&self, p: Proposal) -> (Option<f64>, Option<f64>) {
596        match self.axis {
597            Axis::Vertical => (p.height, p.width),
598            Axis::Horizontal => (p.width, p.height),
599        }
600    }
601    fn proposal(&self, main: Option<f64>, cross: Option<f64>) -> Proposal {
602        match self.axis {
603            Axis::Vertical => Proposal::new(cross, main),
604            Axis::Horizontal => Proposal::new(main, cross),
605        }
606    }
607    fn grows_main(&self, f: Flex) -> bool {
608        f.is_spacer
609            || match self.axis {
610                Axis::Vertical => f.grow_h,
611                Axis::Horizontal => f.grow_w,
612            }
613    }
614    /// Sized after the rigid children, against what they leave, without growing the stack
615    /// (`Flex::yields_h`, a column only).
616    fn yields_main(&self, f: Flex) -> bool {
617        self.axis == Axis::Vertical && f.yields_h && !self.grows_main(f)
618    }
619    fn grows_cross(&self, f: Flex) -> bool {
620        match self.axis {
621            Axis::Vertical => f.grow_w,
622            Axis::Horizontal => f.grow_h,
623        }
624    }
625
626    fn flatten(cx: &mut dyn LayoutOps, children: &[RNode], out: &mut Vec<RNode>) {
627        for &c in children {
628            if cx.flex_of(c).is_group {
629                let inner = cx.children_of(c);
630                Self::flatten(cx, &inner, out);
631            } else {
632                out.push(c);
633            }
634        }
635    }
636
637    fn negotiate(&self, cx: &mut dyn LayoutOps, kids: &[RNode], p: Proposal) -> Vec<Size> {
638        let (main_p, cross_p) = self.split(p);
639        let mut sizes = vec![Size::ZERO; kids.len()];
640        let mut flex_idx = Vec::new();
641        let mut yield_idx = Vec::new();
642        let mut rigid_main = 0.0;
643        for (i, &k) in kids.iter().enumerate() {
644            let f = cx.flex_of(k);
645            if self.grows_main(f) {
646                flex_idx.push(i);
647            } else if self.yields_main(f) {
648                yield_idx.push(i);
649            } else {
650                let s = cx.measure_child(k, self.proposal(None, cross_p));
651                rigid_main += self.main(s);
652                sizes[i] = s;
653            }
654        }
655        // Shrink pass: when the rigid children's natural sizes overflow a bounded main axis,
656        // re-measure the overflowing ones against the space that is left (in order, so
657        // earlier children keep their natural size first). Content that fits keeps its natural
658        // measure, so proposal-expanding kinds (text fields, lists) don't balloon; wrapping
659        // content (a capped message bubble, a long label) folds instead of spilling out of the
660        // stack.
661        if let Some(mp) = main_p {
662            let spacing_total = self.spacing * (kids.len().saturating_sub(1)) as f64;
663            let available = (mp - spacing_total).max(0.0);
664            if rigid_main > available {
665                let mut budget = available;
666                rigid_main = 0.0;
667                for (i, &k) in kids.iter().enumerate() {
668                    let f = cx.flex_of(k);
669                    if self.grows_main(f) || self.yields_main(f) {
670                        continue;
671                    }
672                    if self.main(sizes[i]) > budget {
673                        sizes[i] = cx.measure_child(k, self.proposal(Some(budget), cross_p));
674                    }
675                    let m = self.main(sizes[i]);
676                    rigid_main += m;
677                    budget = (budget - m).max(0.0);
678                }
679            }
680        }
681        // Yielding children, in order, against what the rigid ones (and any yielding one before
682        // them) left: a capped scroll shrinks before the button under it is pushed out.
683        for &i in &yield_idx {
684            let spacing_total = self.spacing * (kids.len().saturating_sub(1)) as f64;
685            let left = main_p.map(|mp| (mp - rigid_main - spacing_total).max(0.0));
686            sizes[i] = cx.measure_child(kids[i], self.proposal(left, cross_p));
687            rigid_main += self.main(sizes[i]);
688        }
689        if !flex_idx.is_empty() {
690            let spacing_total = self.spacing * (kids.len().saturating_sub(1)) as f64;
691            match main_p {
692                Some(mp) => {
693                    let remaining = (mp - rigid_main - spacing_total).max(0.0);
694                    let share = remaining / flex_idx.len() as f64;
695                    for &i in &flex_idx {
696                        let f = cx.flex_of(kids[i]);
697                        sizes[i] = if f.is_spacer {
698                            self.mk(share, 0.0)
699                        } else {
700                            cx.measure_child(kids[i], self.proposal(Some(share), cross_p))
701                        };
702                    }
703                }
704                None => {
705                    for &i in &flex_idx {
706                        let f = cx.flex_of(kids[i]);
707                        sizes[i] = if f.is_spacer {
708                            Size::ZERO
709                        } else {
710                            cx.measure_child(kids[i], self.proposal(None, cross_p))
711                        };
712                    }
713                }
714            }
715        }
716        sizes
717    }
718
719    /// Each child's first baseline at the size it settled on, or `None` where it has none
720    /// (docs/baseline.md). Only a horizontal row aligns on baselines; down a column the
721    /// baselines sit on the stacking axis, where aligning them would pile the children up.
722    fn baselines(
723        &self,
724        cx: &mut dyn LayoutOps,
725        kids: &[RNode],
726        sizes: &[Size],
727    ) -> Vec<Option<f64>> {
728        if self.align != CrossAlign::FirstBaseline || self.axis != Axis::Horizontal {
729            return vec![None; kids.len()];
730        }
731        kids.iter()
732            .zip(sizes)
733            .map(|(&k, &s)| cx.baseline_of(k, s))
734            .collect()
735    }
736
737    /// The row's own cross-axis extent once its children are baseline-shifted: the deepest
738    /// baseline plus the deepest descent below one. A row measured at the tallest child's
739    /// height would clip whichever child got shifted furthest down.
740    fn baseline_cross_extent(&self, baselines: &[Option<f64>], sizes: &[Size]) -> Option<f64> {
741        let deepest = baselines.iter().flatten().copied().fold(f64::MIN, f64::max);
742        if deepest == f64::MIN {
743            return None;
744        }
745        let below = baselines
746            .iter()
747            .zip(sizes)
748            .filter_map(|(b, s)| b.map(|b| self.cross(*s) - b))
749            .fold(0.0, f64::max);
750        Some(deepest + below)
751    }
752}
753
754impl Layout for StackLayout {
755    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
756        let mut kids = Vec::new();
757        Self::flatten(cx, children, &mut kids);
758        if kids.is_empty() {
759            return Size::ZERO;
760        }
761        let (main_p, cross_p) = self.split(p);
762        let sizes = self.negotiate(cx, &kids, p);
763        let spacing_total = self.spacing * (kids.len() - 1) as f64;
764        let has_flex = kids.iter().any(|&k| self.grows_main(cx.flex_of(k)));
765        // A stack with a stretching child fills what it is offered, but never reports less
766        // than its children need together (a `.min_width`, fixed-size pairs), so an overflow
767        // is visible to the parent (a `labeled` row stacks) instead of being clipped.
768        let needed = sizes.iter().map(|&s| self.main(s)).sum::<f64>() + spacing_total;
769        let main_total = match main_p {
770            Some(mp) if has_flex => mp.max(needed),
771            _ => needed,
772        };
773        let grows_cross = kids.iter().any(|&k| self.grows_cross(cx.flex_of(k)));
774        let tallest = sizes.iter().map(|&s| self.cross(s)).fold(0.0, f64::max);
775        // Baseline shifting can push a child below the tallest one's bottom edge, so the row
776        // takes the extent the shifted children actually occupy (docs/baseline.md).
777        let stacked = self
778            .baseline_cross_extent(&self.baselines(cx, &kids, &sizes), &sizes)
779            .unwrap_or(0.0)
780            .max(tallest);
781        let cross_total = match cross_p {
782            Some(cp) if grows_cross => cp,
783            _ => stacked,
784        };
785        self.mk(main_total, cross_total)
786    }
787
788    /// A stack's own baseline is its first baseline-bearing child's, moved by where that child
789    /// sits: down a column that is the child's offset along the axis, across a row every
790    /// aligned child already shares the deepest baseline (docs/baseline.md).
791    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
792        let mut kids = Vec::new();
793        Self::flatten(cx, children, &mut kids);
794        if kids.is_empty() {
795            return None;
796        }
797        let sizes = self.negotiate(cx, &kids, Proposal::exact(size));
798        match self.axis {
799            Axis::Horizontal => {
800                let baselines: Vec<Option<f64>> = kids
801                    .iter()
802                    .zip(&sizes)
803                    .map(|(&k, &s)| cx.baseline_of(k, s))
804                    .collect();
805                let deepest = baselines.iter().flatten().copied().fold(f64::MIN, f64::max);
806                (deepest != f64::MIN).then_some(deepest)
807            }
808            Axis::Vertical => {
809                let mut pos = 0.0;
810                for (&k, &s) in kids.iter().zip(&sizes) {
811                    if let Some(b) = cx.baseline_of(k, s) {
812                        return Some(pos + b);
813                    }
814                    pos += self.main(s) + self.spacing;
815                }
816                None
817            }
818        }
819    }
820
821    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
822        let mut kids = Vec::new();
823        Self::flatten(cx, children, &mut kids);
824        if kids.is_empty() {
825            return;
826        }
827        let sizes = self.negotiate(cx, &kids, Proposal::exact(bounds.size));
828        // The children's settled extent against what this stack was actually given: rigid
829        // children are free to answer a shrink proposal with their natural size (native
830        // buttons and labels routinely do), and then the tail of the stack lands offscreen
831        // with every assertion still green. Say so, once, where a developer will see it.
832        let needed = sizes.iter().map(|&s| self.main(s)).sum::<f64>()
833            + self.spacing * (kids.len() - 1) as f64;
834        let available = self.main(bounds.size);
835        // A container with no extent is not overflowing, it is not laid out yet: the state a
836        // native-owned surface is in until the backend reports its frame (a `cover` lays its
837        // content out once before `Event::FrameChanged` arrives, §7.6/docs/cover.md). Reporting
838        // it would name a real developer's ids for a transient the developer cannot act on:
839        // no fit policy makes a stack fit in zero points.
840        if available > 0.5 && needed > available + 0.5 {
841            cx.report_overflow(needed, available);
842        }
843        let bounds_cross = self.cross(bounds.size);
844        // Baseline row (docs/baseline.md): every child that has a baseline is shifted down so
845        // they all land on the deepest one, which is the offset that keeps every child inside
846        // the row. Children without one keep the centered fallback below.
847        let baselines = self.baselines(cx, &kids, &sizes);
848        let deepest = baselines.iter().flatten().copied().fold(f64::MIN, f64::max);
849        let mut pos = 0.0;
850        for (i, &k) in kids.iter().enumerate() {
851            let s = sizes[i];
852            let cross_off = match (self.align, baselines[i]) {
853                (CrossAlign::FirstBaseline, Some(b)) => deepest - b,
854                (CrossAlign::Leading, _) => 0.0,
855                (CrossAlign::Center | CrossAlign::FirstBaseline, _) => {
856                    ((bounds_cross - self.cross(s)) / 2.0).max(0.0)
857                }
858                (CrossAlign::Trailing, _) => (bounds_cross - self.cross(s)).max(0.0),
859            };
860            let rect = match self.axis {
861                Axis::Vertical => Rect::new(cross_off, pos, s.width, s.height),
862                Axis::Horizontal => Rect::new(pos, cross_off, s.width, s.height),
863            };
864            cx.place_child(k, rect);
865            pos += self.main(s) + self.spacing;
866        }
867    }
868}
869
870/// A wrapping row (docs/size-classes.md "Adaptive pieces"): children keep their natural
871/// measure and lay out leading-to-trailing, breaking onto a new line where the next child
872/// would overflow the proposed width. Wrapping replaces main-axis negotiation, so flex is
873/// inert here: nothing grows, and a `spacer()` contributes nothing. RTL mirroring comes from
874/// `place_child`, the same as every layout.
875pub struct FlowLayout {
876    pub spacing: f64,
877    /// Vertical gap between lines.
878    pub run_spacing: f64,
879    /// How children align within their own line (a line is as tall as its tallest child).
880    pub align: CrossAlign,
881    /// Uniform columns ([`RowFit::WrapColumns`](/api/day_pieces/enum.RowFit.html#variant.WrapColumns)): every cell takes the widest child's width and
882    /// each line holds as many as the available width fits, so the wrapped lines align into
883    /// columns; when any child grows (`grow_w`), the columns stretch to share the whole width.
884    /// `false` packs each line at natural widths, which comes out ragged.
885    pub uniform: bool,
886}
887
888/// One packed flow line: the child range it holds and its settled extent.
889struct FlowLine {
890    start: usize,
891    end: usize,
892    height: f64,
893}
894
895/// A solved flow: the size each child is placed at, and the lines they pack into. Measure and
896/// place both go through this, so the two never disagree about where a line breaks.
897struct FlowPlan {
898    sizes: Vec<Size>,
899    lines: Vec<FlowLine>,
900}
901
902impl FlowLayout {
903    /// Pack natural sizes into lines under `max_w`. Unbounded → one line, the degenerate case
904    /// that keeps a flow inside a horizontal scroll behaving like a plain row.
905    fn pack(&self, sizes: &[Size], max_w: Option<f64>) -> Vec<FlowLine> {
906        let mut lines = Vec::new();
907        let mut start = 0;
908        let mut w = 0.0;
909        let mut h: f64 = 0.0;
910        for (i, s) in sizes.iter().enumerate() {
911            let grown = if i == start {
912                s.width
913            } else {
914                w + self.spacing + s.width
915            };
916            if i > start && max_w.is_some_and(|m| grown > m + 0.5) {
917                lines.push(FlowLine {
918                    start,
919                    end: i,
920                    height: h,
921                });
922                start = i;
923                w = s.width;
924                h = s.height;
925            } else {
926                w = grown;
927                h = h.max(s.height);
928            }
929        }
930        if start < sizes.len() {
931            lines.push(FlowLine {
932                start,
933                end: sizes.len(),
934                height: h,
935            });
936        }
937        lines
938    }
939
940    fn natural_sizes(cx: &mut dyn LayoutOps, kids: &[RNode]) -> Vec<Size> {
941        kids.iter()
942            .map(|&k| cx.measure_child(k, Proposal::UNCONSTRAINED))
943            .collect()
944    }
945
946    /// Solve the flow for an available width: ragged lines at natural widths, or, under
947    /// `uniform`, one column width for every cell with as many columns per line as fit.
948    fn plan(&self, cx: &mut dyn LayoutOps, kids: &[RNode], max_w: Option<f64>) -> FlowPlan {
949        let natural = Self::natural_sizes(cx, kids);
950        if !self.uniform {
951            let lines = self.pack(&natural, max_w);
952            return FlowPlan {
953                sizes: natural,
954                lines,
955            };
956        }
957        // The widest child sets the column, so no cell is ever narrower than its content.
958        let cell_w = natural.iter().map(|s| s.width).fold(0.0, f64::max);
959        let cols = match max_w {
960            // How many whole columns fit, counting the gutter between them; never zero, or a
961            // window narrower than one cell would produce no lines at all.
962            Some(m) if cell_w > 0.0 => {
963                (((m + self.spacing) / (cell_w + self.spacing)).floor() as usize).max(1)
964            }
965            // Unbounded (inside a horizontal scroll): one line, like a plain row.
966            _ => kids.len().max(1),
967        };
968        // A growing child (`.grow_w()`) stretches the columns to share the whole line, the
969        // adaptive grid: `cell_w` is then the narrowest a column gets, and a line with fewer
970        // cells than columns leaves the rest empty. Without one, the leftover width trails the
971        // last column.
972        let col_w = match max_w {
973            Some(m) if kids.iter().any(|&k| cx.flex_of(k).grow_w) => {
974                ((m - self.spacing * (cols - 1) as f64) / cols as f64).max(cell_w)
975            }
976            _ => cell_w,
977        };
978        // Never wider than the line: a child whose natural width exceeds it (a card whose text
979        // runs long on a phone) takes the line's width and wraps its text, rather than running
980        // past the edge.
981        let col_w = max_w.map_or(col_w, |m| col_w.min(m));
982        // Re-measure at the column width: a child handed more width than it asked for can
983        // settle shorter (a label unwrapping), and the line's height follows what it settles
984        // on rather than what it wanted.
985        let sizes: Vec<Size> = kids
986            .iter()
987            .map(|&k| {
988                let s = cx.measure_child(k, Proposal::new(Some(col_w), None));
989                Size::new(col_w, s.height)
990            })
991            .collect();
992        let mut lines = Vec::new();
993        let mut start = 0;
994        while start < sizes.len() {
995            let end = (start + cols).min(sizes.len());
996            let height = sizes[start..end]
997                .iter()
998                .map(|s| s.height)
999                .fold(0.0, f64::max);
1000            lines.push(FlowLine { start, end, height });
1001            start = end;
1002        }
1003        FlowPlan { sizes, lines }
1004    }
1005}
1006
1007impl Layout for FlowLayout {
1008    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1009        let mut kids = Vec::new();
1010        StackLayout::flatten(cx, children, &mut kids);
1011        if kids.is_empty() {
1012            return Size::ZERO;
1013        }
1014        let plan = self.plan(cx, &kids, p.width);
1015        let height = plan.lines.iter().map(|l| l.height).sum::<f64>()
1016            + self.run_spacing * plan.lines.len().saturating_sub(1) as f64;
1017        // Bounded → fill the proposal (lines pack leading inside it, like wrapped text);
1018        // unbounded → the single packed line's own extent.
1019        let width = p.width.unwrap_or_else(|| {
1020            plan.sizes.iter().map(|s| s.width).sum::<f64>()
1021                + self.spacing * plan.sizes.len().saturating_sub(1) as f64
1022        });
1023        Size::new(width, height)
1024    }
1025
1026    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1027        let mut kids = Vec::new();
1028        StackLayout::flatten(cx, children, &mut kids);
1029        if kids.is_empty() {
1030            return;
1031        }
1032        let plan = self.plan(cx, &kids, Some(bounds.size.width));
1033        let mut y = 0.0;
1034        for line in &plan.lines {
1035            let mut x = 0.0;
1036            let range = line.start..line.end;
1037            for (&k, &s) in kids[range.clone()].iter().zip(&plan.sizes[range]) {
1038                let dy = ((line.height - s.height) * self.align.fraction()).max(0.0);
1039                cx.place_child(k, Rect::new(x, y + dy, s.width, s.height));
1040                x += s.width + self.spacing;
1041            }
1042            y += line.height + self.run_spacing;
1043        }
1044    }
1045}
1046
1047/// A grid cell, resolved from the realized tree: the node, its starting column, its span, and
1048/// its layout facts (docs/grid.md).
1049struct GridCell {
1050    node: RNode,
1051    col: usize,
1052    span: usize,
1053    flex: Flex,
1054}
1055
1056/// One grid row: either a `grid_row`'s cells, or a single full-width cell (a non-row child).
1057struct GridRowRef {
1058    cells: Vec<GridCell>,
1059    full_width: bool,
1060    valign: Option<CrossAlign>,
1061}
1062
1063/// The resolved geometry both [`GridLayout::measure`] and [`GridLayout::place`] work from.
1064struct GridGeom {
1065    rows: Vec<GridRowRef>,
1066    col_x: Vec<f64>,
1067    col_w: Vec<f64>,
1068    row_y: Vec<f64>,
1069    row_h: Vec<f64>,
1070    /// Pass-B size per cell, indexed `[row][cell]`; placement uses these, never re-measuring.
1071    cell_sizes: Vec<Vec<Size>>,
1072    size: Size,
1073}
1074
1075/// SwiftUI-style grid negotiation (docs/grid.md): columns are inferred from `grid_row` children,
1076/// a column's width is the max ideal width of its span-1 cells, a `grow_w` cell makes its column
1077/// flexible (leftover width split evenly, the [`StackLayout`] share rule), and a non-row child
1078/// is a full-width cell spanning every column. `spacer()` is an inert empty cell. The contract:
1079/// exactly two measure proposals per cell per layout, unconstrained (pass A) and at the final
1080/// column width (pass B), and `place` re-runs the same proposals, so it measures from cache.
1081pub struct GridLayout {
1082    pub row_spacing: f64,
1083    pub column_spacing: f64,
1084    pub align: Alignment,
1085}
1086
1087impl GridLayout {
1088    /// Expand group anchors and classify children into rows / full-width cells.
1089    fn collect(&self, cx: &mut dyn LayoutOps, children: &[RNode]) -> (Vec<GridRowRef>, usize) {
1090        let mut tops = Vec::new();
1091        StackLayout::flatten(cx, children, &mut tops);
1092        let mut rows = Vec::new();
1093        let mut ncols = 0usize;
1094        for &t in &tops {
1095            let f = cx.flex_of(t);
1096            if f.grid.is_row {
1097                let inner = cx.children_of(t);
1098                let mut cell_nodes = Vec::new();
1099                StackLayout::flatten(cx, &inner, &mut cell_nodes);
1100                let mut cells = Vec::new();
1101                let mut col = 0usize;
1102                for &c in &cell_nodes {
1103                    let cf = cx.flex_of(c);
1104                    let span = cf.grid.col_span.max(1) as usize;
1105                    cells.push(GridCell {
1106                        node: c,
1107                        col,
1108                        span,
1109                        flex: cf,
1110                    });
1111                    col += span;
1112                }
1113                ncols = ncols.max(col);
1114                rows.push(GridRowRef {
1115                    cells,
1116                    full_width: false,
1117                    valign: f.grid.row_valign,
1118                });
1119            } else {
1120                // A non-row child occupies a full-width row spanning every column (span is
1121                // patched to the final column count once it is known).
1122                rows.push(GridRowRef {
1123                    cells: vec![GridCell {
1124                        node: t,
1125                        col: 0,
1126                        span: 1,
1127                        flex: f,
1128                    }],
1129                    full_width: true,
1130                    valign: None,
1131                });
1132            }
1133        }
1134        (rows, ncols)
1135    }
1136
1137    fn span_width(&self, col_w: &[f64], col: usize, span: usize) -> f64 {
1138        let end = (col + span).min(col_w.len());
1139        let cols: f64 = col_w[col..end].iter().sum();
1140        cols + self.column_spacing * (end.saturating_sub(col + 1)) as f64
1141    }
1142
1143    /// The single geometry pass (docs/grid.md): pass A (unconstrained ideals → column widths),
1144    /// pass B (heights at final widths), then prefix sums. All decisions are closed-form (no
1145    /// iterative negotiation) and only `p.width` affects cell proposals, so a `measure` at
1146    /// `(Some(w), None)` and a `place` at `exact(w × h)` generate identical per-cell proposals.
1147    fn geometry(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> GridGeom {
1148        let (mut rows, ncols) = self.collect(cx, children);
1149        if rows.is_empty() {
1150            return GridGeom {
1151                rows,
1152                col_x: Vec::new(),
1153                col_w: Vec::new(),
1154                row_y: Vec::new(),
1155                row_h: Vec::new(),
1156                cell_sizes: Vec::new(),
1157                size: Size::ZERO,
1158            };
1159        }
1160        let ncols = ncols.max(1);
1161        for r in &mut rows {
1162            if r.full_width {
1163                r.cells[0].span = ncols;
1164            }
1165        }
1166        let unconstrained = Proposal::new(None, None);
1167
1168        // Pass A1, span-1 cells: rigid ideals set their column's width; grow_w flags it
1169        // flexible (its unconstrained ideal only matters when the grid itself is unconstrained).
1170        let mut col_ideal = vec![0.0f64; ncols];
1171        let mut col_flex = vec![false; ncols];
1172        let mut flex_ideal = vec![0.0f64; ncols];
1173        for r in rows.iter().filter(|r| !r.full_width) {
1174            for c in r.cells.iter().filter(|c| c.span == 1 && !c.flex.is_spacer) {
1175                if c.flex.grow_w {
1176                    col_flex[c.col] = true;
1177                    if p.width.is_none() {
1178                        let s = cx.measure_child(c.node, unconstrained);
1179                        flex_ideal[c.col] = flex_ideal[c.col].max(s.width);
1180                    }
1181                } else {
1182                    let s = cx.measure_child(c.node, unconstrained);
1183                    col_ideal[c.col] = col_ideal[c.col].max(s.width);
1184                }
1185            }
1186        }
1187        // Pass A2: flexible spanning cells only flag their columns…
1188        for r in rows.iter().filter(|r| !r.full_width) {
1189            for c in r.cells.iter().filter(|c| c.span > 1 && !c.flex.is_spacer) {
1190                if c.flex.grow_w {
1191                    let end = (c.col + c.span).min(ncols);
1192                    col_flex[c.col..end].fill(true);
1193                }
1194            }
1195        }
1196        // …pass A3: then rigid spanning cells distribute any width deficit in one shot: onto
1197        // the spanned flexible columns if any (they absorb width anyway), else evenly.
1198        for r in rows.iter().filter(|r| !r.full_width) {
1199            for c in r.cells.iter().filter(|c| c.span > 1 && !c.flex.is_spacer) {
1200                if c.flex.grow_w {
1201                    continue;
1202                }
1203                let s = cx.measure_child(c.node, unconstrained);
1204                let end = (c.col + c.span).min(ncols);
1205                let avail = self.span_width(&col_ideal, c.col, c.span);
1206                let deficit = s.width - avail;
1207                if deficit > 0.0 {
1208                    let flexed: Vec<usize> = (c.col..end).filter(|&k| col_flex[k]).collect();
1209                    let targets = if flexed.is_empty() {
1210                        (c.col..end).collect()
1211                    } else {
1212                        flexed
1213                    };
1214                    let add = deficit / targets.len() as f64;
1215                    for k in targets {
1216                        col_ideal[k] += add;
1217                    }
1218                }
1219            }
1220        }
1221        // Full-width cells: their ideal widens the grid when nothing constrains it; a grow_w
1222        // full-width cell makes the grid width flexible like a flexible column does.
1223        let mut fw_ideal = 0.0f64;
1224        let mut fw_flex = false;
1225        for r in rows.iter().filter(|r| r.full_width) {
1226            let c = &r.cells[0];
1227            if c.flex.is_spacer {
1228                continue;
1229            }
1230            if c.flex.grow_w {
1231                fw_flex = true;
1232            } else {
1233                let s = cx.measure_child(c.node, unconstrained);
1234                fw_ideal = fw_ideal.max(s.width);
1235            }
1236        }
1237
1238        // Resolve column widths (the StackLayout::negotiate share rule for flexible columns).
1239        let gutters = self.column_spacing * (ncols - 1) as f64;
1240        let has_flex_col = col_flex.iter().any(|&f| f);
1241        let mut col_w = vec![0.0f64; ncols];
1242        match p.width {
1243            Some(pw) if has_flex_col => {
1244                let rigid: f64 = (0..ncols)
1245                    .filter(|&k| !col_flex[k])
1246                    .map(|k| col_ideal[k])
1247                    .sum();
1248                let share = (pw - rigid - gutters).max(0.0)
1249                    / col_flex.iter().filter(|&&f| f).count() as f64;
1250                for k in 0..ncols {
1251                    col_w[k] = if col_flex[k] { share } else { col_ideal[k] };
1252                }
1253            }
1254            _ => {
1255                for k in 0..ncols {
1256                    col_w[k] = if col_flex[k] {
1257                        flex_ideal[k].max(col_ideal[k])
1258                    } else {
1259                        col_ideal[k]
1260                    };
1261                }
1262            }
1263        }
1264        let cols_total: f64 = col_w.iter().sum::<f64>() + gutters;
1265        let grid_w = match p.width {
1266            Some(pw) if has_flex_col || fw_flex => pw,
1267            _ => cols_total.max(fw_ideal),
1268        };
1269
1270        // Pass B: heights at final widths (text height-for-width happens here).
1271        let nrows = rows.len();
1272        let mut cell_sizes: Vec<Vec<Size>> = Vec::with_capacity(nrows);
1273        let mut row_h = vec![0.0f64; nrows];
1274        let mut row_flex = vec![false; nrows];
1275        for (ri, r) in rows.iter().enumerate() {
1276            let mut sizes = Vec::with_capacity(r.cells.len());
1277            for c in &r.cells {
1278                if c.flex.is_spacer {
1279                    sizes.push(Size::ZERO);
1280                    continue;
1281                }
1282                let w = if r.full_width {
1283                    grid_w
1284                } else {
1285                    self.span_width(&col_w, c.col, c.span)
1286                };
1287                let s = cx.measure_child(c.node, Proposal::new(Some(w), None));
1288                row_h[ri] = row_h[ri].max(s.height);
1289                if c.flex.grow_h {
1290                    row_flex[ri] = true;
1291                }
1292                sizes.push(s);
1293            }
1294            cell_sizes.push(sizes);
1295        }
1296        // Flexible rows stretch to a height proposal (additive: this never re-measures cells,
1297        // which keeps measure/place proposal identity).
1298        let vgutters = self.row_spacing * (nrows - 1) as f64;
1299        if let Some(ph) = p.height
1300            && row_flex.iter().any(|&f| f)
1301        {
1302            let total: f64 = row_h.iter().sum::<f64>() + vgutters;
1303            let extra = (ph - total).max(0.0) / row_flex.iter().filter(|&&f| f).count() as f64;
1304            for (ri, flexed) in row_flex.iter().enumerate() {
1305                if *flexed {
1306                    row_h[ri] += extra;
1307                }
1308            }
1309        }
1310        let grid_h = row_h.iter().sum::<f64>() + vgutters;
1311
1312        let mut col_x = vec![0.0f64; ncols];
1313        let mut x = 0.0;
1314        for k in 0..ncols {
1315            col_x[k] = x;
1316            x += col_w[k] + self.column_spacing;
1317        }
1318        let mut row_y = vec![0.0f64; nrows];
1319        let mut y = 0.0;
1320        for ri in 0..nrows {
1321            row_y[ri] = y;
1322            y += row_h[ri] + self.row_spacing;
1323        }
1324        GridGeom {
1325            rows,
1326            col_x,
1327            col_w,
1328            row_y,
1329            row_h,
1330            cell_sizes,
1331            size: Size::new(grid_w, grid_h),
1332        }
1333    }
1334}
1335
1336impl Layout for GridLayout {
1337    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1338        self.geometry(cx, children, p).size
1339    }
1340
1341    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1342        let g = self.geometry(cx, children, Proposal::exact(bounds.size));
1343        for (ri, r) in g.rows.iter().enumerate() {
1344            for (ci, c) in r.cells.iter().enumerate() {
1345                if c.flex.is_spacer {
1346                    continue;
1347                }
1348                let cell = if r.full_width {
1349                    Rect::new(0.0, g.row_y[ri], g.size.width, g.row_h[ri])
1350                } else {
1351                    Rect::new(
1352                        g.col_x[c.col],
1353                        g.row_y[ri],
1354                        self.span_width(&g.col_w, c.col, c.span),
1355                        g.row_h[ri],
1356                    )
1357                };
1358                let s = g.cell_sizes[ri][ci];
1359                let w = if c.flex.grow_w {
1360                    cell.size.width
1361                } else {
1362                    s.width.min(cell.size.width)
1363                };
1364                let h = if c.flex.grow_h {
1365                    cell.size.height
1366                } else {
1367                    s.height.min(cell.size.height)
1368                };
1369                // Alignment precedence per axis: cell `.grid_align` > row `.align` (vertical
1370                // only) > the grid's own alignment.
1371                let hf = match c.flex.grid.align {
1372                    Some(a) => a.h_fraction(),
1373                    None => self.align.h_fraction(),
1374                };
1375                let vf = match (c.flex.grid.align, r.valign) {
1376                    (Some(a), _) => a.v_fraction(),
1377                    (None, Some(v)) => v.fraction(),
1378                    (None, None) => self.align.v_fraction(),
1379                };
1380                let x = cell.origin.x + (cell.size.width - w) * hf;
1381                let y = cell.origin.y + (cell.size.height - h) * vf;
1382                cx.place_child(c.node, Rect::new(x, y, w, h));
1383            }
1384        }
1385        // Row nodes are transparent carriers and are never placed.
1386    }
1387}
1388
1389/// Two-axis placement of a child within a container's bounds (SwiftUI's `Alignment`). Used by
1390/// the z-layering primitives ([`OverlayLayout`]): `zstack`, `overlay`/`overlay_aligned`.
1391#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
1392pub enum Alignment {
1393    TopLeading,
1394    Top,
1395    TopTrailing,
1396    Leading,
1397    #[default]
1398    Center,
1399    Trailing,
1400    BottomLeading,
1401    Bottom,
1402    BottomTrailing,
1403}
1404
1405impl Alignment {
1406    /// Horizontal placement fraction of the free space: 0 = leading, 0.5 = center, 1 = trailing.
1407    fn h_fraction(self) -> f64 {
1408        match self {
1409            Alignment::TopLeading | Alignment::Leading | Alignment::BottomLeading => 0.0,
1410            Alignment::Top | Alignment::Center | Alignment::Bottom => 0.5,
1411            Alignment::TopTrailing | Alignment::Trailing | Alignment::BottomTrailing => 1.0,
1412        }
1413    }
1414    /// Vertical placement fraction of the free space: 0 = top, 0.5 = center, 1 = bottom.
1415    fn v_fraction(self) -> f64 {
1416        match self {
1417            Alignment::TopLeading | Alignment::Top | Alignment::TopTrailing => 0.0,
1418            Alignment::Leading | Alignment::Center | Alignment::Trailing => 0.5,
1419            Alignment::BottomLeading | Alignment::Bottom | Alignment::BottomTrailing => 1.0,
1420        }
1421    }
1422}
1423
1424/// Z-layering (§overlay): children share the container bounds, stacked back-to-front in child
1425/// order (first child = bottom of the z-order), each positioned by a single [`Alignment`].
1426/// `size_to_first` reports only the first child's natural size, the badge/annotation sizing of
1427/// [`overlay`](crate) (the annotation does not grow the frame); otherwise the layout reports the
1428/// union (max) of all children's natural sizes, the ZStack sizing of `zstack`. No native work:
1429/// the container is the same panel as `column`/`row`, so backends stack children by attach order.
1430pub struct OverlayLayout {
1431    pub align: Alignment,
1432    pub size_to_first: bool,
1433}
1434
1435impl OverlayLayout {
1436    /// Expand group anchors (`when`/`each`) inline, exactly like [`StackLayout`].
1437    fn flatten(cx: &mut dyn LayoutOps, children: &[RNode], out: &mut Vec<RNode>) {
1438        for &c in children {
1439            if cx.flex_of(c).is_group {
1440                let inner = cx.children_of(c);
1441                Self::flatten(cx, &inner, out);
1442            } else {
1443                out.push(c);
1444            }
1445        }
1446    }
1447}
1448
1449impl Layout for OverlayLayout {
1450    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1451        let mut kids = Vec::new();
1452        Self::flatten(cx, children, &mut kids);
1453        if self.size_to_first {
1454            return match kids.first() {
1455                Some(&c) => cx.measure_child(c, p),
1456                None => Size::ZERO,
1457            };
1458        }
1459        let mut size = Size::ZERO;
1460        for &c in &kids {
1461            let s = cx.measure_child(c, p);
1462            size.width = size.width.max(s.width);
1463            size.height = size.height.max(s.height);
1464        }
1465        size
1466    }
1467    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1468        let mut kids = Vec::new();
1469        Self::flatten(cx, children, &mut kids);
1470        for &c in &kids {
1471            let s = cx.measure_child(c, Proposal::exact(bounds.size));
1472            let x = (bounds.size.width - s.width) * self.align.h_fraction();
1473            let y = (bounds.size.height - s.height) * self.align.v_fraction();
1474            cx.place_child(c, Rect::new(x, y, s.width, s.height));
1475        }
1476    }
1477}
1478
1479pub struct PaddingLayout {
1480    pub insets: Insets,
1481}
1482
1483impl Layout for PaddingLayout {
1484    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1485        let inner = Proposal::new(
1486            p.width
1487                .map(|w| (w - self.insets.horizontal_total()).max(0.0)),
1488            p.height
1489                .map(|h| (h - self.insets.vertical_total()).max(0.0)),
1490        );
1491        let s = match children.first() {
1492            Some(&c) => cx.measure_child(c, inner),
1493            None => Size::ZERO,
1494        };
1495        Size::new(
1496            s.width + self.insets.horizontal_total(),
1497            s.height + self.insets.vertical_total(),
1498        )
1499    }
1500    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1501        if let Some(&c) = children.first() {
1502            let inner = bounds.inset_by(self.insets);
1503            let s = cx.measure_child(c, Proposal::exact(inner.size));
1504            cx.place_child(
1505                c,
1506                Rect {
1507                    origin: inner.origin,
1508                    size: s,
1509                },
1510            );
1511        }
1512    }
1513    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
1514        // The one wrapper with a real offset: its child starts below the top inset.
1515        let inner = Size::new(
1516            (size.width - self.insets.horizontal_total()).max(0.0),
1517            (size.height - self.insets.vertical_total()).max(0.0),
1518        );
1519        wrapper_baseline(cx, children, inner, self.insets.top)
1520    }
1521}
1522
1523/// The `aspect_ratio` decorator (§5.2): the largest `width / height == ratio` box that fits what
1524/// the parent proposes, with the child given that whole box.
1525///
1526/// With one axis proposed it derives the other, which is what makes `.grow_w().aspect_ratio(r)`
1527/// take the width a container offers and compute the height from it: a canvas that keeps its
1528/// proportions as the window resizes.
1529pub struct AspectRatioLayout {
1530    pub ratio: f64,
1531}
1532
1533impl Layout for AspectRatioLayout {
1534    fn measure(&self, cx: &mut dyn LayoutOps, _children: &[RNode], p: Proposal) -> Size {
1535        match (p.width, p.height) {
1536            (Some(w), Some(h)) => {
1537                if w / h > self.ratio {
1538                    Size::new(h * self.ratio, h)
1539                } else {
1540                    Size::new(w, w / self.ratio)
1541                }
1542            }
1543            (Some(w), None) => Size::new(w, w / self.ratio),
1544            (None, Some(h)) => Size::new(h * self.ratio, h),
1545            (None, None) => cx.measure_leaf(p),
1546        }
1547    }
1548    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1549        // The ratio has already decided the box; hand all of it to the child.
1550        for &c in children {
1551            cx.place_child(c, Rect::from_size(bounds.size));
1552        }
1553    }
1554    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
1555        PassThrough::forward(cx, children, size)
1556    }
1557}
1558
1559/// The `grow`/`grow_w`/`grow_h` decorators (§5.2): a single-child wrapper carrying grow [`Flex`]
1560/// so the parent stack OFFERS it the space, and a greedy measure/place so the child actually
1561/// FILLS it. Non-grown axes hug the child (like `frame(maxWidth: .infinity)` on one axis).
1562pub struct GrowLayout {
1563    pub w: bool,
1564    pub h: bool,
1565}
1566
1567impl Layout for GrowLayout {
1568    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1569        let cs = match children.first() {
1570            Some(&c) => cx.measure_child(c, p),
1571            None => Size::ZERO,
1572        };
1573        // A grown axis fills what is offered, but never reports less than the child needs, so
1574        // a child that cannot shrink to the offer (a `.min_width`, a fixed-size pair) overflows
1575        // visibly and its row can respond (a `labeled` row stacks) instead of clipping it.
1576        Size::new(
1577            if self.w {
1578                p.width.map_or(cs.width, |w| w.max(cs.width))
1579            } else {
1580                cs.width
1581            },
1582            if self.h {
1583                p.height.map_or(cs.height, |h| h.max(cs.height))
1584            } else {
1585                cs.height
1586            },
1587        )
1588    }
1589    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1590        if let Some(&c) = children.first() {
1591            let cs = cx.measure_child(c, Proposal::exact(bounds.size));
1592            // Fill the grown axes; hug the child on the rest.
1593            let w = if self.w { bounds.size.width } else { cs.width };
1594            let h = if self.h {
1595                bounds.size.height
1596            } else {
1597                cs.height
1598            };
1599            cx.place_child(c, Rect::from_size(Size::new(w, h)));
1600        }
1601    }
1602    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
1603        PassThrough::forward(cx, children, size)
1604    }
1605}
1606
1607/// The `.max_width(w)` decorator (docs/layout.md): proposes at most `w` to the child, so
1608/// text wraps instead of overflowing, while narrower content still hugs. The vertical axis
1609/// passes through untouched.
1610pub struct MaxWidthLayout {
1611    pub max: f64,
1612}
1613
1614impl Layout for MaxWidthLayout {
1615    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1616        let capped = Proposal::new(
1617            Some(p.width.map(|w| w.min(self.max)).unwrap_or(self.max)),
1618            p.height,
1619        );
1620        match children.first() {
1621            Some(&c) => {
1622                let s = cx.measure_child(c, capped);
1623                Size::new(s.width.min(self.max), s.height)
1624            }
1625            None => Size::ZERO,
1626        }
1627    }
1628    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1629        if let Some(&c) = children.first() {
1630            let w = bounds.size.width.min(self.max);
1631            let s = cx.measure_child(c, Proposal::exact(Size::new(w, bounds.size.height)));
1632            cx.place_child(c, Rect::from_size(Size::new(s.width.min(w), s.height)));
1633        }
1634    }
1635    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
1636        PassThrough::forward(cx, children, size)
1637    }
1638}
1639
1640/// The `.max_height(h)` decorator, [`MaxWidthLayout`]'s vertical twin: proposes at most `h` to
1641/// the child, so a scroll or anything else that takes the height it is offered stops at `h`,
1642/// while shorter content still hugs. Where the parent offers less than `h` (a phone held
1643/// sideways), the child gets that, so a capped box still fits a short screen. The horizontal
1644/// axis passes through untouched.
1645pub struct MaxHeightLayout {
1646    pub max: f64,
1647}
1648
1649impl Layout for MaxHeightLayout {
1650    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1651        let capped = Proposal::new(
1652            p.width,
1653            Some(p.height.map(|h| h.min(self.max)).unwrap_or(self.max)),
1654        );
1655        match children.first() {
1656            Some(&c) => {
1657                let s = cx.measure_child(c, capped);
1658                Size::new(s.width, s.height.min(self.max))
1659            }
1660            None => Size::ZERO,
1661        }
1662    }
1663    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1664        if let Some(&c) = children.first() {
1665            let h = bounds.size.height.min(self.max);
1666            let s = cx.measure_child(c, Proposal::exact(Size::new(bounds.size.width, h)));
1667            cx.place_child(c, Rect::from_size(Size::new(s.width, s.height.min(h))));
1668        }
1669    }
1670    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
1671        PassThrough::forward(cx, children, size)
1672    }
1673}
1674
1675/// The `.min_width(w)` decorator: the child measures as it would, but never reports narrower
1676/// than `w`, and is placed across whatever width the wrapper is given. A stretching control
1677/// (a slider) in a row that is being squeezed thereby overflows its row instead of shrinking
1678/// to nothing, which is what lets a `labeled` row notice and stack (docs/forms.md). The
1679/// vertical axis passes through untouched.
1680pub struct MinWidthLayout {
1681    pub min: f64,
1682}
1683
1684impl Layout for MinWidthLayout {
1685    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1686        let floored = Proposal::new(p.width.map(|w| w.max(self.min)), p.height);
1687        match children.first() {
1688            Some(&c) => {
1689                let s = cx.measure_child(c, floored);
1690                Size::new(s.width.max(self.min), s.height)
1691            }
1692            None => Size::new(self.min, 0.0),
1693        }
1694    }
1695    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1696        if let Some(&c) = children.first() {
1697            let w = bounds.size.width.max(self.min);
1698            let s = cx.measure_child(c, Proposal::exact(Size::new(w, bounds.size.height)));
1699            cx.place_child(c, Rect::from_size(Size::new(w, s.height)));
1700        }
1701    }
1702    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
1703        PassThrough::forward(cx, children, size)
1704    }
1705}
1706
1707/// Reserve at least the size of a sample child, then lay the content out inside it.
1708///
1709/// The problem it solves: a numeric readout beside a slider changes width as the value changes
1710/// (`1` is narrower than `8` in a proportional font, and `9` → `10` adds a whole glyph), so the
1711/// row reflows on every drag and the control you are aiming at moves. A hardcoded width "fixes"
1712/// that until the reader raises their accessibility text size, at which point the number is
1713/// clipped.
1714///
1715/// Reserving a sample measures a real piece in the real font at the real size, so the reservation
1716/// scales with the text exactly as the content does. `children[0]` is the sample (built
1717/// transparent by `Decorate::reserving`, so it never paints and is placed at zero size);
1718/// `children[1]` is the content, which receives the full reserved bounds and aligns itself.
1719pub struct ReserveLayout;
1720
1721impl Layout for ReserveLayout {
1722    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1723        let (Some(&sample), Some(&content)) = (children.first(), children.get(1)) else {
1724            return children
1725                .first()
1726                .map(|&c| cx.measure_child(c, p))
1727                .unwrap_or(Size::ZERO);
1728        };
1729        let s = cx.measure_child(sample, p);
1730        let c = cx.measure_child(content, p);
1731        Size::new(s.width.max(c.width), s.height.max(c.height))
1732    }
1733
1734    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1735        // The sample has done its work in `measure`; give it no area so it costs nothing to draw.
1736        if let Some(&sample) = children.first() {
1737            cx.place_child(sample, Rect::from_size(Size::ZERO));
1738        }
1739        if let Some(&content) = children.get(1) {
1740            let s = cx.measure_child(content, Proposal::exact(bounds.size));
1741            let _ = s;
1742            cx.place_child(content, Rect::from_size(bounds.size));
1743        }
1744    }
1745}
1746
1747pub struct FrameLayout {
1748    pub width: Option<f64>,
1749    pub height: Option<f64>,
1750}
1751
1752impl Layout for FrameLayout {
1753    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1754        let child_p = Proposal::new(self.width.or(p.width), self.height.or(p.height));
1755        let s = match children.first() {
1756            Some(&c) => cx.measure_child(c, child_p),
1757            None => Size::ZERO,
1758        };
1759        Size::new(
1760            self.width.unwrap_or(s.width),
1761            self.height.unwrap_or(s.height),
1762        )
1763    }
1764    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1765        if let Some(&c) = children.first() {
1766            cx.place_child(c, bounds);
1767        }
1768    }
1769    fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
1770        PassThrough::forward(cx, children, size)
1771    }
1772}
1773
1774/// Scroll viewport (§7.6): greedy on the proposal; content measured unconstrained on the
1775/// scroll axis and reported via `set_scroll_content`. Children are placed in the scroll's
1776/// content coordinate space (the scroll node is their native ancestor).
1777pub struct ScrollLayout {
1778    pub axis: Axis,
1779}
1780
1781impl Layout for ScrollLayout {
1782    fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
1783        let content_p = match self.axis {
1784            Axis::Vertical => Proposal::new(p.width, None),
1785            Axis::Horizontal => Proposal::new(None, p.height),
1786        };
1787        let cs = match children.first() {
1788            Some(&c) => cx.measure_child(c, content_p),
1789            None => Size::ZERO,
1790        };
1791        Size::new(p.width.unwrap_or(cs.width), p.height.unwrap_or(cs.height))
1792    }
1793    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1794        if let Some(&c) = children.first() {
1795            let vertical = self.axis == Axis::Vertical;
1796            let proposal = |cross: f64| match self.axis {
1797                Axis::Vertical => Proposal::new(Some(cross), None),
1798                Axis::Horizontal => Proposal::new(None, Some(cross)),
1799            };
1800            let (cross, main) = match self.axis {
1801                Axis::Vertical => (bounds.size.width, bounds.size.height),
1802                Axis::Horizontal => (bounds.size.height, bounds.size.width),
1803            };
1804            let mut cs = cx.measure_child(c, proposal(cross));
1805            let overflows = |s: Size| if vertical { s.height } else { s.width } > main;
1806            // Content that overflows brings a scroll bar up. Where that bar is a classic one
1807            // taking room from the viewport (Qt's Fusion style), lay the content out beside it,
1808            // or the content is wider than what is left and scrolls sideways by the bar's width.
1809            let mut cross = cross;
1810            if overflows(cs) {
1811                let inset = cx.scroll_bar_inset(vertical);
1812                if inset > 0.0 {
1813                    cross = (cross - inset).max(0.0);
1814                    cs = cx.measure_child(c, proposal(cross));
1815                }
1816            }
1817            let content = match self.axis {
1818                Axis::Vertical => Size::new(cross, cs.height.max(bounds.size.height)),
1819                Axis::Horizontal => Size::new(cs.width.max(bounds.size.width), cross),
1820            };
1821            cx.place_child(c, Rect::from_size(content));
1822            cx.set_scroll_content(content);
1823        }
1824    }
1825}
1826
1827/// Navigation host (docs/navigation.md): page frames are native-owned (splitter panes,
1828/// nav-controller views), so `set_frame` on pages is a toolkit no-op; Day lays each page's
1829/// content within the size the toolkit last reported via `Event::FrameChanged`, falling
1830/// back to a sidebar/detail split (or the full host) of the host bounds.
1831pub struct NavLayout {
1832    pub sizes: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<RNode, Size>>>,
1833    /// The presentation as last resolved. Shared with the piece rather than copied, so a
1834    /// re-present (`NavPatch::Presentation`) changes the fallback split without rebuilding the
1835    /// layout object the host was realized with.
1836    pub presentation: std::rc::Rc<std::cell::Cell<day_spec::props::NavPresentation>>,
1837    /// The host's sidebar page, once it has one. Identity, not position: after a re-present the
1838    /// pages keep their roles but a backend may have re-homed them in a different order.
1839    pub sidebar: std::rc::Rc<std::cell::Cell<Option<RNode>>>,
1840    /// The host's content-list page (`Pane::List`), once it has one; shared like `sidebar`.
1841    pub list: std::rc::Rc<std::cell::Cell<Option<RNode>>>,
1842    /// The content-list pane's preferred width (`NavProps::list_width`), for the fallback split.
1843    pub list_width: f64,
1844    /// Whether the pane is showing (`NavPatch::ListVisible`), shared with the piece so a
1845    /// collapsed pane stops narrowing the detail's fallback immediately, rather than one
1846    /// FrameChanged report later.
1847    pub list_visible: std::rc::Rc<std::cell::Cell<bool>>,
1848}
1849
1850pub use day_spec::{NAV_LIST_WIDTH, NAV_SIDEBAR_WIDTH};
1851
1852impl NavLayout {
1853    /// A host with no sidebar pane and no re-presenting to do: a tab strip's page area, or a
1854    /// `nav_stack` piece, both of which stay stacked at every size.
1855    pub fn stack(
1856        sizes: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<RNode, Size>>>,
1857    ) -> Self {
1858        NavLayout {
1859            sizes,
1860            presentation: std::rc::Rc::new(std::cell::Cell::new(
1861                day_spec::props::NavPresentation::Stack,
1862            )),
1863            sidebar: std::rc::Rc::new(std::cell::Cell::new(None)),
1864            list: std::rc::Rc::new(std::cell::Cell::new(None)),
1865            list_width: NAV_LIST_WIDTH,
1866            list_visible: std::rc::Rc::new(std::cell::Cell::new(true)),
1867        }
1868    }
1869}
1870
1871impl Layout for NavLayout {
1872    fn measure(&self, _cx: &mut dyn LayoutOps, _children: &[RNode], p: Proposal) -> Size {
1873        // Greedy: the host owns the window. A nested stack merges into the enclosing host's page
1874        // list rather than nesting a second host (docs/navigation.md), so one NAV host still
1875        // spans the window.
1876        Size::new(p.width.unwrap_or(480.0), p.height.unwrap_or(640.0))
1877    }
1878    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1879        let pres = self.presentation.get();
1880        let split = pres.is_split();
1881        let sidebar = self.sidebar.get();
1882        let list = self.list.get();
1883        // The list pane narrows the detail's fallback only while it has its own pane and it
1884        // is showing; a collapsed pane gives the detail its width back at once.
1885        let list_w = if split && list.is_some() && self.list_visible.get() {
1886            self.list_width
1887        } else {
1888            0.0
1889        };
1890        for &page in children {
1891            let reported = self.sizes.borrow().get(&page).copied();
1892            // Only a fallback: every backend reports each page's real frame through
1893            // `Event::FrameChanged`, and that wins. This is what the page gets for the frame
1894            // before the first report arrives.
1895            let sz = reported.unwrap_or_else(|| {
1896                let is_sidebar = Some(page) == sidebar;
1897                let is_list = Some(page) == list;
1898                if pres.rows_are_chrome() {
1899                    // The rows are the chrome (a tab bar, a rail): the backend draws them itself
1900                    // and sizes its own bar, so the sidebar page is measured but not shown. Keep
1901                    // it at the pane width rather than zero, since a zero-width menu measured here
1902                    // would have to re-measure from scratch the moment the window widens back
1903                    // into a split.
1904                    if is_sidebar {
1905                        Size::new(NAV_SIDEBAR_WIDTH, bounds.size.height)
1906                    } else if is_list {
1907                        Size::new(self.list_width, bounds.size.height)
1908                    } else {
1909                        bounds.size
1910                    }
1911                } else if !split {
1912                    // Includes a stacked host's list page: full width where it joins the stack
1913                    // (`Cap::NavContentList` Emulated); where its pane persists instead
1914                    // (Native, a narrow Mac window) the first FrameChanged report corrects it.
1915                    bounds.size
1916                } else if is_sidebar {
1917                    Size::new(NAV_SIDEBAR_WIDTH, bounds.size.height)
1918                } else if is_list {
1919                    Size::new(self.list_width, bounds.size.height)
1920                } else {
1921                    Size::new(
1922                        (bounds.size.width - NAV_SIDEBAR_WIDTH - list_w - 1.0).max(0.0),
1923                        bounds.size.height,
1924                    )
1925                }
1926            });
1927            cx.place_child_native(page, Rect::from_size(sz));
1928        }
1929    }
1930}
1931
1932/// Inspector split (docs/inspector.md): the two pane frames are native-owned (an
1933/// `NSSplitView`'s panes, a dock widget), so Day lays each pane's content within the size the
1934/// toolkit last reported via `Event::FrameChanged`, the same native-owned-frame contract as
1935/// [`NavLayout`] pages. The fallback before the first report splits the host bounds at the
1936/// pane width when visible, and gives the content everything when hidden.
1937pub struct InspectorLayout {
1938    pub sizes: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<RNode, Size>>>,
1939    /// Visibility as last patched, shared with the piece so a toggle changes the fallback
1940    /// split without rebuilding the layout object the host was realized with.
1941    pub visible: std::rc::Rc<std::cell::Cell<bool>>,
1942    /// The pane's preferred width (`InspectorProps::width`), for the fallback split.
1943    pub width: f64,
1944}
1945
1946impl Layout for InspectorLayout {
1947    fn measure(&self, _cx: &mut dyn LayoutOps, _children: &[RNode], p: Proposal) -> Size {
1948        // Greedy, like a nav host: the split owns whatever its parent proposes.
1949        Size::new(p.width.unwrap_or(480.0), p.height.unwrap_or(640.0))
1950    }
1951    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1952        // Insertion order is the contract (day-spec `Builtin::Inspector`): 0 content, 1 panel.
1953        for (i, &pane) in children.iter().enumerate() {
1954            let reported = self.sizes.borrow().get(&pane).copied();
1955            let sz = reported.unwrap_or_else(|| {
1956                let panel_w = if self.visible.get() { self.width } else { 0.0 };
1957                if i == 0 {
1958                    Size::new((bounds.size.width - panel_w).max(0.0), bounds.size.height)
1959                } else {
1960                    Size::new(panel_w, bounds.size.height)
1961                }
1962            });
1963            cx.place_child_native(pane, Rect::from_size(sz));
1964        }
1965    }
1966}
1967
1968/// Native split (docs/split.md): the two pane frames are native-owned (an `NSSplitView`'s, a
1969/// `GtkPaned`'s, a `QSplitter`'s), so Day lays each pane's content within the size the toolkit
1970/// last reported via `Event::FrameChanged`, the [`InspectorLayout`] contract. The fallback
1971/// before the first report divides the host bounds by the bound fraction along the axis.
1972pub struct SplitLayout {
1973    pub sizes: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<RNode, Size>>>,
1974    /// Stacked (`true`) or side by side, as last patched; shared with the piece.
1975    pub stacked: std::rc::Rc<std::cell::Cell<bool>>,
1976    /// The first pane's share, as last patched; shared with the piece.
1977    pub fraction: std::rc::Rc<std::cell::Cell<f64>>,
1978}
1979
1980impl Layout for SplitLayout {
1981    fn measure(&self, _cx: &mut dyn LayoutOps, _children: &[RNode], p: Proposal) -> Size {
1982        // Greedy, like a nav host: the split owns whatever its parent proposes.
1983        Size::new(p.width.unwrap_or(480.0), p.height.unwrap_or(640.0))
1984    }
1985    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
1986        // Insertion order is the contract (day-spec `Builtin::Split`): 0 first, 1 second.
1987        let f = self.fraction.get().clamp(0.0, 1.0);
1988        let (w, h) = (bounds.size.width, bounds.size.height);
1989        for (i, &pane) in children.iter().enumerate() {
1990            let reported = self.sizes.borrow().get(&pane).copied();
1991            let sz = reported.unwrap_or_else(|| {
1992                let share = if i == 0 { f } else { 1.0 - f };
1993                if self.stacked.get() {
1994                    Size::new(w, (h * share).floor())
1995                } else {
1996                    Size::new((w * share).floor(), h)
1997                }
1998            });
1999            cx.place_child_native(pane, Rect::from_size(sz));
2000        }
2001    }
2002}
2003
2004/// Fullscreen cover (docs/cover.md): the cover node occupies no space where it sits in the
2005/// tree (its native surface is presented over the window, outside the parent's bounds), and
2006/// its content is laid out at the size the backend reported via `Event::FrameChanged`, the
2007/// same native-owned-frame contract as [`NavLayout`] pages.
2008pub struct CoverLayout {
2009    pub size: std::rc::Rc<std::cell::RefCell<Option<Size>>>,
2010}
2011
2012impl Layout for CoverLayout {
2013    fn measure(&self, _cx: &mut dyn LayoutOps, _children: &[RNode], _p: Proposal) -> Size {
2014        Size::new(0.0, 0.0)
2015    }
2016    fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], _bounds: Rect) {
2017        let Some(sz) = *self.size.borrow() else {
2018            return; // not presented yet; content lays out on the first FrameChanged
2019        };
2020        for &child in children {
2021            cx.place_child_native(child, Rect::from_size(sz));
2022        }
2023    }
2024}
2025
2026/// Helper for constructing shared layout Rcs.
2027pub fn rc_layout<L: Layout>(l: L) -> Rc<dyn Layout> {
2028    Rc::new(l)
2029}