From 8e716e717861db11805af1a0e6b10d33213a5108 Mon Sep 17 00:00:00 2001 From: Vincent Riemer <1398555+vincentriemer@users.noreply.github.com> Date: Fri, 11 Aug 2023 09:26:57 -0700 Subject: [PATCH] Refactor conditional event emitting to the C++ layer (#38674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38674 Changelog: [Internal] - Refactor conditional pointer event emitting to the C++ layer Some background: early on in the implementation of Pointer Events a concern was brought up that events related to hovering pointers could saturate the JS thread if they were fired all the time unconditionally, so as a mitigation we would check in native to see if listeners in the tree were listening for those events and only fire them if there were listeners. Now since we're going to be moving some of the event derivation logic to the C++ layer we need to receive all the events — but recreate the conditional firing in the C++ layer so we can still avoid saturating the JS thread. That's what this diff does. The only change I see being potentially contraversial is the fact that I needed a way to turn an `EventTarget` (the only information I receive regarding which node the event is firing on) to its cooresponding `ShadowNode` which I did in the method `GetShadowNodeFromEventTarget`. It essentially does the exact same thing the `getNodeFromInternalInstanceHandle` method in `ReactNativePublicCompat.js`, but in C++ against the JSI API. I don't know if there's a better way to do this but this was the best one I came up with that actually works. Reviewed By: NickGerleman Differential Revision: D47852371 fbshipit-source-id: cd0df9c43f654ba48dc6bef8dc7e501275d5613c --- .../React/Fabric/RCTSurfacePointerHandler.mm | 60 +----- .../renderer/components/view/primitives.h | 4 + .../components/view/propsConversions.h | 30 ++- .../uimanager/PointerEventsProcessor.cpp | 185 ++++++++++++++++-- 4 files changed, 202 insertions(+), 77 deletions(-) diff --git a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm index 2e7338b3aebcea..dbf5c8cf85e4a1 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm @@ -396,28 +396,6 @@ static void UpdateActivePointerWithUITouch( activePointer.modifierFlags = uiEvent.modifierFlags; } -static BOOL IsViewListeningToEvent(RCTReactTaggedView *taggedView, ViewEvents::Offset eventType) -{ - UIView *view = taggedView.view; - if (view && [view.class conformsToProtocol:@protocol(RCTComponentViewProtocol)]) { - auto props = ((id)view).props; - if (SharedViewProps viewProps = std::dynamic_pointer_cast(props)) { - return viewProps->events[eventType]; - } - } - return NO; -} - -static BOOL IsAnyViewInPathListeningToEvent(NSOrderedSet *viewPath, ViewEvents::Offset eventType) -{ - for (RCTReactTaggedView *taggedView in viewPath) { - if (IsViewListeningToEvent(taggedView, eventType)) { - return YES; - } - } - return NO; -} - /** * Given an ActivePointer determine if it is still within the same event target tree as * the one which initiated the pointer gesture. @@ -634,8 +612,7 @@ - (void)_dispatchActivePointers:(std::vector)activePointers event { for (const auto &activePointer : activePointers) { PointerEvent pointerEvent = CreatePointerEventFromActivePointer(activePointer, eventType, _rootComponentView); - NSOrderedSet *eventPathViews = [self handleIncomingPointerEvent:pointerEvent - onView:activePointer.componentView]; + [self handleIncomingPointerEvent:pointerEvent onView:activePointer.componentView]; SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView( activePointer.componentView, @@ -648,12 +625,7 @@ - (void)_dispatchActivePointers:(std::vector)activePointers event break; } case RCTPointerEventTypeMove: { - BOOL hasMoveEventListeners = - IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) || - IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture); - if (hasMoveEventListeners) { - eventEmitter->onPointerMove(pointerEvent); - } + eventEmitter->onPointerMove(pointerEvent); break; } case RCTPointerEventTypeEnd: { @@ -792,11 +764,9 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer PointerEvent event = CreatePointerEventFromIncompleteHoverData( pointerId, pointerType, clientLocation, screenLocation, offsetLocation, modifierFlags); - NSOrderedSet *eventPathViews = [self handleIncomingPointerEvent:event onView:targetView]; + [self handleIncomingPointerEvent:event onView:targetView]; SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation); - BOOL hasMoveEventListeners = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) || - IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture); - if (eventEmitter != nil && hasMoveEventListeners) { + if (eventEmitter != nil) { eventEmitter->onPointerMove(event); } } @@ -831,10 +801,9 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // Out if (prevTargetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { - BOOL shouldEmitOutEvent = IsAnyViewInPathListeningToEvent(currentlyHoveredViews, ViewEvents::Offset::PointerOut); SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(prevTargetView, [_rootComponentView convertPoint:clientLocation toView:prevTargetView]); - if (shouldEmitOutEvent && eventEmitter != nil) { + if (eventEmitter != nil) { eventEmitter->onPointerOut(event); } } @@ -847,20 +816,14 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // we reverse iterate (now from target to root), actually emitting the events. NSMutableOrderedSet *viewsToEmitLeaveEventsTo = [NSMutableOrderedSet orderedSet]; - BOOL hasParentLeaveListener = NO; for (RCTReactTaggedView *taggedView in [currentlyHoveredViews reverseObjectEnumerator]) { UIView *componentView = taggedView.view; - BOOL shouldEmitEvent = componentView != nil && - (hasParentLeaveListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerLeave)); + BOOL shouldEmitEvent = componentView != nil; if (shouldEmitEvent && ![eventPathViews containsObject:taggedView]) { [viewsToEmitLeaveEventsTo addObject:componentView]; } - - if (shouldEmitEvent && !hasParentLeaveListener) { - hasParentLeaveListener = YES; - } } for (UIView *componentView in [viewsToEmitLeaveEventsTo reverseObjectEnumerator]) { @@ -873,10 +836,9 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // Over if (targetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { - BOOL shouldEmitOverEvent = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerOver); SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, [_rootComponentView convertPoint:clientLocation toView:targetView]); - if (shouldEmitOverEvent && eventEmitter != nil) { + if (eventEmitter != nil) { eventEmitter->onPointerOver(event); } } @@ -888,12 +850,10 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // or if one of its parents is listening in case those listeners care about the capturing phase. Adding the ability // for native to distinguish between capturing listeners and not could be an optimization to further reduce the number // of events we send to JS - BOOL hasParentEnterListener = NO; for (RCTReactTaggedView *taggedView in [eventPathViews reverseObjectEnumerator]) { UIView *componentView = taggedView.view; - BOOL shouldEmitEvent = componentView != nil && - (hasParentEnterListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerEnter)); + BOOL shouldEmitEvent = componentView != nil; if (shouldEmitEvent && ![currentlyHoveredViews containsObject:taggedView]) { SharedTouchEventEmitter eventEmitter = @@ -902,10 +862,6 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer eventEmitter->onPointerEnter(event); } } - - if (shouldEmitEvent && !hasParentEnterListener) { - hasParentEnterListener = YES; - } } [_currentlyHoveredViewsPerPointer setObject:eventPathViews forKey:@(pointerId)]; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index e3591f7c41f7e3..0025e28b5b575e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -62,6 +62,10 @@ struct ViewEvents { ClickCapture = 31, GotPointerCapture = 32, LostPointerCapture = 33, + PointerDown = 34, + PointerDownCapture = 35, + PointerUp = 36, + PointerUpCapture = 37, }; constexpr bool operator[](const Offset offset) const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h index 46f3801c599057..a59feff2e86efc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h @@ -616,18 +616,32 @@ static inline ViewEvents convertRawProp( "onClickCapture", sourceValue[Offset::ClickCapture], defaultValue[Offset::ClickCapture]); - result[Offset::GotPointerCapture] = convertRawProp( + result[Offset::PointerDown] = convertRawProp( context, rawProps, - "onGotPointerCapture", - sourceValue[Offset::GotPointerCapture], - defaultValue[Offset::GotPointerCapture]); - result[Offset::LostPointerCapture] = convertRawProp( + "onPointerDown", + sourceValue[Offset::PointerDown], + defaultValue[Offset::PointerDown]); + result[Offset::PointerDownCapture] = convertRawProp( context, rawProps, - "onLostPointerCapture", - sourceValue[Offset::LostPointerCapture], - defaultValue[Offset::LostPointerCapture]); + "onPointerDownCapture", + sourceValue[Offset::PointerDownCapture], + defaultValue[Offset::PointerDownCapture]); + result[Offset::PointerUp] = convertRawProp( + context, + rawProps, + "onPointerUp", + sourceValue[Offset::PointerUp], + defaultValue[Offset::PointerUp]); + result[Offset::PointerUpCapture] = convertRawProp( + context, + rawProps, + "onPointerUpCapture", + sourceValue[Offset::PointerUpCapture], + defaultValue[Offset::PointerUpCapture]); + // TODO: gotPointerCapture & lostPointerCapture (causes issues with + // RawPropsKey for some reason) // PanResponder callbacks result[Offset::MoveShouldSetResponder] = convertRawProp( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp index 0538f040aca7d0..74f6504817faba 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp @@ -7,9 +7,80 @@ #include "PointerEventsProcessor.h" +#include + namespace facebook::react { -static PointerEventTarget retargetPointerEvent( +static ShadowNode::Shared GetShadowNodeFromEventTarget( + jsi::Runtime &runtime, + EventTarget const &target) { + auto instanceHandle = target.getInstanceHandle(runtime); + if (instanceHandle.isObject()) { + auto handleObj = instanceHandle.asObject(runtime); + if (handleObj.hasProperty(runtime, "stateNode")) { + auto stateNode = handleObj.getProperty(runtime, "stateNode"); + if (stateNode.isObject()) { + auto stateNodeObj = stateNode.asObject(runtime); + if (stateNodeObj.hasProperty(runtime, "node")) { + auto node = stateNodeObj.getProperty(runtime, "node"); + return shadowNodeFromValue(runtime, node); + } + } + } + } + return nullptr; +} + +static bool IsViewListeningToEvents( + ShadowNode const &shadowNode, + std::initializer_list eventTypes) { + if (auto viewShadowNode = traitCast(&shadowNode)) { + auto &viewProps = viewShadowNode->getConcreteProps(); + for (const ViewEvents::Offset eventType : eventTypes) { + if (viewProps.events[eventType]) { + return true; + } + } + } + return false; +} + +static bool IsAnyViewInPathToRootListeningToEvents( + UIManager const &uiManager, + ShadowNode const &shadowNode, + std::initializer_list eventTypes) { + // Check the target view first + if (IsViewListeningToEvents(shadowNode, eventTypes)) { + return true; + } + + // Retrieve the node's root & a list of nodes between the target and the root + auto owningRootShadowNode = ShadowNode::Shared{}; + uiManager.getShadowTreeRegistry().visit( + shadowNode.getSurfaceId(), + [&owningRootShadowNode](ShadowTree const &shadowTree) { + owningRootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + if (owningRootShadowNode == nullptr) { + return false; + } + + auto &nodeFamily = shadowNode.getFamily(); + auto ancestors = nodeFamily.getAncestors(*owningRootShadowNode); + + // Check for listeners from the target's parent to the root + for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) { + auto ¤tNode = it->first.get(); + if (IsViewListeningToEvents(currentNode, eventTypes)) { + return true; + } + } + + return false; +} + +static PointerEventTarget RetargetPointerEvent( PointerEvent const &event, ShadowNode const &nodeToTarget, UIManager const &uiManager) { @@ -59,6 +130,72 @@ static ShadowNode::Shared getCaptureTargetOverride( return maybeTarget.lock(); } +/* + * Centralized method which determines if an event should be sent to JS by + * inspecing the listeners in the target's view path. + */ +static bool ShouldEmitPointerEvent( + ShadowNode const &targetNode, + std::string const &type, + UIManager const &uiManager) { + if (type == "topPointerDown") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerDown, + ViewEvents::Offset::PointerDownCapture}); + } else if (type == "topPointerUp") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerUp, ViewEvents::Offset::PointerUpCapture}); + } else if (type == "topPointerMove") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerMove, + ViewEvents::Offset::PointerMoveCapture}); + } else if (type == "topPointerEnter") { + // This event goes through the capturing phase in full but only bubble + // through the target and no futher up the tree + return IsViewListeningToEvents( + targetNode, {ViewEvents::Offset::PointerEnter}) || + IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerEnterCapture}); + } else if (type == "topPointerLeave") { + // This event goes through the capturing phase in full but only bubble + // through the target and no futher up the tree + return IsViewListeningToEvents( + targetNode, {ViewEvents::Offset::PointerLeave}) || + IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerLeaveCapture}); + } else if (type == "topPointerOver") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerOver, + ViewEvents::Offset::PointerOverCapture}); + } else if (type == "topPointerOut") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerOut, + ViewEvents::Offset::PointerOutCapture}); + } else if (type == "topClick") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::Click, ViewEvents::Offset::ClickCapture}); + } + // This is more of an optimization method so if we encounter a type which + // has not been specifically addressed above we should just let it through. + return true; +} + void PointerEventsProcessor::interceptPointerEvent( jsi::Runtime &runtime, EventTarget const *target, @@ -79,14 +216,18 @@ void PointerEventsProcessor::interceptPointerEvent( if (overrideTarget != nullptr && overrideTarget->getTag() != eventTarget->getTag()) { auto retargeted = - retargetPointerEvent(pointerEvent, *overrideTarget, uiManager); + RetargetPointerEvent(pointerEvent, *overrideTarget, uiManager); pointerEvent = retargeted.event; eventTarget = retargeted.target.get(); } eventTarget->retain(runtime); - eventDispatcher(runtime, eventTarget, type, priority, pointerEvent); + auto shadowNode = GetShadowNodeFromEventTarget(runtime, *eventTarget); + if (shadowNode != nullptr && + ShouldEmitPointerEvent(*shadowNode, type, uiManager)) { + eventDispatcher(runtime, eventTarget, type, priority, pointerEvent); + } eventTarget->release(runtime); // Implicit pointer capture release @@ -156,28 +297,38 @@ void PointerEventsProcessor::processPendingPointerCapture( auto activeOverrideTag = (hasActiveOverride) ? activeOverride->getTag() : -1; if (hasActiveOverride && activeOverrideTag != pendingOverrideTag) { - auto retargeted = retargetPointerEvent(event, *activeOverride, uiManager); + auto retargeted = RetargetPointerEvent(event, *activeOverride, uiManager); retargeted.target->retain(runtime); - eventDispatcher( - runtime, - retargeted.target.get(), - "topLostPointerCapture", - ReactEventPriority::Discrete, - retargeted.event); + auto shadowNode = GetShadowNodeFromEventTarget(runtime, *retargeted.target); + if (shadowNode != nullptr && + ShouldEmitPointerEvent( + *shadowNode, "topLostPointerCapture", uiManager)) { + eventDispatcher( + runtime, + retargeted.target.get(), + "topLostPointerCapture", + ReactEventPriority::Discrete, + retargeted.event); + } retargeted.target->release(runtime); } if (hasPendingOverride && activeOverrideTag != pendingOverrideTag) { - auto retargeted = retargetPointerEvent(event, *pendingOverride, uiManager); + auto retargeted = RetargetPointerEvent(event, *pendingOverride, uiManager); retargeted.target->retain(runtime); - eventDispatcher( - runtime, - retargeted.target.get(), - "topGotPointerCapture", - ReactEventPriority::Discrete, - retargeted.event); + auto shadowNode = GetShadowNodeFromEventTarget(runtime, *retargeted.target); + if (shadowNode != nullptr && + ShouldEmitPointerEvent( + *shadowNode, "topGotPointerCapture", uiManager)) { + eventDispatcher( + runtime, + retargeted.target.get(), + "topGotPointerCapture", + ReactEventPriority::Discrete, + retargeted.event); + } retargeted.target->release(runtime); }