Commit Graph
4 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 4e28f1047e Put positions on the grid, and decode them in the shader
`UiScalar` is `Rel` beside `Px` rather than two floats, so composing a
position down a chain of boxes adds exactly and rounds only at the two
multiplies `within` makes. `UiSpan`, `UiRegion` and `UiVec2` follow it, the
hand-written `Hash` goes away with the bits it hashed, and `impl_op!` grows a
`same` form for a type whose fields are not the same kind of number.

`Len` is still floats, so the seam converts: `Px::from_f32` where a span adds
a child's length to its cursor, and `to_f32` where something outside layout
wants pixels. Those go when `Len` follows.

The GPU reads what the CPU wrote: the instance attributes are `Sint32x2` and
the shader decodes by `1/64` and `1/2^24`, both exact in `f32`, then composes
the move chain in floats as before. It has to agree with itself frame to
frame rather than with the CPU to the last bit.

Two things fell out of making the numbers exact.

`floor` at the rasteriser was picking the pixel below wherever a fraction
divided a window exactly. A fifth of 1920 is 383.99998 through a rounded
`Rel` -- and was 384.0 through an `f32` that happened to round up -- so five
tabs each lost their last column. `snap_floor` takes a coordinate within half
a step of a boundary to be on it, which is the same rule as everywhere else
here: decide where values do not land.

A widget measured on one layer and drawn again on another kept the first
layer, because `try_reuse` compared everything about a retained drawing
except which list it sits in. `Stack` does exactly that for its background,
so every panel's text went under its own background. It only worked before
because the two asks differed by a rounding and forced a redraw;
`tests/retained.rs` pins it now, and `ReuseOutcome` can say `WrongLayer`.

Checked: fmt, clippy, 100 tests, 100 generated seeds in 70 s, all five
shrinker cases at 300 seeds. `tabs`, `view` and `minimal` render
byte-identical at 1920x1200; `random` differs in 36 pixels by one level;
`text` differs where glyph origins moved onto the grid -- same positions,
same spacing, different subpixel coverage, checked at 6x against the old
render.

Measured on the way: with the fuzzer comparing for *equality* rather than
within 0.05 px, `resize`, `repaint` and `size-change` already pass 100 seeds.
`reorder` fails one seed by exactly one step, which is the `Len` seam above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:22:12 -04:00
iris-ai 71c9c39523 Replace placement calls with region nodes 2026-09-15 18:02:28 -04:00
iris-aiandClaude Opus 5 691e3eb23c Rename rest to leftover
The length kind that asks for a part of what is left once the fixed
lengths are taken is called leftover: Len::leftover(2), Len::LEFTOVER,
Size::LEFTOVER, Len::leftover the field, and apply_leftover. It says
what it is where "rest" reads as "the remainder of the list" as often as
"the remaining space", and every agent who has touched this has reached
for a third word for it.

Locals called rest that meant a region or a widget are renamed with it,
since the word now names something else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 14:22:52 -04:00
iris-aiandClaude Opus 5 1b1378b05a Branch on a measurement, so a wrong one shows as a different tree
Comparing boxes catches a widget that moved. It does not catch one that
measured a child, was handed an answer a cold start would not have given,
and took the other branch -- the same defect, arriving where a pixel
comparison cannot see it. Branching on what the painter tells you is
something a widget is allowed to do, so the library owes the same answer
warm and cold; only a widget changing its own configuration is exempt.

`random::Branch` measures a child and draws one of two others on the
result, with both grown either way so the ids match whichever is drawn.
It joins the generator, which makes every existing scenario a control-flow
oracle as well as a geometric one. `tests/determinism.rs` is the same
widget by hand across eight thresholds, including either side of the
answer, and is the fast check -- the sweep is a fuzzer and confirms at the
end rather than being iterated against.

A span behind a branch nobody took is not drawn, so shuffling it cannot
move anything; `reshuffled` now treats that as vacuous, the way it already
treats a tree with no spans, rather than as a shuffle that had no effect.

Both new tests pass, and the sweep passes at depth 4 and 5 over 200 seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:06:54 -04:00