There is one coordinate unit, the window. Every box in the tree is a region in window units and a widget's frame is a length in the same units, which is only what fractions resolve against, so the box need not be the frame and padding can take from both without either becoming the other. A region node's entry is a translation -- a rel 1 region anchored where its box starts -- rather than a box, so nothing composes a frame back up a chain and a node that moves is one entry write. Padding is then an inset of both: its pixels come off the frame, so rel(1.0) under it fills the padded widget rather than overflowing it, and off the box, so what is drawn sits inside. A length a container decides for a child's frame is a length of the window like everything else here -- a row's slot, padding's frame less its pixels, or the box a stack's sizing child decided, which arrives as Part::Sized -- because a slot of a row is not a fraction of anything the row can name, the same reason a node entry is a translation. A declaration is a fraction of whichever of those reached it, and is the only one that also places the box. Frame validity is a pin beside the box's, not a range: a range of window pixels cannot say which frame an answer is a fraction of, since two frames are different lengths at the same window size. A widget pins its frame by reading it or by being answered with it under a fractional rule, and the pin composes up wherever a length of this frame is what reached the child. Also here, because the diagnosis needed them: the shrinker reports the shrunk tree's own divergence with each level's frame, ask, box and size warm against cold, and there is a size-resize case -- a change and then a resize, the order that shows an answer kept as a fraction of the wrong length, which every other case compares at the window it was made at. Three defects the reports found, each pinned: a rule changed over two pads relocated the column under them instead of dividing it again (seed 59, depth 5, resize-size), a share inside padding had the padding taken off twice, and a root resolved its own rule twice. fmt and clippy clean with and without layout-diagnostics, 121 suite, 20 core, 11 generated, the 400-seed depth-5 shrinker over all sixteen cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
3.9 KiB
Rust
106 lines
3.9 KiB
Rust
//! 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 cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
|
|
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
|
|
let measured = painter
|
|
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
|
|
.len(Axis::X);
|
|
let px = measured
|
|
.apply_leftover()
|
|
.to_px(painter.window_px_len(Axis::X));
|
|
|
|
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
|
|
let place = [Place::Within(Part::All), below];
|
|
match px > Px::from_f32(self.threshold) {
|
|
true => painter.widget_at(&self.wide, [None; 2], place),
|
|
false => painter.widget_at(&self.narrow, [None; 2], place),
|
|
};
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
|
|
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"
|
|
);
|
|
}
|
|
}
|