Skip to main content

kui_core/
layout.rs

1//! The flex solver: clay-style layout over the flat tree, run by
2//! [`compute`] once per frame.
3//!
4//! Each node declares a [`Sizing`] per axis (`Fit`,
5//! `Grow`, `Fixed`, `Percent` or a size expression), optional clamps, a
6//! direction, padding, gap and alignment; see
7//! [`LayoutSpec`](crate::spec::LayoutSpec). The solver resolves them in
8//! five passes over the preorder tree:
9//!
10//! 1. fit widths (reverse: children before parents)
11//! 2. grow widths (forward: parents before children)
12//! 3. fit heights (reverse; text wraps at its final width here)
13//! 4. grow heights (forward)
14//! 5. positions (forward)
15//!
16//! A wrapping row breaks its children into lines in pass 2 and every later
17//! pass reads that grouping, which is why wrapping is rows-only: a row's
18//! width is final before its height is measured, where a column would need
19//! its height first. A sixth pass runs only on a frame with a float
20//! anchored to a node by key (`FloatAnchor::Node`), since such a float's
21//! anchor may come later in preorder; its subtree is laid out again from
22//! the anchor's final rect.
23//!
24//! A table (`LayoutSpec::table`) is a column whose rows' cells line up:
25//! pass 1 reaches the table after its rows and cells and sets every fit
26//! cell to its column's widest and every row to the columns' width, so the
27//! table's own fit width is the aligned columns; pass 2 reaches it before
28//! its rows, resolves the columns against the widest row once and writes
29//! each column's width into its cells, and a row of a table then leaves
30//! its children alone. A column, a table or a leaf straight under the
31//! table has no cells.
32//!
33//! Text measurement goes through [`TextMeasure`], so the solver is
34//! testable with a deterministic stub and never depends on system fonts.
35
36use crate::geom::{Rect, Size, Vec2};
37use crate::scroll::ScrollStore;
38use crate::spec::{Align, Dir, FloatAnchor, Min, Sizing};
39use crate::tree::{NIL, NodeContent, Tree};
40
41/// Gated on the tree-level flag first: on a frame with no floats this is
42/// one predicted branch rather than a read through every child's spec.
43#[inline]
44fn is_float(tree: &Tree, i: u32) -> bool {
45    tree.any_float && tree.specs[i as usize].layout.float.is_some()
46}
47
48/// Where in the free space one thing sits: 0 at the start, 1 at the end.
49/// The spreads and `Baseline` mean nothing for a single placement — a
50/// cross axis, a float's attach point — and land where a spread puts a
51/// lone child (`diag::ALIGN_IGNORED` says so where one is declared).
52fn align_factor(a: Align) -> f32 {
53    match a {
54        Align::Start | Align::SpaceBetween | Align::Baseline => 0.0,
55        Align::Center | Align::SpaceAround | Align::SpaceEvenly => 0.5,
56        Align::End => 1.0,
57    }
58}
59
60/// How the main axis's free space is dealt out to `n` in-flow children:
61/// what goes before the first, and what goes between each two on top of
62/// the gap. Nothing is dealt when nothing is free, so a
63/// run that overflows or holds a grow child is laid out by its gaps
64/// alone, whatever the alignment.
65fn main_spread(a: Align, free: f32, n: u32) -> (f32, f32) {
66    let n = n as f32;
67    match a {
68        Align::SpaceBetween if n > 1.0 => (0.0, free / (n - 1.0)),
69        Align::SpaceAround if n > 0.0 => (free / (2.0 * n), free / n),
70        Align::SpaceEvenly => (free / (n + 1.0), free / (n + 1.0)),
71        _ => (align_factor(a) * free, 0.0),
72    }
73}
74
75fn mirror(a: Align) -> Align {
76    match a {
77        Align::Start => Align::End,
78        Align::End => Align::Start,
79        other => other,
80    }
81}
82
83/// Whether the in-flow children of `i` line up by their baselines: a row
84/// that says so. A column's cross axis is horizontal, where a baseline is
85/// not a line, so it lays out as `Start` there (as CSS does).
86#[inline]
87fn baseline_row(tree: &Tree, i: u32) -> bool {
88    let l = &tree.specs[i as usize].layout;
89    l.cross_align == Align::Baseline && l.dir == Dir::Row
90}
91
92/// Whether child `c` of a baseline row takes part in the alignment: a
93/// child whose height is `Grow` or `Percent` is sized against the line
94/// and fills it, so it sits at the line's top and its height does not
95/// count toward the line's (as in `line_extents`).
96#[inline]
97fn aligns_by_baseline(tree: &Tree, c: u32) -> bool {
98    !matches!(
99        child_sizing(tree, c, AxisSel::Height),
100        Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_)
101    )
102}
103
104/// The first baseline of node `i`, logical px below its top edge, or
105/// `None` when nothing inside it is text. A text node's
106/// and an editor's are measured (`TextMeasure::baseline`, stored by
107/// `fit_heights` on a frame that has a baseline row); a container's is
108/// its first in-flow child's, carried down through where that child
109/// sits in it — which is why this reads sizes only, and can answer in
110/// the fit pass as well as in `positions`. Scrolling is ignored: a
111/// scrolled list's baseline is its unscrolled first row's.
112fn first_baseline(tree: &Tree, i: u32) -> Option<f32> {
113    match tree.content[i as usize] {
114        NodeContent::Text(_) | NodeContent::Edit(_) => tree
115            .baseline
116            .get(i as usize)
117            .copied()
118            .filter(|b| b.is_finite()),
119        NodeContent::Container => {
120            let f = first_in_flow(tree, i);
121            if f == NIL {
122                return None;
123            }
124            let spec = &tree.specs[i as usize].layout;
125            let size = tree.size[i as usize];
126            let fs = tree.size[f as usize];
127            match spec.dir {
128                Dir::Column => {
129                    let fb = first_baseline(tree, f)?;
130                    let content = (size.h - spec.padding.y()).max(0.0);
131                    let (mut used, mut n) = (0.0f32, 0u32);
132                    for c in tree.children(i) {
133                        if !is_float(tree, c) {
134                            used += tree.size[c as usize].h;
135                            n += 1;
136                        }
137                    }
138                    if n > 1 {
139                        used += spec.gap * (n - 1) as f32;
140                    }
141                    let (lead, _) = main_spread(spec.main_align, (content - used).max(0.0), n);
142                    Some(spec.padding.t + lead + fb)
143                }
144                Dir::Row => {
145                    let end = if wraps(tree, i) {
146                        line_end(tree, f)
147                    } else {
148                        NIL
149                    };
150                    if spec.cross_align == Align::Baseline {
151                        // The row's own shared baseline, when anything in
152                        // its first line has one.
153                        let (above, _, any) = line_baseline(tree, f, end);
154                        return any.then_some(spec.padding.t + above);
155                    }
156                    let fb = first_baseline(tree, f)?;
157                    let extent = if end == NIL {
158                        (size.h - spec.padding.y()).max(0.0)
159                    } else {
160                        line_extents(tree, f, end, spec.gap).1
161                    };
162                    let off = align_factor(spec.cross_align) * (extent - fs.h).max(0.0);
163                    Some(spec.padding.t + off + fb)
164                }
165            }
166        }
167        _ => None,
168    }
169}
170
171/// A baseline line `[c, end)`'s shared baseline: the most any taking
172/// part reaches above it, the most any hangs below it, and whether any
173/// child had a baseline of its own. A child with none aligns its bottom
174/// edge (CSS's synthesized baseline), so all of it is above.
175fn line_baseline(tree: &Tree, c: u32, end: u32) -> (f32, f32, bool) {
176    let (mut above, mut below, mut any) = (0.0f32, 0.0f32, false);
177    let mut k = c;
178    while k != end && k != NIL {
179        if !is_float(tree, k) && aligns_by_baseline(tree, k) {
180            let h = tree.size[k as usize].h;
181            let b = match first_baseline(tree, k) {
182                Some(b) => {
183                    any = true;
184                    b
185                }
186                None => h,
187            };
188            above = above.max(b);
189            below = below.max(h - b);
190        }
191        k = tree.next_sibling[k as usize];
192    }
193    (above, below, any)
194}
195
196/// One axis of float attachment: anchor point minus self point, plus offset.
197fn attach(
198    anchor_pos: f32,
199    anchor_len: f32,
200    self_len: f32,
201    anchor_pt: Align,
202    self_pt: Align,
203    off: f32,
204) -> f32 {
205    anchor_pos + align_factor(anchor_pt) * anchor_len - align_factor(self_pt) * self_len + off
206}
207
208/// How far `[pos, pos+len]` sticks out of `[0, limit]`.
209fn overflow(pos: f32, len: f32, limit: f32) -> f32 {
210    (-pos).max(0.0) + (pos + len - limit).max(0.0)
211}
212
213/// Whether `i` breaks its children into wrap lines.
214///
215/// Rows only, and never on a main axis that scrolls. Breaking needs a
216/// definite main size to break against, and the pass order hands a row one
217/// — its width is final in pass 2, before its height is measured in pass 3,
218/// so a line's cross extent is known by the time anything needs it. A
219/// column is the mirror image and does not work: its main size is not final
220/// until pass 4, two passes after the cross-axis fit that would have to sum
221/// the lines. `scroll_x` says the same thing a different way — an axis that
222/// scrolls is unbounded, and an unbounded axis has nothing to break
223/// against. `diag::WRAP_IGNORED` reports both. Always inlined: asked of
224/// every node in three passes, as a call it cost the 10k grid 2% (C48).
225#[inline(always)]
226fn wraps(tree: &Tree, i: u32) -> bool {
227    if !tree.any_wrap {
228        return false;
229    }
230    let s = &tree.specs[i as usize].layout;
231    s.wrap && s.dir == Dir::Row && !s.scroll_x && !is_table_row(tree, i)
232}
233
234/// Whether `i` is a row of a table: an in-flow `Row` container child of
235/// a table. Anything else straight under the table — a heading text
236/// beside the rows, a `column` section wrapping a heading over a row, a
237/// nested table — has no cells and keeps its own width, and its children
238/// are its own. Gated on the tree-level flag first, as
239/// [`is_float`] is. The rules (`emit_rules`) ask it too, so a table's
240/// grid is drawn over the rows its columns were laid across.
241#[inline]
242pub(crate) fn is_table_row(tree: &Tree, i: u32) -> bool {
243    if !tree.any_table {
244        return false;
245    }
246    let p = tree.parent[i as usize];
247    p != NIL
248        && tree.specs[p as usize].layout.is_table()
249        && row_shaped(
250            &tree.specs[i as usize].layout,
251            matches!(tree.content[i as usize], NodeContent::Container),
252        )
253}
254
255/// The half of [`is_table_row`] a node answers alone: an in-flow `Row`
256/// container (`container` false for a text, an image, any leaf). A
257/// departing table's ghost has no tree to ask, and asks this of the
258/// children it copied.
259#[inline]
260pub(crate) fn row_shaped(spec: &crate::spec::LayoutSpec, container: bool) -> bool {
261    spec.dir == Dir::Row && container && spec.float.is_none()
262}
263
264/// Whether `i` is a cell of a table: an in-flow child of a table row.
265#[inline]
266fn is_table_cell(tree: &Tree, i: u32) -> bool {
267    if !tree.any_table {
268        return false;
269    }
270    let p = tree.parent[i as usize];
271    p != NIL && is_table_row(tree, p) && !is_float(tree, i)
272}
273
274/// One column of a table, as its cells declared it.
275#[derive(Clone, Copy, Debug, Default)]
276struct Col {
277    /// The widest cell's fitted width: a `Fixed` cell's px, a `Fit`
278    /// cell's content, a `Grow` or `Percent` cell's floor.
279    fit: f32,
280    /// The largest `Grow` factor among the cells, 0 for none: the column
281    /// grows with the table when any cell asked to.
282    grow: f32,
283    /// The largest `Percent` among the cells, 0 for none; read only when
284    /// nothing grows.
285    pct: f32,
286    /// The largest size expression among the cells, in px
287    /// of the room the columns are laid across: the column is the larger
288    /// of it and `pct` of that room. Each cell's is resolved and the
289    /// largest kept, as `pct` keeps the largest percentage — two
290    /// expressions cannot be compared until they are numbers, and the
291    /// first row's once stood for the column. In the fit pass, before
292    /// there is a room, only whether there is one (`Some(0.0)`).
293    calc: Option<f32>,
294    /// Whether any cell is `Fixed`: a fixed column is never shrunk.
295    fixed: bool,
296    /// The strictest clamps its cells declared: the largest floor and
297    /// the smallest ceiling.
298    min: f32,
299    max: f32,
300    /// The width resolved for it.
301    w: f32,
302}
303
304impl Col {
305    fn clamp(&self, w: f32) -> f32 {
306        w.clamp(self.min, self.max.max(self.min))
307    }
308}
309
310/// The columns of table `i`, read off its cells' current widths and
311/// specs: the nth in-flow child of each in-flow row is a cell of column
312/// n, and a row with fewer cells fills the first columns. A text cell
313/// has no spec sizing and reads as `Fit`. `room` is what a calc cell
314/// resolves against — `None` in the fit pass, which reads no calc.
315fn table_columns(tree: &Tree, i: u32, room: Option<f32>) -> Vec<Col> {
316    let mut cols: Vec<Col> = Vec::new();
317    for row in tree.children(i) {
318        if !is_table_row(tree, row) {
319            continue;
320        }
321        let mut j = 0usize;
322        for cell in tree.children(row) {
323            if is_float(tree, cell) {
324                continue;
325            }
326            if j == cols.len() {
327                cols.push(Col {
328                    max: f32::INFINITY,
329                    ..Col::default()
330                });
331            }
332            let col = &mut cols[j];
333            let spec = tree.specs[cell as usize].layout;
334            col.fit = col.fit.max(tree.size[cell as usize].w);
335            match child_sizing(tree, cell, AxisSel::Width) {
336                Sizing::Grow(f) => col.grow = col.grow.max(f.max(0.0)),
337                Sizing::Percent(p) => col.pct = col.pct.max(p),
338                Sizing::Calc(c) => {
339                    let px = room.map_or(0.0, |r| c.resolve(r));
340                    col.calc = Some(col.calc.map_or(px, |w| w.max(px)));
341                }
342                Sizing::Fixed(_) => col.fixed = true,
343                Sizing::Fit => {}
344            }
345            if !matches!(tree.content[cell as usize], NodeContent::Text(_)) {
346                col.min = col.min.max(spec.min_w.resolved());
347                col.max = col.max.min(spec.max_w_px());
348            }
349            j += 1;
350        }
351    }
352    cols
353}
354
355/// The width row `row` needs for `cols` — its cells at the columns'
356/// widths, the gaps between them and its padding — which is what a
357/// `Fit` row of a table is.
358fn table_row_fit(tree: &Tree, row: u32, cols: &[Col]) -> f32 {
359    let spec = tree.specs[row as usize].layout;
360    let mut w = 0.0f32;
361    let mut n = 0u32;
362    for cell in tree.children(row) {
363        if is_float(tree, cell) {
364            continue;
365        }
366        w += cols.get(n as usize).map_or(0.0, |c| c.w);
367        n += 1;
368    }
369    if n > 1 {
370        w += spec.gap * (n - 1) as f32;
371    }
372    w + spec.padding.x()
373}
374
375/// Writes each column's width into its cells and sizes the rows to them.
376/// A text cell takes the width too — `fit_heights` keeps it.
377///
378/// Which rows: in pass 1 (`fitting`) every row but a `Fixed` one — the
379/// `grow` and percent rows included, whose own pass-1 width is 0 — so
380/// the table's fit width, read next, is its columns' and a `Fit` table
381/// of `grow` rows is the aligned list and not nothing. Pass 2 sizes them
382/// for good, and the fit is only the number the table reads.
383/// In pass 2 the `Fit` rows, and — when the table scrolls x — every row
384/// widened to its columns if they overflow it, since a `grow` row is the
385/// table's own width and `positions` measures a scroll container's
386/// content from its children's boxes: without this the overflow the
387/// table kept was clipped and `scroll_max.x` was 0. A
388/// table that does not scroll leaves its rows' boxes alone, as any row
389/// is left when fixed children overflow it.
390fn table_apply(tree: &mut Tree, i: u32, cols: &[Col], fitting: bool) {
391    let scrolls = tree.specs[i as usize].layout.scroll_x;
392    let mut row = tree.first_child[i as usize];
393    while row != NIL {
394        if is_table_row(tree, row) {
395            let mut j = 0usize;
396            let mut cell = tree.first_child[row as usize];
397            while cell != NIL {
398                if !is_float(tree, cell) {
399                    tree.size[cell as usize].w = cols[j].w;
400                    j += 1;
401                }
402                cell = tree.next_sibling[cell as usize];
403            }
404            let spec = tree.specs[row as usize].layout;
405            match spec.width {
406                Sizing::Fit => {
407                    let fit = table_row_fit(tree, row, cols);
408                    tree.size[row as usize].w = spec.clamp_w(fit);
409                }
410                Sizing::Fixed(_) => {}
411                Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) if fitting => {
412                    let fit = table_row_fit(tree, row, cols);
413                    tree.size[row as usize].w = spec.clamp_w(fit);
414                }
415                Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => {}
416            }
417            if scrolls && !fitting {
418                let fit = table_row_fit(tree, row, cols);
419                let w = &mut tree.size[row as usize].w;
420                *w = w.max(fit);
421            }
422        }
423        row = tree.next_sibling[row as usize];
424    }
425}
426
427/// Pass 1's table step: every column at its fit — the widest cell — and
428/// the rows fitted to that, so the table's own fit width (read next, by
429/// the caller) is the aligned one. A growing column sits at its floor
430/// here, as a grow child of any row does.
431fn table_fit(tree: &mut Tree, i: u32) {
432    let mut cols = table_columns(tree, i, None);
433    for col in &mut cols {
434        col.w = col.clamp(col.fit);
435    }
436    table_apply(tree, i, &cols, true);
437}
438
439/// Pass 2's table step, at the table node, before its rows: the columns
440/// resolved once against the widest row's content — `Percent` columns
441/// take their cut, `Fixed` and `Fit` ones sit at their fit, `Grow`
442/// columns split what is left in the freeze loop `distribute_run` runs
443/// (a clamped column is frozen and the rest re-share) — and, when the
444/// fits alone overflow the row and the table does not scroll x, the
445/// `Fit` columns compressed toward their floors largest first, as
446/// `shrink_axis` compresses a row's children. Then written into every
447/// cell, so the rows have nothing left to distribute.
448fn table_resolve(tree: &mut Tree, i: u32) {
449    // How many columns: the most cells any row has, as `table_columns`
450    // counts them — known before the columns are read, since they read
451    // the clamps resolved just below.
452    let n = tree
453        .children(i)
454        .filter(|&row| is_table_row(tree, row))
455        .map(|row| tree.children(row).filter(|&c| !is_float(tree, c)).count())
456        .max()
457        .unwrap_or(0);
458    if n == 0 {
459        return;
460    }
461    // The widest row's content: what the columns are laid across. Rows
462    // are usually `grow`, and then this is the table's content box less
463    // the row's own padding and gaps.
464    let mut avail = 0.0f32;
465    for row in tree.children(i) {
466        if !is_table_row(tree, row) {
467            continue;
468        }
469        let spec = tree.specs[row as usize].layout;
470        let chrome = spec.padding.x() + spec.gap * (n as f32 - 1.0);
471        avail = avail.max(tree.size[row as usize].w - chrome);
472    }
473    let avail = avail.max(0.0);
474    // A cell's size-expression clamps, against the room its column's
475    // width takes its cut of: a `Percent` or calc width resolves against
476    // `avail` below, and in a plain row a child's width and its clamps
477    // share one room, so a cell's do too — `maxWidth "50%"` and `width
478    // "50%"` beside it are one number. They once took the row's content
479    // with the gaps in, and the clamp came out wider than the width. The
480    // rows are wide by now, and the columns read the clamps next. A float
481    // in a row is no cell: its clamps are its anchor's, in `distribute_axis`.
482    if tree.any_calc_bound {
483        for row in tree.children(i).collect::<Vec<_>>() {
484            if !is_table_row(tree, row) {
485                continue;
486            }
487            for cell in tree.children(row).collect::<Vec<_>>() {
488                if !is_float(tree, cell) {
489                    resolve_bounds(tree, cell, AxisSel::Width, avail);
490                }
491            }
492        }
493    }
494    let mut cols = table_columns(tree, i, Some(avail));
495    debug_assert_eq!(cols.len(), n);
496    let mut used = 0.0f32;
497    let mut grow_total = 0.0f32;
498    for col in &mut cols {
499        if col.grow > 0.0 {
500            grow_total += col.grow;
501            col.w = col.clamp(0.0);
502        } else if col.pct > 0.0 || col.calc.is_some() {
503            let calc = col.calc.unwrap_or(0.0);
504            col.w = col.clamp((avail * col.pct).max(calc));
505            used += col.w;
506        } else {
507            col.w = col.clamp(col.fit);
508            used += col.w;
509        }
510    }
511    if grow_total > 0.0 {
512        let mut frozen = vec![false; cols.len()];
513        loop {
514            let remain = (avail - used).max(0.0);
515            let mut froze = false;
516            for (j, col) in cols.iter_mut().enumerate() {
517                if col.grow <= 0.0 || frozen[j] {
518                    continue;
519                }
520                let share = remain * col.grow / grow_total;
521                col.w = col.clamp(share);
522                if (col.w - share).abs() > 0.01 {
523                    frozen[j] = true;
524                    used += col.w;
525                    grow_total -= col.grow;
526                    froze = true;
527                }
528            }
529            if !froze || grow_total <= 0.0 {
530                break;
531            }
532        }
533    }
534    // The shrink: the fit columns pay the overflow, largest first, down
535    // to their floors — never a fixed, a percent or a growing one.
536    let total: f32 = cols.iter().map(|c| c.w).sum();
537    let mut deficit = total - avail;
538    if deficit > 0.5 && !tree.specs[i as usize].layout.scroll_x {
539        let shrinkable = |c: &Col| !c.fixed && c.grow <= 0.0 && c.pct <= 0.0 && c.calc.is_none();
540        let mut guard = 0;
541        while deficit > 0.5 && guard < 128 {
542            guard += 1;
543            let mut largest = f32::NEG_INFINITY;
544            let mut second = 0.0f32;
545            let mut count = 0u32;
546            for c in cols.iter().filter(|c| shrinkable(c) && c.w > c.min + 0.01) {
547                if c.w > largest + 0.01 {
548                    second = if largest.is_finite() {
549                        largest.max(second)
550                    } else {
551                        second
552                    };
553                    largest = c.w;
554                    count = 1;
555                } else if c.w > largest - 0.01 {
556                    count += 1;
557                } else if c.w > second {
558                    second = c.w;
559                }
560            }
561            if count == 0 {
562                break;
563            }
564            let target = (largest - deficit / count as f32).max(second).max(0.0);
565            let mut shrunk_any = false;
566            for c in cols.iter_mut().filter(|c| shrinkable(c)) {
567                if c.w > largest - 0.01 {
568                    let new = target.max(c.min);
569                    if new < c.w {
570                        deficit -= c.w - new;
571                        c.w = new;
572                        shrunk_any = true;
573                    }
574                }
575            }
576            if !shrunk_any {
577                break;
578            }
579        }
580    }
581    table_apply(tree, i, &cols, false);
582}
583
584/// The first in-flow child of `i` (`NIL` when it has none).
585fn first_in_flow(tree: &Tree, i: u32) -> u32 {
586    let mut c = tree.first_child[i as usize];
587    while c != NIL && is_float(tree, c) {
588        c = tree.next_sibling[c as usize];
589    }
590    c
591}
592
593/// The first in-flow child after `c`'s wrap line (`NIL` at the end). Line
594/// numbers only ever go up in child order, so one line's in-flow children
595/// are a contiguous sibling run and `[c, line_end(c))` is the whole line.
596fn line_end(tree: &Tree, c: u32) -> u32 {
597    let l = tree.line[c as usize];
598    let mut n = tree.next_sibling[c as usize];
599    while n != NIL && (is_float(tree, n) || tree.line[n as usize] == l) {
600        n = tree.next_sibling[n as usize];
601    }
602    n
603}
604
605/// Extents of the line `[c, end)`, in one walk: `(main, cross)`.
606///
607/// Main is its children plus the gaps between them. Cross is its tallest
608/// child that has a cross size of its own — Grow and Percent children are
609/// skipped by *sizing*, not by their current number, because they are
610/// sized against the extent this returns and reading them back would make
611/// a line's height depend on whether pass 4 had run yet. Skipping them
612/// measures the same thing in every pass, and matches an unwrapped row,
613/// where a grow child contributes nothing to a fit height either.
614/// Wrapping is rows-only, so main is width and cross is height.
615fn line_extents(tree: &Tree, c: u32, end: u32, gap: f32) -> (f32, f32) {
616    let baseline = tree.any_baseline && c != NIL && baseline_row(tree, tree.parent[c as usize]);
617    let mut main = 0.0f32;
618    let mut cross = 0.0f32;
619    let mut n = 0u32;
620    let mut k = c;
621    while k != end && k != NIL {
622        if !is_float(tree, k) {
623            let size = tree.size[k as usize];
624            main += size.w;
625            if !matches!(
626                child_sizing(tree, k, AxisSel::Height),
627                Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_)
628            ) {
629                cross = cross.max(size.h);
630            }
631            n += 1;
632        }
633        k = tree.next_sibling[k as usize];
634    }
635    if n > 1 {
636        main += gap * (n - 1) as f32;
637    }
638    if baseline {
639        // Aligned on one line, the children reach from the highest top
640        // to the lowest bottom, which is more than the tallest of them.
641        let (above, below, _) = line_baseline(tree, c, end);
642        cross = cross.max(above + below);
643    }
644    (main, cross)
645}
646
647/// A wrapping row's lines in one walk: how many, their stacked cross extent
648/// with the cross gaps between them, and the widest line's main extent.
649fn wrap_measure(tree: &Tree, i: u32) -> (u32, f32, f32) {
650    let spec = tree.specs[i as usize].layout;
651    let mut lines = 0u32;
652    let mut stacked = 0.0f32;
653    let mut widest = 0.0f32;
654    let mut c = first_in_flow(tree, i);
655    while c != NIL {
656        let end = line_end(tree, c);
657        let (main, cross) = line_extents(tree, c, end, spec.gap);
658        stacked += cross;
659        widest = widest.max(main);
660        lines += 1;
661        c = end;
662    }
663    if lines > 1 {
664        stacked += spec.cross_gap * (lines - 1) as f32;
665    }
666    (lines, stacked, widest)
667}
668
669/// Cross space every line gains beyond its content extent: the container's
670/// leftover, shared equally — CSS's `align-content: stretch`, and the
671/// reason a wrapping row that happens to fit on one line lays out exactly
672/// like an unwrapped one. Zero for a Fit height, whose lines already fill
673/// it by construction, and zero when the lines overflow.
674fn line_stretch(lines: u32, stacked: f32, cross_content: f32) -> f32 {
675    if lines == 0 {
676        0.0
677    } else {
678        (cross_content - stacked).max(0.0) / lines as f32
679    }
680}
681
682/// Greedy main-axis line breaking, in child order: a child that no longer
683/// fits the content box starts the next line, and a child too wide to fit
684/// on its own gets a line to itself (and is then the shrink pass's
685/// problem). Grow children break on whatever pass 1 left them — zero, or
686/// their `min_w` — since a grow child has no size of its own until a line
687/// is chosen for it; it then fills what is left of the line it landed on.
688fn break_lines(tree: &mut Tree, i: u32, content: f32, gap: f32) {
689    let mut line = 0u32;
690    let mut used = 0.0f32;
691    let mut n = 0u32;
692    let mut c = tree.first_child[i as usize];
693    while c != NIL {
694        if !is_float(tree, c) {
695            let base = tree.size[c as usize].w;
696            let needed = if n > 0 { gap + base } else { base };
697            if n > 0 && used + needed > content + 0.01 {
698                line += 1;
699                used = base;
700                n = 1;
701            } else {
702                used += needed;
703                n += 1;
704            }
705            tree.line[c as usize] = line;
706        }
707        c = tree.next_sibling[c as usize];
708    }
709}
710
711/// What the solver asks about text, editors, images and cell grids: the
712/// sizes it cannot compute from specs alone. The core implements it over
713/// its text system; a test may implement it with fixed numbers.
714pub trait TextMeasure {
715    /// Unwrapped preferred size.
716    fn intrinsic(&mut self, id: crate::tree::TextId) -> Size;
717    /// Size when wrapped to `max_w` logical pixels.
718    fn wrapped(&mut self, id: crate::tree::TextId, max_w: f32) -> Size;
719    /// The widest stretch of the text no break falls inside, logical px
720    /// — CSS's min-content: the longest word, a whole unwrapped line.
721    /// Asked only on a frame with a share of the room; 0, the default, is
722    /// a text that rewraps to any width.
723    fn min_content(&mut self, _id: crate::tree::TextId) -> f32 {
724        0.0
725    }
726    /// Unwrapped content size of an editable text node.
727    fn edit_intrinsic(&mut self, _key: crate::key::Key) -> Size {
728        Size::ZERO
729    }
730    /// Content size of an editable text node wrapped to `max_w`.
731    fn edit_wrapped(&mut self, _key: crate::key::Key, _max_w: f32) -> Size {
732        Size::ZERO
733    }
734    /// The first line's baseline of a text last wrapped by `wrapped`,
735    /// logical px below its top: what `crossAlign: baseline` lines up.
736    /// Asked only on a frame with a baseline row. `NaN` = not known, and
737    /// the text aligns by its bottom edge.
738    fn baseline(&mut self, _id: crate::tree::TextId) -> f32 {
739        f32::NAN
740    }
741    /// An editor's first baseline, below the top of its text (its box's
742    /// padding is added by the caller). `NaN` = not known.
743    fn edit_baseline(&mut self, _key: crate::key::Key) -> f32 {
744        f32::NAN
745    }
746    /// Pixel dimensions of a registered image (ZERO when unknown).
747    fn image_size(&mut self, _id: crate::resources::ImageId) -> Size {
748        Size::ZERO
749    }
750    /// The laid-out size of a cell grid (`rows × cols` cells).
751    fn cells_size(&mut self, _id: crate::cells::CellsId) -> Size {
752        Size::ZERO
753    }
754}
755
756/// Lays `tree` out into `viewport` logical px: sizes and positions every
757/// node, breaks wrapping rows into lines, resolves tables and floats, and
758/// clamps the scroll offsets in `scroll`. `Core::finish_frame` calls it
759/// once per frame; it is public for a custom runner or a test that drives
760/// the solver directly.
761pub fn compute(
762    tree: &mut Tree,
763    text: &mut dyn TextMeasure,
764    scroll: &mut ScrollStore,
765    viewport: Size,
766    // Physical pixels per logical one: `positions` snaps a scroll offset to them.
767    scale: f32,
768) {
769    if tree.is_empty() {
770        return;
771    }
772    // A `Min::FIT` floor is written back into the spec as the number it
773    // resolved to (`fit_widths`, `fit_heights`), and a size-expression
774    // clamp as the px it came to against its room (`resolve_bounds`);
775    // a node-anchored float is laid out again in the sixth pass, and
776    // would read what the first run left as declared numbers. For a fit
777    // floor that is one measured before the float had a width — a
778    // paragraph folded into a column of one word. For a calc clamp it is
779    // one resolved against a room of 0: the first run meets such a float
780    // before its anchor is placed and gives it no room, so a `maxWidth
781    // "50%"` on it — or a `maxWidth "100%"` on a child of it, resolved
782    // against the float's own 0 — came out 0 and the sixth pass, finding
783    // no expression left to resolve, kept it. Every clamp
784    // in those subtrees that layout writes over is remembered here, as
785    // declared, and put back before the re-run.
786    let declared = if tree.any_node_float {
787        declared_clamps(tree)
788    } else {
789        Vec::new()
790    };
791    fit_widths(tree, text, 0..tree.len());
792    grow_widths(tree, text, viewport);
793    fit_heights(tree, text, 0..tree.len());
794    grow_heights(tree, text, viewport);
795    positions(tree, scroll, viewport, scale, 0..tree.len());
796    if tree.any_node_float {
797        anchored(tree, text, scroll, viewport, scale, &declared);
798    }
799}
800
801/// The four clamps of a node as its spec declared them, for one layout
802/// writes over (a fit floor, a size expression): `(node, min_w, max_w,
803/// min_h, max_h)`.
804type Declared = (usize, Min, f32, Min, f32);
805
806/// The clamps of every node inside a node-anchored float's subtree that
807/// layout resolves in place — the ones the sixth pass must see again as
808/// declared. Read off the raw numbers, a negative
809/// being what both forms are, so no node pays a table lookup.
810fn declared_clamps(tree: &Tree) -> Vec<Declared> {
811    let mut out = Vec::new();
812    for (c, end, _) in node_floats(tree) {
813        for i in c..end {
814            let l = &tree.specs[i].layout;
815            if l.min_w.deferred() || l.min_h.deferred() || l.max_w < 0.0 || l.max_h < 0.0 {
816                out.push((i, l.min_w, l.max_w, l.min_h, l.max_h));
817            }
818        }
819    }
820    out
821}
822
823/// The sixth pass: every float anchored to a node by key, laid out again
824/// against that node's final rect. A subtree is a contiguous index range
825/// in preorder — every descendant's parent index is at or after the
826/// root's — so the five passes run over the range alone: the float's own
827/// size is resolved against the anchor the way a `Parent` float's is
828/// against its parent, and the rest is what the passes always do. An
829/// anchor the frame does not have leaves the float at zero size, which
830/// paints nothing and takes no input; a caller that built the content
831/// without its body has nothing to show it in.
832fn anchored(
833    tree: &mut Tree,
834    text: &mut dyn TextMeasure,
835    scroll: &mut ScrollStore,
836    viewport: Size,
837    scale: f32,
838    declared: &[Declared],
839) {
840    for (c, end, key) in node_floats(tree) {
841        let Some(a) = tree.index_of(key) else {
842            tree.size[c] = Size::default();
843            continue;
844        };
845        let anchor = Rect::from_pos_size(tree.pos[a], tree.size[a]);
846        // The fit floors and size-expression clamps declared in this
847        // subtree, as declared again.
848        for &(i, min_w, max_w, min_h, max_h) in
849            declared.iter().filter(|(i, ..)| (c..end).contains(i))
850        {
851            let l = &mut tree.specs[i].layout;
852            (l.min_w, l.max_w, l.min_h, l.max_h) = (min_w, max_w, min_h, max_h);
853        }
854        // The root's spec is read *after* each fit pass, which is where
855        // a `Min::FIT` floor of its own — just declared again above —
856        // resolves to its number; a copy taken before it clamped with a
857        // floor of 0 and the float lost its own floor.
858        fit_widths(tree, text, c..end);
859        if tree.any_calc_bound {
860            resolve_bounds(tree, c as u32, AxisSel::Width, anchor.w);
861        }
862        let spec = tree.specs[c].layout;
863        tree.size[c].w = spec.clamp_w(match spec.width {
864            Sizing::Grow(_) => anchor.w,
865            s => of_room(s, anchor.w).unwrap_or(tree.size[c].w),
866        });
867        for i in c..end {
868            distribute_axis(tree, text, i as u32, AxisSel::Width, viewport);
869        }
870        fit_heights(tree, text, c..end);
871        if tree.any_calc_bound {
872            resolve_bounds(tree, c as u32, AxisSel::Height, anchor.h);
873        }
874        let spec = tree.specs[c].layout;
875        tree.size[c].h = spec.clamp_h(match spec.height {
876            Sizing::Grow(_) => anchor.h,
877            s => of_room(s, anchor.h).unwrap_or(tree.size[c].h),
878        });
879        for i in c..end {
880            distribute_axis(tree, text, i as u32, AxisSel::Height, viewport);
881        }
882        place_anchored(tree, c, anchor, viewport);
883        positions(tree, scroll, viewport, scale, c..end);
884    }
885}
886
887/// `(root, end, anchor key)` of every node-anchored float, in tree
888/// order, each subtree the index range `root..end`.
889fn node_floats(tree: &Tree) -> Vec<(usize, usize, crate::key::Key)> {
890    let mut out = Vec::new();
891    let mut c = 0usize;
892    while c < tree.len() {
893        if let Some(crate::spec::FloatConfig {
894            anchor: FloatAnchor::Node(key),
895            ..
896        }) = tree.specs[c].layout.float
897        {
898            let mut end = c + 1;
899            while end < tree.len() && (tree.parent[end] as usize) >= c {
900                end += 1;
901            }
902            out.push((c, end, key));
903            c = end;
904        } else {
905            c += 1;
906        }
907    }
908    out
909}
910
911/// Attaches the sized float `c` to `anchor`, its config's points — kept
912/// on screen as a parent-anchored float is when it declares `fit`, which
913/// a leaf's tooltip anchored to the leaf does (backlog RG113).
914fn place_anchored(tree: &mut Tree, c: usize, anchor: Rect, viewport: Size) {
915    let Some(cfg) = tree.specs[c].layout.float else {
916        return;
917    };
918    let vp = float_viewport(tree, c as u32, viewport);
919    tree.pos[c] = attach_fitted(&cfg, anchor, tree.size[c], vp, true);
920}
921
922/// Pass 5 again, with the sizes kept: what a scroll that moved a node
923/// re-runs. The node-anchored floats follow their anchors.
924pub(crate) fn reposition(tree: &mut Tree, scroll: &mut ScrollStore, viewport: Size, scale: f32) {
925    positions(tree, scroll, viewport, scale, 0..tree.len());
926    if tree.any_node_float {
927        for (c, end, key) in node_floats(tree) {
928            if let Some(a) = tree.index_of(key) {
929                let anchor = Rect::from_pos_size(tree.pos[a], tree.size[a]);
930                place_anchored(tree, c, anchor, viewport);
931                positions(tree, scroll, viewport, scale, c..end);
932            }
933        }
934    }
935}
936
937/// The fit width of `i`, a non-text node: what its content wants on its
938/// own. Read for a `Fit` width, and for a `Min::FIT` floor under any other
939/// sizing — a `Grow` tab that must never be narrower than its label.
940#[inline(always)]
941fn fit_width(tree: &Tree, i: usize, text: &mut dyn TextMeasure) -> f32 {
942    let spec = &tree.specs[i].layout;
943    match tree.content[i] {
944        NodeContent::Edit(key) => text.edit_intrinsic(key).w + spec.padding.x(),
945        // Image pixels as logical px (1:1 at scale 1).
946        NodeContent::Image(id, _) => text.image_size(id).w,
947        NodeContent::Cells(id) => text.cells_size(id).w + spec.padding.x(),
948        _ => {
949            let mut w = 0.0f32;
950            let mut n = 0u32;
951            for c in tree.children(i as u32) {
952                if is_float(tree, c) {
953                    continue;
954                }
955                let cw = tree.size[c as usize].w;
956                if spec.dir == Dir::Row {
957                    w += cw;
958                } else {
959                    w = w.max(cw);
960                }
961                n += 1;
962            }
963            if spec.dir == Dir::Row && n > 1 {
964                w += spec.gap * (n - 1) as f32;
965            }
966            w + spec.padding.x()
967        }
968    }
969}
970
971fn fit_widths(tree: &mut Tree, text: &mut dyn TextMeasure, range: std::ops::Range<usize>) {
972    for i in range.rev() {
973        if let NodeContent::Text(tid) = tree.content[i] {
974            tree.size[i].w = text.intrinsic(tid).w;
975            continue;
976        }
977        // A table's rows and cells are fitted by now (children first):
978        // align them, so the fit read next is the aligned one.
979        if tree.any_table && tree.specs[i].layout.is_table() {
980            table_fit(tree, i as u32);
981        }
982        let width = tree.specs[i].layout.width;
983        let min_fit = tree.specs[i].layout.min_w.is_fit();
984        // Measured once for both uses: the Fit sizing, and the Fit floor.
985        let fit = if min_fit || width == Sizing::Fit {
986            fit_width(tree, i, text)
987        } else {
988            0.0
989        };
990        // A `Min::FIT` floor resolves here, once, to the number every later
991        // clamp on this axis reads — written back into the spec so
992        // `set_axis_clamped` in the grow pass and `break_lines` need no
993        // second form.
994        if min_fit {
995            tree.specs[i].layout.min_w = Min::px(fit);
996        }
997        let spec = &tree.specs[i].layout;
998        tree.size[i].w = spec.clamp_w(match width {
999            Sizing::Fixed(px) => px,
1000            // Resolved against the parent later; contributes nothing to fit
1001            // beyond its own floor.
1002            Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => 0.0,
1003            Sizing::Fit => spec.aspect_width().unwrap_or(fit),
1004        });
1005    }
1006}
1007
1008/// Node `i`'s min-content on `axis` — CSS's: the widest (or tallest)
1009/// thing in it that cannot wrap, its padding on top — asked by a shrink
1010/// that gives CSS's way, for the children of a run that overflowed and
1011/// no other node, so a frame whose runs all fit measures nothing.
1012/// Across, a text is its longest word
1013/// (`TextMeasure::min_content`); down, it is its lines at the width it
1014/// has, which the fit pass of that axis measured, as an image's and a
1015/// grid's size is theirs. A box scrolling that axis, or clipping, needs
1016/// nothing of what it holds — CSS's automatic minimum of a box whose
1017/// overflow is not visible. Asked too by the shrink of a column of fit
1018/// children, down, for its children alone.
1019fn min_content(tree: &Tree, text: &mut dyn TextMeasure, i: usize, axis: AxisSel) -> f32 {
1020    let spec = &tree.specs[i].layout;
1021    let (pad, scrolls) = match axis {
1022        AxisSel::Width => (spec.padding.x(), spec.scroll_x),
1023        AxisSel::Height => (spec.padding.y(), spec.scroll_y),
1024    };
1025    let own = match (tree.content[i], axis) {
1026        (NodeContent::Text(tid), AxisSel::Width) => return text.min_content(tid),
1027        (
1028            NodeContent::Text(_) | NodeContent::Image(..) | NodeContent::Cells(_),
1029            AxisSel::Height,
1030        ) => {
1031            return tree.size[i].h;
1032        }
1033        (NodeContent::Edit(_), AxisSel::Width) => 0.0,
1034        (NodeContent::Edit(key), AxisSel::Height) => {
1035            text.edit_wrapped(key, (tree.size[i].w - spec.padding.x()).max(0.0))
1036                .h
1037        }
1038        (NodeContent::Image(id, _), AxisSel::Width) => text.image_size(id).w,
1039        (NodeContent::Cells(id), AxisSel::Width) => text.cells_size(id).w,
1040        _ if scrolls || spec.clip => 0.0,
1041        _ => children_min_content(tree, text, i, axis),
1042    };
1043    own + pad
1044}
1045
1046/// What node `i`'s in-flow children need on `axis` at the least — CSS's
1047/// min-content of a box, less its padding: each child's
1048/// contribution is its fixed size or its own min-content, held to its
1049/// clamps, and the contributions add up with the gaps along a main axis
1050/// that does not wrap, where they cannot be put side by side any other
1051/// way, and are the widest across it or when each may take a line of its
1052/// own. Down a wrapping row, whose lines the width pass has broken, they
1053/// are each line's tallest stacked with the cross gaps, as the row's fit
1054/// height is.
1055fn children_min_content(tree: &Tree, text: &mut dyn TextMeasure, i: usize, axis: AxisSel) -> f32 {
1056    let spec = &tree.specs[i].layout;
1057    let main = (spec.dir == Dir::Row) == (axis == AxisSel::Width);
1058    let adds = main && !(spec.wrap && axis == AxisSel::Width);
1059    let lines = axis == AxisSel::Height && wraps(tree, i as u32);
1060    let (mut total, mut n) = (0.0f32, 0u32);
1061    // The line being read down a wrapping row, and its tallest so far.
1062    let (mut line, mut tallest, mut stacked) = (None, 0.0f32, 0u32);
1063    let mut c = tree.first_child[i];
1064    while c != NIL {
1065        if !is_float(tree, c) {
1066            let l = &tree.specs[c as usize].layout;
1067            let (sizing, min, max) = match axis {
1068                AxisSel::Width => (l.width, l.min_w, l.max_w_px()),
1069                AxisSel::Height => (l.height, l.min_h, l.max_h_px()),
1070            };
1071            let own = match sizing {
1072                Sizing::Fixed(px) => px,
1073                _ => min_content(tree, text, c as usize, axis),
1074            };
1075            let v = own.min(max).max(min.resolved());
1076            if lines {
1077                let l = tree.line[c as usize];
1078                if line != Some(l) {
1079                    total += tallest;
1080                    (line, tallest) = (Some(l), 0.0);
1081                    stacked += 1;
1082                }
1083                tallest = tallest.max(v);
1084            } else if adds {
1085                total += v;
1086            } else {
1087                total = total.max(v);
1088            }
1089            n += 1;
1090        }
1091        c = tree.next_sibling[c as usize];
1092    }
1093    if lines {
1094        total += tallest + spec.cross_gap * stacked.saturating_sub(1) as f32;
1095    } else if adds && n > 1 {
1096        total += spec.gap * (n - 1) as f32;
1097    }
1098    total
1099}
1100
1101/// The fit height of `i`, a non-text node, against its final width:
1102/// `fit_width`'s mirror, read for a `Fit` height and for a `Min::FIT`
1103/// floor. An editor's is its wrapped extent, which the caller has already
1104/// measured (emission and input must share one line layout, whatever the
1105/// sizing) and passes in as `edit`.
1106#[inline(always)]
1107fn fit_height(tree: &Tree, i: usize, text: &mut dyn TextMeasure, edit: Size) -> f32 {
1108    let spec = &tree.specs[i].layout;
1109    match tree.content[i] {
1110        NodeContent::Edit(_) => edit.h + spec.padding.y(),
1111        NodeContent::Cells(id) => text.cells_size(id).h + spec.padding.y(),
1112        // Width is final by now: a Fit height preserves the aspect. A
1113        // cell's width is its column's, which the image did not ask for:
1114        // its height is its aspect at the width its own sizing gave it —
1115        // a 16 px icon in a 200 px column is a 200 x 16 box, not a 200 x
1116        // 200 one — and how the pixels meet the wider box
1117        // is the image's `fit` row.
1118        NodeContent::Image(id, _) => {
1119            let intrinsic = text.image_size(id);
1120            if intrinsic.w <= 0.0 {
1121                return 0.0;
1122            }
1123            let w = if is_table_cell(tree, i as u32) {
1124                match spec.width {
1125                    Sizing::Fixed(px) => spec.clamp_w(px),
1126                    Sizing::Fit => spec.clamp_w(intrinsic.w),
1127                    Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => tree.size[i].w,
1128                }
1129            } else {
1130                tree.size[i].w
1131            };
1132            intrinsic.h * w / intrinsic.w
1133        }
1134        // A wrapping row is as tall as its lines stacked: the lines were
1135        // chosen in pass 2, against a width that is already final.
1136        _ if wraps(tree, i as u32) => wrap_measure(tree, i as u32).1 + spec.padding.y(),
1137        // One line, as tall as its children reach once their baselines
1138        // line up: `line_extents` measures exactly that.
1139        _ if tree.any_baseline && baseline_row(tree, i as u32) => {
1140            let f = first_in_flow(tree, i as u32);
1141            line_extents(tree, f, NIL, spec.gap).1 + spec.padding.y()
1142        }
1143        _ => {
1144            let mut h = 0.0f32;
1145            let mut n = 0u32;
1146            for c in tree.children(i as u32) {
1147                if is_float(tree, c) {
1148                    continue;
1149                }
1150                let ch = tree.size[c as usize].h;
1151                if spec.dir == Dir::Column {
1152                    h += ch;
1153                } else {
1154                    h = h.max(ch);
1155                }
1156                n += 1;
1157            }
1158            if spec.dir == Dir::Column && n > 1 {
1159                h += spec.gap * (n - 1) as f32;
1160            }
1161            h + spec.padding.y()
1162        }
1163    }
1164}
1165
1166fn fit_heights(tree: &mut Tree, text: &mut dyn TextMeasure, range: std::ops::Range<usize>) {
1167    if tree.any_baseline {
1168        // One per node, `NaN` where no text measured one; kept only on a
1169        // frame that has a baseline row.
1170        let n = tree.len();
1171        tree.baseline.resize(n, f32::NAN);
1172    }
1173    for i in range.rev() {
1174        if let NodeContent::Text(tid) = tree.content[i] {
1175            // Width is final by now: wrap to it.
1176            let wrapped = text.wrapped(tid, tree.size[i].w.max(0.0));
1177            // The wrapped measurement is authoritative for both axes (a long
1178            // unbroken word may still exceed the clamp; report it truthfully)
1179            // — except that a text which is a table's cell keeps the column
1180            // width pass 2 gave it, or the cells after it would close up.
1181            if is_table_cell(tree, i as u32) {
1182                tree.size[i].h = wrapped.h;
1183                tree.size[i].w = tree.size[i].w.max(wrapped.w);
1184            } else {
1185                tree.size[i] = wrapped;
1186            }
1187            if tree.any_baseline {
1188                tree.baseline[i] = text.baseline(tid);
1189            }
1190            continue;
1191        }
1192        // Always wrap an editor to the final content width so emission and
1193        // input hit the same line layout, whatever the height sizing is.
1194        let edit = if let NodeContent::Edit(key) = tree.content[i] {
1195            let inner = (tree.size[i].w - tree.specs[i].layout.padding.x()).max(0.0);
1196            let wrapped = text.edit_wrapped(key, inner);
1197            if tree.any_baseline {
1198                tree.baseline[i] = tree.specs[i].layout.padding.t + text.edit_baseline(key);
1199            }
1200            wrapped
1201        } else {
1202            Size::default()
1203        };
1204        let height = tree.specs[i].layout.height;
1205        let min_fit = tree.specs[i].layout.min_h.is_fit();
1206        // Same resolution as `fit_widths`: measured once, the floor written
1207        // back as a number.
1208        let fit = if min_fit || height == Sizing::Fit {
1209            fit_height(tree, i, text, edit)
1210        } else {
1211            0.0
1212        };
1213        if min_fit {
1214            tree.specs[i].layout.min_h = Min::px(fit);
1215        }
1216        let spec = &tree.specs[i].layout;
1217        tree.size[i].h = spec.clamp_h(match height {
1218            Sizing::Fixed(px) => px,
1219            Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => 0.0,
1220            // Width is final by now: a declared ratio reads it, as an
1221            // image's pixels do below it.
1222            Sizing::Fit if spec.aspect_height() => tree.size[i].w / spec.aspect,
1223            Sizing::Fit => fit,
1224        });
1225    }
1226}
1227
1228fn grow_widths(tree: &mut Tree, text: &mut dyn TextMeasure, viewport: Size) {
1229    for i in 0..tree.len() {
1230        if tree.parent[i] == NIL {
1231            if tree.any_calc_bound {
1232                resolve_bounds(tree, i as u32, AxisSel::Width, viewport.w);
1233            }
1234            let spec = tree.specs[i].layout;
1235            tree.size[i].w = spec.clamp_w(resolve_root(spec.width, tree.size[i].w, viewport.w));
1236        }
1237        distribute_axis(tree, text, i as u32, AxisSel::Width, viewport);
1238    }
1239}
1240
1241fn grow_heights(tree: &mut Tree, text: &mut dyn TextMeasure, viewport: Size) {
1242    for i in 0..tree.len() {
1243        if tree.parent[i] == NIL {
1244            if tree.any_calc_bound {
1245                resolve_bounds(tree, i as u32, AxisSel::Height, viewport.h);
1246            }
1247            let spec = tree.specs[i].layout;
1248            tree.size[i].h = spec.clamp_h(resolve_root(spec.height, tree.size[i].h, viewport.h));
1249        }
1250        distribute_axis(tree, text, i as u32, AxisSel::Height, viewport);
1251    }
1252}
1253
1254fn resolve_root(sizing: Sizing, fitted: f32, viewport: f32) -> f32 {
1255    match sizing {
1256        Sizing::Grow(_) => viewport,
1257        Sizing::Percent(p) => viewport * p,
1258        Sizing::Calc(c) => c.resolve(viewport),
1259        Sizing::Fixed(px) => px,
1260        Sizing::Fit => fitted,
1261    }
1262}
1263
1264#[derive(Clone, Copy, PartialEq)]
1265enum AxisSel {
1266    Width,
1267    Height,
1268}
1269
1270/// Resolves Grow/Percent children of `i` along the given axis, assuming `i`'s
1271/// own size on that axis is final.
1272fn distribute_axis(
1273    tree: &mut Tree,
1274    text: &mut dyn TextMeasure,
1275    i: u32,
1276    axis: AxisSel,
1277    viewport: Size,
1278) {
1279    let spec = tree.specs[i as usize].layout;
1280    let (own, pad) = match axis {
1281        AxisSel::Width => (tree.size[i as usize].w, spec.padding.x()),
1282        AxisSel::Height => (tree.size[i as usize].h, spec.padding.y()),
1283    };
1284    let content = (own - pad).max(0.0);
1285    let is_main = (spec.dir == Dir::Row) == (axis == AxisSel::Width);
1286
1287    // Size-expression clamps against the content box, the
1288    // room a percentage takes its cut of, before anything below is sized
1289    // by them; a child whose size is its own — fixed, fit — is clamped
1290    // again here, since its fit pass ran with no such clamp. A float's
1291    // room is its anchor, below.
1292    if tree.any_calc_bound {
1293        let mut c = tree.first_child[i as usize];
1294        while c != NIL {
1295            if !is_float(tree, c) && resolve_bounds(tree, c, axis, content) {
1296                let now = get_axis(tree, c, axis);
1297                set_axis_clamped(tree, c, axis, now);
1298            }
1299            c = tree.next_sibling[c as usize];
1300        }
1301    }
1302
1303    if is_main && axis == AxisSel::Width && is_table_row(tree, i) {
1304        // The cells were sized by the table (`table_resolve`), the same
1305        // in every row: nothing to grow, cut or shrink here.
1306    } else if is_main {
1307        // Percent takes its cut of the content box first: a wrap line
1308        // breaks on sizes that are already resolved against the container,
1309        // not against the line it is about to land on.
1310        // Noting on the way whether any child is such a share, the one
1311        // thing the shrink below needs to know before it picks a rule
1312        // — here, where every child is read anyway: a scan
1313        // of its own cost a column of a thousand fixed rows 2%.
1314        let mut share = false;
1315        let mut c = tree.first_child[i as usize];
1316        while c != NIL {
1317            if !is_float(tree, c)
1318                && let Some(px) = of_room(child_sizing(tree, c, axis), content)
1319            {
1320                set_axis_clamped(tree, c, axis, px);
1321                share = true;
1322            }
1323            c = tree.next_sibling[c as usize];
1324        }
1325        let scrolls = match axis {
1326            AxisSel::Width => spec.scroll_x,
1327            AxisSel::Height => spec.scroll_y,
1328        };
1329        if wraps(tree, i) {
1330            break_lines(tree, i, content, spec.gap);
1331            let mut c = first_in_flow(tree, i);
1332            while c != NIL {
1333                let end = line_end(tree, c);
1334                let line = tree.line[c as usize];
1335                // Each line is its own main-axis box: Grow splits what is
1336                // left of *its* line.
1337                let total = distribute_run(tree, c, end, axis, content, spec.gap);
1338                // Wrapping and shrinking answer the same overflow, and
1339                // wrapping answers it first: greedy breaking never puts a
1340                // second child on a line that is already full, so the only
1341                // line that can still overflow is one holding a single
1342                // child too wide for the box. Nothing can be broken off
1343                // that, which is exactly when shrinking is the remaining
1344                // answer — applied to that line alone, so a wide chip
1345                // compresses without dragging its neighbours on other
1346                // lines down with it.
1347                let deficit = total - content;
1348                if deficit > 0.5 {
1349                    shrink_axis(tree, text, i, axis, deficit, Some(line), share);
1350                }
1351                c = end;
1352            }
1353        } else {
1354            // Fixed/Fit keep their size, Percent has taken its cut, Grow
1355            // splits the rest.
1356            let total = distribute_run(
1357                tree,
1358                tree.first_child[i as usize],
1359                NIL,
1360                axis,
1361                content,
1362                spec.gap,
1363            );
1364            let deficit = total - content;
1365            if deficit > 0.5 && !scrolls {
1366                shrink_axis(tree, text, i, axis, deficit, None, share);
1367            }
1368        }
1369    } else if wraps(tree, i) {
1370        // Cross axis of a wrapping row: a Grow child fills *its line*, not
1371        // the container. The lines share the container's leftover equally
1372        // (see `line_stretch`), so with one line this is the branch below
1373        // exactly, and a row that happens not to wrap keeps its old layout.
1374        let (lines, stacked, _) = wrap_measure(tree, i);
1375        let stretch = line_stretch(lines, stacked, content);
1376        let mut c = first_in_flow(tree, i);
1377        while c != NIL {
1378            let end = line_end(tree, c);
1379            let extent = line_extents(tree, c, end, spec.gap).1 + stretch;
1380            let mut k = c;
1381            while k != end && k != NIL {
1382                if !is_float(tree, k) {
1383                    match child_sizing(tree, k, axis) {
1384                        Sizing::Grow(_) => set_axis_clamped(tree, k, axis, extent),
1385                        s => {
1386                            if let Some(px) = of_room(s, extent) {
1387                                set_axis_clamped(tree, k, axis, px);
1388                            }
1389                        }
1390                    }
1391                }
1392                k = tree.next_sibling[k as usize];
1393            }
1394            c = end;
1395        }
1396    } else {
1397        // Cross axis: Grow/Percent resolve against the content box directly.
1398        // A Fit box across a column is its content's width but no wider
1399        // than the box — CSS's `fit-content` — down to its declared min,
1400        // none undeclared, as a row's fit children give.
1401        // Pass 1 summed it bottom-up with no room in sight; held here,
1402        // before its own children are distributed, its run gives and its
1403        // text wraps. Not across a column that scrolls x, whose overflow
1404        // is the point, nor a width a ratio derives from the height; a
1405        // text has its own clamp, below; and a grid or an image is its
1406        // content's size, CSS's replaced element: a held box would not
1407        // take a column off the grid the app laid out, and would stretch
1408        // an image under a fixed height.
1409        let fits = axis == AxisSel::Width && !spec.scroll_x;
1410        let mut c = tree.first_child[i as usize];
1411        while c != NIL {
1412            if !is_float(tree, c) {
1413                match child_sizing(tree, c, axis) {
1414                    Sizing::Grow(_) => set_axis_clamped(tree, c, axis, content),
1415                    Sizing::Fit
1416                        if fits
1417                            && tree.size[c as usize].w > content
1418                            && !matches!(
1419                                tree.content[c as usize],
1420                                NodeContent::Text(_)
1421                                    | NodeContent::Cells(_)
1422                                    | NodeContent::Image(..)
1423                            )
1424                            && tree.specs[c as usize].layout.aspect_width().is_none() =>
1425                    {
1426                        set_axis_clamped(tree, c, axis, content)
1427                    }
1428                    s => {
1429                        if let Some(px) = of_room(s, content) {
1430                            set_axis_clamped(tree, c, axis, px);
1431                        }
1432                    }
1433                }
1434            }
1435            c = tree.next_sibling[c as usize];
1436        }
1437        // A table's rows are wide by now: lay its columns across them.
1438        if axis == AxisSel::Width && tree.any_table && spec.is_table() {
1439            table_resolve(tree, i);
1440        }
1441    }
1442
1443    // Floating children size Grow/Percent against their anchor. A frame
1444    // with no floats skips the walk: it would read every child's spec to
1445    // find none.
1446    let mut c = if tree.any_float {
1447        tree.first_child[i as usize]
1448    } else {
1449        NIL
1450    };
1451    while c != NIL {
1452        if let Some(cfg) = tree.specs[c as usize].layout.float {
1453            let vp = float_viewport(tree, c, viewport);
1454            let anchor_dim = match (cfg.anchor, axis) {
1455                (FloatAnchor::Parent, AxisSel::Width) => tree.size[i as usize].w,
1456                (FloatAnchor::Parent, AxisSel::Height) => tree.size[i as usize].h,
1457                (FloatAnchor::Viewport, AxisSel::Width) => vp.w,
1458                (FloatAnchor::Viewport, AxisSel::Height) => vp.h,
1459                // Sized in the sixth pass, once the anchor is placed.
1460                (FloatAnchor::Node(_), _) => 0.0,
1461            };
1462            if tree.any_calc_bound && resolve_bounds(tree, c, axis, anchor_dim) {
1463                let now = get_axis(tree, c, axis);
1464                set_axis_clamped(tree, c, axis, now);
1465            }
1466            match child_sizing(tree, c, axis) {
1467                Sizing::Grow(_) => set_axis_clamped(tree, c, axis, anchor_dim),
1468                s => {
1469                    if let Some(px) = of_room(s, anchor_dim) {
1470                        set_axis_clamped(tree, c, axis, px);
1471                    }
1472                }
1473            }
1474        }
1475        c = tree.next_sibling[c as usize];
1476    }
1477
1478    // Text children have no spec sizing; clamp their width to the content box
1479    // so fit_heights wraps them. Same walk, same gate: no text, no clamp.
1480    if axis == AxisSel::Width && tree.any_text {
1481        let mut c = tree.first_child[i as usize];
1482        while c != NIL {
1483            if matches!(tree.content[c as usize], NodeContent::Text(_))
1484                && tree.size[c as usize].w > content
1485            {
1486                tree.size[c as usize].w = content;
1487            }
1488            c = tree.next_sibling[c as usize];
1489        }
1490    }
1491}
1492
1493/// Resolves the Grow children of one main-axis run — a whole child list
1494/// (`end == NIL`) or one wrap line — into `content`, and returns what the
1495/// run ends up occupying, gaps included.
1496fn distribute_run(
1497    tree: &mut Tree,
1498    start: u32,
1499    end: u32,
1500    axis: AxisSel,
1501    content: f32,
1502    gap: f32,
1503) -> f32 {
1504    let mut used = 0.0f32;
1505    let mut grow_total = 0.0f32;
1506    let mut n = 0u32;
1507    let mut c = start;
1508    while c != end && c != NIL {
1509        if !is_float(tree, c) {
1510            match child_sizing(tree, c, axis) {
1511                Sizing::Grow(f) => grow_total += f.max(0.0),
1512                _ => used += get_axis(tree, c, axis),
1513            }
1514            n += 1;
1515        }
1516        c = tree.next_sibling[c as usize];
1517    }
1518    if n > 1 {
1519        used += gap * (n - 1) as f32;
1520    }
1521    let mut total = used;
1522    if grow_total > 0.0 {
1523        // Flexbox's freeze loop (CSS Flexible Box §9.7, step 6). A grow
1524        // child's share is `remain` split by factor; one whose own min or
1525        // max holds it off that share is a violator, min or max by the
1526        // sign of the difference. A pass sums its violations and freezes
1527        // only the violators of the dominant sign — the min ones when the
1528        // sum is positive, the max ones when negative — each at its
1529        // clamp, its size moved into `used`; the rest, the other sign's
1530        // violators included, share what is left in the next pass, until
1531        // a pass's violations sum to nothing. A max on one child is room
1532        // for its siblings, not a hole at the end of the run (a devtools
1533        // inspector capped at 300 left the node list above it short of
1534        // the panel by the same 300); and a min on one is not the cue to
1535        // freeze a capped sibling at its cap while a plain one gets
1536        // nothing (RG5: 600 over A max 100, B min 500, C — 50 / 500 / 50,
1537        // not 100 / 500 / 0). At most one pass per grow child, since each
1538        // pass past the first froze one. The per-child state is a byte in
1539        // `grow_scratch`, indexed by the child's place in the run, so a
1540        // pass over n children costs n whichever way the earlier ones
1541        // went.
1542        const FROZEN: u8 = 1;
1543        const MIN_VIOLATOR: u8 = 2;
1544        const MAX_VIOLATOR: u8 = 4;
1545        let mut scratch = std::mem::take(&mut tree.grow_scratch);
1546        scratch.clear();
1547        scratch.resize(n as usize, 0);
1548        loop {
1549            let remain = (content - used).max(0.0);
1550            let mut violation = 0.0f32;
1551            let mut unfrozen = 0.0f32;
1552            let mut k = 0usize;
1553            let mut c = start;
1554            while c != end && c != NIL {
1555                if !is_float(tree, c) {
1556                    if scratch[k] & FROZEN == 0
1557                        && let Sizing::Grow(f) = child_sizing(tree, c, axis)
1558                    {
1559                        let share = remain * f.max(0.0) / grow_total;
1560                        set_axis_clamped(tree, c, axis, share);
1561                        let got = get_axis(tree, c, axis);
1562                        unfrozen += got;
1563                        let off = got - share;
1564                        scratch[k] = if off > 0.01 {
1565                            violation += off;
1566                            MIN_VIOLATOR
1567                        } else if off < -0.01 {
1568                            violation += off;
1569                            MAX_VIOLATOR
1570                        } else {
1571                            0
1572                        };
1573                    }
1574                    k += 1;
1575                }
1576                c = tree.next_sibling[c as usize];
1577            }
1578            if violation.abs() <= 0.01 {
1579                // Nothing to freeze: every unfrozen child is at its
1580                // share, or the clamps cancel and the run adds up.
1581                total = used + unfrozen;
1582                break;
1583            }
1584            let freeze = if violation > 0.0 {
1585                MIN_VIOLATOR
1586            } else {
1587                MAX_VIOLATOR
1588            };
1589            let mut k = 0usize;
1590            let mut c = start;
1591            while c != end && c != NIL {
1592                if !is_float(tree, c) {
1593                    if scratch[k] & freeze != 0
1594                        && let Sizing::Grow(f) = child_sizing(tree, c, axis)
1595                    {
1596                        scratch[k] = FROZEN;
1597                        used += get_axis(tree, c, axis);
1598                        grow_total -= f.max(0.0);
1599                    }
1600                    k += 1;
1601                }
1602                c = tree.next_sibling[c as usize];
1603            }
1604            if grow_total <= 0.0 {
1605                total = used;
1606                break;
1607            }
1608        }
1609        tree.grow_scratch = scratch;
1610    }
1611    total
1612}
1613
1614/// The shrink pass: pays off `deficit` (how far in-flow children overflow
1615/// the parent's main-axis content box) by compressing Fit-sized children
1616/// and the shares of the room — `Percent` and a size expression. A share
1617/// was cut from the content box before the gaps between the
1618/// children took theirs, so two `"50%"` children and a gap overflow until
1619/// this gives. Two rules, by what the run holds:
1620///
1621/// - With a share in it, CSS's ([`shrink_as_css`]): every
1622///   shrinkable child gives in proportion to its size, down to its floor —
1623///   its declared min, or where none was declared ([`Min::AUTO`]) its
1624///   min-content ([`min_content`], measured here and only here), none for
1625///   a child that scrolls that axis — `min-width: auto`.
1626/// - Fit children alone, clay's: largest first, so equal children end up
1627///   equal, each toward its declared min — 0 across when none was
1628///   declared, and down its min-content, none for a child
1629///   that scrolls or clips that axis: `min-height: auto`, so a row is
1630///   never squeezed below the text in it.
1631///
1632/// Fixed keeps its declared size; Grow never overflows. Text shrinks in
1633/// width (it rewraps at the new width in fit_heights) but never in height.
1634/// Scroll axes skip this entirely — overflow is the point of a scroll
1635/// container.
1636///
1637/// `only_line` restricts it to one wrap line. A wrapping container reaches
1638/// here only for a line it could not break any further (a single child
1639/// wider than the box), so the compression stays on that line instead of
1640/// squeezing children that are already comfortable on other ones.
1641fn shrink_axis(
1642    tree: &mut Tree,
1643    text: &mut dyn TextMeasure,
1644    i: u32,
1645    axis: AxisSel,
1646    mut deficit: f32,
1647    only_line: Option<u32>,
1648    share: bool,
1649) {
1650    let shrinkable = |tree: &Tree, c: u32| -> Option<f32> {
1651        if is_float(tree, c)
1652            || !matches!(
1653                child_sizing(tree, c, axis),
1654                Sizing::Fit | Sizing::Percent(_) | Sizing::Calc(_)
1655            )
1656        {
1657            return None;
1658        }
1659        if only_line.is_some_and(|l| tree.line[c as usize] != l) {
1660            return None;
1661        }
1662        // Squashing text/editors vertically would clip lines, and images
1663        // would distort; width shrink rewraps (and re-aspects) instead.
1664        if axis == AxisSel::Height
1665            && matches!(
1666                tree.content[c as usize],
1667                NodeContent::Text(_) | NodeContent::Edit(_) | NodeContent::Image(..)
1668            )
1669        {
1670            return None;
1671        }
1672        // Resolved to a number by the fit pass of this axis, which ran.
1673        let spec = tree.specs[c as usize].layout;
1674        // An axis a declared ratio set would come out of shrinking at some
1675        // other ratio; like an image, it keeps its size.
1676        let derived = match axis {
1677            AxisSel::Width => spec.aspect_width().is_some(),
1678            AxisSel::Height => spec.aspect_height(),
1679        };
1680        if derived {
1681            return None;
1682        }
1683        Some(match axis {
1684            AxisSel::Width => spec.min_w.resolved(),
1685            AxisSel::Height => spec.min_h.resolved(),
1686        })
1687    };
1688
1689    // A run holding a share of the room gives as CSS's flex items do,
1690    // every shrinkable child of it with the share; a run
1691    // of fit children alone keeps clay's rule below. `share` is what the
1692    // caller saw resolving the shares, so a run without one — a column
1693    // of a thousand fixed rows overflowing its box, the common one here —
1694    // walks its children once, not once more to look (6% of such a frame
1695    // when it did).
1696    if share {
1697        let mut c = tree.first_child[i as usize];
1698        let mut items = std::mem::take(&mut tree.shrink_scratch);
1699        items.clear();
1700        let mut share = false;
1701        while c != NIL {
1702            if let Some(declared) = shrinkable(tree, c) {
1703                share |= matches!(
1704                    child_sizing(tree, c, axis),
1705                    Sizing::Percent(_) | Sizing::Calc(_)
1706                );
1707                let l = &tree.specs[c as usize].layout;
1708                let (min, scrolls) = match axis {
1709                    AxisSel::Width => (l.min_w, l.scroll_x),
1710                    AxisSel::Height => (l.min_h, l.scroll_y),
1711                };
1712                let base = get_axis(tree, c, axis);
1713                // `min-width: auto`: the content's, where nothing was
1714                // declared — no more than the child's own size, and none
1715                // for a child that scrolls its overflow, as CSS has it.
1716                let floor = match () {
1717                    _ if !min.is_auto() => declared,
1718                    _ if scrolls => 0.0,
1719                    _ => min_content(tree, text, c as usize, axis),
1720                };
1721                items.push(Give {
1722                    c,
1723                    base,
1724                    floor: floor.min(base),
1725                    size: base,
1726                    held: false,
1727                });
1728            }
1729            c = tree.next_sibling[c as usize];
1730        }
1731        if share {
1732            shrink_as_css(tree, axis, deficit, &mut items);
1733        }
1734        tree.shrink_scratch = items;
1735        if share {
1736            return;
1737        }
1738    }
1739
1740    // Largest-first, like grow in reverse: pull the biggest children down
1741    // to the second-biggest, repeat until the deficit is paid or every
1742    // shrinkable child sits at its floor. Across, the floor is the
1743    // declared min (0 undeclared), so a row of labels squeezes them into
1744    // their ellipses. Down, an undeclared one is the child's min-content
1745    // — CSS's `min-height: auto` — since nothing in a box
1746    // gives vertically but a scroller or a clip: a row squeezed below
1747    // its text painted the text over the rows under it. One that scrolls
1748    // or clips that axis goes to 0.
1749    let mut items = std::mem::take(&mut tree.shrink_scratch);
1750    items.clear();
1751    let mut c = tree.first_child[i as usize];
1752    while c != NIL {
1753        if let Some(declared) = shrinkable(tree, c) {
1754            let base = get_axis(tree, c, axis);
1755            let l = &tree.specs[c as usize].layout;
1756            let floor = match axis {
1757                AxisSel::Height if l.min_h.is_auto() => {
1758                    if l.scroll_y || l.clip {
1759                        0.0
1760                    } else {
1761                        min_content(tree, text, c as usize, axis)
1762                    }
1763                }
1764                _ => declared,
1765            };
1766            items.push(Give {
1767                c,
1768                base,
1769                floor: floor.min(base),
1770                size: base,
1771                held: false,
1772            });
1773        }
1774        c = tree.next_sibling[c as usize];
1775    }
1776    let mut guard = 0;
1777    while deficit > 0.5 && guard < 128 {
1778        guard += 1;
1779        let mut largest = f32::NEG_INFINITY;
1780        let mut second = 0.0f32;
1781        let mut count = 0u32;
1782        for g in items.iter().filter(|g| g.size > g.floor + 0.01) {
1783            let s = g.size;
1784            if s > largest + 0.01 {
1785                second = if largest.is_finite() {
1786                    largest.max(second)
1787                } else {
1788                    second
1789                };
1790                largest = s;
1791                count = 1;
1792            } else if s > largest - 0.01 {
1793                count += 1;
1794            } else if s > second {
1795                second = s;
1796            }
1797        }
1798        if count == 0 {
1799            break;
1800        }
1801        let target = (largest - deficit / count as f32).max(second).max(0.0);
1802        let mut shrunk_any = false;
1803        for g in items.iter_mut().filter(|g| g.size > largest - 0.01) {
1804            let new = target.max(g.floor);
1805            if new < g.size {
1806                deficit -= g.size - new;
1807                g.size = new;
1808                shrunk_any = true;
1809            }
1810        }
1811        if !shrunk_any {
1812            break;
1813        }
1814    }
1815    for g in items.iter() {
1816        if g.size < g.base {
1817            set_axis(tree, g.c, axis, g.size);
1818        }
1819    }
1820    tree.shrink_scratch = items;
1821}
1822
1823/// CSS's shrink of flex items (css-flexbox §9.7, `flex-shrink: 1`),
1824/// over `items`: each gives in proportion to its
1825/// size — its scaled shrink factor is its base — and one that would go
1826/// under its floor is held there and the rest share what it could not
1827/// pay, until nothing is under its floor or every child is held. What is
1828/// left unpaid overflows, as it does in CSS.
1829#[inline(never)]
1830fn shrink_as_css(tree: &mut Tree, axis: AxisSel, deficit: f32, items: &mut [Give]) {
1831    // Each round holds at least one more child or ends, so `n + 1` rounds
1832    // settle it.
1833    for _ in 0..=items.len() {
1834        let (mut paid, mut weight) = (0.0f32, 0.0f32);
1835        for g in items.iter() {
1836            if g.held {
1837                paid += g.base - g.size;
1838            } else {
1839                weight += g.base;
1840            }
1841        }
1842        let left = deficit - paid;
1843        if left <= 0.0 || weight <= 0.0 {
1844            break;
1845        }
1846        let mut under = false;
1847        for g in items.iter_mut().filter(|g| !g.held) {
1848            let want = g.base - left * g.base / weight;
1849            if want < g.floor {
1850                (g.size, g.held, under) = (g.floor, true, true);
1851            } else {
1852                g.size = want;
1853            }
1854        }
1855        if !under {
1856            break;
1857        }
1858    }
1859    for g in items.iter() {
1860        if g.size < g.base {
1861            set_axis(tree, g.c, axis, g.size);
1862        }
1863    }
1864}
1865
1866/// One child of a run giving CSS's way ([`shrink_as_css`]): its size
1867/// before, its floor, the size it is given, and whether it is held there.
1868#[derive(Clone, Copy, Debug)]
1869pub(crate) struct Give {
1870    c: u32,
1871    base: f32,
1872    floor: f32,
1873    size: f32,
1874    held: bool,
1875}
1876
1877/// A sizing that takes its size from the room once the parent's is
1878/// known — a `Percent`, or a size expression — in px of
1879/// `room`; `None` for the others.
1880#[inline]
1881fn of_room(sizing: Sizing, room: f32) -> Option<f32> {
1882    match sizing {
1883        Sizing::Percent(p) => Some(room * p),
1884        Sizing::Calc(c) => Some(c.resolve(room)),
1885        _ => None,
1886    }
1887}
1888
1889/// Writes node `c`'s size-expression clamps on `axis` as px of `room`
1890/// into the spec's, so every later clamp — and every
1891/// reader of `min_w` / `max_w` — reads a number, as a `Min::FIT` floor
1892/// is written back once its fit pass ran. Before this a calc clamp is
1893/// none, as a percentage clamp is in CSS's intrinsic sizing. `true` when
1894/// the node had one to write.
1895fn resolve_bounds(tree: &mut Tree, c: u32, axis: AxisSel, room: f32) -> bool {
1896    let l = &mut tree.specs[c as usize].layout;
1897    let (min, max) = match axis {
1898        AxisSel::Width => (&mut l.min_w, &mut l.max_w),
1899        AxisSel::Height => (&mut l.min_h, &mut l.max_h),
1900    };
1901    let mut any = false;
1902    if let Some(k) = min.as_calc() {
1903        *min = Min::px(k.resolve(room));
1904        any = true;
1905    }
1906    if let Some(k) = crate::spec::max_calc(*max) {
1907        *max = k.resolve(room);
1908        any = true;
1909    }
1910    any
1911}
1912
1913fn child_sizing(tree: &Tree, c: u32, axis: AxisSel) -> Sizing {
1914    if matches!(tree.content[c as usize], NodeContent::Text(_)) {
1915        return Sizing::Fit;
1916    }
1917    match axis {
1918        AxisSel::Width => tree.specs[c as usize].layout.width,
1919        AxisSel::Height => tree.specs[c as usize].layout.height,
1920    }
1921}
1922
1923fn get_axis(tree: &Tree, c: u32, axis: AxisSel) -> f32 {
1924    match axis {
1925        AxisSel::Width => tree.size[c as usize].w,
1926        AxisSel::Height => tree.size[c as usize].h,
1927    }
1928}
1929
1930fn set_axis(tree: &mut Tree, c: u32, axis: AxisSel, v: f32) {
1931    match axis {
1932        AxisSel::Width => tree.size[c as usize].w = v,
1933        AxisSel::Height => tree.size[c as usize].h = v,
1934    }
1935}
1936
1937/// set_axis clamped by the child's own min/max on that axis. Inlined,
1938/// and borrowing the spec rather than copying it: a call per child that
1939/// copied the whole layout spec cost the 10k grid 2% (C48).
1940#[inline]
1941fn set_axis_clamped(tree: &mut Tree, c: u32, axis: AxisSel, v: f32) {
1942    let spec = &tree.specs[c as usize].layout;
1943    let v = match axis {
1944        AxisSel::Width => spec.clamp_w(v),
1945        AxisSel::Height => spec.clamp_h(v),
1946    };
1947    set_axis(tree, c, axis, v);
1948}
1949
1950/// The "viewport" a float of `c` means: the window for the devtools' own
1951/// nodes and for the core's menu (a transient the platform's own would
1952/// not confine either — and one the panel's select opens *in* the dock,
1953/// where the host area would push it into the app), and for everyone
1954/// else the host area — the window less the devtools' dock — when one
1955/// is set (`Tree::host_area`).
1956fn float_viewport(tree: &Tree, c: u32, viewport: Size) -> Rect {
1957    use crate::tree::OriginId;
1958    let window = Rect::new(0.0, 0.0, viewport.w, viewport.h);
1959    let origin = tree.origins[c as usize];
1960    if tree.host_area.w <= 0.0 || origin == OriginId::DEVTOOLS || origin == OriginId::MENU {
1961        window
1962    } else {
1963        tree.host_area
1964    }
1965}
1966
1967/// Places `c`, which is out of flow, against its anchor.
1968fn place_float(
1969    tree: &mut Tree,
1970    cfg: crate::spec::FloatConfig,
1971    c: u32,
1972    parent: Rect,
1973    viewport: Size,
1974) {
1975    let vp = float_viewport(tree, c, viewport);
1976    let anchor = match cfg.anchor {
1977        FloatAnchor::Parent => parent,
1978        FloatAnchor::Viewport => vp,
1979        // Placed in the sixth pass, once the anchor is.
1980        FloatAnchor::Node(_) => return,
1981    };
1982    let cs = tree.size[c as usize];
1983    // Mirroring across the viewport itself would teleport a
1984    // cursor-anchored float to the opposite side of the window,
1985    // so viewport floats only clamp.
1986    let mirrors = cfg.anchor == FloatAnchor::Parent;
1987    tree.pos[c as usize] = attach_fitted(&cfg, anchor, cs, vp, mirrors);
1988}
1989
1990/// Where a float of size `cs` sits against `anchor` by its config's
1991/// attach points and offset — and, when it declares `fit`, kept in `vp`:
1992/// mirrored across the anchor per axis where that side is less off-screen
1993/// (if `mirrors`), then clamped. A parent-anchored float and a
1994/// node-anchored one both mirror; a viewport one only clamps.
1995fn attach_fitted(
1996    cfg: &crate::spec::FloatConfig,
1997    anchor: Rect,
1998    cs: Size,
1999    vp: Rect,
2000    mirrors: bool,
2001) -> Vec2 {
2002    let mut x = attach(
2003        anchor.x,
2004        anchor.w,
2005        cs.w,
2006        cfg.anchor_point.0,
2007        cfg.self_point.0,
2008        cfg.offset.x,
2009    );
2010    let mut y = attach(
2011        anchor.y,
2012        anchor.h,
2013        cs.h,
2014        cfg.anchor_point.1,
2015        cfg.self_point.1,
2016        cfg.offset.y,
2017    );
2018    if cfg.fit && mirrors {
2019        // Mirror the attachment across the anchor per axis when
2020        // the mirrored side is less off-screen (ties keep the
2021        // declared side), then clamp the rest. Clamp order pins
2022        // the top/left edge on screen when nothing fits.
2023        let fx = attach(
2024            anchor.x,
2025            anchor.w,
2026            cs.w,
2027            mirror(cfg.anchor_point.0),
2028            mirror(cfg.self_point.0),
2029            -cfg.offset.x,
2030        );
2031        if overflow(x - vp.x, cs.w, vp.w) > overflow(fx - vp.x, cs.w, vp.w) {
2032            x = fx;
2033        }
2034        let fy = attach(
2035            anchor.y,
2036            anchor.h,
2037            cs.h,
2038            mirror(cfg.anchor_point.1),
2039            mirror(cfg.self_point.1),
2040            -cfg.offset.y,
2041        );
2042        if overflow(y - vp.y, cs.h, vp.h) > overflow(fy - vp.y, cs.h, vp.h) {
2043            y = fy;
2044        }
2045    }
2046    if cfg.fit {
2047        x = x.min(vp.x + vp.w - cs.w).max(vp.x);
2048        y = y.min(vp.y + vp.h - cs.h).max(vp.y);
2049    }
2050    Vec2::new(x, y)
2051}
2052
2053fn positions(
2054    tree: &mut Tree,
2055    scroll: &mut ScrollStore,
2056    viewport: Size,
2057    scale: f32,
2058    range: std::ops::Range<usize>,
2059) {
2060    for i in range {
2061        if tree.parent[i] == NIL {
2062            tree.pos[i] = Vec2::ZERO;
2063        }
2064        let spec = tree.specs[i].layout;
2065        let origin = tree.pos[i];
2066        let size = tree.size[i];
2067        let wrap = wraps(tree, i as u32);
2068
2069        let (main_content, cross_content, main_pad_start, cross_pad_start) = match spec.dir {
2070            Dir::Row => (
2071                size.w - spec.padding.x(),
2072                size.h - spec.padding.y(),
2073                spec.padding.l,
2074                spec.padding.t,
2075            ),
2076            Dir::Column => (
2077                size.h - spec.padding.y(),
2078                size.w - spec.padding.x(),
2079                spec.padding.t,
2080                spec.padding.l,
2081            ),
2082        };
2083
2084        // The content box the children occupy, in main/cross terms. For a
2085        // wrapping row that is the widest line by the longest stack of
2086        // lines; for everything else the one run of children.
2087        let mut stretch = 0.0f32;
2088        let (total_main, max_cross) = if wrap {
2089            let (lines, stacked, widest) = wrap_measure(tree, i as u32);
2090            stretch = line_stretch(lines, stacked, cross_content);
2091            (widest, stacked + stretch * lines as f32)
2092        } else {
2093            let mut total_main = 0.0f32;
2094            let mut max_cross = 0.0f32;
2095            let mut n = 0u32;
2096            for c in tree.children(i as u32) {
2097                if is_float(tree, c) {
2098                    continue;
2099                }
2100                let (c_main, c_cross) = match spec.dir {
2101                    Dir::Row => (tree.size[c as usize].w, tree.size[c as usize].h),
2102                    Dir::Column => (tree.size[c as usize].h, tree.size[c as usize].w),
2103                };
2104                total_main += c_main;
2105                max_cross = max_cross.max(c_cross);
2106                n += 1;
2107            }
2108            if n > 1 {
2109                total_main += spec.gap * (n - 1) as f32;
2110            }
2111            (total_main, max_cross)
2112        };
2113
2114        // Scroll containers: clamp the retained offset to this frame's
2115        // overflow and shift children by it.
2116        let mut offset = Vec2::ZERO;
2117        // Anchoring: the scroll axis is the main axis,
2118        // the container is not wrapping, and a previous layout recorded
2119        // which child was first in view and where its leading edge sat in
2120        // the content. Where that edge sits *now* is the same walk the
2121        // placement below makes, minus the offset; the difference is added
2122        // to the retained offset before it is clamped, so the child stays
2123        // where it was on screen whatever grew or shrank before it.
2124        let anchors = spec.anchor
2125            && !wrap
2126            && match spec.dir {
2127                Dir::Row => spec.scroll_x,
2128                Dir::Column => spec.scroll_y,
2129            };
2130        let free_main = (main_content - total_main).max(0.0);
2131        // The unwrapped run's spread; a wrapping row deals out each line's
2132        // own below. Counting the children is skipped for the three
2133        // alignments that need no count.
2134        let n_main = if matches!(spec.main_align, Align::Start | Align::Center | Align::End) {
2135            0
2136        } else {
2137            tree.children(i as u32)
2138                .filter(|&c| !is_float(tree, c))
2139                .count() as u32
2140        };
2141        let (lead_main, between_main) = main_spread(spec.main_align, free_main, n_main);
2142        let content_start = main_pad_start + lead_main;
2143        if anchors && let Some((anchor, was_at)) = scroll.anchor(tree.keys[i]) {
2144            let mut at = content_start;
2145            let mut c = first_in_flow(tree, i as u32);
2146            while c != NIL {
2147                if !is_float(tree, c) {
2148                    if tree.keys[c as usize] == anchor {
2149                        let delta = at - was_at;
2150                        if delta != 0.0 {
2151                            scroll.scroll_by(
2152                                tree.keys[i],
2153                                match spec.dir {
2154                                    Dir::Row => Vec2::new(delta, 0.0),
2155                                    Dir::Column => Vec2::new(0.0, delta),
2156                                },
2157                            );
2158                        }
2159                        break;
2160                    }
2161                    let c_main = match spec.dir {
2162                        Dir::Row => tree.size[c as usize].w,
2163                        Dir::Column => tree.size[c as usize].h,
2164                    };
2165                    at += c_main + spec.gap + between_main;
2166                }
2167                c = tree.next_sibling[c as usize];
2168            }
2169        }
2170        if spec.scroll_x || spec.scroll_y {
2171            let (content_w, content_h) = match spec.dir {
2172                Dir::Row => (total_main + spec.padding.x(), max_cross + spec.padding.y()),
2173                Dir::Column => (max_cross + spec.padding.x(), total_main + spec.padding.y()),
2174            };
2175            let max = Vec2::new(
2176                if spec.scroll_x {
2177                    (content_w - size.w).max(0.0)
2178                } else {
2179                    0.0
2180                },
2181                if spec.scroll_y {
2182                    (content_h - size.h).max(0.0)
2183                } else {
2184                    0.0
2185                },
2186            );
2187            tree.scroll_max[i] = max;
2188            // The one place the container's resolved box and its content
2189            // size exist together; the store keeps a copy, since the tree
2190            // holding them is cleared before the next view reads it.
2191            // The retained offset stays exact — a wheel notch of 0.3 px is
2192            // not lost, it accumulates — and what the children are *moved*
2193            // by is whole physical pixels, so a row's text does not wobble
2194            // inside the row while the list scrolls (`Vec2::snapped`).
2195            offset = scroll
2196                .resolve(
2197                    tree.keys[i],
2198                    Rect::from_pos_size(origin, size),
2199                    Size::new(content_w, content_h),
2200                    max,
2201                    tree.specs[i].transition,
2202                )
2203                .snapped(scale);
2204        }
2205        let (main_scroll, cross_scroll) = match spec.dir {
2206            Dir::Row => (offset.x, offset.y),
2207            Dir::Column => (offset.y, offset.x),
2208        };
2209        if anchors {
2210            // The anchor for the next layout: the first in-flow child whose
2211            // trailing edge is past the offset — the first one in view —
2212            // and where its leading edge sits in the content.
2213            let mut next = None;
2214            let mut at = content_start;
2215            let mut c = first_in_flow(tree, i as u32);
2216            while c != NIL {
2217                if !is_float(tree, c) {
2218                    let c_main = match spec.dir {
2219                        Dir::Row => tree.size[c as usize].w,
2220                        Dir::Column => tree.size[c as usize].h,
2221                    };
2222                    if at + c_main > main_scroll {
2223                        next = Some((tree.keys[c as usize], at));
2224                        break;
2225                    }
2226                    at += c_main + spec.gap + between_main;
2227                }
2228                c = tree.next_sibling[c as usize];
2229            }
2230            scroll.set_anchor(tree.keys[i], next);
2231        }
2232
2233        // Out of flow first, so the in-flow walk is one shape whether or
2234        // not it goes line by line. A frame with no floats skips the walk.
2235        let mut c = if tree.any_float {
2236            tree.first_child[i]
2237        } else {
2238            NIL
2239        };
2240        while c != NIL {
2241            if let Some(cfg) = tree.specs[c as usize].layout.float {
2242                place_float(tree, cfg, c, Rect::from_pos_size(origin, size), viewport);
2243            }
2244            c = tree.next_sibling[c as usize];
2245        }
2246
2247        // One line for an unwrapped container, N for a wrapping row. Main
2248        // alignment places each line's children in the content box the way
2249        // it places the single run's, and cross alignment places a child in
2250        // its own line; with one line the two compose back into the
2251        // unwrapped placement exactly.
2252        let mut cross_cursor = cross_pad_start - cross_scroll;
2253        let mut line_start = first_in_flow(tree, i as u32);
2254        while line_start != NIL {
2255            let (end, extent, run_main) = if wrap {
2256                let end = line_end(tree, line_start);
2257                let (main, cross) = line_extents(tree, line_start, end, spec.gap);
2258                (end, cross + stretch, main)
2259            } else {
2260                (NIL, cross_content, total_main)
2261            };
2262            let free = (main_content - run_main).max(0.0);
2263            let (lead, between) = if wrap {
2264                let mut n = 0u32;
2265                let mut k = line_start;
2266                while k != end && k != NIL {
2267                    n += u32::from(!is_float(tree, k));
2268                    k = tree.next_sibling[k as usize];
2269                }
2270                main_spread(spec.main_align, free, n)
2271            } else {
2272                (lead_main, between_main)
2273            };
2274            // A baseline line's shared baseline, from the line's top.
2275            let base_above =
2276                if tree.any_baseline && spec.cross_align == Align::Baseline && spec.dir == Dir::Row
2277                {
2278                    Some(line_baseline(tree, line_start, end).0)
2279                } else {
2280                    None
2281                };
2282            let mut cursor = main_pad_start + lead - main_scroll;
2283            let mut c = line_start;
2284            while c != end && c != NIL {
2285                if is_float(tree, c) {
2286                    c = tree.next_sibling[c as usize];
2287                    continue;
2288                }
2289                let cs = tree.size[c as usize];
2290                let (c_main, c_cross) = match spec.dir {
2291                    Dir::Row => (cs.w, cs.h),
2292                    Dir::Column => (cs.h, cs.w),
2293                };
2294                let cross_off = match base_above {
2295                    Some(above) if aligns_by_baseline(tree, c) => {
2296                        let b = first_baseline(tree, c).unwrap_or(c_cross);
2297                        cross_cursor + above - b
2298                    }
2299                    Some(_) => cross_cursor,
2300                    None => {
2301                        cross_cursor + align_factor(spec.cross_align) * (extent - c_cross).max(0.0)
2302                    }
2303                };
2304                tree.pos[c as usize] = match spec.dir {
2305                    Dir::Row => Vec2::new(origin.x + cursor, origin.y + cross_off),
2306                    Dir::Column => Vec2::new(origin.x + cross_off, origin.y + cursor),
2307                };
2308                cursor += c_main + spec.gap + between;
2309                c = tree.next_sibling[c as usize];
2310            }
2311            cross_cursor += extent + spec.cross_gap;
2312            line_start = end;
2313        }
2314    }
2315}
2316
2317#[cfg(test)]
2318mod tests {
2319    use super::*;
2320    use crate::geom::Edges;
2321    use crate::key::Key;
2322    use crate::spec::{FloatConfig, NodeSpec};
2323    use crate::tree::{NodeContent, OriginId, TextId};
2324
2325    /// Deterministic measurer: every text is `10px * chars_hint` wide and wraps
2326    /// into 20px lines. The TextId encodes the char count for test purposes.
2327    struct StubText;
2328
2329    impl TextMeasure for StubText {
2330        fn intrinsic(&mut self, id: TextId) -> Size {
2331            Size::new(10.0 * id.0 as f32, 20.0)
2332        }
2333        fn wrapped(&mut self, id: TextId, max_w: f32) -> Size {
2334            let full = 10.0 * id.0 as f32;
2335            if max_w <= 0.0 || full <= max_w {
2336                return Size::new(full, 20.0);
2337            }
2338            let lines = (full / max_w).ceil();
2339            Size::new(max_w, lines * 20.0)
2340        }
2341        /// A 20 px line with 5 px of it below the baseline.
2342        fn baseline(&mut self, _id: TextId) -> f32 {
2343            15.0
2344        }
2345    }
2346
2347    struct T {
2348        tree: Tree,
2349    }
2350
2351    impl T {
2352        fn new(root_spec: NodeSpec) -> Self {
2353            let mut tree = Tree::new();
2354            tree.push(
2355                NIL,
2356                Key::ROOT,
2357                OriginId::HOST,
2358                root_spec,
2359                NodeContent::Container,
2360            );
2361            T { tree }
2362        }
2363
2364        fn node(&mut self, parent: u32, spec: NodeSpec) -> u32 {
2365            let key = Key::ROOT.index(self.tree.len() as u64);
2366            self.tree
2367                .push(parent, key, OriginId::HOST, spec, NodeContent::Container)
2368        }
2369
2370        fn text(&mut self, parent: u32, chars: u32) -> u32 {
2371            let key = Key::ROOT.index(self.tree.len() as u64);
2372            self.tree.push(
2373                parent,
2374                key,
2375                OriginId::HOST,
2376                NodeSpec::default(),
2377                NodeContent::Text(TextId(chars)),
2378            )
2379        }
2380
2381        fn run(&mut self, vw: f32, vh: f32) {
2382            let mut scroll = ScrollStore::default();
2383            compute(
2384                &mut self.tree,
2385                &mut StubText,
2386                &mut scroll,
2387                Size::new(vw, vh),
2388                1.0,
2389            );
2390        }
2391
2392        fn size(&self, i: u32) -> Size {
2393            self.tree.size[i as usize]
2394        }
2395
2396        fn pos(&self, i: u32) -> Vec2 {
2397            self.tree.pos[i as usize]
2398        }
2399    }
2400
2401    fn px(v: f32) -> Sizing {
2402        Sizing::Fixed(v)
2403    }
2404
2405    /// Two grow children in a 600 column, one capped at 100: the cap is
2406    /// the other's room, not a hole. A min that holds a child past its
2407    /// share takes from its siblings the same way, and a run of clamps
2408    /// resolves in one layout.
2409    #[test]
2410    fn a_grow_childs_clamp_is_its_siblings_room() {
2411        let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(600.0)));
2412        let a = t.node(0, NodeSpec::row().grow_height());
2413        let b = t.node(0, NodeSpec::row().grow_height().max_height(100.0));
2414        t.run(1000.0, 1000.0);
2415        assert_eq!(t.size(b).h, 100.0);
2416        assert_eq!(t.size(a).h, 500.0);
2417
2418        let mut t = T::new(NodeSpec::row().width(px(300.0)).height(px(50.0)));
2419        let a = t.node(0, NodeSpec::row().grow_width());
2420        let b = t.node(0, NodeSpec::row().grow_width().min_width(200.0));
2421        let c = t.node(0, NodeSpec::row().grow_width().max_width(20.0));
2422        t.run(1000.0, 1000.0);
2423        assert_eq!(t.size(b).w, 200.0);
2424        assert_eq!(t.size(c).w, 20.0);
2425        assert_eq!(t.size(a).w, 80.0);
2426        assert_eq!(t.pos(c).x, 280.0);
2427    }
2428
2429    /// Flexbox's sign rule (CSS Flexible Box §9.7, step 6): a pass sums
2430    /// its violations and freezes only the violators of the dominant
2431    /// sign, then re-shares. A 600 column of three grow rows, A capped
2432    /// at 100, B held to 500, C plain: the first pass shares 200, A's
2433    /// −100 and B's +300 sum positive, so only B is frozen and A shares
2434    /// the remaining 100 with C — 50 / 500 / 50, not A at its cap and C
2435    /// empty (RG5). Mirrored: A capped at 100, B held to 210, the sum
2436    /// −90 is negative, so only A is frozen, and B's re-share of 250
2437    /// clears its min on its own — 100 / 250 / 250, not 100 / 210 / 290.
2438    #[test]
2439    fn a_pass_freezes_only_the_violators_of_the_dominant_sign() {
2440        let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(600.0)));
2441        let a = t.node(0, NodeSpec::row().grow_height().max_height(100.0));
2442        let b = t.node(0, NodeSpec::row().grow_height().min_height(Min::px(500.0)));
2443        let c = t.node(0, NodeSpec::row().grow_height());
2444        t.run(1000.0, 1000.0);
2445        assert_eq!(t.size(a).h, 50.0);
2446        assert_eq!(t.size(b).h, 500.0);
2447        assert_eq!(t.size(c).h, 50.0);
2448        assert_eq!(t.pos(c).y, 550.0);
2449
2450        let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(600.0)));
2451        let a = t.node(0, NodeSpec::row().grow_height().max_height(100.0));
2452        let b = t.node(0, NodeSpec::row().grow_height().min_height(Min::px(210.0)));
2453        let c = t.node(0, NodeSpec::row().grow_height());
2454        t.run(1000.0, 1000.0);
2455        assert_eq!(t.size(a).h, 100.0);
2456        assert_eq!(t.size(b).h, 250.0);
2457        assert_eq!(t.size(c).h, 250.0);
2458    }
2459
2460    #[test]
2461    fn fit_row_sums_children_and_gaps() {
2462        let mut t = T::new(NodeSpec::row().pad(10.0).gap(5.0));
2463        let r = 0;
2464        t.node(r, NodeSpec::column().width(px(30.0)).height(px(40.0)));
2465        t.node(r, NodeSpec::column().width(px(20.0)).height(px(25.0)));
2466        t.run(1000.0, 1000.0);
2467        // 30 + 5 + 20 + 2*10 pad = 75; height = max(40,25) + 20 = 60
2468        assert_eq!(t.size(r), Size::new(75.0, 60.0));
2469    }
2470
2471    #[test]
2472    fn fit_column_sums_heights() {
2473        let mut t = T::new(NodeSpec::column().gap(4.0));
2474        t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
2475        t.node(0, NodeSpec::row().width(px(50.0)).height(px(10.0)));
2476        t.node(0, NodeSpec::row().width(px(30.0)).height(px(10.0)));
2477        t.run(1000.0, 1000.0);
2478        assert_eq!(t.size(0), Size::new(50.0, 38.0));
2479    }
2480
2481    #[test]
2482    fn grow_splits_remaining_space_by_factor() {
2483        let mut t = T::new(NodeSpec::row().width(px(300.0)).height(px(100.0)).gap(10.0));
2484        let a = t.node(0, NodeSpec::column().width(px(50.0)).height(px(10.0)));
2485        let b = t.node(0, NodeSpec::column().grow_width().height(px(10.0)));
2486        let c = t.node(
2487            0,
2488            NodeSpec::column().width(Sizing::Grow(2.0)).height(px(10.0)),
2489        );
2490        t.run(1000.0, 1000.0);
2491        // content 300, fixed 50, gaps 20 -> remain 230 split 1:2
2492        let bw = t.size(b).w;
2493        let cw = t.size(c).w;
2494        assert!((bw - 230.0 / 3.0).abs() < 0.01, "b={bw}");
2495        assert!((cw - 460.0 / 3.0).abs() < 0.01, "c={cw}");
2496        assert_eq!(t.size(a).w, 50.0);
2497    }
2498
2499    #[test]
2500    fn percent_resolves_against_content_box() {
2501        let mut t = T::new(NodeSpec::row().width(px(200.0)).height(px(100.0)).pad(10.0));
2502        let a = t.node(
2503            0,
2504            NodeSpec::column()
2505                .width(Sizing::Percent(0.5))
2506                .height(Sizing::Percent(1.0)),
2507        );
2508        t.run(1000.0, 1000.0);
2509        assert_eq!(t.size(a), Size::new(90.0, 80.0)); // (200-20)*0.5, (100-20)*1.0
2510    }
2511
2512    #[test]
2513    fn cross_axis_grow_fills_content() {
2514        let mut t = T::new(
2515            NodeSpec::column()
2516                .width(px(120.0))
2517                .height(px(200.0))
2518                .pad(8.0),
2519        );
2520        let a = t.node(0, NodeSpec::row().grow_width().height(px(30.0)));
2521        t.run(1000.0, 1000.0);
2522        assert_eq!(t.size(a).w, 104.0);
2523    }
2524
2525    #[test]
2526    fn nested_fit_propagates_up() {
2527        let mut t = T::new(NodeSpec::column());
2528        let mid = t.node(0, NodeSpec::row().pad(5.0).gap(2.0));
2529        t.node(mid, NodeSpec::column().width(px(10.0)).height(px(10.0)));
2530        t.node(mid, NodeSpec::column().width(px(10.0)).height(px(10.0)));
2531        t.run(1000.0, 1000.0);
2532        assert_eq!(t.size(mid), Size::new(32.0, 20.0));
2533        assert_eq!(t.size(0), Size::new(32.0, 20.0));
2534    }
2535
2536    #[test]
2537    fn root_grow_takes_viewport() {
2538        let mut t = T::new(NodeSpec::column().fill());
2539        t.run(800.0, 600.0);
2540        assert_eq!(t.size(0), Size::new(800.0, 600.0));
2541    }
2542
2543    #[test]
2544    fn positions_row_with_gap_and_padding() {
2545        let mut t = T::new(
2546            NodeSpec::row()
2547                .width(px(300.0))
2548                .height(px(100.0))
2549                .pad(10.0)
2550                .gap(5.0),
2551        );
2552        let a = t.node(0, NodeSpec::column().width(px(40.0)).height(px(20.0)));
2553        let b = t.node(0, NodeSpec::column().width(px(40.0)).height(px(20.0)));
2554        t.run(1000.0, 1000.0);
2555        assert_eq!(t.pos(a), Vec2::new(10.0, 10.0));
2556        assert_eq!(t.pos(b), Vec2::new(55.0, 10.0));
2557    }
2558
2559    #[test]
2560    fn main_center_alignment_offsets_children() {
2561        let mut t = T::new(NodeSpec::row().width(px(200.0)).height(px(50.0)).center());
2562        let a = t.node(0, NodeSpec::column().width(px(60.0)).height(px(20.0)));
2563        t.run(1000.0, 1000.0);
2564        assert_eq!(t.pos(a), Vec2::new(70.0, 15.0));
2565    }
2566
2567    #[test]
2568    fn main_end_alignment() {
2569        let mut t = T::new(
2570            NodeSpec::column()
2571                .width(px(100.0))
2572                .height(px(100.0))
2573                .main_align(Align::End)
2574                .gap(10.0),
2575        );
2576        let a = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
2577        let b = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
2578        t.run(1000.0, 1000.0);
2579        assert_eq!(t.pos(a).y, 50.0);
2580        assert_eq!(t.pos(b).y, 80.0);
2581    }
2582
2583    /// Three 10 px children in a 100 px row: 70 px free, dealt out the
2584    /// way each spread says.
2585    fn spread(a: Align, gap: f32) -> Vec<f32> {
2586        let mut t = T::new(
2587            NodeSpec::row()
2588                .width(px(100.0))
2589                .height(px(10.0))
2590                .gap(gap)
2591                .main_align(a),
2592        );
2593        let kids: Vec<u32> = (0..3)
2594            .map(|_| t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0))))
2595            .collect();
2596        t.run(1000.0, 1000.0);
2597        kids.iter().map(|&k| t.pos(k).x).collect()
2598    }
2599
2600    #[test]
2601    fn space_between_puts_the_free_space_between_the_children() {
2602        assert_eq!(spread(Align::SpaceBetween, 0.0), [0.0, 45.0, 90.0]);
2603        // The spread is on top of the gap: 50 free, 25 each.
2604        assert_eq!(spread(Align::SpaceBetween, 10.0), [0.0, 45.0, 90.0]);
2605    }
2606
2607    #[test]
2608    fn space_around_gives_the_ends_half_a_share() {
2609        // 70 / 3 each, half of it on either side of a child.
2610        let x = spread(Align::SpaceAround, 0.0);
2611        let share = 70.0 / 3.0;
2612        for (i, want) in [share / 2.0, share * 1.5 + 10.0, share * 2.5 + 20.0]
2613            .into_iter()
2614            .enumerate()
2615        {
2616            assert!((x[i] - want).abs() < 1e-3, "{x:?}");
2617        }
2618    }
2619
2620    #[test]
2621    fn space_evenly_makes_every_gap_and_both_ends_equal() {
2622        assert_eq!(spread(Align::SpaceEvenly, 0.0), [17.5, 45.0, 72.5]);
2623    }
2624
2625    /// One child: `space-between` has nothing to go between and starts it,
2626    /// the other two centre it, as CSS does.
2627    #[test]
2628    fn a_lone_child_under_a_spread_starts_or_centres() {
2629        for (a, want) in [
2630            (Align::SpaceBetween, 0.0),
2631            (Align::SpaceAround, 45.0),
2632            (Align::SpaceEvenly, 45.0),
2633        ] {
2634            let mut t = T::new(NodeSpec::row().width(px(100.0)).main_align(a));
2635            let c = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
2636            t.run(1000.0, 1000.0);
2637            assert_eq!(t.pos(c).x, want, "{a:?}");
2638        }
2639    }
2640
2641    /// Nothing free, nothing dealt: a grow child takes the space, and a run
2642    /// that overflows keeps its plain gaps.
2643    #[test]
2644    fn a_spread_with_nothing_free_is_the_gaps_alone() {
2645        let mut t = T::new(
2646            NodeSpec::row()
2647                .width(px(100.0))
2648                .gap(5.0)
2649                .main_align(Align::SpaceBetween),
2650        );
2651        let a = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
2652        let b = t.node(0, NodeSpec::row().grow_width().height(px(10.0)));
2653        let c = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
2654        t.run(1000.0, 1000.0);
2655        assert_eq!((t.pos(a).x, t.pos(b).x, t.pos(c).x), (0.0, 15.0, 90.0));
2656
2657        let mut t = T::new(
2658            NodeSpec::row()
2659                .width(px(100.0))
2660                .gap(5.0)
2661                .scroll_x()
2662                .main_align(Align::SpaceEvenly),
2663        );
2664        let kids: Vec<u32> = (0..3)
2665            .map(|_| t.node(0, NodeSpec::row().width(px(50.0)).height(px(10.0))))
2666            .collect();
2667        t.run(1000.0, 1000.0);
2668        let x: Vec<f32> = kids.iter().map(|&k| t.pos(k).x).collect();
2669        assert_eq!(x, [0.0, 55.0, 110.0]);
2670    }
2671
2672    /// A column spreads down, and floats take no share.
2673    #[test]
2674    fn a_column_spreads_its_height_and_floats_take_no_share() {
2675        let mut t = T::new(
2676            NodeSpec::column()
2677                .width(px(10.0))
2678                .height(px(100.0))
2679                .main_align(Align::SpaceBetween),
2680        );
2681        let a = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
2682        t.node(
2683            0,
2684            NodeSpec::row()
2685                .width(px(5.0))
2686                .height(px(5.0))
2687                .float(FloatConfig::below()),
2688        );
2689        let b = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
2690        t.run(1000.0, 1000.0);
2691        assert_eq!((t.pos(a).y, t.pos(b).y), (0.0, 80.0));
2692    }
2693
2694    /// A wrapping row deals out each line's own free space.
2695    #[test]
2696    fn a_wrapping_row_spreads_each_line_by_itself() {
2697        let mut t = T::new(
2698            NodeSpec::row()
2699                .width(px(100.0))
2700                .wrap()
2701                .main_align(Align::SpaceBetween),
2702        );
2703        let kids: Vec<u32> = (0..3)
2704            .map(|_| t.node(0, NodeSpec::row().width(px(40.0)).height(px(10.0))))
2705            .collect();
2706        t.run(1000.0, 1000.0);
2707        let at: Vec<(f32, f32)> = kids.iter().map(|&k| (t.pos(k).x, t.pos(k).y)).collect();
2708        // Line one holds two with 20 between; line two one, at the start.
2709        assert_eq!(at, [(0.0, 0.0), (60.0, 0.0), (0.0, 10.0)]);
2710    }
2711
2712    /// A 20 px text (baseline 15) and a 40 px box on one row: the box has
2713    /// no text, so its bottom edge is its baseline, and the text drops to
2714    /// meet it. The fit row holds both: 40 above, the text's 5 below.
2715    #[test]
2716    fn baseline_lines_up_text_with_a_box_s_bottom_edge() {
2717        let mut t = T::new(NodeSpec::row().cross_align(Align::Baseline));
2718        let txt = t.text(0, 3);
2719        let bx = t.node(0, NodeSpec::row().width(px(10.0)).height(px(40.0)));
2720        t.run(1000.0, 1000.0);
2721        assert_eq!(t.pos(bx).y, 0.0);
2722        assert_eq!(t.pos(txt).y, 25.0);
2723        assert_eq!(t.size(0).h, 45.0);
2724    }
2725
2726    /// A container's baseline is its first child's, carried down through
2727    /// where that child sits in it: a column padded 10 on top holds its
2728    /// text 10 lower, so a bare text beside it drops 10 to meet it.
2729    #[test]
2730    fn a_container_s_baseline_is_its_first_text_s() {
2731        let mut t = T::new(NodeSpec::row().cross_align(Align::Baseline));
2732        let bare = t.text(0, 3);
2733        let col = t.node(
2734            0,
2735            NodeSpec::column().padding(Edges {
2736                l: 0.0,
2737                r: 0.0,
2738                t: 10.0,
2739                b: 0.0,
2740            }),
2741        );
2742        let inner = t.text(col, 3);
2743        t.node(col, NodeSpec::row().width(px(10.0)).height(px(30.0)));
2744        t.run(1000.0, 1000.0);
2745        assert_eq!(t.pos(col).y, 0.0);
2746        assert_eq!(t.pos(inner).y, 10.0);
2747        assert_eq!(t.pos(bare).y, 10.0);
2748        // The column (60 tall, baseline 25) sets the row's height.
2749        assert_eq!(t.size(0).h, 60.0);
2750    }
2751
2752    /// A grow height fills the line from its top instead of aligning.
2753    #[test]
2754    fn a_grow_height_child_of_a_baseline_row_fills_from_the_top() {
2755        let mut t = T::new(
2756            NodeSpec::row()
2757                .height(px(50.0))
2758                .cross_align(Align::Baseline),
2759        );
2760        let txt = t.text(0, 3);
2761        let g = t.node(0, NodeSpec::row().width(px(10.0)).grow_height());
2762        t.run(1000.0, 1000.0);
2763        assert_eq!((t.pos(g).y, t.size(g).h), (0.0, 50.0));
2764        assert_eq!(t.pos(txt).y, 0.0);
2765    }
2766
2767    /// A column's cross axis is horizontal: `baseline` there is `start`.
2768    #[test]
2769    fn baseline_on_a_column_is_start() {
2770        let mut t = T::new(
2771            NodeSpec::column()
2772                .width(px(100.0))
2773                .cross_align(Align::Baseline),
2774        );
2775        let c = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
2776        t.run(1000.0, 1000.0);
2777        assert_eq!(t.pos(c).x, 0.0);
2778    }
2779
2780    /// `width: grow` and a ratio keeps its shape: the fit height is the
2781    /// final width over the ratio, and the children overflow it.
2782    #[test]
2783    fn a_ratio_sizes_a_fit_height_from_the_final_width() {
2784        let mut t = T::new(NodeSpec::column().width(px(320.0)));
2785        let v = t.node(0, NodeSpec::column().grow_width().aspect_ratio(16.0 / 9.0));
2786        t.node(v, NodeSpec::row().width(px(10.0)).height(px(500.0)));
2787        t.run(1000.0, 1000.0);
2788        assert_eq!(t.size(v), Size::new(320.0, 180.0));
2789    }
2790
2791    /// Under a fixed height, a fit width is the height times the ratio.
2792    #[test]
2793    fn a_ratio_sizes_a_fit_width_from_a_fixed_height() {
2794        let mut t = T::new(NodeSpec::row());
2795        let sq = t.node(0, NodeSpec::row().height(px(24.0)).aspect_ratio(1.0));
2796        t.run(1000.0, 1000.0);
2797        assert_eq!(t.size(sq), Size::new(24.0, 24.0));
2798    }
2799
2800    /// A derived height is not shrunk: a column too short for a ratio box
2801    /// shrinks its other fit children and leaves the box its shape.
2802    #[test]
2803    fn a_ratio_s_derived_height_is_not_shrunk() {
2804        let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(100.0)));
2805        let r = t.node(0, NodeSpec::column().grow_width().aspect_ratio(1.25));
2806        let other = t.node(0, NodeSpec::column().height(Sizing::Fit));
2807        t.node(other, NodeSpec::row().width(px(10.0)).height(px(60.0)));
2808        t.run(1000.0, 1000.0);
2809        assert_eq!(t.size(r).h, 80.0);
2810        // Nor is its sibling below the 60 px it holds: the
2811        // column overflows, where the sibling was squeezed to 20.
2812        assert_eq!(t.size(other).h, 60.0);
2813    }
2814
2815    /// `minHeight: fit` floors a derived height at the children.
2816    #[test]
2817    fn min_fit_floors_a_ratio_height_at_its_children() {
2818        let mut t = T::new(NodeSpec::column().width(px(100.0)));
2819        let r = t.node(
2820            0,
2821            NodeSpec::column()
2822                .grow_width()
2823                .aspect_ratio(4.0)
2824                .min_height(Min::FIT),
2825        );
2826        t.node(r, NodeSpec::row().width(px(10.0)).height(px(40.0)));
2827        t.run(1000.0, 1000.0);
2828        assert_eq!(t.size(r).h, 40.0);
2829    }
2830
2831    #[test]
2832    fn text_gets_intrinsic_size_when_it_fits() {
2833        let mut t = T::new(NodeSpec::column().width(px(500.0)).height(px(500.0)));
2834        let txt = t.text(0, 8); // 80px wide
2835        t.run(1000.0, 1000.0);
2836        assert_eq!(t.size(txt), Size::new(80.0, 20.0));
2837    }
2838
2839    #[test]
2840    fn text_wraps_when_clamped_by_parent() {
2841        let mut t = T::new(
2842            NodeSpec::column()
2843                .width(px(100.0))
2844                .height(px(500.0))
2845                .pad(10.0),
2846        );
2847        let txt = t.text(0, 20); // 200px intrinsic, clamped to 80 -> 3 lines
2848        t.run(1000.0, 1000.0);
2849        assert_eq!(t.size(txt).w, 80.0);
2850        assert_eq!(t.size(txt).h, 60.0);
2851    }
2852
2853    #[test]
2854    fn text_wrapping_grows_fit_parent_height() {
2855        let mut t = T::new(NodeSpec::column().width(px(100.0)));
2856        let txt = t.text(0, 30); // 300px intrinsic -> wraps to 100 -> 3 lines
2857        t.run(1000.0, 1000.0);
2858        assert_eq!(t.size(txt).h, 60.0);
2859        assert_eq!(t.size(0).h, 60.0);
2860    }
2861
2862    #[test]
2863    fn grow_with_no_space_left_gets_zero() {
2864        let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
2865        let a = t.node(0, NodeSpec::column().width(px(120.0)).height(px(10.0)));
2866        let b = t.node(0, NodeSpec::column().grow_width().height(px(10.0)));
2867        t.run(1000.0, 1000.0);
2868        assert_eq!(t.size(a).w, 120.0); // no shrinking in v0
2869        assert_eq!(t.size(b).w, 0.0);
2870    }
2871
2872    #[test]
2873    fn deep_nesting_positions_accumulate() {
2874        let mut t = T::new(NodeSpec::column().pad(10.0));
2875        let l1 = t.node(0, NodeSpec::column().pad(10.0));
2876        let l2 = t.node(l1, NodeSpec::column().pad(10.0));
2877        let leaf = t.node(l2, NodeSpec::row().width(px(10.0)).height(px(10.0)));
2878        t.run(1000.0, 1000.0);
2879        assert_eq!(t.pos(leaf), Vec2::new(30.0, 30.0));
2880        assert_eq!(t.size(0), Size::new(70.0, 70.0));
2881    }
2882
2883    #[test]
2884    fn grow_respects_max_width() {
2885        let mut t = T::new(NodeSpec::row().width(px(800.0)).height(px(100.0)));
2886        let a = t.node(
2887            0,
2888            NodeSpec::column()
2889                .grow_width()
2890                .max_width(560.0)
2891                .height(px(10.0)),
2892        );
2893        t.run(1000.0, 1000.0);
2894        assert_eq!(t.size(a).w, 560.0);
2895        // And tracks the parent when it's smaller than the cap.
2896        let mut t = T::new(NodeSpec::row().width(px(400.0)).height(px(100.0)));
2897        let a = t.node(
2898            0,
2899            NodeSpec::column()
2900                .grow_width()
2901                .max_width(560.0)
2902                .height(px(10.0)),
2903        );
2904        t.run(1000.0, 1000.0);
2905        assert_eq!(t.size(a).w, 400.0);
2906    }
2907
2908    #[test]
2909    fn min_width_forces_fit_up() {
2910        let mut t = T::new(NodeSpec::column());
2911        let a = t.node(0, NodeSpec::row().min_width(120.0).height(px(10.0)));
2912        t.node(a, NodeSpec::column().width(px(30.0)).height(px(10.0)));
2913        t.run(1000.0, 1000.0);
2914        assert_eq!(t.size(a).w, 120.0);
2915        // Min propagates into the fit parent.
2916        assert_eq!(t.size(0).w, 120.0);
2917    }
2918
2919    #[test]
2920    fn percent_respects_max() {
2921        let mut t = T::new(NodeSpec::column().width(px(1000.0)).height(px(1000.0)));
2922        let a = t.node(
2923            0,
2924            NodeSpec::row()
2925                .width(Sizing::Percent(0.9))
2926                .max_width(300.0)
2927                .height(px(10.0)),
2928        );
2929        t.run(1000.0, 1000.0);
2930        assert_eq!(t.size(a).w, 300.0);
2931    }
2932
2933    #[test]
2934    fn text_rewraps_when_capped_parent_shrinks() {
2935        // Same tree shape at two parent widths: the narrow one wraps taller.
2936        let build = |parent_w: f32| {
2937            let mut t = T::new(NodeSpec::column().width(px(parent_w)));
2938            let txt = t.text(0, 40); // 400px intrinsic
2939            t.run(1000.0, 1000.0);
2940            t.size(txt)
2941        };
2942        let wide = build(500.0);
2943        let narrow = build(100.0);
2944        assert_eq!(wide.h, 20.0);
2945        assert_eq!(narrow.h, 80.0); // 400 / 100 -> 4 lines
2946    }
2947
2948    #[test]
2949    fn shrink_compresses_largest_fit_child_first() {
2950        let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
2951        let a = t.node(0, NodeSpec::column());
2952        t.node(a, NodeSpec::row().width(px(80.0)).height(px(10.0)));
2953        let b = t.node(0, NodeSpec::column());
2954        t.node(b, NodeSpec::row().width(px(40.0)).height(px(10.0)));
2955        t.run(1000.0, 1000.0);
2956        // 120 into 100: the 80 child pays the whole 20px deficit.
2957        assert_eq!(t.size(a).w, 60.0);
2958        assert_eq!(t.size(b).w, 40.0);
2959    }
2960
2961    #[test]
2962    fn shrink_respects_min_and_spills_to_the_next() {
2963        let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
2964        let a = t.node(0, NodeSpec::column().min_width(70.0));
2965        t.node(a, NodeSpec::row().width(px(80.0)).height(px(10.0)));
2966        let b = t.node(0, NodeSpec::column());
2967        t.node(b, NodeSpec::row().width(px(40.0)).height(px(10.0)));
2968        t.run(1000.0, 1000.0);
2969        // a stops at its min; b pays the rest.
2970        assert_eq!(t.size(a).w, 70.0);
2971        assert_eq!(t.size(b).w, 30.0);
2972    }
2973
2974    #[test]
2975    fn equal_children_shrink_equally() {
2976        let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
2977        let mut kids = Vec::new();
2978        for _ in 0..3 {
2979            let c = t.node(0, NodeSpec::column());
2980            t.node(c, NodeSpec::row().width(px(60.0)).height(px(10.0)));
2981            kids.push(c);
2982        }
2983        t.run(1000.0, 1000.0);
2984        for c in kids {
2985            assert!(
2986                (t.size(c).w - 100.0 / 3.0).abs() < 0.1,
2987                "got {}",
2988                t.size(c).w
2989            );
2990        }
2991    }
2992
2993    #[test]
2994    fn shrunk_text_rewraps() {
2995        let mut t = T::new(NodeSpec::row().width(px(200.0)).height(px(500.0)));
2996        t.node(0, NodeSpec::column().width(px(80.0)).height(px(10.0)));
2997        let txt = t.text(0, 20); // 200px intrinsic
2998        t.run(1000.0, 1000.0);
2999        // 280 into 200: text pays the deficit, then wraps at 120 -> 2 lines.
3000        assert_eq!(t.size(txt).w, 120.0);
3001        assert_eq!(t.size(txt).h, 40.0);
3002    }
3003
3004    #[test]
3005    fn text_never_shrinks_vertically() {
3006        let mut t = T::new(NodeSpec::column().width(px(200.0)).height(px(30.0)));
3007        let txt = t.text(0, 30); // wraps to 200 -> 2 lines = 40 > 30 parent
3008        t.run(1000.0, 1000.0);
3009        assert_eq!(
3010            t.size(txt).h,
3011            40.0,
3012            "text overflows rather than clipping lines"
3013        );
3014    }
3015
3016    #[test]
3017    fn scroll_axis_skips_shrink() {
3018        let mut t = T::new(
3019            NodeSpec::column()
3020                .width(px(100.0))
3021                .height(px(100.0))
3022                .scroll_y(),
3023        );
3024        for _ in 0..2 {
3025            let c = t.node(0, NodeSpec::column());
3026            t.node(c, NodeSpec::row().width(px(10.0)).height(px(80.0)));
3027        }
3028        t.run(1000.0, 1000.0);
3029        // 160 of content in a 100 box stays 160: it scrolls instead.
3030        for c in [1u32, 3u32] {
3031            assert_eq!(t.size(c).h, 80.0);
3032        }
3033    }
3034
3035    /// An i3-style tab bar: every tab `grow` with a `min_width`, on a row
3036    /// that scrolls x. With room the tabs split the bar evenly; past it
3037    /// each sits at its min and the bar scrolls by the overflow — the
3038    /// clamp in `distribute_run` and the shrink pass a scroll axis skips
3039    /// are what make one declaration cover both regimes.
3040    #[test]
3041    fn grow_tabs_split_evenly_then_scroll_at_their_min() {
3042        let bar = || NodeSpec::row().width(px(600.0)).height(px(30.0)).scroll_x();
3043        let tab = || {
3044            NodeSpec::column()
3045                .grow_width()
3046                .min_width(80.0)
3047                .height(px(30.0))
3048        };
3049        // Three tabs: 200 each, nothing to scroll.
3050        let mut t = T::new(bar());
3051        let tabs: Vec<u32> = (0..3).map(|_| t.node(0, tab())).collect();
3052        t.run(1000.0, 1000.0);
3053        for (k, &c) in tabs.iter().enumerate() {
3054            assert_eq!(t.size(c).w, 200.0);
3055            assert_eq!(t.pos(c).x, 200.0 * k as f32);
3056        }
3057        assert_eq!(t.tree.scroll_max[0].x, 0.0);
3058        // Ten tabs: 800 of min in a 600 bar, every tab at 80, 200 to scroll.
3059        let mut t = T::new(bar());
3060        let tabs: Vec<u32> = (0..10).map(|_| t.node(0, tab())).collect();
3061        t.run(1000.0, 1000.0);
3062        for (k, &c) in tabs.iter().enumerate() {
3063            assert_eq!(t.size(c).w, 80.0);
3064            assert_eq!(t.pos(c).x, 80.0 * k as f32);
3065        }
3066        assert_eq!(t.tree.scroll_max[0].x, 200.0);
3067        // Same bar without scroll_x: the mins still hold (grow is not
3068        // shrinkable), so the row overflows and clips instead.
3069        let mut t = T::new(NodeSpec::row().width(px(600.0)).height(px(30.0)));
3070        let tabs: Vec<u32> = (0..10).map(|_| t.node(0, tab())).collect();
3071        t.run(1000.0, 1000.0);
3072        for &c in &tabs {
3073            assert_eq!(t.size(c).w, 80.0);
3074        }
3075        // The floor as the tab's own content: `Min::FIT` under `Grow`.
3076        // Ten tabs each around an 80 px label split a 600 bar as the
3077        // numeric min did — the label is the min — and the fit resolves
3078        // to a number the spec keeps.
3079        let mut t = T::new(bar());
3080        let tabs: Vec<u32> = (0..10)
3081            .map(|_| {
3082                let c = t.node(0, tab().min_width(Min::FIT));
3083                t.text(c, 8);
3084                c
3085            })
3086            .collect();
3087        t.run(1000.0, 1000.0);
3088        for (k, &c) in tabs.iter().enumerate() {
3089            assert_eq!(t.size(c).w, 80.0);
3090            assert_eq!(t.pos(c).x, 80.0 * k as f32);
3091            assert_eq!(t.tree.specs[c as usize].layout.min_w, Min::px(80.0));
3092        }
3093        assert_eq!(t.tree.scroll_max[0].x, 200.0);
3094        // And with room, the same tabs split it: 3 × 200, the floor idle.
3095        let mut t = T::new(bar());
3096        let tabs: Vec<u32> = (0..3)
3097            .map(|_| {
3098                let c = t.node(0, tab().min_width(Min::FIT));
3099                t.text(c, 8);
3100                c
3101            })
3102            .collect();
3103        t.run(1000.0, 1000.0);
3104        for &c in &tabs {
3105            assert_eq!(t.size(c).w, 200.0);
3106        }
3107    }
3108
3109    /// `Min::FIT` on the cross axis and under a percent: a 50%-tall cell
3110    /// in a 20-tall row floors at its 16-tall child, and a fit floor with
3111    /// nothing inside is no floor.
3112    #[test]
3113    fn min_fit_floors_a_percent_height_at_its_content() {
3114        let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(20.0)));
3115        let a = t.node(
3116            0,
3117            NodeSpec::column()
3118                .width(px(10.0))
3119                .height(Sizing::Percent(0.5))
3120                .min_height(Min::FIT),
3121        );
3122        t.node(a, NodeSpec::column().width(px(10.0)).height(px(16.0)));
3123        let b = t.node(
3124            0,
3125            NodeSpec::column()
3126                .width(px(10.0))
3127                .height(Sizing::Percent(0.5))
3128                .min_height(Min::FIT),
3129        );
3130        t.run(1000.0, 1000.0);
3131        assert_eq!(t.size(a).h, 16.0);
3132        assert_eq!(t.size(b).h, 10.0);
3133    }
3134
3135    #[test]
3136    fn padding_asymmetric() {
3137        let mut t = T::new(
3138            NodeSpec::column()
3139                .width(px(100.0))
3140                .height(px(100.0))
3141                .padding(Edges {
3142                    l: 1.0,
3143                    r: 2.0,
3144                    t: 3.0,
3145                    b: 4.0,
3146                }),
3147        );
3148        let a = t.node(0, NodeSpec::row().fill());
3149        t.run(1000.0, 1000.0);
3150        assert_eq!(t.pos(a), Vec2::new(1.0, 3.0));
3151        assert_eq!(t.size(a), Size::new(97.0, 93.0));
3152    }
3153}