Decide layout on the grid end to end, and delete the tolerance
`Px` and `PxVec2` reach the last places a pixel was a float: the window, the box a widget reads, the box it is compared against, and `PixelRegion`. A pointer, a wheel notch and a shaped glyph advance still arrive as floats, and each is put on the grid where it arrives. `Holds` is an interval of `Px`. `HOLDS_EPSILON_PX` is gone with the `exact`/tolerant split it existed for: `at` is the length a widget read, an open end is the next step along, and `same_px` is equality. `Span`'s margin from `5ed9e87` goes too -- the box a parent hands back and the sum of what its children asked for are counts of the same step, so the boundary decides the same way from either side. Three things had to be true for that, and were not: `Holds::through` inverts `px + rel * box`, which rounds -- so a part of a given length came from a range of boxes, and inverting the length alone gave a point that need not contain the box the part was drawn in. It now maps the half step either side, and one more for a length composed down the chain against the same length measured against the window. `RegionRemap` translates when a box only moved, rather than dividing to find each part's fraction and multiplying to place it again. Two roundings landed a step from where growing the tree that way does; a move is exact on a grid, which is the whole reason `tests/drift.rs` was written. A pixel is `1/1024` rather than `1/64`. At `1/64` the residue of a length reached two ways was one step, and one step was 0.016 px -- enough to move a box. `PX_SHIFT` and `REL_SHIFT` are the only statement of the grid now, and the shader's copy is prepended from them rather than written twice. Checked: fmt, clippy, 102 tests, 100 generated seeds in 75 s, all five shrinker cases at 300 seeds, and `tabs`, `view`, `minimal`, `text` and `random` byte-identical at 1920x1200. What the fuzzers ask for is now a step, not a twentieth of a pixel: the shrinker's five cases agree within one (`resize` exactly), and the oracle's two-operation cases within two. The residue is a single rounding either way -- it scales with the grid rather than accumulating, which is why it is a thousandth of a pixel now. Closing it means one way of asking how long a box is, rather than a chain composed down and a length measured against the window; that is a bigger change than this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd6de71a55
commit
39e4ca20e6
24 files changed
+420
-194
No files matched your search
@@ -24,14 +24,11 @@ impl Widget for BranchesOnMeasurement {
|
||||
let mut top = UiRegion::FULL;
|
||||
top.y.end = top.y.start.offset(Px::from_int(40));
|
||||
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
|
||||
let px = measured
|
||||
.apply_leftover()
|
||||
.to_px(Px::from_f32(painter.px_len(Axis::X)))
|
||||
.to_f32();
|
||||
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
|
||||
|
||||
let mut below = UiRegion::FULL;
|
||||
below.y.start = below.y.start.offset(Px::from_int(40));
|
||||
match px > self.threshold {
|
||||
match px > Px::from_f32(self.threshold) {
|
||||
true => painter.widget_within(&self.wide, below),
|
||||
false => painter.widget_within(&self.narrow, below),
|
||||
};
|
||||
|
||||
+12
-8
@@ -30,19 +30,23 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
|
||||
const REGION_EPSILON_PX: f32 = 0.05;
|
||||
|
||||
fn same_coordinate(got: f32, want: f32) -> bool {
|
||||
(got - want).abs() <= REGION_EPSILON_PX
|
||||
}
|
||||
/// The same box, to a step of the grid per operation. Warm and cold reach a
|
||||
/// coordinate by different arithmetic: a move lands on the same number now,
|
||||
/// and a length composed one way against the same length measured another can
|
||||
/// land one step out. These cases apply two operations in turn, so they allow
|
||||
/// two steps -- a thousandth of a pixel each, where this was a twentieth of
|
||||
/// one before any of it was on a grid.
|
||||
const AGREE_STEPS: i32 = 2;
|
||||
|
||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
||||
match (got, want) {
|
||||
(Some(got), Some(want)) => {
|
||||
same_coordinate(got.top_left.x, want.top_left.x)
|
||||
&& same_coordinate(got.top_left.y, want.top_left.y)
|
||||
&& same_coordinate(got.bot_right.x, want.bot_right.x)
|
||||
&& same_coordinate(got.bot_right.y, want.bot_right.y)
|
||||
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
|
||||
same(got.top_left.x, want.top_left.x)
|
||||
&& same(got.top_left.y, want.top_left.y)
|
||||
&& same(got.bot_right.x, want.bot_right.x)
|
||||
&& same(got.bot_right.y, want.bot_right.y)
|
||||
}
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
|
||||
+22
-10
@@ -255,7 +255,7 @@ struct ReadsBox {
|
||||
impl Widget for ReadsBox {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
Size::px(painter.px_size() / 4.0)
|
||||
Size::from_px(painter.px_size().div_int(4))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,10 @@ struct ReadsWidth {
|
||||
impl Widget for ReadsWidth {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
Size::px((painter.px_len(Axis::X) / 4.0, 20.0).into())
|
||||
Size::from_px(PxVec2::new(
|
||||
painter.px_len(Axis::X).div_int(4),
|
||||
Px::from_int(20),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,8 +372,11 @@ fn a_resize_only_redraws_read_axes() {
|
||||
assert_eq!(draws.get(), settled + 2, "width changes its answer");
|
||||
}
|
||||
|
||||
/// A window is measured onto the grid like everything else, so a resize too
|
||||
/// small to reach the next step is not a resize at all -- and one that does
|
||||
/// reach it is, however little of a pixel it is worth.
|
||||
#[test]
|
||||
fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
||||
fn a_resize_within_one_step_is_not_a_resize() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsWidth {
|
||||
@@ -380,30 +386,36 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
for width in [400.02, 400.04, 400.05] {
|
||||
h.resize((width, 200.0));
|
||||
// All of these are 400 px to the nearest step.
|
||||
let step = Px::STEP.to_f32();
|
||||
for part in [0.1, 0.2, 0.3] {
|
||||
h.resize((400.0 + step * part, 200.0));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled);
|
||||
}
|
||||
|
||||
h.resize((400.06, 200.0));
|
||||
h.resize((400.0 + step, 200.0));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 2);
|
||||
}
|
||||
|
||||
/// The same for a box that changes because a sibling did: what is compared
|
||||
/// is the length on the grid, and three lengths that land on one step are
|
||||
/// one length.
|
||||
#[test]
|
||||
fn subpixel_box_changes_accumulate_from_the_last_draw() {
|
||||
fn a_box_change_within_one_step_is_not_a_change() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, draws, _) = pair(&mut h, true);
|
||||
let settled = draws.get();
|
||||
|
||||
for width in [100.02, 100.04, 100.05] {
|
||||
h.rsc[first].size.x = Len::px(width);
|
||||
let step = Px::STEP.to_f32();
|
||||
for part in [0.1, 0.2, 0.3] {
|
||||
h.rsc[first].size.x = Len::px(100.0 + step * part);
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled);
|
||||
}
|
||||
|
||||
h.rsc[first].size.x = Len::px(100.06);
|
||||
h.rsc[first].size.x = Len::px(100.0 + step);
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
}
|
||||
|
||||
+4
-1
@@ -515,9 +515,12 @@ fn diverges(node: &Node, case: Case) -> Option<String> {
|
||||
|
||||
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
// To one step of the grid. A move or a resize lands on the same
|
||||
// number now; a length measured one way and composed another can
|
||||
// still be a step apart.
|
||||
let same = match (got, want) {
|
||||
(Some(g), Some(c)) => {
|
||||
let d = |a: f32, b: f32| (a - b).abs() <= 0.05;
|
||||
let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP;
|
||||
d(g.top_left.x, c.top_left.x)
|
||||
&& d(g.top_left.y, c.top_left.y)
|
||||
&& d(g.bot_right.x, c.bot_right.x)
|
||||
|
||||
+4
-1
@@ -284,7 +284,10 @@ struct Wider {
|
||||
impl Widget for Wider {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
Size {
|
||||
x: Len::px(painter.px_len(Axis::X) + self.extra),
|
||||
x: Len {
|
||||
px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
|
||||
..Len::ZERO
|
||||
},
|
||||
y: Len::LEFTOVER,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user