Files
iris/tests/cases/layout.rs
T
iris-aiandClaude Opus 5 de1eb7e406 Hold what a widget answers with a rule, and its box with a widget
Bryan's call, given the measurements in `76aaf06`: `SizeRule::{Min, Max,
Clamp}` holds the length a widget answers and never touches the box it draws
in, and `MaxSize` is the box version.

The split is the difference between a rule and a widget here. A box is
whoever asked's to decide, and the retained machinery hands a widget one by
paths that never ask it anything -- a parent re-placing a child, a subtree
repositioned after its parent's box moved. A rule that read the box was
therefore decided again by whichever path arrived last, which is what the
oracle was refusing. A widget has no such trouble: it is drawn again whenever
its own box changes, so `MaxSize` asks `longer_than` where the answer can be
kept, and `region_len` pins the box lengths its drawing holds for.

What that costs is nothing the app wanted: `a_capped_scroll_takes_its_
viewport_from_the_cap` puts 400 px of content under `.max_height(100)` and
gets a 100 px viewport with 300 to scroll, which is what `MaxSize` gave on the
app's pin, and `.max_width`/`.max_height` are that widget rather than a rule.
A cap narrows the offer and not a declared length, so a child that declares
500 px still draws 500 and the cap holds what `MaxSize` itself answers; a
child that asked for a share takes the box the cap allows and the share passes
up, since whoever divides one is `MaxSize`'s parent.

`.min_width`/`.min_height` stay a rule: answering at least so much is a claim
about the length, and a row honours it without anyone narrowing anything.

Bounds in the generated trees are pixels for now, with the reason written
where the next tree is grown: a fraction in a bound is resolved against the
rel base the widget was asked with, and `place_at` hands a parent a retained
answer without checking that it still holds for the rel base this place
gives. Seeds 4 and 196 at depth 5 are where that showed. The hole is older
than bounds -- an `Exact` rule that is a fraction can reach it too -- and
closing it is a check at the re-place site rather than anything about bounds.
A fraction through `MaxSize` is fine and tested, since the widget compares
against its own box.

Format, clippy with and without layout-diagnostics, and the suite (142 + 19 +
13 + 4) are clean. All three seed scans pass: 400 at depth 5 (62s), 1,000 at
depth 6 (162s), 2,000 at depth 4 (299s). The cold dump is 34,986 boxes and
moves wholesale against `2dba90b`, which is the generator growing rules it
did not grow before rather than a layout change; it is the new baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:13:46 -04:00

1119 lines
44 KiB
Rust

//! Where a frame puts things, with no window to put them in.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
let left = rect(Color::RED).width(100).add(&mut h.rsc);
let right = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
(left.id(), right.id())
}
#[test]
fn a_span_gives_each_child_the_width_it_asked_for() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
assert_corners!(h, left, (0, 0), (100, 200));
assert_corners!(h, right, (100, 0), (400, 200));
}
/// A span places each child in the room left after the one before, because a
/// text has to wrap at the width actually there, but the child's region is
/// the whole row. So two children asking for half each take the whole row
/// between them, however much of it was left when each was asked, and a third
/// overflows -- and a span passes its own region on unchanged, so a child of
/// a nested span asking for half asks for half of the same row.
#[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported, and its own child
// asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100));
}
/// The same fraction either way round: after a 100 px child in a 400 px row,
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
/// drew half of what it was offered and reported that. Half the row, not half
/// of the 300 px left of it.
#[test]
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
let mut declaring = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(declaring, declared, (100, 0), (300, 100));
let mut reporting = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(reporting, reported, (100, 0), (300, 100));
}
/// What the fraction a child reports is of and what box it is offered are
/// two different lengths, and only the first is the whole row: a text still
/// wraps at the room actually left after its neighbour, so the same
/// paragraph is taller where less of the row is left for it.
#[test]
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
let paragraph = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer.";
let height_after = |head_width: i32| {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
let region = h.region(&text).unwrap();
(region.bot_right.y - region.top_left.y).to_f32()
};
let (crowded, whole_row) = (height_after(300), height_after(0));
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// Padding is an inset: it narrows the frame a fraction resolves against and
/// adds itself back to the padded widget's reported length.
#[test]
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
// Ruled to the window: a root reporting a fraction of it is otherwise
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
}
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.";
/// The worked example of what padding insets: in a 900 px row after a 24 px
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
/// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there.
#[test]
fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let fill_width = h.region(&fill).unwrap().size().x;
assert_eq!(fill_width, Px::from_int(868));
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
let asked = active.region.x.len().to_px(window);
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844));
}
/// The other way round: a share inside padding. A slot is a length of the
/// row, which is already the padded width, so what the span decided reaches
/// the child as it stands -- taking the padding off a second time would make
/// `rel(1.0)` in the slot shorter than the slot.
#[test]
fn a_share_inside_padding_fills_the_slot_it_was_given() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let first = Span {
children: vec![fill.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.width(leftover(1))
.add(&mut h.rsc);
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row.pad(16));
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
}
/// The same padding in a share instead: the slot is 450, so both the
/// fraction and the wrap are the slot less the padding, and the two agree.
#[test]
fn padding_narrows_both_rel_base_and_box_inside_a_share() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
let mut h = Harness::new((900, 200));
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
}
#[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200));
let child = rect(Color::RED).height(40).add(&mut h.rsc);
let span = (child,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
}
#[test]
fn a_span_reports_its_tallest_fixed_child() {
let mut h = Harness::new((400, 200));
let short = rect(Color::RED).height(40).add(&mut h.rsc);
let tall = rect(Color::BLUE).height(70).add(&mut h.rsc);
let span = (short, tall).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::px(70.0));
}
#[test]
fn resizing_relays_out_against_the_new_output() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100));
}
#[test]
fn an_empty_widget_takes_a_share_of_a_span() {
let mut h = Harness::new((400, 200));
let gap = ().add(&mut h.rsc);
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((gap, right).span(Dir::RIGHT));
assert_corners!(h, gap, (0, 0), (300, 200));
assert_corners!(h, right, (300, 0), (400, 200));
}
/// A widget with a natural pixel size, like an image, which records the box
/// it was asked in so a test can see which length decided it.
struct NaturalSize {
len: f32,
asked: Rc<Cell<f32>>,
}
impl Widget for NaturalSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.asked.set(painter.px_len(Axis::X).to_f32());
Size::px(Vec2::new(self.len, self.len))
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(self.len))
}
}
/// A rule wins over what the widget says about itself, and a share is a rule:
/// it is a length only to whoever divides one, and nobody here does, so the
/// widget is asked in the whole box rather than in the size it asked for.
#[test]
fn a_share_rule_beats_the_widgets_own_pixel_size() {
let mut h = Harness::new((400, 200));
let asked = Rc::new(Cell::new(0.0));
let natural = NaturalSize {
len: 50.0,
asked: asked.clone(),
}
.add(&mut h.rsc);
h.set_root(natural.wrapper());
assert_eq!(asked.get(), 50.0, "its hint gives it its own size");
h.set_len(natural, Axis::X, LayoutLen::LEFTOVER);
h.frame();
assert_eq!(asked.get(), 400.0, "the share is all of the box");
}
/// Every box a widget is given comes of one ask, and the window is one of
/// them: the root is asked in it exactly as a child is asked in its parent's
/// box, so a rule of its own reads the same way at either place.
#[derive(Clone, Copy, Debug)]
enum Asked {
Root,
Wrapped,
InASpan,
}
impl Asked {
const ALL: [Self; 3] = [Self::Root, Self::Wrapped, Self::InASpan];
/// The width the probe is given under this parent, in a 400 px window.
fn width(&self, rule: LayoutLen) -> Px {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, rule);
match self {
Self::Root => h.set_root(probe),
Self::Wrapped => h.set_root(probe.wrapper()),
Self::InASpan => h.set_root((probe,).span(Dir::RIGHT)),
}
h.region(&probe).unwrap().size().x
}
}
/// A share with pixels or a fraction beside it is the longer of the two: it
/// fills what they leave of the box and overflows the box where they are
/// longer than it. A parent that divides nothing gives the same length as a
/// span with one child, because in both there is nobody else to divide with --
/// and so does the window, which divides nothing either.
#[test]
fn a_share_is_a_minimum_wherever_nothing_divides_it() {
for (rule, want) in [
(LayoutLen::LEFTOVER, 400),
(LayoutLen::px(50.0) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500.0) + LayoutLen::LEFTOVER, 500),
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
(LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800),
(LayoutLen::px(500.0), 500),
] {
let want = Px::from_int(want);
for asked in Asked::ALL {
assert_eq!(asked.width(rule), want, "{rule:?} asked {asked:?}");
}
}
}
/// Which of the two is longer is a question in pixels, so the box is decided
/// again wherever the answer can change: a window that crosses the length the
/// pixels ask for, and the rule itself crossing it while the window holds
/// still. The first is a range the drawing holds for; the second cannot be
/// seen in what the widget declares, since a share declares nothing either
/// way, so it reaches the parent as a length only the parent can resolve.
#[test]
fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() {
// At the root as well as under a parent: the comparison is the same one,
// and nothing above the root will make it again on its behalf, so the
// range it holds for is the root's own.
for wrapped in [false, true] {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
match wrapped {
true => h.set_root(probe.wrapper()),
false => h.set_root(probe),
}
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
h.resize((900, 200));
h.frame();
assert_eq!(width(&h), Px::from_int(900), "wrapped: {wrapped}");
h.resize((400, 200));
h.frame();
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
h.set_len(probe, Axis::X, LayoutLen::px(50.0) + LayoutLen::LEFTOVER);
h.frame();
assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}");
h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
h.frame();
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
}
}
#[test]
fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200));
// The span measures a child and then places it; listing it twice would
// move it twice. The span's own fixed total is shorter than the window,
// so the span is centred in it and everything under it carries that.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let centered = inner.center().width(200).add(&mut h.rsc);
let left = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (150, 0), (350, 200));
h.set_len(left, Axis::X, 150);
h.frame();
assert_corners!(h, inner, (175, 0), (375, 200));
}
#[test]
fn alignment_accepts_an_arbitrary_fraction_and_changes_at_runtime() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).sized((100, 100)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::X, AxisAlign::new(0.25));
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::Y, AxisAlign::NEG);
h.set_root(fixed);
assert_corners!(h, fixed, (75, 0), (175, 100));
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::X, AxisAlign::new(0.75));
h.frame();
assert_corners!(h, fixed, (225, 0), (325, 100));
}
#[test]
fn a_resize_lands_where_a_cold_start_would() {
let build = |h: &mut Harness| {
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves room \
for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.pad(16)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
let root = (para, below).span(Dir::DOWN).pad(12);
h.set_root(root);
(para, below)
};
let mut cold = Harness::new((900, 1200));
let (cold_para, cold_below) = build(&mut cold);
let mut resized = Harness::new((1920, 1200));
let (para, below) = build(&mut resized);
resized.resize((900, 1200));
resized.frame();
assert_eq!(resized.region(&para), cold.region(&cold_para), "paragraph");
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
}
#[test]
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
let mut h = Harness::new((400, 400));
// The panel fills a stack sized by its sibling, so it is first asked in
// the whole box and then given the shorter one. Reusing it in that fixed
// box afterwards would leave it whatever height it happened to have.
let panel = rect(Color::BLUE).add(&mut h.rsc);
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
let stack = (panel, leaf)
.stack()
.size(StackSize::Child(1))
.add(&mut h.rsc);
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.set_len(leaf, Axis::Y, 250);
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
}
#[test]
fn a_moved_subtree_takes_its_children_with_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
// middle of what it was given.
h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 210), (390, 230));
h.set_len(first, Axis::Y, 80);
h.frame();
// The row opted into one movable region, so its descendants follow one
// entry rather than having their primitive regions rewritten.
assert_corners!(h, inner, (10, 230), (390, 250));
}
#[test]
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
let leftover = rect(Color::GREEN).add(&mut h.rsc);
let panel = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
// Changing the bar's width is the only thing that changes the box the
// panel and everything under it was drawn for.
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, panel).span(Dir::RIGHT));
assert_corners!(h, fixed, (100, 0), (150, 200));
assert_corners!(h, leftover, (150, 0), (400, 200));
h.set_len(bar, Axis::X, 200);
h.frame();
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
// against its new start and the one taking what is left absorbs the change.
assert_corners!(h, fixed, (200, 0), (250, 200));
assert_corners!(h, leftover, (250, 0), (400, 200));
}
#[test]
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
let mut h = Harness::new((400, 200));
// The row is 40 tall whatever happens, which used to make its drawing
// impossible to take out of: recovering a fraction of a box needs a
// relative extent, and it has none on that axis.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
let filler = rect(Color::GREEN).add(&mut h.rsc);
// This column is an item in a row, so it takes the width left for it
// rather than asking for a full row-width in addition to the bar.
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30));
h.set_len(bar, Axis::X, 200);
h.frame();
assert_corners!(h, inner, (210, 10), (390, 30));
}
#[test]
fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::BLUE).add(&mut h.rsc);
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, buried).span(Dir::RIGHT));
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
h.rsc.widgets_mut().set_region_node(buried, true);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(
h.render.moves.depth(move_idx),
1,
"the opted-in widget's region alone"
);
h.rsc.widgets_mut().set_region_node(buried, false);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 0);
}
/// A span that sizes from its children passes their `leftover` weight up
/// than collapsing it to one share, so nesting divides the same space instead
/// of re-dividing a share of it.
#[test]
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
}
}
/// The same space, unevenly nested: weights carried up mean a share is a
/// share of the whole, not of whatever branch a widget happens to sit in.
///
/// Each edge lands on the even division or one step below it, since a share
/// is a fraction of the room and a truncating multiply gives up what that
/// fraction does not divide. What stays exact is that each share starts
/// where the last one ended and the row ends at its own edge.
#[test]
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((one, three).span(Dir::RIGHT));
let mut start = Px::ZERO;
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let got = h.region(&id).expect("widget drew nothing");
let even = Px::from_int((i as i32 + 1) * 100);
assert_eq!(got.top_left, PxVec2::new(start, Px::ZERO), "share {i}");
assert_eq!(got.bot_right.y, Px::from_int(200), "share {i}");
assert!(
got.bot_right.x == even || got.bot_right.x == even.next_down(),
"share {i} ends at {:?}, not {even:?}",
got.bot_right.x
);
start = got.bot_right.x;
}
assert_eq!(
start,
Px::from_int(400),
"the row stopped short of its edge"
);
}
/// However many ways a row is divided, the shares add up to the row: each
/// one is the fixed parts before it plus a share of the room, rather than a
/// step from where the last one ended, so the roundings do not accumulate
/// along it. Chained, two hundred of them ended a step short of the edge.
#[test]
fn a_row_of_equal_shares_fills_it_exactly() {
for n in [2usize, 3, 7, 64, 200] {
let mut h = Harness::new((1000, 100));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..n {
let kid = rect(Color::RED).add(&mut h.rsc);
ids.push(kid.id());
kids.push(kid.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.set_root(span);
h.frame();
for (i, id) in ids.iter().enumerate() {
let at = h.region(id).expect("a share drew nothing").top_left.x;
let want = Px::from_f32(1000.0 * (i as f32) / (n as f32));
assert!(
(at - want).abs() <= Px::STEP,
"{n} shares: the {i}th starts at {at:?}, not {want:?}"
);
}
let end = h.region(ids.last().unwrap()).unwrap().bot_right.x;
assert_eq!(end, Px::from_int(1000), "{n} shares do not reach the edge");
}
}
/// Where the shader puts an edge: the fraction resolved against the window
/// plus the pixel offset, taken to the boundary it composes to within half
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.move_idx, active.placement);
let dim = h.size()[axis];
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region[axis];
(edge(span.start), edge(span.end))
}
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
let mark = rect(Color::RED).width(1).add_strong(&mut h.rsc);
marks.push(mark.id());
mark
}
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
h.set_len(&inner, Axis::X, LayoutLen::leftover(ratio));
inner
}
/// Shares in weights no binary fraction lands on, a padding on one branch
/// and not the other, so an edge falls near an integer as often as it can.
fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec<WidgetId>) -> StrongWidget {
let mut span = Span::empty(Dir::RIGHT);
if depth == 0 {
let left = rect(Color::BLUE).add_strong(&mut h.rsc);
let left = share(h, left, 3.0);
span.push(left);
let mark = hairline(h, marks);
span.push(mark);
let right = rect(Color::BLUE).add_strong(&mut h.rsc);
let right = share(h, right, 7.0);
span.push(right);
return span.add_strong(&mut h.rsc);
}
let first = hairlines(h, depth - 1, marks);
let first = share(h, first, 3.0);
span.push(first);
let second = hairlines(h, depth - 1, marks);
let second = Pad {
padding: Padding {
left: Px::from_int(3),
right: Px::from_int(7),
top: Px::ZERO,
bottom: Px::ZERO,
},
inner: second,
}
.add_strong(&mut h.rsc);
let second = share(h, second, 5.0);
span.push(second);
span.add_strong(&mut h.rsc)
}
/// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed
/// length share their box's fraction, so composing the chain moves them
/// together and the shader's `floor` cannot round the pixel between them
/// away -- only shift it. A separator that disappeared at one window size
/// would be a defect no size comparison catches.
#[test]
fn a_one_pixel_line_keeps_its_pixel_through_a_chain() {
let mut h = Harness::new((1920, 1200));
let mut marks = Vec::new();
let root = hairlines(&mut h, 4, &mut marks);
h.state.set_root(root);
h.frame();
assert_eq!(marks.len(), 16);
for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] {
h.resize(size);
h.frame();
for mark in &marks {
let (start, end) = drawn_edges(&h, *mark, Axis::X);
assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}");
}
}
}
/// A span short of room takes it from its shares, which go to nothing and
/// then to nothing wider; the fixed lengths between them keep their pixels.
/// Collapsing those to make room would delete a separator the caller asked
/// for, which is worse than overflowing.
#[test]
fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
let mut h = Harness::new((400, 20));
let mut marks = Vec::new();
let mut span = Span::empty(Dir::RIGHT);
for _ in 0..3 {
let share_of = rect(Color::BLUE).add_strong(&mut h.rsc);
let share_of = share(&mut h, share_of, 1.0);
span.push(share_of);
let mark = hairline(&mut h, &mut marks);
span.push(mark);
}
let root = span.add_strong(&mut h.rsc);
h.state.set_root(root);
h.frame();
for width in [400, 10, 3, 1] {
h.resize((width, 20));
h.frame();
for mark in &marks {
let (start, end) = drawn_edges(&h, *mark, Axis::X);
assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}");
}
}
}
#[test]
fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let leftover = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((fixed, leftover).span(Dir::RIGHT));
assert_corners!(h, fixed, (0, 0), (100, 20));
assert_eq!(h.region(&leftover), None);
// An undrawn child remains a dependency of the span, so making room for
// it draws it without rebuilding the tree.
h.set_len(fixed, Axis::X, 60);
h.frame();
assert_corners!(h, leftover, (60, 0), (100, 20));
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let mixed = rect(Color::BLUE)
.width(LayoutLen::px(20.0) + LayoutLen::LEFTOVER)
.add(&mut h.rsc);
h.set_root((fixed, mixed).span(Dir::RIGHT));
// Pixels and fractions still overflow; only a child whose entire length
// is leftover is omitted.
assert_corners!(h, mixed, (100, 0), (120, 20));
}
#[test]
fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
let mut h = Harness::new((100, 100));
let first = rect(Color::RED).height(90).add(&mut h.rsc);
let a = rect(Color::GREEN).add(&mut h.rsc);
let b = rect(Color::BLUE).add(&mut h.rsc);
let inner = (a, b).span(Dir::DOWN).gap(4).add(&mut h.rsc);
h.set_root((first, inner).span(Dir::DOWN));
assert!(h.region(&a).is_some());
assert!(h.region(&b).is_some());
h.set_len(first, Axis::Y, 96.0);
h.frame();
assert!(h.region(&a).is_none());
assert!(h.region(&b).is_none());
}
/// **A stack child smaller than the stack sits where its own alignment
/// says.** `Stack` gives every child the box its sizing child defines and
/// used to force the near edge on all of them; that override is owed only to
/// the sizing child, which has already placed its own content in the box the
/// stack derived from its answer. Every other child is handed a box that owes
/// nothing to it, so where it sits in one bigger than itself is its own
/// business -- and with the override it could not be aligned at all, which is
/// what moved the `tabs` example's counters to the wrong corner.
#[test]
fn a_stack_child_smaller_than_the_stack_keeps_its_own_alignment() {
let mut h = Harness::new((400, 200));
let big = rect(Color::BLUE).add(&mut h.rsc);
let small = rect(Color::RED).sized((50, 50)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(small.id(), Axis::X, AxisAlign::POS);
let (a, b) = (big.add_strong(&mut h.rsc), small.add_strong(&mut h.rsc));
let children: Vec<StrongWidget> = vec![a, b];
h.set_root(Stack {
children,
size: StackSize::Default,
});
assert_corners!(h, big, (0, 0), (400, 200));
// The far edge on X because it asked for it, the middle on Y because
// that is the default.
assert_corners!(h, small, (350, 75), (400, 125));
}
/// Five children of one span, buried under three containers that are each a
/// fraction of their parent so no length reaches the window without being
/// composed and rounded on the way. Returns each child's drawn width and
/// each gap between them, in pixels.
fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>, Vec<Px>) {
let mut h = Harness::new((box_w, 400.0));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..5 {
let r = rect(Color::RED).add(&mut h.rsc);
if let Some(len) = kid {
h.rsc
.widgets_mut()
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
}
ids.push(r.id());
kids.push(r.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::from_f32(gap),
}
.add(&mut h.rsc);
let a = (span.width(rel(0.9)),).span(Dir::RIGHT).add(&mut h.rsc);
let b = (a.width(rel(0.8)),).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((b.width(rel(0.7)),).span(Dir::RIGHT));
let boxes: Vec<_> = ids
.iter()
.map(|id| h.region(id).expect("a child drew nothing"))
.collect();
(
boxes.iter().map(|b| b.bot_right.x - b.top_left.x).collect(),
boxes
.windows(2)
.map(|p| p[1].top_left.x - p[0].bot_right.x)
.collect(),
)
}
/// **A length given in pixels is that many pixels, wherever it ends up.** A
/// gap and a declared width compose additively -- `Len::within` adds a part's
/// own pixels rather than scaling them, and both ends of a gap carry the same
/// fraction, so the multiply that rounds is the same on each -- which is why
/// nesting the row inside fractions of fractions cannot move them. Swept over
/// 2,100 box widths when this was written and exact at every one; five here,
/// including widths that divide badly by five.
#[test]
fn a_length_in_pixels_is_that_many_pixels_however_it_is_nested() {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let want = Px::from_int(7);
let (_, gaps) = row_under_fractions(None, 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between leftover children are {gaps:?}"
);
let (widths, gaps) = row_under_fractions(Some(LayoutLen::px(100.0)), 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between fixed children are {gaps:?}"
);
assert!(
widths.iter().all(|w| *w == Px::from_int(100)),
"box {box_w}: declared widths came out {widths:?}"
);
}
}
/// **Children asking for the same share of a row are not the same length**,
/// and this pins by how much rather than claiming they are equal. A position
/// is the quantity that gets rounded, so the row fills exactly and no two
/// children leave a seam; what that costs is a step or two between lengths
/// that were asked for identically. Exact composition would shrink the
/// spread, not remove it: five equal lengths cannot fill a row whose step
/// count is not a multiple of five.
#[test]
fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
for kid in [None, Some(LayoutLen::rel(0.2))] {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let (widths, gaps) = row_under_fractions(kid, 0.0, box_w);
let spread = *widths.iter().max().unwrap() - *widths.iter().min().unwrap();
assert!(
spread <= Px::from_raw(2),
"box {box_w}, {kid:?}: widths {widths:?} spread {spread:?}"
);
assert!(
gaps.iter().all(|g| *g == Px::ZERO),
"box {box_w}, {kid:?}: children left seams {gaps:?}"
);
}
}
}
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
#[test]
fn a_fixed_child_is_centered_in_its_wrappers_share() {
let mut h = Harness::new((600, 300));
let leaf = rect(Color::RED).sized((100, 100)).center().add(&mut h.rsc);
let wrapper = leaf
.wrapper()
.width(leftover(2))
.height(rel(1.0))
.add(&mut h.rsc);
let other = rect(Color::BLUE).width(200).add(&mut h.rsc);
h.set_root((other, wrapper).span(Dir::RIGHT));
assert_corners!(h, wrapper, (200, 0), (600, 300));
assert_corners!(h, leaf, (350, 100), (450, 200));
h.resize((900, 400));
h.frame();
assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250));
}
/// The root's frame is the window and its rule is a fraction of that, which
/// is one resolution and not two: nothing above it narrowed anything.
#[test]
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
let mut h = Harness::new((900, 200));
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
}
#[test]
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
for collapsed in [1, 2] {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).add(&mut h.rsc);
h.set_len(head, dir.axis, 200);
let tail = rect(Color::BLUE).add(&mut h.rsc);
let tail_len = 200 - 10 * (collapsed + 1);
h.set_len(tail, dir.axis, tail_len);
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
let mut shares = Vec::new();
for _ in 0..collapsed {
let share = rect(Color::GREEN).add(&mut h.rsc);
shares.push(share);
children.push(share.add_strong(&mut h.rsc));
}
children.push(tail.add_strong(&mut h.rsc));
h.set_root(Span {
children,
dir,
gap: Px::from_int(10),
});
for share in shares {
assert!(h.region(&share).is_none());
}
let region = h.region(&tail).unwrap();
let (from, to) = match dir.sign {
Sign::Pos => (400 - tail_len, 400),
Sign::Neg => (0, tail_len),
};
assert_eq!(region.top_left[dir.axis], Px::from_int(from));
assert_eq!(region.bot_right[dir.axis], Px::from_int(to));
}
}
}
/// The root is asked the way any child is, so what it says about itself is
/// read there too: a root that opted into a region node gets one, where the
/// path it used to have ignored the flag.
#[test]
fn a_region_node_root_is_a_region_node() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
let root = (probe,).span(Dir::RIGHT).region_node().add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400));
h.resize((900, 200));
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
}
/// A bound is a rule about what a widget answers: it holds the length that
/// reaches whoever asked and leaves the box alone. Here the content is 400
/// wide in a 250 window, so a cap cuts what the row reports and a floor
/// raises it, while the rects inside stay where the 250 box put them.
#[test]
fn a_bound_holds_what_a_widget_answers() {
let row = |rule: SizeRule| {
let mut h = Harness::new((250, 200));
let left = rect(Color::RED).width(200).add(&mut h.rsc);
let right = rect(Color::BLUE).width(200).add(&mut h.rsc);
let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rule(row, Axis::X, rule);
h.set_root(row);
(
h.region(&row).unwrap().size().x,
h.region(&left).unwrap().size().x,
)
};
let (capped, left) = row(SizeRule::Max(Len::px(300.0)));
assert_eq!(capped, Px::from_int(300), "the cap, not the 400 drawn");
assert_eq!(left, Px::from_int(200), "the box the children were given");
let (floored, _) = row(SizeRule::Min(Len::px(600.0)));
assert_eq!(floored, Px::from_int(600), "the floor, not the 400 drawn");
let (free, _) = row(SizeRule::Free);
assert_eq!(free, Px::from_int(400), "what it drew");
}
/// A cap on the box is `MaxSize`, which asks its child in the shorter of the
/// cap and its own box. That is the box a text wraps at and a scroll takes
/// its viewport from, so it cannot be had by holding the answer.
#[test]
fn a_cap_widget_asks_its_child_in_the_shorter_box() {
let mut h = Harness::new((400, 200));
// A fraction of its box, so it says what box it was asked in.
let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc);
let capped = fills.max_width(300).add(&mut h.rsc);
h.set_root(capped);
assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300));
assert_eq!(
h.region(&capped).unwrap().size().x,
Px::from_int(300),
"as long as its child used"
);
// A child that asked for a share takes the box the cap allows, and the
// share itself passes up: whoever divides one is this widget's parent.
let mut h = Harness::new((400, 200));
let share = rect(Color::RED).add(&mut h.rsc);
let capped = share.max_width(300).add(&mut h.rsc);
h.set_root(capped);
assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300));
assert_eq!(h.region(&capped).unwrap().size().x, Px::from_int(400));
}
/// Which of the cap and the box is shorter is a question in pixels, so it is
/// asked again wherever the answer can change -- and the widget asking it is
/// drawn again whenever its own box is, which is what keeps the two in step.
#[test]
fn a_cap_widget_is_decided_again_on_either_side_of_the_crossing() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_root(probe.max_width(300));
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
assert_eq!(width(&h), Px::from_int(300));
h.resize((250, 200));
h.frame();
assert_eq!(
width(&h),
Px::from_int(250),
"its box, which is under the cap"
);
h.resize((400, 200));
h.frame();
assert_eq!(width(&h), Px::from_int(300));
}
/// A fraction in a cap is a fraction of the box the widget capping it was
/// given, which is the box a declared length of its own would be a fraction
/// of -- not of the window, and not of what the cap itself decided.
#[test]
fn a_cap_is_a_fraction_of_the_box_it_was_given() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_root(probe.max_width(Len::rel(0.5)).pad(Padding::uniform(50)));
// Half of the 300 left by the padding, not half of the window.
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(150));
}
/// A cap is a promise about the length as well as the box: a widget whose
/// content is longer than the box it was given reports what it drew, and the
/// cap holds that down even though it never decided the box.
#[test]
fn a_cap_holds_an_answer_that_overflowed_its_box() {
let mut h = Harness::new((250, 200));
let left = rect(Color::RED).width(200).add(&mut h.rsc);
let right = rect(Color::BLUE).width(200).add(&mut h.rsc);
let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc);
h.rsc.widgets_mut().set_max_len(row, Axis::X, 300.into());
h.set_root(row);
// The box is the 250 window, which the cap of 300 leaves alone, and the
// row draws 400 of it. Its answer is the cap, and the window centres it.
assert_corners!(h, row, (-25, 0), (275, 200));
}