From 62ee7379e61a188cd78c17cafa97d8308515385d Mon Sep 17 00:00:00 2001 From: Vincent Riemer <1398555+vincentriemer@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:37:16 -0700 Subject: [PATCH] Refactor conditional event emitting to the C++ layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: 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. Differential Revision: D47852371 fbshipit-source-id: f5d4e34e4a4ea11a58e5b667cf59b7fb946b99f8 --- .../React/Fabric/RCTSurfacePointerHandler.mm | 60 +----- .../renderer/components/view/primitives.h | 4 + .../components/view/propsConversions.h | 30 ++- .../uimanager/PointerEventsProcessor.cpp | 177 ++++++++++++++++-- 4 files changed, 198 insertions(+), 73 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 ac418daa87fef8..a6c91fed3d08e5 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 6aabbf23835d97..9ab8709e8af8f8 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 4c1915cc5db29e..b0b4485936cb4e 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp @@ -9,6 +9,77 @@ namespace facebook::react { +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 (shadowNode.getTraits().check(ShadowNodeTraits::Trait::ViewKind)) { + auto props = shadowNode.getProps(); + auto viewProps = static_cast(*props); + + 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, @@ -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, @@ -85,7 +222,11 @@ void PointerEventsProcessor::interceptPointerEvent( } eventTarget->retain(runtime); - eventDispatcher(runtime, eventTarget, type, priority, event); + auto shadowNode = GetShadowNodeFromEventTarget(runtime, *eventTarget); + if (shadowNode != nullptr && + ShouldEmitPointerEvent(*shadowNode, type, uiManager)) { + eventDispatcher(runtime, eventTarget, type, priority, event); + } eventTarget->release(runtime); // Implicit pointer capture release @@ -157,12 +298,17 @@ void PointerEventsProcessor::processPendingPointerCapture( 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); } @@ -170,12 +316,17 @@ void PointerEventsProcessor::processPendingPointerCapture( 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); }