Record the leftover minimum and the images in the trees

Two rounds past the ninth sweep: `b295c8b` reads a share under a parent that
divides nothing as a minimum, which is what Bryan said it means and what a span
already did, sharing one comparison with it; `2dba90b` grows images in the
generated trees, which is the only widget here whose hint is a length in pixels
and so the only one that reaches the case.

The dump baseline is 34,571 boxes now that the trees carry images, and a seed no
longer names the tree it used to -- the log says what that costs and where those
defects live instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-20 04:51:13 -04:00
1 parent da6b003a1e
commit 6a92857079
2 files changed
+68 -21

No files matched your search

+17 -11
View File
@@ -6,8 +6,8 @@ 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 `c2b8bf8`,
and past the reviewed `cadfba0` it is eleven rounds, each described in
**Iris PR #19** (`layout/one-ask`) replaces closed #18. The tip is `2dba90b`,
and past the reviewed `cadfba0` it is twelve rounds, each described in
`docs/LAYOUT_LOG.md`:
- **The repair**, `add6774` and `84dad21` -- collapsed-share placement,
@@ -50,6 +50,11 @@ and past the reviewed `cadfba0` it is eleven rounds, each described in
call sites, the helper the eighth sweep shared being unable to see the
defect it had just fixed, two bare arrays where the framework names the
pair, forty-five lines nothing references, and one word for two things.
- **A leftover as a minimum, and images in the trees**, `b295c8b` and
`2dba90b` -- a share under a parent that divides nothing losing the
overflow its pixels asked for, said as the place the parent gives and
sharing one comparison with the span; and `Image` grown in the generated
trees, which is the only widget here whose hint is a length in pixels.
The settled design of the vocabulary rounds is in `docs/LAYOUT.md` under
"Three names, and the one argument that says them". Bryan settled the API
@@ -106,9 +111,9 @@ the name it is reachable by.
**Always**, because they cost nothing: format, workspace clippy under
`-D warnings` with and without `layout-diagnostics`, the workspace tests, and
the **cold dump**. `layout_dump` over 400 depth-5 trees is **34,488** boxes
-- counted again on 2026-09-20, the fourth through eighth sweeps having all
repeated 34,492, which is a `wc -l` of the whole run rather than of its box
lines -- and it is the only thing that catches two same-typed values being
-- **34,571 since `2dba90b` grew images in the trees**; counted again on
2026-09-20, the fourth through eighth sweeps having all repeated 34,492, which
is a `wc -l` of the whole run rather than of its box lines -- and it is the only thing that catches two same-typed values being
swapped, which is the failure mode of a rename or a move. Count the lines
that are a box (`grep -cE '^[0-9]+ [0-9]+ '`), not the output. The repair moved 650 of those
boxes, all from the collapsed-share correction; every commit since has been
@@ -178,11 +183,10 @@ glyphs do not follow a shortened entry.
- A span can overflow itself without bound, so boxes of negative length reach
children and nothing states what a widget may assume about one.
- A `leftover` under a parent that does not divide is a minimum size --
`max(box, px + rel*box)` (Bryan, 2026-09-20). A span does that; the
non-dividing path drops the overflow where the fixed part is longer than
the box. Measured in `docs/LAYOUT_LOG.md` under the ninth sweep, with the
window contract a fix needs. Nothing here mixes the two, so nothing is
wrong on screen.
`max(box, px + rel*box)` (Bryan, 2026-09-20), which is a `SizeRule::Min` of
`rel(1.0)` and shares `Painter::longer_than` with the span. Done in
`b295c8b` for a widget its parent asks; `root_layout` still reads the old
way, for which see `docs/LAYOUT_LOG.md`.
- `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`; both
are caller bugs under `debug_assert`, but the fallbacks differ.
- `docs/LAYOUT.md` §4, §5 and the density section name `Painter::place`,
@@ -191,7 +195,9 @@ glyphs do not follow a shortened entry.
`OnResize::Translate` or `OrthoSize`.
- `LazySpan`, then `SizeRule::{Min, Max, Clamp}`. A cap may not contain
`leftover`; whether `Max` narrows the child's drawing box is a product
decision.
decision. `Min` is `Painter::longer_than` again -- a `leftover` where
nothing divides it is already a `Min` of `rel(1.0)` -- so it belongs on
that path rather than beside it.
- `Scroll` taking a direction rather than one axis.
Other product work is in `docs/PLAN.md` and the focused documents it links.
+51 -10
View File
@@ -6,6 +6,28 @@ nothing here is rediscovered. Each entry says who found it and when.
it (settled design, the measurement method) belongs in `docs/LAYOUT.md`, and
the current plan is in `docs/HANDOFF.md`.
## Images in the generated trees (2026-09-20)
`Image` is the only widget here whose size hint is a length in pixels, so it is
the only one that exercises a rule beside a hint -- which is what the two
findings above are about, and why neither was reachable from a generated tree.
Bryan asked for one in the trees, as a committed 64x64 checkerboard reusing one
texture handle. `2dba90b`: `Kind::Image` is a fifth leaf drawn one time in five,
stepping to a plain rect under the shrinker; the picture is
`src/assets/checkerboard.png`, purple and black in 8 px cells, included rather
than opened; and a `TextureHandle` is a counted reference, so the first image in
a tree uploads and the rest clone it. Measured: seed 1 at depth 4 grows 13
images and holds 1 texture, and `a_tree_of_images_uploads_one_texture` asserts
it.
The cost is that a seed no longer names the tree it used to, so the corpus in
`generated.rs` is a corpus and not a set of regression cases -- 20 and 86 no
longer grow the trees whose defects they caught. Both live on as shrunk fixtures
in `unsettled.rs`, which are trees rather than numbers, and both files now say
so. New dump baseline: **34,571** boxes over the 400 depth-5 trees. All three
scans pass over the new trees (400 at depth 5 in 62.79s, 1,000 at depth 6 in
160.20s, 2,000 at depth 4 in 299.58s).
## Ninth sweep: the widget vocabulary, and the eighth sweep's fix (2026-09-20)
Over the part no earlier round named -- `src/widget/trait_fns.rs` and
@@ -65,16 +87,35 @@ learns the fixed part and falls back to the offer. The same length without the
share does overflow (drawn -50..450, its alignment centring it), so what
swallows it is the share and not the overflow.
Fixing it means answering the longer of the offer and the fixed part, which is
not a `Len`: a maximum of two linear forms is not linear in the box, so it has
to be resolved where the box is known and the crossover pinned as a window
contract -- which is exactly what `Span` does with `has_room` and
`painter.window_holds(axis, holds.through(room))`. In `widget_at` that range
belongs to the parent, whose box decides it, so a parent of such a child would
redraw across the crossover. **Not done: it is what a length means, and it is
Bryan's to say whether that cost is wanted.** Nothing in the repository mixes
`px` or `rel` with `leftover` outside the span's own test, so nothing is wrong
on screen today.
**Fixed in `b295c8b`**, after Bryan asked for it if it was cheap and pointed out
that the machinery is a minimum size of `rel(1.0)` and should share a path with
one. What made it cheap was saying it as the place the parent gives rather than
as a declaration: where the fixed part is the longer, `widget_at` hands the
child `fixed.as_desc().fills()`, which is what a declared length already comes
to, and `active.placed` keeps it -- so nothing about `Declared`, `ActiveData` or
the resize path had to change. A place that is already the child's placement is
skipped, because a parent that divides has given the share whatever it was owed;
without that guard a span's slot was re-placed and its child moved from 0..500
to -50..450.
The comparison is one operation now, `Painter::longer_than`, and both callers
share it: the span's room for the shares it divides, and a share past the box it
was given. It narrows the widget's window range where the span replaced it,
since a comparison the framework makes on an arbitrary parent's behalf is one
more reason its drawing holds rather than the only one. `SizeRule::Min` of
`rel(1.0)` is the same operation and belongs on the same path when it lands.
Two things were tried and dropped. An escalation for a changed rule
(`shares_past_a_length`) turned out to buy nothing: the reported size is the
rule resolved, so a changed rule changes the answer and the parent refuses its
own drawing -- the tests pass without it. And `Holds::crossing`, a named
constructor for the range, had one caller and read better spelled there.
**The root still reads the old way.** `root_layout` asks
`Widgets::declared_lens` with no painter, so a mixed share on the root is the
window rather than the longer of the two. It would mean the comparison in a
second place, and the root's box is the window and is relaid out on every
resize; nothing here puts a share on a root.
**Marking a widget for redraw had no name.** Twenty-one sites under `tests/`
said it as `widgets_mut().get_dyn_mut(id);` with the widget thrown away: five