Skip to content

Commit ea52bb4

Browse files
committed
Merge remote-tracking branch 'origin/mouse' into drag-drop
2 parents c49d7b2 + 9d2ec8f commit ea52bb4

File tree

7 files changed

+259
-2
lines changed

7 files changed

+259
-2
lines changed

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

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ @implementation RCTViewComponentView {
4949
BOOL _needsInvalidateLayer;
5050
BOOL _isJSResponder;
5151
BOOL _removeClippedSubviews;
52+
#if TARGET_OS_OSX // [macOS
53+
BOOL _hasMouseOver;
54+
BOOL _hasClipViewBoundsObserver;
55+
NSTrackingArea *_trackingArea;
56+
#endif // macOS]
5257
NSMutableArray<RCTUIView *> *_reactSubviews; // [macOS]
5358
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
5459
RCTPlatformView *_containerView; // [macOS]
@@ -645,6 +650,11 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
645650

646651
_needsInvalidateLayer = NO;
647652
[self invalidateLayer];
653+
654+
#if TARGET_OS_OSX // [macOS
655+
[self updateTrackingAreas];
656+
[self updateClipViewBoundsObserverIfNeeded];
657+
#endif // macOS]
648658
}
649659

650660
- (void)prepareForRecycle
@@ -1630,7 +1640,7 @@ - (BOOL)handleKeyboardEvent:(NSEvent *)event {
16301640
static const char kRCTViewKeyboardEventEmittedKey = 0;
16311641
NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey);
16321642
BOOL alreadyEmitted = [emitted boolValue];
1633-
if (!alreadyEmitted) {
1643+
if (!alreadyEmitted && _eventEmitter) {
16341644
if (event.type == NSEventTypeKeyDown) {
16351645
_eventEmitter->onKeyDown(keyEvent);
16361646
} else {
@@ -1656,6 +1666,147 @@ - (void)keyUp:(NSEvent *)event {
16561666
}
16571667
}
16581668

1669+
1670+
#pragma mark - Mouse Events
1671+
1672+
- (void)emitMouseEvent {
1673+
if (!_eventEmitter) {
1674+
return;
1675+
}
1676+
1677+
NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
1678+
NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
1679+
1680+
NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags;
1681+
1682+
MouseEvent mouseEvent = {
1683+
.clientX = locationInView.x,
1684+
.clientY = locationInView.y,
1685+
.screenX = locationInWindow.x,
1686+
.screenY = locationInWindow.y,
1687+
.altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
1688+
.ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
1689+
.shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
1690+
.metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
1691+
};
1692+
1693+
if (_hasMouseOver) {
1694+
_eventEmitter->onMouseEnter(mouseEvent);
1695+
} else {
1696+
_eventEmitter->onMouseLeave(mouseEvent);
1697+
}
1698+
}
1699+
1700+
- (void)updateMouseOverIfNeeded
1701+
{
1702+
// When an enclosing scrollview is scrolled using the scrollWheel or trackpad,
1703+
// the mouseExited: event does not get called on the view where mouseEntered: was previously called.
1704+
// This creates an unnatural pairing of mouse enter and exit events and can cause problems.
1705+
// We therefore explicitly check for this here and handle them by calling the appropriate callbacks.
1706+
1707+
BOOL hasMouseOver = _hasMouseOver;
1708+
NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
1709+
NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
1710+
BOOL insideBounds = NSPointInRect(locationInView, self.visibleRect);
1711+
1712+
// On macOS 14+ visibleRect can be larger than the view bounds
1713+
insideBounds &= NSPointInRect(locationInView, self.bounds);
1714+
1715+
if (hasMouseOver && !insideBounds) {
1716+
hasMouseOver = NO;
1717+
} else if (!hasMouseOver && insideBounds) {
1718+
// The window's frame view must be used for hit testing against `locationInWindow`
1719+
NSView *hitView = [self.window.contentView.superview hitTest:locationInWindow];
1720+
hasMouseOver = [hitView isDescendantOf:self];
1721+
}
1722+
1723+
if (hasMouseOver != _hasMouseOver) {
1724+
_hasMouseOver = hasMouseOver;
1725+
[self emitMouseEvent];
1726+
}
1727+
}
1728+
1729+
- (void)updateClipViewBoundsObserverIfNeeded
1730+
{
1731+
// Subscribe to view bounds changed notification so that the view can be notified when a
1732+
// scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event
1733+
// both of which would not cause the mouseExited to be invoked.
1734+
1735+
NSClipView *clipView = self.window ? self.enclosingScrollView.contentView : nil;
1736+
1737+
BOOL hasMouseEventHandler =
1738+
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
1739+
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];
1740+
1741+
if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) {
1742+
_hasClipViewBoundsObserver = NO;
1743+
[[NSNotificationCenter defaultCenter] removeObserver:self
1744+
name:NSViewBoundsDidChangeNotification
1745+
object:nil];
1746+
} else if (!_hasClipViewBoundsObserver && clipView && hasMouseEventHandler) {
1747+
_hasClipViewBoundsObserver = YES;
1748+
[[NSNotificationCenter defaultCenter] addObserver:self
1749+
selector:@selector(updateMouseOverIfNeeded)
1750+
name:NSViewBoundsDidChangeNotification
1751+
object:clipView];
1752+
[self updateMouseOverIfNeeded];
1753+
}
1754+
}
1755+
1756+
- (void)viewDidMoveToWindow
1757+
{
1758+
[self updateClipViewBoundsObserverIfNeeded];
1759+
[super viewDidMoveToWindow];
1760+
}
1761+
1762+
- (void)updateTrackingAreas
1763+
{
1764+
BOOL hasMouseEventHandler =
1765+
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
1766+
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];
1767+
BOOL wouldRecreateIdenticalTrackingArea =
1768+
hasMouseEventHandler && _trackingArea && NSEqualRects(self.bounds, [_trackingArea rect]);
1769+
1770+
if (!wouldRecreateIdenticalTrackingArea) {
1771+
[self removeTrackingArea:_trackingArea];
1772+
if (hasMouseEventHandler) {
1773+
_trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
1774+
options:NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited
1775+
owner:self
1776+
userInfo:nil];
1777+
[self addTrackingArea:_trackingArea];
1778+
[self updateMouseOverIfNeeded];
1779+
}
1780+
}
1781+
1782+
[super updateTrackingAreas];
1783+
}
1784+
1785+
- (void)mouseEntered:(NSEvent *)event
1786+
{
1787+
if (_hasMouseOver) {
1788+
return;
1789+
}
1790+
1791+
// The window's frame view must be used for hit testing against `locationInWindow`
1792+
NSView *hitView = [self.window.contentView.superview hitTest:event.locationInWindow];
1793+
if (![hitView isDescendantOf:self]) {
1794+
return;
1795+
}
1796+
1797+
_hasMouseOver = YES;
1798+
[self emitMouseEvent];
1799+
}
1800+
1801+
- (void)mouseExited:(NSEvent *)event
1802+
{
1803+
if (!_hasMouseOver) {
1804+
return;
1805+
}
1806+
1807+
_hasMouseOver = NO;
1808+
[self emitMouseEvent];
1809+
}
16591810
#endif // macOS]
16601811

