From d679a7cfe47dbae3c69ed5070b679dc3ccd3eb1c Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 13 Aug 2019 23:10:26 -0400 Subject: [PATCH] feat: add "cursor" prop to RCTView --- @types/index.d.ts | 5 ++ Libraries/Components/View/ViewPropTypes.js | 10 ++++ .../Text/TextInput/RCTBaseTextInputView.m | 1 + .../TextInput/Singleline/RCTUITextField.m | 3 ++ React/Base/RCTConvert.h | 2 + React/Base/RCTConvert.m | 11 ++++ React/Modules/RCTCursorManager.m | 42 --------------- React/React.xcodeproj/project.pbxproj | 14 +++-- React/Views/NSView+React.h | 4 ++ React/Views/NSView+React.m | 6 +++ .../RCTCursorManager.h => Views/RCTCursor.h} | 19 +++++-- React/Views/RCTView.h | 6 +++ React/Views/RCTViewManager.m | 3 +- React/Views/RCTWindow.m | 53 +++++++++++++++++++ 14 files changed, 125 insertions(+), 54 deletions(-) delete mode 100644 React/Modules/RCTCursorManager.m rename React/{Modules/RCTCursorManager.h => Views/RCTCursor.h} (52%) diff --git a/@types/index.d.ts b/@types/index.d.ts index 45bc967c4..da0b33a71 100644 --- a/@types/index.d.ts +++ b/@types/index.d.ts @@ -2093,6 +2093,11 @@ export interface ViewProps */ pointerEvents?: "box-none" | "none" | "box-only" | "auto"; + /** + * The cursor image to show while the mouse is in inside this view. + */ + cursor?: "inherit" | "none" | "default" | "pointer" | "text" | "move" | "grab" | "grabbing" + /** * * This is a special performance property exposed by RCTView and is useful for scrolling content when there are many subviews, diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 4cff708c9..98d5cd8c2 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -443,6 +443,16 @@ module.exports = { onDrop: PropTypes.func, onContextMenu: PropTypes.func, onContextMenuItemClick: PropTypes.func, + cursor: PropTypes.oneOf([ + 'inherit', + 'none', + 'default', + 'pointer', + 'text', + 'move', + 'grab', + 'grabbing', + ]), /** * Mapped to toolTip property of NSView. Used to show extra information when * mouse hovering. diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index 64fd599e5..71f17ddd1 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -39,6 +39,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _eventDispatcher = bridge.eventDispatcher; self.clipsToBounds = YES; + self.cursor = RCTCursorText; } return self; diff --git a/Libraries/Text/TextInput/Singleline/RCTUITextField.m b/Libraries/Text/TextInput/Singleline/RCTUITextField.m index dc0727ec3..e49318a90 100644 --- a/Libraries/Text/TextInput/Singleline/RCTUITextField.m +++ b/Libraries/Text/TextInput/Singleline/RCTUITextField.m @@ -111,6 +111,9 @@ - (void)textDidEndEditing:(NSNotification *)notification } } +// Do nothing here, as it messes with RCTWindow cursor support. +- (void)resetCursorRects {} + - (BOOL)becomeFirstResponder { if ([super becomeFirstResponder]) { diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index c69290f26..76a69aae8 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -13,6 +13,7 @@ #import #import #import +#import #import #import #import @@ -114,6 +115,7 @@ typedef BOOL css_backface_visibility_t; + (RCTAnimationType)RCTAnimationType:(id)json; + (RCTBlendMode)RCTBlendMode:(id)json; + (RCTBorderStyle)RCTBorderStyle:(id)json; ++ (RCTCursor)RCTCursor:(id)json; + (RCTTextDecorationLineType)RCTTextDecorationLineType:(id)json; @end diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index ffaeea7b5..327d8aa71 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -645,6 +645,17 @@ + (NSPropertyList)NSPropertyList:(id)json @"overlay": @(RCTBlendModeOverlay), }), RCTBlendModeNone, integerValue) +RCT_ENUM_CONVERTER(RCTCursor, (@{ + @"inherit": @(RCTCursorInherit), + @"none": @(RCTCursorNone), + @"default": @(RCTCursorDefault), + @"pointer": @(RCTCursorPointer), + @"text": @(RCTCursorText), + @"move": @(RCTCursorMove), + @"grab": @(RCTCursorGrab), + @"grabbing": @(RCTCursorGrabbing), +}), RCTCursorInherit, integerValue) + @end @interface RCTImageSource (Packager) diff --git a/React/Modules/RCTCursorManager.m b/React/Modules/RCTCursorManager.m deleted file mode 100644 index 62a997494..000000000 --- a/React/Modules/RCTCursorManager.m +++ /dev/null @@ -1,42 +0,0 @@ -/** - * 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 "RCTCursorManager.h" - -// Macros for creation set methods for provided cursor type -#define EXPORT_CURSOR_SET_METHOD(name) \ - RCT_EXPORT_METHOD(name) { \ - [[NSCursor name] set]; \ - } - - -@implementation RCTCursorManager - -RCT_EXPORT_MODULE(); - -EXPORT_CURSOR_SET_METHOD(arrowCursor); -EXPORT_CURSOR_SET_METHOD(IBeamCursor); -EXPORT_CURSOR_SET_METHOD(crosshairCursor); -EXPORT_CURSOR_SET_METHOD(closedHandCursor); -EXPORT_CURSOR_SET_METHOD(openHandCursor); -EXPORT_CURSOR_SET_METHOD(pointingHandCursor); -EXPORT_CURSOR_SET_METHOD(resizeLeftCursor); -EXPORT_CURSOR_SET_METHOD(resizeRightCursor); -EXPORT_CURSOR_SET_METHOD(resizeLeftRightCursor); -EXPORT_CURSOR_SET_METHOD(resizeUpCursor); -EXPORT_CURSOR_SET_METHOD(resizeDownCursor); -EXPORT_CURSOR_SET_METHOD(resizeUpDownCursor); -EXPORT_CURSOR_SET_METHOD(disappearingItemCursor); -EXPORT_CURSOR_SET_METHOD(IBeamCursorForVerticalLayout); -EXPORT_CURSOR_SET_METHOD(operationNotAllowedCursor); -EXPORT_CURSOR_SET_METHOD(dragLinkCursor); -EXPORT_CURSOR_SET_METHOD(dragCopyCursor); -EXPORT_CURSOR_SET_METHOD(contextualMenuCursor); - -@end diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 847a8b4c6..62b25ac14 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -529,6 +529,8 @@ 70347E2122FF448600A4F09B /* RCTGradient.m in Sources */ = {isa = PBXBuildFile; fileRef = 70347E2022FF448600A4F09B /* RCTGradient.m */; }; 70347E2322FF44B700A4F09B /* RCTGradientManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 70347E2222FF44B700A4F09B /* RCTGradientManager.m */; }; 70347E2522FF44C100A4F09B /* RCTGradientManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 70347E2422FF44BF00A4F09B /* RCTGradientManager.h */; }; + 7045C32223038208000AB285 /* RCTCursor.h in Headers */ = {isa = PBXBuildFile; fileRef = 7045C32123038208000AB285 /* RCTCursor.h */; }; + 7045C323230384FE000AB285 /* RCTCursor.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 7045C32123038208000AB285 /* RCTCursor.h */; }; 705EDE2922107DD0000CAA67 /* Utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 705EDE2722107DD0000CAA67 /* Utils.h */; }; 705EDE2A22107DD0000CAA67 /* Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 705EDE2822107DD0000CAA67 /* Utils.cpp */; }; 705EDE2C221082CB000CAA67 /* RCTSurfaceSizeMeasureMode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 705EDE2B221082CB000CAA67 /* RCTSurfaceSizeMeasureMode.mm */; }; @@ -599,8 +601,6 @@ CF2731C11E7B8DE40044CA4F /* RCTDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = CF2731BF1E7B8DE40044CA4F /* RCTDeviceInfo.m */; }; D426ACD020D57642003B4C4D /* RCTAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = D426ACCF20D57642003B4C4D /* RCTAppearance.h */; }; D426ACD220D57659003B4C4D /* RCTAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = D426ACD120D57659003B4C4D /* RCTAppearance.m */; }; - D49593DE202C937C00A7694B /* RCTCursorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D49593D8202C937B00A7694B /* RCTCursorManager.h */; }; - D49593DF202C937C00A7694B /* RCTCursorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D49593D9202C937B00A7694B /* RCTCursorManager.m */; }; D49593E0202C937C00A7694B /* RCTMenuManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D49593DA202C937B00A7694B /* RCTMenuManager.h */; }; D49593E1202C937C00A7694B /* RCTMenuManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D49593DB202C937C00A7694B /* RCTMenuManager.m */; }; D49593E7202C970600A7694B /* YGNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D49593E3202C96FF00A7694B /* YGNode.h */; }; @@ -704,6 +704,7 @@ dstPath = include/React; dstSubfolderSpec = 16; files = ( + 7045C323230384FE000AB285 /* RCTCursor.h in Copy Headers */, 70CDF31522F9E3BE00ECA452 /* RCTBlendMode.h in Copy Headers */, 7095B6D02247C86300BE2245 /* RCTFieldEditor.h in Copy Headers */, 702B7FFF221C88D70027174A /* RCTWindow.h in Copy Headers */, @@ -1286,6 +1287,7 @@ 70347E2022FF448600A4F09B /* RCTGradient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTGradient.m; sourceTree = ""; }; 70347E2222FF44B700A4F09B /* RCTGradientManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTGradientManager.m; sourceTree = ""; }; 70347E2422FF44BF00A4F09B /* RCTGradientManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTGradientManager.h; sourceTree = ""; }; + 7045C32123038208000AB285 /* RCTCursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTCursor.h; sourceTree = ""; }; 705EDE2722107DD0000CAA67 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = ""; }; 705EDE2822107DD0000CAA67 /* Utils.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils.cpp; sourceTree = ""; }; 705EDE2B221082CB000CAA67 /* RCTSurfaceSizeMeasureMode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTSurfaceSizeMeasureMode.mm; sourceTree = ""; }; @@ -1362,8 +1364,6 @@ CF2731BF1E7B8DE40044CA4F /* RCTDeviceInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDeviceInfo.m; sourceTree = ""; }; D426ACCF20D57642003B4C4D /* RCTAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTAppearance.h; sourceTree = ""; }; D426ACD120D57659003B4C4D /* RCTAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTAppearance.m; sourceTree = ""; }; - D49593D8202C937B00A7694B /* RCTCursorManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCursorManager.h; sourceTree = ""; }; - D49593D9202C937B00A7694B /* RCTCursorManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCursorManager.m; sourceTree = ""; }; D49593DA202C937B00A7694B /* RCTMenuManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMenuManager.h; sourceTree = ""; }; D49593DB202C937C00A7694B /* RCTMenuManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMenuManager.m; sourceTree = ""; }; D49593E3202C96FF00A7694B /* YGNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YGNode.h; sourceTree = ""; }; @@ -1562,8 +1562,6 @@ 13B07FE01A69315300A75B9A /* Modules */ = { isa = PBXGroup; children = ( - D49593D8202C937B00A7694B /* RCTCursorManager.h */, - D49593D9202C937B00A7694B /* RCTCursorManager.m */, D49593DA202C937B00A7694B /* RCTMenuManager.h */, D49593DB202C937C00A7694B /* RCTMenuManager.m */, 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */, @@ -1638,6 +1636,7 @@ 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 130443C31E401A8C00D93A67 /* RCTConvert+Transform.h */, 130443C41E401A8C00D93A67 /* RCTConvert+Transform.m */, + 7045C32123038208000AB285 /* RCTCursor.h */, 133CAE8C1B8E5CFD00F6AD92 /* RCTDatePicker.h */, 133CAE8D1B8E5CFD00F6AD92 /* RCTDatePicker.m */, 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */, @@ -2251,6 +2250,7 @@ 3D80DA1E1DF820620028D040 /* RCTNetworkTask.h in Headers */, 657734841EE834C900A0E9EA /* RCTInspectorDevServerHelper.h in Headers */, 3D80DA1F1DF820620028D040 /* RCTPushNotificationManager.h in Headers */, + 7045C32223038208000AB285 /* RCTCursor.h in Headers */, 3D80DA211DF820620028D040 /* RCTBridge.h in Headers */, 13F880381E296D2800C3C7A1 /* JSCWrapper.h in Headers */, 3D80DA221DF820620028D040 /* RCTBridge+Private.h in Headers */, @@ -2382,7 +2382,6 @@ 657734901EE8354A00A0E9EA /* RCTInspectorPackagerConnection.h in Headers */, 3D7BFD1D1EA8E351008DFB7A /* RCTPackagerConnection.h in Headers */, 3D80DA811DF820620028D040 /* RCTSegmentedControl.h in Headers */, - D49593DE202C937C00A7694B /* RCTCursorManager.h in Headers */, 599FAA3C1FB274980058CCF6 /* RCTSurfaceRootShadowView.h in Headers */, 3D80DA821DF820620028D040 /* RCTSegmentedControlManager.h in Headers */, 3D80DA831DF820620028D040 /* RCTShadowView.h in Headers */, @@ -2849,7 +2848,6 @@ 3D7BFD1F1EA8E351008DFB7A /* RCTPackagerConnection.mm in Sources */, 13456E931ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m in Sources */, 70347E2322FF44B700A4F09B /* RCTGradientManager.m in Sources */, - D49593DF202C937C00A7694B /* RCTCursorManager.m in Sources */, 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */, 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */, 130443C61E401A8C00D93A67 /* RCTConvert+Transform.m in Sources */, diff --git a/React/Views/NSView+React.h b/React/Views/NSView+React.h index 41755759d..4bbfd15ae 100644 --- a/React/Views/NSView+React.h +++ b/React/Views/NSView+React.h @@ -10,6 +10,7 @@ #import #import +#import #import @class RCTShadowView; @@ -119,6 +120,9 @@ /** Populate the `layer` ivar when nil */ - (void)ensureLayerExists; +/** Default implementation to avoid crashes */ +- (RCTCursor)cursor; + @end @interface CALayer (React) diff --git a/React/Views/NSView+React.m b/React/Views/NSView+React.m index 2abb9fa27..2f61c2e8c 100644 --- a/React/Views/NSView+React.m +++ b/React/Views/NSView+React.m @@ -329,6 +329,12 @@ - (void)setTransform:(__unused CATransform3D)transform // override "displayLayer:", and apply the transform there. } +// Default implementation for React-managed views. +- (RCTCursor)cursor +{ + return RCTCursorInherit; +} + @end #pragma mark - diff --git a/React/Modules/RCTCursorManager.h b/React/Views/RCTCursor.h similarity index 52% rename from React/Modules/RCTCursorManager.h rename to React/Views/RCTCursor.h index 449019d95..e08b59c78 100644 --- a/React/Modules/RCTCursorManager.h +++ b/React/Views/RCTCursor.h @@ -7,8 +7,21 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import -#import "RCTBridgeModule.h" +#import + +@interface NSCursor (Private) + ++ (NSCursor *)_moveCursor; -@interface RCTCursorManager : NSObject @end + +typedef NS_ENUM(NSInteger, RCTCursor) { + RCTCursorInherit = 0, + RCTCursorNone, + RCTCursorDefault, + RCTCursorPointer, + RCTCursorText, + RCTCursorMove, + RCTCursorGrab, + RCTCursorGrabbing, +}; diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 5a3c920a0..09b8a71ec 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -11,6 +11,7 @@ #import #import +#import #import #import @@ -129,4 +130,9 @@ @property (nonatomic, copy) RCTDirectEventBlock onDrop; @property (nonatomic, copy) RCTDirectEventBlock onContextMenuItemClick; +/** + * The cursor image to show while the mouse is inside this view. + */ +@property (nonatomic, assign) RCTCursor cursor; + @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 46206d3da..c111601a9 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -111,7 +111,8 @@ - (RCTShadowView *)shadowView RCT_EXPORT_LAYER_PROPERTY(shadowOffset, CGSize) RCT_EXPORT_LAYER_PROPERTY(shadowOpacity, float) RCT_EXPORT_LAYER_PROPERTY(shadowRadius, CGFloat) -RCT_REMAP_VIEW_PROPERTY(toolTip, toolTip, NSString) +RCT_EXPORT_VIEW_PROPERTY(cursor, RCTCursor) +RCT_EXPORT_VIEW_PROPERTY(toolTip, NSString) RCT_CUSTOM_VIEW_PROPERTY(overflow, YGOverflow, RCTView) { if (json) { diff --git a/React/Views/RCTWindow.m b/React/Views/RCTWindow.m index 896323d3a..8f4730fb6 100644 --- a/React/Views/RCTWindow.m +++ b/React/Views/RCTWindow.m @@ -10,6 +10,7 @@ #import "RCTWindow.h" #import "RCTUtils.h" +#import "RCTCursor.h" #import "RCTMouseEvent.h" #import "RCTTouchEvent.h" #import "RCTFieldEditor.h" @@ -19,6 +20,7 @@ @implementation RCTWindow { RCTBridge *_bridge; + RCTCursor _lastCursor; NSMutableDictionary *_mouseInfo; NSView *_clickTarget; NSEventType _clickType; @@ -256,6 +258,53 @@ - (NSView *)hitTest:(NSPoint)point withEvent:(NSEvent *)event return targetView; } +static NSCursor *NSCursorForRCTCursor(RCTCursor cursor) +{ + switch (cursor) { + case RCTCursorDefault: return NSCursor.arrowCursor; + case RCTCursorPointer: return NSCursor.pointingHandCursor; + case RCTCursorText: return NSCursor.IBeamCursor; + case RCTCursorMove: return NSCursor._moveCursor; + case RCTCursorGrab: return NSCursor.openHandCursor; + case RCTCursorGrabbing: return NSCursor.closedHandCursor; + default: return nil; + } +} + +// HACK: Do nothing here to prevent AppKit default behavior of updating the cursor whenever a view moves. +- (void)_setCursorForMouseLocation:(__unused CGPoint)point {} + +- (void)_updateCursor:(NSView *)view +{ + // Find the nearest React-managed view with a defined cursor. + while (view) { + RCTCursor cursor = view.cursor; + if (cursor != RCTCursorInherit) { + if (cursor == _lastCursor) { + return; + } + + if (cursor == RCTCursorNone) { + [NSCursor hide]; + } else { + [NSCursor unhide]; + [NSCursorForRCTCursor(cursor) set]; + } + + _lastCursor = cursor; + return; + } + view = view.superview; + } + // Reset the cursor to its default. + if (_lastCursor != RCTCursorDefault) { + _lastCursor = RCTCursorDefault; + + [NSCursor unhide]; + [NSCursor.arrowCursor set]; + } +} + - (void)_setHoverTarget:(NSView *)view { NSNumber *target = view.reactTag; @@ -269,6 +318,9 @@ - (void)_setHoverTarget:(NSView *)view _hoverTarget = nil; [self _sendMouseEvent:@"mouseOut"]; + if (!view) { + [self _updateCursor:nil]; + } } } @@ -279,6 +331,7 @@ - (void)_setHoverTarget:(NSView *)view if (_hoverTarget == nil) { _hoverTarget = view; [self _sendMouseEvent:@"mouseOver"]; + [self _updateCursor:view]; // Ensure "mouseMove" events have no "relatedTarget" property. _mouseInfo[@"relatedTarget"] = nil;