Record rel_base and the container API, and sweep LAYOUT.md

`frame` is gone from the layout vocabulary in `docs/LAYOUT.md` too, since
leaving the design document in the old words defeats the rename; the
rendered-frame sense of the word is untouched. The `local == UiRegion::FULL`
item is dropped from the handoff: that comment was #18's and is not on the
review branch.
This commit is contained in:
iris-ai committed 2026-09-19 16:35:02 -04:00
1 parent 90400f50ac
commit 93a6830fcd
3 files changed
+70 -60

No files matched your search

+29 -29
View File
@@ -8,7 +8,7 @@ without reading that result does not make the parent's size depend on it.
carries those commits whole plus the retained-layout repair built on them. §4 to §6 and the density section
retain the rationale of the design but still name types that have since been
replaced; they are not an API reference. `docs/HANDOFF.md` is where the work
in flight stands. The sections from "Frames, decided boxes and padding"
in flight stands. The sections from "Rel bases, decided boxes and padding"
onwards are the settled design, the findings that outlived the working log,
and the measurement method.
@@ -302,7 +302,7 @@ physical pixels, unchanged.
## Masks
This section and "UI ownership and frame access" describe the app's pinned
This section and "UI ownership and rel base access" describe the app's pinned
Iris implementation. The upstream #19 review branch still has single
rectangular masks and separate `UiData`/`UiRenderState`; adopting it requires
integrating the app-side capabilities as well as migrating layout calls.
@@ -319,18 +319,18 @@ fragment stage cannot make. Rendering and hit-testing both traverse the full
mask chain and use the same rounded-rectangle coverage; `iris/tests/mask_sdf.rs`
checks the WGSL implementation against the CPU SDF.
## Frames, decided boxes and padding
## Rel bases, decided boxes and padding
Containers that only divide room are transparent to fractions. A child frame
Containers that only divide room are transparent to fractions. A child rel base
is narrowed by a length its parent decided: a declared `px` or `rel` length,
or the resolved slot of a `leftover` child. A box a widget reports for itself
does not narrow its descendants' frame.
does not narrow its descendants' rel base.
`Pad` is an inset (Bryan, 2026-09-18): it subtracts the padding from both
the child's frame and the child's box, and reports the child's size plus the
the child's rel base and the child's box, and reports the child's size plus the
padding. A `rel(1.0)` child inside padding fills the pad without overflowing
it; a wrapping text inside padding wraps at the box the pad was given less
the padding. A span's frame never subtracts siblings, so a padded fixed
the padding. A span's rel base never subtracts siblings, so a padded fixed
child measured after a 24 px sibling in a 900 px row wraps at 844 while a
`rel(1.0)` inside it is 868. There is no outset kind and no mixed kind.
@@ -349,13 +349,13 @@ asserted exactly that and was unsatisfiable.
A container that can learn a child's length without drawing it does.
`Painter::size_hint` answers from the child's rule, or from
`Widget::size_hint` where it implements one, resolved against the asking
widget's frame; `Scroll`, `Masked` and a `Stack` without a sizing child hint
widget's rel base; `Scroll`, `Masked` and a `Stack` without a sizing child hint
`LEFTOVER`, since each always reports the whole of its box. `Span` takes each
child's length from its hint where there is one and asks that child exactly
once, in the slot it decided; a child with no hint is drawn in the room left
from the cursor, because a text has to wrap at the width actually there, and
its drawing is moved to its slot with `place_at`. Reading a hint records a
size dependency and pins the frame where the hint declared a fraction, the
size dependency and pins the rel base where the hint declared a fraction, the
same pin a rule that is a fraction takes.
**What that leaves, and the design that would remove it.** A widget that
@@ -373,23 +373,23 @@ either answer moved, which also retires `re_asked`'s deferral. Do it only
once an app screen shows the cost, and measure that screen first.
**There is one coordinate unit, the window** (`1512d84`, `23523ee`). Every box in the
tree is a region in window units; a widget's frame is a *length* in the same
tree is a region in window units; a widget's rel base is a *length* in the same
units and is only what fractions resolve against, so a widget's box need not
be its frame and padding can take from both. A region node's entry is a
be its rel base and padding can take from both. A region node's entry is a
translation -- a `rel 1` region anchored where its box starts -- so nothing
composes a frame back up a chain.
composes a rel base back up a chain.
A frame decided for a child is therefore a length of the window too: a row's
slot, padding's frame less its pixels, or the box a stack's sizing child
A rel base decided for a child is therefore a length of the window too: a row's
slot, padding's rel base less its pixels, or the box a stack's sizing child
decided (which arrives as `Part::Sized`). It is never a fraction of the
parent's frame, because a slot of a row is not a fraction of anything the row
parent's rel base, because a slot of a row is not a fraction of anything the row
can name -- a division of two lengths, which a `rel + px` length cannot hold.
A widget's own declaration *is* a fraction, of whichever of those reached it,
and it is the only one of them that also places the box inside the part.
Validity has a pin for each: `LayoutHolds::region_len` for the box's symbolic
length and `frame_len` for the frame's. A range of window pixels cannot say
which frame an answer is a fraction of, since two frames are different
length and `rel_base` for the rel base's. A range of window pixels cannot say
which rel base an answer is a fraction of, since two rel bases are different
lengths at the same window size. Resolving any of these lengths in pixels is
a read of the window, and `Painter::to_px` is where that read is taken --
pinning the window where the length has a fraction in it, and nothing where
@@ -449,10 +449,10 @@ floats.
Every length in the tree is a length of the window, so it becomes pixels in
one multiply wherever it is read: `Painter::px_size` and `px_len` for the
box, `frame_len` for the frame. There is no chain to walk and no coordinate
frame in the way, so a region node cannot break it and warm and cold reach
every length by the same expression. (Before `1512d84` a frame was a length
of its parent's frame and a local redraw walked back up the parent chain in
box, `rel_base` for the rel base. There is no chain to walk and no coordinate
rel base in the way, so a region node cannot break it and warm and cold reach
every length by the same expression. (Before `1512d84` a rel base was a length
of its parent's rel base and a local redraw walked back up the parent chain in
`asked_px`; both are gone.)
- **`Holds::through` is the exact preimage of `px + floor(rel * box)`**:
@@ -470,7 +470,7 @@ of its parent's frame and a local redraw walked back up the parent chain in
- **`px` is not stored on `ActiveData`, deliberately.** A resize every
widget's `Holds` admits redraws nothing, so a stored pixel length would
be stale on every widget in the tree with nothing to say so. What is
stored is the symbolic frame, which a redraw resolves against the window
stored is the symbolic rel base, which a redraw resolves against the window
it has.
- **The window is not a move entry** (`5b78002`). A chain bottoms out in
`MoveIdx::NONE`; the window is applied where a fraction becomes pixels,
@@ -502,8 +502,8 @@ of its parent's frame and a local redraw walked back up the parent chain in
parent may reuse an answer while redrawing the placed output. Translate the
drawing contract back through its placement; do not intersect it into the
answer contract.
- A fraction resolves once against its frame. A report returns raw and is
composed only where a parent narrowed that frame. A part's own pixel length
- A fraction resolves once against its rel base. A report returns raw and is
composed only where a parent narrowed that rel base. A part's own pixel length
is added rather than scaled, so a pixel length remains that many pixels at
every nesting depth.
- An asked-but-undrawn size dependency belongs to the widget that asked. Keep
@@ -557,7 +557,7 @@ must still agree exactly.
parts from cold at every other window. There is still no case that changes
a tree twice with a resize between, and none that resizes twice.
- The fuzzer grows no `rel` rules and never re-parents a widget, so neither
a fraction resolved against a frame nor a subtree changing hands is
a fraction resolved against a rel base nor a subtree changing hands is
covered by any generated tree. Both have cost a defect that only a
hand-written expectation or a render caught.
@@ -568,8 +568,8 @@ must still agree exactly.
existed only because a widget was drawn twice; one draw leaves it nothing
to name. A plan that proposes bookkeeping for a distinction should first
ask whether the distinction has to exist.
- **A frame narrowed by a region does not move when its part moves.** A
narrowed frame must be a *length*, put back into the part on every
- **A rel base narrowed by a region does not move when its part moves.** A
narrowed rel base must be a *length*, put back into the part on every
placement, exactly as a declared rule already is.
- **Choosing between a fixed and a relative child in pixels at the span's
current width** admits multiple self-sizing fixed points; seed 13 settles
@@ -584,8 +584,8 @@ must still agree exactly.
phase. A symbolic pin survives a resize because every ask box is
symbolically stable across one, so the pin only fails where an answer
changed, which is a real relayout.
- **Resolving a rule that is a fraction of the frame from the record instead
of redrawing** recovers nothing: `reuse outside: a frame length` is 3 of
- **Resolving a rule that is a fraction of the rel base from the record instead
of redrawing** recovers nothing: `reuse outside: a rel base` is 3 of
264 cold draws at seed 1 and 1 of 758 at seed 13.
- **A `git bisect`** once named a commit that could not be the cause; read
the tree rather than the bisect when that happens.