Repository navigation
Keep a touch that stops a fling from inheriting the fling's axis - #14
Merged
Merged
Conversation
GPUI's touch recognizer (gpui-pre 0.3.5) treats a contact that lands during scroll momentum as a pan from its first pixel — and locks it to the axis of the fling it stopped. A quick sideways swipe (across a wide table, or across text: momentum starts on any fast release) followed by a swipe up produced only horizontal deltas, so the page looked frozen until the finger lifted. The fix belongs in the recognizer (zed#64239); this is what the platform can do until that reaches a release. The recognizer takes its momentum on every `Started`, and a contact that is cancelled while still pending emits nothing. So before a contact that may land on momentum, `FlingGuard` relays a synthetic contact that begins and is cancelled at the same point: it stops the momentum without moving anything, and the real contact then begins on an idle recognizer and picks its own axis. Momentum is not visible from the platform, so the guard runs for any contact that begins within 4 s of a release that had panned; when no momentum is left the synthetic pair is a no-op. Both platforms relay through it: `handle_touch` on iOS and the `on_touch` bridge on Android. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A contact that catches a fling is never offered as a touch drag either: the recognizer enters `Panning` straight from `Started`, and only a pending contact is offered. A scrollbar thumb is visible only while its content scrolls and coasts, so in practice it could not be grabbed until an earlier touch had stopped the coasting — which read as needing a long press. The guard already fixes this by starting the real contact on an idle recognizer; say so in the module docs, and pin it with a test that drives GPUI's recognizer through the guard against a drag-claiming element: unguarded, the contact on the thumb is swallowed by the fling; guarded, it is claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
GPUI's touch recognizer (gpui-pre 0.3.5) treats a contact that lands during scroll momentum as a pan from its first pixel — and locks it to the axis of the fling it stopped. A quick sideways swipe (across a wide table, or just across text: momentum starts on any fast release) followed by a swipe up produced only horizontal deltas, so the page looked frozen until the finger lifted. Reproduced on an iPhone 17 Pro in the AI chat.
The same catch also swallows control drags: a contact that catches a fling is never offered as a touch drag (the recognizer enters
Panningstraight fromStarted; only a pending contact is offered). A scrollbar thumb is visible only while its content scrolls and coasts, so in practice it could not be grabbed until an earlier touch had stopped the coasting — which read as needing a long press on the thumb.The fix belongs in the recognizer (zed-industries/zed#64239 for the axis; offering a drag to a catching contact is a second upstream gap); this is what the platform can do until that reaches a gpui-pre release.
Change
src/fling_guard.rs, relayed through on both platforms (handle_touchon iOS, theon_touchbridge on Android):Started, and a contact that is cancelled while still pending emits nothing. So before a real contact that may land on momentum, the guard relays a synthetic contact that begins and is cancelled at the same point: it stops the momentum (a zero-deltaStarted/Cancelledscroll pair the consumers treat as a no-op), and the real contact then begins on an idle recognizer, which offers it as a drag and otherwise lets it pick its own axis at the slop.What the guard cannot restore is the catching contact's no-tap rule: a contact that stops a fling and lifts without moving is an ordinary tap here.
Test
5 host-side unit tests on
FlingGuard(armed after a pan release, once per release, not after the window, not after a tap, not after a cancelled pan), plus one end-to-end test that drives GPUI's real recognizer (gpui-pretest-support, dev-dependency only) through the guard against an element that claims touch drags in a strip along the right edge, the way a scrollbar thumb does: after a swipe with velocity, an unguarded contact on the thumb is swallowed by the fling, a guarded one is claimed as a drag.cargo check --target aarch64-apple-iosandcargo ndk -t arm64-v8a checkboth build.🤖 Generated with Claude Code