From 4b976e240eb0c30f296d0bfd47653a8bf0a6ceda Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Fri, 18 Sep 2026 13:03:14 -0400 Subject: [PATCH] Say what the two open transparent-frames seeds are and what they turn on A second planning pass over 49cec82: both seeds are the rule that picks which answer places a box, shown by shrunk trees, a draw trace and two counter-experiments, with the recommendation they point at. Co-Authored-By: Claude Fable 5.1 --- docs/HANDOFF.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index a53b0b1..e817d41 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -351,6 +351,123 @@ narrow (`Part::Of`, what `Pad` does), or a third thing Bryan decides. row), or "outset pixels, inset `rel` and `leftover`", which needs a way to say a box in a narrowed frame that the grid cannot express today. +### What the two open seeds are (2026-09-18, second planning pass) + +Read by the planning session from the code and from traces at `49cec82`, +after Bryan asked whether the two questions above need answering or whether +there is a larger flaw. Both, and the flaw is one level below the frame: it +is the rule that decides **which answer places a widget's box**. + +```rust +// draw_inner +let measured = match info.offer() { + true => answer.0, + false => self.active[&id].measured().unwrap_or(answer.0), +}; +// DrawInfo +fn offer(&self) -> bool { self.place == self.offer_place } +// draw_at: the placing re-draw reuses the parent's `info`, so it runs with +// the same flag as the measuring one +let at_offer = info.offer(); +``` + +The `offer` bit is meant to say "this ask is a measurement, keep its +answer". It has no consistent value once a container's body is evaluated in +more than one box, and the code evaluates it in three kinds: the room its +parent measured it in, a box the parent decided (a span slot, a fill), and a +box derived from its own answer (`place()`, and a scroll's content box). + +- **Seed 2, `repaint`.** Shrinks to `Stack{sized by child 0}[x: leftover] + > [OneLine, Pad{0} > Span{DOWN}[Rect, OneLine], Branch]`. The stack's box + is one text line tall. The span measured in the stack's *room* has leftover + room, draws the rect and reports `leftover` across; measured in the + stack's *box* it has none, undraws the rect and reports the one-line + text's width. Cold kept the first because the stack's placing evaluation + reused the pad's retained answer: the span's `extent_len` pin is dropped + by `in_parent`'s `Part::Of` arm (only `All` composes a pin), so the pad's + answer looked valid for a box the span had never been measured in. Warm + re-asked the span in the box and got the second. Composing the pin through + `Of` where the part is the whole box less pixels (`Len::from_parts(len.rel, + len.px - part_len.px)`) makes seed 2 agree, and breaks seed 220 (`reorder`) + and `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`. +- **Seed 108, `reorder`.** Shrinks to `Span{LEFT} > [Span{LEFT} > [Span{DOWN} + > [Branch{probe Rect, wide Rect, narrow Wrapped, 483}, Wrapped], Rect], + Rect]`. The trace shows the `DOWN` span evaluated at 900, 450, 600 and + 300 px across in one cold layout, the wrapped text re-shaped at each. In + the last, the narrow text is drawn in its real 300 px box and answers 286 + px, and `draw_inner` discards that for the retained 438.9 px answer from + the 450 px evaluation, because the branch's `below` place embeds + `extent_len(Y)`, which moved when the sibling text's height changed, so + `place != offer_place`. It then redraws the text 438.9 px wide inside a + 300 px box. Warm does the same with a different stale answer (286 px from + the first frame). Making `measured = answer.0` unconditionally and + recording every ask's answer fixes seed 108 and fails seeds 184, 246, 292, + 372 and two suite tests, among them + `unsettled::a_span_given_the_box_its_answer_decided_matches_a_cold_layout`; + the new failures are all under a `Scroll`. + +So the gate is load-bearing in one case and wrong in the other two. A +container re-drawn in a box its own answer derived (a scroll's content box +holding a row with a `leftover` child and a wrapping text) must place its +children at the answers the layout was computed from, or the text is offered +the room the leftover child gave back and the layout chases its own tail. A +container drawn in a box its parent decided (a 300 px slot after being +measured in 600) must re-measure its children there. A same-tree +re-evaluation whose place expression happens to differ must keep the fresh +answer. `place == offer_place` cannot tell these apart; nor could the four +rules tried before it; nor can the deferral, which is about local redraws +while these failures are inside full parent redraws. **The answers to +question 1 as posed do not resolve this**: (a) leaves it, (c) is the same bit +with more state. + +**Where the plan was wrong.** It claimed the retained model rests on the +frame's length being the same on every ask. Answers depend on the *part*, +not the frame, for every widget that reads its box, so transparent frames +fixed fraction resolution (a real win) and moved the second geometry from the +frame to the extent rather than removing it. Step 6's premise followed from +that claim and was false for the same reason. And the plan carried the +`offer_place` concept forward from the old protocol without noticing it is +undefinable here. The worker executed the plan as written, found exactly +this, and stopped; `Part::Of` was a sound addition apart from the dropped +pin. + +**Recommendation for Bryan to decide.** Make a container's body run only in +boxes its parent offered or decided, never in one derived from its own +answer: the placing step becomes reuse-or-translate and never `draw_at`, +and the offer machinery (`answer` gating, `offer_place`, `offer_part`, +`at_offer`, `measured()`) goes, leaving one answer per widget with the holds +that say which parts it is valid for, re-asked by `redraw` in the part of +the last ask. That needs every drawing to hold for its own answer box, which +today fails in three widgets and is fixable locally in each: + +- `Span` reads `extent_len` unconditionally. The slots depend on `far` only + with `leftover` children (when it fills, so the box is the part) or with + `Sign::Neg` (compute slots from `total` instead). Pin only in those cases. +- `Stack` draws non-sizing children in `All` of its measuring box, a box it + will never have when the sizing child reports `px`/`rel`. Draw them in + `From(0..size)` per axis where `size` is that child's answer, `All` where + it is `leftover`. +- `Branch` reads `extent_len(Y)` for "the rest of my box"; that is + `Of(40px..FULL)`. + +`Scroll` is the one legitimate own-answer box left. With the gate gone its +content box lays the row out afresh, the text takes the room the leftover +child gave back, and `content_len` from the viewport measurement is stale by +that difference; it is deterministic on both paths, so warm equals cold, but +the scroll range is off in the circular case. That case (a wrapping text +beside a `leftover` child in a horizontally scrolling row) needs its own +decision; Compose gives the text an unbounded width there. None of this was +run; it is what the traces and the two experiments point at. + +**Question 2, `Pad`.** The contradiction is not specific to `Pad`. A +`rel(1.0)` child directly in a half-row `Fill` slot overflows its slot +because `rel` is of the row, and Bryan accepted that. A `rel(1.0)` inside a +pad in that slot overflows the pad's box for the same reason and by the same +amount less the padding. "Fill my padded box" is `leftover` inside the pad, +which already works. So: keep `Pad` as the outset, drop the "outset pixels, +inset `rel` and `leftover`" variant as inexpressible and unnecessary, and +change `examples/text.rs` from `.width(rel(1.0))` to `leftover` or no rule. + ### The plan as written, for reference What follows is the plan as it was handed over on 2026-09-18, kept verbatim @@ -1443,7 +1560,9 @@ The replay used for the reference check: In order, from the review above and Bryan's steer (2026-09-17): -1. **Two questions for Bryan, both from "What is not done, and why".** What +1. **Two questions for Bryan, both from "What is not done, and why"**, now + sharpened by **What the two open seeds are**, which says why answering + them as posed does not settle the seeds and what would. What a widget's answer is the answer *to*, when a container is drawn in two of its own boxes in one frame -- which is what the two open fuzzer seeds turn on and what step 6 needs before the deferral can go. And what `Pad`