Record the frame rule, the decided axes, and the root entry's removal
Iris 5b78002 on #18: a child's answer comes back in the asker's frame, the near-edge override is per-axis decided flags, and the window is no longer a move entry. Also records the threading plan that closes the region-node offer defect, and why widening the grid would not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
82646e8408
commit
9a26862bce
1 file changed
+97
-20
+97
-20
@@ -12,6 +12,12 @@ head is **`5f16617`**, eighty-two commits, pushed. Built-in alignment is
|
||||
complete there; see "Built-in alignment" below for the retained-layout
|
||||
details. No PR reviews were present when checked on 2026-09-15.
|
||||
|
||||
**`5b78002` on top of it** (2026-09-16) does two things Bryan asked for after
|
||||
the frame and root questions below. A child's answer now comes back to the
|
||||
widget that asked in *that widget's* frame, and the window is no longer a
|
||||
move entry -- see "A report is a fraction of the box it was given" and the
|
||||
root item under "Fixed point". `Holds::through` is untouched by either.
|
||||
|
||||
The current head completes LAYOUT.md §2's position chain and the requested
|
||||
`leftover` behavior. A child whose length is only `leftover` is not drawn when
|
||||
nothing is left. A child that also asks for pixels or a relative fraction keeps
|
||||
@@ -551,6 +557,24 @@ across it.
|
||||
`a_length_in_pixels_is_that_many_pixels_however_it_is_nested`, over a row
|
||||
buried under three containers that are each a fraction of their parent;
|
||||
swept over 2,100 box widths and exact at every one.
|
||||
- **A report is a fraction of the box the widget was given, and comes back
|
||||
to the asker in the asker's frame** (Bryan, 2026-09-16: `rel` has nothing
|
||||
to do with the remainder; it is the proportion of the space the widget
|
||||
was given). `placed_box` always read it that way. `Span` did not: it added
|
||||
a drawn child's `rel` straight into a cursor that counts fractions of the
|
||||
row, which is right only while the offer has the row's whole extent --
|
||||
and a `Span` offers what is left after the cursor, so it broke once a
|
||||
relative child preceded another. `Pad` summed padding onto a fraction of
|
||||
its inset the same way and reported its box plus the padding for a child
|
||||
that filled the inset. `DrawResult::size`/`len` and `known_len` now
|
||||
compose the answer through the offer's length before handing it back
|
||||
(`in_parent_frame`), so a container reads lengths of its own box and the
|
||||
box `Span` hands back afterwards is the one `placed_box` chose. A
|
||||
declared axis is excepted: the parent resolved the rule in its own box
|
||||
and the rule is what the report says there. A hint is used as a
|
||||
declaration and so is in the parent's frame too; `hints_agree` compares
|
||||
it with the raw report, which holds because every hint is pixels or
|
||||
`leftover`. A hint with a fraction in it would need one frame chosen.
|
||||
- **A length given as a share is not**, and cannot be: children asking for
|
||||
the same fraction come out one or two steps apart, 0.001 to 0.002 px. A
|
||||
position is the quantity that gets rounded, so the row fills exactly and no
|
||||
@@ -641,13 +665,31 @@ box** gets its content placed twice: once by the child, once by the box around
|
||||
it. Found three times before the class was fixed rather than the instances --
|
||||
`Stack::size(Child(i))`, `Scroll`'s orthogonal axis, and `Pad`.
|
||||
|
||||
`Painter::widget_aligned(child, region, align)` is the override: an
|
||||
`Option<RegionAlign>` carried in `DrawInfo` and resolved once in `draw_inner`,
|
||||
so the root resolves like anything else. `Pad`, `Stack` and `Scroll` pass the
|
||||
near edge for children whose size they report. `ActiveData` keeps both the
|
||||
resolved alignment, so a local redraw asks the question its parent asked, and
|
||||
the widget's own, which is what a change is compared against -- an override
|
||||
means the answer is the parent's to give again.
|
||||
`Painter::widget_decided(child, region, [bool; 2])` is the override as of
|
||||
`5b78002`, and it is per axis and not an alignment: it says the parent chose
|
||||
this box from the child's own answer along those axes, so the answer is not
|
||||
placed inside it again. It replaced `widget_aligned(child, region, NEAR)`,
|
||||
which forced the near edge on *both* axes and, being an alignment, still let
|
||||
`placed_box` scale a fractional answer against the box -- fine while every
|
||||
overriding container handed a box its child had reported in pixels, and
|
||||
wrong the moment `Span` had to use it. `Span` now decides the row axis alone
|
||||
when it places a child at the length it reported, since across the row the
|
||||
child sits where its alignment says; `Scroll` decides both, and `Stack` both
|
||||
for its sizing child. `DrawInfo::decided` and `ActiveData::decided` carry it,
|
||||
so a local redraw asks the question its parent asked; alignment is always the
|
||||
widget's own property now, and `ActiveData::align` is gone.
|
||||
|
||||
**A fractional answer taken of the parent's second box shrinks twice.**
|
||||
Found by the frame fix's own test: a nested span reporting `rel(0.5)` was
|
||||
placed by its parent at a quarter of the row, correctly, and then
|
||||
`placed_box` took half of *that* box because the parent's second ask was not
|
||||
an override. A report of "half of what you give me" has no fixed point but
|
||||
zero, so the framework asks exactly twice -- once at the offer, once in the
|
||||
box chosen from the answer -- and the second box is final on the axes the
|
||||
parent decided. This is also why a root that reports a fraction of the
|
||||
window is placed inside it by its own alignment and then scaled to fit,
|
||||
which the two new tests in `tests/cases/layout.rs` avoid by ruling the root
|
||||
to the window; that is old behaviour and was not changed.
|
||||
|
||||
### Alignment as a property changed two examples, unnoticed
|
||||
|
||||
@@ -784,6 +826,16 @@ pixels and `Holds`. Decided with Bryan on 2026-09-15.
|
||||
and shrinking this allowance is the test of whether that worked. Too wide
|
||||
is the unsound side of it. **Run the generated cases in debug**, since the
|
||||
assertion is compiled out of every release run.
|
||||
- **The window is not a move entry** (`5b78002`). It was one, holding the
|
||||
output in pixels, so composing through it zeroed every `rel` below and the
|
||||
shader's `rel * window.dim` and the CPU's final multiply by the output
|
||||
were both dead. Now a chain bottoms out in `MoveIdx::NONE`, the window is
|
||||
applied where a fraction becomes pixels -- `to_px(output_size)` on the
|
||||
CPU, the uniform in the shader -- and a resize rewrites no retained entry
|
||||
and re-uploads nothing but the uniform. Instances were never re-uploaded
|
||||
on a resize either way; they are `(rel, px)` in their node's frame and the
|
||||
shader walks the chain per vertex. The cost of a resize is whatever
|
||||
`Holds` redraws.
|
||||
- A pointer, a wheel notch, a shaped glyph advance and a window size arrive
|
||||
as floats and are put on the grid where they arrive. `Vec2` stays what the
|
||||
GPU and the platform speak; `PxVec2` is what layout decides in.
|
||||
@@ -833,19 +885,42 @@ Traced, at a 900x1200 window, with `reorder` rotating both spans:
|
||||
- Marking every widget dirty converges warm onto cold, which is what says
|
||||
the retained path and not the arithmetic is at fault.
|
||||
|
||||
**What makes the fix awkward** is coordinate frames, and it is worth knowing
|
||||
before starting. `ActiveData::offer` is in the parent *widget's* coordinates
|
||||
and `ActiveData::region` is in the parent *node's*, so "was this node placed
|
||||
where it was offered" is not a comparison either field can answer alone. Two
|
||||
shapes look plausible:
|
||||
**The fix is to stop recomputing the offer at all** (agreed with Bryan,
|
||||
2026-09-16). `ActiveData::offer` is in the parent *widget's* coordinates and
|
||||
`ActiveData::region` is in the parent *node's*, so "was this node placed
|
||||
where it was offered" is not a comparison either field can answer alone --
|
||||
and neither answer is needed. By the retained contract a drawing and its
|
||||
answer depend on the box's pixel *lengths* only; checked against every
|
||||
widget, the pixel reads are `px_len` and `px_size` and nothing reads a
|
||||
position. The offer's symbolic position was only ever an input to its pixel
|
||||
size, and that the parent already knew when it asked. So:
|
||||
|
||||
- Compose offers through a region node by expressing the node's offer as a
|
||||
part of its placed box -- an inverse composition, with a rounding of its
|
||||
own.
|
||||
- Let `offered_region` say it cannot answer, and have `redraw` escalate to
|
||||
the parent the way `parent_must_place` does. Correct and small, but it
|
||||
costs region nodes their point for partial repaint unless the "placed
|
||||
where offered" case is detected, which is the frame problem again.
|
||||
- Thread the placed box's pixel size down beside `offered_px` -- one
|
||||
expression per level, `local.size().to_px(parent_px)` -- and keep both on
|
||||
`ActiveData`. Cold layout computes them the same way, so warm and cold
|
||||
agree by construction rather than by allowance.
|
||||
- `at_offer` becomes `px == offered_px`. Equal lengths at a different
|
||||
position no longer escalate: the widget draws at `region`, which is the
|
||||
`aea878d` fix without the second draw. A region node whose lengths differ
|
||||
still draws twice, and the first box only needs the right length.
|
||||
- `redraw` reads the stored `offered_px`. `offered_region`, `px_of`,
|
||||
`px_region`, `Moves::compose`/`size_of`, `slot_wide` and `wide.rs` go; a
|
||||
narrow `resolve` stays for `window_region`, which is hit testing and not a
|
||||
layout decision.
|
||||
- `remap_subtree`'s `Translate` keeps lengths, so stored sizes stay valid
|
||||
at no cost on the hot path; `Scale` is the resize path and recomputes
|
||||
them down the subtree from the same expression.
|
||||
- `Holds::through` becomes the exact preimage of the truncating map
|
||||
`px + floor(rel * B)`: two integer divisions, no `ROUTES`, no `way_in`,
|
||||
and the whole-of-a-box case is the identity. The allowance existed only
|
||||
because the box arrived by a second route.
|
||||
|
||||
Symbolic regions then exist for the GPU and for remaps alone, and their
|
||||
rounding is cosmetic below a step. That is the separation the fixed-point
|
||||
work kept reaching for. **Do not widen the grid instead**: every failure in
|
||||
this document is one value reached by two expressions, sitting exactly on a
|
||||
boundary because the boundary was defined by the same value coming back the
|
||||
other way, and no precision shrinks a residue that is the whole distance.
|
||||
|
||||
## The leftover boundary
|
||||
|
||||
@@ -1043,7 +1118,9 @@ Queued from this work, in order:
|
||||
chain in floats -- with the comment saying so, "what has to hold is that
|
||||
this agrees with itself frame to frame, not that it matches the CPU to the
|
||||
last bit" -- and then lands each edge with
|
||||
`snap_floor(rel * dim) + snap_floor(px)`, which is a whole pixel.
|
||||
`snap_floor(rel * dim + px)`, which is a whole pixel. (It floored the two
|
||||
parts apart until `5b78002`, which was harmless only while the root entry
|
||||
zeroed every `rel`.)
|
||||
**Measured: every box edge in the `tabs` render is a hard edge**, no
|
||||
partially covered column anywhere along the band, and the two `pad(10)`
|
||||
gaps are exactly ten pixels on both sides of the truncation change. So the
|
||||
|
||||
Reference in new issue
Block a user