diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index de77b65aaba7f5..7ecdb3f3b5d16b 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -144,7 +144,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { borderTopLeftRadius: true, borderTopRightRadius: true, borderTopStartRadius: true, - cursor: true, + cursor: true, // [macOS] [visionOS] opacity: true, pointerEvents: true, diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts index 072d38691b9bd8..4bdd849668a141 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -27,6 +27,30 @@ export type DimensionValue = type AnimatableNumericValue = number | Animated.AnimatedNode; type AnimatableStringValue = string | Animated.AnimatedNode; +// [macOS +export type CursorValue = + | 'alias' + | 'auto' + | 'col-resize' + | 'context-menu' + | 'copy' + | 'crosshair' + | 'default' + | 'disappearing-item' + | 'e-resize' + | 'grab' + | 'grabbing' + | 'n-resize' + | 'no-drop' + | 'not-allowed' + | 'pointer' + | 'row-resize' + | 's-resize' + | 'text' + | 'vertical-text' + | 'w-resize'; +// macOS] + /** * Flex Prop Types * @see https://reactnative.dev/docs/flexbox @@ -273,6 +297,7 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { * Controls whether the View can be the target of touch events. */ pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto' | undefined; + cursor?: CursorValue | undefined; } export type FontVariant = @@ -363,4 +388,5 @@ export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { tintColor?: ColorValue | undefined; opacity?: AnimatableNumericValue | undefined; objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | undefined; + cursor?: CursorValue | undefined; } diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 27d3f868257431..9ff7c20aa5b189 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -20,8 +20,25 @@ import type { } from './private/_StyleSheetTypesOverrides'; import type {____TransformStyle_Internal} from './private/_TransformStyle'; +declare export opaque type NativeColorValue; +export type ____ColorValue_Internal = null | string | number | NativeColorValue; +export type ColorArrayValue = null | $ReadOnlyArray<____ColorValue_Internal>; +export type PointValue = { + x: number, + y: number, +}; +export type EdgeInsetsValue = { + top: number, + left: number, + right: number, + bottom: number, +}; + +export type DimensionValue = number | string | 'auto' | AnimatedNode | null; +export type AnimatableNumericValue = number | AnimatedNode; + // [macOS -export type CursorValue = ?( +export type CursorValue = | 'alias' | 'auto' | 'col-resize' @@ -41,27 +58,9 @@ export type CursorValue = ?( | 's-resize' | 'text' | 'vertical-text' - | 'w-resize' -); + | 'w-resize'; // macOS] -declare export opaque type NativeColorValue; -export type ____ColorValue_Internal = null | string | number | NativeColorValue; -export type ColorArrayValue = null | $ReadOnlyArray<____ColorValue_Internal>; -export type PointValue = { - x: number, - y: number, -}; -export type EdgeInsetsValue = { - top: number, - left: number, - right: number, - bottom: number, -}; - -export type DimensionValue = number | string | 'auto' | AnimatedNode | null; -export type AnimatableNumericValue = number | AnimatedNode; - /** * React Native's layout system is based on Flexbox and is powered both * on iOS and Android by an open source project called `Yoga`: @@ -752,7 +751,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ opacity?: AnimatableNumericValue, elevation?: number, pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only', - cursor?: CursorValue, // [macOS] + cursor?: CursorValue, // [macOS][visionOS] }>; export type ____ViewStyle_Internal = $ReadOnly<{ diff --git a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm index 427a4254e77d0a..d250da3386f3a5 100644 --- a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm +++ b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm @@ -6,9 +6,7 @@ */ #import -#if TARGET_OS_OSX // [macOS -#import -#endif // macOS] +#import // [macOS] @implementation RCTBaseTextViewManager diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.h b/packages/react-native/Libraries/Text/RCTTextAttributes.h index 518d0535e26f50..8265439a286a6c 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.h +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.h @@ -7,15 +7,12 @@ #import // [macOS] +#import // [macOS] #import #import #import "RCTTextTransform.h" -#if TARGET_OS_OSX // [macOS -#import -#endif // macOS] - NS_ASSUME_NONNULL_BEGIN extern NSString *const RCTTextAttributesIsHighlightedAttributeName; diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.mm b/packages/react-native/Libraries/Text/RCTTextAttributes.mm index 9bfdb93cc9a82c..fe9ccb5d47ba00 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.mm +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.mm @@ -8,13 +8,10 @@ #import #import +#import // [macOS] #import #import -#if TARGET_OS_OSX // [macOS -#import -#endif // macOS] - NSString *const RCTTextAttributesIsHighlightedAttributeName = @"RCTTextAttributesIsHighlightedAttributeName"; NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttributeName"; @@ -235,7 +232,7 @@ - (NSParagraphStyle *)effectiveParagraphStyle #if TARGET_OS_OSX // [macOS if (_cursor != RCTCursorAuto) { - attributes[NSCursorAttributeName] = [RCTConvert NSCursor:_cursor]; + attributes[NSCursorAttributeName] = NSCursorFromRCTCursor(_cursor); } #endif // macOS] diff --git a/packages/react-native/React/Base/RCTConvert.h b/packages/react-native/React/Base/RCTConvert.h index 6976bb9df10d36..98e832e0a35ece 100644 --- a/packages/react-native/React/Base/RCTConvert.h +++ b/packages/react-native/React/Base/RCTConvert.h @@ -11,6 +11,7 @@ #import #import #import +#import // [macOS] [visionOS] #import #import #import @@ -91,6 +92,8 @@ typedef NSURL RCTFileURL; #endif #endif // [macOS] ++ (RCTCursor)RCTCursor:(id)json; // [macOS] [visionOS] + #if TARGET_OS_OSX // [macOS + (NSTextCheckingTypes)NSTextCheckingTypes:(id)json; #endif // macOS] diff --git a/packages/react-native/React/Base/RCTConvert.m b/packages/react-native/React/Base/RCTConvert.m index 7d1b03cda28d5b..7637221d6fd207 100644 --- a/packages/react-native/React/Base/RCTConvert.m +++ b/packages/react-native/React/Base/RCTConvert.m @@ -562,7 +562,38 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC }), UIBarStyleDefault, integerValue) -#else // [macOS +#endif // [macOS] + +// [macOS [visionOS] +RCT_ENUM_CONVERTER( + RCTCursor, + (@{ + @"alias" : @(RCTCursorAlias), + @"auto" : @(RCTCursorAuto), + @"col-resize" : @(RCTCursorColumnResize), + @"context-menu" : @(RCTCursorContextualMenu), + @"copy" : @(RCTCursorCopy), + @"crosshair" : @(RCTCursorCrosshair), + @"default" : @(RCTCursorDefault), + @"disappearing-item" : @(RCTCursorDisappearingItem), + @"e-resize" : @(RCTCursorEastResize), + @"grab" : @(RCTCursorGrab), + @"grabbing" : @(RCTCursorGrabbing), + @"n-resize" : @(RCTCursorNorthResize), + @"no-drop" : @(RCTCursorNoDrop), + @"not-allowed" : @(RCTCursorNotAllowed), + @"pointer" : @(RCTCursorPointer), + @"row-resize" : @(RCTCursorRowResize), + @"s-resize" : @(RCTCursorSouthResize), + @"text" : @(RCTCursorText), + @"vertical-text" : @(RCTCursorVerticalText), + @"w-resize" : @(RCTCursorWestResize), + }), + RCTCursorAuto, + integerValue) +// macOS] [visionOS] + +#if TARGET_OS_OSX // [macOS RCT_MULTI_ENUM_CONVERTER(NSTextCheckingTypes, (@{ @"ortography": @(NSTextCheckingTypeOrthography), @"spelling": @(NSTextCheckingTypeSpelling), diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index d1340d92eda18d..afd8d23a5d989a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -102,6 +102,18 @@ - (void)setBackgroundColor:(RCTUIColor *)backgroundColor // [macOS] _backgroundColor = backgroundColor; } +#if TARGET_OS_OSX // [macOS +- (void)resetCursorRects +{ + [self discardCursorRects]; + if (_props->cursor != Cursor::Auto) + { + NSCursor *cursor = NSCursorFromCursor(_props->cursor); + [self addCursorRect:self.bounds cursor:cursor]; + } +} +#endif // macOS] + #pragma mark - RCTComponentViewProtocol + (ComponentDescriptorProvider)componentDescriptorProvider @@ -258,6 +270,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldViewProps.backfaceVisibility != newViewProps.backfaceVisibility) { self.layer.doubleSided = newViewProps.backfaceVisibility == BackfaceVisibility::Visible; } + + // `cursor` + if (oldViewProps.cursor != newViewProps.cursor) { + needsInvalidateLayer = YES; + } // `shouldRasterize` if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) { @@ -579,6 +596,55 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) } } +#if TARGET_OS_OSX // [macOS + static NSCursor *NSCursorFromCursor(Cursor cursor) +{ + switch (cursor) { + case Cursor::Auto: + return [NSCursor arrowCursor]; + case Cursor::Alias: + return [NSCursor dragLinkCursor]; + case Cursor::ColumnResize: + return [NSCursor resizeLeftRightCursor]; + case Cursor::ContextualMenu: + return [NSCursor contextualMenuCursor]; + case Cursor::Copy: + return [NSCursor dragCopyCursor]; + case Cursor::Crosshair: + return [NSCursor crosshairCursor]; + case Cursor::Default: + return [NSCursor arrowCursor]; + case Cursor::DisappearingItem: + return [NSCursor disappearingItemCursor]; + case Cursor::EastResize: + return [NSCursor resizeRightCursor]; + case Cursor::Grab: + return [NSCursor openHandCursor]; + case Cursor::Grabbing: + return [NSCursor closedHandCursor]; + case Cursor::NorthResize: + return [NSCursor resizeUpCursor]; + case Cursor::NoDrop: + return [NSCursor operationNotAllowedCursor]; + case Cursor::NotAllowed: + return [NSCursor operationNotAllowedCursor]; + case Cursor::Pointer: + return [NSCursor pointingHandCursor]; + case Cursor::RowResize: + return [NSCursor resizeUpDownCursor]; + case Cursor::SouthResize: + return [NSCursor resizeDownCursor]; + case Cursor::Text: + return [NSCursor IBeamCursor]; + case Cursor::VerticalText: + return [NSCursor IBeamCursorForVerticalLayout]; + case Cursor::WestResize: + return [NSCursor resizeLeftCursor]; + } +} +#endif // macOS] + + - (void)invalidateLayer { CALayer *layer = self.layer; @@ -606,6 +672,33 @@ - (void)invalidateLayer } else { layer.shadowPath = nil; } + +#if !TARGET_OS_OSX // [visionOS] + // Stage 1.5. Cursor / Hover Effects + if (@available(iOS 17.0, *)) { + UIHoverStyle *hoverStyle = nil; + if (_props->cursor == Cursor::Pointer) { + const RCTCornerInsets cornerInsets = + RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero); +#if TARGET_OS_IOS + // Due to an Apple bug, it seems on iOS, UIShapes made with `[UIShape shapeWithBezierPath:]` + // evaluate their shape on the superviews' coordinate space. This leads to the hover shape + // rendering incorrectly on iOS, iOS apps in compatibility mode on visionOS, but not on visionOS. + // To work around this, for iOS, we can calculate the border path based on `view.frame` (the + // superview's coordinate space) instead of view.bounds. + CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.frame, cornerInsets, NULL); +#else // TARGET_OS_VISION + CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL); +#endif + UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath]; + CGPathRelease(borderPath); + UIShape *shape = [UIShape shapeWithBezierPath:bezierPath]; + + hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverAutomaticEffect effect] shape:shape]; + } + [self setHoverStyle:hoverStyle]; + } +#endif // [visionOS] // Stage 2. Border Rendering const bool useCoreAnimationBorderRendering = diff --git a/packages/react-native/React/Views/RCTCursor.h b/packages/react-native/React/Views/RCTCursor.h index a893cd40b66fa5..5d6727ca2cb1e5 100644 --- a/packages/react-native/React/Views/RCTCursor.h +++ b/packages/react-native/React/Views/RCTCursor.h @@ -5,35 +5,76 @@ * LICENSE file in the root directory of this source tree. */ -#import + #import typedef NS_ENUM(NSInteger, RCTCursor) { + RCTCursorAlias, RCTCursorAuto, - RCTCursorArrow, - RCTCursorIBeam, + RCTCursorColumnResize, + RCTCursorContextualMenu, + RCTCursorCopy, RCTCursorCrosshair, - RCTCursorClosedHand, - RCTCursorOpenHand, - RCTCursorPointingHand, - RCTCursorResizeLeft, - RCTCursorResizeRight, - RCTCursorResizeLeftRight, - RCTCursorResizeUp, - RCTCursorResizeDown, - RCTCursorResizeUpDown, + RCTCursorDefault, RCTCursorDisappearingItem, - RCTCursorIBeamCursorForVerticalLayout, - RCTCursorOperationNotAllowed, - RCTCursorDragLink, - RCTCursorDragCopy, - RCTCursorContextualMenu, + RCTCursorEastResize, + RCTCursorGrab, + RCTCursorGrabbing, + RCTCursorNorthResize, + RCTCursorNoDrop, + RCTCursorNotAllowed, + RCTCursorPointer, + RCTCursorRowResize, + RCTCursorSouthResize, + RCTCursorText, + RCTCursorVerticalText, + RCTCursorWestResize, }; -@interface RCTConvert (RCTCursor) - -+ (RCTCursor)RCTCursor:(id)json; #if TARGET_OS_OSX // [macOS -+ (NSCursor *)NSCursor:(RCTCursor)rctCursor; +inline static NSCursor *NSCursorFromRCTCursor(RCTCursor cursor) +{ + switch (cursor) { + case RCTCursorAlias: + return [NSCursor dragLinkCursor]; + case RCTCursorAuto: + return [NSCursor arrowCursor]; + case RCTCursorColumnResize: + return [NSCursor resizeLeftRightCursor]; + case RCTCursorContextualMenu: + return [NSCursor contextualMenuCursor]; + case RCTCursorCopy: + return [NSCursor dragCopyCursor]; + case RCTCursorCrosshair: + return [NSCursor crosshairCursor]; + case RCTCursorDefault: + return [NSCursor arrowCursor]; + case RCTCursorDisappearingItem: + return [NSCursor disappearingItemCursor]; + case RCTCursorEastResize: + return [NSCursor resizeRightCursor]; + case RCTCursorGrab: + return [NSCursor openHandCursor]; + case RCTCursorGrabbing: + return [NSCursor closedHandCursor]; + case RCTCursorNorthResize: + return [NSCursor resizeUpCursor]; + case RCTCursorNoDrop: + return [NSCursor operationNotAllowedCursor]; + case RCTCursorNotAllowed: + return [NSCursor operationNotAllowedCursor]; + case RCTCursorPointer: + return [NSCursor pointingHandCursor]; + case RCTCursorRowResize: + return [NSCursor resizeUpDownCursor]; + case RCTCursorSouthResize: + return [NSCursor resizeDownCursor]; + case RCTCursorText: + return [NSCursor IBeamCursor]; + case RCTCursorVerticalText: + return [NSCursor IBeamCursorForVerticalLayout]; + case RCTCursorWestResize: + return [NSCursor resizeLeftCursor]; + } +} #endif // macOS] -@end diff --git a/packages/react-native/React/Views/RCTCursor.m b/packages/react-native/React/Views/RCTCursor.m deleted file mode 100644 index a0f3b21146d907..00000000000000 --- a/packages/react-native/React/Views/RCTCursor.m +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -@implementation RCTConvert (RCTCursor) - -RCT_ENUM_CONVERTER( - RCTCursor, - (@{ - @"alias" : @(RCTCursorDragLink), - @"auto" : @(RCTCursorAuto), - @"col-resize" : @(RCTCursorResizeLeftRight), - @"context-menu" : @(RCTCursorContextualMenu), - @"copy" : @(RCTCursorDragCopy), - @"crosshair" : @(RCTCursorCrosshair), - @"default" : @(RCTCursorArrow), - @"disappearing-item" : @(RCTCursorDisappearingItem), - @"e-resize" : @(RCTCursorResizeRight), - @"grab" : @(RCTCursorOpenHand), - @"grabbing" : @(RCTCursorClosedHand), - @"n-resize" : @(RCTCursorResizeUp), - @"no-drop" : @(RCTCursorOperationNotAllowed), - @"not-allowed" : @(RCTCursorOperationNotAllowed), - @"pointer" : @(RCTCursorPointingHand), - @"row-resize" : @(RCTCursorResizeUpDown), - @"s-resize" : @(RCTCursorResizeDown), - @"text" : @(RCTCursorIBeam), - @"vertical-text" : @(RCTCursorIBeamCursorForVerticalLayout), - @"w-resize" : @(RCTCursorResizeLeft), - }), - RCTCursorAuto, - integerValue) - -#if TARGET_OS_OSX // [macOS -+ (NSCursor *)NSCursor:(RCTCursor)rctCursor -{ - NSCursor *cursor; - - switch (rctCursor) { - case RCTCursorArrow: - cursor = [NSCursor arrowCursor]; - break; - case RCTCursorClosedHand: - cursor = [NSCursor closedHandCursor]; - break; - case RCTCursorContextualMenu: - cursor = [NSCursor contextualMenuCursor]; - break; - case RCTCursorCrosshair: - cursor = [NSCursor crosshairCursor]; - break; - case RCTCursorDisappearingItem: - cursor = [NSCursor disappearingItemCursor]; - break; - case RCTCursorDragCopy: - cursor = [NSCursor dragCopyCursor]; - break; - case RCTCursorDragLink: - cursor = [NSCursor dragLinkCursor]; - break; - case RCTCursorIBeam: - cursor = [NSCursor IBeamCursor]; - break; - case RCTCursorIBeamCursorForVerticalLayout: - cursor = [NSCursor IBeamCursorForVerticalLayout]; - break; - case RCTCursorOpenHand: - cursor = [NSCursor openHandCursor]; - break; - case RCTCursorOperationNotAllowed: - cursor = [NSCursor operationNotAllowedCursor]; - break; - case RCTCursorPointingHand: - cursor = [NSCursor pointingHandCursor]; - break; - case RCTCursorResizeDown: - cursor = [NSCursor resizeDownCursor]; - break; - case RCTCursorResizeLeft: - cursor = [NSCursor resizeLeftCursor]; - break; - case RCTCursorResizeLeftRight: - cursor = [NSCursor resizeLeftRightCursor]; - break; - case RCTCursorResizeRight: - cursor = [NSCursor resizeRightCursor]; - break; - case RCTCursorResizeUp: - cursor = [NSCursor resizeUpCursor]; - break; - case RCTCursorResizeUpDown: - cursor = [NSCursor resizeUpDownCursor]; - break; - } - - return cursor; -} -#endif // macOS] - -@end diff --git a/packages/react-native/React/Views/RCTView.h b/packages/react-native/React/Views/RCTView.h index f556254869ef13..da1704914906ae 100644 --- a/packages/react-native/React/Views/RCTView.h +++ b/packages/react-native/React/Views/RCTView.h @@ -10,13 +10,10 @@ #import #import #import +#import // [macOS] [visionOS] #import // [macOS] #import -#if TARGET_OS_OSX // [macOS -#import -#endif // macOS] - #if !TARGET_OS_OSX // [macOS] extern const UIAccessibilityTraits SwitchAccessibilityTrait; #endif // [macOS] @@ -143,6 +140,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; */ @property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; +@property (nonatomic, assign) RCTCursor cursor; + /** * (Experimental and unused for Paper) Pointer event handlers. */ @@ -162,8 +161,6 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; /** * macOS Properties */ -@property (nonatomic, assign) RCTCursor cursor; - @property (nonatomic, assign) CATransform3D transform3D; // `allowsVibrancy` is readonly on NSView, so let's create a new property to make it assignable diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 1c9e45862ad73b..d9481022f067e0 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -179,6 +179,7 @@ - (instancetype)initWithFrame:(CGRect)frame _borderCurve = RCTBorderCurveCircular; _borderStyle = RCTBorderStyleSolid; _hitTestEdgeInsets = UIEdgeInsetsZero; + _cursor = RCTCursorAuto; #if TARGET_OS_OSX // [macOS _transform3D = CATransform3DIdentity; _shadowColor = nil; @@ -1208,7 +1209,11 @@ - (void)displayLayer:(CALayer *)layer } RCTUpdateShadowPathForView(self); - + +#if !TARGET_OS_OSX // [visionOS] + RCTUpdateHoverStyleForView(self); +#endif // [visionOS] + #if TARGET_OS_OSX // [macOS // clipsToBounds is stubbed out on macOS because it's not part of NSView layer.masksToBounds = self.clipsToBounds; @@ -1347,6 +1352,33 @@ static void RCTUpdateShadowPathForView(RCTView *view) } } +#if !TARGET_OS_OSX // [visionOS +static void RCTUpdateHoverStyleForView(RCTView *view) +{ + if (@available(iOS 17.0, *)) { + UIHoverStyle *hoverStyle = nil; + if ([view cursor] == RCTCursorPointer) { + const RCTCornerRadii cornerRadii = [view cornerRadii]; + const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero); +#if TARGET_OS_IOS + // Due to an Apple bug, it seems on iOS, `[UIShape shapeWithBezierPath:]` needs to + // be calculated in the superviews' coordinate space (view.frame). This is not true + // on other platforms like visionOS. + CGPathRef borderPath = RCTPathCreateWithRoundedRect(view.frame, cornerInsets, NULL); +#else // TARGET_OS_VISION + CGPathRef borderPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL); +#endif + UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath]; + CGPathRelease(borderPath); + UIShape *shape = [UIShape shapeWithBezierPath:bezierPath]; + + hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverHighlightEffect effect] shape:shape]; + } + [view setHoverStyle:hoverStyle]; + } +} +#endif // visionOS] + - (void)updateClippingForLayer:(CALayer *)layer { CALayer *mask = nil; @@ -1474,8 +1506,9 @@ - (void)drawFocusRingMask - (void)resetCursorRects { [self discardCursorRects]; - NSCursor *cursor = [RCTConvert NSCursor:self.cursor]; - if (cursor) { + if ([self cursor] != RCTCursorAuto) + { + NSCursor *cursor = NSCursorFromRCTCursor(self.cursor); [self addCursorRect:self.bounds cursor:cursor]; } } diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index 467614a58084c5..c676eca8eb72ec 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -13,6 +13,7 @@ #import "RCTBridge.h" #import "RCTConvert+Transform.h" #import "RCTConvert.h" +#import "RCTCursor.h" // [macOS] [visionOS] #import "RCTLog.h" #import "RCTShadowView.h" #import "RCTTransformOrigin.h" @@ -22,10 +23,6 @@ #import "RCTView.h" #import "UIView+React.h" -#if TARGET_OS_OSX // [macOS -#import "RCTCursor.h" -#endif // macOS] - @implementation RCTConvert (UIAccessibilityTraits) #if !TARGET_OS_OSX // [macOS] @@ -253,6 +250,7 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t) +RCT_EXPORT_VIEW_PROPERTY(cursor, RCTCursor) // [visionOS] #if !TARGET_OS_OSX // [macOS] RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) #else // [macOS @@ -588,7 +586,6 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie #if TARGET_OS_OSX // [macOS #pragma mark - macOS properties -RCT_EXPORT_VIEW_PROPERTY(cursor, RCTCursor) RCT_EXPORT_VIEW_PROPERTY(onMouseEnter, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMouseLeave, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 58bb38b20385c1..59ab0732b4e9ee 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -140,6 +140,15 @@ BaseViewProps::BaseViewProps( "shadowRadius", sourceProps.shadowRadius, {})), + cursor( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.cursor + : convertRawProp( + context, + rawProps, + "cursor", + sourceProps.cursor, + {})), transform( CoreFeatures::enablePropIteratorSetter ? sourceProps.transform : convertRawProp( diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index b7bdd9afc7eb55..9e8d56f6d50a5a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -51,6 +51,8 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { Size shadowOffset{0, -3}; Float shadowOpacity{}; Float shadowRadius{3}; + + Cursor cursor{}; // Transform Transform transform{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index 42575ec6085f35..12a9dd40370e34 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -51,7 +51,7 @@ void ViewShadowNode::initialize() noexcept { viewProps.accessibilityElementsHidden || viewProps.accessibilityViewIsModal || viewProps.importantForAccessibility != ImportantForAccessibility::Auto || - viewProps.removeClippedSubviews || + viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto || HostPlatformViewTraitsInitializer::formsStackingContext(viewProps); bool formsView = formsStackingContext || diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index ca969ed54dd2b3..469740e4242edf 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -686,6 +686,102 @@ inline void fromRawValue( react_native_expect(false); } +// [macOS [visionOS] +inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + Cursor& result) { + result = Cursor::Auto; + react_native_expect(value.hasType()); + if (!value.hasType()) { + return; + } + auto stringValue = (std::string)value; + if (stringValue == "alias") { + result = Cursor::Alias; + return; + } + if (stringValue == "auto") { + result = Cursor::Auto; + return; + } + if (stringValue == "col-resize") { + result = Cursor::ColumnResize; + return; + } + if (stringValue == "context-menu") { + result = Cursor::ContextualMenu; + return; + } + if (stringValue == "copy") { + result = Cursor::Copy; + return; + } + if (stringValue == "crosshair") { + result = Cursor::Crosshair; + return; + } + if (stringValue == "default") { + result = Cursor::Default; + return; + } + if (stringValue == "disappearing-item") { + result = Cursor::DisappearingItem; + return; + } + if (stringValue == "e-resize") { + result = Cursor::EastResize; + return; + } + if (stringValue == "grab") { + result = Cursor::Grab; + return; + } + if (stringValue == "grabbing") { + result = Cursor::Grabbing; + return; + } + if (stringValue == "n-resize") { + result = Cursor::NorthResize; + return; + } + if (stringValue == "no-drop") { + result = Cursor::NoDrop; + return; + } + if (stringValue == "not-allowed") { + result = Cursor::NotAllowed; + return; + } + if (stringValue == "pointer") { + result = Cursor::Pointer; + return; + } + if (stringValue == "row-resize") { + result = Cursor::RowResize; + return; + } + if (stringValue == "s-resize") { + result = Cursor::SouthResize; + return; + } + if (stringValue == "text") { + result = Cursor::Text; + return; + } + if (stringValue == "vertical-text") { + result = Cursor::VerticalText; + return; + } + if (stringValue == "w-resize") { + result = Cursor::WestResize; + return; + } + LOG(ERROR) << "Could not parse Cursor:" << stringValue; + react_native_expect(false); +} +// macOS] [visionOS] + inline void fromRawValue( const PropsParserContext& /*context*/, const RawValue& value, diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index 162f2292cc6a64..b97035e7cde480 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -91,6 +91,31 @@ enum class BorderCurve : uint8_t { Circular, Continuous }; enum class BorderStyle : uint8_t { Solid, Dotted, Dashed }; +// [macOS [visionOS] +enum class Cursor : uint8_t { + Auto, + Alias, + ColumnResize, + ContextualMenu, + Copy, + Crosshair, + Default, + DisappearingItem, + EastResize, + Grab, + Grabbing, + NorthResize, + NoDrop, + NotAllowed, + Pointer, + RowResize, + SouthResize, + Text, + VerticalText, + WestResize, +}; +// macOS] [visionOS] + enum class LayoutConformance : uint8_t { Undefined, Classic, Strict }; template diff --git a/packages/rn-tester/js/examples/Cursor/CursorExample.js b/packages/rn-tester/js/examples/Cursor/CursorExample.js new file mode 100644 index 00000000000000..bba1ed7870850c --- /dev/null +++ b/packages/rn-tester/js/examples/Cursor/CursorExample.js @@ -0,0 +1,195 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +// [macOS] + +const React = require('react'); +const {StyleSheet, Text, View} = require('react-native'); + +const styles = StyleSheet.create({ + invisibleBox: { + width: 100, + height: 100, + }, + box: { + width: 100, + height: 100, + borderWidth: 2, + }, + circle: { + width: 100, + height: 100, + borderWidth: 2, + borderRadius: 100, + }, + halfcircle: { + width: 100, + height: 100, + borderWidth: 2, + borderTopStartRadius: 100, + borderBottomStartRadius: 100, + }, + solid: { + backgroundColor: 'blue', + }, + pointer: { + cursor: 'alias', + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + centerContent: { + justifyContent: 'center', + alignItems: 'center', + }, +}); + +function CursorExampleAuto() { + return ( + + + + + + + + + ); +} + +function CursorExamplePointer() { + return ( + + + + + + + + + ); +} + +function CursorExamplePointer() { + return ( + + + + + + + + + ); +} + +function CursorExampleViewFlattening() { + return ( + + + pointer + + + ); +} + +// [macOS +function CursorExampleMacOS() { + return ( + + <> + + auto + + + default + + + context-menu + + + pointer + + + text + + + vertical-text + + + alias + + + copy + + + not-allowed + + + grab + + + grabbing + + + col-resize + + + row-resize + + + n-resize + + + e-resize + + + s-resize + + + w-resize + + + + ); +} +// macOS] + +exports.title = 'Cursor'; +exports.category = 'UI'; +exports.description = + 'Demonstrates setting a cursor, which affects the appearance when a pointer is over the View.'; +exports.examples = [ + { + title: 'Default', + description: "Cursor: 'auto' or no cursor set ", + render: CursorExampleAuto, + }, + { + title: 'Pointer', + description: 'cursor: pointer', + render: CursorExamplePointer, + }, + { + title: 'Pointer', + description: 'Views with a cursor do not get flattened', + render: CursorExampleViewFlattening, + }, + // [macOS + { + title: 'macOS Cursors', + description: 'macOS supports many more cursors', + render: CursorExampleMacOS, + }, + // macOS] +]; diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index cbecdf8d5334f1..d8e8953e909b98 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -234,7 +234,10 @@ const APIs: Array = ([ key: 'CrashExample', module: require('../examples/Crash/CrashExample'), }, - + { + key: 'CursorExample', + module: require('../examples/Cursor/CursorExample'), + }, { key: 'DevSettings', module: require('../examples/DevSettings/DevSettingsExample'),