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}