Skip to content

Commit

Permalink
Refactor hover tracking logic to the shared C++ renderer (#41519)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #41519

Changelog: [Internal] - Refactor hover tracking logic to the shared c++ renderer

This diff refactors our hover tracking logic out of the platform (in this case only iOS, integrating Android is going to require extra work due to their pointer events being untyped) and puts it into the event intercepting infra in Fabric's C++ core. This is a big-ish diff so I'm going to try my best to use this summary to guide you through the changes.

To begin with — the changes inside `RCTSurfacePointerHandler.mm` are mostly about removing the existing hover tracking logic. The logic of the hover tracking is largely the same between the objective-c and c++ implementations with minor tweaks in order to be a better citizen when it comes to storing node references. One small "addition" to this file is explicitly firing a `pointerleave` event from the iOS layer when we detect that the pointer has left the app entirely because the C++ would otherwise not know when the pointer leaves the app. We don't need to include a special case for `pointerenter` because we can derive that in C++ from the first `pointermove` event that gets sent once the pointer re-enters the app's root view.

Next I think it makes most sense to continue onto the `PointerEventsProcessor.h/mm` which is the central class we're working in. One small change is adding a flag to the `ActivePointer` struct (`shouldLeaveWhenReleased`) which we will set during `ActivePointer` registration — setting to false if the pointer in question exists in the (also) newly added `previousHoverTrackersPerPointer_` registry. This logic is primarily used for knowing later when the pointer is released and whether we should emit the synthetic leave/out event on release. If the pointer existed in `previousHoverTrackersPerPointer_` **before** the `ActivePointer` registration that implies that the pointer is capable of hovering to some degree and we should **not** emit those leave/out events yet.

The real meat & potatoes of this diff is the `handleIncomingPointerEventOnNode` method which matches the `handleIncomingPointerEvent` method we removed from `RCTSurfacePointerHandler.mm`. This method derives the enter/leave/over/out pointer events by comparing the current event's target path (list of nodes from the root node to the target node of the event) to the previously recorded event target path. The representation of this event path is through the new `PointerHoverTracker` class which stores a pointer to just the root node and the target node as we can recreate the entire event path from these.

For over/out events all that matters is when the deepest-most target changes which is checked in `handleIncomingPointerEventOnNode` by leveraging `PointerHoverTracker`'s `hasSameTarget` method. For enter/leave events we need to fire discrete events for every node in the path which has either been removed or added, so the `diffEventPath` method was introduced on `PointerHoverTracker` to provide that.

Reviewed By: yungsters

Differential Revision: D51317492

fbshipit-source-id: e15ac3a396d5afa7ab921e4589861b43b07a33b5
  • Loading branch information
vincentriemer authored and facebook-github-bot committed Dec 21, 2023
1 parent 3b80531 commit 72b876a
Show file tree
Hide file tree
Showing 8 changed files with 881 additions and 176 deletions.
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
Loading

0 comments on commit 72b876a

Please sign in to comment.