From 546c4b4c6cbbae0339a9c23ea2bd7f34bb552247 Mon Sep 17 00:00:00 2001 From: Vincent Riemer Date: Tue, 26 Jul 2022 11:41:48 -0700 Subject: [PATCH] Add isPrimary property implementation to the PointerEvent object Summary: Changelog: [iOS][Internal] - Add isPrimary property implementation to the PointerEvent object This diff adds the `isPrimary` property to the PointerEvent object iOS implementation. In addition this adds a related change where we "reserve" the 0 touch identifier for mouse events and the 1 identifier for apple pencil events. This is an easy way to ensure that these pointers are always consistent no matter what happens. Since mouse & pencil pointers should always be considered the primary pointer, that allows us to focus the more advanced primary pointer differentiation purely on touch events. The logic for this touch event primary pointer differentiation is essentially setting the first touch it recieves as a primary pointer, setting it on touch registration, and sets all subsequent touchs (while the first touch is down) as not the primary pointers. When that primary pointer is lifted, the class property keeping track of the primary pointer is reset and then the **next** pointer (secondary pointers which had already started before the previous primary pointer was lifted are not "upgraded" to primary) is marked as primary. A new platform test is also included in this diff in order to verify the aforementioned behavior. Reviewed By: lunaleaps Differential Revision: D37961707 fbshipit-source-id: ae8b78c5bfea6902fb73094fca1552e4e648ea44 --- React/Fabric/RCTSurfaceTouchHandler.mm | 70 ++++++++- .../renderer/components/view/PointerEvent.cpp | 1 + .../renderer/components/view/PointerEvent.h | 5 + .../components/view/TouchEventEmitter.cpp | 1 + .../PointerEventPrimaryTouchPointer.js | 144 ++++++++++++++++++ .../Experimental/W3CPointerEventsExample.js | 9 ++ 6 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer.js 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, ], };