A widget is asked in two boxes rather than one. Its frame is what a fraction it declares or reports is a fraction of, and it passes through a span, a stack and a scroll unchanged, so `rel(0.5)` is half the same area however many containers sit between: a frame is narrowed only by what is decided above the widget -- a declared length, the root. Its extent is where the drawing goes, given as a `Place` per axis: a part of the parent's own box, measured in frame lengths from where that box starts, which the child either fills or has its answer placed inside. What that buys is that nothing under a container depends on where the container sits. A container reads `extent_len` for the length it divides and nothing about the start, so moving it re-places its children by re-adding that start and draws nobody again; and a fraction is resolved once, against the frame, rather than once per box it is composed through -- a stack sized by a child that reports `rel(0.5)` no longer takes half of half. `Place` replaces `DrawRegion`, `ExtentPlacement`, `widget_within`, `measure_len`, `region()`, `placement()` and `box_of`. Primitives and masks are written in the widget's own box's coordinates alone, so the drawing has one reference rather than two. The placement pin goes with them: reading the extent's length pins that length symbolically, and pins compose only where a child's box is its parent's own. Placing an answer waits for the end of the parent's draw or for the next ask of that child in it, so a span child is one drawing and one move rather than two moves. `Pad` is transparent: its padding goes around what it pads and its child keeps the outer frame, which is where `Outset` was going anyway. A fraction under a pad is now a fraction of the frame rather than of the inset box. Checked: fmt, clippy with -D warnings, 109 suite tests and 20 core tests in debug, the 11 generated cases, and the shrinker at 400 trees of depth 5 over all fifteen cases -- which still finds seed 108 under `reorder`, where a wrapping text measured in one box and drawn in another settles differently warm than cold. `redraw` therefore keeps the baseline's deferral for a box that is not as long as the one the widget was measured in; the plan's step 6 is not done, and the next commit message or the handoff says why.
743 lines
28 KiB
Rust
743 lines
28 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));
|
|
}
|
|
|
|
/// 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}");
|
|
}
|
|
|
|
/// The same reading through a pad: padding goes around what it pads and
|
|
/// does not narrow what a fraction under it is a fraction of, so half of the
|
|
/// window plus the padding is what the pad takes and where the next child
|
|
/// starts.
|
|
#[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), (210, 90));
|
|
assert_corners!(h, padded, (0, 0), (220, 100));
|
|
assert_corners!(h, tail, (220, 0), (320, 100));
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
|
|
#[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).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 drawn = active.extent.within(&active.frame_abs);
|
|
let region = h.render.moves.resolve(active.parent_move, drawn);
|
|
let dim = h.size().axis(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(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));
|
|
}
|