Eleven `tests/*.rs` were eleven binaries, each linking the whole graph -- `wgpu` and all -- to run a handful of cases. They are modules of one target now, under `tests/cases/`, and `cargo test --test suite layout::` still picks one out. The fuzzers and the `*_cost` measurements stay their own targets: they are run on their own and want to be selectable without building the rest. `profile.test` takes `debug = "line-tables-only"`, which is what a backtrace here actually reads; the type and variable information was the bulk of what the linker was writing. Measured on this machine, rebuilding `iris`'s test targets after a change to the crate: 14.3 s before, 9.8 s with one target, 7.7 s with both. `target/` went from 45 GB to 13 GB. The suite still passes 102 tests, and the binary still carries `.debug_line`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
455 lines
16 KiB
Rust
455 lines
16 KiB
Rust
//! Where a frame puts things, with no window to put them in.
|
|
|
|
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));
|
|
}
|
|
|
|
#[test]
|
|
fn a_full_ortho_span_reports_relative_full() {
|
|
let mut h = Harness::new((400, 200));
|
|
let child = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
let span = (child,)
|
|
.span(Dir::RIGHT)
|
|
.ortho(OrthoSize::Full)
|
|
.add(&mut h.rsc);
|
|
h.set_root(span);
|
|
|
|
assert_eq!(h.render.active[&span.id()].size.y, Len::rel(1.0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_children_ortho_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)
|
|
.ortho(OrthoSize::Children)
|
|
.add(&mut h.rsc);
|
|
h.set_root(span);
|
|
|
|
assert_eq!(h.render.active[&span.id()].size.y, Len::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));
|
|
}
|
|
|
|
#[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(¶), 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)
|
|
.ortho(OrthoSize::Children)
|
|
.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), 1, "only the root region");
|
|
|
|
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),
|
|
2,
|
|
"the opted-in widget's region and the root region"
|
|
);
|
|
|
|
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), 1);
|
|
}
|
|
|
|
/// 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.
|
|
#[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));
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// Where the shader puts an edge: the two parts of a scalar are floored
|
|
/// apart, so a fraction and a pixel offset snap independently, and each is
|
|
/// 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.parent_move, active.region);
|
|
let dim = h.size().axis(axis);
|
|
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
|
let edge = |s: UiScalar| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32());
|
|
let span = region.axis(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, Len::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(Len::px(20) + Len::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());
|
|
}
|