Skip to content

feat(core): pan gesture recognizer with UIKit's hysteresis and velocity - #8060

Merged
cixzhang merged 53 commits into
meta:mainfrom
vjeux:wip/uikit-pr-g1c
Oct 26, 2026
Merged

cixzhang merged 53 commits into
meta:mainfrom
vjeux:wip/uikit-pr-g1c

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8059 (#8053 and #8058 are merged); review the commits after #8059 that are not merges.

Primary intent

Add the pan recognizer, reading translation and velocity as UIKit's UIPanGestureRecognizer does, with the velocity estimator a release hands to a spring.

User or maintainer impact

Drags differ most from iOS: release speeds, slops, jumps. The pan gives every component UIKit's numbers: it begins past 10 pt on either axis, its translation leaves that threshold out, and velocity() is UIKit's estimate.

Problem and solution fit

Each component measured its own drag: a flick that dismisses one surface snaps another back, and springs seeded from another velocity cannot match UIKit's.

Deltas:

  • PanGestureRecognizer:
    • minimum and maximum touches;
    • axis and one-way directions; axisRatio sets how far off its axis a drag still counts, decided at the 10 pt begin;
    • axis lock;
    • re-basing across finger changes;
    • scrollEdge (begin only where the content under the touch cannot scroll that way): who gets a drag is decided once, when it begins (spec:AST-094 FR14);
    • claims: a pan claims its touch only when it begins, at FR17's 10 pt; nothing claims earlier;
    • translation(), totalTranslation(), velocity(), setters.
  • VelocityTracker: UIKit's blend, scrollVelocity() for scrolling surfaces, zero after a 40 ms rest.
  • The pan imports only the table's pan group and the tracker only its velocity group.
  • Arena conflicts: native scrolling, long press, nested pans, two fingers.

Authority

spec:AST-100 FR1, FR2 and architecture:interaction-modality INV10 for velocity; spec:AST-094 FR10, FR14, FR17, FR19 for claims. Thresholds come from motion/uikit/gestures.ts, each measured source dated (FR2).

Change and evidence

Read from UIKit's recognizers (iOS 26.2 simulator, HID touches, state log):

  • the hysteresis is 10 pt per axis (a diagonal began at 10.67, 10.67);
  • translation(in:) reads 0 when a slow drag begins; an overshooting step keeps the overshoot;
  • velocity(in:) is 0.2 × the newest event's instantaneous velocity + 0.8 × the previous one's, reproducing all 69 values UIKit reported on six recorded streams. UIKit's never decays; ours reads 0 after a 40 ms rest (FR2). The tests replay two streams against UIKit's numbers.
  • A scrolling surface's release: four UIPickerView swipes replay within 0.1% of the velocity UIKit handed scrollViewWillEndDragging (INV10).

Not modelled: some fast first moves read 0 at .began in UIKit.

Scope

  • This PR has one primary intent.
  • Independently removable deltas were removed or split (the other recognizers follow).
  • Public text and artifacts contain no internal Meta context.

Testing

At 7f712c4:

  • pnpm exec vitest run --project ui packages/core/src/gestures packages/core/src/motion: 245 tests passed. Failing first: the four picker cases with the weight at 0.2 instead of 0.25. A case pins FR14: a drag begun as a scroll never reaches the sheet; another pins the 10 pt decision with an axisRatio above 1.
  • tsc --project packages/core/tsconfig.json --noEmit: clean.
  • ASTRYX_STRICT_LINT=1 eslint on the changed files: 0 errors. The CI duration guard passes (23/23).
  • pnpm check:changesets: clean.
  • Size against the feat(core): tap and long-press recognizers with UIKit's measured thresholds #8059 it carries: 9 files, 1,447 lines added, 20 removed (the recognizer, the tracker, their tests, the arena cases, the table's pan and velocity groups and their measurement notes).

Stack (merge in this order): #8059 tap and long press → #8060 pan → #8061 other recognizers → #8055 animator → #8056 animator curves; the gesture DOM host, press tracking and browser proof follow the animator.

