Skip to content

Commit 546c4b4

Browse files
vincentriemerfacebook-github-bot
authored andcommitted
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
1 parent 8441c4a commit 546c4b4

File tree

6 files changed

+227
-3
lines changed

6 files changed

+227
-3
lines changed

React/Fabric/RCTSurfaceTouchHandler.mm

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
8888
*/
8989
UIKeyModifierFlags modifierFlags;
9090

91+
/*
92+
* Indicates if the active touch represents the primary pointer of this pointer type.
93+
*/
94+
bool isPrimary;
95+
9196
/*
9297
* A component view on which the touch was begun.
9398
*/
@@ -108,6 +113,15 @@ bool operator()(const ActiveTouch &lhs, const ActiveTouch &rhs) const
108113
};
109114
};
110115

116+
// Mouse and Pen pointers get reserved IDs so they stay consistent no matter the order
117+
// at which events come in
118+
static int const kMousePointerId = 0;
119+
static int const kPencilPointerId = 1;
120+
121+
// If a new reserved ID is added above this should be incremented to ensure touch events
122+
// do not conflict
123+
static int const kTouchIdentifierPoolOffset = 2;
124+
111125
// Returns a CGPoint which represents the tiltX/Y values (in RADIANS)
112126
// Adapted from https://gist.github.com/k3a/2903719bb42b48c9198d20c2d6f73ac1
113127
static CGPoint SphericalToTilt(CGFloat altitudeAngleRad, CGFloat azimuthAngleRad)
@@ -309,6 +323,7 @@ static PointerEvent CreatePointerEventFromActiveTouch(ActiveTouch activeTouch, R
309323

310324
event.tangentialPressure = 0.0;
311325
event.twist = 0;
326+
event.isPrimary = activeTouch.isPrimary;
312327

313328
return event;
314329
}
@@ -324,7 +339,7 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData(
324339
// "touch" events produced from a mouse cursor on iOS always have the ID 0 so
325340
// we can just assume that here since these sort of hover events only ever come
326341
// from the mouse
327-
event.pointerId = 0;
342+
event.pointerId = kMousePointerId;
328343
event.pressure = 0.0;
329344
event.pointerType = "mouse";
330345
event.clientPoint = RCTPointFromCGPoint(clientLocation);
@@ -339,6 +354,7 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData(
339354
UpdatePointerEventModifierFlags(event, modifierFlags);
340355
event.tangentialPressure = 0.0;
341356
event.twist = 0;
357+
event.isPrimary = true;
342358
return event;
343359
}
344360

@@ -412,6 +428,8 @@ @implementation RCTSurfaceTouchHandler {
412428

413429
UIHoverGestureRecognizer *_hoverRecognizer API_AVAILABLE(ios(13.0));
414430
NSOrderedSet *_currentlyHoveredViews;
431+
432+
int _primaryTouchPointerId;
415433
}
416434

417435
- (instancetype)init
@@ -429,6 +447,7 @@ - (instancetype)init
429447

430448
_hoverRecognizer = nil;
431449
_currentlyHoveredViews = [NSOrderedSet orderedSet];
450+
_primaryTouchPointerId = -1;
432451
}
433452

