docs: Iris's second 2026-09-08 phone report, and the workaround list closed

RUST.md gets the report verbatim with what each of the four defects
actually was, the tests that pin them, and two traps worth not
re-finding (a fixed-coordinate tap that "failed" by 544px because it had
toggled a tool group, and a layer-1 repro that only reproduces inside a
`List`). IRIS.md and DECISIONS.md get the design half: one `Flinger`
whose seam puts the sign convention and the content's end with the
caller, a cancel as a first-class end to a gesture, and why a row is
drawn twice on the frame its height changes.

LAYOUT.md gains the two rules those turned on, since both govern the
layout rather than this pass: padding works in any container and is an
inset or an outset depending on how tight the parent's region is (Iris's
own words), and a widget offered a box it does not fit is drawn again at
its true box in the same frame rather than the next one.

IRIS_TODO.md's "worked around in tool.rs rather than fixed here" is gone
-- Iris, 2026-09-08: "There should never be workaround code." Two of the
four entries are ticked; the two that remain are missing capabilities
rather than defects being dodged, and each now carries a diagnosis of
what building it costs instead of a workaround: an overflow ellipsis
needs `TextBuffer` to have a displayed string distinct from its source
(parley has none of its own, and every byte-offset consumer -- spans,
`byte_at`, `Selection`, `apply_delta` -- moves if the buffer is
truncated), and selectable tool-card text needs a register/unregister
lifecycle across the three routes that rebuild a card, which is where a
stale `Selection` handle panics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 15:26:12 -04:00
1 parent 02b277e7ad
commit fe7dc9c728
5 files changed
+353 -16

No files matched your search

+63
View File
@@ -12,6 +12,69 @@ things still stay out.
An entry gives the date, what changed, why, and a short before/after where
it helps judge the change without the session that made it. Newest first.
## 2026-09-08: what a cancel means, what a row's box is, and one fling for every scroll area
Iris's second 2026-09-08 report, from the bench on her phone. Four items,
and each turned out to be a rule stated in one place and missing from its
siblings rather than a special case.
**A gesture the *platform* takes away is a cancel, not a release**
(`CursorState::cancelled`). Android's `ACTION_CANCEL` used to take the
same arm as `ACTION_UP`, so the system's own swipe up from the bottom
edge to leave the app arrived as a flick released at speed: the
transcript flung while the app was in the background, and came back
somewhere else. A cancelled sample now hands `CursorSense::Cancel` to
the capture holder *and* every widget still tracking the press, clears
both, and derives nothing else from that sample -- no tap, no selection,
no fling. That is the same sense a widget already gets when it loses a
capture race; what is new is that the platform can raise it, and that
the *winner* hears it too when the platform is the one cancelling.
**A `DragGesture` ignores a cancel when it is the one holding the
capture.** A cancel goes to every pressed widget that did not capture,
and one gesture is routinely driven by several of those: a transcript
row's text block feeds the shared gesture that captures under the
*list's* id, so the block is a "loser" on the very frame its own pan
committed. `Cancel` means "somebody else won", so the question is
whether the holder is us -- and now it is asked. With that, a row's
block registers the whole `drag_senses()` set, which is what the doc on
that set has always said a widget driving a gesture must do; it was the
one place that did not, and it is why panning a code fence sideways and
then tapping made the transcript jump.
**A row is drawn at the box its own height implies, in the frame that
height changes** (`List::place`). A row is offered its *cached* height
so that an unchanged row takes `draw_inner`'s cheap path; a
`.background(rect(..))` fills whatever box it is handed. So on the frame
a row changed height its text laid out at the new height and its
background painted at the old one -- collapsing or opening a tool card
looked closed while its text was there, then open while it was not. When
the measurement disagrees with the offer, the row is now drawn again at
its true box. The bottom-anchored half had a `reposition` for this,
which writes an offset and never a size, so it could not fix it either:
the same rule, applied to one member of a set of two.
**Every scroll area flings, on either axis** (`iris::sense::Flinger`).
The fling was `List`'s alone -- the curve, the clock, the incremental
delta, Compose's two release thresholds -- and a `Scroll` dropped its
released velocity on the floor, with a comment explaining that the areas
it wrapped were only a screenful. That stopped being true the moment a
code fence became one. `Flinger` is that machinery as a type both use;
what it deliberately does not know is which way a positive delta moves
the content or where the content ends, because a `List` and a `Scroll`
answer those oppositely. The caller applies `tick`'s delta in its own
convention and calls `stop` at its own wall. `Scroll::drag` now answers
whether it started a fling, which is what `scroll_area` needs to call
`UiData::animate` -- the same split `List::fling` already documented,
for the same reason: only the caller can reach the frame loop.
**Removed, not worked around**: `tool.rs` no longer flattens its two
`Span`s into one, so a tool group holds its cards 4dp off its own edge
again. The defect that shape was avoiding -- "a `Span` of `Pad`ded
children inside another `Span` places those children a slot out of step"
-- is not reproducible on 2026-09-08, checked both with a headless
render and with a new layer-1 test.
## 2026-09-08: a gesture can be cancelled, and the pointer belongs to the input handler
Two changes to how a drag ends, from defects on Iris's phone (a code