Both edges of a fixed length share their box's fraction, so composing the chain moves them together and the shader's floor can shift the pixel between them but not round it away. The second test is the case that makes the first one worth having: a span short of room takes it from its shares, which go to nothing and then past it, and never from the fixed lengths between them. Expressing the same line as a fraction of the output fails both, which is what the tests are there to keep visible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
352 lines
12 KiB
Rust
352 lines
12 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 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));
|
|
// `Aligned` draws its child twice; listing it twice would move it twice.
|
|
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, (100, 0), (300, 200));
|
|
|
|
h.rsc[left].x = Some(Len::px(150));
|
|
h.frame();
|
|
|
|
assert_corners!(h, inner, (150, 0), (350, 200));
|
|
}
|
|
|
|
#[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 drawn in the
|
|
// whole box and then placed in 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.rsc[leaf].y = Some(Len::px(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).add(&mut h.rsc);
|
|
h.set_root((first, row).span(Dir::DOWN));
|
|
assert_corners!(h, inner, (10, 50), (390, 70));
|
|
|
|
h.rsc[first].y = Some(Len::px(80));
|
|
h.frame();
|
|
|
|
// The row is the same shape somewhere else, so one slot moved it and
|
|
// `inner`'s own region was never rewritten.
|
|
assert_corners!(h, inner, (10, 90), (390, 110));
|
|
}
|
|
|
|
#[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 rest = rect(Color::GREEN).add(&mut h.rsc);
|
|
let panel = (fixed, rest).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, rest, (150, 0), (400, 200));
|
|
|
|
h.rsc[bar].x = Some(Len::px(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 the rest absorbs the change.
|
|
assert_corners!(h, fixed, (200, 0), (250, 200));
|
|
assert_corners!(h, rest, (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);
|
|
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.rsc[bar].x = Some(Len::px(200));
|
|
h.frame();
|
|
|
|
assert_corners!(h, inner, (210, 10), (390, 30));
|
|
}
|
|
|
|
#[test]
|
|
fn only_a_container_that_places_its_children_lengthens_the_chain() {
|
|
let mut h = Harness::new((400, 200));
|
|
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
|
// Four widgets between the span and the leaf, none of which places what
|
|
// it draws, so all of them share the span's slot.
|
|
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 slot = h.render.active[&leaf.id()].parent_move;
|
|
assert_eq!(
|
|
h.render.moves.depth(slot),
|
|
2,
|
|
"the span above the leaf, and the root the window is held in"
|
|
);
|
|
}
|
|
|
|
/// A span that sizes from its children passes their `rest` weight up rather
|
|
/// 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.
|
|
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 edge = |s: UiScalar| (s.rel * dim).floor() + s.px.floor();
|
|
let span = region.axis(axis);
|
|
(edge(span.start), edge(span.end))
|
|
}
|
|
|
|
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
|
let inner = rect(Color::RED).add_strong(&mut h.rsc);
|
|
let mark = SetSize {
|
|
inner,
|
|
x: Some(Len::px(1.0)),
|
|
y: None,
|
|
}
|
|
.add_strong(&mut h.rsc);
|
|
marks.push(mark.id());
|
|
mark
|
|
}
|
|
|
|
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
|
|
SetSize {
|
|
inner,
|
|
x: Some(Len::rest(ratio)),
|
|
y: None,
|
|
}
|
|
.add_strong(&mut h.rsc)
|
|
}
|
|
|
|
/// 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: 3.0,
|
|
right: 7.0,
|
|
top: 0.0,
|
|
bottom: 0.0,
|
|
},
|
|
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:?}");
|
|
}
|
|
}
|
|
}
|