Read a child's answer in the asker's frame, and drop the root move entry

A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.

That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.

The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.

Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Fable 5.1 committed 2026-09-16 23:00:15 -04:00
1 parent 5f16617511
commit 5b7800264d
11 files changed
+182 -109

No files matched your search

+8 -4
View File
@@ -35,6 +35,10 @@ struct MoveOffset {
// belongs to the pixel above it. Flooring the product instead drops a pixel
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
return floor(v + PX_STEP * 0.5);
}
@@ -147,8 +151,8 @@ fn vs_main(
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px);
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
let size = bot_right - top_left;
let uv = vec2<f32>(
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px);
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px);
let top_left = snap_floor(tl * window.dim + tl_px);
let bot_right = snap_floor(br * window.dim + br_px);
let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0;