diff --git a/React/Fabric/RCTSurfaceTouchHandler.mm b/React/Fabric/RCTSurfaceTouchHandler.mm index d748a99641c..67c6aa9b60d 100644 --- a/React/Fabric/RCTSurfaceTouchHandler.mm +++ b/React/Fabric/RCTSurfaceTouchHandler.mm @@ -88,6 +88,11 @@ struct ActiveTouch { */ UIKeyModifierFlags modifierFlags; + /* + * Indicates if the active touch represents the primary pointer of this pointer type. + */ + bool isPrimary; + /* * A component view on which the touch was begun. */ @@ -108,6 +113,15 @@ struct ActiveTouch { }; }; +// Mouse and Pen pointers get reserved IDs so they stay consistent no matter the order +// at which events come in +static int const kMousePointerId = 0; +static int const kPencilPointerId = 1; + +// If a new reserved ID is added above this should be incremented to ensure touch events +// do not conflict +static int const kTouchIdentifierPoolOffset = 2; + // Returns a CGPoint which represents the tiltX/Y values (in RADIANS) // Adapted from https://gist.github.com/k3a/2903719bb42b48c9198d20c2d6f73ac1 static CGPoint SphericalToTilt(CGFloat altitudeAngleRad, CGFloat azimuthAngleRad) @@ -309,6 +323,7 @@ static PointerEvent CreatePointerEventFromActiveTouch(ActiveTouch activeTouch, R event.tangentialPressure = 0.0; event.twist = 0; + event.isPrimary = activeTouch.isPrimary; return event; } @@ -324,7 +339,7 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData( // "touch" events produced from a mouse cursor on iOS always have the ID 0 so // we can just assume that here since these sort of hover events only ever come // from the mouse - event.pointerId = 0; + event.pointerId = kMousePointerId; event.pressure = 0.0; event.pointerType = "mouse"; event.clientPoint = RCTPointFromCGPoint(clientLocation); @@ -339,6 +354,7 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData( UpdatePointerEventModifierFlags(event, modifierFlags); event.tangentialPressure = 0.0; event.twist = 0; + event.isPrimary = true; return event; } @@ -412,6 +428,8 @@ struct PointerHasher { UIHoverGestureRecognizer *_hoverRecognizer API_AVAILABLE(ios(13.0)); NSOrderedSet *_currentlyHoveredViews; + + int _primaryTouchPointerId; } - (instancetype)init @@ -429,6 +447,7 @@ struct PointerHasher { _hoverRecognizer = nil; _currentlyHoveredViews = [NSOrderedSet orderedSet]; + _primaryTouchPointerId = -1; } return self; @@ -469,7 +488,34 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)act { for (UITouch *touch in touches) { auto activeTouch = CreateTouchWithUITouch(touch, event, _rootComponentView, _viewOriginOffset); - activeTouch.touch.identifier = _identifierPool.dequeue(); + + if (@available(iOS 13.4, *)) { + switch (touch.type) { + case UITouchTypeIndirectPointer: + activeTouch.touch.identifier = kMousePointerId; + activeTouch.isPrimary = true; + break; + case UITouchTypePencil: + activeTouch.touch.identifier = kPencilPointerId; + activeTouch.isPrimary = true; + break; + default: + // use the identifier pool offset to ensure no conflicts between the reserved IDs and the + // touch IDs + activeTouch.touch.identifier = _identifierPool.dequeue() + kTouchIdentifierPoolOffset; + if (_primaryTouchPointerId == -1) { + _primaryTouchPointerId = activeTouch.touch.identifier; + activeTouch.isPrimary = true; + } + break; + } + } else { + activeTouch.touch.identifier = _identifierPool.dequeue(); + if (_primaryTouchPointerId == -1) { + _primaryTouchPointerId = activeTouch.touch.identifier; + activeTouch.isPrimary = true; + } + } _activeTouches.emplace(touch, activeTouch); } } @@ -496,7 +542,25 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)act continue; } auto &activeTouch = iterator->second; - _identifierPool.enqueue(activeTouch.touch.identifier); + + if (activeTouch.touch.identifier == _primaryTouchPointerId) { + _primaryTouchPointerId = -1; + } + + if (@available(iOS 13.4, *)) { + // only need to enqueue if the touch type isn't one with a reserved identifier + switch (touch.type) { + case UITouchTypeIndirectPointer: + case UITouchTypePencil: + break; + default: + // since the touch's identifier has been offset we need to re-normalize it to 0-based + // which is what the identifier pool expects + _identifierPool.enqueue(activeTouch.touch.identifier - kTouchIdentifierPoolOffset); + } + } else { + _identifierPool.enqueue(activeTouch.touch.identifier); + } _activeTouches.erase(touch); } } diff --git a/ReactCommon/react/renderer/components/view/PointerEvent.cpp b/ReactCommon/react/renderer/components/view/PointerEvent.cpp index ce0627986f9..7c5c1869dca 100644 --- a/ReactCommon/react/renderer/components/view/PointerEvent.cpp +++ b/ReactCommon/react/renderer/components/view/PointerEvent.cpp @@ -39,6 +39,7 @@ std::vector getDebugProps( {"shiftKey", getDebugDescription(pointerEvent.shiftKey, options)}, {"altKey", getDebugDescription(pointerEvent.altKey, options)}, {"metaKey", getDebugDescription(pointerEvent.metaKey, options)}, + {"isPrimary", getDebugDescription(pointerEvent.isPrimary, options)}, }; } diff --git a/ReactCommon/react/renderer/components/view/PointerEvent.h b/ReactCommon/react/renderer/components/view/PointerEvent.h index abe80ead902..4812e55b331 100644 --- a/ReactCommon/react/renderer/components/view/PointerEvent.h +++ b/ReactCommon/react/renderer/components/view/PointerEvent.h @@ -100,6 +100,11 @@ struct PointerEvent { * Returns true if the meta key was down when the event was fired. */ bool metaKey; + /* + * Indicates if the pointer represents the primary pointer of this pointer + * type. + */ + bool isPrimary; }; #if RN_DEBUG_STRING_CONVERTIBLE diff --git a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp index 8eb4bc8d144..5d5b17f28b6 100644 --- a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp +++ b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp @@ -91,6 +91,7 @@ static jsi::Value pointerEventPayload( object.setProperty(runtime, "shiftKey", event.shiftKey); object.setProperty(runtime, "altKey", event.altKey); object.setProperty(runtime, "metaKey", event.metaKey); + object.setProperty(runtime, "isPrimary", event.isPrimary); return object; } diff --git a/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer.js b/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer.js new file mode 100644 index 00000000000..8516d265647 --- /dev/null +++ b/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer.js @@ -0,0 +1,144 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import type {PlatformTestComponentBaseProps} from '../PlatformTest/RNTesterPlatformTestTypes'; +import type {PointerEvent} from 'react-native/Libraries/Types/CoreEventTypes'; + +import {useTestEventHandler} from './PointerEventSupport'; +import RNTesterPlatformTest from '../PlatformTest/RNTesterPlatformTest'; +import * as React from 'react'; +import {useRef, useCallback, useMemo} from 'react'; +import {StyleSheet, View} from 'react-native'; + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + justifyContent: 'space-around', + paddingTop: 20, + }, + box: { + width: 80, + height: 80, + }, +}); + +const listenedEvents = ['pointerDown', 'pointerUp']; + +const expectedOrder = [ + ['red', 'pointerDown', true], + ['green', 'pointerDown', false], + ['red', 'pointerUp', true], + ['blue', 'pointerDown', true], + ['green', 'pointerUp', false], + ['blue', 'pointerUp', true], +]; + +function PointerEventPrimaryTouchPointerTestCase( + props: PlatformTestComponentBaseProps, +) { + const {harness} = props; + + const detected_eventsRef = useRef({}); + + const handleIncomingPointerEvent = useCallback( + (boxLabel: string, eventType: string, isPrimary: boolean) => { + const detected_events = detected_eventsRef.current; + + const pointerEventIdentifier = `${boxLabel}-${eventType}-${String( + isPrimary, + )}`; + if (detected_events[pointerEventIdentifier]) { + return; + } + + const [expectedBoxLabel, expectedEventType, expectedIsPrimary] = + expectedOrder[Object.keys(detected_events).length]; + detected_events[pointerEventIdentifier] = true; + + harness.test(({assert_equals}) => { + assert_equals( + boxLabel, + expectedBoxLabel, + 'event should be coming from the correct box', + ); + assert_equals( + eventType, + expectedEventType.toLowerCase(), + 'event should have the right type', + ); + assert_equals( + isPrimary, + expectedIsPrimary, + 'event should be correctly primary', + ); + }, `${expectedBoxLabel} box's ${expectedEventType} should${!expectedIsPrimary ? ' not' : ''} be marked as the primary pointer`); + }, + [harness], + ); + + const createBoxHandler = useCallback( + (boxLabel: string) => (event: PointerEvent, eventName: string) => { + if ( + Object.keys(detected_eventsRef.current).length < expectedOrder.length + ) { + handleIncomingPointerEvent( + boxLabel, + eventName, + event.nativeEvent.isPrimary, + ); + } + }, + [handleIncomingPointerEvent], + ); + + const {handleBoxAEvent, handleBoxBEvent, handleBoxCEvent} = useMemo( + () => ({ + handleBoxAEvent: createBoxHandler('red'), + handleBoxBEvent: createBoxHandler('green'), + handleBoxCEvent: createBoxHandler('blue'), + }), + [createBoxHandler], + ); + + const boxAHandlers = useTestEventHandler(listenedEvents, handleBoxAEvent); + const boxBHandlers = useTestEventHandler(listenedEvents, handleBoxBEvent); + const boxCHandlers = useTestEventHandler(listenedEvents, handleBoxCEvent); + + return ( + + + + + + ); +} + +type Props = $ReadOnly<{}>; +export default function PointerEventPrimaryTouchPointer( + props: Props, +): React.MixedElement { + return ( + + ); +} diff --git a/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js index 0e1745d2504..2663e95fb22 100644 --- a/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js +++ b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js @@ -16,6 +16,7 @@ import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTyp import PointerEventAttributesHoverablePointers from './W3CPointerEventPlatformTests/PointerEventAttributesHoverablePointers'; import PointerEventPointerMove from './W3CPointerEventPlatformTests/PointerEventPointerMove'; import CompatibilityAnimatedPointerMove from './Compatibility/CompatibilityAnimatedPointerMove'; +import PointerEventPrimaryTouchPointer from './W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer'; function EventfulView(props: {| name: string, @@ -247,6 +248,14 @@ export default { return ; }, }, + { + name: 'pointerevent_primary_touch_pointer', + description: '', + title: 'Pointer Event primary touch pointer test', + render(): React.Node { + return ; + }, + }, CompatibilityAnimatedPointerMove, ], };