Skip to content

Commit 5db7cd1

Browse files
committed
Bring over RCTSurfaceTouchHandler diffs
1 parent 3ab5b85 commit 5db7cd1

File tree

2 files changed

+294
-1
lines changed

2 files changed

+294
-1
lines changed

packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
NS_ASSUME_NONNULL_BEGIN
1111

12+
#if TARGET_OS_OSX // [macOS
13+
static NSString *const RCTSurfaceTouchHandlerOutsideViewMouseUpNotification = @"RCTSurfaceTouchHandlerOutsideViewMouseUpNotification";
14+
#endif // macOS]
15+
1216
@interface RCTSurfaceTouchHandler : UIGestureRecognizer
1317

1418
/*
@@ -23,6 +27,15 @@ NS_ASSUME_NONNULL_BEGIN
2327
*/
2428
@property (nonatomic, assign) CGPoint viewOriginOffset;
2529

30+
#if TARGET_OS_OSX // [macOS
31+
+ (instancetype)surfaceTouchHandlerForEvent:(NSEvent *)event;
32+
+ (instancetype)surfaceTouchHandlerForView:(NSView *)view;
33+
+ (void)notifyOutsideViewMouseUp:(NSEvent *)event;
34+
35+
- (void)cancelTouchWithEvent:(NSEvent *)event;
36+
- (void)reset;
37+
#endif // macOS]
38+
2639
@end
2740

2841
NS_ASSUME_NONNULL_END

packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm

Lines changed: 281 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,65 @@
1111
#import <React/RCTUtils.h>
1212
#import <React/RCTViewComponentView.h>
1313
#import <React/RCTUIKit.h>
14+
#if TARGET_OS_OSX // [macOS
15+
#import <React/RCTSurfaceHostingView.h>
16+
#endif // macOS]
17+
1418

1519
#import "RCTConversions.h"
1620
#import "RCTSurfacePointerHandler.h"
1721
#import "RCTTouchableComponentViewProtocol.h"
1822

23+
24+
#if TARGET_OS_OSX // [macOS
25+
@interface RCTSurfaceTouchHandler (Private)
26+
- (void)endFromEventTrackingLeftMouseUp:(NSEvent *)event;
27+
- (void)endFromEventTrackingRightMouseUp:(NSEvent *)event;
28+
@end
29+
30+
@interface NSApplication (RCTSurfaceTouchHandlerOverride)
31+
- (NSEvent*)override_surface_nextEventMatchingMask:(NSEventMask)mask
32+
untilDate:(NSDate*)expiration
33+
inMode:(NSRunLoopMode)mode
34+
dequeue:(BOOL)dequeue;
35+
@end
36+
37+
@implementation NSApplication (RCTSurfaceTouchHandlerOverride)
38+
39+
+ (void)load
40+
{
41+
RCTSwapInstanceMethods(self, @selector(nextEventMatchingMask:untilDate:inMode:dequeue:), @selector(override_surface_nextEventMatchingMask:untilDate:inMode:dequeue:));
42+
}
43+
44+
- (NSEvent*)override_surface_nextEventMatchingMask:(NSEventMask)mask
45+
untilDate:(NSDate*)expiration
46+
inMode:(NSRunLoopMode)mode
47+
dequeue:(BOOL)dequeue
48+
{
49+
NSEvent* event = [self override_surface_nextEventMatchingMask:mask
50+
untilDate:expiration
51+
inMode:mode
52+
dequeue:dequeue];
53+
if (dequeue && (event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp)) {
54+
RCTSurfaceTouchHandler *targetSurfaceTouchHandler = [RCTSurfaceTouchHandler surfaceTouchHandlerForEvent:event];
55+
if (!targetSurfaceTouchHandler) {
56+
[RCTSurfaceTouchHandler notifyOutsideViewMouseUp:event];
57+
} else if (event.type == NSEventTypeRightMouseUp && [mode isEqualTo:NSEventTrackingRunLoopMode]) {
58+
// If the event is consumed by an event tracking loop, we won't get the mouse up event
59+
if (event.type == NSEventTypeLeftMouseUp) {
60+
[targetSurfaceTouchHandler endFromEventTrackingLeftMouseUp:event];
61+
} else if (event.type == NSEventTypeRightMouseUp) {
62+
[targetSurfaceTouchHandler endFromEventTrackingRightMouseUp:event];
63+
}
64+
}
65+
}
66+
67+
return event;
68+
}
69+
70+
@end
71+
#endif // macOS]
72+
1973
using namespace facebook::react;
2074