434453
return self;
@@ -469,7 +488,34 @@ - (void)_registerTouches:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
469488
{
470489
for (UITouch *touch in touches) {
471490
auto activeTouch = CreateTouchWithUITouch(touch, event, _rootComponentView, _viewOriginOffset);
472-
activeTouch.touch.identifier = _identifierPool.dequeue();
491+
492+
if (@available(iOS 13.4, *)) {
493+
switch (touch.type) {
494+
case UITouchTypeIndirectPointer:
495+
activeTouch.touch.identifier = kMousePointerId;
496+
activeTouch.isPrimary = true;
497+
break;
498+
case UITouchTypePencil:
499+
activeTouch.touch.identifier = kPencilPointerId;
500+
activeTouch.isPrimary = true;
501+
break;
502+
default:
503+
// use the identifier pool offset to ensure no conflicts between the reserved IDs and the
504+
// touch IDs
505+
activeTouch.touch.identifier = _identifierPool.dequeue() + kTouchIdentifierPoolOffset;
506+
if (_primaryTouchPointerId == -1) {
507+
_primaryTouchPointerId = activeTouch.touch.identifier;
508+
activeTouch.isPrimary = true;
509+
}
510+
break;
511+
}
512+
} else {
513+
activeTouch.touch.identifier = _identifierPool.dequeue();
514+
if (_primaryTouchPointerId == -1) {
515+
_primaryTouchPointerId = activeTouch.touch.identifier;
516+
activeTouch.isPrimary = true;
517+
}
518+
}
473519
_activeTouches.emplace(touch, activeTouch);
474520
}
475521
}
@@ -496,7 +542,25 @@ - (void)_unregisterTouches:(NSSet<UITouch *> *)touches
496542
continue;
497543
}
498544
auto &activeTouch = iterator->second;
499-
_identifierPool.enqueue(activeTouch.touch.identifier);
545+
546+
if (activeTouch.touch.identifier == _primaryTouchPointerId) {
547+
_primaryTouchPointerId = -1;
548+
}
549+
550+
if (@available(iOS 13.4, *)) {
551+
// only need to enqueue if the touch type isn't one with a reserved identifier
552+
switch (touch.type) {
553+
case UITouchTypeIndirectPointer:
554+
case UITouchTypePencil:
555+
break;
556+
default:
557+
// since the touch's identifier has been offset we need to re-normalize it to 0-based
558+
// which is what the identifier pool expects
559+
_identifierPool.enqueue(activeTouch.touch.identifier - kTouchIdentifierPoolOffset);
560+
}
561+
} else {
562+
_identifierPool.enqueue(activeTouch.touch.identifier);
563+
}
500564
_activeTouches.erase(touch);
501565
}
502566
}

ReactCommon/react/renderer/components/view/PointerEvent.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ std::vector<DebugStringConvertibleObject> getDebugProps(
3939
{"shiftKey", getDebugDescription(pointerEvent.shiftKey, options)},
4040
{"altKey", getDebugDescription(pointerEvent.altKey, options)},
4141
{"metaKey", getDebugDescription(pointerEvent.metaKey, options)},
42+
{"isPrimary", getDebugDescription(pointerEvent.isPrimary, options)},
4243
};
4344
}
4445

ReactCommon/react/renderer/components/view/PointerEvent.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ struct PointerEvent {
100100
* Returns true if the meta key was down when the event was fired.
101101
*/
102102
bool metaKey;
103+
/*
104+
* Indicates if the pointer represents the primary pointer of this pointer
105+
* type.
106+
*/
107+
bool isPrimary;
103108
};
104109

