`Pad` moves its child's box in rather than shrinking it, `Inset` is the old behaviour under a new name, and `in_parent_frame`'s composition and the `reports_of` argument are gone -- a report comes up raw and the parent says what it is a fraction of, which `Inset` does for itself. Not landed. Everything passes except the new `Inset` test: a child declaring `rel(0.5)` under an inset comes out 47.5 px wide of the 190 inside rather than 95, and I have not accounted for where the second halving is. The `Pad` half is green on its own -- the three tests that changed to `.inset()` were using padding as scaffolding -- but landing it without a working `Inset` would break every `.pad()` that meant inset.
718 lines
28 KiB
Rust
718 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 offers each child the room left after the one before, because a
|
|
/// text has to wrap at the width actually there, but reads what the child
|
|
/// reports as a fraction of 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.
|
|
#[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 drawn there
|
|
// once more; half of that final box is what its own child takes.
|
|
assert_corners!(h, nested, (200, 0), (400, 100));
|
|
assert_corners!(h, inner, (200, 0), (300, 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 outside what it pads, so a fraction under one is a fraction of
|
|
/// the box the padding is measured from: half of a 400 px row is 200, and
|
|
/// the pad is that plus both edges. Inset it instead and `rel` would mean the
|
|
/// inner box while `px` meant the outer one, which is the one thing a length
|
|
/// may not do.
|
|
#[test]
|
|
fn a_pad_is_outside_the_fraction_its_child_asked_for() {
|
|
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, padded, (0, 0), (220, 100));
|
|
assert_corners!(h, tail, (220, 0), (320, 100));
|
|
}
|
|
|
|
/// The other half of the pair: an inset takes its room off the inside, so it
|
|
/// is exactly as long as the box it was given and the fraction its child
|
|
/// asked for is a fraction of what is left inside. Half of the 380 left in a
|
|
/// 400 px row is 190, and the inset is the whole 400.
|
|
#[test]
|
|
fn an_inset_is_inside_the_fraction_its_child_asked_for() {
|
|
let mut h = Harness::new((400, 100));
|
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
|
let inset = (inner,).span(Dir::RIGHT).inset(10).add(&mut h.rsc);
|
|
h.set_root((inset,).span(Dir::RIGHT).width(rel(1.0)));
|
|
|
|
assert_corners!(h, inset, (0, 0), (200, 100));
|
|
assert_corners!(h, inner, (10, 0), (200, 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.inset(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.inset(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.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: 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:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|