Link the ordinary tests once, and keep their debug info to line tables
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>
This commit is contained in:
1 parent
39e4ca20e6
commit
cb955f1023
13 files changed
+35
No files matched your search
@@ -0,0 +1,101 @@
|
||||
//! A measurement that decides control flow.
|
||||
//!
|
||||
//! Comparing boxes catches a widget that moved. It does not catch a widget
|
||||
//! that measured a child, believed a different answer from the one a cold
|
||||
//! start would give, and took the other branch -- which is the same defect
|
||||
//! arriving somewhere it cannot be ignored. A widget here branches on what it
|
||||
//! measured, so a disagreement shows up as a different tree.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
/// Measures `probe` across `axis` and draws one of two children on the
|
||||
/// answer. Its own configuration never changes, so which child is drawn is a
|
||||
/// property of the layout alone.
|
||||
struct BranchesOnMeasurement {
|
||||
probe: StrongWidget,
|
||||
wide: StrongWidget,
|
||||
narrow: StrongWidget,
|
||||
threshold: f32,
|
||||
}
|
||||
|
||||
impl Widget for BranchesOnMeasurement {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let mut top = UiRegion::FULL;
|
||||
top.y.end = top.y.start.offset(Px::from_int(40));
|
||||
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
|
||||
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
|
||||
|
||||
let mut below = UiRegion::FULL;
|
||||
below.y.start = below.y.start.offset(Px::from_int(40));
|
||||
match px > Px::from_f32(self.threshold) {
|
||||
true => painter.widget_within(&self.wide, below),
|
||||
false => painter.widget_within(&self.narrow, below),
|
||||
};
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
|
||||
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
|
||||
let words = "the quick brown fox jumps over the lazy dog and keeps running";
|
||||
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let wide = rect(Color::RED).add(&mut h.rsc);
|
||||
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let branch = BranchesOnMeasurement {
|
||||
probe: probe.add_strong(&mut h.rsc),
|
||||
wide: wide.add_strong(&mut h.rsc),
|
||||
narrow: narrow.add_strong(&mut h.rsc),
|
||||
threshold,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
|
||||
h.set_root((side, branch).span(Dir::RIGHT));
|
||||
(wide.id(), narrow.id())
|
||||
}
|
||||
|
||||
/// Which of the two branches drew, as a pair a test can compare.
|
||||
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
|
||||
(h.region(&wide).is_some(), h.region(&narrow).is_some())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
|
||||
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||
let mut h = Harness::new((900, 600));
|
||||
let (wide, narrow) = plant(&mut h, threshold);
|
||||
let first = taken(&h, wide, narrow);
|
||||
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
|
||||
|
||||
for frame in 0..4 {
|
||||
h.rsc.widgets_mut().get_dyn_mut(wide);
|
||||
h.rsc.widgets_mut().get_dyn_mut(narrow);
|
||||
h.frame();
|
||||
assert_eq!(
|
||||
taken(&h, wide, narrow),
|
||||
first,
|
||||
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
|
||||
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||
let mut warm = Harness::new((900, 600));
|
||||
let (wide, narrow) = plant(&mut warm, threshold);
|
||||
warm.resize((640, 480));
|
||||
warm.frame();
|
||||
warm.rsc.widgets_mut().get_dyn_mut(wide);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 480));
|
||||
let (cwide, cnarrow) = plant(&mut cold, threshold);
|
||||
|
||||
assert_eq!(
|
||||
taken(&warm, wide, narrow),
|
||||
taken(&cold, cwide, cnarrow),
|
||||
"threshold {threshold}: warm and cold took different branches"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//! What a retained drawing costs in accuracy when it is moved instead of made
|
||||
//! again. A subtree's stored regions are the only record of where it is, so a
|
||||
//! move that works from the last answer rather than from the box it is now in
|
||||
//! integrates its own rounding, and nothing later recomputes it. Re-expressing
|
||||
//! each part as the same fraction of the new box is what keeps a long-lived
|
||||
//! layout on the one a cold start produces.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
/// A row of a fixed height under a bar, so changing the bar's height moves the
|
||||
/// row without changing the box it is given: the move path, repeatedly.
|
||||
fn plant(h: &mut Harness, bar_height: f32) -> (WeakWidget<Rect>, WeakWidget<Rect>) {
|
||||
let bar = rect(Color::RED).height(bar_height).add(&mut h.rsc);
|
||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let row = (inner, rect(Color::GREEN)).span(Dir::RIGHT).height(100);
|
||||
h.set_root((bar, row).span(Dir::DOWN));
|
||||
(bar, inner)
|
||||
}
|
||||
|
||||
/// Enough moves to pass the 0.05 physical pixels layout treats as the same
|
||||
/// place, for a move that adds an offset to the last answer. Measured on this
|
||||
/// fixture on 2026-09-15: adding the offset to both ends of a span shortened
|
||||
/// the row by 0.071 over this many moves and by 0.712 over ten times as many,
|
||||
/// growing with the count rather than settling. Placing the far end from the
|
||||
/// near one instead left 0.069, because the length is re-derived either way.
|
||||
const MOVES: usize = 20_000;
|
||||
|
||||
#[test]
|
||||
fn a_subtree_moved_many_times_stays_where_a_cold_layout_puts_it() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let (bar, inner) = plant(&mut warm, 40.0);
|
||||
let mut height = 40.0;
|
||||
for step in 0..MOVES {
|
||||
height = 40.0 + (step % 300) as f32 * 0.37;
|
||||
warm.set_len(bar, Axis::Y, height);
|
||||
warm.frame();
|
||||
}
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let (_, cold_inner) = plant(&mut cold, height);
|
||||
cold.frame();
|
||||
|
||||
assert_eq!(warm.region(&inner), cold.region(&cold_inner));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Whether measuring a widget and then giving it the length it reported is a
|
||||
//! fixed point, which is what a span that sizes to its children needs.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn a_wrapping_text_in_a_span_settles_on_one_width() {
|
||||
let mut h = Harness::new((900, 600));
|
||||
let words = "the quick brown fox jumps over the lazy dog and keeps on running \
|
||||
until it reaches the end of a rather long line of text";
|
||||
let t = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let filler = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((t, filler).span(Dir::RIGHT));
|
||||
|
||||
let mut widths = Vec::new();
|
||||
for _ in 0..6 {
|
||||
let r = h.region(&t.id()).unwrap();
|
||||
widths.push(r.bot_right.x - r.top_left.x);
|
||||
// Redrawing it changes nothing about the state, so nothing may move.
|
||||
h.rsc.widgets_mut().get_dyn_mut(t.id());
|
||||
h.frame();
|
||||
}
|
||||
println!("widths over six frames: {widths:?}");
|
||||
assert!(
|
||||
widths.windows(2).all(|w| w[0] == w[1]),
|
||||
"a repaint that changed nothing moved it: {widths:?}"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
//! 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());
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Which widget an input reaches.
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use iris::harness::{Harness, TouchScript};
|
||||
use iris::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn a_press_reaches_only_the_widget_under_the_cursor() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let clicks = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let (on_left, on_right) = (clicks.clone(), clicks.clone());
|
||||
let left = rect(Color::RED)
|
||||
.width(100)
|
||||
.on(CursorSense::click(), move |_, _| {
|
||||
on_left.borrow_mut().push("left")
|
||||
})
|
||||
.add(&mut h.rsc);
|
||||
let right = rect(Color::BLUE)
|
||||
.on(CursorSense::click(), move |_, _| {
|
||||
on_right.borrow_mut().push("right")
|
||||
})
|
||||
.add(&mut h.rsc);
|
||||
h.set_root((left, right).span(Dir::RIGHT));
|
||||
|
||||
h.click((50, 100));
|
||||
assert_eq!(*clicks.borrow(), ["left"]);
|
||||
|
||||
h.click((300, 100));
|
||||
assert_eq!(*clicks.borrow(), ["left", "right"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_ends_when_the_cursor_leaves_the_window() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let hovered = Rc::new(RefCell::new(0));
|
||||
let ended = Rc::new(RefCell::new(0));
|
||||
|
||||
let (h_count, e_count) = (hovered.clone(), ended.clone());
|
||||
let widget = rect(Color::RED)
|
||||
.on(CursorSense::HoverStart, move |_, _| {
|
||||
*h_count.borrow_mut() += 1
|
||||
})
|
||||
.on(CursorSense::HoverEnd, move |_, _| {
|
||||
*e_count.borrow_mut() += 1
|
||||
})
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(widget);
|
||||
|
||||
h.move_to((200, 100));
|
||||
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||
|
||||
// A second sample inside the same widget is not a second hover.
|
||||
h.move_to((210, 100));
|
||||
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||
|
||||
h.leave();
|
||||
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recorded_gesture_presses_where_it_says() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let clicks = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let (on_left, on_right) = (clicks.clone(), clicks.clone());
|
||||
let left = rect(Color::RED)
|
||||
.width(100)
|
||||
.on(CursorSense::click(), move |_, _| {
|
||||
on_left.borrow_mut().push("left")
|
||||
})
|
||||
.add(&mut h.rsc);
|
||||
let right = rect(Color::BLUE)
|
||||
.on(CursorSense::click(), move |_, _| {
|
||||
on_right.borrow_mut().push("right")
|
||||
})
|
||||
.add(&mut h.rsc);
|
||||
h.set_root((left, right).span(Dir::RIGHT));
|
||||
|
||||
let script = TouchScript::parse("0 down 300 100\n80 up 300 100").unwrap();
|
||||
h.replay(&script);
|
||||
|
||||
assert_eq!(*clicks.borrow(), ["right"]);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
//! Input across layers: what stops at a layer, what passes through it, and
|
||||
//! where hovering stops.
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
const WINDOW: f32 = 100.0;
|
||||
|
||||
/// Every sense that has fired on one widget since it was last read.
|
||||
#[derive(Default, Clone)]
|
||||
struct Fired(Rc<RefCell<Vec<CursorSense>>>);
|
||||
|
||||
impl Fired {
|
||||
fn take(&self) -> Vec<CursorSense> {
|
||||
std::mem::take(&mut self.0.borrow_mut())
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget filling whatever it is given, recording the senses it is sent.
|
||||
fn listener(h: &mut Harness, senses: impl Into<CursorSenses>) -> (WeakWidget<Rect>, Fired) {
|
||||
let fired = Fired::default();
|
||||
let record = fired.clone();
|
||||
let id = rect(Color::WHITE)
|
||||
.on(senses.into(), move |ctx, _| {
|
||||
record.0.borrow_mut().push(ctx.data.sense)
|
||||
})
|
||||
.add(&mut h.rsc);
|
||||
(id, fired)
|
||||
}
|
||||
|
||||
/// A widget with no senses of its own, to leave a gap beside one that has.
|
||||
fn blank(h: &mut Harness) -> WeakWidget<Rect> {
|
||||
rect(Color::WHITE).add(&mut h.rsc)
|
||||
}
|
||||
|
||||
fn harness() -> Harness {
|
||||
Harness::new((WINDOW, WINDOW))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_stops_at_the_topmost_widget() {
|
||||
let mut h = harness();
|
||||
let (bottom, bottom_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||
let (middle, middle_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||
let (top, top_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||
h.set_root((bottom, middle, top).stack());
|
||||
|
||||
h.move_to((50, 50));
|
||||
|
||||
assert_eq!(top_hover.take(), [CursorSense::HoverStart]);
|
||||
assert_eq!(
|
||||
middle_hover.take(),
|
||||
[],
|
||||
"hover is not shared with a layer below"
|
||||
);
|
||||
assert_eq!(bottom_hover.take(), []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scroll_passes_through_every_widget_that_does_not_want_it() {
|
||||
let mut h = harness();
|
||||
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
|
||||
let (button, clicked) = listener(&mut h, CursorSense::click());
|
||||
let (overlay, overlay_clicked) = listener(&mut h, CursorSense::click());
|
||||
h.set_root((list, button, overlay).stack());
|
||||
|
||||
h.move_to((50, 50));
|
||||
h.scroll((0, 10));
|
||||
|
||||
assert_eq!(
|
||||
scrolled.take(),
|
||||
[CursorSense::Scroll],
|
||||
"two layers of click-only widgets do not stop a scroll"
|
||||
);
|
||||
assert_eq!(clicked.take(), []);
|
||||
assert_eq!(overlay_clicked.take(), []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hovering_a_button_above_does_not_stop_a_later_scroll() {
|
||||
let mut h = harness();
|
||||
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
|
||||
let (button, _clicked) = listener(&mut h, CursorSense::click());
|
||||
h.set_root((list, button).stack());
|
||||
|
||||
// The hover arrives in its own frame, as a window delivers it.
|
||||
h.move_to((50, 50));
|
||||
assert_eq!(scrolled.take(), []);
|
||||
|
||||
h.scroll((0, 10));
|
||||
assert_eq!(
|
||||
scrolled.take(),
|
||||
[CursorSense::Scroll],
|
||||
"a hover already resting on the button must not consume the wheel"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_topmost_listener_takes_a_press() {
|
||||
let mut h = harness();
|
||||
let (below, below_clicked) = listener(&mut h, CursorSense::click());
|
||||
let (above, above_clicked) = listener(&mut h, CursorSense::click());
|
||||
h.set_root((below, above).stack());
|
||||
|
||||
h.click((50, 50));
|
||||
|
||||
assert_eq!(above_clicked.take(), [CursorSense::click()]);
|
||||
assert_eq!(below_clicked.take(), [], "one press goes to one widget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_beside_the_button_reaches_the_layer_below() {
|
||||
let mut h = harness();
|
||||
let (list, list_clicked) = listener(&mut h, CursorSense::click());
|
||||
// The row above the list covers it, but only its left half is the button.
|
||||
let (button, button_clicked) = listener(&mut h, CursorSense::click());
|
||||
let row = (button, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root((list, row).stack());
|
||||
|
||||
h.click((20, 50));
|
||||
assert_eq!(button_clicked.take(), [CursorSense::click()]);
|
||||
assert_eq!(list_clicked.take(), []);
|
||||
|
||||
h.click((80, 50));
|
||||
assert_eq!(button_clicked.take(), [], "the cursor is not on the button");
|
||||
assert_eq!(
|
||||
list_clicked.take(),
|
||||
[CursorSense::click()],
|
||||
"a press beside the button belongs to what is under it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_a_widget_still_ends_its_hover() {
|
||||
let mut h = harness();
|
||||
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||
h.set_root(widget);
|
||||
|
||||
h.move_to((50, 50));
|
||||
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
||||
|
||||
h.leave();
|
||||
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_a_widget_does_not_block_the_layer_below() {
|
||||
let mut h = harness();
|
||||
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||
// Only the left half of the layer above is a widget, so the cursor can
|
||||
// leave it without leaving the one underneath.
|
||||
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root((below, row).stack());
|
||||
|
||||
h.move_to((20, 50));
|
||||
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
|
||||
assert_eq!(below_hover.take(), [], "the layer above is over it");
|
||||
|
||||
h.move_to((80, 50));
|
||||
assert_eq!(above_hover.take(), [CursorSense::HoverEnd]);
|
||||
assert_eq!(
|
||||
below_hover.take(),
|
||||
[CursorSense::HoverStart],
|
||||
"ending a hover above must not stop the hover below"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn covering_a_widget_ends_its_hover() {
|
||||
let mut h = harness();
|
||||
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root((below, row).stack());
|
||||
|
||||
h.move_to((80, 50));
|
||||
assert_eq!(below_hover.take(), [CursorSense::HoverStart]);
|
||||
|
||||
h.move_to((20, 50));
|
||||
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
|
||||
assert_eq!(
|
||||
below_hover.take(),
|
||||
[CursorSense::HoverEnd],
|
||||
"a widget covered by one that took the input is no longer hovered"
|
||||
);
|
||||
|
||||
h.move_to((80, 50));
|
||||
assert_eq!(
|
||||
below_hover.take(),
|
||||
[CursorSense::HoverStart],
|
||||
"uncovering it hovers it again"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_starts_and_ends_once_each() {
|
||||
let mut h = harness();
|
||||
// Only the left half is the widget, so the cursor can leave it without
|
||||
// leaving the window.
|
||||
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||
let row = (widget, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root(row);
|
||||
|
||||
h.move_to((20, 50));
|
||||
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
||||
|
||||
h.move_to((30, 50));
|
||||
assert_eq!(hover.take(), [], "staying inside is not a second start");
|
||||
|
||||
h.move_to((80, 50));
|
||||
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
||||
|
||||
h.move_to((90, 50));
|
||||
assert_eq!(hover.take(), [], "an ended hover does not end again");
|
||||
|
||||
h.move_to((20, 50));
|
||||
assert_eq!(
|
||||
hover.take(),
|
||||
[CursorSense::HoverStart],
|
||||
"re-entering starts it"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
//! What a second frame draws again, and what it keeps.
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use iris::harness::{Harness, assert_corners};
|
||||
use iris::prelude::*;
|
||||
|
||||
/// A leaf that counts its draws and reports whatever size it is given, so a
|
||||
/// test can see what the retained path skipped. One that reads its box in
|
||||
/// pixels has a drawing that holds for that box alone.
|
||||
struct Counted {
|
||||
draws: Rc<Cell<usize>>,
|
||||
size: Size,
|
||||
reads_box: bool,
|
||||
}
|
||||
|
||||
impl Widget for Counted {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
if self.reads_box {
|
||||
painter.px_size();
|
||||
}
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
struct Counts(Rc<Cell<usize>>);
|
||||
|
||||
impl Counts {
|
||||
fn get(&self) -> usize {
|
||||
self.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>, Counts) {
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let id = Counted {
|
||||
draws: draws.clone(),
|
||||
size,
|
||||
reads_box,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
(id, Counts(draws))
|
||||
}
|
||||
|
||||
struct Layered {
|
||||
children: [StrongWidget<Rect>; 2],
|
||||
_revision: usize,
|
||||
}
|
||||
|
||||
impl Widget for Layered {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.child_layer();
|
||||
painter.widget(&self.children[0]);
|
||||
painter.next_layer();
|
||||
painter.widget(&self.children[1]);
|
||||
Size::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let children = [
|
||||
rect(Color::RED).add_strong(&mut h.rsc),
|
||||
rect(Color::BLUE).add_strong(&mut h.rsc),
|
||||
];
|
||||
let root = Layered {
|
||||
children,
|
||||
_revision: 0,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
|
||||
h.rsc[root]._revision += 1;
|
||||
h.frame();
|
||||
|
||||
let label = h.rsc.widgets().label(root.id());
|
||||
let active = h
|
||||
.render
|
||||
.debug(h.rsc.widgets(), label)
|
||||
.find(|active| active.id == root.id())
|
||||
.unwrap();
|
||||
assert_eq!(active.layer, 0);
|
||||
}
|
||||
|
||||
/// A fixed-width leaf beside one that takes what is left over, so changing
|
||||
/// the first hands the second a different box without the output changing.
|
||||
fn pair(h: &mut Harness, reads_box: bool) -> (WeakWidget<Counted>, Counts, WidgetId) {
|
||||
let (first, _) = counted(h, Size::from((100, 200)), false);
|
||||
let (second, draws) = counted(h, Size::LEFTOVER, reads_box);
|
||||
h.set_root((first, second).span(Dir::RIGHT));
|
||||
(first, draws, second.id())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, draws, second) = pair(&mut h, false);
|
||||
let settled = draws.get();
|
||||
assert_corners!(h, second, (100, 0), (400, 200));
|
||||
|
||||
h.rsc[first].size = Size::from((150, 200));
|
||||
h.frame();
|
||||
|
||||
assert_eq!(
|
||||
draws.get(),
|
||||
settled,
|
||||
"its box is a field to write, not a reason to draw"
|
||||
);
|
||||
assert_corners!(h, second, (150, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_an_ordinary_subtree_remaps_its_mask() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, _) = counted(&mut h, Size::from((100, 200)), false);
|
||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let masked = inner.masked().add(&mut h.rsc);
|
||||
h.set_root((first, masked).span(Dir::RIGHT));
|
||||
|
||||
h.rsc[first].size = Size::from((150, 200));
|
||||
h.frame();
|
||||
|
||||
let active = &h.render.active[&masked.id()];
|
||||
assert_eq!(
|
||||
h.rsc.ui().masks[active.mask.idx()].region,
|
||||
UiRegion::new(
|
||||
UiSpan::new(UiScalar::px(150.0), UiScalar::rel_max()),
|
||||
UiSpan::FULL,
|
||||
)
|
||||
);
|
||||
assert_corners!(h, inner, (150, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, draws, second) = pair(&mut h, true);
|
||||
let settled = draws.get();
|
||||
|
||||
h.rsc[first].size = Size::from((150, 200));
|
||||
h.frame();
|
||||
|
||||
// The preceding fixed child makes the remaining box this child's real
|
||||
// box, so measuring it also draws it in its final box.
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
assert_corners!(h, second, (150, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_child_that_declares_its_length_is_drawn_once() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), false);
|
||||
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), true);
|
||||
// The span takes one child's length from its hint and has to draw the
|
||||
// other to find out, so only the second is drawn before its final box.
|
||||
let hinted = told.width(100).add(&mut h.rsc);
|
||||
h.set_root((hinted, asked).span(Dir::RIGHT));
|
||||
|
||||
assert_eq!(told_draws.get(), 1);
|
||||
// Reading its box makes its drawing hold for the measuring box alone,
|
||||
// and it reports less than that box: so it is drawn again in the box its
|
||||
// answer places it in, and once more in the final box the span chooses.
|
||||
// A widget that says what it holds for, as text does, skips the middle
|
||||
// one.
|
||||
assert_eq!(
|
||||
asked_draws.get(),
|
||||
3,
|
||||
"drawn to be measured, in its placed box, then in its final box"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_relays_out_when_a_child_it_measured_changes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, _, second) = pair(&mut h, false);
|
||||
|
||||
h.rsc[first].size = Size::from((250, 200));
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, first, (0, 0), (250, 200));
|
||||
assert_corners!(h, second, (250, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, draws) = counted(&mut h, Size::from((100, 200)), false);
|
||||
let (second, _) = counted(&mut h, Size::LEFTOVER, false);
|
||||
h.set_root((first, second).span(Dir::RIGHT));
|
||||
let settled = draws.get();
|
||||
|
||||
// Taking mutable access is the ordinary content-change signal. This
|
||||
// widget returns the same size, so the parent has nothing to lay out.
|
||||
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_child_survives_the_next_frame() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// Both children declare a length, so the span chooses their boxes from
|
||||
// hints rather than drawing them to find out.
|
||||
let top = rect(Color::RED).height(80).add(&mut h.rsc);
|
||||
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
||||
h.set_root((top, bottom).span(Dir::DOWN));
|
||||
|
||||
h.rsc.widgets_mut().get_dyn_mut(top.id());
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, top, (0, 0), (400, 80));
|
||||
assert_corners!(h, bottom, (0, 80), (400, 200));
|
||||
}
|
||||
|
||||
/// Lays its child out from the hint alone, never reading what it drew.
|
||||
struct FromHint {
|
||||
inner: StrongWidget,
|
||||
}
|
||||
|
||||
impl Widget for FromHint {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
||||
let mut region = UiRegion::FULL;
|
||||
region.y.end = region.y.start.offset(len.px);
|
||||
painter.widget_within(&self.inner, region);
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
|
||||
let parent = FromHint {
|
||||
inner: inner.add_strong(&mut h.rsc),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(parent);
|
||||
assert_corners!(h, inner, (0, 0), (400, 80));
|
||||
|
||||
h.set_len(inner, Axis::Y, 120);
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, inner, (0, 0), (400, 120));
|
||||
}
|
||||
|
||||
/// Reads its box's size, which nothing but its own draw can put right.
|
||||
struct ReadsBox {
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for ReadsBox {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
Size::from_px(painter.px_size().div_int(4))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads its box across one axis only, so its drawing holds for a taller
|
||||
/// box on its own and only a wider one is worth a draw.
|
||||
///
|
||||
/// Both of these report a quarter of what they read, without saying that the
|
||||
/// drawing holds there too, so each length they are asked at costs two draws:
|
||||
/// one to answer, and one in the quarter-sized box that answer places them
|
||||
/// in. The counts below are in those pairs.
|
||||
struct ReadsWidth {
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for ReadsWidth {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
Size::from_px(PxVec2::new(
|
||||
painter.px_len(Axis::X).div_int(4),
|
||||
Px::from_int(20),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((800, 100));
|
||||
assert!(h.needs_redraw());
|
||||
h.frame();
|
||||
|
||||
assert_eq!(
|
||||
draws.get(),
|
||||
settled,
|
||||
"a scaling drawing follows its box, and the output is one"
|
||||
);
|
||||
assert_corners!(h, leaf, (0, 0), (800, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_full_ortho_span_moves_its_child_without_redrawing_it() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
||||
let span = (leaf,)
|
||||
.span(Dir::RIGHT)
|
||||
.ortho(OrthoSize::Full)
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(span);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled);
|
||||
assert_corners!(h, leaf, (0, 0), (400, 100));
|
||||
assert_eq!(h.render.active[&span.id()].size.y, Len::rel(1.0));
|
||||
}
|
||||
|
||||
/// The output is the root of the box chain, so a resize is a box that changed
|
||||
/// length like any other -- there is not a second rule for the window. A
|
||||
/// drawing that holds for one length is drawn again whichever box moved.
|
||||
#[test]
|
||||
fn a_resize_redraws_what_does_not_scale() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, true);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((800, 100));
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled + 1, "its box is a different length");
|
||||
assert_corners!(h, leaf, (0, 0), (800, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_redraws_what_read_its_box() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsBox {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((800, 100));
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_only_redraws_read_axes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsWidth {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((400, 300));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled, "height was never read");
|
||||
|
||||
h.resize((800, 300));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 2, "width changes its answer");
|
||||
}
|
||||
|
||||
/// A window is measured onto the grid like everything else, so a resize too
|
||||
/// small to reach the next step is not a resize at all -- and one that does
|
||||
/// reach it is, however little of a pixel it is worth.
|
||||
#[test]
|
||||
fn a_resize_within_one_step_is_not_a_resize() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsWidth {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
// All of these are 400 px to the nearest step.
|
||||
let step = Px::STEP.to_f32();
|
||||
for part in [0.1, 0.2, 0.3] {
|
||||
h.resize((400.0 + step * part, 200.0));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled);
|
||||
}
|
||||
|
||||
h.resize((400.0 + step, 200.0));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 2);
|
||||
}
|
||||
|
||||
/// The same for a box that changes because a sibling did: what is compared
|
||||
/// is the length on the grid, and three lengths that land on one step are
|
||||
/// one length.
|
||||
#[test]
|
||||
fn a_box_change_within_one_step_is_not_a_change() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, draws, _) = pair(&mut h, true);
|
||||
let settled = draws.get();
|
||||
|
||||
let step = Px::STEP.to_f32();
|
||||
for part in [0.1, 0.2, 0.3] {
|
||||
h.rsc[first].size.x = Len::px(100.0 + step * part);
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled);
|
||||
}
|
||||
|
||||
h.rsc[first].size.x = Len::px(100.0 + step);
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsBox {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((400, 200));
|
||||
|
||||
assert!(!h.needs_redraw());
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
|
||||
let mut h = Harness::new((600, 400));
|
||||
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)
|
||||
.add(&mut h.rsc);
|
||||
let below = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root((para, below).span(Dir::DOWN));
|
||||
let top = h.region(&below).expect("drew nothing").top_left.y;
|
||||
|
||||
h.resize((300, 400));
|
||||
h.frame();
|
||||
|
||||
let lower = h.region(&below).expect("drew nothing").top_left.y;
|
||||
assert!(lower > top, "same words, half the width: {top} -> {lower}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
||||
let mut h = Harness::new((400, 400));
|
||||
// Every wrapper up to the outer pad read the size below it, so the outer
|
||||
// pad is what draws again -- and the span it hands the box to is the same
|
||||
// size as before, which is what lets a draw reuse its way past the leaf.
|
||||
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
|
||||
let padded = leaf.pad(10).add(&mut h.rsc);
|
||||
let below = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
||||
assert_corners!(h, below, (12, 132), (388, 388));
|
||||
|
||||
h.rsc[leaf].size = Size::px((100, 200).into());
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, below, (12, 232), (388, 388));
|
||||
}
|
||||
|
||||
/// Reads nothing of its box, so its drawing holds for any length, and has a
|
||||
/// child so that whatever asks about the subtree has one to reach.
|
||||
struct Stretchy {
|
||||
inner: StrongWidget,
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for Stretchy {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
painter.widget(&self.inner).size()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stretching_a_subtree_carries_the_children_in_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 draws = Rc::new(Cell::new(0));
|
||||
let outer = Stretchy {
|
||||
inner: inner.add_strong(&mut h.rsc),
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root((first, outer).span(Dir::DOWN));
|
||||
let settled = draws.get();
|
||||
assert_corners!(h, inner, (0, 40), (400, 400));
|
||||
|
||||
h.set_len(first, Axis::Y, 80);
|
||||
h.frame();
|
||||
|
||||
assert_eq!(
|
||||
draws.get(),
|
||||
settled,
|
||||
"its drawing follows its box, rather than being made again"
|
||||
);
|
||||
assert_corners!(h, outer, (0, 80), (400, 400));
|
||||
assert_corners!(h, inner, (0, 80), (400, 400));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// What a transcript row is: something whose shaping depends on the width
|
||||
// it is given, beside something that only has to be the right shape.
|
||||
let (wraps, wrap_draws) = counted(&mut h, Size::LEFTOVER, true);
|
||||
let (backing, back_draws) = counted(&mut h, Size::LEFTOVER, false);
|
||||
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||
h.set_root((bar, row).span(Dir::RIGHT));
|
||||
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
||||
|
||||
h.set_len(bar, Axis::X, 200);
|
||||
h.frame();
|
||||
|
||||
// The span reads every child's size, so redrawing one takes the span
|
||||
// with it -- and the span then measures and places the redrawn child.
|
||||
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
|
||||
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
|
||||
assert_corners!(h, backing, (200, 0), (300, 200));
|
||||
assert_corners!(h, wraps, (300, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// Its box is a fixed 80 wherever the row's edges end up, so drawing it
|
||||
// again would be for a width it does not have. The declared width is what
|
||||
// lets the span say that without drawing it: a width the span learnt by
|
||||
// drawing the child in its own box is only an answer for that box.
|
||||
let (counter, draws) = counted(&mut h, Size::from((80, 200)), true);
|
||||
let fixed = counter.width(80).add(&mut h.rsc);
|
||||
let (leftover, _) = counted(&mut h, Size::LEFTOVER, false);
|
||||
let row = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||
h.set_root((bar, row).span(Dir::RIGHT));
|
||||
let settled = draws.get();
|
||||
|
||||
h.set_len(bar, Axis::X, 200);
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled, "its own length did not change");
|
||||
assert_corners!(h, fixed, (200, 0), (280, 200));
|
||||
}
|
||||
|
||||
/// `Stack` measures the child that sizes it by drawing it, then draws it
|
||||
/// again above the background it stacks over. The second ask is for the same
|
||||
/// box, so nothing geometric says the answer has gone stale, and reusing it
|
||||
/// leaves the drawing under the background. The same tree got away with it
|
||||
/// while the two asks differed by a rounding.
|
||||
#[test]
|
||||
fn a_widget_asked_again_on_another_layer_is_drawn_there() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let background = rect(Color::RED).add(&mut h.rsc);
|
||||
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
|
||||
let stack = Stack {
|
||||
children: vec![
|
||||
background.add_strong(&mut h.rsc),
|
||||
front.add_strong(&mut h.rsc),
|
||||
],
|
||||
size: StackSize::Child(1),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(stack);
|
||||
h.frame();
|
||||
|
||||
let layer = |id| h.render.active[&id].layer;
|
||||
assert_ne!(
|
||||
layer(front.id()),
|
||||
layer(stack.id()),
|
||||
"the measured drawing was left on the stack's own layer"
|
||||
);
|
||||
assert_ne!(layer(front.id()), layer(background.id()));
|
||||
// Measured once for the size and once where it goes, which is what the
|
||||
// stack costs and not something this test is asserting a number for.
|
||||
assert_eq!(draws.get(), 2);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Scrolling moves content and stops at its ends.
|
||||
|
||||
use iris::harness::{Harness, assert_corners};
|
||||
use iris::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn scrollable_enables_a_region_node_but_raw_scroll_does_not() {
|
||||
let mut h = Harness::new((100, 100));
|
||||
let default_child = ().add(&mut h.rsc);
|
||||
let _default = default_child.scrollable().add(&mut h.rsc);
|
||||
assert!(h.rsc.widgets().is_region_node(default_child));
|
||||
h.rsc.widgets_mut().set_region_node(default_child, false);
|
||||
assert!(!h.rsc.widgets().is_region_node(default_child));
|
||||
|
||||
let raw_child = ().add(&mut h.rsc);
|
||||
let _raw = Scroll::new(raw_child.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||
assert!(!h.rsc.widgets().is_region_node(raw_child));
|
||||
|
||||
let explicit = ().region_node().add(&mut h.rsc);
|
||||
assert!(h.rsc.widgets().is_region_node(explicit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scrollable_child_can_drop_its_region_node() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||
let content = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
|
||||
h.set_root(content.scrollable());
|
||||
h.rsc.widgets_mut().set_region_node(content, false);
|
||||
h.frame();
|
||||
|
||||
h.move_to((200, 100));
|
||||
h.scroll((0, 1));
|
||||
h.frame();
|
||||
|
||||
assert!(!h.rsc.widgets().is_region_node(content));
|
||||
assert_corners!(h, top, (0, -150), (400, 50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// Twice the window's height, so there is 200 to scroll.
|
||||
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
||||
h.move_to((200, 100));
|
||||
|
||||
// `Scroll` starts snapped to the end.
|
||||
assert_corners!(h, top, (0, -200), (400, 0));
|
||||
|
||||
// The handler scales a wheel line by 50.
|
||||
h.scroll((0, 1));
|
||||
h.frame();
|
||||
assert_corners!(h, top, (0, -150), (400, 50));
|
||||
|
||||
h.scroll((0, 10));
|
||||
h.frame();
|
||||
assert_corners!(h, top, (0, 0), (400, 200));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! What a background task can change, and how it gets back to the ui.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn a_task_update_reaches_the_tree() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let widget = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
|
||||
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
|
||||
}));
|
||||
|
||||
h.click((200, 100));
|
||||
|
||||
assert!(
|
||||
h.await_update(Duration::from_secs(5)),
|
||||
"the task sent no update"
|
||||
);
|
||||
assert_eq!(h.rsc[widget].color, Color::BLUE);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use iris::prelude::*;
|
||||
|
||||
fn editor(text: &str, mode: EditMode) -> (TextEdit, TextData) {
|
||||
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None);
|
||||
(TextEdit::new(view, mode), TextData::default())
|
||||
}
|
||||
|
||||
fn press(edit: &mut TextEditCtx<'_>, x: f32) {
|
||||
edit.select(vec2(x, 10.0), vec2(400.0, 200.0), false, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_an_empty_field_places_input() {
|
||||
let (mut text, mut data) = editor("", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
press(&mut edit, 40.0);
|
||||
edit.insert("hello");
|
||||
|
||||
assert_eq!(edit.text.content(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_replaces_the_previous_composition() {
|
||||
let (mut text, mut data) = editor("", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
press(&mut edit, 0.0);
|
||||
edit.replace(0, "に");
|
||||
edit.replace(1, "日本");
|
||||
|
||||
assert_eq!(edit.text.content(), "日本");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_respects_utf8_boundaries() {
|
||||
let (mut text, mut data) = editor("aé", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
press(&mut edit, f32::MAX);
|
||||
edit.backspace(false);
|
||||
|
||||
assert_eq!(edit.text.content(), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_replaces_the_selection() {
|
||||
let (mut text, mut data) = editor("hello", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
edit.select_all();
|
||||
edit.insert("goodbye");
|
||||
|
||||
assert_eq!(edit.text.content(), "goodbye");
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
//! The smallest trees that laid out differently warm than cold, each shrunk
|
||||
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
|
||||
//! frame that had not settled: a wrapping text shaped at a width it was
|
||||
//! measured in rather than the one it was given. The rest are a widget
|
||||
//! measured again in a box its own answer had decided, where the old answer
|
||||
//! is a fixed point whatever the content now says. The last is neither: one
|
||||
//! box length, composed two ways, landing either side of the boundary that
|
||||
//! decided whether a child was drawn at all.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||
/// taken again -- so no box may move, and a warm frame has to land where a
|
||||
/// cold one does.
|
||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||
let aligned = sized;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||
let stack = Stack {
|
||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
vec![
|
||||
plain.id(),
|
||||
wrapped.id(),
|
||||
sized.id(),
|
||||
aligned.id(),
|
||||
stack.id(),
|
||||
root.id(),
|
||||
]
|
||||
}
|
||||
|
||||
/// The first frame does not reach the layout a second one does, so "cold" is
|
||||
/// not a fixed point and comparing against it compares against a tree that
|
||||
/// has not settled.
|
||||
#[test]
|
||||
fn one_frame_is_enough() {
|
||||
let mut h = Harness::new((640, 900));
|
||||
let ids = plant(&mut h);
|
||||
let first = h.region(&ids[1]).unwrap();
|
||||
for _ in 0..3 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
h.frame();
|
||||
}
|
||||
let settled = h.region(&ids[1]).unwrap();
|
||||
println!(
|
||||
"first frame {} tall, settled {} tall",
|
||||
first.bot_right.y - first.top_left.y,
|
||||
settled.bot_right.y - settled.top_left.y
|
||||
);
|
||||
assert_eq!(
|
||||
first.bot_right.y - first.top_left.y,
|
||||
settled.bot_right.y - settled.top_left.y,
|
||||
"the first frame had not finished laying out"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repainting_everything_moves_nothing() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let ids = plant(&mut warm);
|
||||
for &id in &ids {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let cold_ids = plant(&mut cold);
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||
/// is the same size whatever the output is, so a resize may not change any of
|
||||
/// it -- but the text comes out 3.92px narrower warm than cold.
|
||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let aligned = text;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||
vec![
|
||||
text.id(),
|
||||
aligned.id(),
|
||||
inner.id(),
|
||||
sized.id(),
|
||||
filler.id(),
|
||||
root.id(),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let ids = plant_fixed(&mut warm);
|
||||
warm.frame();
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let cold_ids = plant_fixed(&mut cold);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
||||
/// ends up 29.9px from where the other does.
|
||||
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
||||
let wrapped = wtext("Wrapping shapes one source into as many lines")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.add(&mut h.rsc);
|
||||
let plain = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
|
||||
let second: StrongWidget = plain.add_strong(&mut h.rsc);
|
||||
let children = match swapped {
|
||||
true => vec![second, first],
|
||||
false => vec![first, second],
|
||||
};
|
||||
let span = Span {
|
||||
children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let span_handle = span;
|
||||
let aligned = span;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
||||
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
||||
(
|
||||
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
||||
span_handle,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let (ids, span) = plant_pair(&mut warm, false);
|
||||
warm.frame();
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let (cold_ids, _) = plant_pair(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
||||
/// content from what the content says, and hands that box down through a
|
||||
/// pass-through; the span under it was given that box once, so nothing at its
|
||||
/// own edge says the box was its own answer.
|
||||
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
|
||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let mut inner_children: Vec<StrongWidget> =
|
||||
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
|
||||
if swapped {
|
||||
inner_children.rotate_left(1);
|
||||
}
|
||||
let inner = Span {
|
||||
children: inner_children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let block = rect(Color::RED).add(&mut h.rsc);
|
||||
let fixed = block.width(87).add(&mut h.rsc);
|
||||
let mut outer_children: Vec<StrongWidget> =
|
||||
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||
if swapped {
|
||||
outer_children.rotate_left(1);
|
||||
}
|
||||
let outer = Span {
|
||||
children: outer_children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
// Carried no rule even before rules were a property: it is here to be a
|
||||
// widget between the span and the scroll, not to declare anything.
|
||||
let through = (outer,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||
h.state.root = Some(scroll.add_strong(&mut h.rsc));
|
||||
(
|
||||
vec![
|
||||
text.id(),
|
||||
filler.id(),
|
||||
inner.id(),
|
||||
block.id(),
|
||||
fixed.id(),
|
||||
outer.id(),
|
||||
through.id(),
|
||||
scroll.id(),
|
||||
],
|
||||
[inner, outer],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let (ids, spans) = plant_scrolled(&mut warm, false);
|
||||
warm.frame();
|
||||
for span in spans {
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Reports a width derived from the box it is asked in. Reading through the
|
||||
/// painter is its declaration that the answer holds for that width only.
|
||||
struct Wider {
|
||||
extra: f32,
|
||||
}
|
||||
|
||||
impl Widget for Wider {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
Size {
|
||||
x: Len {
|
||||
px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
|
||||
..Len::ZERO
|
||||
},
|
||||
y: Len::LEFTOVER,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
||||
let content = Wider { extra }.add(&mut h.rsc);
|
||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||
let root = scroll;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
||||
h.set_root(root);
|
||||
(content, scroll.id())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() {
|
||||
let mut warm = Harness::new((100, 100));
|
||||
let (content, scroll) = plant_wider(&mut warm, 50.0);
|
||||
warm.rsc[content].extra = 70.0;
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((100, 100));
|
||||
let (_, cold_scroll) = plant_wider(&mut cold, 70.0);
|
||||
|
||||
assert_eq!(warm.region(&scroll), cold.region(&cold_scroll));
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from 266. `measured`'s box is exactly the height of its
|
||||
/// one fixed child, which is the box a parent sizing itself from that answer
|
||||
/// hands back -- so whether its leftover-only child was drawn at all came down
|
||||
/// to the 0.00003 px the composed length differs by, one way warm and the
|
||||
/// other cold.
|
||||
fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let plain = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let mut pair: Vec<StrongWidget> =
|
||||
vec![filler.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
||||
if swapped {
|
||||
pair.rotate_left(1);
|
||||
}
|
||||
let measured = Span {
|
||||
children: pair,
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
// Takes the whole box on its own, so the span above has nothing left to
|
||||
// divide and `measured` is given exactly the text's height.
|
||||
let whole = rect(Color::RED).add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(whole, None, Some(Len::rel(1.0)));
|
||||
let mut inner_children: Vec<StrongWidget> = vec![
|
||||
measured.add_strong(&mut h.rsc),
|
||||
whole.add_strong(&mut h.rsc),
|
||||
];
|
||||
if swapped {
|
||||
inner_children.rotate_left(1);
|
||||
}
|
||||
let inner = Span {
|
||||
children: inner_children,
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(inner, None, Some(Len::px(198.0)));
|
||||
// One more span above it: without a box composed through it, both trees
|
||||
// round the same way and the boundary is never crossed.
|
||||
let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc);
|
||||
h.set_root(outer);
|
||||
(
|
||||
vec![
|
||||
filler.id(),
|
||||
plain.id(),
|
||||
measured.id(),
|
||||
whole.id(),
|
||||
inner.id(),
|
||||
outer.id(),
|
||||
],
|
||||
[measured, inner],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let (ids, spans) = plant_boundary(&mut warm, false);
|
||||
warm.frame();
|
||||
for span in spans {
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let (cold_ids, _) = plant_boundary(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
Reference in new issue
Block a user