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:
1 parent
02b277e7ad
commit
fe7dc9c728
5 files changed
+353
-16
No files matched your search
+147
-1
@@ -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).
|
||||
Reference in new issue
Block a user