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}