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:
1 parent
da1807664a
commit
36ce66554b
3 files changed
+196
-146
No files matched your search
+61
-131
@@ -14,24 +14,31 @@ detached comparison checkout is `/home/bob/repos/iris-layout-baseline`. It is
|
||||
the reviewed baseline this work must preserve or improve.
|
||||
|
||||
The continuation is `/home/bob/repos/iris-layout-experiment`, now on branch
|
||||
**`wip/one-ask`** at **`0ef87eb`**, two commits over `4328eac` (the head of
|
||||
`wip/transparent-frames`, which is unchanged). It replaces the step 3 plan
|
||||
below with a smaller protocol change, and it passes every check:
|
||||
**`wip/one-ask`** at **`23523ee`**, four commits over `4328eac` (the head of
|
||||
`wip/transparent-frames`, which is unchanged). It replaces the old step 3
|
||||
plan with the one-ask protocol below, and `1512d84` and `23523ee` make the
|
||||
frame a length of the window. It passes every check:
|
||||
|
||||
| check at `0ef87eb` | result |
|
||||
| check at `23523ee` | result |
|
||||
| --- | --- |
|
||||
| `cargo fmt --all --check`, clippy `-D warnings`, with and without `layout-diagnostics` | clean |
|
||||
| `cargo test --workspace` (debug) | 115 suite, 20 core, 11 generated, all green |
|
||||
| the two decided-box pins from step 1, and the new seed 946 pin | **pass** (the first two were red at `4328eac`) |
|
||||
| `cargo test --workspace` (debug) | 122 suite, 20 core, 11 generated, all green |
|
||||
| `cargo test --release --test generated` | 11/11 |
|
||||
| shrinker, 400 seeds, depth 5, all fifteen cases | agree, 56 s |
|
||||
| 2000-seed depth-4 scan, all fifteen cases | agree, 262 s |
|
||||
| 1000 seeds at depth 6 | agree, 148 s (found seed 946 at `3091fb8`, fixed and pinned in `0ef87eb`) |
|
||||
| shrinker, 400 seeds, depth 5, all sixteen cases | agree, 66 s |
|
||||
| 1000 seeds at depth 6 | agree |
|
||||
| 2000-seed depth-4 scan, all sixteen cases | agree |
|
||||
|
||||
Not done: the renders and the `tabs` replay (step 2), and the pre-submit
|
||||
review of the two commits.
|
||||
What implementing it corrected in the plan is in `docs/LAYOUT_LOG.md`; the
|
||||
four that would have shipped as wrong layout are a share inside padding
|
||||
losing the padding twice, a root resolving its own rule twice, a rule changed
|
||||
over two pads relocating the column under them instead of dividing it again,
|
||||
and a resize leaving a short scroll's window-tall content where it was. Each
|
||||
is pinned as a named test.
|
||||
|
||||
The worker's uncommitted step 3/4 experiment is preserved as branch
|
||||
Not done: the renders and the `tabs` replay, the retained-cost work, the
|
||||
renames, and a pre-submit review of the three commits as one diff.
|
||||
|
||||
The worker's older step 3/4 experiment is preserved as branch
|
||||
`wip/step3-experiment` (one commit over `4328eac`) and as
|
||||
`~/repos/iris-step3-experiment.patch`. It is evidence, not the protocol.
|
||||
The app's Iris pin is unchanged.
|
||||
@@ -77,7 +84,7 @@ is the third plan for this repair and the next one should not repeat it:
|
||||
**A widget draws once, in the box it is asked in. Its answer is placed inside
|
||||
that box by re-expressing the drawing. Nothing is drawn again in a box an
|
||||
answer chose.** `Holds` is a contract about the ask box alone, consulted only
|
||||
to decide whether a re-ask can be skipped. This is `draw_inner` at `3091fb8`:
|
||||
to decide whether a re-ask can be skipped. This is `draw_inner` at `1512d84`:
|
||||
|
||||
```rust
|
||||
let reused = (!stale)
|
||||
@@ -85,8 +92,7 @@ let reused = (!stale)
|
||||
.flatten()
|
||||
.and_then(|answer| {
|
||||
let extent = placed_extent(part, answer.0, declared, info.fill(), align);
|
||||
self.try_reuse(id, frame, part, extent, info, rsc)
|
||||
.map(|()| answer)
|
||||
self.try_reuse(id, part, extent, info, rsc).map(|()| answer)
|
||||
});
|
||||
let answer = reused.unwrap_or_else(|| {
|
||||
if old.is_none() {
|
||||
@@ -95,7 +101,7 @@ let answer = reused.unwrap_or_else(|| {
|
||||
let answer = self.draw_at(id, part, info, old.take(), rsc);
|
||||
let extent = placed_extent(part, answer.0, declared, info.fill(), align);
|
||||
if extent != part {
|
||||
self.reposition(id, frame, extent, info, rsc);
|
||||
self.relocate(id, extent, info, rsc);
|
||||
}
|
||||
answer
|
||||
});
|
||||
@@ -118,8 +124,10 @@ with a new call that never runs the body:
|
||||
pub fn place_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, place: [Place; 2])
|
||||
```
|
||||
|
||||
A frame is narrowed by a *length* of the parent's frame, never a region, and
|
||||
is put back into the part by the child's alignment on every placement:
|
||||
A frame is narrowed by a *length of the window*, never a region and never a
|
||||
fraction of the parent's frame -- a row's slot cannot be written as a
|
||||
fraction of the row. The box stays whatever `place` names; only a declaration
|
||||
places the box inside it, by the child's alignment, on every placement:
|
||||
|
||||
```rust
|
||||
pub fn widget_at<'s, W: ?Sized>(
|
||||
@@ -150,9 +158,11 @@ let used = match len.leftover > Weight::ZERO && shares {
|
||||
};
|
||||
```
|
||||
|
||||
`Stack` asks non-sizing children in the box the sizing child decided, with
|
||||
the frame narrowed to it on every axis that is not a share, and `Scroll` asks
|
||||
its content once in the viewport and `place_at`s it to the scrolled offset.
|
||||
`Stack` asks non-sizing children with `Part::Sized(len)` of what its sizing
|
||||
child decided, on every axis that is not a share -- a box of that length
|
||||
where their own alignment puts it, and that length as their frame -- and
|
||||
`Scroll` asks its content once in the viewport and `place_at`s it to the
|
||||
scrolled offset.
|
||||
|
||||
A local redraw asks the retained question again -- the same place of the box
|
||||
the parent was *asked* in -- and, if the answer stands, puts the fresh drawing
|
||||
@@ -216,12 +226,11 @@ row by exactly the icon's width. With the pad in a share instead, both are
|
||||
the share less 32. An icon *after* the padded text overflows; a user who
|
||||
wanted otherwise meant `leftover`.
|
||||
|
||||
**The experiment does not implement this**, and cannot in its representation:
|
||||
a narrowed frame is its own box there (`frame_and_extent` sets the extent to
|
||||
`FULL` of it), so 868 for `rel` and 844 for the wrap cannot both hold. `Pad`
|
||||
still insets only the box through `Part::Of` and forwards the frame whole,
|
||||
which makes `rel(1.0)` under it 900. Step 2 below is the representation
|
||||
change that expresses the rule.
|
||||
Implemented at `1512d84`. `Pad` reads its own frame (`Painter::frame_len`,
|
||||
which pins it), takes the padding off, and hands that down as the child's
|
||||
frame, while the box it gives is the inset part of its own box. The two are
|
||||
different lengths whenever the box is narrower than the frame -- which is
|
||||
exactly the wrapping case above.
|
||||
|
||||
### A share never adds room beyond the deciding box
|
||||
|
||||
@@ -235,114 +244,31 @@ Unchanged; see `docs/LAYOUT.md`.
|
||||
## Implementation plan
|
||||
|
||||
Work in `/home/bob/repos/iris-layout-experiment` on `wip/one-ask` from
|
||||
`3091fb8`. Make each step a warning-clean commit and run its named checks
|
||||
`23523ee`. Make each step a warning-clean commit and run its named checks
|
||||
before the next. If a step exposes a different mechanism, stop and update
|
||||
this handoff rather than papering over it.
|
||||
|
||||
### 1. Review the two commits
|
||||
Anything a fuzzer finds is shrunk first (`SHRINK_SEED=<seed>
|
||||
SHRINK_DEPTH=<depth> SHRINK_CASE=<case>`, which now prints each level's
|
||||
frame, ask, box and size warm against cold), pinned as a named test in
|
||||
`tests/cases/unsettled.rs`, then fixed under the two rules above. Do not add
|
||||
a second draw back.
|
||||
|
||||
Run the pre-submit review over `3091fb8` and `0ef87eb` as one diff against
|
||||
`4328eac`; they were written as a probe and reviewed only by their tests.
|
||||
Anything a fuzzer finds later is shrunk first (`SHRINK_SEED=<seed>
|
||||
SHRINK_DEPTH=<depth> SHRINK_CASE=<case>`), pinned as a named test in
|
||||
`tests/cases/unsettled.rs`, then fixed under the rule above. Do not add a
|
||||
second draw back.
|
||||
### 1. Review the four commits
|
||||
|
||||
Run the pre-submit review over `3091fb8`, `0ef87eb`, `1512d84` and `23523ee`
|
||||
as one diff against `4328eac`. `1512d84` rewrote much of what the first two did to
|
||||
`painter.rs` and `render_state.rs` and was reviewed as it was written, but
|
||||
the three have never been read as one change, and the parts of the one-ask
|
||||
protocol it did not touch -- `place_at`, the twice-asked deferral, `Span`'s
|
||||
two passes -- were written as a probe and reviewed only by their tests.
|
||||
|
||||
### 2. Make the frame a length and the box a region
|
||||
|
||||
The frame stops being a coordinate system. **There is one coordinate unit,
|
||||
the window**: every box in the tree is a region in window units, a widget's
|
||||
frame is a *length* in the same units and is only what fractions resolve
|
||||
against, and a region node's move entry is a *translation* -- a region `rel
|
||||
1` long anchored at the node's box start -- rather than a box. The last
|
||||
point is forced, not chosen: a node whose unit was its frame would need its
|
||||
box expressed in frame units, which is a division of two lengths and not a
|
||||
`rel + px` length (a `.region_node()` child of a pad in a span room is the
|
||||
case). **Decided by Bryan, 2026-09-18: yes.** It changes one cost. A node that
|
||||
moves, or scrolls, is one entry write whatever narrowed it (today only a
|
||||
narrowed node gets that; a transparent one is re-expressed), and a node
|
||||
whose box changes *length* at the same window size is re-expressed as a
|
||||
subtree (today a narrowed node is one write). The shader's `resolve_move`
|
||||
composes a `rel 1` entry as a translation already and does not change.
|
||||
|
||||
```rust
|
||||
pub struct Painter<'a> {
|
||||
/// This widget's frame as a length of its region node's box, per axis:
|
||||
/// what a fraction it declares or reports, and a `From` span under it,
|
||||
/// are fractions of. A length and not a region, so the box need not be
|
||||
/// the frame -- padding takes from both without either becoming the other.
|
||||
pub(super) frame: UiVec2,
|
||||
/// Where this widget's drawing goes, in the node's coordinates.
|
||||
pub(super) extent: UiRegion,
|
||||
/// The window in pixels: the frame in pixels is `frame.to_px(window)`
|
||||
/// and the box's is `extent.size().to_px(window)`. Every `Holds` range
|
||||
/// is in these pixels, so `in_parent` combines them without `through`,
|
||||
/// and a widget's own frame reads convert once: `Holds::at(frame_px)
|
||||
/// .through(frame)`.
|
||||
pub(super) window: PxVec2,
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`Part::of` does not change: with one unit a `From` span is already a
|
||||
window-unit length, so a span's cursor, `far`, `room` and slots stay as they
|
||||
are and only its shares boundary reads `room.to_px(window)`. Two places
|
||||
resolve through a frame: a reported fraction (`in_parent_frame`, now
|
||||
`size.within_len(child_frame)`) and a declared one (`narrowed_by`, then a
|
||||
`Sized` part placed by alignment). A widget's own frame reads convert once,
|
||||
`Holds::at(frame_px).through(frame)`, the exact preimage of the `to_px` that
|
||||
made them. `recompose_subtree` and `reposition` become one operation, since
|
||||
a child's box is always `Part::of(parent box, place)`; `asked_px` is the
|
||||
window.
|
||||
|
||||
A child's frame is `narrow.within_len(parent.frame)` where the caller or a
|
||||
rule narrowed it, else the parent's; a declared length also decides the box
|
||||
(`len` placed in the part by alignment, as `frame_and_extent` does today),
|
||||
where a caller's `narrow` decides the frame alone and the place decides the
|
||||
box. `Pad` then says both halves of the rule:
|
||||
|
||||
```rust
|
||||
let inset = |lead: Px, trail: Px| {
|
||||
Place::Within(Part::Of(UiSpan::new(
|
||||
Len::from_parts(Rel::ZERO, lead),
|
||||
Len::from_parts(Rel::ONE, -trail),
|
||||
)))
|
||||
};
|
||||
let less = |lead: Px, trail: Px| Some(Len::from_parts(Rel::ONE, -(lead + trail)));
|
||||
let inner = painter
|
||||
.widget_at(
|
||||
&self.inner,
|
||||
[less(self.padding.left, self.padding.right), less(self.padding.top, self.padding.bottom)],
|
||||
[inset(self.padding.left, self.padding.right), inset(self.padding.top, self.padding.bottom)],
|
||||
)
|
||||
.size();
|
||||
```
|
||||
|
||||
What this changes in the core, and why it is not merely more code:
|
||||
|
||||
- `Painter::resolve` becomes `region.within(&self.extent)`; the second
|
||||
`within` through the frame goes.
|
||||
- `in_parent` no longer maps a child's frame holds through the frame length
|
||||
per level: every widget holds for the window's pixels, so the holds `and`
|
||||
directly, at region nodes too.
|
||||
- `DrawInfo::frame`/`frame_abs`, `ActiveData::frame`/`frame_abs`, `local`
|
||||
in `Placing`, `recompose_subtree` and `asked_px` all change meaning:
|
||||
`frame` is a `UiVec2` length, `frame_abs` goes, a node's move entry is a
|
||||
`rel 1` region at the node's box start, and `asked_px` becomes the window
|
||||
size.
|
||||
- `Stack` needs a part that is "a box of this length, placed by the child's
|
||||
alignment" (`Part::Sized(Len)`, the same thing a declared length does),
|
||||
since `narrow` no longer makes the box.
|
||||
|
||||
The random rig and the shrunk tests do not change. Expected precision is the
|
||||
same as today (one `rel × rel` per level of narrowing). Checks: the suite,
|
||||
the fast oracle, the 400/5 shrinker, then both long fuzzers; the `many`,
|
||||
`resize` and `cold` counters must not move by more than the reposition work
|
||||
this removes; and two new tests, `rel(1.0)` inside `.pad(16)` after a 24 px
|
||||
icon is 868 px while the wrapped text beside it wraps at 844, and the same
|
||||
inside a 450 px share is 418 for both. `examples/text.rs` then renders
|
||||
inside its padding.
|
||||
**Done at `1512d84` and `23523ee`.** The settled rule is in `docs/LAYOUT.md` under
|
||||
"Frames, decided boxes and padding"; what implementing it corrected in the
|
||||
plan -- three of them wrong layout that would have shipped -- is in
|
||||
`docs/LAYOUT_LOG.md`.
|
||||
|
||||
### 3. Render and replay
|
||||
|
||||
@@ -358,8 +284,12 @@ likely to show here; inspect every intentional difference and record it.
|
||||
Rename `ActiveData::offer_part` to `part`, `offer_place` to `asked`, `place`
|
||||
to `placed`, and `DrawInfo` likewise; delete `ActiveData::measured` in favour
|
||||
of reading `answer`; delete `answers_at` if `resize` is its only caller and
|
||||
inline it. Every use was written against the old names on purpose to keep
|
||||
the probe's diff readable; do this as one mechanical commit. Suite, oracle.
|
||||
inline it. Rename `LayoutHolds::frame` to `window` and `Painter::frame_own`
|
||||
with it: since `1512d84` those ranges are window pixels, and the only thing
|
||||
that writes them is `Painter::window_holds`; the frame's own entry is the
|
||||
`frame_len` pin beside it. Every use was written against the old names on
|
||||
purpose to keep the probe's diff readable; do this as one mechanical commit.
|
||||
Suite, oracle.
|
||||
|
||||
### 5. Restore the expected retained cost
|
||||
|
||||
@@ -423,7 +353,7 @@ SHRINK_CASE=all SHRINK_SEEDS=2000 SHRINK_DEPTH=4 \
|
||||
cargo test --release --test shrink -- --ignored --nocapture
|
||||
```
|
||||
|
||||
The last line is the 2000-seed depth-4 scan over all fifteen cases; the
|
||||
The last line is the 2000-seed depth-4 scan over all sixteen cases; the
|
||||
shrinker runs the same cases as the scan and reduces anything it finds, so
|
||||
no temporary test body is needed any more. `Rng::new` uses `seed | 1`, so
|
||||
adjacent even/odd seed pairs describe the same tree.
|
||||
|
||||
+35
-15
@@ -333,11 +333,30 @@ that box by re-expressing the drawing, and nothing is drawn again in a box an
|
||||
answer chose. `Holds` is a contract about the ask box alone, read only to
|
||||
decide whether a re-ask can be skipped. A container that puts an answer
|
||||
somewhere other than where it asked says so with `Painter::place_at`, which
|
||||
never runs the body. A frame is narrowed by a length of the parent's frame,
|
||||
never by a region, and is put back into the part by the child's alignment on
|
||||
every placement. This is `wip/one-ask` in the experiment checkout. The
|
||||
padding rule above needs the frame to be a length and the box a region in
|
||||
the region node's coordinates, which is the next step in `docs/HANDOFF.md`.
|
||||
never runs the body. This is `wip/one-ask` in the experiment checkout.
|
||||
|
||||
**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
|
||||
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
|
||||
translation -- a `rel 1` region anchored where its box starts -- so nothing
|
||||
composes a frame 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
|
||||
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
|
||||
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::extent_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
|
||||
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
|
||||
it is only pixels.
|
||||
|
||||
## Layout decisions and invariants (2026-09-15 to 2026-09-17)
|
||||
|
||||
@@ -389,14 +408,15 @@ floats.
|
||||
so a boundary derived with a division guards a drawing made with a
|
||||
multiply (`53b00c6`).
|
||||
|
||||
### A box in pixels is one multiply from its parent's
|
||||
### A box in pixels is one multiply from the window
|
||||
|
||||
A draw threads pixel lengths down: the box a parent gave a widget, then the
|
||||
part of that box its own answer placed its drawing in. `Painter::px_size`
|
||||
and `px_len` read that value, and a local redraw takes the same steps back
|
||||
up the parent chain (`asked_px`). Neither chain has a coordinate frame in it,
|
||||
so a region node cannot break either, and warm and cold reach every length
|
||||
by the same expression.
|
||||
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
|
||||
`asked_px`; both are gone.)
|
||||
|
||||
- **`Holds::through` is the exact preimage of `px + floor(rel * box)`**:
|
||||
`floor(rel * B) >= lo - px` is `rel * B >= (lo - px) << REL` and
|
||||
@@ -412,9 +432,9 @@ by the same expression.
|
||||
Nothing layout decides is composed back up the move chain.
|
||||
- **`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. `asked_px`
|
||||
walks up only where a widget is already being redrawn; the mean chain is
|
||||
2.8 levels.
|
||||
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
|
||||
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,
|
||||
`to_px(output_size)` on the CPU and the uniform in the shader. A resize
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user