From 82c006cd598fd4c516a20afc5908170a104ab203 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Wed, 16 Sep 2026 18:18:26 -0400 Subject: [PATCH] Record four fixes that came out of one look at the tabs render Iris #18 is at `e166e00`, and warm layout now agrees with cold on every rig the repository has: all fifteen shrinker cases at 400 seeds of depth 5, and the oracle at 1000 seeds of depth 6. Both pass for the first time. The handoff's two standing leads, shrinker seed 288 and oracle seed 326, were one defect and are gone. What found them was asking why the `tabs` render differed, rather than counting the pixels that differed: - `d8ae9c3`: `redraw`'s second ask handed the final box over as if it were an offer, so a locally redrawn widget with its own alignment was placed twice. - `2bc6bdf`: `Stack` and `Pad` overrode every child's alignment with the near edge, which is owed only where the box really is the child's own answer. `tabs`'s counters and `text`'s narrow panel match canonical `main` again. - `38eba54`: `Holds::through`'s allowance was four half steps where three is the floor, and tightening it moved none of the rig's work counters. Why it cannot go to zero is recorded with it. - `e166e00`: pins what the gap question was actually about -- a length in pixels is that many pixels at any nesting depth, and a share is one or two steps off, which is a trade for the row filling exactly. Two corrections to this document. The bisect it reported for the partial-repaint defect named a commit that cannot have caused it, and the reasoning built on that was wrong. And its claim that `tabs` "differs only in the widget count it prints about itself" was checked at `8220a78` and left to cover `d3b0ebf`, which changed the render twice more. One API question is open: `.width()` now overwrites what `.sized()` set, since both are properties of one widget, so `tabs`'s centred square is a bar. Co-Authored-By: Claude Opus 5 --- docs/HANDOFF.md | 168 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 122 insertions(+), 46 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 4136c90..31ad512 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -8,7 +8,7 @@ log. Canonical Iris `main` is **`ca2b4b2`** (#17, the headless rig). **#18 `split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`; its local -head is **`08c9d5a`**, seventy-five commits, pushed. Built-in alignment is +head is **`e166e00`**, seventy-nine commits, pushed. Built-in alignment is complete there; see "Built-in alignment" below for the retained-layout details. No PR reviews were present when checked on 2026-09-15. @@ -89,7 +89,7 @@ rewrite: `core/src/ui/holds.rs`, the retained tests, and the generated cold-layout oracle pin those rules. Seeds 10 and 86 are now in the ordinary generated set. -## A warm frame moved a scrolled span, and the rigs that found it +## Warm layout agreed with cold, and the rigs that got it there **Fixed in `aea878d`, and the bisect was a red herring.** `git bisect` named `95fb4f9`, the commit that made `Masked` report `Size::LEFTOVER`; the failing @@ -128,51 +128,64 @@ whatever it failed on. Seeds still mean the trees they meant: the 1000-seed depth-6 run gives the same three failures with the same boxes before and after the split, which is the check to repeat if the generator is touched again. -**What is left, at `08c9d5a`.** Two failures, both older than the fix above -and neither reduced to a test yet: +**Nothing is left, as of `d8ae9c3`.** All fifteen shrinker cases pass at 400 +seeds of depth 5 and the oracle passes 1000 seeds of depth 6, both for the +first time. What closed the last two was one line: `redraw`'s second ask, in +the box its parent chose from the widget's answer, handed that box over as if +it were an offer, so `draw_inner` ran `placed_box` on an already-placed box +and applied the widget's own alignment to it twice. That only showed where +the alignment was the widget's own to apply, which is why it stayed hidden +while `Stack`, `Pad` and `Scroll` overrode every child. -- **Shrinker seed 288 on `region-node`**, 400 seeds of depth 5. Reduces to 11 - widgets from 42, and widget 34 -- a `Text` under `Span(Y-)` under two - `Stack`s -- sits at y 1032.80..1261.60 warm against 1024..1270.40 cold, so - it is inset 8.8px at each end rather than displaced. The plan is a - `Scroll(X)` over a `Span` of three rects with `region_node` set on the - first and third, inside two `Stack`s. -- **Oracle seed 326 at depth 6 on `repaint-some`**. Reduces only to 43 - widgets from 205, around two `Branch`es -- the widget whose shape depends - on a measured length -- with the differing `Text` at - `Text < Branch < Branch < Scroll < Span < Pad`, 88px out. The `Branch`es - are why it will not reduce further, and probably why it diverges. +Closed by it: shrinker seed 288 on `region-node` (11 widgets from 42, a +`Text` inset 8.8px at each end) and oracle seed 326 at depth 6 (43 from 205, +88px out, around two `Branch`es). Neither reduced small enough to hand-write, +so **generated seed 20 at depth 4 is the record** -- it is in `SEEDS`, so +`cargo test` fails without the fix rather than only the ignored long run. +Closed earlier by `aea878d`: shrinker seeds 174, 175 and 2, oracle seeds 18 +and 190. -Fixed with `aea878d`, from what this section used to list as unreduced leads: -shrinker seeds 174 and 175 on `repaint-some` and seed 2 on `region-node`, and -oracle seeds 18 and 190 at depth 6. The `repaint-some` guess was right. +**Depth is what found these.** Nothing failed at 100 seeds of depth 4, which +is all the oracle had ever routinely run. Before fixed point, `5ed9e87` fails +the 1000-seed depth-6 run on seed 40 after a resize; that one is older than +all of the rest and is also gone. -**Depth is what finds these.** Nothing fails at 100 seeds of depth 4, which is -all the oracle has ever routinely run. Before fixed point, `5ed9e87` fails the -1000-seed depth-6 run on seed 40 after a resize and on nothing else, so the -partial-repaint family is newer than that and the resize one is older than all -of it. +**Run the long two before believing a rounding change.** They are 14 s, 57 s +and 145 s, which is nothing against how long the two above took to find: + +```sh +cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees +SHRINK_CASE=all SHRINK_SEEDS=400 SHRINK_DEPTH=5 \ + cargo test --release --test shrink -- --ignored --nocapture +IRIS_GENERATED_SEEDS=1000 IRIS_GENERATED_DEPTH=6 \ + cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees +``` + +And run the ordinary suite in **debug**: the `Holds` assertion in `draw_at` +is what says a validity range stopped containing the box a drawing was made +in, and it is compiled out of the release runs above. ## Verification -**At `08c9d5a`**, the current head: +**At `e166e00`**, the current head: - `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- - -D warnings`, `cargo test --workspace`: sixteen targets green, 80 suite + -D warnings`, `cargo test --workspace`: sixteen targets green, 83 suite tests, 17 core unit tests, 11 generated cases. What stays ignored is the long runs and the profiling rigs; no known defect is ignored any more. -- The release oracle at 100 seeds, in 14.3 s. -- All fifteen shrinker cases at 400 seeds of depth 5, in 81 s: seed 288 on - `region-node` fails and nothing else does. -- Depth-6 oracle seeds 18, 190 and 326 run one at a time: 18 and 190 pass, - 326 fails. -- `view`, `minimal`, `text`, `random` and the `tabs` touch replay render - byte-identical at 1920x1200 against `98d4e98`. `tabs` cold and - `tabs`-before-the-gesture differ on 4,664 of 2,304,000 pixels; see - "Performance" for why, and take the oracle rather than `tabs` as the check - from here. +- The release oracle at 100 seeds in 14.3 s, **all fifteen shrinker cases at + 400 seeds of depth 5** in 57 s, and **1000 seeds of depth 6** in 145 s. The + last two pass for the first time. +- `view`, `minimal` and `random` render byte-identical at 1920x1200 against + `98d4e98`. `tabs` and `text` changed twice on purpose: `08c9d5a` moved an + antialiased edge by less than a pixel (see "Performance") and `2bc6bdf` + gave stack and pad children their own alignment back, which is what `tabs` + and `text` were asking for and what canonical `main` draws. **Take the + oracle rather than a render as the reference from here**; `view`, + `minimal` and `random` are the three still worth comparing. - Twenty-five rig work counters identical between `60367d8` and `08c9d5a` on - the `many` fixture, which is what makes their timings comparable. + the `many` fixture, which is what makes their timings comparable, and + unmoved again by `38eba54`. **At `394d514`**, kept because several of these have not been re-run since: @@ -206,6 +219,10 @@ against `394d514`. The measurements are under "Performance". `8220a78`. `tabs` differs only in the widget count it prints about itself, which is two wrapper types smaller -- so it is no longer a byte-identical reference and the generated oracle is the check that matters. + **That last claim was then left to cover `d3b0ebf` as well, and it does + not**: `tabs` changed twice more there, and nobody looked (2026-09-16). + See "Alignment as a property changed two examples" below. A claim about a + render holds for the commit it was checked at and no further. - At preceding head `29c7881`, reference renders against `/home/bob/repos/iris-main-cmp` at `ca2b4b2` covered: `tabs`, `view`, `minimal`, and `text` at 1920x1200; `tabs` cold at @@ -487,6 +504,20 @@ across it. in its final placed box remain valid. The final drawing's `Holds` interval is translated back into lengths of the offered box and intersected with the answer's interval. +- **A container's near-edge override is owed only where the box really is the + child's own answer.** `Stack` gives every child the box `box_of` derives + from its sizing child, so that one child has no room in it and would be + placed twice; every other child is handed a box that owes nothing to it and + keeps its own alignment. `Pad` reports its inner plus the padding, so where + its box is that answer the inset box is exactly the inner, and where the box + is bigger the slack is the inner's to sit in -- it overrides nothing + (`2bc6bdf`). `Scroll` still overrides, on both axes and deliberately: the + box it hands its content along the scrolling axis is the content's own + length, and where shorter content sits across the viewport is `Scroll`'s own + alignment by design. +- **A box a parent has already placed is asked about at the near edge.** The + second ask in `redraw` is in the final box, not at an offer, so applying the + widget's alignment to it again places its content twice (`d8ae9c3`). - Equal box lengths do not imply equal placement. An ordinary widget whose offered and current boxes differ in position must involve its parent again; a region node can settle itself only when its own alignment, rather than a @@ -511,6 +542,21 @@ across it. - A pixel comparison is equality: lengths are whole counts of `1/1024` px, so a change too small to reach the next step is not a change and one that reaches it is, however little of a pixel it is worth. +- **A length given in pixels is that many pixels wherever it ends up**, and + structurally rather than by luck: `Len::within` adds a part's own pixels + rather than scaling them, and both ends of a gap carry the same fraction, + so the multiply that rounds is the same on each and cancels. Pinned by + `a_length_in_pixels_is_that_many_pixels_however_it_is_nested`, over a row + buried under three containers that are each a fraction of their parent; + swept over 2,100 box widths and exact at every one. +- **A length given as a share is not**, and cannot be: children asking for + the same fraction come out one or two steps apart, 0.001 to 0.002 px. A + position is the quantity that gets rounded, so the row fills exactly and no + two children leave a seam, and the spread between lengths is what that + costs. Exact composition would shrink it, not remove it -- five equal + lengths cannot fill a row whose step count is not a multiple of five. Both + halves of this are in + `equal_shares_differ_by_at_most_two_steps_and_fill_the_row`. - Text shaping is retained separately from line breaking. A greedy line break remains valid from its longest produced line through the width at which it was made, and `TextView` reports that interval through `Painter::holds`. @@ -601,6 +647,34 @@ resolved alignment, so a local redraw asks the question its parent asked, and the widget's own, which is what a change is compared against -- an override means the answer is the parent's to give again. +### Alignment as a property changed two examples, unnoticed + +Found 2026-09-16 by looking at `tabs` beside canonical `main` rather than at +its pixel count. `d3b0ebf` deleted `Aligned`, and `8220a78` deleted +`SetSize`, so `.align()`, `.center()`, `.sized()`, `.width()` and `.height()` +all set properties on **one** widget where each used to wrap it in another. +Chaining them therefore means something different now, and two of it showed: + +- **A stack or pad child could not be aligned at all**, because those + containers overrode every child with the near edge. `tabs`'s counters + asked for `Align::RIGHT` and sat at the left; `text`'s narrow panel asked + to sit at the top of its row and filled it. Fixed in `2bc6bdf`; both match + `main` again. +- **`.width()` overwrites what `.sized()` set on the same axis**, so + `tabs`'s `rrect.sized((100, 100)).center().width(leftover(2))` is a + 100-tall bar across two shares where `main` draws a 100x100 square centred + in them. **Open, and an API question rather than a bug**: one widget + carries one length per axis, so "a small thing centred in a big box" now + needs a container to hold the big box -- `(thing.sized(..).center(),) + .stack().width(leftover(2))` is the idiom, and it is what the old builders + were inserting silently. The alternatives are to update the example, or to + give a widget an outer length as well as its own. Awaiting Bryan. + +`text` still differs from `main` in its `aligned` panel, and that one is a +decision rather than a defect: `9d8415d` deleted `OrthoSize`, so a Y span +across itself is as long as its longest child instead of full width. The +example needs a width, not the framework a change. + ### A widget occupies its box, and must not report more than it draws `Scroll` reports `Size::LEFTOVER` on **both** axes: it clips its content to its @@ -684,22 +758,24 @@ pixels and `Holds`. Decided with Bryan on 2026-09-15. point that need not contain the box the part was drawn in. What it allows for has two parts and they are not the same shape. - **The two routes to a length** -- composed down the chain against - measured against the window -- get two steps either side. One step either - side is the derivation for a single rounding on each route, and it is - measurably too narrow: each route is a *chain* of multiplies, and a - generated tree wanted 2 steps where the derivation allowed 1. + measured against the window -- get three half steps either side. That is + the floor: two fires the `Holds` assertion on eleven generated cases. + Four was tried and was overshoot, and tightening it moved none of the + rig's twenty-five work counters, so slack here buys no reuse (`38eba54`). - **The multiply on the way in** gets one more step at the top of the range and nothing at the bottom, since truncation only ever drops. The whole of a box has no multiply in it, however many pixels are added to it, and allowing for one there compounded a step per level down a chain of widgets each taking the whole of its parent (`60367d8`). - Too wide is the unsound direction: it admits reusing a drawing where it - does not hold. Too narrow costs a redraw and fires the `Holds` assertion in - a debug build, which is how both of these were found -- **run the generated - cases in debug before trusting a release measurement of a rounding - change**, since `debug_assert` is what says the contract broke and the - release oracle passed both of these anyway. + **It cannot go to zero while a length reached two ways is two numbers.** + The range has to contain the box a drawing was made in, which the `Holds` + assertion in `draw_at` checks, and it must not contain a box the drawing + does not hold for, which the warm-against-cold oracle checks. Those two + only coincide where composition is exact -- the `i64` item under "Next" -- + and shrinking this allowance is the test of whether that worked. Too wide + is the unsound side of it. **Run the generated cases in debug**, since the + assertion is compiled out of every release run. - A pointer, a wheel notch, a shaped glyph advance and a window size arrive as floats and are put on the grid where they arrive. `Vec2` stays what the GPU and the platform speak; `PxVec2` is what layout decides in.