vjeux added 4 commits October 23, 2026 20:53
Springs, scroll physics, timing curves, navigation values and gesture
thresholds for the touch presentations of core components, each with its
source (Apple documentation, a WWDC session, WebKit's UIKit-matched source,
or a measurement of UIKit on the iOS 26.2 simulator) or labelled a guess
with its measurement task. A test enforces the source rules on every domain
file, in their JSDoc and typed forms, and checks the list of simulator
measurements still owed.
…rena

An internal gesture system for touch presentations, not exported from the
package. GestureRecognizer carries UIKit's states and allowed transitions,
discrete and continuous recognizers, require-to-fail, simultaneous
recognition on request, shouldBegin and shouldReceiveTouch,
cancelsTouchesInView and the touch delays. GestureArena settles each touch
sequence without the DOM: participants, resolution order, claims, delivery
to the view, click dispositions (allow, suppress, defer and replay) and
timers as deadlines on the event clock. Its rules are tested with scripted
recognizers, independent of any real one.
…sholds

Tap recognizes at the lift within 45 pt of movement, taps 0.35 s and 45 pt
apart, and a 1.5 s hold; long press begins at exactly touch-down + 0.5 s on
the event clock within 10 pt, even when the main thread was blocked, and a
zero duration begins on the touch itself. The arena tests with these real
recognizers cover tap against double tap, delegate requirements, deferred
clicks and cancellation.
@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 26, 2026 4:52am UTC

Request Review

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 26, 2026, 5:09 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: the pan and the velocity tracker now follow the current spec:AST-094, spec:AST-100 and architecture:interaction-modality INV10, including their per-group table imports, but two items remain before this can land.

  • The compare view is 5,197 lines because it carries older copies of #8058 and #8059; merging main, which now has #8058, and landing #8059 first leaves this pull request at its 1,467 lines of pan and velocity work.
  • The description still lists claimDistance, its tests and an earlier size, none of which this head has.

The FR17 and FR19 exception question in this thread is for an engineering owner to answer for a later push; this head follows the current text.

[Automated review]

# Conflicts:
#	packages/core/src/gestures/GestureArena.ts
#	packages/core/src/gestures/GestureRecognizer.ts
#	packages/core/src/gestures/types.ts
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
No conflicts. Brings main with meta#8058 landed and meta#8054 merged, plus meta#8060's
per-group pan and velocity imports and its FR19 decide-by-10-px fix.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
No conflicts. Brings main with meta#8058 landed, through meta#8060 and meta#8061.
# Conflicts:
#	packages/core/src/gestures/LongPressGestureRecognizer.ts
#	packages/core/src/gestures/TapGestureRecognizer.ts
#	packages/core/src/motion/MEASUREMENTS.md
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…R17, FR19)

cixzhang's specs ruling on meta#8060: a pan starts at 10 pt and nothing allows
an earlier committed claim. The opt-in early claim this PR kept goes with
it: the claimDistance option and its 'early' table distance
(pan.earlyClaimDistance), the host's cancelled moves before a pan begins
and the committed claim that kept a reversal or a second finger from
failing it. The README's early-claim section becomes that rule, the
migration guide no longer points a banner at it, and the changeset drops
it (and a stale 'no component uses it yet').

