SIGN IN SIGN UP

[macOS] Fix touch events never being delivered (#4390)

## Description

`onTouchesMove` and `onTouchesUp` callbacks never fired on macOS — a
gesture produced a single `onTouchesDown` and then a cancel sweep when
the recognizer reset, never a move or up event. Found while testing the
`manualActivation` fix (#4389), which this bug masks entirely in its
real-world form (activating from `onTouchesMove`).

`RNGestureHandlerPointerTracker` tracks pointers by storing the touch
object at registration and matching subsequent events by object identity
(`_trackedPointers[index] == touch`). That's correct on iOS, where a
`UITouch` is one stable object for the whole lifetime of a touch. On
macOS the recognizers forward `NSEvent`s, and every event in a mouse
sequence is a fresh instance — so `findTouchIndex:` / `unregisterTouch:`
never matched, move/up events were dropped with `changedCount == 0`, and
the registered pointer leaked until the tracker's `reset` swept it out
via `cancelPointers` (which is why JS saw down → cancel instead of down
→ moves → up).

Since macOS has exactly one mouse pointer, the tracker now matches the
tracked slot itself on macOS instead of comparing object identity, and
`touchesMoved` replaces the stored event with the latest one so
`extractAllTouches` reports the pointer's current position rather than
where the sequence started.

## Test plan

<details>
<summary>Tested on the following code:</summary>

```tsx
import React, { useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  GestureDetector,
  GestureStateManager,
  usePanGesture,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';

export default function EmptyExample() {
  // Box A: manualActivation, JS never calls activate().
  // Expected: down -> move stream -> up, onFinalize canceled=true, never onActivate.
  const neverActivatePan = usePanGesture({
    manualActivation: true,
    onBegin: () => console.log('[never-activate] onBegin'),
    onActivate: () => console.log('[never-activate] onActivate (SHOULD NOT HAPPEN)'),
    onTouchesDown: () => console.log('[never-activate] onTouchesDown'),
    onTouchesMove: () => console.log('[never-activate] onTouchesMove'),
    onTouchesUp: () => console.log('[never-activate] onTouchesUp'),
    onDeactivate: () => console.log('[never-activate] onDeactivate'),
    onFinalize: (e) => console.log(`[never-activate] onFinalize canceled=${e.canceled}`),
  });

  // Box B: manualActivation, activates from onTouchesMove — the real-world pattern.
  // Expected: onActivate on first movement (tx ~0), onUpdate while dragging,
  // onTouchesUp + onDeactivate + onFinalize canceled=false on release.
  const selfTag = useSharedValue(-1);
  const selfActivatePan = usePanGesture({
    manualActivation: true,
    onTouchesDown: () => console.log(`[self-activate] onTouchesDown tag=${selfTag.value}`),
    onTouchesMove: () => {
      console.log(`[self-activate] onTouchesMove tag=${selfTag.value}`);
      if (selfTag.value !== -1) {
        GestureStateManager.activate(selfTag.value);
      }
    },
    onTouchesUp: () => console.log('[self-activate] onTouchesUp'),
    onBegin: () => console.log('[self-activate] onBegin'),
    onActivate: (e) =>
      console.log(`[self-activate] onActivate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
    onUpdate: (e) =>
      console.log(`[self-activate] onUpdate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
    onDeactivate: () => console.log('[self-activate] onDeactivate'),
    onFinalize: (e) => console.log(`[self-activate] onFinalize canceled=${e.canceled}`),
  });

  useEffect(() => {
    selfTag.value = selfActivatePan.handlerTag;
  }, [selfActivatePan.handlerTag, selfTag]);

  return (
    <View style={styles.container}>
      <Text style={styles.label}>A: manualActivation, never activated — click & drag, release</Text>
      <GestureDetector gesture={neverActivatePan}>
        <View style={[styles.box, { backgroundColor: 'darkorange' }]} />
      </GestureDetector>

      <Text style={styles.label}>B: manualActivation, self-activates on touch move — drag</Text>
      <GestureDetector gesture={selfActivatePan}>
        <View style={[styles.box, { backgroundColor: 'mediumpurple' }]} />
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 8 },
  label: { marginTop: 16, fontSize: 15, opacity: 0.6 },
  box: { width: 150, height: 150, borderRadius: 12 },
});
```

</details>
M
Michał Bert committed
bc605eb9be8d8638daf901d0298eb2d7cdfe7d6b
Parent: 787f165
Committed by GitHub <noreply@github.com> on 8/6/2026, 9:01:44 AM