iris: a press only reaches the widget the pointer is on
Iris's 2026-09-08 report, both halves, and her own diagnosis of the second: "tapping outside of something that a fling is currently active for should have no code in common with the fling that could influence it." `run_sensors` runs a widget one frame after the pointer leaves it (`ActivationState::End`, which is not `Off`) so `HoverEnd` can fire, and `should_run` derived the press and wheel senses from raw button state without consulting `hover`. That farewell frame carried a `PressStart` to a widget the finger was nowhere near -- and a press on already-coasting content is a catch, which commits to a pan with no `DRAG_SLOP`, so the widget captured the pointer and swallowed the whole gesture. Its hover was stale because a gesture that ends while captured returns from the capture branch, which never reaches the loop that updates it. Measured before the fix on the real screen: a fence flicked sideways, then a finger down on a row 500px above it dragged 160px down the screen -- the list moved by zero, the fence moved by zero, and the fence held the pointer throughout. After: the list follows the finger and the fence's fling carries on coasting, which is what she asked for and falls out of the fix rather than being arranged. `should_run` now requires `hover.is_on()` for every non-hover sense. `Drop`/`Cancel` are unaffected -- they are delivered deliberately to a widget that is not under the pointer, with an explicit `On`. Also: the composer is clipped to its own bar rather than inside its padding (`.masked_by(rect(BAR_FILL))` in place of a `.masked()` + `.background()` pair) -- "the box should be clipped rather than the inset text". A long message was being sliced mid-glyph 12dp in from the bar's edge, leaving a band of bare surface above the cut. New: `Scroll::is_scrolling`, the name `List` already uses; the phone rig's `--message TEXT` and `--ime PX`, since the composer's overflowing and keyboard-open states cannot otherwise be looked at headlessly. Tests fail on the old code, one per layer: `a_press_does_not_reach_a_widget_the_pointer_has_just_left` (sensors, no screen) and `a_drag_away_from_a_coasting_fence_scrolls_the_list_and_ leaves_it_coasting` (the report itself, layer 1). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cc8148cbec
commit
5e34dba2fd
7 files changed
+438
-22
No files matched your search
@@ -1529,3 +1529,82 @@ This does not reopen the 2026-09-07 platform-fonts decision. Body and
|
||||
monospace text still come from the platform's own collection; an icon is
|
||||
the opposite case, a small closed set of codepoints no system font is
|
||||
guaranteed to have, and it is the same division the Compose app makes.
|
||||
|
||||
## 2026-09-08: a press only reaches what the pointer is actually on
|
||||
|
||||
Iris's report -- "if I try to scroll vertically while a horizontal scroll
|
||||
animation is still active, it stays locked to the horizontal scroll. It
|
||||
should let it keep going and instead only affect vertical scrolling" --
|
||||
and her own diagnosis of it, which was the right one: "it seems like iris
|
||||
is set up so the animation stuff is global which it definitely should not
|
||||
be. Tapping outside of something that a fling is currently active for
|
||||
should have no code in common with the fling that could influence it."
|
||||
|
||||
It was global, and it was in `sense::should_run`. `run_sensors` runs a
|
||||
widget one frame *after* the pointer leaves it (`ActivationState::End`,
|
||||
which is not `Off`) so a `HoverEnd` can fire, and `should_run` derived
|
||||
`PressStart`/`Pressing`/`PressEnd`/`Scroll` from the raw button and wheel
|
||||
state without consulting `hover` at all. So that farewell frame carried a
|
||||
press to a widget the finger was nowhere near.
|
||||
|
||||
That alone would have been a stray event; what made it eat the gesture is
|
||||
the catch added on 2026-09-07 (`PressState::scrolling`), which commits a
|
||||
press on already-moving content to a pan immediately, with no `DRAG_SLOP`
|
||||
-- so the widget captured the pointer on that frame and every later sample
|
||||
went to it. And the widget's hover was stale in the first place because a
|
||||
gesture that ends while captured returns from `run_sensors`' capture
|
||||
branch, which never reaches the loop that would have updated it.
|
||||
|
||||
Measured on the real screen before the fix: a fence flicked sideways, then
|
||||
a finger put down on a row **500px above it** and dragged 160px down the
|
||||
screen. The list moved by zero, the fence moved by zero, and the fence
|
||||
held the pointer for the whole gesture -- the report, exactly.
|
||||
|
||||
- **`should_run` now requires `hover.is_on()` for every non-hover sense.**
|
||||
Press and wheel both, since a wheel event reaching a widget the cursor
|
||||
has just left is the same fault with a different sense. `Drop` and
|
||||
`Cancel` are unaffected: they are delivered deliberately to a widget
|
||||
that is *not* under the pointer, and `run_sensors` hands both an
|
||||
explicit `On`.
|
||||
- **`Scroll::is_scrolling`** (new): whether a fling is coasting in this
|
||||
area, the same question and the same name `List::is_scrolling` already
|
||||
answers for the other scrolling widget.
|
||||
|
||||
Nothing about the fling, the arbiter or the catch changed. A press outside
|
||||
a coasting area now has no code in common with it, so the horizontal fling
|
||||
keeps coasting through a vertical drag on its own -- which is the second
|
||||
half of what Iris asked for, and it falls out of the fix rather than being
|
||||
arranged. A press *inside* a coasting area is still a catch on either
|
||||
axis, which is what Compose does ("Compose does catch no matter what axis
|
||||
if you tap in the horizontal area").
|
||||
|
||||
Two tests, one per layer:
|
||||
`sense_tests::a_press_does_not_reach_a_widget_the_pointer_has_just_left`
|
||||
is the mechanism with two stacked scroll areas and no screen, and
|
||||
`fence_fling.rs`'s
|
||||
`a_drag_away_from_a_coasting_fence_scrolls_the_list_and_leaves_it_coasting`
|
||||
is the report itself over the real transcript. Both fail on the old code.
|
||||
|
||||
## 2026-09-08: the composer is clipped to its bar, not inside its padding
|
||||
|
||||
Iris: "the message input box doesn't clip correctly ... the box should be
|
||||
clipped rather than the inset text."
|
||||
|
||||
The composer was `.masked().background(rect(...))` -- two boxes, one
|
||||
inside the other. The mask sat *inside* the `dp(FIELD_PAD_DP)` padding, so
|
||||
a message longer than the six lines shown was cut through the middle of a
|
||||
glyph 12dp in from the bar's edge, with a band of bare surface above the
|
||||
cut. Measured at the phone's own size and density (1080x2424 at 2.55): the
|
||||
bar's top edge at y=1995.6 and the text sliced at y=2026.2.
|
||||
|
||||
It is `.masked_by(rect(BAR_FILL))` now: the same rect is the surface drawn
|
||||
behind the field *and* the shape the field is clipped to, so the two
|
||||
cannot fall out of step -- the idiom `row.rs` already uses to cut a code
|
||||
fence to its own rounded panel. Text now disappears under the bar's edge
|
||||
at 1995.6. The padding still holds text off the edge at the end the
|
||||
content is anchored to, which is the end anybody is reading.
|
||||
|
||||
The composer's overflowing and keyboard-open states had no way to be
|
||||
looked at headlessly, since that window has no keyboard: the phone rig
|
||||
takes `--message TEXT` and `--ime PX` for them
|
||||
(`transcript-fixture/examples/phone.rs`, through `RUN_HEADLESS_ARGS`).
|
||||
Reference in new issue
Block a user