16611812
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,31 @@ void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const {
5050
});
5151
}
5252

53+
#pragma mark - Mouse Events
54+
55+
static jsi::Value mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& event) {
56+
auto payload = jsi::Object(runtime);
57+
payload.setProperty(runtime, "clientX", event.clientX);
58+
payload.setProperty(runtime, "clientY", event.clientY);
59+
payload.setProperty(runtime, "screenX", event.screenX);
60+
payload.setProperty(runtime, "screenY", event.screenY);
61+
payload.setProperty(runtime, "altKey", event.altKey);
62+
payload.setProperty(runtime, "ctrlKey", event.ctrlKey);
63+
payload.setProperty(runtime, "shiftKey", event.shiftKey);
64+
payload.setProperty(runtime, "metaKey", event.metaKey);
65+
return payload;
66+
};
67+
68+
void HostPlatformViewEventEmitter::onMouseEnter(const MouseEvent& mouseEvent) const {
69+
dispatchEvent("mouseEnter", [mouseEvent](jsi::Runtime &runtime) {
70+
return mouseEventPayload(runtime, mouseEvent);
71+
});
72+
}
73+
74+
void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) const {
75+
dispatchEvent("mouseLeave", [mouseEvent](jsi::Runtime &runtime) {
76+
return mouseEventPayload(runtime, mouseEvent);
77+
});
78+
}
79+
5380
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include <react/renderer/components/view/BaseViewEventEmitter.h>
1313
#include <react/renderer/components/view/KeyEvent.h>
14+
#include <react/renderer/components/view/MouseEvent.h>
1415

