Skip to content

Commit d655dd4

Browse files
Nick LefeverSaadnajmi
authored andcommitted
[fabric] Add mouse enter/leave tracking to the View component
Summary: This diff implements mouse enter/leave tracking for the area covered by the View component. The tracking is handled by configuring a tracking area on the NSView when either of the handlers is set on the view. This enables the NSResponder `mouseEntered:` and `mouseExited:` notifications which are translated to mouse events. Test Plan: * Run Zeratul with Fabric enabled. * Move the cursor over controls that have hover-dependent styling. https://pxl.cl/4jSw5 Reviewers: shawndempsey, #rn-desktop Reviewed By: shawndempsey Differential Revision: https://phabricator.intern.facebook.com/D53529015 Tasks: T154617556
1 parent 06f4930 commit d655dd4

File tree

1 file changed

+106
-0
lines changed

1 file changed

+106
-0
lines changed

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ @implementation RCTViewComponentView {
4949
BOOL _needsInvalidateLayer;
5050
BOOL _isJSResponder;
5151
BOOL _removeClippedSubviews;
52+
BOOL _hasMouseOver; // [macOS]
53+
NSTrackingArea *_trackingArea; // [macOS]
5254
NSMutableArray<RCTUIView *> *_reactSubviews; // [macOS]
5355
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
5456
RCTPlatformView *_containerView; // [macOS]
@@ -645,6 +647,8 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
645647

646648
_needsInvalidateLayer = NO;
647649
[self invalidateLayer];
650+
651+
[self updateTrackingAreas];
648652
}
649653

650654
- (void)prepareForRecycle
@@ -1656,6 +1660,108 @@ - (void)keyUp:(NSEvent *)event {
16561660
}
16571661
}
16581662

1663+
1664+
#pragma mark - Mouse Events
1665+
1666+
- (void)sendMouseEvent:(BOOL)isMouseOver {
1667+
NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
1668+
NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
1669+
1670+
NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags;
1671+
1672+
MouseEvent mouseEvent = {
1673+
.clientX = locationInView.x,
1674+
.clientY = locationInView.y,
1675+
.screenX = locationInWindow.x,
1676+
.screenY = locationInWindow.y,
1677+
.altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
1678+
.ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
1679+
.shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
1680+
.metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
1681+
};
1682+
1683+
if (isMouseOver) {
1684+
_eventEmitter->onMouseEnter(mouseEvent);
1685+
} else {
1686+
_eventEmitter->onMouseLeave(mouseEvent);
1687+
}
1688+
}
1689+
1690+
- (void)updateMouseOverIfNeeded
1691+
{
1692+
// When an enclosing scrollview is scrolled using the scrollWheel or trackpad,
1693+
// the mouseExited: event does not get called on the view where mouseEntered: was previously called.
1694+
// This creates an unnatural pairing of mouse enter and exit events and can cause problems.
1695+
// We therefore explicitly check for this here and handle them by calling the appropriate callbacks.
1696+
1697+
BOOL hasMouseOver = _hasMouseOver;
1698+
NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
1699+
NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
1700+
BOOL insideBounds = NSPointInRect(locationInView, self.visibleRect);
1701+
1702+
// On macOS 14.0 visibleRect can be larger than the view bounds
1703+
insideBounds &= NSPointInRect(locationInView, self.bounds);
1704+
1705+
if (hasMouseOver && !insideBounds) {
1706+
hasMouseOver = NO;
1707+
} else if (!hasMouseOver && insideBounds) {
1708+
// The window's frame view must be used for hit testing against `locationInWindow`
1709+
NSView *hitView = [self.window.contentView.superview hitTest:locationInWindow];
1710+
hasMouseOver = [hitView isDescendantOf:self];
1711+
}
1712+
1713+
if (hasMouseOver != _hasMouseOver) {
1714+
_hasMouseOver = hasMouseOver;
1715+
[self sendMouseEvent:hasMouseOver];
1716+
}
1717+
}
1718+
1719+
- (void)updateTrackingAreas
1720+
{
1721+
if (_trackingArea) {
1722+
[self removeTrackingArea:_trackingArea];
1723+
}
1724+
1725+
if (
1726+
_props->macOSViewEvents[facebook::react::MacOSViewEvents::Offset::MouseEnter] ||
1727+
_props->macOSViewEvents[facebook::react::MacOSViewEvents::Offset::MouseLeave]
1728+
) {
1729+
_trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
1730+
options:NSTrackingActiveAlways | NSTrackingMouseEnteredAndExited
1731+
owner:self
1732+
userInfo:nil];
1733+
[self addTrackingArea:_trackingArea];
1734+
[self updateMouseOverIfNeeded];
1735+
}
1736+
1737+
[super updateTrackingAreas];
1738+
}
1739+
1740+
- (void)mouseEntered:(NSEvent *)event
1741+
{
1742+
if (_hasMouseOver) {
1743+
return;
1744+
}
1745+
1746+
// The window's frame view must be used for hit testing against `locationInWindow`
1747+
NSView *hitView = [self.window.contentView.superview hitTest:event.locationInWindow];
1748+
if (![hitView isDescendantOf:self]) {
1749+
return;
1750+
}
1751+
1752+
_hasMouseOver = YES;
1753+
[self sendMouseEvent:_hasMouseOver];
1754+
}
1755+
1756+
- (void)mouseExited:(NSEvent *)event
1757+
{
1758+
if (!_hasMouseOver) {
1759+
return;
1760+
}
1761+
1762+
_hasMouseOver = NO;
1763+
[self sendMouseEvent:_hasMouseOver];
1764+
}
16591765
#endif // macOS]
16601766

16611767
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point

0 commit comments

Comments
 (0)