Skip to main content

frus_layout/
tree.rs

1//! Building the layout tree and computing absolute rectangles.
2
3use std::collections::HashMap;
4
5use frus_core::{Rect, Size};
6use taffy::{AvailableSpace, TaffyTree, TraversePartialTree};
7
8use crate::style::Style;
9
10/// An opaque identifier for a layout node.
11pub type NodeId = taffy::NodeId;
12
13/// A **constrained measurement** for a leaf whose size depends on the space
14/// offered — wrapping text, custom-painted content. It receives the maximum
15/// width and height (`None` = unconstrained) and returns the content's size. Taffy
16/// calls it during layout, including for **intrinsic** sizes (min-content gives a
17/// width of `Some(0.0)`, max-content gives `None`).
18/// The lifetime is what lets a measurement **borrow**: a text's closure owns everything
19/// it needs and is `'static`, but a [`crate::Layout`] built for a widget tree can hold a
20/// closure that borrows the tree, which is how a box whose content is built from the
21/// space offered gets measured at all.
22pub type MeasureFn<'a> = Box<dyn Fn(Option<f32>, Option<f32>) -> Size + 'a>;
23
24/// The edge a box's content ran past.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Side {
27    /// Past the left edge.
28    Left,
29    /// Past the right edge.
30    Right,
31    /// Past the top edge.
32    Top,
33    /// Past the bottom edge.
34    Bottom,
35}
36
37/// A box whose children do not fit inside it, and by how much.
38///
39/// The reference reports this to the console and paints a striped band across the
40/// offending edge. Here a child simply draws outside its parent and nothing says so,
41/// which is how a task row's delete button came to be laid out past the window — drawn
42/// nowhere, hittable nowhere, and undiagnosed across three milestones (327, 333, 334).
43#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct Overflowing {
45    /// The **parent's** absolute box: the one that turned out to be too small.
46    pub rect: Rect,
47    /// The edge the content ran past.
48    pub side: Side,
49    /// By how many logical pixels.
50    pub amount: f32,
51}
52
53/// A layout tree. `T` is user data attached to the nodes — a colour, say — handed
54/// back with each computed rectangle.
55pub struct Layout<'a, T> {
56    tree: TaffyTree<T>,
57    /// Constrained measurements, one per "measured" leaf.
58    measures: HashMap<NodeId, MeasureFn<'a>>,
59}
60
61/// **The rule a self-measuring widget has to keep**, checked where every measurement
62/// passes through — in debug builds only, the cost in release being a rule nobody can
63/// see broken.
64///
65/// **A measurement is a whole number of pixels.** The boxes this engine hands out are
66/// rounded, so a widget that measures itself at `146.4` and says so is given `146`, and
67/// `146 < 146.4` — at paint time, where it is measured again, it no longer fits and
68/// wraps, clips or overflows. That is milestone 289, and it presented as a line of text
69/// that vanished rather than as arithmetic. The cure is [`frus_core::fits`]; this is what
70/// says so when a new measurer rounds to nearest instead, which is what anyone writing
71/// one does by reflex.
72///
73/// **Two things it deliberately does not check.**
74///
75/// A measurement *larger than the space it was offered* is not a violation here. This
76/// framework lets content overflow and reports it ([`crate::Overflowing`], milestones
77/// 327–334) rather than crushing it, so a measurer answering with its natural size is
78/// doing its job; a box too small for it is a fact about the layout, not about the
79/// measurement.
80///
81/// And a measurement that came back **exactly** at its bound is exempt from the rule
82/// above: it was clamped there, the bound is the caller's number rather than the
83/// measurer's, and rounding it up would make a box a fraction wider than it was allowed
84/// — which is a different bug. Clamp after rounding up, never before.
85#[inline]
86fn debug_assert_measurement(measured: f32, bound: Option<f32>, axis: &str) {
87    #[cfg(debug_assertions)]
88    {
89        const EPSILON: f32 = 1e-3;
90        if bound.is_some_and(|bound| (measured - bound).abs() <= EPSILON) {
91            return;
92        }
93        debug_assert!(
94            (measured - measured.ceil()).abs() <= EPSILON,
95            "a measured {axis} of {measured} is not a whole number of pixels, so the box \
96             it is rounded to is one it no longer fits in — go through `frus_core::fits` \
97             (milestone 289, issue #54)"
98        );
99    }
100    #[cfg(not(debug_assertions))]
101    {
102        let _ = (measured, bound, axis);
103    }
104}
105
106impl<'a, T> Layout<'a, T> {
107    /// Creates an empty tree.
108    pub fn new() -> Self {
109        let tree = TaffyTree::new();
110        Self {
111            tree,
112            measures: HashMap::new(),
113        }
114    }
115
116    /// Adds a leaf — a node with no children — carrying associated data.
117    pub fn leaf(&mut self, style: Style, data: T) -> NodeId {
118        self.tree
119            .new_leaf_with_context(style.to_taffy(), data)
120            .expect("creating a layout leaf")
121    }
122
123    /// Adds a **measured** leaf: its size comes from `measure`, given the space
124    /// offered, rather than from a dimension fixed in the style.
125    pub fn measured_leaf(&mut self, style: Style, data: T, measure: MeasureFn<'a>) -> NodeId {
126        let node = self.leaf(style, data);
127        self.measures.insert(node, measure);
128        node
129    }
130
131    /// Adds `dy` to a node's **top margin**, after it has been built.
132    ///
133    /// Baseline alignment needs it: a row can only know how far to push a child down
134    /// once it has measured every child, and by then the children are nodes rather
135    /// than styles. Nothing else should reach for this — a style is otherwise settled
136    /// when the node is made.
137    pub fn add_margin_top(&mut self, node: NodeId, dy: f32) {
138        if dy <= 0.0 {
139            return;
140        }
141        let mut style = self.tree.style(node).expect("a node we made").clone();
142        let base = match style.margin.top {
143            taffy::LengthPercentageAuto::Length(v) => v,
144            // A percentage or `auto` margin is resolved against the parent, which is
145            // not a number we have here; the shift is added as if it were zero, which
146            // is the only reading that cannot make the row worse than it was.
147            _ => 0.0,
148        };
149        style.margin.top = taffy::LengthPercentageAuto::Length(base + dy);
150        self.tree.set_style(node, style).expect("setting a style");
151    }
152
153    /// Forgets the size a node was given on one axis, so that it is sized by the layout —
154    /// what a default gives way to when the room on offer replaces it.
155    pub fn clear_size(&mut self, node: NodeId, horizontal: bool) {
156        let mut style = self.tree.style(node).expect("a node we made").clone();
157        if horizontal {
158            style.size.width = taffy::Dimension::Auto;
159        } else {
160            style.size.height = taffy::Dimension::Auto;
161        }
162        self.tree.set_style(node, style).expect("setting a style");
163    }
164
165    /// Makes a node **fill the space its parent leaves it**, after it has been built:
166    /// growing along the parent's main axis, stretching across it, or both.
167    ///
168    /// This is what `MainAxisSize::Max` needs and cannot express on its own. A row that
169    /// fills its line is asking about the *parent's* axis, not its own — grow if the
170    /// parent runs the same way, stretch if it runs across — and a widget does not know
171    /// what it was put inside. The parent does, so it says so here.
172    pub fn fill_parent(&mut self, node: NodeId, horizontal: bool, grow: bool) {
173        let mut style = self.tree.style(node).expect("a node we made").clone();
174        let size = if horizontal {
175            style.size.width
176        } else {
177            style.size.height
178        };
179        // A box that was given a size has already answered the question, and growing it
180        // past that number would be answering a different one.
181        if !matches!(size, taffy::Dimension::Auto) {
182            return;
183        }
184        if grow {
185            // From the content rather than from zero: filling is about taking the room
186            // that is going spare, not about splitting the line into equal shares.
187            if style.flex_grow != 0.0 {
188                return;
189            }
190            style.flex_grow = 1.0;
191        } else {
192            if style.align_self.is_some() {
193                return;
194            }
195            style.align_self = Some(taffy::AlignItems::Stretch);
196        }
197        self.tree.set_style(node, style).expect("setting a style");
198    }
199
200    /// Lets a node **give way** along its parent's main axis, after it has been built.
201    ///
202    /// The default is not to: the reference hands an inflexible child of a row or a
203    /// column an *unbounded* main axis, so it is never squeezed and a line that does not
204    /// fit overflows and says so. Flexbox's default is the opposite — every item absorbs
205    /// its share of a deficit — and a framework that quietly crushes a button rather than
206    /// reporting a layout that does not fit is a framework whose bugs are invisible.
207    ///
208    /// The exception is a box with **one** child, which is not dividing a line: it is
209    /// handing its own constraints down, and the reference bounds a lone child by them.
210    /// Only a node that has not asked for something else is touched. Growing is not
211    /// asking for something else: a lone child that fills its parent is still bounded by
212    /// it, and one that both grows and refuses to give way is a box that can only get
213    /// bigger than the thing holding it.
214    pub fn allow_shrink(&mut self, node: NodeId) {
215        let mut style = self.tree.style(node).expect("a node we made").clone();
216        // **The floor, not just the willingness.** A flex item's automatic minimum is its
217        // **min-content** size, so `flex_shrink` on its own buys nothing: the item agrees
218        // to shrink and then refuses to go below the widest thing inside it. A lone child
219        // holding one over-wide row came out at that row's width, the box it was given
220        // came out at the same, and every sibling stretched to match — a whole screen
221        // laid out to the width of its widest line and clipped by the window. Found in a
222        // real application whose product grid, search field and banner were all cut off
223        // at the same edge.
224        //
225        // Zero is what makes the box the child's bound rather than a suggestion, which is
226        // the reference's rule: a lone child is handed its parent's constraints, and it
227        // cannot come back bigger than them.
228        let mut changed = false;
229        if style.flex_shrink == 0.0 {
230            style.flex_shrink = 1.0;
231            changed = true;
232        }
233        // **The width, and only the width.** A box is bounded by the width it was given;
234        // a height is what scrolling is for, and squeezing one squashes text nobody
235        // asked to squash. Tried both ways round: bounding the height too shortened a
236        // pane in a golden until its background no longer covered its own content.
237        //
238        // **Only a minimum nobody asked for.** `Auto` is the automatic one — the item's
239        // own min-content — and that is the one to drop. A box that named a floor, or a
240        // tight one, keeps it: the point is to stop a child inventing a width, not to
241        // overrule one it was handed.
242        if matches!(style.min_size.width, taffy::Dimension::Auto) {
243            style.min_size.width = taffy::style_helpers::length(0.0_f32);
244            changed = true;
245        }
246        if changed {
247            self.tree.set_style(node, style).expect("setting a style");
248        }
249    }
250
251    /// Gives a node the **shape of a tile**: a height derived from the width it ends up
252    /// with by `ratio` (`width / height`, taffy's convention and the reference's).
253    ///
254    /// A grid's tiles are all the same shape in the reference, and that shape is the
255    /// *container's* say rather than each tile's — a tile cannot know how wide its
256    /// column came out — which is why it is imposed here instead of asked of the child.
257    ///
258    /// Only the ratio is set, and that is enough **in a grid**: a track's width is
259    /// definite, so the height follows from it. It would not be enough across a flex,
260    /// where a box is merely stretched by the cross-axis alignment and there is no
261    /// number to derive the other axis from (which is why [`crate::Style::aspect_ratio`]
262    /// is paired with a percentage width there).
263    ///
264    /// A child that has already chosen a size of its own keeps it.
265    pub fn set_tile_shape(&mut self, node: NodeId, ratio: f32) {
266        let mut style = self.tree.style(node).expect("a node we made").clone();
267        if !matches!(style.size.width, taffy::Dimension::Auto)
268            || !matches!(style.size.height, taffy::Dimension::Auto)
269        {
270            return;
271        }
272        style.aspect_ratio = Some(ratio);
273        self.tree.set_style(node, style).expect("setting a style");
274    }
275
276    /// Sets a node's **minimum width**, after it has been built.
277    ///
278    /// It is what stops a leaf being squeezed along a row, and only a row can say so: the
279    /// same number across a column would be a floor on the *cross* axis, where the box is
280    /// handed a width rather than asked for one.
281    pub fn set_min_width(&mut self, node: NodeId, width: f32) {
282        let mut style = self.tree.style(node).expect("a node we made").clone();
283        if !matches!(style.min_size.width, taffy::Dimension::Auto) {
284            return;
285        }
286        style.min_size.width = taffy::Dimension::Length(width);
287        self.tree.set_style(node, style).expect("setting a style");
288    }
289
290    /// Makes the **root** of a layout fill one axis of whatever room it is computed in.
291    ///
292    /// A root has no parent to grow into, so the flex machinery `fill_parent` uses has
293    /// nothing to work with. A percentage does: it resolves against the available space
294    /// when that space is definite, and falls back to hugging the content when it is not
295    /// — which is the right answer in both cases. A row at the top of a frame fills the
296    /// window; the same row measured for its natural size still shrink-wraps.
297    pub fn fill_root(&mut self, node: NodeId, horizontal: bool) {
298        let mut style = self.tree.style(node).expect("a node we made").clone();
299        let axis = if horizontal {
300            &mut style.size.width
301        } else {
302            &mut style.size.height
303        };
304        if !matches!(axis, taffy::Dimension::Auto) {
305            return;
306        }
307        *axis = taffy::Dimension::Percent(1.0);
308        self.tree.set_style(node, style).expect("setting a style");
309    }
310
311    /// Adds a container — a node with children — with no associated data.
312    pub fn container(&mut self, style: Style, children: &[NodeId]) -> NodeId {
313        // An **overlapping** container puts every child in its one cell ([`Style::overlap`]).
314        // A placement belongs to the child's taffy style, and a child's style is its own,
315        // so it is added here: the only point where a parent and its children meet.
316        if style.overlap {
317            use taffy::style_helpers::{line, span};
318            for &child in children {
319                let mut placed = self.tree.style(child).expect("a node we made").clone();
320                placed.grid_row = taffy::Line {
321                    start: line(1),
322                    end: span(1),
323                };
324                placed.grid_column = taffy::Line {
325                    start: line(1),
326                    end: span(1),
327                };
328                self.tree.set_style(child, placed).expect("setting a style");
329            }
330        }
331        self.tree
332            .new_with_children(style.to_taffy(), children)
333            .expect("creating a layout container")
334    }
335
336    /// Computes the layout from `root`, within the given available space.
337    pub fn compute(&mut self, root: NodeId, available: Size) {
338        self.compute_in(
339            root,
340            taffy::Size {
341                width: AvailableSpace::Definite(available.width),
342                height: AvailableSpace::Definite(available.height),
343            },
344        );
345    }
346
347    /// Computes the layout of content that is **handed a box**: both axes are
348    /// definite, and an unset (`Auto`) root dimension takes the box's size instead
349    /// of hugging the content.
350    ///
351    /// The distinction from [`Layout::compute`] is who decides the size. A widget
352    /// laid out in the ordinary way is asked how big it wants to be; a page of a
353    /// paged view is *told*, and a page that hugged its content would leave the rest
354    /// of the panel empty and unclickable.
355    pub fn compute_filled(&mut self, root: NodeId, width: f32, height: f32) {
356        if let Ok(current) = self.tree.style(root) {
357            let mut style = current.clone();
358            let mut changed = false;
359            if matches!(style.size.width, taffy::Dimension::Auto) {
360                style.size.width = taffy::Dimension::Length(width);
361                changed = true;
362            }
363            if matches!(style.size.height, taffy::Dimension::Auto) {
364                style.size.height = taffy::Dimension::Length(height);
365                changed = true;
366            }
367            if changed {
368                let _ = self.tree.set_style(root, style);
369            }
370        }
371        self.compute(root, Size::new(width, height));
372    }
373
374    /// Computes the layout of content **forced** into a box on the axes that are not
375    /// free: the node's own width or height is overruled rather than merely bounded.
376    ///
377    /// The difference from [`Layout::compute_filled`] is who wins when the content has
378    /// already chosen. Filling defers to it — an explicit size is a size the caller
379    /// asked for — while this overrules it, which is what a stack layer pinned to two
380    /// opposite edges means: the edges say how wide it is, and a width of its own is not
381    /// a second opinion but a contradiction.
382    pub fn compute_tight(
383        &mut self,
384        root: NodeId,
385        width: f32,
386        height: f32,
387        free_x: bool,
388        free_y: bool,
389    ) {
390        if let Ok(current) = self.tree.style(root) {
391            let mut style = current.clone();
392            if !free_x {
393                style.size.width = taffy::Dimension::Length(width);
394            }
395            if !free_y {
396                style.size.height = taffy::Dimension::Length(height);
397            }
398            let _ = self.tree.set_style(root, style);
399        }
400        self.compute_scroll(root, width, height, free_x, free_y);
401    }
402
403    /// Computes the layout of scrollable content: each axis is either constrained
404    /// to the viewport or **free**, in which case the content takes its natural
405    /// size, according to `free_x` and `free_y`.
406    pub fn compute_scroll(
407        &mut self,
408        root: NodeId,
409        width: f32,
410        height: f32,
411        free_x: bool,
412        free_y: bool,
413    ) {
414        // **Fill the constrained axis**: the content of a **single-axis** scrollable takes
415        // the viewport's size on the **cross** (constrained) axis, instead of hugging its
416        // content. Without this a `flex(1)` or `Percent` child on that axis would collapse
417        // for want of a definite basis, and the app would have to insert a "filler"
418        // container. We only touch the axis when it is **constrained while the other is
419        // free** — true single-axis scrolling, neither definite layout (both constrained)
420        // nor 2D scrolling (both free) — and **only** when the root dimension is `Auto`,
421        // since an explicit size choice is respected.
422        let fill_w = !free_x && free_y;
423        let fill_h = !free_y && free_x;
424        self.compute_each(root, width, height, [free_x, free_y], [fill_w, fill_h]);
425    }
426
427    /// Computes the layout with each axis said separately: **free** (the content takes its
428    /// natural size), or bounded by `width` / `height` and then either **filled** (an unset
429    /// root dimension takes the bound) or not (the bound is only the most it may take).
430    ///
431    /// The other `compute*` are fixed choices of these: [`Layout::compute_filled`] fills both
432    /// bounds, [`Layout::compute_scroll`] fills a lone bound, and [`Layout::compute`] fills
433    /// none. A box that hands its child the room on one axis and only allows it on the other
434    /// needs the choice per axis.
435    ///
436    /// A bound that is not filled is still **the most** the root may take: a root with no
437    /// size of its own that came out past it — a container whose paragraph was measured on
438    /// one line — is laid out again at the bound, where the paragraph wraps. A root with a
439    /// size of its own keeps it, and spills.
440    pub fn compute_axes(
441        &mut self,
442        root: NodeId,
443        width: f32,
444        height: f32,
445        free: [bool; 2],
446        fill: [bool; 2],
447    ) {
448        self.compute_each(root, width, height, free, fill);
449        let size = self.size_of(root);
450        let past =
451            |free: bool, fill: bool, came: f32, bound: f32| !free && !fill && came > bound + 0.5;
452        let over = [
453            past(free[0], fill[0], size.width, width),
454            past(free[1], fill[1], size.height, height),
455        ];
456        if over[0] || over[1] {
457            let fill = [fill[0] || over[0], fill[1] || over[1]];
458            self.compute_each(root, width, height, free, fill);
459        }
460    }
461
462    /// [`Layout::compute_axes`] without the second look at a bound that was not filled.
463    fn compute_each(
464        &mut self,
465        root: NodeId,
466        width: f32,
467        height: f32,
468        free: [bool; 2],
469        fill: [bool; 2],
470    ) {
471        let [free_x, free_y] = free;
472        let fill_w = fill[0] && !free_x;
473        let fill_h = fill[1] && !free_y;
474        if fill_w || fill_h {
475            if let Ok(current) = self.tree.style(root) {
476                let mut style = current.clone();
477                let mut changed = false;
478                if fill_w && matches!(style.size.width, taffy::Dimension::Auto) {
479                    style.size.width = taffy::Dimension::Length(width);
480                    changed = true;
481                }
482                if fill_h && matches!(style.size.height, taffy::Dimension::Auto) {
483                    style.size.height = taffy::Dimension::Length(height);
484                    changed = true;
485                }
486                if changed {
487                    let _ = self.tree.set_style(root, style);
488                }
489            }
490        }
491
492        let axis = |free: bool, size: f32| {
493            if free {
494                AvailableSpace::MaxContent
495            } else {
496                AvailableSpace::Definite(size)
497            }
498        };
499        self.compute_in(
500            root,
501            taffy::Size {
502                width: axis(free_x, width),
503                height: axis(free_y, height),
504            },
505        );
506    }
507
508    /// The shared computation: taffy, with measured leaves' **constrained
509    /// measurements** routed to their closures.
510    fn compute_in(&mut self, root: NodeId, available: taffy::Size<AvailableSpace>) {
511        let measures = &self.measures;
512        self.tree
513            .compute_layout_with_measure(
514                root,
515                available,
516                |known: taffy::Size<Option<f32>>,
517                 space: taffy::Size<AvailableSpace>,
518                 node,
519                 _ctx,
520                 _style| {
521                    let Some(measure) = measures.get(&node) else {
522                        return taffy::Size::ZERO;
523                    };
524                    // The constraint resolved per axis: the known dimension if taffy
525                    // has already settled it, otherwise from the space offered
526                    // (min-content = as narrow as possible, 0; max-content = free).
527                    let bound = |known: Option<f32>, space: AvailableSpace| {
528                        known.or(match space {
529                            AvailableSpace::Definite(v) => Some(v),
530                            AvailableSpace::MinContent => Some(0.0),
531                            AvailableSpace::MaxContent => None,
532                        })
533                    };
534                    let (bw, bh) = (
535                        bound(known.width, space.width),
536                        bound(known.height, space.height),
537                    );
538                    let size = measure(bw, bh);
539                    debug_assert_measurement(size.width, bw, "width");
540                    debug_assert_measurement(size.height, bh, "height");
541                    taffy::Size {
542                        width: size.width,
543                        height: size.height,
544                    }
545                },
546            )
547            .expect("computing the layout");
548    }
549
550    /// The size a node came out at, after a `compute*`.
551    ///
552    /// [`Layout::absolute_rects`] answers the same question for a whole subtree and
553    /// allocates one entry per node to do it; this is for the one caller that wants a
554    /// single box back — a measurement asking how big the thing it just laid out is.
555    pub fn size_of(&self, node: NodeId) -> Size {
556        self.tree
557            .layout(node)
558            .map(|l| Size::new(l.size.width, l.size.height))
559            .unwrap_or(Size::new(0.0, 0.0))
560    }
561
562    /// Walks the tree in prefix order and returns, for each node, its rectangle in
563    /// **absolute** coordinates along with any associated data.
564    ///
565    /// taffy expresses positions relative to the parent; here we accumulate the
566    /// offsets to get absolute coordinates that can be rendered directly.
567    pub fn absolute_rects(&self, root: NodeId) -> Vec<(Rect, Option<&T>)> {
568        let mut out = Vec::new();
569        self.collect(root, 0.0, 0.0, &mut out);
570        out
571    }
572
573    /// Every box in the tree whose children spill out of it, with the edge and the
574    /// amount — the measurement behind [`Overflowing`].
575    ///
576    /// Only within **one** taffy tree, which is exactly the useful scope: a scrollable, a
577    /// stack, a page view, a fitter and an overflow box are all laid out as leaves here
578    /// and their content computed separately, so content larger than its viewport — the
579    /// one overflow that is deliberate — never reaches this walk.
580    pub fn overflows(&self, root: NodeId) -> Vec<Overflowing> {
581        let mut out = Vec::new();
582        self.collect_overflow(root, 0.0, 0.0, &mut out);
583        out
584    }
585
586    fn collect_overflow(
587        &self,
588        node: NodeId,
589        offset_x: f32,
590        offset_y: f32,
591        out: &mut Vec<Overflowing>,
592    ) {
593        // The **unrounded** layout, which is the only one the question can be asked of.
594        // taffy rounds each node's edges to whole pixels independently, so a box 169.6
595        // tall whose child sits at 21.6 and is 148 tall becomes a box of 169 with a child
596        // at 22: one pixel of overflow that exists nowhere but in the rounding, reported
597        // against a widget nobody wrote wrong. Four of this framework's own goldens wore a
598        // band for it the day the band was painted.
599        let layout = self.tree.unrounded_layout(node);
600        let x = offset_x + layout.location.x;
601        let y = offset_y + layout.location.y;
602
603        let child_count = self.tree.child_count(node);
604        if child_count > 0 {
605            // The content box, in the node's own coordinates. Taffy places children
606            // relative to the border box, so these bounds are directly comparable.
607            let left = layout.border.left + layout.padding.left;
608            let top = layout.border.top + layout.padding.top;
609            let right = layout.size.width - layout.border.right - layout.padding.right;
610            let bottom = layout.size.height - layout.border.bottom - layout.padding.bottom;
611
612            let (mut over_l, mut over_t, mut over_r, mut over_b) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
613            for i in 0..child_count {
614                let child = self.tree.child_at_index(node, i).expect("the node's child");
615                let c = self.tree.unrounded_layout(child);
616                over_l = over_l.max(left - c.location.x);
617                over_t = over_t.max(top - c.location.y);
618                over_r = over_r.max(c.location.x + c.size.width - right);
619                over_b = over_b.max(c.location.y + c.size.height - bottom);
620            }
621
622            // Sub-pixel slack. Rounding a fractional text width against a fractional box
623            // produces overflows of a few hundredths that no one can see and nobody
624            // should be told about; the reference has the same tolerance for the same
625            // reason.
626            const TOLERANCE: f32 = 0.5;
627            let rect = Rect::new(x, y, layout.size.width, layout.size.height);
628            for (amount, side) in [
629                (over_l, Side::Left),
630                (over_t, Side::Top),
631                (over_r, Side::Right),
632                (over_b, Side::Bottom),
633            ] {
634                if amount > TOLERANCE {
635                    out.push(Overflowing { rect, side, amount });
636                }
637            }
638        }
639
640        for i in 0..child_count {
641            let child = self.tree.child_at_index(node, i).expect("the node's child");
642            self.collect_overflow(child, x, y, out);
643        }
644    }
645
646    fn collect<'s>(
647        &'s self,
648        node: NodeId,
649        offset_x: f32,
650        offset_y: f32,
651        out: &mut Vec<(Rect, Option<&'s T>)>,
652    ) {
653        let layout = self.tree.layout(node).expect("the node's layout");
654        let x = offset_x + layout.location.x;
655        let y = offset_y + layout.location.y;
656
657        let rect = Rect::new(x, y, layout.size.width, layout.size.height);
658        out.push((rect, self.tree.get_node_context(node)));
659
660        let child_count = self.tree.child_count(node);
661        for i in 0..child_count {
662            let child = self.tree.child_at_index(node, i).expect("the node's child");
663            self.collect(child, x, y, out);
664        }
665    }
666}
667
668impl<T> Default for Layout<'_, T> {
669    fn default() -> Self {
670        Self::new()
671    }
672}
673
674#[cfg(test)]
675mod tests {
676
677    /// A row that does not fit says so, on the side it ran past and by how much.
678    #[test]
679    fn a_row_too_small_for_its_children_reports_it() {
680        let mut layout: Layout<'_, ()> = Layout::new();
681        let a = layout.leaf(
682            Style {
683                width: Dimension::Length(80.0),
684                flex_shrink: 0.0,
685                ..Default::default()
686            },
687            (),
688        );
689        let b = layout.leaf(
690            Style {
691                width: Dimension::Length(80.0),
692                flex_shrink: 0.0,
693                ..Default::default()
694            },
695            (),
696        );
697        let row = layout.container(
698            Style {
699                width: Dimension::Length(100.0),
700                height: Dimension::Length(20.0),
701                ..Default::default()
702            },
703            &[a, b],
704        );
705        layout.compute(row, Size::new(200.0, 200.0));
706        let over = layout.overflows(row);
707        assert_eq!(over.len(), 1, "one edge, once: {over:?}");
708        assert_eq!(over[0].side, Side::Right);
709        assert_eq!(over[0].amount, 60.0);
710        assert_eq!(over[0].rect.width, 100.0, "the box named is the parent's");
711    }
712
713    /// Padding is part of the box the children have to fit inside.
714    #[test]
715    fn padding_counts_as_room_taken() {
716        let mut layout: Layout<'_, ()> = Layout::new();
717        let child = layout.leaf(
718            Style {
719                width: Dimension::Length(100.0),
720                flex_shrink: 0.0,
721                ..Default::default()
722            },
723            (),
724        );
725        let boxed = layout.container(
726            Style {
727                width: Dimension::Length(100.0),
728                height: Dimension::Length(20.0),
729                padding: frus_core::Insets::uniform(10.0),
730                ..Default::default()
731            },
732            &[child],
733        );
734        layout.compute(boxed, Size::new(200.0, 200.0));
735        let over = layout.overflows(boxed);
736        assert_eq!(over.len(), 1);
737        assert_eq!(over[0].amount, 20.0, "10 px of padding on either side");
738    }
739
740    /// And a row that fits says nothing — including when the arithmetic lands a hair off,
741    /// which fractional text widths do constantly.
742    #[test]
743    fn a_row_that_fits_is_silent() {
744        let mut layout: Layout<'_, ()> = Layout::new();
745        let a = layout.leaf(
746            Style {
747                width: Dimension::Length(50.2),
748                flex_shrink: 0.0,
749                ..Default::default()
750            },
751            (),
752        );
753        let b = layout.leaf(
754            Style {
755                width: Dimension::Length(50.1),
756                flex_shrink: 0.0,
757                ..Default::default()
758            },
759            (),
760        );
761        let row = layout.container(
762            Style {
763                width: Dimension::Length(100.0),
764                height: Dimension::Length(20.0),
765                ..Default::default()
766            },
767            &[a, b],
768        );
769        layout.compute(row, Size::new(200.0, 200.0));
770        assert!(layout.overflows(row).is_empty(), "0.3 px is nobody's bug");
771    }
772    use super::*;
773    use crate::{Align, Dimension, FlexDirection, Justify, Style};
774    use frus_core::Insets;
775
776    #[test]
777    fn flex_row_computes_absolute_positions() {
778        let mut layout: Layout<'_, ()> = Layout::new();
779
780        // Child A: fixed width 120. Child B: flex_grow 1, filling the rest.
781        let a = layout.leaf(
782            Style {
783                width: Dimension::Length(120.0),
784                ..Default::default()
785            },
786            (),
787        );
788        let b = layout.leaf(
789            Style {
790                flex_grow: 1.0,
791                ..Default::default()
792            },
793            (),
794        );
795        let root = layout.container(
796            Style {
797                width: Dimension::Length(400.0),
798                height: Dimension::Length(100.0),
799                flex_direction: FlexDirection::Row,
800                padding: Insets::uniform(10.0),
801                gap: 8.0,
802                ..Default::default()
803            },
804            &[a, b],
805        );
806
807        layout.compute(root, Size::new(400.0, 100.0));
808        let rects = layout.absolute_rects(root);
809
810        // Prefix walk: [root, a, b].
811        let (root_rect, _) = rects[0];
812        let (a_rect, _) = rects[1];
813        let (b_rect, _) = rects[2];
814
815        assert_eq!(root_rect, Rect::new(0.0, 0.0, 400.0, 100.0));
816
817        // The content area: 10 of padding on every side.
818        // A: x = 10, width 120, stretched in height (align stretch) => 80, y = 10.
819        assert_eq!(a_rect, Rect::new(10.0, 10.0, 120.0, 80.0));
820
821        // B: after A plus the gap => x = 10 + 120 + 8 = 138.
822        // Width = content(380) - A(120) - gap(8) = 252.
823        assert_eq!(b_rect, Rect::new(138.0, 10.0, 252.0, 80.0));
824    }
825
826    #[test]
827    fn flex_wrap_moves_overflowing_child_to_next_line() {
828        let mut layout: Layout<'_, ()> = Layout::new();
829
830        // Three 80px children in a 200px container: 80+80 fit on the first line, and
831        // the third (240 > 200) wraps to the next.
832        let kids: Vec<_> = (0..3)
833            .map(|_| {
834                layout.leaf(
835                    Style {
836                        width: Dimension::Length(80.0),
837                        height: Dimension::Length(40.0),
838                        ..Default::default()
839                    },
840                    (),
841                )
842            })
843            .collect();
844        let root = layout.container(
845            Style {
846                width: Dimension::Length(200.0),
847                height: Dimension::Length(200.0),
848                flex_direction: FlexDirection::Row,
849                align: Align::Start,
850                flex_wrap: true,
851                ..Default::default()
852            },
853            &kids,
854        );
855
856        layout.compute(root, Size::new(200.0, 200.0));
857        let rects = layout.absolute_rects(root);
858        // Prefix walk: [root, k0, k1, k2].
859        let (k0, _) = rects[1];
860        let (k1, _) = rects[2];
861        let (k2, _) = rects[3];
862
863        // k0 and k1 on the first line; k2 comes back to the left, lower down.
864        assert_eq!(k0.y, k1.y);
865        assert_eq!(k2.x, k0.x);
866        assert!(k2.y >= k0.y + 40.0, "k2.y = {}, expected >= 40", k2.y);
867    }
868
869    #[test]
870    fn measured_leaf_wraps_to_the_offered_width() {
871        // Simulates a 250px text that wraps: given a width W, it occupies min(W, 250)
872        // across and grows in height when it wraps.
873        let measure: crate::MeasureFn<'_> = Box::new(|w, _| {
874            let w = w.unwrap_or(250.0).min(250.0);
875            let lines = (250.0 / w).ceil();
876            Size::new(w, lines * 20.0)
877        });
878        let mut layout: Layout<'_, ()> = Layout::new();
879        let text = layout.measured_leaf(Style::default(), (), measure);
880        let root = layout.container(
881            Style {
882                width: Dimension::Length(100.0),
883                flex_direction: FlexDirection::Column,
884                align: Align::Start,
885                ..Default::default()
886            },
887            &[text],
888        );
889        layout.compute(root, Size::new(100.0, 600.0));
890        let rects = layout.absolute_rects(root);
891        let (text_rect, _) = rects[1];
892        assert!(
893            text_rect.width <= 100.0,
894            "wrapped to the offered width: {text_rect:?}"
895        );
896        assert!(
897            text_rect.height >= 60.0,
898            "3 lines expected (250/100 → 3 × 20): {text_rect:?}"
899        );
900    }
901
902    /// The same measured leaf, but **centred** on the cross axis instead of
903    /// stretched. Centring means the item is sized to fit rather than filled, and the
904    /// height must still be the one that goes with the width it ends up at.
905    #[test]
906    fn a_centred_measured_leaf_reports_the_height_of_the_width_it_got() {
907        let measure: crate::MeasureFn<'_> = Box::new(|w, _| {
908            let w = w.unwrap_or(250.0).min(250.0);
909            let lines = (250.0 / w).ceil();
910            Size::new(w, lines * 20.0)
911        });
912        let mut layout: Layout<'_, ()> = Layout::new();
913        let text = layout.measured_leaf(Style::default(), (), measure);
914        let root = layout.container(
915            Style {
916                width: Dimension::Length(100.0),
917                flex_direction: FlexDirection::Column,
918                align: Align::Center,
919                ..Default::default()
920            },
921            &[text],
922        );
923        layout.compute(root, Size::new(100.0, 600.0));
924        let rects = layout.absolute_rects(root);
925        let (text_rect, _) = rects[1];
926        assert!(
927            text_rect.width <= 100.0,
928            "wrapped to the offered width: {text_rect:?}"
929        );
930        assert!(
931            text_rect.height >= 60.0,
932            "3 lines expected (250/100 -> 3 x 20): {text_rect:?}"
933        );
934    }
935
936    #[test]
937    fn justify_center_centers_child() {
938        let mut layout: Layout<'_, ()> = Layout::new();
939        let child = layout.leaf(
940            Style {
941                width: Dimension::Length(100.0),
942                height: Dimension::Length(40.0),
943                align: Align::Start,
944                ..Default::default()
945            },
946            (),
947        );
948        let root = layout.container(
949            Style {
950                width: Dimension::Length(400.0),
951                height: Dimension::Length(100.0),
952                flex_direction: FlexDirection::Row,
953                justify: Justify::Center,
954                ..Default::default()
955            },
956            &[child],
957        );
958        layout.compute(root, Size::new(400.0, 100.0));
959        let rects = layout.absolute_rects(root);
960
961        // A child centred on the main axis: x = (400 - 100) / 2 = 150.
962        assert_eq!(rects[1].0, Rect::new(150.0, 0.0, 100.0, 40.0));
963    }
964
965    /// **A measurer that rounds to nearest is caught.** This is the whole point of the
966    /// rule: the failure it prevents is a clipped glyph three crates away, and the thing
967    /// that reintroduces it is one `round()` written by reflex where a `ceil()` belonged.
968    ///
969    /// Debug builds only, which is where tests run.
970    #[test]
971    #[should_panic(expected = "not a whole number of pixels")]
972    fn a_measurement_that_is_not_whole_is_caught() {
973        let mut layout: Layout<'_, ()> = Layout::new();
974        let node = layout.measured_leaf(
975            Style::default(),
976            (),
977            // 146.4, the number from milestone 289, reported as it came.
978            Box::new(|_, _| Size::new(146.4, 20.0)),
979        );
980        layout.compute(node, Size::new(400.0, 400.0));
981    }
982
983    /// And one that goes through the helper is not. The pair is the test: an assertion
984    /// that fired on everything would be turned off within a week.
985    #[test]
986    fn a_measurement_put_through_the_helper_passes() {
987        let mut layout: Layout<'_, ()> = Layout::new();
988        let node = layout.measured_leaf(
989            Style::default(),
990            (),
991            Box::new(|_, _| Size::new(frus_core::fits(146.4), frus_core::fits(20.0))),
992        );
993        layout.compute(node, Size::new(400.0, 400.0));
994        let size = layout.size_of(node);
995        assert_eq!(size.width, 147.0, "rounded up, so the box still fits it");
996    }
997
998    /// **A measurement clamped to its constraint is exempt.** The bound is the caller's
999    /// number, not the measurer's: rounding it up would hand back a box a fraction wider
1000    /// than the space offered, which is a different bug. Clamp after rounding up, never
1001    /// before — and this is what says the rule knows the difference.
1002    #[test]
1003    fn a_measurement_clamped_to_a_fractional_bound_is_not_a_violation() {
1004        let mut layout: Layout<'_, ()> = Layout::new();
1005        let node = layout.measured_leaf(
1006            Style::default(),
1007            (),
1008            Box::new(|w, _| Size::new(frus_core::fits(500.0).min(w.unwrap_or(0.0)), 20.0)),
1009        );
1010        // A fractional offer, which is what an intermediate flex computation hands down.
1011        layout.compute(node, Size::new(146.4, 400.0));
1012    }
1013
1014    /// **A measurement larger than the space it was offered is not a violation either.**
1015    /// This framework lets content overflow and reports it (`Overflowing`) rather than
1016    /// crushing it, so a measurer answering with its natural size is doing its job. Said
1017    /// as a test because the obvious assertion to add here is the one that would break
1018    /// every screen with a line too long for its column.
1019    #[test]
1020    fn a_measurement_bigger_than_the_offer_is_reported_not_forbidden() {
1021        let mut layout: Layout<'_, ()> = Layout::new();
1022        let node = layout.measured_leaf(
1023            Style::default(),
1024            (),
1025            Box::new(|_, _| Size::new(500.0, 20.0)),
1026        );
1027        layout.compute(node, Size::new(100.0, 100.0));
1028        assert_eq!(layout.size_of(node).width, 500.0);
1029    }
1030
1031    /// **Overlapping children share one cell**: the box is the largest of them on each
1032    /// axis — not their sum, as a line would make it — and a smaller one is centred in it.
1033    /// Handed a bigger box, the cell fills it and centres both in that.
1034    #[test]
1035    fn overlapping_children_share_one_cell_the_size_of_the_largest() {
1036        let place = |width: Dimension| {
1037            let mut layout: Layout<'_, u8> = Layout::new();
1038            let wide = layout.leaf(
1039                Style {
1040                    width: Dimension::Length(80.0),
1041                    height: Dimension::Length(20.0),
1042                    ..Default::default()
1043                },
1044                1,
1045            );
1046            let tall = layout.leaf(
1047                Style {
1048                    width: Dimension::Length(30.0),
1049                    height: Dimension::Length(50.0),
1050                    ..Default::default()
1051                },
1052                2,
1053            );
1054            let cell = layout.container(
1055                Style {
1056                    width,
1057                    overlap: true,
1058                    align: crate::style::Align::Center,
1059                    ..Default::default()
1060                },
1061                &[wide, tall],
1062            );
1063            layout.compute(cell, Size::new(400.0, 400.0));
1064            let rects = layout.absolute_rects(cell);
1065            let at = |tag: u8| {
1066                let r = rects.iter().find(|(_, d)| *d == Some(&tag)).unwrap().0;
1067                (r.x, r.y)
1068            };
1069            (layout.size_of(cell), at(1), at(2))
1070        };
1071
1072        let (size, wide, tall) = place(Dimension::Auto);
1073        assert_eq!(
1074            (size.width, size.height),
1075            (80.0, 50.0),
1076            "the largest, per axis"
1077        );
1078        assert_eq!(wide, (0.0, 15.0), "the wide one centred down the cell");
1079        assert_eq!(tall, (25.0, 0.0), "the tall one centred across it");
1080
1081        let (size, wide, tall) = place(Dimension::Length(200.0));
1082        assert_eq!(size.width, 200.0);
1083        assert_eq!(wide.0, 60.0, "a box handed over is filled, and centred in");
1084        assert_eq!(tall.0, 85.0);
1085    }
1086}