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

+147 -1
View File
@@ -34,7 +34,7 @@ the emulator, not by Mesa" and "the present mode was not the cause" are
worth as much as the successes, because they are what stops the next
session spending an afternoon on them again.
## Where things stand (2026-09-06)
## Where things stand (2026-09-06; see the 2026-09-08 evening box at the end)
Written on picking the branch up after a `/clear`, so the next session can
resume from here. P0 is delivered and Iris's phone report v2 is in
@@ -8188,3 +8188,149 @@ marks in `run-headless.sh phone --phone` (before and after a tap that
opens the card), and the collapse bar's up mark in `IRIS_TOOLS_EXPANDED=1
run-headless.sh transcript`. All three draw, at the size and alignment the
drawn triangle had.
## Iris's phone report, 2026-09-08 (evening: four defects, all fixed)
Her words, verbatim, from the iris bench on her phone:
> - if I scroll in a horizontal area and then tap in a vertical area, it
> seems to snap. Leaving and reopening the app also randomly moved the
> vertical scroll. This is ridiculous and it sounds like the code is
> pretty bad / connected.
> - Flinging doesn't work in horizontal scroll areas. Flinging should be
> enabled by default in all scroll areas on android to match composes
> behavior.
> - Collapsing and opening an edit card draws the card background a frame
> late, so it looks closed even when there's text, and then looks open
> even when the text is collapsed. This is also ridiculous and suggests
> the framework is being used in a bad way.
Plus, on reading the workaround list in `IRIS_TODO.md`: "All of those
should be fixed. There should never be workaround code. Do the same for
those; fix them if they're trivial, diagnose and report if not." And two
notes on the *shape* of a fix: "make sure all fixes you do work under all
circumstances and just make the code more correct rather than tape on
edge cases", with padding as the example -- "padding should work no
matter what container a widget is placed in, and acts as both inset and
outset depending on how tight the parent region is" (that one is written
up in docs/LAYOUT.md, since it is a rule about the layout rather than
about this pass).
Commits `fc82d9d` and `02b277e`. Every fix below has a layer-1 repro that
was **confirmed to fail with the change backed out**, which is what says
the test is about the defect and not about the code.
### The two "snaps" were two different faults with one shape
Both are a press that ended without the thing tracking it being told, so
the *next* press was measured from an origin belonging to a finger long
gone. `DragArbiter::update`'s `Undecided` crosses its slop instantly at
that distance and pans in one step.
**Leaving and reopening the app** was `MotionAction::Cancel` sharing an
arm with `MotionAction::Up` in `android/view.rs`. The system's own swipe
up from the bottom edge to go home is delivered to the app as moves and
then `ACTION_CANCEL`, so iris read it as a flick released at speed and
flung the transcript while the app was in the background. `CursorState::
cancelled` is the fix: a cancelled sample delivers `CursorSense::Cancel`
to the capture holder **and** every widget still tracking the press,
clears both, and derives nothing else from that sample. `TouchAction::
Cancel` in the harness sets the same flag, which is what makes it
testable from a `.touch` file (`touch/flick-cancelled.touch`).
Tests: `gesture_cancel.rs`'s `a_cancelled_flick_does_not_fling` (fails
with `Some(-15249.9)` when a cancel is a release) and
`a_press_ended_by_a_cancel_leaves_no_origin_for_the_next_one`.
**Panning a code fence and then tapping** was the `Cancel` this file's
earlier entry added, arriving nowhere. A cancel is delivered to the
widget that was **pressed**, not to whoever holds the capture -- and the
widget the press landed on is the fence's own text block, which drives
`Selection`'s shared `DragGesture` under the *list's* id. `row.rs`
registered `click_or_drag() | unclick()`, so it never heard it. It
registers `drag_senses()` now, which is what that set's own doc has
always said a widget driving a gesture must do.
That change alone would have broken panning, and this is the part worth
keeping: when the shared gesture *wins* a capture, the block is still a
"loser" by `run_sensors`' accounting, so it would be handed a `Cancel`
on the very frame its own pan committed and would release it.
`DragGesture::handle` now ignores a `Cancel` when `pointer.holder()` is
its own `id` -- "somebody else won" is what the sense means, so whether
the winner is us is the question to ask. Confirmed load-bearing:
`catch_a_fling.rs`'s `a_press_on_a_flinging_list_pins_the_content_to_the_
finger` fails without it.
Test: `gesture_cancel.rs`'s
`panning_a_code_fence_then_tapping_elsewhere_moves_nothing`, which
pushes a real fence into the real screen so the pan has something to
capture it. **A trap worth not re-finding**: the first version of that
test tapped at a fixed y and "failed" by 544px with every fix in place --
it had landed on a tool group's header and toggled it. The tap has to be
on ordinary text, so the test pushes a paragraph of its own to aim at.
### The card background was a frame late because a row is offered its cached height
`List::place` offers an already-measured row a box sized to the height it
cached, so an unchanged row hits `draw_inner`'s cheap skip-or-move path
(the alternative, `draw_twice` every frame, forces a real redraw every
frame -- `place`'s own doc has that history). A `Rect` fills whatever
region it is given, and `.background(rect(..))` is how a tool card is
styled, so on the frame a card changed height its text laid out at the
new height and its background painted at the old one. A row whose
measurement disagrees with its offer is now drawn again at its true box,
in that frame, on both placements -- the bottom-anchored half used a
`reposition`, which writes an offset and never a size, so it could not
fix it either. Test:
`a_row_that_changes_height_draws_its_background_at_the_new_height_immediately`,
which changes each of five rows in both directions so both placements are
covered without the test knowing which is which.
### The fling was `List`'s alone
`Scroll` dropped its released velocity on the floor, with a comment
explaining that the areas it wrapped were at most a screenful. That
stopped being true when a code fence became one. `iris::sense::Flinger`
is `List`'s fling as a type both use: the curve, the clock (started at
the first tick, not the release, so a caller on an explicit clock is not
handed a fling that already expired), the incremental delta, Compose's
two release thresholds and the trace line. It deliberately does not know
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` also gains the two things a coasting widget needs and had
no reason to have: the real display density (a hardcoded 1.0 is what made
a one-second coast run for 45 on a list) and `PressState::scrolling`, so
a finger on a coasting fence stops it from the first sample rather than
after `DRAG_SLOP`.
Tests: three in `scroll.rs`, plus `fence_fling.rs`, which flicks a real
fence in the real transcript screen and reads that fence's own `Scroll`
back out of what was drawn -- found by downcasting through
`UiRenderState::active`, since there is no handle to it from outside and
a bare coordinate would only prove that *something* moved.
### The workaround list
`transcript-ui/src/tool.rs` no longer flattens its two `Span`s into one,
so a tool group holds its cards 4dp off its own edge again. "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 with the
headless render that found it (`IRIS_TOOLS_EXPANDED=1
iris/run-headless.sh transcript --shot -- -p transcript-ui`, cards
correct with the spans nested) and pinned at layer 1 by
`a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is`.
Worth knowing for the next one of these: that layer-1 test does **not**
reproduce the fault outside a `List` — the plain nested-span shape passed
immediately, and it only became a faithful reproduction of the real tree
once the row was placed inside a masked `List`, which is where `place`'s
oversized measurement pass lives.
The other two entries there are missing capabilities rather than defects
being dodged, and each now carries its diagnosis in `IRIS_TODO.md`
instead of a workaround: **overflow ellipsis** (parley has none, and the
easy implementation breaks every byte-offset consumer of `TextBuffer` --
spans, `byte_at`, `Selection`, `apply_delta` -- so it needs a displayed
string distinct from the source, plus an `Overflow` enum in place of
`TextAttrs::wrap`), and **selectable tool-card text** (no key collision
to design around, since a tools row has no markdown blocks at all; the
cost is the register/unregister lifecycle across the three routes that
rebuild a card, which is exactly where a stale `Selection` handle
panics).