Tests: a one-way pan is possible with its touch unclaimed at 9 pt and
begins and claims at 10 pt (from her meta#8060); the early-claim tests go,
and the browser fixture's overlay card claims when it begins, keeping its
direction from the page in Chromium as before.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
No conflicts. Brings main with meta#8059 landed, through meta#8060 and meta#8061.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended for approval: the pan recognizer and velocity tracker follow the current spec:AST-094 (FR10, FR14, FR17–FR19), spec:AST-100 FR2 and architecture:interaction-modality INV10, the compare view now holds only this PR's 1,467 lines, and the description matches the code. For the components that adopt it, a one-finger drag needs maximumNumberOfTouches: 1, because at the unbounded default a second finger moves a claimed drag and cancels it if the browser takes that finger; the FR17/FR19 exception question for Mobile Safari stays with the engineering owners. Optional cleanup: the unused scrollProbe import in PanGestureRecognizer.test.ts and the stale "handoff" case listed in the arenaContinuous.test.ts header.

[Automated review]

super(options);
this.minimumNumberOfTouches = options.minimumNumberOfTouches ?? 1;
this.maximumNumberOfTouches =
options.maximumNumberOfTouches ?? Number.POSITIVE_INFINITY;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR10 note for adopters: at this unbounded default (UIKit's), a second finger that lands on a begun pan joins its centroid, so moving it 40 px moves translation() 20 px, and if the browser takes that unclaimed finger the arena cancels the pan. A one-finger presentation needs maximumNumberOfTouches: 1, as the FR10 tests set; a default of 1 would keep spec:AST-094 FR10 ("A second finger MUST NOT cancel or redirect a drag a recognizer has claimed") without each adopter remembering it.

import {describe, expect, it} from 'vitest';
import {UIKIT_GESTURES} from '../../motion/uikit/gestures';
import {PanGestureRecognizer} from '../PanGestureRecognizer';
import {createHarness, scrollProbe} from './harness';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scrollProbe is imported but unused in this file (an ESLint @typescript-eslint/no-unused-vars warning).

* @input GestureArena with pans, long presses, taps, consumers and scroll
* probes, driven by synthetic touches with explicit timestamps
* @output The pan's conflicts: native scrolling (failing, scroll edge,
* handoff), long press vs pan, nested pans, delegate hooks, two fingers on

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No handoff case remains in this file since scrollHandoff was removed, so the header can drop it.

@cixzhang
cixzhang merged commit cd4aca6 into meta:main Oct 26, 2026
61 checks passed
@vjeux
vjeux deleted the wip/uikit-pr-g1c branch October 26, 2026 05:12
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
meta#8060's final head (99b47ce) is already in this branch through meta#8072, and
main's VelocityTracker.ts, PanGestureRecognizer.test.ts and types.ts each
equal that head's copy, so their add/adds resolve to this branch's side
(that head plus the chain's later changes).

# Conflicts:
#	packages/core/src/gestures/VelocityTracker.ts
#	packages/core/src/gestures/__tests__/PanGestureRecognizer.test.ts
#	packages/core/src/gestures/types.ts
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
meta#8060's final head is already in this branch, and main's copies of its
files equal that head's, so the pan changeset, VelocityTracker, the pan
recognizer test and the gesture types keep this branch's copies.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
The touch foundation's fold, mergeable against main cd4aca6: her chain
on main (meta#8058, meta#8054, meta#8059, meta#8060) and meta#8258's rework, which keeps touch
motion internal (no theme surface): the generated role table, its script and
the role kinds file are gone, and a table registers only the roles it lists.
The motion doc and its golden take meta#8076's side; the role kinds file stays
deleted.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
meta#8076's head is F1's fold on the maintainer's chain now on main (meta#8058,
meta#8054, meta#8059, meta#8060): touch motion is internal (the tables register their
values as internal motion roles, with no theme surface), and the
`isRtlElement` resolver.

Conflicts, resolved:
- The theme-roles changeset and the motion guide take meta#8076's side; their
  sentence on ChatLayout's Reduce Motion cross-fade goes with the theme
  surface.
- The CLI goldens for the empty icon set take meta#8076's side and regenerate.
- keyboardModel.test.ts reads the internal role registry (`motionRoleIn`)
  now that the generated role-kind table is gone, and no longer names a
  theme.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
meta#8060 has landed (squashed). Three add/add conflicts with this branch's
copy of it resolve to this branch's version, which is main's plus its own
deltas: VelocityTracker's isMotion, the pan test without an unused import,
and NativeScrollProbe without the handoff-only distanceToEdge. This
branch's diff is now its own change over main.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
No conflicts. Brings main with meta#8060 landed.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…eta#8258 194aab4) into meta#8270

The long press reads its own and the tap's UIKit groups, as main's meta#8059
does, and keeps this PR's lock rule (a hold brings lockSelection; 0 ms and
null bring none); the host keeps her coarse-pointer option without the
held-selection map this PR dropped.

This branch was successfully deployed

1 active deployment
Preview — 99b47ce5 Deployed Oct 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants