A local redraw drew a dirty widget in the box it was placed in. When a reader decided that box from the widget's own answer -- an aligned span sized to its children, a text at the tail of a row, a scroll's content -- the old answer is a fixed point of measuring there whatever the content now says, so the layout had two stable answers and which one it reached depended on the tree's history. `tests/unsettled.rs` has the two shrunk cases: the four-widget aligned span, and a scroll placing a pass-through `SetSize` in a box the content decided, where the span under it was placed once and nothing at its own edge said so. `ActiveData::offered_px` keeps the pixel size of the box the parent first asked about the child in, whether through `known_len` or a first `place`, beside `px`, the box it drew against. A dirty widget whose size reads an axis on which some reader up its chain gave what it read a box other than the one it asked in is not drawn locally: the chain is marked and the parent of the highest such placement draws, since above it every box is a constraint rather than an answer. The walk goes up the whole reader chain because a pass-through hands a derived box down unchanged. `Scroll` read its box's length for the clamp through `px_len`, which records the reported size as depending on it, and it does not: its size is its content's. That made every scroll tick a size question asked in a derived box, at 34x the instructions. `Painter::px_len_for_draw` is the read that records nothing. Instructions per frame on the depth-8 rig against the previous head: `many` at 32 dirty 0.66M to 0.74M, at 130 dirty 27.7M to 26.5M, `resize` 15.8M to 15.0M, `scroll`, `repaint` and `size` unchanged. The shrinking fuzzer passes 200 trees at depth 7 in all four cases, the hundred-seed sweep passes, and the five reference renders and the resize render are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
299 lines
9.4 KiB
Rust
299 lines
9.4 KiB
Rust
//! 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.
|
|
|
|
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 = SetSize {
|
|
inner: wrapped.add_strong(&mut h.rsc),
|
|
x: Some(Len::px(76.0)),
|
|
y: None,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let aligned = Aligned {
|
|
inner: sized.add_strong(&mut h.rsc),
|
|
align: Align {
|
|
x: Some(AxisAlign::Pos),
|
|
y: Some(AxisAlign::Pos),
|
|
},
|
|
}
|
|
.add(&mut h.rsc);
|
|
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 = Aligned {
|
|
inner: text.add_strong(&mut h.rsc),
|
|
align: Align {
|
|
x: Some(AxisAlign::Neg),
|
|
y: None,
|
|
},
|
|
}
|
|
.add(&mut h.rsc);
|
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
|
let sized = SetSize {
|
|
inner: inner.add_strong(&mut h.rsc),
|
|
x: Some(Len::px(189.0)),
|
|
y: Some(Len::px(176.0)),
|
|
}
|
|
.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: 0.0,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let span_handle = span;
|
|
let aligned = Aligned {
|
|
inner: span.add_strong(&mut h.rsc),
|
|
align: Align {
|
|
x: Some(AxisAlign::Center),
|
|
y: None,
|
|
},
|
|
}
|
|
.add(&mut h.rsc);
|
|
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 placed once, in that box, 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: 0.0,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let block = rect(Color::RED).add(&mut h.rsc);
|
|
let fixed = SetSize {
|
|
inner: block.add_strong(&mut h.rsc),
|
|
x: Some(Len::px(87.0)),
|
|
y: None,
|
|
}
|
|
.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: 0.0,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let through = SetSize {
|
|
inner: outer.add_strong(&mut h.rsc),
|
|
x: None,
|
|
y: None,
|
|
}
|
|
.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_placed_once_in_a_box_its_answer_decided() {
|
|
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"));
|
|
}
|