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