Skip to content

Commit

Permalink
Add isPrimary property implementation to the PointerEvent object
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vincentriemer authored and facebook-github-bot committed Jul 26, 2022
1 parent 8441c4a commit 546c4b4
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 3 deletions.
70 changes: 67 additions & 3 deletions React/Fabric/RCTSurfaceTouchHandler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
*/
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.
*/
Expand All @@ -108,6 +113,15 @@ bool operator()(const ActiveTouch &lhs, const ActiveTouch &rhs) const
};
};

// 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)
Expand Down Expand Up @@ -309,6 +323,7 @@ static PointerEvent CreatePointerEventFromActiveTouch(ActiveTouch activeTouch, R

event.tangentialPressure = 0.0;
event.twist = 0;
event.isPrimary = activeTouch.isPrimary;

return event;
}
Expand All @@ -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);
Expand All @@ -339,6 +354,7 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData(
UpdatePointerEventModifierFlags(event, modifierFlags);
event.tangentialPressure = 0.0;
event.twist = 0;
event.isPrimary = true;
return event;
}

Expand Down Expand Up @@ -412,6 +428,8 @@ @implementation RCTSurfaceTouchHandler {

UIHoverGestureRecognizer *_hoverRecognizer API_AVAILABLE(ios(13.0));
NSOrderedSet *_currentlyHoveredViews;

int _primaryTouchPointerId;
}

- (instancetype)init
Expand All @@ -429,6 +447,7 @@ - (instancetype)init

_hoverRecognizer = nil;
_currentlyHoveredViews = [NSOrderedSet orderedSet];
_primaryTouchPointerId = -1;
}

return self;
Expand Down Expand Up @@ -469,7 +488,34 @@ - (void)_registerTouches:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
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);
}
}
Expand All @@ -496,7 +542,25 @@ - (void)_unregisterTouches:(NSSet<UITouch *> *)touches
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ std::vector<DebugStringConvertibleObject> getDebugProps(
{"shiftKey", getDebugDescription(pointerEvent.shiftKey, options)},
{"altKey", getDebugDescription(pointerEvent.altKey, options)},
{"metaKey", getDebugDescription(pointerEvent.metaKey, options)},
{"isPrimary", getDebugDescription(pointerEvent.isPrimary, options)},
};
}

Expand Down
5 changes: 5 additions & 0 deletions ReactCommon/react/renderer/components/view/PointerEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<View style={styles.root}>
<View {...boxAHandlers} style={[styles.box, {backgroundColor: 'red'}]} />
<View
{...boxBHandlers}
style={[styles.box, {backgroundColor: 'green'}]}
/>
<View {...boxCHandlers} style={[styles.box, {backgroundColor: 'blue'}]} />
</View>
);
}

type Props = $ReadOnly<{}>;
export default function PointerEventPrimaryTouchPointer(
props: Props,
): React.MixedElement {
return (
<RNTesterPlatformTest
component={PointerEventPrimaryTouchPointerTestCase}
description="This test checks for the correct differentiation of a primary pointer in a multitouch scenario"
instructions={[
'Touch and hold your finger on the red box',
'Take different finger and touch and hold the green box',
'Lift your finger from the red box and place it on the blue box',
'Lift your finger from the green box',
'Lift your finger from the blue box',
]}
title="Pointer Event primary touch pointer test"
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -247,6 +248,14 @@ export default {
return <PointerEventPointerMove />;
},
},
{
name: 'pointerevent_primary_touch_pointer',
description: '',
title: 'Pointer Event primary touch pointer test',
render(): React.Node {
return <PointerEventPrimaryTouchPointer />;
},
},
CompatibilityAnimatedPointerMove,
],
};

0 comments on commit 546c4b4

Please sign in to comment.