Reorder a span's children in the fuzzer, and find two fixed points
The shrinking fuzzer had no case for what `generated.rs` calls a reshuffle,
which was the only thing still failing there. `Case::Reorder` rotates every
span's children after a warm frame and compares against a tree grown that
way -- which needs a span's creation order kept apart from the order its
children are attached in, or the two trees make the same widgets in
different orders and cannot be lined up.
It found a four-widget tree, from 486, and the trace says the layout has
more than one answer rather than one answer reached twice.
Aligned(mid, -, Span[ Text(wrap), OneLine ])
A span measures its children in its own box. Its own box is what its parent
gave it, from the size it reported, from those children. So with the
wrapping text second it is offered `cursor..end` of a span 663.376 wide and
asked for 357.44, which is what it already holds -- the size is valid, the
span reports 663.376 again, and nothing moves. Grown in that order from
scratch the span is offered the window, the text is asked for 334.06 and
answers 318.45, and the span settles at 624.38. Both are stable. Which one
you get depends on what the tree was before.
So this is not a stale drawing kept too long, and no rule about when to
keep one will fix it: it is a circular dependency with two solutions.
`Painter::settle` in `Aligned` -- place into the child's own size without
measuring there -- makes all four cases in `unsettled.rs` pass and breaks
two in `generated.rs`, whether or not the child is drawn first. Not kept;
the shape of the fix is the constraint a container measures under being
something it is given rather than something it ends up with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99131940ab
commit
65f68bbb8a
2 files changed
+152
-30
No files matched your search
+90
-30
@@ -60,7 +60,11 @@ enum Node {
|
|||||||
/// The leaf that overflows whatever box it is given rather than wrapping.
|
/// The leaf that overflows whatever box it is given rather than wrapping.
|
||||||
OneLine,
|
OneLine,
|
||||||
Rect,
|
Rect,
|
||||||
Span(bool, f32, Vec<Node>),
|
/// Direction, gap, children in creation order, and the order they are
|
||||||
|
/// attached in -- separate so a tree that reorders its children
|
||||||
|
/// still makes the same widgets in the same order, and two
|
||||||
|
/// builds line up index for index.
|
||||||
|
Span(bool, f32, Vec<Node>, Vec<usize>),
|
||||||
Stack(Vec<Node>),
|
Stack(Vec<Node>),
|
||||||
Pad(f32, Box<Node>),
|
Pad(f32, Box<Node>),
|
||||||
Aligned(u8, u8, Box<Node>),
|
Aligned(u8, u8, Box<Node>),
|
||||||
@@ -85,7 +89,12 @@ fn dir(down: bool) -> Dir {
|
|||||||
impl Node {
|
impl Node {
|
||||||
/// Builds into `h`, pushing every id in tree order, so two builds of one
|
/// Builds into `h`, pushing every id in tree order, so two builds of one
|
||||||
/// node line up index for index and their boxes can be compared.
|
/// node line up index for index and their boxes can be compared.
|
||||||
fn build(&self, h: &mut Harness, out: &mut Vec<WidgetId>) -> StrongWidget {
|
fn build(
|
||||||
|
&self,
|
||||||
|
h: &mut Harness,
|
||||||
|
out: &mut Vec<WidgetId>,
|
||||||
|
spans: &mut Vec<WeakWidget<Span>>,
|
||||||
|
) -> StrongWidget {
|
||||||
let id: StrongWidget = match self {
|
let id: StrongWidget = match self {
|
||||||
Node::Text(words, wrap) => {
|
Node::Text(words, wrap) => {
|
||||||
let n = (*words).clamp(1, WORDS.len());
|
let n = (*words).clamp(1, WORDS.len());
|
||||||
@@ -96,17 +105,24 @@ impl Node {
|
|||||||
}
|
}
|
||||||
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
||||||
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
||||||
Node::Span(down, gap, kids) => {
|
Node::Span(down, gap, kids, order) => {
|
||||||
let children = kids.iter().map(|k| k.build(h, out)).collect();
|
let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect();
|
||||||
Span {
|
// `order` is a permutation, so each is taken exactly once.
|
||||||
|
let children = order
|
||||||
|
.iter()
|
||||||
|
.map(|&i| built[i].take().expect("order repeats an index"))
|
||||||
|
.collect();
|
||||||
|
let handle = Span {
|
||||||
children,
|
children,
|
||||||
dir: dir(*down),
|
dir: dir(*down),
|
||||||
gap: *gap,
|
gap: *gap,
|
||||||
}
|
}
|
||||||
.add_strong(&mut h.rsc)
|
.add(&mut h.rsc);
|
||||||
|
spans.push(handle);
|
||||||
|
handle.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Stack(kids) => {
|
Node::Stack(kids) => {
|
||||||
let children = kids.iter().map(|k| k.build(h, out)).collect();
|
let children = kids.iter().map(|k| k.build(h, out, spans)).collect();
|
||||||
Stack {
|
Stack {
|
||||||
children,
|
children,
|
||||||
size: StackSize::Child(0),
|
size: StackSize::Child(0),
|
||||||
@@ -114,7 +130,7 @@ impl Node {
|
|||||||
.add_strong(&mut h.rsc)
|
.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Pad(p, kid) => {
|
Node::Pad(p, kid) => {
|
||||||
let inner = kid.build(h, out);
|
let inner = kid.build(h, out, spans);
|
||||||
Pad {
|
Pad {
|
||||||
padding: Padding {
|
padding: Padding {
|
||||||
left: *p,
|
left: *p,
|
||||||
@@ -127,7 +143,7 @@ impl Node {
|
|||||||
.add_strong(&mut h.rsc)
|
.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Aligned(x, y, kid) => {
|
Node::Aligned(x, y, kid) => {
|
||||||
let inner = kid.build(h, out);
|
let inner = kid.build(h, out, spans);
|
||||||
Aligned {
|
Aligned {
|
||||||
inner,
|
inner,
|
||||||
align: Align {
|
align: Align {
|
||||||
@@ -138,7 +154,7 @@ impl Node {
|
|||||||
.add_strong(&mut h.rsc)
|
.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Sized(x, y, kid) => {
|
Node::Sized(x, y, kid) => {
|
||||||
let inner = kid.build(h, out);
|
let inner = kid.build(h, out, spans);
|
||||||
SetSize {
|
SetSize {
|
||||||
inner,
|
inner,
|
||||||
x: *x,
|
x: *x,
|
||||||
@@ -147,14 +163,14 @@ impl Node {
|
|||||||
.add_strong(&mut h.rsc)
|
.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Scroll(down, kid) => {
|
Node::Scroll(down, kid) => {
|
||||||
let inner = kid.build(h, out);
|
let inner = kid.build(h, out, spans);
|
||||||
let axis = if *down { Axis::Y } else { Axis::X };
|
let axis = if *down { Axis::Y } else { Axis::X };
|
||||||
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Branch(probe, a, b, at) => {
|
Node::Branch(probe, a, b, at) => {
|
||||||
let probe = probe.build(h, out);
|
let probe = probe.build(h, out, spans);
|
||||||
let wide = a.build(h, out);
|
let wide = a.build(h, out, spans);
|
||||||
let narrow = b.build(h, out);
|
let narrow = b.build(h, out, spans);
|
||||||
Branch {
|
Branch {
|
||||||
probe,
|
probe,
|
||||||
wide,
|
wide,
|
||||||
@@ -171,7 +187,7 @@ impl Node {
|
|||||||
fn size(&self) -> usize {
|
fn size(&self) -> usize {
|
||||||
1 + match self {
|
1 + match self {
|
||||||
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
||||||
Node::Span(_, _, kids) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
|
Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
|
||||||
Node::Pad(_, k)
|
Node::Pad(_, k)
|
||||||
| Node::Aligned(_, _, k)
|
| Node::Aligned(_, _, k)
|
||||||
| Node::Sized(_, _, k)
|
| Node::Sized(_, _, k)
|
||||||
@@ -199,23 +215,24 @@ impl Node {
|
|||||||
}
|
}
|
||||||
Node::OneLine => out.push(Node::Rect),
|
Node::OneLine => out.push(Node::Rect),
|
||||||
Node::Rect => {}
|
Node::Rect => {}
|
||||||
Node::Span(down, gap, kids) => {
|
Node::Span(down, gap, kids, order) => {
|
||||||
out.extend(kids.iter().cloned());
|
out.extend(order.iter().map(|&i| kids[i].clone()));
|
||||||
for i in 0..kids.len() {
|
for i in 0..kids.len() {
|
||||||
if kids.len() > 1 {
|
if kids.len() > 1 {
|
||||||
let mut less = kids.clone();
|
let mut less = kids.clone();
|
||||||
less.remove(i);
|
less.remove(i);
|
||||||
out.push(Node::Span(*down, *gap, less));
|
let order = (0..less.len()).collect();
|
||||||
|
out.push(Node::Span(*down, *gap, less, order));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *gap != 0.0 {
|
if *gap != 0.0 {
|
||||||
out.push(Node::Span(*down, 0.0, kids.clone()));
|
out.push(Node::Span(*down, 0.0, kids.clone(), order.clone()));
|
||||||
}
|
}
|
||||||
for (i, kid) in kids.iter().enumerate() {
|
for (i, kid) in kids.iter().enumerate() {
|
||||||
for small in kid.smaller() {
|
for small in kid.smaller() {
|
||||||
let mut next = kids.clone();
|
let mut next = kids.clone();
|
||||||
next[i] = small;
|
next[i] = small;
|
||||||
out.push(Node::Span(*down, *gap, next));
|
out.push(Node::Span(*down, *gap, next, order.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,11 +363,11 @@ fn grow(rng: &mut Rng, depth: usize) -> Node {
|
|||||||
rng.below(500) as f32,
|
rng.below(500) as f32,
|
||||||
),
|
),
|
||||||
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
|
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
|
||||||
_ => Node::Span(
|
_ => {
|
||||||
rng.chance(),
|
let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect();
|
||||||
rng.below(3) as f32 * 4.0,
|
let order = (0..kids.len()).collect();
|
||||||
(0..2 + rng.below(3)).map(|_| kid(rng)).collect(),
|
Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order)
|
||||||
),
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,32 +376,74 @@ enum Case {
|
|||||||
Resize,
|
Resize,
|
||||||
Repaint,
|
Repaint,
|
||||||
ResizeRepaint,
|
ResizeRepaint,
|
||||||
|
Reorder,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every span's children rotated by one, as a tree rather than as a change:
|
||||||
|
/// what a warm frame reaches by moving them has to be where growing them that
|
||||||
|
/// way lands.
|
||||||
|
fn reordered(node: &Node) -> Node {
|
||||||
|
match node {
|
||||||
|
Node::Span(down, gap, kids, order) => {
|
||||||
|
let kids = kids.iter().map(reordered).collect::<Vec<_>>();
|
||||||
|
let mut order = order.clone();
|
||||||
|
order.rotate_left(1);
|
||||||
|
Node::Span(*down, *gap, kids, order)
|
||||||
|
}
|
||||||
|
Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()),
|
||||||
|
Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))),
|
||||||
|
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))),
|
||||||
|
Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))),
|
||||||
|
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))),
|
||||||
|
Node::Branch(p, a, b, at) => Node::Branch(
|
||||||
|
Box::new(reordered(p)),
|
||||||
|
Box::new(reordered(a)),
|
||||||
|
Box::new(reordered(b)),
|
||||||
|
*at,
|
||||||
|
),
|
||||||
|
leaf => leaf.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs one scenario warm and cold and says where they disagree.
|
/// Runs one scenario warm and cold and says where they disagree.
|
||||||
fn diverges(node: &Node, case: Case) -> Option<String> {
|
fn diverges(node: &Node, case: Case) -> Option<String> {
|
||||||
let start = if case == Case::Repaint { INNER } else { OUTER };
|
let resizes = matches!(case, Case::Resize | Case::ResizeRepaint);
|
||||||
|
let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint);
|
||||||
|
let start = if resizes { OUTER } else { INNER };
|
||||||
let mut warm = Harness::new(start);
|
let mut warm = Harness::new(start);
|
||||||
let mut warm_ids = Vec::new();
|
let mut warm_ids = Vec::new();
|
||||||
let root = node.build(&mut warm, &mut warm_ids);
|
let mut warm_spans = Vec::new();
|
||||||
|
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans);
|
||||||
warm.state.root = Some(root);
|
warm.state.root = Some(root);
|
||||||
// The frame that makes it warm: without it there is nothing retained and
|
// The frame that makes it warm: without it there is nothing retained and
|
||||||
// the comparison is two cold starts agreeing with each other.
|
// the comparison is two cold starts agreeing with each other.
|
||||||
warm.frame();
|
warm.frame();
|
||||||
if case != Case::Repaint {
|
if resizes {
|
||||||
warm.resize(INNER);
|
warm.resize(INNER);
|
||||||
warm.frame();
|
warm.frame();
|
||||||
}
|
}
|
||||||
if case != Case::Resize {
|
if repaints {
|
||||||
for &id in &warm_ids {
|
for &id in &warm_ids {
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
}
|
}
|
||||||
warm.frame();
|
warm.frame();
|
||||||
}
|
}
|
||||||
|
if case == Case::Reorder {
|
||||||
|
for span in &warm_spans {
|
||||||
|
warm.rsc[*span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the warm tree was moved into, grown that way from the start.
|
||||||
|
let want = match case {
|
||||||
|
Case::Reorder => reordered(node),
|
||||||
|
_ => node.clone(),
|
||||||
|
};
|
||||||
let mut cold = Harness::new(INNER);
|
let mut cold = Harness::new(INNER);
|
||||||
let mut cold_ids = Vec::new();
|
let mut cold_ids = Vec::new();
|
||||||
let root = node.build(&mut cold, &mut cold_ids);
|
let mut cold_spans = Vec::new();
|
||||||
|
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans);
|
||||||
cold.state.root = Some(root);
|
cold.state.root = Some(root);
|
||||||
cold.frame();
|
cold.frame();
|
||||||
|
|
||||||
@@ -437,6 +496,7 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
|||||||
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
|
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
|
||||||
"repaint" => Case::Repaint,
|
"repaint" => Case::Repaint,
|
||||||
"resize-repaint" => Case::ResizeRepaint,
|
"resize-repaint" => Case::ResizeRepaint,
|
||||||
|
"reorder" => Case::Reorder,
|
||||||
_ => Case::Resize,
|
_ => Case::Resize,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -151,3 +151,65 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
|||||||
}
|
}
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
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"));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user