|
11 | 11 | #import <React/RCTUtils.h>
|
12 | 12 | #import <React/RCTViewComponentView.h>
|
13 | 13 | #import <React/RCTUIKit.h>
|
| 14 | +#if TARGET_OS_OSX // [macOS |
| 15 | +#import <React/RCTSurfaceHostingView.h> |
| 16 | +#endif // macOS] |
| 17 | + |
14 | 18 |
|
15 | 19 | #import "RCTConversions.h"
|
16 | 20 | #import "RCTSurfacePointerHandler.h"
|
17 | 21 | #import "RCTTouchableComponentViewProtocol.h"
|
18 | 22 |
|
| 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 | + |
19 | 73 | using namespace facebook::react;
|
20 | 74 |
|
21 | 75 | typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
@@ -207,7 +261,13 @@ - (instancetype)init
|
207 | 261 | self.cancelsTouchesInView = NO;
|
208 | 262 | self.delaysTouchesBegan = NO; // This is default value.
|
209 | 263 | 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 | + |
211 | 271 |
|
212 | 272 | self.delegate = self;
|
213 | 273 |
|
@@ -586,4 +646,224 @@ - (void)_cancelTouches
|
586 | 646 | [self setEnabled:YES];
|
587 | 647 | }
|
588 | 648 |
|
| 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 | + |
589 | 869 | @end
|
0 commit comments