Record what making the frame a window length corrected

Step 2 of the layout plan is done in the experiment checkout, at 1512d84
and 23523ee. The handoff carries the commits and what is left; LAYOUT.md
carries the settled rule, which is now one coordinate unit with the frame
a length of it and both pins beside each other; the log carries what
implementing the plan corrected in it, including the four that would have
shipped as wrong layout and the two rig gaps that hid them.
This commit is contained in:
iris-ai committed 2026-09-19 00:21:44 -04:00
1 parent da1807664a
commit 36ce66554b
3 files changed
+196 -146

No files matched your search

+100
View File
@@ -7,6 +7,100 @@ must outlive it (settled design, the measurement method) is already in
`docs/LAYOUT.md`, and the current plan is in `docs/HANDOFF.md`. Commit ids
are in `/home/bob/repos/iris-layout-experiment` unless said otherwise.
## What making the frame a length found (worker, 2026-09-19)
Step 2 of the handoff, implemented over `0ef87eb` in
`/home/bob/repos/iris-layout-experiment`. Six of these are corrections to the
plan, not to the code that implemented it, so they are worth keeping even
after the step lands.
- **A narrowed frame is a window length, not a fraction of the parent's
frame.** The plan said both at once: `Pad` narrowing by `rel 1 - 32px` (a
fraction) and `Span` narrowing by `slot.len()` (a window length). The first
implementation resolved every `narrow` against the parent frame, which took
padding off twice for anything inside a padded row -- a `leftover(1)` slot
of a 900 px row inside `pad(16)` gave its child a frame of 418 where the
slot is 434. A slot *cannot* be written as a fraction of the row's frame:
that is a division of two lengths, the same argument that forced region
node entries to be translations. So `narrow` is a window length, `Pad` reads
its own frame with the new `Painter::frame_len` and takes the pixels off,
and nothing resolves a narrow a second time. Pinned by
`layout::a_share_inside_padding_fills_the_slot_it_was_given`.
- **The root resolved its own rule twice**, because `root_layout` passed the
declaration as both the narrow and the declaration: a root of `rel(0.5)` in
a 900 px window came out 225 wide. Pinned by
`layout::a_root_with_a_fraction_rule_is_that_fraction_of_the_window`.
- **Which box arm `in_parent` takes is decided by the box, not the frame.**
The first implementation treated any decided frame -- a narrow, a share, a
declaration -- as a box this widget chose, so a `Pad` (which narrows) lost
its child's box pin, and a rule changed over two zero pads relocated the
column under them instead of dividing it again (seed 59, depth 5,
`resize-size`). Only a declaration places the box inside the part it was
given; a narrowed frame leaves the box exactly the part. Pinned by
`unsettled::changing_a_rule_over_two_pads_divides_the_column_again`.
- **Frame validity is a pin, not a range.** A range of window pixels cannot
say "this answer is a fraction of *that* frame": two frames are different
lengths at the same window size. `LayoutHolds` therefore carries
`frame_len` beside `extent_len`, set where a widget reads its frame
(`frame_len`, which `Pad` does) and where a rule that is a fraction of the
frame is answered with it, and composed up where a length of this frame is
what reached the child. It replaced a `holds_for_frame` helper that
compared expressions and asked `Holds::ANY` whether anything depended on
them, which missed the rule-answered case entirely. **No test in the repo
distinguishes the rule-answered half**: 400 seeds over sixteen cases at
depth 5 agree with it and without it, and hand-built trees for the shape
the seed-30 records showed (a stale `1 rel` answer beside a stack's sizing
child) do not reproduce it either, because a frame that changes almost
always changes a box as well and the box pins catch it. Kept anyway,
because "these two invalidations always coincide" is the kind of unstated
assumption the three earlier plans died of; it is one line at each of two
sites if it is ever shown to be dead.
- **`Scroll` resolved its content length against its own box.** With one unit
its child's answer is a window length, so it becomes pixels against the
window; the two differ wherever a scroll's box is not its frame. Fixing it
then broke `resize`, because resolving a length against the window is a
*read of the window* and nothing recorded it: a viewport 40 px tall inside
a branch's box does not change when the window does, so an end-snapped
scroll kept the offset it had (seed 942, depth 6). `Painter::to_px` takes
the read where the resolution happens and pins the window only where the
length has a fraction in it; `window_px_len`, which returned the number
and recorded nothing, is gone. Pinned as
`unsettled::resizing_under_a_short_scroll_snaps_its_window_tall_content_again`.
- **A local redraw resolves its own frame** from its record's narrow and
declaration (`ask_again`, shared with `place_in`) rather than reusing the
retained frame. That deleted `asked_px` and the assert that the two agree:
they legitimately disagree when an ancestor's frame changed and the
ancestor was reused because nothing under it pinned the frame, and the
freshly resolved one is the right one.
- **The clipping debug assert compared the reported size with the frame**
rather than with the box it is a claim about.
Two rig changes came out of it, both kept:
- The shrinker prints the *shrunk* tree's divergence rather than the grown
tree's, and the divergence now lists, for the failing widget and every
ancestor, warm and cold `frame`, `ask`, `box` and `size`. Reading that
table is what found the box-arm defect; reconstructing a 20-widget plan by
hand would not have.
- A `size-resize` case: a size change and *then* a resize. Every other case
changes something and compares at once or resizes first, so an answer kept
as a fraction of the wrong length agreed at the window it was made at and
parted from cold at every other one.
Checks at the end of the step (`23523ee`): `cargo fmt --all --check`, clippy
with and without `layout-diagnostics`, 122 suite tests, 20 core, 11
generated, the 400-seed depth-5 shrinker over all sixteen cases, the
1000-seed depth-6 oracle, and the 2000-seed depth-4 scan over all sixteen
cases.
One thing is unexplained and worth knowing about: seed 30 at depth 5
(`shuffle-all-but-first`) failed once, with a span keeping an answer of
`1 rel` for an axis whose frame had become `78 px`, and then stopped failing
before the pin was added -- reverting each part of the change since that run
did not bring it back. Either an edit fixed it that I could not identify, or
the rig is not as deterministic as it looks. If a seed ever fails once and
not again, suspect the second.
## What the one-ask protocol found (planner, 2026-09-18, third session)
Branch `wip/one-ask` at `3091fb8` over `4328eac`. The change is described in
@@ -404,6 +498,12 @@ is the padded share and fits. The clipped render records the current bug.
was checked this way and is new under the pin experiment).
- Depth finds things and so does breadth; widen one axis at a time and
record which.
- The cases are what a change is *then* compared at, and until 2026-09-19
every one of them compared at the window the change was made in. A
`size-resize` case was added because a length kept as a fraction of the
wrong box agrees with cold at that window and parts from it at every other
one. There is still no case that changes a tree twice with a resize
between, and no case that resizes twice.
## Smaller open items (carried from the old handoff)