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:
iris-aiandClaude Opus 5 committed 2026-09-16 02:56:48 -04:00
1 parent bd6de71a55
commit 39e4ca20e6
24 files changed
+420 -194

No files matched your search

+30 -14
View File
@@ -3,8 +3,8 @@ use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, placed_box};
use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, Weight,
WidgetId, Widgets,
PixelRegion, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan,
Weight, WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -23,7 +23,7 @@ pub(super) struct DrawInfo {
/// The box it was first asked about in, as a part of its parent's, and
/// that box in pixels.
pub offer: UiRegion,
pub offered_px: Vec2,
pub offered_px: PxVec2,
/// A container's answer for where the widget sits. `None` uses the
/// widget's own property.
pub align: Option<RegionAlign>,
@@ -32,7 +32,7 @@ pub(super) struct DrawInfo {
pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers,
pub(super) output_size: Vec2,
pub(super) output_size: PxVec2,
old_root: Option<WidgetId>,
/// The slot every chain bottoms out in, holding the output as a box.
@@ -58,7 +58,7 @@ impl UiRenderState {
Self {
active: Default::default(),
layers: Default::default(),
output_size: Vec2::ZERO,
output_size: PxVec2::ZERO,
old_root: None,
slots: Default::default(),
answer_invalid: Default::default(),
@@ -75,8 +75,14 @@ impl UiRenderState {
/// downstream has to know the output's size to resolve a position.
fn write_root(&mut self) {
let region = UiRegion::new(
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)),
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)),
UiSpan::new(
UiScalar::ZERO,
UiScalar::from_parts(Rel::ZERO, self.output_size.x),
),
UiSpan::new(
UiScalar::ZERO,
UiScalar::from_parts(Rel::ZERO, self.output_size.y),
),
);
match self.root_move == MoveIdx::NONE {
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
@@ -84,8 +90,10 @@ impl UiRenderState {
}
}
/// The window, in whatever the platform measures it in, onto the grid
/// everything below it is decided on.
pub fn resize(&mut self, size: impl Into<Vec2>) {
let size = size.into();
let size = PxVec2::from_f32(size.into());
if size == self.output_size {
return;
}
@@ -108,7 +116,7 @@ impl UiRenderState {
}
}
pub fn output_size(&self) -> Vec2 {
pub fn output_size(&self) -> PxVec2 {
self.output_size
}
@@ -447,7 +455,7 @@ impl UiRenderState {
}
/// The pixel size of a region held in `slot`'s coordinates.
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 {
self.moves
.resolve(slot, region)
.size()
@@ -460,7 +468,7 @@ impl UiRenderState {
pub(super) fn retained_size(
&self,
id: WidgetId,
px: Vec2,
px: PxVec2,
parent_move: MoveIdx,
widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> {
@@ -1003,8 +1011,10 @@ impl UiRenderState {
}
}
fn same_px(a: Vec2, b: Vec2) -> bool {
Holds::at(a.x).contains(b.x) && Holds::at(a.y).contains(b.y)
/// The same box is the same number of steps, both of these being lengths on
/// the grid rather than floats to be compared for nearness.
fn same_px(a: PxVec2, b: PxVec2) -> bool {
a == b
}
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
@@ -1045,7 +1055,13 @@ impl RegionRemap {
fn apply_scalar(self, scalar: UiScalar, from: UiSpan, to: UiSpan) -> UiScalar {
let extent = from.end.rel - from.start.rel;
if extent == Rel::ZERO {
// A box that only moved, or that has no relative extent to divide,
// carries its parts by moving them, which is exact. Dividing to find
// the fraction each sits at and multiplying to place it again are two
// roundings, and they land a step from where growing the tree that
// way does. Where the box changed length there is nothing else to do,
// and the fraction is what a part means.
if from.len() == to.len() || extent == Rel::ZERO {
return scalar + to.start - from.start;
}
let fraction = (scalar.rel - from.start.rel) / extent;