diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 7470099842..1bf17b3dc1 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -259,8 +259,10 @@ var TouchableOpacity = createReactClass({ onResponderMove={this.touchableHandleResponderMove} onResponderRelease={this.touchableHandleResponderRelease} onResponderTerminate={this.touchableHandleResponderTerminate} - onMouseEnter={this.props.onMouseEnter} - onMouseLeave={this.props.onMouseLeave} + onMouseMove={this.props.onMouseMove} + onMouseOver={this.props.onMouseOver} + onMouseOut={this.props.onMouseOut} + onContextMenu={this.props.onContextMenu} onContextMenuItemClick={this.props.onContextMenuItemClick} contextMenu={this.props.contextMenu}> {this.props.children} diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index b55f63eb42..390426fa8c 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -110,8 +110,10 @@ const TouchableWithoutFeedback = createReactClass({ * views. */ hitSlop: EdgeInsetsPropType, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, + onMouseMove: PropTypes.func, + onMouseOver: PropTypes.func, + onMouseOut: PropTypes.func, + onContextMenu: PropTypes.func, onContextMenuItemClick: PropTypes.func, contextMenu: PropTypes.array, }, @@ -213,8 +215,10 @@ const TouchableWithoutFeedback = createReactClass({ onResponderMove: this.touchableHandleResponderMove, onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate, - onMouseEnter: this.props.onMouseEnter, - onMouseLeave: this.props.onMouseLeave, + onMouseMove: this.props.onMouseMove, + onMouseOver: this.props.onMouseOver, + onMouseOut: this.props.onMouseOut, + onContextMenu: this.props.onContextMenu, onContextMenuItemClick: this.props.onContextMenuItemClick, contextMenu: this.props.contextMenu, style, diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap index 091bc6e748..07852b5d88 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap @@ -11,8 +11,8 @@ exports[`TouchableHighlight renders correctly 1`] = ` isTVSelectable={true} nativeID={undefined} onLayout={undefined} - onMouseEnter={undefined} - onMouseLeave={undefined} + onMouseOver={undefined} + onMouseOut={undefined} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index 2648ff966b..bad80fe220 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -30,8 +30,10 @@ ReactNativeViewAttributes.UIView = { renderToHardwareTextureAndroid: true, shouldRasterizeIOS: true, onLayout: true, - onMouseEnter: true, - onMouseLeave: true, + onMouseMove: true, + onMouseOver: true, + onMouseOut: true, + onContextMenu: true, onAccessibilityTap: true, onMagicTap: true, collapsable: true, diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index d9771b890a..1a1254b9ff 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -433,11 +433,13 @@ module.exports = { * Desktop specific events * @platform macos */ - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, + onMouseMove: PropTypes.func, + onMouseOver: PropTypes.func, + onMouseOut: PropTypes.func, onDragEnter: PropTypes.func, onDragLeave: PropTypes.func, onDrop: PropTypes.func, + onContextMenu: PropTypes.func, onContextMenuItemClick: PropTypes.func, /** * Mapped to toolTip property of NSView. Used to show extra information when diff --git a/RNTester/RNTester/AppDelegate.m b/RNTester/RNTester/AppDelegate.m index 2c5edb6987..6bbece42f6 100644 --- a/RNTester/RNTester/AppDelegate.m +++ b/RNTester/RNTester/AppDelegate.m @@ -21,6 +21,7 @@ #import #import #import +#import @interface AppDelegate() @@ -28,54 +29,33 @@ @interface AppDelegate() @implementation AppDelegate - --(id)init { - if(self = [super init]) { - - // -- Init Window - NSRect contentSize = NSMakeRect(200, 500, 1000, 500); - - self.window = [[NSWindow alloc] initWithContentRect:contentSize - styleMask:NSTitledWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask | NSClosableWindowMask - backing:NSBackingStoreBuffered - defer:NO]; - NSWindowController *windowController = [[NSWindowController alloc] initWithWindow:self.window]; - - [[self window] setTitle:@"RNTester"]; - [[self window] setTitleVisibility:NSWindowTitleHidden]; - [windowController showWindow:self.window]; - - [windowController setShouldCascadeWindows:NO]; - [windowController setWindowFrameAutosaveName:@"RNTester"]; - [self setDefaultURL]; - - // -- Init Toolbar - NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"mainToolbar"]; - [toolbar setDelegate:self]; - [toolbar setSizeMode:NSToolbarSizeModeRegular]; - - [self.window setToolbar:toolbar]; - - // -- Init Menu - [self setUpMainMenu]; - } - return self; + NSToolbar *_toolbar; } - (void)applicationDidFinishLaunching:(NSNotification * __unused)aNotification { + [self setDefaultURL]; _bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:@{@"argv": [self argv]}]; - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge - moduleName:@"RNTesterApp" - initialProperties:nil]; + _window = [[RCTWindow alloc] initWithBridge:_bridge + contentRect:NSMakeRect(200, 500, 1000, 500) + styleMask:(NSTitledWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask | NSClosableWindowMask) + defer:NO]; + + _window.title = @"RNTester"; + _window.titleVisibility = NSWindowTitleHidden; + [self setUpToolbar]; + [self setUpMainMenu]; + _window.contentView = [[RCTRootView alloc] initWithBridge:_bridge + moduleName:@"RNTesterApp" + initialProperties:nil]; - [self.window setContentView:rootView]; + [_window makeKeyAndOrderFront:nil]; } - (void)setDefaultURL @@ -105,6 +85,14 @@ - (void)loadSourceForBridge:(RCTBridge *)bridge onComplete:loadCallback]; } +- (void)setUpToolbar +{ + NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"mainToolbar"]; + toolbar.delegate = self; + toolbar.sizeMode = NSToolbarSizeModeRegular; + _window.toolbar = toolbar; +} + - (NSArray *)toolbarAllowedItemIdentifiers:(__unused NSToolbar *)toolbar { return @[NSToolbarFlexibleSpaceItemIdentifier, @"searchBar", NSToolbarFlexibleSpaceItemIdentifier, @"resetButton"]; diff --git a/RNTester/js/DragnDropExample.macos.js b/RNTester/js/DragnDropExample.macos.js index 43eab54820..221960f6bc 100644 --- a/RNTester/js/DragnDropExample.macos.js +++ b/RNTester/js/DragnDropExample.macos.js @@ -41,8 +41,8 @@ class DragExample extends React.Component { this.state.mouseOver ? 'orange' : 'white', padding: 40, alignItems: 'center'}} draggedTypes={['NSFilenamesPboardType']} - onMouseEnter={() => this.setState({mouseOver: true})} - onMouseLeave={() => this.setState({mouseOver: false})} + onMouseOver={() => this.setState({mouseOver: true})} + onMouseOut={() => this.setState({mouseOver: false})} onDragEnter={() => this.setState({dragOver: true})} onDragLeave={() => this.setState({dragOver: false})} onDrop={(e) => this.setState({files: e.nativeEvent.files, dragOver: false})}> diff --git a/React/Base/RCTMouseEvent.h b/React/Base/RCTMouseEvent.h new file mode 100644 index 0000000000..2023d096c4 --- /dev/null +++ b/React/Base/RCTMouseEvent.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@interface RCTMouseEvent : NSObject + +- (instancetype)initWithEventName:(NSString *)eventName + target:(NSNumber *)target + userInfo:(NSDictionary *)userInfo + coalescingKey:(uint16_t)coalescingKey NS_DESIGNATED_INITIALIZER; + +@property (readonly) NSTimeInterval timestamp; + +@end diff --git a/React/Base/RCTMouseEvent.m b/React/Base/RCTMouseEvent.m new file mode 100644 index 0000000000..8aa130dce7 --- /dev/null +++ b/React/Base/RCTMouseEvent.m @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTMouseEvent.h" + +#import "RCTAssert.h" + +@implementation RCTMouseEvent +{ + NSDictionary *_userInfo; + uint16_t _coalescingKey; +} + +@synthesize eventName = _eventName; +@synthesize viewTag = _viewTag; + +- (instancetype)initWithEventName:(NSString *)eventName + target:(NSNumber *)target + userInfo:(NSDictionary *)userInfo + coalescingKey:(uint16_t)coalescingKey +{ + if (self = [super init]) { + _viewTag = target; + _userInfo = [NSDictionary dictionaryWithDictionary:userInfo]; + _eventName = eventName; + _coalescingKey = coalescingKey; + } + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)init) + +#pragma mark - RCTEvent + +- (BOOL)canCoalesce +{ + return [_eventName isEqual:@"mouseMove"]; +} + +// We coalesce only move events, while holding some assumptions that seem reasonable but there are no explicit guarantees about them. +- (id)coalesceWithEvent:(id)newEvent +{ + RCTAssert([newEvent isKindOfClass:[RCTMouseEvent class]], @"Mouse event cannot be coalesced with any other type of event, such as provided %@", newEvent); + return ((RCTMouseEvent *)newEvent).timestamp > self.timestamp ? newEvent : self; +} + ++ (NSString *)moduleDotMethod +{ + return @"RCTEventEmitter.receiveEvent"; +} + +- (NSArray *)arguments +{ + return @[_viewTag, RCTNormalizeInputEventName(_eventName), _userInfo]; +} + +- (uint16_t)coalescingKey +{ + return _coalescingKey; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p; name = %@; coalescing key = %hu>", [self class], self, _eventName, _coalescingKey]; +} + +- (NSTimeInterval)timestamp +{ + return [_userInfo[@"timestamp"] doubleValue]; +} + +@end diff --git a/React/Base/RCTRootContentView.m b/React/Base/RCTRootContentView.m index 06f17326cf..88834b49ce 100644 --- a/React/Base/RCTRootContentView.m +++ b/React/Base/RCTRootContentView.m @@ -15,6 +15,7 @@ #import "RCTRootViewInternal.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" +#import "RCTWindow.h" #import "NSView+React.h" @implementation RCTRootContentView @@ -28,8 +29,6 @@ - (instancetype)initWithFrame:(CGRect)frame _bridge = bridge; self.reactTag = reactTag; _sizeFlexibility = sizeFlexibility; - _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; - [_touchHandler attachToView:self]; [_bridge.uiManager registerRootView:self]; } return self; @@ -85,15 +84,13 @@ - (void)updateAvailableSize [_bridge.uiManager setAvailableSize:self.availableSize forRootView:self]; } -- (NSView *)hitTest:(CGPoint)point withEvent:(NSEvent *)event +- (NSView *)hitTest:(CGPoint)point { - // The root content view itself should never receive touches -// NSView *hitView = [super hitTest:point withEvent:event]; -// if (_passThroughTouches && hitView == self) { -// return nil; -// } -// return hitView; - return nil; + // Flip the coordinate system to top-left origin. + NSPoint convertedPoint = [self convertPoint:point fromView:nil]; + + NSView *hitView = [super hitTest:convertedPoint]; + return _passThroughTouches && hitView == self ? nil : hitView; } - (void)invalidate @@ -108,4 +105,21 @@ - (void)invalidate //} } +- (void)viewDidMoveToWindow +{ + if (self.window == nil) { + return; + } + // RCTWindow handles all touches within + if ([self.window isKindOfClass:RCTWindow.class] == NO) { + if (_touchHandler == nil) { + _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; + [_touchHandler attachToView:self]; + } + } else if (_touchHandler) { + [_touchHandler detachFromView:self]; + _touchHandler = nil; + } +} + @end diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index cf87929004..e8b6965d17 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -333,15 +333,14 @@ - (void)setSizeFlexibility:(RCTRootViewSizeFlexibility)sizeFlexibility _contentView.sizeFlexibility = _sizeFlexibility; } -- (NSView *)hitTest:(CGPoint)point withEvent:(NSEvent *)event +- (NSView *)hitTest:(CGPoint)point { // The root view itself should never receive touches -// NSView *hitView = [super hitTest:point withEvent:event]; -// if (self.passThroughTouches && hitView == self) { -// return nil; -// } -// return hitView; - return nil; + NSView *hitView = [super hitTest:point]; + if (self.passThroughTouches && hitView == self) { + return nil; + } + return hitView; } - (void)setAppProperties:(NSDictionary *)appProperties diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index 412eda07b1..6ce83514ce 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -47,7 +47,7 @@ @implementation RCTTouchHandler CFTimeInterval _mostRecentEnqueueJS; /* - * Storing tag to dispatch mouseEnter and mouseLeave events + * Storing tag to dispatch mouseOver and mouseOut events */ NSNumber *_currentMouseOverTag; } @@ -336,13 +336,13 @@ - (void)mouseMoved:(NSEvent *)event } if (_currentMouseOverTag != reactTag && _currentMouseOverTag.intValue > 0) { [_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent" - args:@[_currentMouseOverTag, @"topMouseLeave"]]; + args:@[_currentMouseOverTag, @"topMouseOut"]]; [_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent" - args:@[reactTag, @"topMouseEnter"]]; + args:@[reactTag, @"topMouseOver"]]; _currentMouseOverTag = reactTag; } else if (_currentMouseOverTag == 0) { [_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent" - args:@[reactTag, @"topMouseEnter"]]; + args:@[reactTag, @"topMouseOver"]]; _currentMouseOverTag = reactTag; } } diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 20c44d446b..c2e9ac24ab 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -1038,6 +1038,12 @@ 66CD94B71F1045E700CB3C7C /* RCTMaskedViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 66CD94B01F1045E700CB3C7C /* RCTMaskedViewManager.m */; }; 66CD94B81F1045E700CB3C7C /* RCTMaskedViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 66CD94B01F1045E700CB3C7C /* RCTMaskedViewManager.m */; }; 68EFE4EE1CF6EB3900A1DE13 /* RCTBundleURLProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 68EFE4ED1CF6EB3900A1DE13 /* RCTBundleURLProvider.m */; }; + 702B7FF8221C88AF0027174A /* RCTWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = 702B7FF6221C88AF0027174A /* RCTWindow.h */; }; + 702B7FF9221C88AF0027174A /* RCTWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 702B7FF7221C88AF0027174A /* RCTWindow.m */; }; + 702B7FFC221C88BB0027174A /* RCTMouseEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 702B7FFA221C88BB0027174A /* RCTMouseEvent.h */; }; + 702B7FFD221C88BB0027174A /* RCTMouseEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 702B7FFB221C88BB0027174A /* RCTMouseEvent.m */; }; + 702B7FFE221C88CE0027174A /* RCTMouseEvent.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 702B7FFA221C88BB0027174A /* RCTMouseEvent.h */; }; + 702B7FFF221C88D70027174A /* RCTWindow.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 702B7FF6221C88AF0027174A /* RCTWindow.h */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 83392EB21B6634E10013B15F /* RCTModalHostViewController.m */; }; 83A1FE8C1B62640A00BE0E65 /* RCTModalHostView.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A1FE8B1B62640A00BE0E65 /* RCTModalHostView.m */; }; @@ -1469,6 +1475,8 @@ dstPath = include/React; dstSubfolderSpec = 16; files = ( + 702B7FFF221C88D70027174A /* RCTWindow.h in Copy Headers */, + 702B7FFE221C88CE0027174A /* RCTMouseEvent.h in Copy Headers */, D4EEE3542020933B00C4CBB6 /* UIImageUtils.h in Copy Headers */, 59EDBCBD1FDF4E43003573DE /* RCTScrollableProtocol.h in Copy Headers */, 59EDBCBE1FDF4E43003573DE /* RCTScrollContentShadowView.h in Copy Headers */, @@ -2064,6 +2072,10 @@ 68EFE4EC1CF6EB3000A1DE13 /* RCTBundleURLProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBundleURLProvider.h; sourceTree = ""; }; 68EFE4ED1CF6EB3900A1DE13 /* RCTBundleURLProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBundleURLProvider.m; sourceTree = ""; }; 6A15FB0C1BDF663500531DFB /* RCTRootViewInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootViewInternal.h; sourceTree = ""; }; + 702B7FF6221C88AF0027174A /* RCTWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWindow.h; sourceTree = ""; }; + 702B7FF7221C88AF0027174A /* RCTWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWindow.m; sourceTree = ""; }; + 702B7FFA221C88BB0027174A /* RCTMouseEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMouseEvent.h; sourceTree = ""; }; + 702B7FFB221C88BB0027174A /* RCTMouseEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMouseEvent.m; sourceTree = ""; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; @@ -2445,6 +2457,8 @@ 13C156021AB1A2840079392D /* RCTWebView.m */, 13C156031AB1A2840079392D /* RCTWebViewManager.h */, 13C156041AB1A2840079392D /* RCTWebViewManager.m */, + 702B7FF6221C88AF0027174A /* RCTWindow.h */, + 702B7FF7221C88AF0027174A /* RCTWindow.m */, 59D031E41F8353D3008361F0 /* SafeAreaView */, 59EDBC9B1FDF4E0C003573DE /* ScrollView */, 83F15A171B7CC46900F10295 /* NSView+Private.h */, @@ -2768,6 +2782,8 @@ 14C2CA731B3AC64300E6CBB2 /* RCTModuleData.mm */, 14C2CA6F1B3AC63800E6CBB2 /* RCTModuleMethod.h */, C606692D1F3CC60500E67165 /* RCTModuleMethod.mm */, + 702B7FFA221C88BB0027174A /* RCTMouseEvent.h */, + 702B7FFB221C88BB0027174A /* RCTMouseEvent.m */, 006FC4121D9B20820057AAAD /* RCTMultipartDataTask.h */, 006FC4131D9B20820057AAAD /* RCTMultipartDataTask.m */, 001BFCCE1D8381DE008E587E /* RCTMultipartStreamReader.h */, @@ -3212,6 +3228,7 @@ 3D80DA221DF820620028D040 /* RCTBridge+Private.h in Headers */, 599FAA461FB274980058CCF6 /* RCTSurfaceStage.h in Headers */, 599FAA361FB274980058CCF6 /* RCTSurface.h in Headers */, + 702B7FFC221C88BB0027174A /* RCTMouseEvent.h in Headers */, 3D80DA231DF820620028D040 /* RCTBridgeDelegate.h in Headers */, 3D80DA241DF820620028D040 /* RCTBridgeMethod.h in Headers */, 3D7BFD151EA8E351008DFB7A /* RCTPackagerClient.h in Headers */, @@ -3323,6 +3340,7 @@ 3DF1BE831F26576400068F1A /* JSCTracing.h in Headers */, 3D80DA721DF820620028D040 /* RCTModalHostViewManager.h in Headers */, 13134C9C1E296B2A00B9F3CB /* RCTCxxModule.h in Headers */, + 702B7FF8221C88AF0027174A /* RCTWindow.h in Headers */, 594F0A321FD23228007FBE96 /* RCTSurfaceHostingView.h in Headers */, 3D80DA771DF820620028D040 /* RCTPicker.h in Headers */, 3D80DA781DF820620028D040 /* RCTPickerManager.h in Headers */, @@ -4142,6 +4160,7 @@ 001BFCD01D8381DE008E587E /* RCTMultipartStreamReader.m in Sources */, 133CAE8E1B8E5CFD00F6AD92 /* RCTDatePicker.m in Sources */, 14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */, + 702B7FF9221C88AF0027174A /* RCTWindow.m in Sources */, D49593E6202C96FF00A7694B /* YGNode.cpp in Sources */, 594F0A341FD23228007FBE96 /* RCTSurfaceHostingView.mm in Sources */, 13134C861E296B2A00B9F3CB /* RCTCxxBridge.mm in Sources */, @@ -4255,6 +4274,7 @@ 135A9BFC1E7B0EAE00587AEB /* RCTJSCErrorHandling.mm in Sources */, 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */, 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */, + 702B7FFD221C88BB0027174A /* RCTMouseEvent.m in Sources */, 83A1FE8F1B62643A00BE0E65 /* RCTModalHostViewManager.m in Sources */, 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */, 1384E2091E806D4E00545659 /* RCTNativeModule.mm in Sources */, diff --git a/React/Views/NSView+React.h b/React/Views/NSView+React.h index 3fee72d909..2559b5909a 100644 --- a/React/Views/NSView+React.h +++ b/React/Views/NSView+React.h @@ -114,4 +114,6 @@ */ @property (nonatomic, assign) BOOL clipsToBounds; +- (NSView *)reactHitTest:(NSPoint)point; + @end diff --git a/React/Views/NSView+React.m b/React/Views/NSView+React.m index 382e3f3afe..149ecb65c6 100644 --- a/React/Views/NSView+React.m +++ b/React/Views/NSView+React.m @@ -295,4 +295,15 @@ - (NSView *)reactAccessibilityElement return self; } +#pragma mark - Interaction + +- (NSView *)reactHitTest:(NSPoint)point +{ + NSView *view = [self hitTest:point]; + while (view && !view.reactTag) { + view = view.superview; + } + return view; +} + @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index d95ab58749..fb08b2c267 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -73,8 +73,10 @@ - (RCTShadowView *)shadowView @"touchEnd", // Mouse events - @"mouseEnter", - @"mouseLeave", + @"mouseMove", + @"mouseOver", + @"mouseOut", + @"contextMenu", ]; } diff --git a/React/Views/RCTWindow.h b/React/Views/RCTWindow.h new file mode 100644 index 0000000000..71ac4f0a48 --- /dev/null +++ b/React/Views/RCTWindow.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTBridge.h" +#import "RCTRootView.h" + +@interface RCTWindow : NSWindow + +- (instancetype)initWithBridge:(RCTBridge *)bridge + contentRect:(NSRect)contentRect + styleMask:(NSWindowStyleMask)style + defer:(BOOL)defer NS_DESIGNATED_INITIALIZER; + +@property (nullable, strong) RCTRootView *contentView; + +@end diff --git a/React/Views/RCTWindow.m b/React/Views/RCTWindow.m new file mode 100644 index 0000000000..9f1095587d --- /dev/null +++ b/React/Views/RCTWindow.m @@ -0,0 +1,284 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTWindow.h" + +#import "RCTUtils.h" +#import "RCTMouseEvent.h" +#import "RCTTouchEvent.h" +#import "NSView+React.h" + +@implementation RCTWindow +{ + RCTBridge *_bridge; + + NSMutableDictionary *_mouseInfo; + NSView *_hoveredView; + NSView *_clickedView; + NSEventType _clickType; + uint16_t _coalescingKey; + + BOOL _inContentView; + BOOL _enabled; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType defer:(BOOL)flag) + +- (instancetype)initWithBridge:(RCTBridge *)bridge + contentRect:(NSRect)contentRect + styleMask:(NSWindowStyleMask)style + defer:(BOOL)defer +{ + self = [super initWithContentRect:contentRect + styleMask:style + backing:NSBackingStoreBuffered + defer:defer]; + + if (self) { + _bridge = bridge; + + _mouseInfo = [NSMutableDictionary new]; + _mouseInfo[@"changedTouches"] = @[]; // Required for "mouseMove" events + _mouseInfo[@"identifier"] = @0; // Required for "touch*" events + + // The owner must set "contentView" manually. + super.contentView = nil; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_javaScriptDidLoad:) + name:RCTJavaScriptDidLoadNotification + object:bridge]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_bridgeWillReload:) + name:RCTBridgeWillReloadNotification + object:bridge]; + } + + return self; +} + +@dynamic contentView; + +- (NSView *)rootView +{ + return self.contentView.contentView; +} + +- (void)sendEvent:(NSEvent *)event +{ + [super sendEvent:event]; + + // Avoid sending JS events too early. + if (_enabled == NO) { + return; + } + + NSEventType type = event.type; + + if (type == NSEventTypeMouseEntered) { + if (event.trackingArea.owner == self.contentView) { + _inContentView = YES; + } + return; + } + + if (type == NSEventTypeMouseExited) { + if (event.trackingArea.owner == self.contentView) { + _inContentView = NO; + + if (_clickedView) { + if (_clickType == NSEventTypeLeftMouseDown) { + [self _sendTouchEvent:@"touchCancel"]; + } + _clickedView = nil; + _clickType = 0; + } + + [self _setHoveredView:nil]; + } + return; + } + + if (type != NSEventTypeMouseMoved && + type != NSEventTypeLeftMouseDragged && + type != NSEventTypeLeftMouseUp && + type != NSEventTypeLeftMouseDown && + type != NSEventTypeRightMouseUp && + type != NSEventTypeRightMouseDown) { + return; + } + + NSView *targetView = [self hitTest:event.locationInWindow withEvent:event]; + + if (_clickedView) { + if (type == NSEventTypeLeftMouseDragged) { + if (_clickType == NSEventTypeLeftMouseDown) { + [self _sendTouchEvent:@"touchMove"]; + } + return; + } + } else { + if (type == NSEventTypeMouseMoved) { + if (_inContentView == NO) { + return; // Ignore "mouseMove" events outside the "contentView" + } + + [self _setHoveredView:targetView]; + return; + } + + if (targetView == nil) { + return; + } + + if (type == NSEventTypeLeftMouseDown || type == NSEventTypeRightMouseDown) { + // When the "firstResponder" is a NSTextView, "mouseUp" and "mouseDragged" events are swallowed, + // so we should skip tracking of "mouseDown" events in order to avoid corrupted state. + if ([self.firstResponder isKindOfClass:NSTextView.class]) { + NSView *clickedView = [self.rootView hitTest:event.locationInWindow]; + NSView *fieldEditor = (NSView *)self.firstResponder; + if ([clickedView isDescendantOf:fieldEditor]) { + return; + } + + // Blur the field editor when clicking outside it. + [self makeFirstResponder:nil]; + } + + if (type == NSEventTypeLeftMouseDown) { + [self _sendTouchEvent:@"touchStart"]; + } + + _clickedView = targetView; + _clickType = type; + return; + } + } + + if (type == NSEventTypeLeftMouseUp) { + if (_clickType == NSEventTypeLeftMouseDown) { + [self _sendTouchEvent:@"touchEnd"]; + _clickedView = nil; + _clickType = 0; + } + + // Update the "hoveredView" now, instead of waiting for the next "mouseMove" event. + [self _setHoveredView:targetView]; + return; + } + + if (type == NSEventTypeRightMouseUp) { + if (_clickType == NSEventTypeRightMouseDown) { + // Right clicks must end in the same React "ancestor chain" they started in. + if ([_clickedView isDescendantOf:targetView]) { + [self _sendMouseEvent:@"contextMenu"]; + } + _clickedView = nil; + _clickType = 0; + } + + // Update the "hoveredView" now, instead of waiting for the next "mouseMove" event. + [self _setHoveredView:targetView]; + return; + } +} + +#pragma mark - Private methods + +static inline BOOL hasFlag(NSUInteger flags, NSUInteger flag) { + return (flags & flag) == flag; +} + +- (NSView *)hitTest:(NSPoint)point withEvent:(NSEvent *)event +{ + NSView *targetView = [self.rootView reactHitTest:point]; + + // By convention, all coordinates, whether they be touch coordinates, or + // measurement coordinates are with respect to the root view. + CGPoint absoluteLocation = [self.rootView convertPoint:point fromView:nil]; + CGPoint relativeLocation = [self.rootView convertPoint:absoluteLocation toView:targetView]; + + _mouseInfo[@"pageX"] = @(RCTSanitizeNaNValue(absoluteLocation.x, @"pageX")); + _mouseInfo[@"pageY"] = @(RCTSanitizeNaNValue(absoluteLocation.y, @"pageY")); + _mouseInfo[@"locationX"] = @(RCTSanitizeNaNValue(relativeLocation.x, @"locationX")); + _mouseInfo[@"locationY"] = @(RCTSanitizeNaNValue(relativeLocation.y, @"locationY")); + _mouseInfo[@"timestamp"] = @(event.timestamp * 1000); // in ms, for JS + _mouseInfo[@"target"] = targetView.reactTag; + + NSUInteger flags = event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask; + _mouseInfo[@"altKey"] = @(hasFlag(flags, NSEventModifierFlagOption)); + _mouseInfo[@"ctrlKey"] = @(hasFlag(flags, NSEventModifierFlagControl)); + _mouseInfo[@"metaKey"] = @(hasFlag(flags, NSEventModifierFlagCommand)); + _mouseInfo[@"shiftKey"] = @(hasFlag(flags, NSEventModifierFlagShift)); + + return targetView; +} + +- (void)_setHoveredView:(NSView *)view +{ + if (_hoveredView && !(view && view == _hoveredView)) { + _mouseInfo[@"target"] = _hoveredView.reactTag; + _hoveredView = nil; + + [self _sendMouseEvent:@"mouseOut"]; + } + + if (view) { + _mouseInfo[@"target"] = view.reactTag; + + if (_hoveredView == nil) { + _hoveredView = view; + [self _sendMouseEvent:@"mouseOver"]; + } + + [self _sendMouseEvent:@"mouseMove"]; + } +} + +- (void)_sendMouseEvent:(NSString *)eventName +{ + RCTMouseEvent *event = [[RCTMouseEvent alloc] initWithEventName:eventName + target:_mouseInfo[@"target"] + userInfo:_mouseInfo + coalescingKey:_coalescingKey]; + + if (![eventName isEqualToString:@"mouseMove"]) { + _coalescingKey++; + } + + [_bridge.eventDispatcher sendEvent:event]; +} + +- (void)_sendTouchEvent:(NSString *)eventName +{ + RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName + reactTag:self.rootView.reactTag + reactTouches:@[_mouseInfo] + changedIndexes:@[@0] + coalescingKey:_coalescingKey]; + + if (![eventName isEqualToString:@"touchMove"]) { + _coalescingKey++; + } + + [_bridge.eventDispatcher sendEvent:event]; +} + +- (void)_javaScriptDidLoad:(__unused NSNotification *)notification +{ + _enabled = YES; +} + +- (void)_bridgeWillReload:(__unused NSNotification *)notification +{ + _enabled = NO; +} + +@end