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
|
complete there; see "Built-in alignment" below for the retained-layout
|
||||||
details. No PR reviews were present when checked on 2026-09-15.
|
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
|
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
|
`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
|
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
|
`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;
|
buried under three containers that are each a fraction of their parent;
|
||||||
swept over 2,100 box widths and exact at every one.
|
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
|
- **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
|
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
|
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 --
|
it. Found three times before the class was fixed rather than the instances --
|
||||||
`Stack::size(Child(i))`, `Scroll`'s orthogonal axis, and `Pad`.
|
`Stack::size(Child(i))`, `Scroll`'s orthogonal axis, and `Pad`.
|
||||||
|
|
||||||
`Painter::widget_aligned(child, region, align)` is the override: an
|
`Painter::widget_decided(child, region, [bool; 2])` is the override as of
|
||||||
`Option<RegionAlign>` carried in `DrawInfo` and resolved once in `draw_inner`,
|
`5b78002`, and it is per axis and not an alignment: it says the parent chose
|
||||||
so the root resolves like anything else. `Pad`, `Stack` and `Scroll` pass the
|
this box from the child's own answer along those axes, so the answer is not
|
||||||
near edge for children whose size they report. `ActiveData` keeps both the
|
placed inside it again. It replaced `widget_aligned(child, region, NEAR)`,
|
||||||
resolved alignment, so a local redraw asks the question its parent asked, and
|
which forced the near edge on *both* axes and, being an alignment, still let
|
||||||
the widget's own, which is what a change is compared against -- an override
|
`placed_box` scale a fractional answer against the box -- fine while every
|
||||||
means the answer is the parent's to give again.
|
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
|
### 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
|
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
|
is the unsound side of it. **Run the generated cases in debug**, since the
|
||||||
assertion is compiled out of every release run.
|
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
|
- 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
|
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.
|
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
|
- Marking every widget dirty converges warm onto cold, which is what says
|
||||||
the retained path and not the arithmetic is at fault.
|
the retained path and not the arithmetic is at fault.
|
||||||
|
|
||||||
**What makes the fix awkward** is coordinate frames, and it is worth knowing
|
**The fix is to stop recomputing the offer at all** (agreed with Bryan,
|
||||||
before starting. `ActiveData::offer` is in the parent *widget's* coordinates
|
2026-09-16). `ActiveData::offer` is in the parent *widget's* coordinates and
|
||||||
and `ActiveData::region` is in the parent *node's*, so "was this node placed
|
`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
|
where it was offered" is not a comparison either field can answer alone --
|
||||||
shapes look plausible:
|
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
|
- Thread the placed box's pixel size down beside `offered_px` -- one
|
||||||
part of its placed box -- an inverse composition, with a rounding of its
|
expression per level, `local.size().to_px(parent_px)` -- and keep both on
|
||||||
own.
|
`ActiveData`. Cold layout computes them the same way, so warm and cold
|
||||||
- Let `offered_region` say it cannot answer, and have `redraw` escalate to
|
agree by construction rather than by allowance.
|
||||||
the parent the way `parent_must_place` does. Correct and small, but it
|
- `at_offer` becomes `px == offered_px`. Equal lengths at a different
|
||||||
costs region nodes their point for partial repaint unless the "placed
|
position no longer escalate: the widget draws at `region`, which is the
|
||||||
where offered" case is detected, which is the frame problem again.
|
`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
|
## 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
|
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
|
this agrees with itself frame to frame, not that it matches the CPU to the
|
||||||
last bit" -- and then lands each edge with
|
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
|
**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)`
|
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
|
gaps are exactly ten pixels on both sides of the truncation change. So the
|
||||||
|
|||||||
Reference in new issue
Block a user