From 5a7a39ec6d0c8bceceba8210cf4ca91c19f2523b Mon Sep 17 00:00:00 2001 From: Adam Gleitman Date: Wed, 26 Jun 2024 14:48:25 -0700 Subject: [PATCH] Use *shadow* view traversal for handling nested mouse events --- .../Libraries/Text/Text/RCTTextView.mm | 16 +++++++++++++--- .../react-native/React/Views/RCTShadowView.h | 7 +++++++ .../react-native/React/Views/RCTShadowView.m | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.mm b/packages/react-native/Libraries/Text/Text/RCTTextView.mm index 3f20a4cf5a994d..5c78e25db8d960 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.mm @@ -12,6 +12,7 @@ #endif // [macOS] #import // [macOS] +#import // [macOS] #import #import #import // [macOS] @@ -506,13 +507,20 @@ - (void)updateHoveredSubviewWithEvent:(NSEvent *)event // self will always be an ancestor of any views we pass in here, so it serves as a good default option. // Also, if we do set from/to nil, we have to call the relevant events on the entire subtree. - RCTPlatformView *commonAncestor = [(_currentHoveredSubview ?: self) ancestorSharedWithView:(hoveredView ?: self)] ?: self; + RCTUIManager *uiManager = [[_eventDispatcher bridge] uiManager]; + RCTShadowView *oldShadowView = [uiManager shadowViewForReactTag:[(_currentHoveredSubview ?: self) reactTag]]; + RCTShadowView *newShadowView = [uiManager shadowViewForReactTag:[(hoveredView ?: self) reactTag]]; - for (RCTPlatformView *exitedView = _currentHoveredSubview; exitedView != commonAncestor && exitedView != nil; exitedView = [exitedView superview]) { + // Find the common ancestor between the two shadow views + RCTShadowView *commonAncestor = [oldShadowView ancestorSharedWithShadowView:newShadowView]; + + for (RCTShadowView *exitedShadowView = oldShadowView; exitedShadowView != commonAncestor && exitedShadowView != nil; exitedShadowView = [exitedShadowView reactSuperview]) { + RCTPlatformView *exitedView = [uiManager viewForReactTag:[exitedShadowView reactTag]]; if (![exitedView isKindOfClass:[RCTUIView class]]) { RCTLogError(@"Unexpected view of type %@ found in hierarchy, must be RCTUIView or subclass", [exitedView class]); continue; } + RCTUIView *exitedReactView = (RCTUIView *)exitedView; [self sendMouseEventWithBlock:[exitedReactView onMouseLeave] locationInfo:[self locationInfoFromEvent:event] @@ -522,11 +530,13 @@ - (void)updateHoveredSubviewWithEvent:(NSEvent *)event // We cache these so we can call them from outermost to innermost NSMutableArray *enteredViewHierarchy = [NSMutableArray new]; - for (RCTPlatformView *enteredView = hoveredView; enteredView != commonAncestor && enteredView != nil; enteredView = [enteredView superview]) { + for (RCTShadowView *enteredShadowView = newShadowView; enteredShadowView != commonAncestor && enteredShadowView != nil; enteredShadowView = [enteredShadowView reactSuperview]) { + RCTPlatformView *enteredView = [uiManager viewForReactTag:[enteredShadowView reactTag]]; if (![enteredView isKindOfClass:[RCTUIView class]]) { RCTLogError(@"Unexpected view of type %@ found in hierarchy, must be RCTUIView or subclass", [enteredView class]); continue; } + [enteredViewHierarchy addObject:(RCTUIView *)enteredView]; } for (NSInteger i = [enteredViewHierarchy count] - 1; i >= 0; i--) { diff --git a/packages/react-native/React/Views/RCTShadowView.h b/packages/react-native/React/Views/RCTShadowView.h index 8c7567076bb309..e02c71bef76c0c 100644 --- a/packages/react-native/React/Views/RCTShadowView.h +++ b/packages/react-native/React/Views/RCTShadowView.h @@ -241,6 +241,13 @@ typedef void (^RCTApplierBlock)(NSDictionary *vie */ - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor; +// [macOS +/** + * Returns the closest ancestor shared by this shadow view and another specified shadow view. + */ +- (RCTShadowView *)ancestorSharedWithShadowView:(RCTShadowView *)shadowView; +// macOS] + /** * Checks if the current shadow view is a descendant of the provided `ancestor` */ diff --git a/packages/react-native/React/Views/RCTShadowView.m b/packages/react-native/React/Views/RCTShadowView.m index 802d6b6da8d149..c54940a2212866 100644 --- a/packages/react-native/React/Views/RCTShadowView.m +++ b/packages/react-native/React/Views/RCTShadowView.m @@ -182,6 +182,25 @@ - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor return (CGRect){offset, self.layoutMetrics.frame.size}; } +// [macOS +- (RCTShadowView *)ancestorSharedWithShadowView:(RCTShadowView *)shadowView +{ + // TODO: This can be optimized by climbing up both hierarchies at the same time + NSMutableSet *selfSuperviews = [NSMutableSet set]; + for (RCTShadowView *view = self; view != nil; view = [view reactSuperview]) { + [selfSuperviews addObject:view]; + } + + for (RCTShadowView *candidateView = shadowView; candidateView != nil; candidateView = [candidateView reactSuperview]) { + if ([selfSuperviews containsObject:candidateView]) { + return candidateView; + } + } + + return nil; +} +// macOS] + - (BOOL)viewIsDescendantOf:(RCTShadowView *)ancestor { RCTShadowView *shadowView = self;