2175
typedef NS_ENUM(NSInteger, RCTTouchEventType) {
@@ -207,7 +261,13 @@ - (instancetype)init
207261
self.cancelsTouchesInView = NO;
208262
self.delaysTouchesBegan = NO; // This is default value.
209263
self.delaysTouchesEnded = NO;
210-
#endif // [macOS]
264+
#else // [macOS
265+
[[NSNotificationCenter defaultCenter] addObserver:self
266+
selector:@selector(endOutsideViewMouseUp:)
267+
name:RCTSurfaceTouchHandlerOutsideViewMouseUpNotification
268+
object:[RCTSurfaceTouchHandler class]];
269+
#endif // macOS]
270+
211271

212272
self.delegate = self;
213273

@@ -586,4 +646,224 @@ - (void)_cancelTouches
586646
[self setEnabled:YES];
587647
}
588648

649+
650+
#if TARGET_OS_OSX // [macOS
651+
652+
#pragma mark - macOS
653+
654+
+ (instancetype)surfaceTouchHandlerForEvent:(NSEvent *)event {
655+
RCTPlatformView *hitView = [event.window.contentView.superview hitTest:event.locationInWindow];
656+
return [self surfaceTouchHandlerForView:hitView];
657+
}
658+
659+
+ (instancetype)surfaceTouchHandlerForView:(RCTPlatformView *)view {
660+
if ([view isKindOfClass:[RCTSurfaceHostingView class]]) {
661+
// The RCTSurfaceTouchHandler is attached to surface's view.
662+
view = (RCTPlatformView *)(((RCTSurfaceHostingView *)view).surface.view);
663+
}
664+
665+
while (view) {
666+
for (NSGestureRecognizer *gestureRecognizer in view.gestureRecognizers) {
667+
if ([gestureRecognizer isKindOfClass:[self class]]) {
668+
return (RCTSurfaceTouchHandler *)gestureRecognizer;
669+
}
670+
}
671+
672+
view = view.superview;
673+
}
674+
675+
return nil;
676+
}
677+
678+
+ (void)notifyOutsideViewMouseUp:(NSEvent *)event {
679+
[[NSNotificationCenter defaultCenter] postNotificationName:RCTSurfaceTouchHandlerOutsideViewMouseUpNotification
680+
object:self
681+
userInfo:@{@"event": event}];
682+
}
683+
684+
- (void)endOutsideViewMouseUp:(NSNotification *)notification {
685+
NSEvent *event = notification.userInfo[@"event"];
686+
687+
auto iterator = _activeTouches.find(event.eventNumber);
688+
if (iterator == _activeTouches.end()) {
689+
// A contextual menu click would generate a mouse up with a diffrent event
690+
// and leave a touchable/pressable session open. This would cause touch end
691+
// events from a modal window to end the touchable/pressable session and
692+
// potentially trigger an onPress event. Hence the need to reset and cancel
693+
// that session when a mouse up event was detected outside the touch handler
694+
// view bounds.
695+
[self reset];
696+
return;
697+
}
698+
699+
[self cancelTouchWithEvent:event];
700+
}
701+
702+
- (void)endFromEventTrackingRightMouseUp:(NSEvent *)event
703+
{
704+
auto iterator = _activeTouches.find(event.eventNumber);
705+
if (iterator == _activeTouches.end()) {
706+
return;
707+
}
708+
709+
[self cancelTouchWithEvent:event];
710+
}
711+
712+
- (void)cancelTouchWithEvent:(NSEvent *)event
713+
{
714+
NSSet *touches = [NSSet setWithObject:event];
715+
[self _updateTouches:touches];
716+
[self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchCancel];
717+
[self _unregisterTouches:touches];
718+
719+
self.state = NSGestureRecognizerStateCancelled;
720+
}
721+
#endif // macOS]
722+
723+
#if !TARGET_OS_OSX
724+
- (void)hovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0))
725+
{
726+
RCTUIView *listenerView = recognizer.view; // [macOS]
727+
CGPoint clientLocation = [recognizer locationInView:listenerView];
728+
CGPoint screenLocation = [listenerView convertPoint:clientLocation
729+
toCoordinateSpace:listenerView.window.screen.coordinateSpace];
730+
731+
RCTUIView *targetView = [listenerView hitTest:clientLocation withEvent:nil]; // [macOS]
732+
targetView = FindClosestFabricManagedTouchableView(targetView);
733+
734+
CGPoint offsetLocation = [recognizer locationInView:targetView];
735+
736+
UIKeyModifierFlags modifierFlags;
737+
if (@available(iOS 13.4, *)) {
738+
modifierFlags = recognizer.modifierFlags;
739+
} else {
740+
modifierFlags = 0;
741+
}
742+
743+
PointerEvent event =
744+
CreatePointerEventFromIncompleteHoverData(clientLocation, screenLocation, offsetLocation, modifierFlags);
745+
746+
NSOrderedSet<RCTReactTaggedView *> *eventPathViews = [self handleIncomingPointerEvent:event onView:targetView];
747+
SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation);
748+
bool hasMoveEventListeners = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) ||
749+
IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture);
750+
if (eventEmitter != nil && hasMoveEventListeners) {
751+
eventEmitter->onPointerMove(event);
752+
}
753+
}
754+
#endif
755+
756+
/**
757+
* Private method which is used for tracking the location of pointer events to manage the entering/leaving events.
758+
* The primary idea is that a pointer's presence & movement is dicated by a variety of underlying events such as down,
759+
* move, and up — and they should all be treated the same when it comes to tracking the entering & leaving of pointers
760+
* to views. This method accomplishes that by recieving the pointer event, the target view (can be null in cases when
761+
* the event indicates that the pointer has left the screen entirely), and a block/callback where the underlying event
762+
* should be fired.
763+
*/
764+
#if !TARGET_OS_OSX
765+
- (NSOrderedSet<RCTReactTaggedView *> *)handleIncomingPointerEvent:(PointerEvent)event
766+
onView:(nullable RCTUIView *)targetView // [macOS]
767+
{
768+
int pointerId = event.pointerId;
769+
CGPoint clientLocation = CGPointMake(event.clientPoint.x, event.clientPoint.y);
770+
771+
NSOrderedSet<RCTReactTaggedView *> *currentlyHoveredViews =
772+
[_currentlyHoveredViewsPerPointer objectForKey:@(pointerId)];
773+
if (currentlyHoveredViews == nil) {
774+
currentlyHoveredViews = [NSOrderedSet orderedSet];
775+
}
776+
777+
RCTReactTaggedView *targetTaggedView = [RCTReactTaggedView wrap:targetView];
778+
RCTReactTaggedView *prevTargetTaggedView = [currentlyHoveredViews firstObject];
779+
RCTUIView *prevTargetView = prevTargetTaggedView.view; // [macOS]
780+
781+
NSOrderedSet<RCTReactTaggedView *> *eventPathViews = GetTouchableViewsInPathToRoot(targetView);
782+
783+
// Out
784+
if (prevTargetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) {
785+
BOOL shouldEmitOutEvent = IsAnyViewInPathListeningToEvent(currentlyHoveredViews, ViewEvents::Offset::PointerOut);
786+
SharedTouchEventEmitter eventEmitter =
787+
GetTouchEmitterFromView(prevTargetView, [_rootComponentView convertPoint:clientLocation toView:prevTargetView]);
788+
if (shouldEmitOutEvent && eventEmitter != nil) {
789+
eventEmitter->onPointerOut(event);
790+
}
791+
}
792+
793+
// Leaving
794+
795+
// pointerleave events need to be emited from the deepest target to the root but
796+
// we also need to efficiently keep track of if a view has a parent which is listening to the leave events,
797+
// so we first iterate from the root to the target, collecting the views which need events fired for, of which
798+
// we reverse iterate (now from target to root), actually emitting the events.
799+
NSMutableOrderedSet<RCTUIView *> *viewsToEmitLeaveEventsTo = [NSMutableOrderedSet orderedSet]; // [macOS]
800+
801+
BOOL hasParentLeaveListener = NO;
802+
for (RCTReactTaggedView *taggedView in [currentlyHoveredViews reverseObjectEnumerator]) {
803+
RCTUIView *componentView = taggedView.view; // [macOS]
804+
805+
BOOL shouldEmitEvent = componentView != nil &&
806+
(hasParentLeaveListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerLeave));
807+
808+
if (shouldEmitEvent && ![eventPathViews containsObject:taggedView]) {
809+
[viewsToEmitLeaveEventsTo addObject:componentView];
810+
}
811+
812+
if (shouldEmitEvent && !hasParentLeaveListener) {
813+
hasParentLeaveListener = YES;
814+
}
815+
}
816+
817+
for (RCTUIView *componentView in [viewsToEmitLeaveEventsTo reverseObjectEnumerator]) { // [macOS]
818+
SharedTouchEventEmitter eventEmitter =
819+
GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]);
820+
if (eventEmitter != nil) {
821+
eventEmitter->onPointerLeave(event);
822+
}
823+
}
824+
825+
// Over
826+
if (targetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) {
827+
BOOL shouldEmitOverEvent = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerOver);
828+
SharedTouchEventEmitter eventEmitter =
829+
GetTouchEmitterFromView(targetView, [_rootComponentView convertPoint:clientLocation toView:targetView]);
830+
if (shouldEmitOverEvent && eventEmitter != nil) {
831+
eventEmitter->onPointerOver(event);
832+
}
833+
}
834+
835+
// Entering
836+
837+
// We only want to emit events to JS if there is a view that is currently listening to said event
838+
// so we only send those event to the JS side if the element which has been entered is itself listening,
839+
// or if one of its parents is listening in case those listeners care about the capturing phase. Adding the ability
840+
// for native to distingusih between capturing listeners and not could be an optimization to futher reduce the number
841+
// of events we send to JS
842+
BOOL hasParentEnterListener = NO;
843+
for (RCTReactTaggedView *taggedView in [eventPathViews reverseObjectEnumerator]) {
844+
RCTUIView *componentView = taggedView.view; // [macOS]
845+
846+
BOOL shouldEmitEvent = componentView != nil &&
847+
(hasParentEnterListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerEnter));
848+
849+
if (shouldEmitEvent && ![currentlyHoveredViews containsObject:taggedView]) {
850+
SharedTouchEventEmitter eventEmitter =
851+
GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]);
852+
if (eventEmitter != nil) {
853+
eventEmitter->onPointerEnter(event);
854+
}
855+
}
856+
857+
if (shouldEmitEvent && !hasParentEnterListener) {
858+
hasParentEnterListener = YES;
859+
}
860+
}
861+
862+
[_currentlyHoveredViewsPerPointer setObject:eventPathViews forKey:@(pointerId)];
863+
864+
return eventPathViews;
865+
}
866+
#endif
867+
868+
589869
@end

0 commit comments

Comments
 (0)