Move the settled layout vocabulary into LAYOUT.md and prune the handoff

The handoff had grown a chronicle of what each commit renamed. What is
durable about it -- the three names, and the one argument a container uses
to say them -- is design, so it goes to `docs/LAYOUT.md` as its own section
and the handoff keeps a pointer plus where the branch stands.

`docs/PLAN.md`'s "frame/extent experiment" entry described a prototype in
retired words; it becomes a pointer, and the one finding it held that was
not recorded elsewhere -- why the extent-relative child-frame trial was
removed -- joins the failed hypotheses in `docs/LAYOUT.md`.
This commit is contained in:
iris-ai committed 2026-09-19 18:06:28 -04:00
1 parent 265ce6f829
commit 61a2f601bb
4 files changed
+113 -98

No files matched your search

+38 -62
View File
@@ -1,92 +1,68 @@
# Handoff # Handoff
Where the work in flight stands. The durable layout design and the Where the work in flight stands. The settled layout design, the vocabulary
measurement method are in `docs/LAYOUT.md`; what the review of #19 found is and the measurement method are in `docs/LAYOUT.md`; what the review of #19
in `docs/LAYOUT_LOG.md`. found is 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 `58ce74d`: **Iris PR #19** (`layout/one-ask`) replaces closed #18. The tip is `58ce74d`,
`add6774` fixes collapsed-share placement, retained mask ownership and a and past the reviewed `cadfba0` it is two rounds:
redraw-on-reparent defect, `84dad21` removes repeated work in the test
harness, and `5642f20` through `58ce74d` are the vocabulary and API work
below.
The core design remains sound; round-to-nearest is still unchanged.
Two checkouts share one Git storage. `/home/bob/repos/iris` is the active - **The repair.** `add6774` fixes collapsed-share placement, retained mask
`layout/one-ask` worktree; `ai-app-2/iris` stays on `main` at the app's ownership and a redraw-on-reparent defect; `84dad21` removes repeated work
`32f6ad8` pin, and is kept until the app's integration is ready. in the test harness.
- **The vocabulary and the container API**, `5642f20` through `58ce74d`. What
they settled is described as design in `docs/LAYOUT.md` under "Three names,
and the one argument that says them" -- region, placement and rel_base, and
the single `impl Into<PlaceDesc>` argument that replaced `Place`, `Part`,
`widget_within` and the separate rel-base argument. **Bryan is still
refining this**, so treat the API as current rather than frozen.
Each round was checked with format, workspace clippy under `-D warnings`, the The core design remains sound. Round-to-nearest is still unchanged.
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
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 nothing since has moved
one.
## The vocabulary and the container API are settled Two checkouts share one Git storage: `/home/bob/repos/iris` is the active
`layout/one-ask` worktree, and `ai-app-2/iris` stays on `main` at the app's
`32f6ad8` pin until the integration below is ready.
`5642f20` finished the rename decided on 2026-09-17, which `frame`'s return ### How each round was checked
as a length had left half-applied, and `aeb60e5` finished the word `frame`
itself. Three names, and only two of them are boxes:
- **region** -- the box a parent asks a widget in. It draws there, and a text Format, workspace clippy under `-D warnings` with and without
wraps at it. Was `Painter::extent`, `ActiveData::part`, `DrawInfo::part`, `layout-diagnostics`, the workspace tests both ways, and the three prescribed
`Placing::extent`, `LayoutHolds::extent`. seed scans -- 400 at depth 5, 1,000 at depth 6, 2,000 at depth 4.
- **placement** -- the box its drawing ends up in, the size it reported,
once it has answered. Equal to the region wherever it fills one. Was
`ActiveData::extent`; `placed_extent` is now `placement`.
- **rel_base** -- not a box: the length a fraction it declares or reports is
a fraction of. Was `frame`, which said the wrong shape.
`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 The gate that matters for anything shaped like a rename or a refactor is the
had been paid for and ergonomics had not: **cold dump**: `layout_dump` over 400 depth-5 trees is 34,492 boxes, and the
compiler cannot catch two same-typed values being swapped. The repair moved
- **One argument says where a child goes.** `widget_at` and `place_at` take 650 of those boxes, all from the collapsed-share correction. Every commit
`impl Into<PlaceDesc>`, so a wrapper passes a `UiRegion` and says nothing since has been byte-identical to `84dad21`, including the whole API rewrite.
else -- `Pad` and `Offset` are one call each. `Place` and `Part` are gone:
`Place` was a product written as a sum, and `Part`'s three cases named
operations the geometry already had (`UiSpan::within`, `UiSpan::shift`,
and `placement`'s own body).
- **`PlaceDescAxis` is named after the operation**: `within`, `shifted`,
`sized`, `WHOLE`, with `fills` and `rel_base` as builders. The rel base a
caller does not state follows the constructor -- `within` narrows it,
`shifted` passes it through, `sized` is it -- which removes the one rule a
caller could get wrong with nothing failing.
- **`PlaceDesc` is a proper pair**, with `x`/`y`, `axis`, `axis_mut` and
`from_axis` like every other pair here, so joint work is written once
rather than per axis.
- `Painter::place_at` returns the answer and draws the child where there is
no answer to re-express, so `Span`'s second pass is one call and its
`drawn_across` bookkeeping is gone.
- `widget_within` and `ActiveData::narrow_rel_base` are deleted; `asked` and
`placed` carry the rel base their ask stated.
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
1. **Bryan's review of #19.** Fixes are themselves unreviewed code: repeat 1. **Bryan's review of #19.** Fixes are themselves unreviewed code: repeat
`pre-submit-review` over each round's changes, and re-run the three `pre-submit-review` over each round's changes, and re-run the three
fuzzers and dump diff for edits to `Span`, `Painter`, or `render_state`. fuzzers and the dump diff for edits to `Span`, `Painter`, or
The ordinary oracle does not replace absolute geometry and `render_state`. The ordinary oracle does not replace absolute geometry and
retained-primitive expectations. retained-primitive expectations.
2. **Integrate the app's Iris capabilities before changing its pin.** 2. **Integrate the app's Iris capabilities before changing its pin.**
`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 and container API, all of it already upstream: `rest` vocabulary and container API, all of it already upstream: `rest` ->
`leftover`, `SizeRule` beside widgets, alignment properties, the removal `leftover`, `SizeRule` beside widgets, alignment properties, the removal
of `OrthoSize`, the region/placement/rel_base names above, and of `OrthoSize`, region/placement/rel_base, and `PlaceDesc`.
`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`
under "Rendering the grid (pending)". under "Rendering the grid (pending)".
Wanted but not started, recorded in `iris/TODO`: transforms on a move entry,
so a whole subtree scales or rotates with one buffer write and no redraw.
Compose-style stretch at the end of a scroll area is the use that prompted
it. A move entry only translates today, and composing through one scales the
`rel` part while `px` passes through untouched, so fixed-size content and
glyphs do not follow a shortened entry.
## Smaller layout items, none urgent ## Smaller layout items, none urgent
- Nested spans pass `leftover` weight up, so three leftover children in one - Nested spans pass `leftover` weight up, so three leftover children in one
+60 -4
View File
@@ -8,9 +8,9 @@ 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 "Rel bases, decided boxes and padding" in flight stands. The sections from "Three names, and the one argument that
onwards are the settled design, the findings that outlived the working log, says them" onwards are the settled design, the findings that outlived the
and the measurement method. working log, and the measurement method.
## Design ## Design
@@ -319,6 +319,56 @@ 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.
## Three names, and the one argument that says them
Settled with Bryan on 2026-09-17 and 2026-09-19. Layout has exactly three
things, and only two of them are boxes:
- **region** — the box a parent asks a widget in. It draws there, and a text
wraps at it. `ActiveData::region`, `Painter::region`.
- **placement** — the box its drawing ends up in: a box the size it reported,
positioned inside the region by its own alignment, and equal to the region
wherever it fills one. It is computed after `draw` returns, is not on
`Painter`, and no widget mentions it. `ActiveData::placement` is what
`window_region` resolves for hit testing.
- **rel_base** — not a box. The length a fraction the widget declares or
reports is a fraction of, per axis, as a length of the window. It is the
one value that never changes coordinate space: a region node re-expresses
the region locally and takes the rel base untouched.
`frame` and `extent` are retired and must not come back in this sense; the
word "frame" for one rendered frame is unaffected.
A container says all three in one argument. `Painter::widget_at(id, place)`
and `place_at(id, place)` take `impl Into<PlaceDesc>`, so a wrapper that only
hands over a box passes a `UiRegion` and says nothing else — which is all
`Pad`, `Offset` and `Painter::widget` do.
`PlaceDescAxis` is one axis, and its three constructors are named after the
operations the geometry already had, because two of them take the same span
and differ only in how it is applied:
- `within(span)``UiSpan::within`: composed into the caller's box, so it
moves and scales with it. What an inset speaks.
- `shifted(span)``UiSpan::shift`: window lengths from where the caller's
box starts. What a container dividing room speaks, and what makes a moved
box re-place every child by addition, exactly. Not directional: `Dir::Neg`
is handled by the span before the numbers get here.
- `sized(len)` — the body of `placement` with the length given from above
rather than reported. What a stack's sizing child decides for the rest.
What is optional is a builder, so a caller writes only what it decided:
`.fills()` says the region is the placement, and `.rel_base(len)` names the
child's rel base outright. **The rel base a caller does not name follows the
constructor** — `within` narrows it the way the box is narrowed, `shifted`
passes it through, `sized` is it. That rule is what makes the common case
right by default; stating it by hand was the one thing a container could get
wrong with nothing failing.
`PlaceDesc` is the pair, with `x` and `y` fields and the `axis`, `axis_mut`
and `from_axis` of every other pair here, so the joint work — resolving a
region, reading the fill flags — is written once rather than per axis.
## Rel bases, decided boxes and padding ## Rel bases, decided boxes and padding
Containers that only divide room are transparent to fractions. A child rel base Containers that only divide room are transparent to fractions. A child rel base
@@ -381,7 +431,7 @@ composes a rel base back up a chain.
A rel base 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 rel base 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 `PlaceDescAxis::sized`). It is never a fraction of the
parent's rel base, 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,
@@ -587,6 +637,12 @@ must still agree exactly.
- **Resolving a rule that is a fraction of the rel base 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 rel base` 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 child rel base expressed as a fraction of the parent's region** (the
"extent-relative child frame" trial, 2026-09-17) was removed. The
size/resize cost it was aimed at came from conflating answer and drawing
dependencies, not from `Pad` and `Stack` reading their raw placement
conservatively. A decided rel base is a length of the window, not a
fraction of anything the parent can name.
- **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.
+5 -4
View File
@@ -83,7 +83,7 @@ the tail is placed at `(215, 0)..(395, 100)`. Its slot was 210..400, a gap
too long and a gap too early, and the declared 180 was then centred in it. too long and a gap too early, and the declared 180 was then centred in it.
The row reports a total that ends at 400. A child drawn with `rel` lengths The row reports a total that ends at 400. A child drawn with `rel` lengths
is stretched into the extra gap instead of being centred in it, because the is stretched into the extra gap instead of being centred in it, because the
slot is a `Place::Fill`. slot fills.
Recomputing the cursor in that branch fixes it, and the tail lands at Recomputing the cursor in that branch fixes it, and the tail lands at
`(220, 0)..(400, 100)`: `(220, 0)..(400, 100)`:
@@ -136,9 +136,10 @@ The descriptions above preserve the original failure at `cadfba0`.
### Clarity, in the order worth doing ### Clarity, in the order worth doing
The first three -- the unswept `extent`, the two same-typed boxes on Everything about naming is done: the unswept `extent`, the two same-typed
`ActiveData`, and `Painter`'s four holds accumulators -- were done in boxes on `ActiveData` and `Painter`'s four holds accumulators in `5642f20`,
`5642f20`, and `Part::All` in `aeb60e5`. What is left: `Part::All` in `aeb60e5`, and `Place`/`Part` themselves in `58ce74d`. The
settled vocabulary and the ask API are in `docs/LAYOUT.md`. 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.
+10 -28
View File
@@ -864,34 +864,16 @@ such as corner radius and text's position within horizontal overflow use
`Len`. Flexible padding participates in the same proportional allocation as a `Len`. Flexible padding participates in the same proportional allocation as a
span rather than silently discarding its `rest` component. span rather than silently discarding its `rest` component.
**The frame/extent experiment keeps measurement and geometry references separate** **The one-ask layout design is settled, and lives in `docs/LAYOUT.md`**
(2026-09-17, prototype only; verification and costs in `HANDOFF.md`). The parent (2026-09-17 to 2026-09-19). What this entry used to describe as the
chooses the reference for fractions; placing a widget's reported size does not "frame/extent experiment" is Iris PR #19, and it has its own vocabulary now:
replace that reference. A measurement also records the available extent, so the box a parent asks a widget in is its *region*, where the drawing ends up
reusing it checks both inputs. Retained primitive and mask coordinates name is its *placement*, and the length a fraction resolves against is its *rel
whether they follow the frame or the extent. Numeric extent reads constrain base*. A container says all three in one `PlaceDesc` argument. `Widget::draw`
layout validity; merely drawing in an extent does not require another widget remains the only layout body. The invariants, the measured costs, the fuzzing
evaluation when it moves. Widgets also retain their original local frames, method and the failed hypotheses are all in `docs/LAYOUT.md`; the app's pin at
so reuse composes positions in the same order as a fresh draw instead of `32f6ad8` predates every part of it, so nothing here depends on it until the
recovering fractions by division. The experimental oracle requires exact integration described in `docs/HANDOFF.md` lands.
pixel-region equality. `Widget::draw` remains the only layout body. The
prototype has significant container-update costs and is not yet adopted into
the pinned framework. A container's length query can evaluate the offered extent
without first placing the child's reported size; the parent's subsequent draw
assigns the final slot using the same `Widget::draw` body. A parent-selected slot
still honors a fixed child's alignment within that slot. Answer dependencies
are collected only from child sizes actually read; drawing dependencies
include all painted children. Root resize and local propagation check both
contracts. An unchanged answer with a wider validity range retains its old
contract: the parent's guarantee is still valid, and narrowing back must not
trigger layout. The same rule applies to retained drawing validity. A changed
declared size defers to its direct parent, then ordinary bottom-up propagation
stops when both contracts remain unchanged. Blanket ancestor invalidation is
unnecessary. The extent-relative child-frame API trial was removed: the major
size/resize cost came from conflating these dependencies, not from Pad and
Stack's conservative raw-placement reads. Cold layout and widespread content
changes still perform excess provisional work; that residual has not been
established as necessary for correctness.
**A `Span` leaves its children in the offered orthogonal region unless **A `Span` leaves its children in the offered orthogonal region unless
explicitly compacted** (2026-09-12). It still reports the widest orthogonal explicitly compacted** (2026-09-12). It still reports the widest orthogonal