1516
namespace facebook::react {
1617

@@ -27,6 +28,11 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter {
2728

2829
void onKeyDown(KeyEvent const &keyEvent) const;
2930
void onKeyUp(KeyEvent const &keyEvent) const;
31+
32+
#pragma mark - Mouse Events
33+
34+
void onMouseEnter(MouseEvent const &mouseEvent) const;
35+
void onMouseLeave(MouseEvent const &mouseEvent) const;
3036
};
3137

3238
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ struct HostPlatformViewEvents {
2525
// Keyboard Events
2626
KeyDown = 2,
2727
KeyUp = 3,
28+
29+
// Mouse Events
30+
MouseEnter = 2,
31+
MouseLeave = 3,
2832
};
2933

3034
constexpr bool operator[](const Offset offset) const {
@@ -57,10 +61,16 @@ static inline HostPlatformViewEvents convertRawProp(
5761
convertRawProp(context, rawProps, "onFocus", sourceValue[Offset::Focus], defaultValue[Offset::Focus]);
5862
result[Offset::Blur] =
5963
convertRawProp(context, rawProps, "onBlur", sourceValue[Offset::Blur], defaultValue[Offset::Blur]);
64+
// Keyboard Events
6065
result[Offset::KeyDown] =
6166
convertRawProp(context, rawProps, "onKeyDown", sourceValue[Offset::KeyDown], defaultValue[Offset::KeyDown]);
6267
result[Offset::KeyUp] =
6368
convertRawProp(context, rawProps, "onKeyUp", sourceValue[Offset::KeyUp], defaultValue[Offset::KeyUp]);
69+
// Mouse Events
70+
result[Offset::MouseEnter] =
71+
convertRawProp(context, rawProps, "onMouseEnter", sourceValue[Offset::MouseEnter], defaultValue[Offset::MouseEnter]);
72+
result[Offset::MouseLeave] =
73+
convertRawProp(context, rawProps, "onMouseLeave", sourceValue[Offset::MouseLeave], defaultValue[Offset::MouseLeave]);
6474

6575
return result;
6676
}

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ void HostPlatformViewProps::setProp(
9696
VIEW_EVENT_CASE_MACOS(Blur);
9797
VIEW_EVENT_CASE_MACOS(KeyDown);
9898
VIEW_EVENT_CASE_MACOS(KeyUp);
99+
VIEW_EVENT_CASE_MACOS(MouseEnter);
100+
VIEW_EVENT_CASE_MACOS(MouseLeave);
99101
RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
100102
RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing);
101103
RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents);

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ inline bool formsStackingContext(const ViewProps& props) {
1717
}
1818

1919
inline bool formsView(const ViewProps& props) {
20-
return props.focusable;
20+
return props.focusable ||
21+
props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
22+
props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];
2123
}
2224

2325
} // namespace facebook::react::HostPlatformViewTraitsInitializer
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <react/renderer/graphics/Geometry.h>
11+
12+
namespace facebook::react {
13+
14+
/*
15+
* Describes a mouse enter/leave event.
16+
*/
17+
struct MouseEvent {
18+
/**
19+
* Pointer horizontal location in target view.
20+
*/
21+
Float clientX{0};
22+
23+
/**
24+
* Pointer vertical location in target view.
25+
*/
26+
Float clientY{0};
27+
28+
/**
29+
* Pointer horizontal location in window.
30+
*/
31+
Float screenX{0};
32+
33+
/**
34+
* Pointer vertical location in window.
35+
*/
36+
Float screenY{0};
37+
38+
/*
39+
* A flag indicating if the alt key is pressed.
40+
*/
41+
bool altKey{false};
42+
43+
/*
44+
* A flag indicating if the control key is pressed.
45+
*/
46+
bool ctrlKey{false};
47+
48+
/*
49+
* A flag indicating if the shift key is pressed.
50+
*/
51+
bool shiftKey{false};
52+
53+
/*
54+
* A flag indicating if the meta key is pressed.
55+
*/
56+
bool metaKey{false};
57+
};
58+
59+
} // namespace facebook::react

0 commit comments

Comments
 (0)