Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor hover tracking logic to the shared C++ renderer #41519

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 5 additions & 106 deletions packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,6 @@ - (void)_dispatchActivePointers:(std::vector<ActivePointer>)activePointers event
{
for (const auto &activePointer : activePointers) {
PointerEvent pointerEvent = CreatePointerEventFromActivePointer(activePointer, eventType, _rootComponentView);
[self handleIncomingPointerEvent:pointerEvent onView:activePointer.componentView];

SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(
activePointer.componentView,
Expand All @@ -630,19 +629,13 @@ - (void)_dispatchActivePointers:(std::vector<ActivePointer>)activePointers event
}
case RCTPointerEventTypeEnd: {
eventEmitter->onPointerUp(pointerEvent);

if (pointerEvent.isPrimary && pointerEvent.button == 0 && IsPointerWithinInitialTree(activePointer)) {
eventEmitter->onClick(pointerEvent);
}

if (activePointer.shouldLeaveWhenReleased) {
[self handleIncomingPointerEvent:pointerEvent onView:nil];
}
break;
}
case RCTPointerEventTypeCancel: {
eventEmitter->onPointerCancel(pointerEvent);
[self handleIncomingPointerEvent:pointerEvent onView:nil];
break;
}
}
Expand Down Expand Up @@ -764,109 +757,15 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer
PointerEvent event = CreatePointerEventFromIncompleteHoverData(
pointerId, pointerType, clientLocation, screenLocation, offsetLocation, modifierFlags);

[self handleIncomingPointerEvent:event onView:targetView];
SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation);
if (eventEmitter != nil) {
eventEmitter->onPointerMove(event);
}
}

#pragma mark - Shared pointer handlers

/**
* Private method which is used for tracking the location of pointer events to manage the entering/leaving events.
* The primary idea is that a pointer's presence & movement is dicated by a variety of underlying events such as down,
* move, and up — and they should all be treated the same when it comes to tracking the entering & leaving of pointers
* to views. This method accomplishes that by receiving the pointer event, the target view (can be null in cases when
* the event indicates that the pointer has left the screen entirely), and a block/callback where the underlying event
* should be fired.
*/
- (NSOrderedSet<RCTReactTaggedView *> *)handleIncomingPointerEvent:(PointerEvent)event
onView:(nullable UIView *)targetView
{
NSInteger pointerId = event.pointerId;
CGPoint clientLocation = CGPointMake(event.clientPoint.x, event.clientPoint.y);

NSOrderedSet<RCTReactTaggedView *> *currentlyHoveredViews =
[_currentlyHoveredViewsPerPointer objectForKey:@(pointerId)];
if (currentlyHoveredViews == nil) {
currentlyHoveredViews = [NSOrderedSet orderedSet];
}

RCTReactTaggedView *targetTaggedView = [RCTReactTaggedView wrap:targetView];
RCTReactTaggedView *prevTargetTaggedView = [currentlyHoveredViews firstObject];
UIView *prevTargetView = prevTargetTaggedView.view;

NSOrderedSet<RCTReactTaggedView *> *eventPathViews = GetTouchableViewsInPathToRoot(targetView);

// Out
if (prevTargetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) {
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(prevTargetView, [_rootComponentView convertPoint:clientLocation toView:prevTargetView]);
if (eventEmitter != nil) {
eventEmitter->onPointerOut(event);
}
}

// Leaving

// pointerleave events need to be emitted from the deepest target to the root but
// we also need to efficiently keep track of if a view has a parent which is listening to the leave events,
// so we first iterate from the root to the target, collecting the views which need events fired for, of which
// we reverse iterate (now from target to root), actually emitting the events.
NSMutableOrderedSet<UIView *> *viewsToEmitLeaveEventsTo = [NSMutableOrderedSet orderedSet];

for (RCTReactTaggedView *taggedView in [currentlyHoveredViews reverseObjectEnumerator]) {
UIView *componentView = taggedView.view;

BOOL shouldEmitEvent = componentView != nil;

if (shouldEmitEvent && ![eventPathViews containsObject:taggedView]) {
[viewsToEmitLeaveEventsTo addObject:componentView];
}
}

for (UIView *componentView in [viewsToEmitLeaveEventsTo reverseObjectEnumerator]) {
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]);
if (eventEmitter != nil) {
eventEmitter->onPointerLeave(event);
}
}

// Over
if (targetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) {
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(targetView, [_rootComponentView convertPoint:clientLocation toView:targetView]);
if (eventEmitter != nil) {
eventEmitter->onPointerOver(event);
}
}

// Entering

// We only want to emit events to JS if there is a view that is currently listening to said event
// so we only send those event to the JS side if the element which has been entered is itself listening,
// 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
for (RCTReactTaggedView *taggedView in [eventPathViews reverseObjectEnumerator]) {
UIView *componentView = taggedView.view;

BOOL shouldEmitEvent = componentView != nil;

if (shouldEmitEvent && ![currentlyHoveredViews containsObject:taggedView]) {
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]);
if (eventEmitter != nil) {
eventEmitter->onPointerEnter(event);
}
switch (recognizer.state) {
case UIGestureRecognizerStateEnded:
eventEmitter->onPointerLeave(event);
default:
eventEmitter->onPointerMove(event);
}
}

