Track retained layout validity explicitly

This commit is contained in:
iris-ai committed 2026-09-15 16:03:53 -04:00
1 parent 691e3eb23c
commit 29c7881c8a
25 files changed
+836 -795

No files matched your search

+2 -11
View File
@@ -30,7 +30,7 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
const REGION_EPSILON_PX: f32 = 0.05;
fn same_coordinate(got: f32, want: f32) -> bool {
@@ -280,7 +280,6 @@ fn changed_size(seed: u64) {
..Default::default()
},
);
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
}
@@ -299,16 +298,9 @@ fn reshuffled(seed: u64, shuffle: Shuffle) {
if !shuffles {
return;
}
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
warm.frame();
// Or the two trees would agree for want of anything having happened.
let after = grown.ids.iter().map(|id| warm.region(id));
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
@@ -343,7 +335,6 @@ fn changed_every_size(seed: u64) {
..Default::default()
},
);
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same));
}
@@ -448,7 +439,7 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
/// small enough to read.
#[test]
#[ignore = "a hundred seeds, rather than the seven the others check"]
#[ignore = "a hundred seeds, rather than the nine the others check"]
fn a_long_run_of_seeds_agrees() {
let seeds = std::env::var("IRIS_GENERATED_SEED")
.ok()
+31
View File
@@ -349,3 +349,34 @@ fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
}
}
}
#[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.rsc[fixed].x = Some(Len::px(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 = SetSize {
inner: rect(Color::BLUE).add_strong(&mut h.rsc),
x: Some(Len::px(20) + Len::LEFTOVER),
y: None,
}
.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));
}
+83 -53
View File
@@ -6,22 +6,22 @@ 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.
/// 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,
dependence: OnResize,
reads_box: bool,
}
impl Widget for Counted {
fn draw(&mut self, _: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
if self.reads_box {
painter.px_size();
}
self.size
}
fn on_resize(&self, _: Axis) -> OnResize {
self.dependence
}
}
struct Counts(Rc<Cell<usize>>);
@@ -32,22 +32,63 @@ impl Counts {
}
}
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
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,
dependence,
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, leftover: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
let (second, draws) = counted(h, Size::LEFTOVER, leftover);
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())
}
@@ -55,7 +96,7 @@ fn pair(h: &mut Harness, leftover: OnResize) -> (WeakWidget<Counted>, Counts, Wi
#[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, OnResize::Scale);
let (first, draws, second) = pair(&mut h, false);
let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200));
@@ -73,7 +114,7 @@ fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
#[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, OnResize::Redraw);
let (first, draws, second) = pair(&mut h, true);
let settled = draws.get();
h.rsc[first].size = Size::from((150, 200));
@@ -88,8 +129,8 @@ fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
#[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)), OnResize::Translate);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
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 it is placed.
let hinted = told.width(100).add(&mut h.rsc);
@@ -106,7 +147,7 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
#[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, OnResize::Translate);
let (first, _, second) = pair(&mut h, false);
h.rsc[first].size = Size::from((250, 200));
h.frame();
@@ -118,8 +159,8 @@ fn a_span_relays_out_when_a_child_it_measured_changes() {
#[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)), OnResize::Translate);
let (second, _) = counted(&mut h, Size::LEFTOVER, OnResize::Translate);
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();
@@ -179,20 +220,20 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads the output's size, which nothing but its own draw can put right.
struct ReadsOutput {
/// Reads its box's size, which nothing but its own draw can put right.
struct ReadsBox {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsOutput {
impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::px(painter.output_size() / 4.0)
Size::px(painter.px_size() / 4.0)
}
}
/// Reads the output across one axis only, and says so: its drawing follows
/// a taller box on its own, so only a wider one is worth a draw.
/// 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.
struct ReadsWidth {
draws: Rc<Cell<usize>>,
}
@@ -200,21 +241,14 @@ struct ReadsWidth {
impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::px((painter.output_len(Axis::X) / 4.0, 20.0).into())
}
fn on_resize(&self, axis: Axis) -> OnResize {
match axis {
Axis::X => OnResize::Redraw,
Axis::Y => OnResize::Scale,
}
Size::px((painter.px_len(Axis::X) / 4.0, 20.0).into())
}
}
#[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, OnResize::Scale);
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
h.set_root(leaf);
let settled = draws.get();
@@ -231,12 +265,12 @@ fn a_resize_does_not_redraw_what_the_shader_can_move() {
}
/// The output is the root of the box chain, so a resize is a box that changed
/// length and `OnResize` answers for it -- there is not a second rule for the
/// window. A drawing that does not scale is redrawn whichever box moved.
/// 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, OnResize::Redraw);
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, true);
h.set_root(leaf);
let settled = draws.get();
@@ -248,10 +282,10 @@ fn a_resize_redraws_what_does_not_scale() {
}
#[test]
fn a_resize_redraws_what_read_the_output() {
fn a_resize_redraws_what_read_its_box() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsOutput {
let leaf = ReadsBox {
draws: draws.clone(),
}
.add(&mut h.rsc);
@@ -265,7 +299,7 @@ fn a_resize_redraws_what_read_the_output() {
}
#[test]
fn a_resize_only_redraws_read_output_axes() {
fn a_resize_only_redraws_read_axes() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
@@ -309,7 +343,7 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() {
#[test]
fn subpixel_box_changes_accumulate_from_the_last_draw() {
let mut h = Harness::new((400, 200));
let (first, draws, _) = pair(&mut h, OnResize::Redraw);
let (first, draws, _) = pair(&mut h, true);
let settled = draws.get();
for width in [100.02, 100.04, 100.05] {
@@ -327,7 +361,7 @@ fn subpixel_box_changes_accumulate_from_the_last_draw() {
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 = ReadsOutput {
let leaf = ReadsBox {
draws: draws.clone(),
}
.add(&mut h.rsc);
@@ -368,7 +402,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// 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()), OnResize::Redraw);
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));
@@ -380,8 +414,8 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
assert_corners!(h, below, (12, 232), (388, 388));
}
/// Claims its drawing survives its box changing length, and has a child so
/// that the walk looking for what does not has one to reach.
/// 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>>,
@@ -392,10 +426,6 @@ impl Widget for Stretchy {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.inner).size()
}
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
}
#[test]
@@ -430,8 +460,8 @@ 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, OnResize::Redraw);
let (backing, back_draws) = counted(&mut h, Size::LEFTOVER, OnResize::Scale);
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));
@@ -455,9 +485,9 @@ fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
// 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)), OnResize::Redraw);
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, OnResize::Scale);
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));
+43
View File
@@ -296,3 +296,46 @@ fn a_span_placed_once_in_a_box_its_answer_decided() {
}
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) + self.extra),
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 = Aligned {
inner: scroll.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Neg),
y: None,
},
}
.add(&mut h.rsc);
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));
}