Record the root unification, and where bounds stand

`0d03267` in iris unifies the root's layout path with every other widget's:
`Placing::WINDOW` is the box nobody drew and `Placing::ask` the one place a
box is decided. The leftover-as-a-minimum gap the log recorded for
`root_layout` is closed by it rather than left.

`SizeRule::{Min, Max, Clamp}` is on `layout/bounds` and not in #19. Every
hand-written test passes and the 400-seed scan does not, for a reason that is
a design question: a bound is the first rule whose effect depends on the box
its parent gives it, and the retained machinery hands a widget a box by paths
that never ask it again. The log records the four readings measured and what
each one's seeds were, so none of them is tried twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-20 14:35:28 -04:00
1 parent 6a92857079
commit 149a6838af
2 files changed
+91 -15

No files matched your search

+27 -15
View File
@@ -55,6 +55,12 @@ and past the reviewed `cadfba0` it is twelve rounds, each described in
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.
- **One ask, the root's included**, `0d03267` -- the root had a layout path
of its own, so a rule that reads the box it is offered reached every widget
but that one. `Placing::WINDOW` is the box nobody drew and `Placing::ask`
the one place a box is decided; the root's own path is now the bookkeeping
a widget with no parent keeps. Bryan asked for this rather than a widget
above the root, which would pin the tree to 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
@@ -110,14 +116,15 @@ 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
-- **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
byte-identical to `84dad21`.
the **cold dump**. `layout_dump` over 400 depth-5 trees is **34,571** boxes
since `2dba90b` grew images in the trees, and `0d03267` left every one of them
byte-identical. It was 34,488 before the images; the fourth through eighth
sweeps all repeated 34,492, which is a `wc -l` of the whole run rather than of
its box lines, so count the lines that are a box
(`grep -cE '^[0-9]+ [0-9]+ '`). It is the only thing that catches two
same-typed values being swapped, which is the failure mode of a rename or a
move. The repair moved 650 of those boxes, all from the collapsed-share
correction; every commit since has been byte-identical to `84dad21`.
**Only when the change can alter what layout computes**: the three seed scans
-- 400 at depth 5, 1,000 at depth 6, 2,000 at depth 4. They cost about a
@@ -185,19 +192,24 @@ glyphs do not follow a shortened entry.
- A `leftover` under a parent that does not divide is a minimum size --
`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`.
`b295c8b`, and in `0d03267` for the root as well, which used to read it the
old way.
- `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`,
`Painter::region()`, `SetSize`, `desired_width`, `apply_rest`, `Len::dp`,
`Aligned` and `MaxSize`, none of which exist. Do not restore
`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. `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.
- `SizeRule::{Min, Max, Clamp}` is written and pushed as `layout/bounds`
(`76aaf06`), and **is not ready to land**: every hand-written test passes,
including a capped scroll taking its viewport from the cap, but the
400-seed scan does not, and the reason is a design question Bryan has to
settle -- see `docs/LAYOUT_LOG.md` under "Bounds are a rule that reads its
box". Bryan settled two things already: `Max` narrows the child's drawing
box (2026-09-20), and a rule gives a length or bounds one but never both,
since a capped share is said with two widgets. A cap may not contain
`leftover`.
- `LazySpan`.
- `Scroll` taking a direction rather than one axis.
Other product work is in `docs/PLAN.md` and the focused documents it links.
+64
View File
@@ -6,6 +6,70 @@ 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`.
## One ask, the root's included (2026-09-20)
`0d03267`. `root_layout` read the root's declared lengths against the window
while every other widget's box came of `Painter::widget_at`, where a rule of
the widget's own can take the box past what its parent offered. Bryan asked
for the two paths unified, and ruled out doing it by putting a widget above
the root: a root sized in pixels would make every fraction under it a fraction
of a px length, which pins the whole tree to those pixels and re-asks it on
every resize.
`Placing::WINDOW` is the unification -- the full output, fractions of the full
output, no move entry and no mask -- and `Placing::ask` is the one place a box
is decided, called by the painter, by a local redraw, and by the root's first
draw. What is left of the root's own path is the bookkeeping a widget with no
parent keeps.
Two things fell out. `DrawInfo::asked` is now the place the parent offered
rather than the place the ask came to, so a local redraw re-decides a rule
instead of re-reading its decision; the two were the same until a rule could
move the box. And the root's `is_region_node` is read, where the old path
passed `false`.
The comparison a rule makes is kept on the widget asked about rather than on
the asker. A window range means the same thing at either end of an ask and
`in_parent` passes one up unchanged, so the asker still ends up holding it
through the child's drawing -- and the root, which has no asker, needs nothing
of its own, since `resize` already checks its record.
## Bounds are a rule that reads its box, and the oracle refuses one (2026-09-20)
On the branch `layout/bounds` (`76aaf06`), not in #19. `SizeRule::{Min, Max,
Clamp}` is the capability `MaxSize` had on the app's pin and nothing on this
branch has. Every hand-written test passes, including a capped scroll taking
its viewport from the cap; the 400-seed depth-5 scan does not.
The finding is general and worth keeping whatever is decided. A bound is the
first rule whose effect depends on the box its parent gives it, and the
retained machinery hands a widget a box by paths that never ask it again
(`place_in` from a re-placing parent, `reposition` after a parent's box moved).
A decision made when the box was one length survives into a box of another, so
warm and cold disagree about a tree they agree on structurally. Four readings
measured over 400 seeds at depth 5:
- decided at every ask and kept: seeds 291, 1, 120, 178, 64 differ.
- re-decided at `place_in` as well: seeds 1, 362, 188, 254, 156, because that
path's box is the one the answer chose, not the one the widget was asked in.
- skipping a place the parent decided outright, which is the rule the share
follows: worse, since the same widget then gets two decisions by two paths.
- the bound as an answer rule only, leaving the box alone: seeds 4 and 196 --
much the closest.
The share is the one existing rule of this kind, and it is stable for two
reasons that do not generalise: `place_at` re-asks a child whose rel base it
narrows, and a share that binds is baked into the retained place as a `Sized`
length. A bound that binds is a length of the rel base, and `Sized` cannot say
"this slot, narrowed" for a `Within` place.
Neutering the bounds in the generator while leaving its random draws in place
puts the same shapes back to green, so the divergence is the bounds and not
the new trees. Two findings from the branch are worth having whatever happens
to it: a rule that is a fraction must pin its rel base, which `Exact` was
already missing for bounds-shaped reasons; and `widget_trait!` dropped
attributes, so no method it defines could carry a doc comment.
## 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