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

+40 -24
View File
@@ -6,11 +6,11 @@ in `docs/LAYOUT_LOG.md`.
## The Iris layout repair is submitted ## The Iris layout repair is submitted
**Iris PR #19** (`layout/one-ask`) replaces closed #18. The tip is `5642f20`: **Iris PR #19** (`layout/one-ask`) replaces closed #18. The tip is `aeb60e5`:
`add6774` fixes collapsed-share placement, retained mask ownership and a `add6774` fixes collapsed-share placement, retained mask ownership and a
redraw-on-reparent defect, `84dad21` removes repeated work in the test redraw-on-reparent defect, `84dad21` removes repeated work in the test
harness, and `5642f20` is the naming sweep below. The core design remains harness, and `5642f20` and `aeb60e5` are the vocabulary and API work below.
sound; round-to-nearest is still unchanged. The core design remains sound; round-to-nearest is still unchanged.
Two checkouts share one Git storage. `/home/bob/repos/iris` is the active Two checkouts share one Git storage. `/home/bob/repos/iris` is the active
`layout/one-ask` worktree; `ai-app-2/iris` stays on `main` at the app's `layout/one-ask` worktree; `ai-app-2/iris` stays on `main` at the app's
@@ -20,24 +20,42 @@ Each round was checked with format, workspace clippy under `-D warnings`, the
workspace tests with and without `layout-diagnostics`, the release fast workspace tests with and without `layout-diagnostics`, the release fast
oracle, and the three prescribed seed scans — 400 at depth 5, 1,000 at depth oracle, and the three prescribed seed scans — 400 at depth 5, 1,000 at depth
6, 2,000 at depth 4. The cold dump covers 34,488 boxes: the fixes moved 650 of 6, 2,000 at depth 4. The cold dump covers 34,488 boxes: the fixes moved 650 of
them, all from the collapsed-share correction, and the naming sweep moved them, all from the collapsed-share correction, and nothing since has moved
none. one.
## The naming sweep is done ## The vocabulary and the container API are settled
`5642f20` finished the rename decided on 2026-09-17, which `frame`'s return as `5642f20` finished the rename decided on 2026-09-17, which `frame`'s return
a length had left half-applied. The box a parent asks a widget in is the as a length had left half-applied, and `aeb60e5` finished the word `frame`
**region**, and where its drawing ends up is its **placement**: itself. Three names, and only two of them are boxes:
- `Painter::extent``region`, and its `extent_len()``region_len()`. - **region** -- the box a parent asks a widget in. It draws there, and a text
- `ActiveData::{part, extent}``{region, placement}`, and with them wraps at it. Was `Painter::extent`, `ActiveData::part`, `DrawInfo::part`,
`DrawInfo::part` and `Placing::extent`. `Placing::extent`, `LayoutHolds::extent`.
- `LayoutHolds::{extent, extent_len}``{region, region_len}`, and - **placement** -- the box its drawing ends up in, the size it reported,
`Counter::OutsideExtent``OutsideRegion`. once it has answered. Equal to the region wherever it fills one. Was
- `placed_extent` `placement`, `frame_and_extent``frame_and_region`, `ActiveData::extent`; `placed_extent` is now `placement`.
`part_of``region_of`. - **rel_base** -- not a box: the length a fraction it declares or reports is
- `Painter`'s `window_own`, `extent_own`, `frame_own_len` and `extent_len` a fraction of. Was `frame`, which said the wrong shape.
become the one `own: LayoutHolds` they were assembled into. `Painter::frame_len()` is `Painter::rel_base()`, `LayoutHolds::frame_len`
is `rel_base`, and `frame_and_extent` is `rel_base_and_region`.
`aeb60e5` also cut what a container has to say, since correctness and cost
had been paid for and ergonomics had not:
- `Painter::widget_within(id, region)` takes a box in the widget's own
coordinates and derives the child's rel base from it, so `Pad` and
`Offset` are one call each. `Offset` had been reading `region_len`, which
pinned its drawing to a box length an offset does not depend on.
- `Painter::place_at` takes the rel base, returns the answer, and draws the
child where there is no answer to re-express. `Span`'s second pass is one
call, and its `drawn_across` bookkeeping is gone.
- `Part::All` is a `Part::WHOLE` constant rather than a variant. It was
exactly `Of(UiSpan::FULL)`; measured at 0.07% of instructions retired
against 0.04% run-to-run noise on a 60-seed depth-5 shrink.
Cold layout is byte-identical to `84dad21` across all of it, over 400
depth-5 trees.
## What is next, in order ## What is next, in order
@@ -50,9 +68,10 @@ a length had left half-applied. The box a parent asks a widget in is the
`32f6ad8` has 45 commits not reachable from the review branch; shared UI `32f6ad8` has 45 commits not reachable from the review branch; shared UI
ownership, richer masks, Android support, and app-side performance work ownership, richer masks, Android support, and app-side performance work
must survive the integration. The app then absorbs the branch's must survive the integration. The app then absorbs the branch's
vocabulary, all of it already upstream: `rest` `leftover`, `SizeRule` vocabulary and container API, all of it already upstream: `rest`
beside widgets, alignment properties, the removal of `OrthoSize`, and the `leftover`, `SizeRule` beside widgets, alignment properties, the removal
region/placement names above. of `OrthoSize`, the region/placement/rel_base names above, and
`widget_within`/`place_at`.
3. **Round-to-nearest**, CPU and shader together as one verified change. 3. **Round-to-nearest**, CPU and shader together as one verified change.
Bryan approved it on 2026-09-17 and neither half has landed; the Bryan approved it on 2026-09-17 and neither half has landed; the
derivation, the form to use and what to re-check are in `docs/LAYOUT.md` derivation, the form to use and what to re-check are in `docs/LAYOUT.md`
@@ -67,9 +86,6 @@ a length had left half-applied. The box a parent asks a widget in is the
children and nothing states what a widget may assume about one. children and nothing states what a widget may assume about one.
- `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`; both - `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`; both
are caller bugs under `debug_assert`, but the fallbacks differ. are caller bugs under `debug_assert`, but the fallbacks differ.
- The comment on the `local == UiRegion::FULL` shortcut in `widget_at` says
composing through `FULL` "is not quite the identity in f32"; on the grid it
is exact and the shortcut is performance only.
- `docs/LAYOUT.md` §4, §5 and the density section name `Painter::place`, - `docs/LAYOUT.md` §4, §5 and the density section name `Painter::place`,
`Painter::region()`, `SetSize`, `desired_width`, `apply_rest`, `Len::dp`, `Painter::region()`, `SetSize`, `desired_width`, `apply_rest`, `Len::dp`,
`Aligned` and `MaxSize`, none of which exist. Do not restore `Aligned` and `MaxSize`, none of which exist. Do not restore
+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 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 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 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, onwards are the settled design, the findings that outlived the working log,
and the measurement method. and the measurement method.
@@ -302,7 +302,7 @@ physical pixels, unchanged.
## Masks ## 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 Iris implementation. The upstream #19 review branch still has single
rectangular masks and separate `UiData`/`UiRenderState`; adopting it requires rectangular masks and separate `UiData`/`UiRenderState`; adopting it requires
integrating the app-side capabilities as well as migrating layout calls. 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` mask chain and use the same rounded-rectangle coverage; `iris/tests/mask_sdf.rs`
checks the WGSL implementation against the CPU SDF. 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, 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 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 `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 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 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 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. `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. A container that can learn a child's length without drawing it does.
`Painter::size_hint` answers from the child's rule, or from `Painter::size_hint` answers from the child's rule, or from
`Widget::size_hint` where it implements one, resolved against the asking `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 `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 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 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 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 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. same pin a rule that is a fraction takes.
**What that leaves, and the design that would remove it.** A widget that **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. 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 **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 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 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 A rel base 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 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 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. 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, 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. 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 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 length and `rel_base` for the rel base's. A range of window pixels cannot say
which frame an answer is a fraction of, since two frames are different 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 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 -- 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 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 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 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 box, `rel_base` for the rel base. 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 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 frame was a length every length by the same expression. (Before `1512d84` a rel base was a length
of its parent's frame and a local redraw walked back up the parent chain in of its parent's rel base and a local redraw walked back up the parent chain in
`asked_px`; both are gone.) `asked_px`; both are gone.)
- **`Holds::through` is the exact preimage of `px + floor(rel * box)`**: - **`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 - **`px` is not stored on `ActiveData`, deliberately.** A resize every
widget's `Holds` admits redraws nothing, so a stored pixel length would 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 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. it has.
- **The window is not a move entry** (`5b78002`). A chain bottoms out in - **The window is not a move entry** (`5b78002`). A chain bottoms out in
`MoveIdx::NONE`; the window is applied where a fraction becomes pixels, `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 parent may reuse an answer while redrawing the placed output. Translate the
drawing contract back through its placement; do not intersect it into the drawing contract back through its placement; do not intersect it into the
answer contract. answer contract.
- A fraction resolves once against its frame. A report returns raw and is - A fraction resolves once against its rel base. A report returns raw and is
composed only where a parent narrowed that frame. A part's own pixel length 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 is added rather than scaled, so a pixel length remains that many pixels at
every nesting depth. every nesting depth.
- An asked-but-undrawn size dependency belongs to the widget that asked. Keep - 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 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. 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 - 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 covered by any generated tree. Both have cost a defect that only a
hand-written expectation or a render caught. 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 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 to name. A plan that proposes bookkeeping for a distinction should first
ask whether the distinction has to exist. ask whether the distinction has to exist.
- **A frame narrowed by a region does not move when its part moves.** A - **A rel base 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 narrowed rel base must be a *length*, put back into the part on every
placement, exactly as a declared rule already is. placement, exactly as a declared rule already is.
- **Choosing between a fixed and a relative child in pixels at the span's - **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 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 phase. A symbolic pin survives a resize because every ask box is
symbolically stable across one, so the pin only fails where an answer symbolically stable across one, so the pin only fails where an answer
changed, which is a real relayout. changed, which is a real relayout.
- **Resolving a rule that is a fraction of the frame from the record instead - **Resolving a rule that is a fraction of the rel base from the record instead
of redrawing** recovers nothing: `reuse outside: a frame length` is 3 of 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. 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 - **A `git bisect`** once named a commit that could not be the cause; read
the tree rather than the bisect when that happens. the tree rather than the bisect when that happens.
+1 -7
View File
@@ -138,7 +138,7 @@ The descriptions above preserve the original failure at `cadfba0`.
The first three -- the unswept `extent`, the two same-typed boxes on The first three -- the unswept `extent`, the two same-typed boxes on
`ActiveData`, and `Painter`'s four holds accumulators -- were done in `ActiveData`, and `Painter`'s four holds accumulators -- were done in
`5642f20`. What is left: `5642f20`, and `Part::All` in `aeb60e5`. What is left:
- `draw_inner` returns `(Size, LayoutHolds, LayoutHolds)`, two same-typed - `draw_inner` returns `(Size, LayoutHolds, LayoutHolds)`, two same-typed
values ordered by convention. A named pair makes the swap unwriteable. values ordered by convention. A named pair makes the swap unwriteable.
@@ -152,9 +152,3 @@ The first three -- the unswept `extent`, the two same-typed boxes on
`under.iter_mut().find`, `depend_on`), so a span of *n* children is `under.iter_mut().find`, `depend_on`), so a span of *n* children is
O(n^2) per draw. Not a problem at today's sizes; it is worth knowing O(n^2) per draw. Not a problem at today's sizes; it is worth knowing
before a long transcript list lands on it. before a long transcript list lands on it.
- `Part::All` is exactly `Part::Of(UiSpan::FULL)`: `Of` composes through
`within`, and `through(Len::FULL)` maps a range back to itself, which
`the_whole_of_a_box_maps_back_to_itself` already asserts. It buys a
separate arm in `in_parent` and in `Part::of`. Keeping it as sugar is
defensible; the case analysis being wider than the geometry is the thing
to decide about rather than to leave unstated.