iris: the composer scrolls on a finger -- a dp cap worth zero, a stale mask slot, a hit box moved twice
Wrapping the composer's field in .scrollable().masked() needed three layout defects fixed first, each with a headless regression test that was confirmed to fail without its fix: - MaxSize/Sized reported a caller's declared dp length unresolved, and Span places a child from the abs/rel of what it reported, so dp(168) was worth zero: the bar got a slot of nothing the moment its content passed six lines and the Scroll inside measured its container at -63px (container=-63 content=415.8 amt=478.8 on the emulator). Len::fold_dp, used on the way out, plus a debug_assert in draw_inner that a reported Size carries no dp -- the rule is about every widget, not those two. - Masked allocated a fresh mask slot per draw, and draw_inner's unchanged-region fast path does not revisit descendants, so they kept clipping against a box the bar had moved away from: four live mask entries, none of them current, and the field drew nothing. ActiveData::own_mask, allocated once and rewritten in place. - mov updates active.region and accumulates the same delta on the move slot, and resolved_region added both, so a panned widget's own hit box sat at twice the pan -- the composer's field was untappable after a drag. ActiveData::move_applied. Scroll itself measured the right number by a misleading route; it is written against painter.px_size() now and still reports its content's size, since reporting the container makes the answer a function of itself. Verified on this checkout's emulator: swipe 540 1200 -> 540 1460 moved the field's Message box 31,1041..1048,1509 -> 31,1131..1048,1651 with its height unchanged at 468px. run-bench.sh polled logcat for a prefix copy_report also logs at startup, so it printed a report that had never been run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d73db97629
commit
167862ca1b
14 files changed
+567
-39
No files matched your search
@@ -43,6 +43,83 @@ gated on her verdict**, so this pass works the P0 defects and the pure
|
||||
prerequisites in this order. Each item is ticked here by the agent that
|
||||
closes it.
|
||||
|
||||
### Task A, closed 2026-09-06: the composer scrolls on a finger
|
||||
|
||||
`iris/transcript-ui/src/composer.rs` is `field.scrollable().masked()` now.
|
||||
Verified on this checkout's emulator -- the evidence and the numbers are
|
||||
in docs/IRIS_TODO.md's ticked "composer has no touch-drag scroll" item.
|
||||
|
||||
**The premise the task was given under was wrong, and that is worth
|
||||
recording**: `Scroll` did *not* measure against the window. Its
|
||||
`used.within_len(container).to_abs(output_size)` came to exactly
|
||||
`abs + rel * container_px` -- the right number by a route that reads as if
|
||||
the window were the container, which is what cost a session. It is
|
||||
`painter.px_size()` and `to_abs(container_len)` now: same arithmetic,
|
||||
stated the way the invariant is. `Scroll` also still reports its
|
||||
**content's** size upward, deliberately -- reporting the container makes
|
||||
the answer a function of itself (the bar is sized *from* that report, so
|
||||
it collapses to nothing and never recovers; measured in the headless
|
||||
harness before the shape was settled).
|
||||
|
||||
What actually broke the composer was three separate defects, each now
|
||||
carrying a headless regression test in `iris/src/layout_tests.rs` that was
|
||||
confirmed to fail without its fix:
|
||||
|
||||
1. **A `MaxSize` reported its cap as an unresolved `dp`.**
|
||||
`Span::draw` places a child from the `abs`/`rel` of the length it
|
||||
reported, so `dp(168)` was worth **zero** and the bar got a slot of
|
||||
nothing the instant its content passed six lines; the `Scroll` inside
|
||||
then measured its container at **-63px** (the padding subtracted from
|
||||
nothing) and panned the whole message out of view. Emulator log, before
|
||||
the fix: `container=-63 content=415.8 amt=478.8`. Fixed by
|
||||
`Len::fold_dp` (new), used by `MaxSize` and `Sized` on the way out, and
|
||||
guarded for every widget by a `debug_assert!` in
|
||||
`UiRenderState::draw_inner` that a reported `Size` carries no `dp`.
|
||||
Test: `a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it`.
|
||||
2. **A `Masked` allocated a fresh mask slot on every draw.**
|
||||
`draw_inner`'s unchanged-region fast path means its descendants are
|
||||
mostly *not* redrawn with it, so they kept clipping against the slot
|
||||
they were first drawn under -- measured on the composer's tree at
|
||||
**four live mask entries, none of them the widget's current box**, and
|
||||
the field drew nothing at all. The slot is allocated once and rewritten
|
||||
in place now (`ActiveData::own_mask`, `Painter::set_mask`), with its
|
||||
path out in `remove`'s `undraw` branch. Test:
|
||||
`a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region`.
|
||||
3. **A panned widget's own hit box moved twice.** `mov` updates
|
||||
`active.region` *and* accumulates the same delta on the widget's move
|
||||
slot, and `resolved_region` added both -- so after a finger pan the
|
||||
composer's field was untappable, while its descendants were fine (which
|
||||
is why `hit_testing_follows_a_scrolled_widget`, which checks a
|
||||
descendant, never saw it). `ActiveData::move_applied` records the part
|
||||
of the slot's delta `region` already accounts for. Test:
|
||||
`a_panned_widgets_own_hit_box_moves_exactly_once` (fails at exactly
|
||||
2x the pan without it).
|
||||
|
||||
Still open, and **pre-existing** (present in the build before this change,
|
||||
so not the scroll area's doing): the composer bar's grey background is not
|
||||
drawn on the `transcript-screen bench` build, so the message reads as
|
||||
white text over the transcript. `Stack{StackSize::Child(1)}` is the thing
|
||||
to look at.
|
||||
|
||||
Rig fix on the way past: `iris/android-app/run-bench.sh` polled logcat for
|
||||
`"iris bench report:"`, which `copy_report` also logs at startup
|
||||
("nothing to copy -- run the benchmark first"), so it returned instantly
|
||||
and printed a report that had never been run. It polls for the report's
|
||||
own first line now.
|
||||
|
||||
### Bench, before Task B (emulator, 2026-09-06)
|
||||
|
||||
`iris/android-app/build-apk.sh debug --abi x86_64 --features
|
||||
"transcript-screen bench force-gles"` + `run-bench.sh`, this checkout's
|
||||
AVD. Emulator absolutes transfer nothing; the before/after ratio on the
|
||||
same emulator does.
|
||||
|
||||
stream: 202 frames over 21.0s
|
||||
late: 197 (97.5%)
|
||||
total p50 61.5ms p90 211.7ms p99 342.6ms
|
||||
worst 403.6ms
|
||||
|
||||
|
||||
- [x] **Merge the `DragGesture` work** -- done 2026-09-06 (merge commit
|
||||
`f802de9`, `git merge --no-ff worktree-agent-a754368325fa06839`,
|
||||
clean, no conflicts across the 8 files `e12c708` touched). Targets
|
||||
|
||||
Reference in new issue
Block a user