diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 9add035..41f24a7 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -6,11 +6,11 @@ in `docs/LAYOUT_LOG.md`. ## 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 redraw-on-reparent defect, `84dad21` removes repeated work in the test -harness, and `5642f20` is the naming sweep below. The core design remains -sound; round-to-nearest is still unchanged. +harness, and `5642f20` and `aeb60e5` 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 `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 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 the naming sweep moved -none. +them, all from the collapsed-share correction, and nothing since has moved +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 -a length had left half-applied. The box a parent asks a widget in is the -**region**, and where its drawing ends up is its **placement**: +`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: -- `Painter::extent` → `region`, and its `extent_len()` → `region_len()`. -- `ActiveData::{part, extent}` → `{region, placement}`, and with them - `DrawInfo::part` and `Placing::extent`. -- `LayoutHolds::{extent, extent_len}` → `{region, region_len}`, and - `Counter::OutsideExtent` → `OutsideRegion`. -- `placed_extent` → `placement`, `frame_and_extent` → `frame_and_region`, - `part_of` → `region_of`. -- `Painter`'s `window_own`, `extent_own`, `frame_own_len` and `extent_len` - become the one `own: LayoutHolds` they were assembled into. +- **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`. + +`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 @@ -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 ownership, richer masks, Android support, and app-side performance work must survive the integration. The app then absorbs the branch's - vocabulary, all of it already upstream: `rest` → `leftover`, `SizeRule` - beside widgets, alignment properties, the removal of `OrthoSize`, and the - region/placement names above. + 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`. 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` @@ -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. - `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`; both 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`, `Painter::region()`, `SetSize`, `desired_width`, `apply_rest`, `Len::dp`, `Aligned` and `MaxSize`, none of which exist. Do not restore diff --git a/docs/LAYOUT.md b/docs/LAYOUT.md index c6c3421..68c4642 100644 --- a/docs/LAYOUT.md +++ b/docs/LAYOUT.md @@ -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. diff --git a/docs/LAYOUT_LOG.md b/docs/LAYOUT_LOG.md index 4887303..77c1087 100644 --- a/docs/LAYOUT_LOG.md +++ b/docs/LAYOUT_LOG.md @@ -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 `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 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 O(n^2) per draw. Not a problem at today's sizes; it is worth knowing 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.