From 61a2f601bbbc0c6be897b7315ba9033841bb9376 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sat, 19 Sep 2026 18:06:28 -0400 Subject: [PATCH] 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`. --- docs/HANDOFF.md | 100 +++++++++++++++++---------------------------- docs/LAYOUT.md | 64 +++++++++++++++++++++++++++-- docs/LAYOUT_LOG.md | 9 ++-- docs/PLAN.md | 38 +++++------------ 4 files changed, 113 insertions(+), 98 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 1c8ca33..2843997 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,92 +1,68 @@ # Handoff -Where the work in flight stands. The durable layout design and the -measurement method are in `docs/LAYOUT.md`; what the review of #19 found is -in `docs/LAYOUT_LOG.md`. +Where the work in flight stands. The settled layout design, the vocabulary +and the measurement method are in `docs/LAYOUT.md`; what the review of #19 +found is in `docs/LAYOUT_LOG.md`. ## The Iris layout repair is submitted -**Iris PR #19** (`layout/one-ask`) replaces closed #18. The tip is `58ce74d`: -`add6774` fixes collapsed-share placement, retained mask ownership and a -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. +**Iris PR #19** (`layout/one-ask`) replaces closed #18. The tip is `58ce74d`, +and past the reviewed `cadfba0` it is two rounds: -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 -`32f6ad8` pin, and is kept until the app's integration is ready. +- **The repair.** `add6774` fixes collapsed-share placement, retained mask + ownership and a redraw-on-reparent defect; `84dad21` removes repeated work + 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` 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 -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 core design remains sound. Round-to-nearest is still unchanged. -## 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 -as a length had left half-applied, and `aeb60e5` finished the word `frame` -itself. Three names, and only two of them are boxes: +### How each round was checked -- **region** -- the box a parent asks a widget in. It draws there, and a text - wraps at it. Was `Painter::extent`, `ActiveData::part`, `DrawInfo::part`, - `Placing::extent`, `LayoutHolds::extent`. -- **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`. +Format, workspace clippy under `-D warnings` with and without +`layout-diagnostics`, the workspace tests both ways, and the three prescribed +seed scans -- 400 at depth 5, 1,000 at depth 6, 2,000 at depth 4. -`aeb60e5` also cut what a container has to say, since correctness and cost -had been paid for and ergonomics had not: - -- **One argument says where a child goes.** `widget_at` and `place_at` take - `impl Into`, so a wrapper passes a `UiRegion` and says nothing - 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. +The gate that matters for anything shaped like a rename or a refactor is the +**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 +650 of those boxes, all from the collapsed-share correction. Every commit +since has been byte-identical to `84dad21`, including the whole API rewrite. ## What is next, in order 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 - fuzzers and dump diff for edits to `Span`, `Painter`, or `render_state`. - The ordinary oracle does not replace absolute geometry and + fuzzers and the dump diff for edits to `Span`, `Painter`, or + `render_state`. The ordinary oracle does not replace absolute geometry and retained-primitive expectations. 2. **Integrate the app's Iris capabilities before changing its pin.** `32f6ad8` has 45 commits not reachable from the review branch; shared UI ownership, richer masks, Android support, and app-side performance work 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 - of `OrthoSize`, the region/placement/rel_base names above, and - `widget_within`/`place_at`. + of `OrthoSize`, region/placement/rel_base, and `PlaceDesc`. 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 derivation, the form to use and what to re-check are in `docs/LAYOUT.md` 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 - Nested spans pass `leftover` weight up, so three leftover children in one diff --git a/docs/LAYOUT.md b/docs/LAYOUT.md index 68c4642..6d27bdd 100644 --- a/docs/LAYOUT.md +++ b/docs/LAYOUT.md @@ -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`, 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. diff --git a/docs/LAYOUT_LOG.md b/docs/LAYOUT_LOG.md index 77c1087..ec832c8 100644 --- a/docs/LAYOUT_LOG.md +++ b/docs/LAYOUT_LOG.md @@ -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. 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 -slot is a `Place::Fill`. +slot fills. Recomputing the cursor in that branch fixes it, and the tail lands at `(220, 0)..(400, 100)`: @@ -136,9 +136,10 @@ The descriptions above preserve the original failure at `cadfba0`. ### Clarity, in the order worth doing -The first three -- the unswept `extent`, the two same-typed boxes on -`ActiveData`, and `Painter`'s four holds accumulators -- were done in -`5642f20`, and `Part::All` in `aeb60e5`. What is left: +Everything about naming is done: the unswept `extent`, the two same-typed +boxes on `ActiveData` and `Painter`'s four holds accumulators in `5642f20`, +`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 values ordered by convention. A named pair makes the swap unwriteable. diff --git a/docs/PLAN.md b/docs/PLAN.md index 2fc0092..be28774 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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 span rather than silently discarding its `rest` component. -**The frame/extent experiment keeps measurement and geometry references separate** -(2026-09-17, prototype only; verification and costs in `HANDOFF.md`). The parent -chooses the reference for fractions; placing a widget's reported size does not -replace that reference. A measurement also records the available extent, so -reusing it checks both inputs. Retained primitive and mask coordinates name -whether they follow the frame or the extent. Numeric extent reads constrain -layout validity; merely drawing in an extent does not require another widget -evaluation when it moves. Widgets also retain their original local frames, -so reuse composes positions in the same order as a fresh draw instead of -recovering fractions by division. The experimental oracle requires exact -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. +**The one-ask layout design is settled, and lives in `docs/LAYOUT.md`** +(2026-09-17 to 2026-09-19). What this entry used to describe as the +"frame/extent experiment" is Iris PR #19, and it has its own vocabulary now: +the box a parent asks a widget in is its *region*, where the drawing ends up +is its *placement*, and the length a fraction resolves against is its *rel +base*. A container says all three in one `PlaceDesc` argument. `Widget::draw` +remains the only layout body. The invariants, the measured costs, the fuzzing +method and the failed hypotheses are all in `docs/LAYOUT.md`; the app's pin at +`32f6ad8` predates every part of it, so nothing here depends on it until the +integration described in `docs/HANDOFF.md` lands. **A `Span` leaves its children in the offered orthogonal region unless explicitly compacted** (2026-09-12). It still reports the widest orthogonal