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

+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
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 "Rel bases, decided boxes and padding"
onwards are the settled design, the findings that outlived the working log,
and the measurement method.
in flight stands. The sections from "Three names, and the one argument that
says them" onwards are the settled design, the findings that outlived the
working log, and the measurement method.
## 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`
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
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
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
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,
@@ -587,6 +637,12 @@ must still agree exactly.
- **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 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
the tree rather than the bisect when that happens.