diff --git a/src/random.rs b/src/random.rs index 30700bd..ae3ad94 100644 --- a/src/random.rs +++ b/src/random.rs @@ -84,6 +84,35 @@ pub struct Tree { pub detached: Vec, } +/// Branches on a child's measured length. Comparing boxes catches a widget +/// that moved; this catches one that believed a measurement a cold start +/// would not have given it, by turning that into a different tree. Its own +/// configuration never changes, so which side draws is a property of the +/// layout alone. +pub struct Branch { + pub probe: StrongWidget, + pub wide: StrongWidget, + pub narrow: StrongWidget, + pub threshold: f32, +} + +impl Widget for Branch { + fn draw(&mut self, painter: &mut Painter) -> Size { + let mut top = UiRegion::FULL; + top.y.end = top.y.start.offset(40.0); + let measured = painter.place(&self.probe, top).len(Axis::X); + let px = measured.apply_rest().to_px(painter.px_len(Axis::X)); + + let mut rest = UiRegion::FULL; + rest.y.start = rest.y.start.offset(40.0); + match px > self.threshold { + true => painter.place(&self.wide, rest), + false => painter.place(&self.narrow, rest), + }; + Size::REST + } +} + pub struct Spanned { pub id: WeakWidget, /// Leaves grown with the span whether or not they end up in it, so both @@ -199,6 +228,23 @@ impl Grow<'_, Rsc> { self.tree.ids.push(id.id()); return id.add_strong(self.rsc); } + if positioned == 2 { + // Both sides are grown either way, so a tree that draws one has + // the same ids as a tree that draws the other. + let probe = self.node(depth - 1); + let wide = self.node(depth - 1); + let narrow = self.node(depth - 1); + let threshold = self.rng.below(500) as f32; + let id = Branch { + probe, + wide, + narrow, + threshold, + } + .add(self.rsc); + self.tree.ids.push(id.id()); + return id.add_strong(self.rsc); + } if positioned == 1 { let inner = self.node(depth - 1); let inner = self.sized(inner); diff --git a/tests/determinism.rs b/tests/determinism.rs new file mode 100644 index 0000000..da9ddf6 --- /dev/null +++ b/tests/determinism.rs @@ -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(40.0); + let measured = painter.place(&self.probe, top).len(Axis::X); + let px = measured.apply_rest().to_px(painter.px_len(Axis::X)); + + let mut rest = UiRegion::FULL; + rest.y.start = rest.y.start.offset(40.0); + match px > self.threshold { + true => painter.place(&self.wide, rest), + false => painter.place(&self.narrow, rest), + }; + Size::REST + } +} + +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" + ); + } +} diff --git a/tests/generated.rs b/tests/generated.rs index 124018d..e77f99e 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -224,8 +224,15 @@ fn reshuffled(seed: u64, shuffle: Shuffle) { let mut warm = Harness::new((900, 1200)); let mut grown = plant(&mut warm, seed, &Edits::default()); // Some seeds grow nothing but wrappers, and a shuffle with no span to - // shuffle is not the same thing as one that had no effect. - if grown.spans.is_empty() { + // shuffle is not the same thing as one that had no effect. A span behind + // a branch nobody took is the same kind of nothing: it is not drawn, so + // shuffling it cannot move anything. + let shuffles = grown + .spans + .iter() + .step_by(3) + .any(|span| warm.region(&span.id.id()).is_some()); + if !shuffles { return; } let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();