[_currentlyHoveredViewsPerPointer setObject:eventPathViews forKey:@(pointerId)];

return eventPathViews;
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

#include "PointerEventsProcessor.h"

#include <ranges>

namespace facebook::react {

static ShadowNode::Shared getShadowNodeFromEventTarget(
jsi::Runtime& runtime,
const EventTarget& target) {
target.retain(runtime);
auto instanceHandle = target.getInstanceHandle(runtime);
target.release(runtime);
if (instanceHandle.isObject()) {
auto handleObj = instanceHandle.asObject(runtime);
if (handleObj.hasProperty(runtime, "stateNode")) {
Expand Down Expand Up @@ -220,6 +224,13 @@ void PointerEventsProcessor::interceptPointerEvent(
eventTarget = retargeted.target.get();
}

if (type == "topClick") {
// Click events are synthetic so should just be passed on instead of going
// through any sort of processing.
eventDispatcher(runtime, eventTarget, type, priority, pointerEvent);
return;
}

if (type == "topPointerDown") {
registerActivePointer(pointerEvent);
} else if (type == "topPointerMove") {
Expand All @@ -230,13 +241,37 @@ void PointerEventsProcessor::interceptPointerEvent(
}
}

eventTarget->retain(runtime);
auto shadowNode = getShadowNodeFromEventTarget(runtime, *eventTarget);
if (shadowNode != nullptr &&
shouldEmitPointerEvent(*shadowNode, type, uiManager)) {
eventDispatcher(runtime, eventTarget, type, priority, pointerEvent);
// Getting a pointerleave event from the platform is a special case telling us
// that the pointer has left the root so we don't forward the event raw but
// instead just run through our hover tracking logic with a null target.
//
// Notably: we do not forward the platform's leave event but instead will emit
// leave events through our unified hover tracking logic.
if (type == "topPointerLeave") {
handleIncomingPointerEventOnNode(
pointerEvent, nullptr, runtime, eventDispatcher, uiManager);
} else {
auto shadowNode = getShadowNodeFromEventTarget(runtime, *eventTarget);
handleIncomingPointerEventOnNode(
pointerEvent, shadowNode, runtime, eventDispatcher, uiManager);

if (shadowNode != nullptr &&
shouldEmitPointerEvent(*shadowNode, type, uiManager)) {
eventDispatcher(runtime, eventTarget, type, priority, pointerEvent);
}

// All pointercancel events and certain pointerup events (when using an
// direct pointer w/o the concept of hover) should be treated as the
// pointer leaving the device entirely so we go through our hover tracking
// logic again but pass in a null target.
auto activePointer = getActivePointer(pointerEvent.pointerId);
if (type == "topPointerCancel" ||
(type == "topPointerUp" && activePointer != nullptr &&
activePointer->shouldLeaveWhenReleased)) {
handleIncomingPointerEventOnNode(
pointerEvent, nullptr, runtime, eventDispatcher, uiManager);
}
}
eventTarget->release(runtime);

// Implicit pointer capture release
if (overrideTarget != nullptr &&
Expand Down Expand Up @@ -308,6 +343,13 @@ void PointerEventsProcessor::registerActivePointer(const PointerEvent& event) {
ActivePointer activePointer = {};
activePointer.event = event;

// If the pointer has not been tracked by the hover infrastructure then when
// the pointer is released we're gonna have to treat it as if the pointer is
// leaving the screen entirely.
activePointer.shouldLeaveWhenReleased =
previousHoverTrackersPerPointer_.find(event.pointerId) ==
previousHoverTrackersPerPointer_.end();

activePointers_[event.pointerId] = activePointer;
}

Expand Down Expand Up @@ -354,7 +396,6 @@ void PointerEventsProcessor::processPendingPointerCapture(
if (hasActiveOverride && activeOverrideTag != pendingOverrideTag) {
auto retargeted = retargetPointerEvent(event, *activeOverride, uiManager);

retargeted.target->retain(runtime);
auto shadowNode = getShadowNodeFromEventTarget(runtime, *retargeted.target);
if (shadowNode != nullptr &&
shouldEmitPointerEvent(
Expand All @@ -366,13 +407,11 @@ void PointerEventsProcessor::processPendingPointerCapture(
ReactEventPriority::Discrete,
retargeted.event);
}
retargeted.target->release(runtime);
}

if (hasPendingOverride && activeOverrideTag != pendingOverrideTag) {
auto retargeted = retargetPointerEvent(event, *pendingOverride, uiManager);

retargeted.target->retain(runtime);
auto shadowNode = getShadowNodeFromEventTarget(runtime, *retargeted.target);
if (shadowNode != nullptr &&
shouldEmitPointerEvent(
Expand All @@ -384,7 +423,6 @@ void PointerEventsProcessor::processPendingPointerCapture(
ReactEventPriority::Discrete,
retargeted.event);
}
retargeted.target->release(runtime);
}

if (!hasPendingOverride) {
Expand All @@ -394,4 +432,134 @@ void PointerEventsProcessor::processPendingPointerCapture(
}
}

void PointerEventsProcessor::handleIncomingPointerEventOnNode(
PointerEvent const& event,
ShadowNode::Shared const& targetNode,
jsi::Runtime& runtime,
DispatchEvent const& eventDispatcher,
UIManager const& uiManager) {
// Get the hover tracker from the previous event (default to null if the
// pointer hasn't been tracked before)
auto prevHoverTrackerIt =
previousHoverTrackersPerPointer_.find(event.pointerId);
PointerHoverTracker::Unique prevHoverTracker =
prevHoverTrackerIt != previousHoverTrackersPerPointer_.end()
? std::move(prevHoverTrackerIt->second)
: std::make_unique<PointerHoverTracker>(nullptr, uiManager);
// The previous tracker was stored from a previous tick so we mark it as old
prevHoverTracker->markAsOld();

auto curHoverTracker =
std::make_unique<PointerHoverTracker>(targetNode, uiManager);

// Out
if (!prevHoverTracker->hasSameTarget(*curHoverTracker) &&
prevHoverTracker->areAnyTargetsListeningToEvents(
{ViewEvents::Offset::PointerOut,
ViewEvents::Offset::PointerOutCapture},
uiManager)) {
auto prevTarget = prevHoverTracker->getTarget(uiManager);
if (prevTarget != nullptr) {
eventDispatcher(
runtime,
prevTarget->getEventEmitter()->getEventTarget().get(),
"topPointerOut",
ReactEventPriority::Discrete,
event);
}
}

// REMINDER: The order of these lists are from the root to the target
const auto [leavingNodes, enteringNodes] =
prevHoverTracker->diffEventPath(*curHoverTracker, uiManager);

// Leaving

// pointerleave events need to be emitted from the deepest target to the root
// but we also need to efficiently keep track of if a view has a parent which
// is listening to the leave events, so we first iterate from the root to the
// target, collecting the views which need events fired for, of which we
// reverse iterate (now from target to root), actually emitting the events.
bool hasParentLeaveCaptureListener = false;
std::vector<const EventTarget*> targetsToEmitLeaveTo;
for (auto nodeRef : leavingNodes) {
const auto& node = nodeRef.get();

bool hasCapturingListener = isViewListeningToEvents(
node, {ViewEvents::Offset::PointerLeaveCapture});
bool shouldEmitEvent = hasParentLeaveCaptureListener ||
hasCapturingListener ||
isViewListeningToEvents(node, {ViewEvents::Offset::PointerLeave});

if (shouldEmitEvent) {
targetsToEmitLeaveTo.emplace_back(
node.getEventEmitter()->getEventTarget().get());
}

if (hasCapturingListener && !hasParentLeaveCaptureListener) {
hasParentLeaveCaptureListener = true;
}
}

// Actually emit the leave events (in order from target to root)
for (auto it : std::ranges::reverse_view(targetsToEmitLeaveTo)) {
eventDispatcher(
runtime, it, "topPointerLeave", ReactEventPriority::Discrete, event);
}

// Over
if (!prevHoverTracker->hasSameTarget(*curHoverTracker) &&
curHoverTracker->areAnyTargetsListeningToEvents(
{ViewEvents::Offset::PointerOver,
ViewEvents::Offset::PointerOverCapture},
uiManager)) {
auto curTarget = curHoverTracker->getTarget(uiManager);
if (curTarget != nullptr) {
eventDispatcher(
runtime,
curTarget->getEventEmitter()->getEventTarget().get(),
"topPointerOver",
ReactEventPriority::Discrete,
event);
}
}

// Entering

// We want to impose the same filtering based on what events are being
// listened to as we did with leaving earlier in this function but we can emit
// the events in this loop inline since it's expected to fire the evens in
// order from root to target.
bool hasParentEnterCaptureListener = false;
for (auto nodeRef : enteringNodes) {
const auto& node = nodeRef.get();

bool hasCapturingListener = isViewListeningToEvents(
node, {ViewEvents::Offset::PointerEnterCapture});
bool shouldEmitEvent = hasParentEnterCaptureListener ||
hasCapturingListener ||
isViewListeningToEvents(node, {ViewEvents::Offset::PointerEnter});

if (shouldEmitEvent) {
eventDispatcher(
runtime,
node.getEventEmitter()->getEventTarget().get(),
"topPointerEnter",
ReactEventPriority::Discrete,
event);
}

if (hasCapturingListener && !hasParentEnterCaptureListener) {
hasParentEnterCaptureListener = true;
}
}

if (targetNode != nullptr) {
previousHoverTrackersPerPointer_[event.pointerId] =
std::move(curHoverTracker);
} else {
previousHoverTrackersPerPointer_.erase(event.pointerId);
}
}

} // namespace facebook::react
Loading