Files
iris/tests/cases/layout.rs
T
iris-aiandClaude Opus 5 0d0326769c Ask the root the way every other widget is asked
The root had a layout path of its own: `root_layout` read its declared
lengths against the window, while every other widget's box came of
`Painter::widget_at`, where a rule of the widget's own -- a share with pixels
or a fraction beside it -- is compared against the offer and can take the box
past it. So a share on the root was the window whatever it asked for, which
`docs/LAYOUT_LOG.md` recorded as a gap rather than fixing, and any later rule
that reads the offer would have had to be written twice.

There is one box nobody drew, and that is the whole of what the root is
asked in. `Placing::WINDOW` says it -- the full output, fractions of the full
output, no move entry and no mask -- and `Placing::ask` is then the one place
a box is decided, called by the painter, by a local redraw, and by the root's
first draw. The root's own path is what is left of it: a widget with no
parent keeps different bookkeeping, not a different layout.

Measured in a 400 px window, a probe under each of three parents, which now
agree on every row where two of them agreed before:

    rule                      as root   wrapped   in a span
    leftover(1)                   400       400         400
    px(50) + leftover(1)          400       400         400
    px(500) + leftover(1)         500       500         500   (was 400 as root)
    rel(0.5) + leftover(1)        400       400         400
    rel(2.0) + leftover(1)        800       800         800   (was 400 as root)
    px(500)                       500       500         500
    rel(0.5)                      200       200         200

The comparison is kept on the widget asked about rather than on the asker,
which is what makes the root need nothing of its own: a window range means
the same thing at either end of an ask, `in_parent` passes one up unchanged,
and the asker ends up holding it through the child's drawing exactly as it
did when `longer_than` narrowed the asker directly. The root has no asker, so
its own record is the only place that range can live -- and `resize` already
checks that record, so a share crossing its length is caught with no new
code. `a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing`
now runs at the root too: 500 at a 400 window, 900 at 900, 500 again at 400.

Two things this changes beyond the share. `DrawInfo::asked` is now the place
the parent offered rather than the place the ask came to, so a local redraw
re-decides the rule instead of re-reading the decision -- the two were the
same until a rule could move the box. And the root's `is_region_node` is
read, where the old path passed `false`: a region-node root now gets its
entry, whose translation is the identity, pinned by
`a_region_node_root_is_a_region_node`.

Format, clippy with and without layout-diagnostics, and the suite (136 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`2dba90b` across all 34,571 boxes, and the three seed scans pass: 400 at
depth 5 (61s), 1,000 at depth 6 (155s), 2,000 at depth 4 (291s).

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

1007 lines
39 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) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500) + LayoutLen::LEFTOVER, 500),
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
(LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800),
(LayoutLen::px(500), 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) + 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) + LayoutLen::LEFTOVER);
h.frame();
assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}");
h.set_len(probe, Axis::X, LayoutLen::px(500) + 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) + 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));
}