105110
#if RN_DEBUG_STRING_CONVERTIBLE

ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ static jsi::Value pointerEventPayload(
9191
object.setProperty(runtime, "shiftKey", event.shiftKey);
9292
object.setProperty(runtime, "altKey", event.altKey);
9393
object.setProperty(runtime, "metaKey", event.metaKey);
94+
object.setProperty(runtime, "isPrimary", event.isPrimary);
9495
return object;
9596
}
9697

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow
9+
*/
10+
11+
import type {PlatformTestComponentBaseProps} from '../PlatformTest/RNTesterPlatformTestTypes';
12+
import type {PointerEvent} from 'react-native/Libraries/Types/CoreEventTypes';
13+
14+
import {useTestEventHandler} from './PointerEventSupport';
15+
import RNTesterPlatformTest from '../PlatformTest/RNTesterPlatformTest';
16+
import * as React from 'react';
17+
import {useRef, useCallback, useMemo} from 'react';
18+
import {StyleSheet, View} from 'react-native';
19+
20+
const styles = StyleSheet.create({
21+
root: {
22+
flexDirection: 'row',
23+
justifyContent: 'space-around',
24+
paddingTop: 20,
25+
},
26+
box: {
27+
width: 80,
28+
height: 80,
29+
},
30+
});
31+
32+
const listenedEvents = ['pointerDown', 'pointerUp'];
33+
34+
const expectedOrder = [
35+
['red', 'pointerDown', true],
36+
['green', 'pointerDown', false],
37+
['red', 'pointerUp', true],
38+
['blue', 'pointerDown', true],
39+
['green', 'pointerUp', false],
40+
['blue', 'pointerUp', true],
41+
];
42+
43+
function PointerEventPrimaryTouchPointerTestCase(
44+
props: PlatformTestComponentBaseProps,
45+
) {
46+
const {harness} = props;
47+
48+
const detected_eventsRef = useRef({});
49+
50+
const handleIncomingPointerEvent = useCallback(
51+
(boxLabel: string, eventType: string, isPrimary: boolean) => {
52+
const detected_events = detected_eventsRef.current;
53+
54+
const pointerEventIdentifier = `${boxLabel}-${eventType}-${String(
55+
isPrimary,
56+
)}`;
57+
if (detected_events[pointerEventIdentifier]) {
58+
return;
59+
}
60+
61+
const [expectedBoxLabel, expectedEventType, expectedIsPrimary] =
62+
expectedOrder[Object.keys(detected_events).length];
63+
detected_events[pointerEventIdentifier] = true;
64+
65+
harness.test(({assert_equals}) => {
66+
assert_equals(
67+
boxLabel,
68+
expectedBoxLabel,
69+
'event should be coming from the correct box',
70+
);
71+
assert_equals(
72+
eventType,
73+
expectedEventType.toLowerCase(),
74+
'event should have the right type',
75+
);
76+
assert_equals(
77+
isPrimary,
78+
expectedIsPrimary,
79+
'event should be correctly primary',
80+
);
81+
}, `${expectedBoxLabel} box's ${expectedEventType} should${!expectedIsPrimary ? ' not' : ''} be marked as the primary pointer`);
82+
},
83+
[harness],
84+
);
85+
86+
const createBoxHandler = useCallback(
87+
(boxLabel: string) => (event: PointerEvent, eventName: string) => {
88+
if (
89+
Object.keys(detected_eventsRef.current).length < expectedOrder.length
90+
) {
91+
handleIncomingPointerEvent(
92+
boxLabel,
93+
eventName,
94+
event.nativeEvent.isPrimary,
95+
);
96+
}
97+
},
98+
[handleIncomingPointerEvent],
99+
);
100+
101+
const {handleBoxAEvent, handleBoxBEvent, handleBoxCEvent} = useMemo(
102+
() => ({
103+
handleBoxAEvent: createBoxHandler('red'),
104+
handleBoxBEvent: createBoxHandler('green'),
105+
handleBoxCEvent: createBoxHandler('blue'),
106+
}),
107+
[createBoxHandler],
108+
);
109+
110+
const boxAHandlers = useTestEventHandler(listenedEvents, handleBoxAEvent);
111+
const boxBHandlers = useTestEventHandler(listenedEvents, handleBoxBEvent);
112+
const boxCHandlers = useTestEventHandler(listenedEvents, handleBoxCEvent);
113+
114+
return (
115+
<View style={styles.root}>
116+
<View {...boxAHandlers} style={[styles.box, {backgroundColor: 'red'}]} />
117+
<View
118+
{...boxBHandlers}
119+
style={[styles.box, {backgroundColor: 'green'}]}
120+
/>
121+
<View {...boxCHandlers} style={[styles.box, {backgroundColor: 'blue'}]} />
122+
</View>
123+
);
124+
}
125+
126+
type Props = $ReadOnly<{}>;
127+
export default function PointerEventPrimaryTouchPointer(
128+
props: Props,
129+
): React.MixedElement {
130+
return (
131+
<RNTesterPlatformTest
132+
component={PointerEventPrimaryTouchPointerTestCase}
133+
description="This test checks for the correct differentiation of a primary pointer in a multitouch scenario"
134+
instructions={[
135+
'Touch and hold your finger on the red box',
136+
'Take different finger and touch and hold the green box',
137+
'Lift your finger from the red box and place it on the blue box',
138+
'Lift your finger from the green box',
139+
'Lift your finger from the blue box',
140+
]}
141+
title="Pointer Event primary touch pointer test"
142+
/>
143+
);
144+
}

packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTyp
1616
import PointerEventAttributesHoverablePointers from './W3CPointerEventPlatformTests/PointerEventAttributesHoverablePointers';
1717
import PointerEventPointerMove from './W3CPointerEventPlatformTests/PointerEventPointerMove';
1818
import CompatibilityAnimatedPointerMove from './Compatibility/CompatibilityAnimatedPointerMove';
19+
import PointerEventPrimaryTouchPointer from './W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer';
1920

2021
function EventfulView(props: {|
2122
name: string,
@@ -247,6 +248,14 @@ export default {
247248
return <PointerEventPointerMove />;
248249
},
249250
},
251+
{
252+
name: 'pointerevent_primary_touch_pointer',
253+
description: '',
254+
title: 'Pointer Event primary touch pointer test',
255+
render(): React.Node {
256+
return <PointerEventPrimaryTouchPointer />;
257+
},
258+
},
250259
CompatibilityAnimatedPointerMove,
251260
],
252261
};

0 commit comments

Comments
 (0)