diff --git a/tests/shrink.rs b/tests/shrink.rs index 3c64258..728d7b6 100644 --- a/tests/shrink.rs +++ b/tests/shrink.rs @@ -60,7 +60,11 @@ enum Node { /// The leaf that overflows whatever box it is given rather than wrapping. OneLine, Rect, - Span(bool, f32, Vec), + /// 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, Vec), Stack(Vec), Pad(f32, Box), Aligned(u8, u8, Box), @@ -85,7 +89,12 @@ fn dir(down: bool) -> Dir { impl Node { /// 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. - fn build(&self, h: &mut Harness, out: &mut Vec) -> StrongWidget { + fn build( + &self, + h: &mut Harness, + out: &mut Vec, + spans: &mut Vec>, + ) -> StrongWidget { let id: StrongWidget = match self { Node::Text(words, wrap) => { 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::Rect => rect(Color::RED).add_strong(&mut h.rsc), - Node::Span(down, gap, kids) => { - let children = kids.iter().map(|k| k.build(h, out)).collect(); - Span { + Node::Span(down, gap, kids, order) => { + let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect(); + // `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, dir: dir(*down), gap: *gap, } - .add_strong(&mut h.rsc) + .add(&mut h.rsc); + spans.push(handle); + handle.add_strong(&mut h.rsc) } 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 { children, size: StackSize::Child(0), @@ -114,7 +130,7 @@ impl Node { .add_strong(&mut h.rsc) } Node::Pad(p, kid) => { - let inner = kid.build(h, out); + let inner = kid.build(h, out, spans); Pad { padding: Padding { left: *p, @@ -127,7 +143,7 @@ impl Node { .add_strong(&mut h.rsc) } Node::Aligned(x, y, kid) => { - let inner = kid.build(h, out); + let inner = kid.build(h, out, spans); Aligned { inner, align: Align { @@ -138,7 +154,7 @@ impl Node { .add_strong(&mut h.rsc) } Node::Sized(x, y, kid) => { - let inner = kid.build(h, out); + let inner = kid.build(h, out, spans); SetSize { inner, x: *x, @@ -147,14 +163,14 @@ impl Node { .add_strong(&mut h.rsc) } 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 }; Scroll::new(inner, axis).add_strong(&mut h.rsc) } Node::Branch(probe, a, b, at) => { - let probe = probe.build(h, out); - let wide = a.build(h, out); - let narrow = b.build(h, out); + let probe = probe.build(h, out, spans); + let wide = a.build(h, out, spans); + let narrow = b.build(h, out, spans); Branch { probe, wide, @@ -171,7 +187,7 @@ impl Node { fn size(&self) -> usize { 1 + match self { 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::Aligned(_, _, k) | Node::Sized(_, _, k) @@ -199,23 +215,24 @@ impl Node { } Node::OneLine => out.push(Node::Rect), Node::Rect => {} - Node::Span(down, gap, kids) => { - out.extend(kids.iter().cloned()); + Node::Span(down, gap, kids, order) => { + out.extend(order.iter().map(|&i| kids[i].clone())); for i in 0..kids.len() { if kids.len() > 1 { let mut less = kids.clone(); 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 { - 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 small in kid.smaller() { let mut next = kids.clone(); 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, ), 5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()), - _ => Node::Span( - rng.chance(), - rng.below(3) as f32 * 4.0, - (0..2 + rng.below(3)).map(|_| kid(rng)).collect(), - ), + _ => { + let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect(); + let order = (0..kids.len()).collect(); + Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order) + } } } @@ -359,32 +376,74 @@ enum Case { Resize, Repaint, 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::>(); + 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. fn diverges(node: &Node, case: Case) -> Option { - 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_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); // The frame that makes it warm: without it there is nothing retained and // the comparison is two cold starts agreeing with each other. warm.frame(); - if case != Case::Repaint { + if resizes { warm.resize(INNER); warm.frame(); } - if case != Case::Resize { + if repaints { for &id in &warm_ids { warm.rsc.widgets_mut().get_dyn_mut(id); } 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_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.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() { "repaint" => Case::Repaint, "resize-repaint" => Case::ResizeRepaint, + "reorder" => Case::Reorder, _ => Case::Resize, }; diff --git a/tests/unsettled.rs b/tests/unsettled.rs index d823854..3971e2f 100644 --- a/tests/unsettled.rs +++ b/tests/unsettled.rs @@ -151,3 +151,65 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() { } 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, WeakWidget) { + 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")); +}