Skip to content

Commit dfb303a

Browse files
authored
Implement cursor style prop for iOS/visionOS (#2087)
1 parent 3669173 commit dfb303a

File tree

21 files changed

+614
-177
lines changed

21 files changed

+614
-177
lines changed

packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
135135
borderTopLeftRadius: true,
136136
borderTopRightRadius: true,
137137
borderTopStartRadius: true,
138-
cursor: true,
138+
cursor: true, // [macOS] [visionOS]
139139
opacity: true,
140140
pointerEvents: true,
141141

packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ export type DimensionValue =
2727
type AnimatableNumericValue = number | Animated.AnimatedNode;
2828
type AnimatableStringValue = string | Animated.AnimatedNode;
2929

30+
// [macOS
31+
export type CursorValue =
32+
| 'alias'
33+
| 'auto'
34+
| 'col-resize'
35+
| 'context-menu'
36+
| 'copy'
37+
| 'crosshair'
38+
| 'default'
39+
| 'disappearing-item'
40+
| 'e-resize'
41+
| 'grab'
42+
| 'grabbing'
43+
| 'n-resize'
44+
| 'no-drop'
45+
| 'not-allowed'
46+
| 'pointer'
47+
| 'row-resize'
48+
| 's-resize'
49+
| 'text'
50+
| 'vertical-text'
51+
| 'w-resize';
52+
// macOS]
53+
3054
/**
3155
* Flex Prop Types
3256
* @see https://reactnative.dev/docs/flexbox
@@ -267,6 +291,7 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
267291
* Controls whether the View can be the target of touch events.
268292
*/
269293
pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto' | undefined;
294+
cursor?: CursorValue | undefined;
270295
}
271296

272297
export type FontVariant =
@@ -349,4 +374,5 @@ export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
349374
tintColor?: ColorValue | undefined;
350375
opacity?: AnimatableNumericValue | undefined;
351376
objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | undefined;
377+
cursor?: CursorValue | undefined;
352378
}

packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,24 @@ import type {
2121
} from './private/_StyleSheetTypesOverrides';
2222
import type {____TransformStyle_Internal} from './private/_TransformStyle';
2323

24+
export type ____ColorValue_Internal = null | string | number | NativeColorValue;
25+
export type ColorArrayValue = null | $ReadOnlyArray<____ColorValue_Internal>;
26+
export type PointValue = {
27+
x: number,
28+
y: number,
29+
};
30+
export type EdgeInsetsValue = {
31+
top: number,
32+
left: number,
33+
right: number,
34+
bottom: number,
35+
};
36+
37+
export type DimensionValue = number | string | 'auto' | AnimatedNode | null;
38+
export type AnimatableNumericValue = number | AnimatedNode;
39+
2440
// [macOS
25-
export type CursorValue = ?(
41+
export type CursorValue =
2642
| 'alias'
2743
| 'auto'
2844
| 'col-resize'
@@ -42,26 +58,9 @@ export type CursorValue = ?(
4258
| 's-resize'
4359
| 'text'
4460
| 'vertical-text'
45-
| 'w-resize'
46-
);
61+
| 'w-resize';
4762
// macOS]
4863

49-
export type ____ColorValue_Internal = null | string | number | NativeColorValue;
50-
export type ColorArrayValue = null | $ReadOnlyArray<____ColorValue_Internal>;
51-
export type PointValue = {
52-
x: number,
53-
y: number,
54-
};
55-
export type EdgeInsetsValue = {
56-
top: number,
57-
left: number,
58-
right: number,
59-
bottom: number,
60-
};
61-
62-
export type DimensionValue = number | string | 'auto' | AnimatedNode | null;
63-
export type AnimatableNumericValue = number | AnimatedNode;
64-
6564
/**
6665
* React Native's layout system is based on Flexbox and is powered both
6766
* on iOS and Android by an open source project called `Yoga`:
@@ -678,7 +677,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
678677
opacity?: AnimatableNumericValue,
679678
elevation?: number,
680679
pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only',
681-
cursor?: CursorValue, // [macOS]
680+
cursor?: CursorValue, // [macOS][visionOS]
682681
}>;
683682

684683
export type ____ViewStyle_Internal = $ReadOnly<{

packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.m

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
*/
77

88
#import <React/RCTBaseTextViewManager.h>
9-
#if TARGET_OS_OSX // [macOS
10-
#import <React/RCTCursor.h>
11-
#endif // macOS]
9+
#import <React/RCTCursor.h> // [macOS]
1210

1311
@implementation RCTBaseTextViewManager
1412

packages/react-native/Libraries/Text/RCTTextAttributes.h

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77

88
#import <React/RCTUIKit.h> // [macOS]
99

10+
#import <React/RCTCursor.h> // [macOS]
1011
#import <React/RCTDynamicTypeRamp.h>
1112
#import <React/RCTTextDecorationLineType.h>
1213

1314
#import "RCTTextTransform.h"
1415

15-
#if TARGET_OS_OSX // [macOS
16-
#import <React/RCTCursor.h>
17-
#endif // macOS]
18-
1916
NS_ASSUME_NONNULL_BEGIN
2017

2118
extern NSString *const RCTTextAttributesIsHighlightedAttributeName;

packages/react-native/Libraries/Text/RCTTextAttributes.m

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@
88
#import <React/RCTTextAttributes.h>
99

1010
#import <React/RCTAssert.h>
11+
#import <React/RCTCursor.h> // [macOS]
1112
#import <React/RCTFont.h>
1213
#import <React/RCTLog.h>
1314

14-
#if TARGET_OS_OSX // [macOS
15-
#import <React/RCTCursor.h>
16-
#endif // macOS]
17-
1815
NSString *const RCTTextAttributesIsHighlightedAttributeName = @"RCTTextAttributesIsHighlightedAttributeName";
1916
NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttributeName";
2017

@@ -235,7 +232,7 @@ - (NSParagraphStyle *)effectiveParagraphStyle
235232

236233
#if TARGET_OS_OSX // [macOS
237234
if (_cursor != RCTCursorAuto) {
238-
attributes[NSCursorAttributeName] = [RCTConvert NSCursor:_cursor];
235+
attributes[NSCursorAttributeName] = NSCursorFromRCTCursor(_cursor);
239236
}
240237
#endif // macOS]
241238

packages/react-native/React/Base/RCTConvert.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <React/RCTAnimationType.h>
1212
#import <React/RCTBorderCurve.h>
1313
#import <React/RCTBorderStyle.h>
14+
#import <React/RCTCursor.h> // [macOS] [visionOS]
1415
#import <React/RCTDefines.h>
1516
#import <React/RCTLog.h>
1617
#import <React/RCTPointerEvents.h>
@@ -89,6 +90,8 @@ typedef NSURL RCTFileURL;
8990
#endif
9091
#endif // [macOS]
9192

93+
+ (RCTCursor)RCTCursor:(id)json; // [macOS] [visionOS]
94+
9295
#if TARGET_OS_OSX // [macOS
9396
+ (NSTextCheckingTypes)NSTextCheckingTypes:(id)json;
9497
#endif // macOS]

packages/react-native/React/Base/RCTConvert.m

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,38 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC
551551
}),
552552
UIBarStyleDefault,
553553
integerValue)
554-
#else // [macOS
554+
#endif // [macOS]
555+
556+
// [macOS [visionOS]
557+
RCT_ENUM_CONVERTER(
558+
RCTCursor,
559+
(@{
560+
@"alias" : @(RCTCursorAlias),
561+
@"auto" : @(RCTCursorAuto),
562+
@"col-resize" : @(RCTCursorColumnResize),
563+
@"context-menu" : @(RCTCursorContextualMenu),
564+
@"copy" : @(RCTCursorCopy),
565+
@"crosshair" : @(RCTCursorCrosshair),
566+
@"default" : @(RCTCursorDefault),
567+
@"disappearing-item" : @(RCTCursorDisappearingItem),
568+
@"e-resize" : @(RCTCursorEastResize),
569+
@"grab" : @(RCTCursorGrab),
570+
@"grabbing" : @(RCTCursorGrabbing),
571+
@"n-resize" : @(RCTCursorNorthResize),
572+
@"no-drop" : @(RCTCursorNoDrop),
573+
@"not-allowed" : @(RCTCursorNotAllowed),
574+
@"pointer" : @(RCTCursorPointer),
575+
@"row-resize" : @(RCTCursorRowResize),
576+
@"s-resize" : @(RCTCursorSouthResize),
577+
@"text" : @(RCTCursorText),
578+
@"vertical-text" : @(RCTCursorVerticalText),
579+
@"w-resize" : @(RCTCursorWestResize),
580+
}),
581+
RCTCursorAuto,
582+
integerValue)
583+
// macOS] [visionOS]
584+
585+
#if TARGET_OS_OSX // [macOS
555586
RCT_MULTI_ENUM_CONVERTER(NSTextCheckingTypes, (@{
556587
@"ortography": @(NSTextCheckingTypeOrthography),
557588
@"spelling": @(NSTextCheckingTypeSpelling),

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ - (void)setBackgroundColor:(RCTUIColor *)backgroundColor // [macOS]
8989
_backgroundColor = backgroundColor;
9090
}
9191

92+
#if TARGET_OS_OSX // [macOS
93+
- (void)resetCursorRects
94+
{
95+
[self discardCursorRects];
96+
if (_props->cursor != Cursor::Auto)
97+
{
98+
NSCursor *cursor = NSCursorFromCursor(_props->cursor);
99+
[self addCursorRect:self.bounds cursor:cursor];
100+
}
101+
}
102+
#endif // macOS]
103+
92104
#pragma mark - RCTComponentViewProtocol
93105

94106
+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -250,6 +262,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
250262
if (oldViewProps.backfaceVisibility != newViewProps.backfaceVisibility) {
251263
self.layer.doubleSided = newViewProps.backfaceVisibility == BackfaceVisibility::Visible;
252264
}
265+
266+
// `cursor`
267+
if (oldViewProps.cursor != newViewProps.cursor) {
268+
needsInvalidateLayer = YES;
269+
}
253270

254271
// `shouldRasterize`
255272
if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) {
@@ -559,6 +576,55 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
559576
}
560577
}
561578

579+
#if TARGET_OS_OSX // [macOS
580+
static NSCursor *NSCursorFromCursor(Cursor cursor)
581+
{
582+
switch (cursor) {
583+
case Cursor::Auto:
584+
return [NSCursor arrowCursor];
585+
case Cursor::Alias:
586+
return [NSCursor dragLinkCursor];
587+
case Cursor::ColumnResize:
588+
return [NSCursor resizeLeftRightCursor];
589+
case Cursor::ContextualMenu:
590+
return [NSCursor contextualMenuCursor];
591+
case Cursor::Copy:
592+
return [NSCursor dragCopyCursor];
593+
case Cursor::Crosshair:
594+
return [NSCursor crosshairCursor];
595+
case Cursor::Default:
596+
return [NSCursor arrowCursor];
597+
case Cursor::DisappearingItem:
598+
return [NSCursor disappearingItemCursor];
599+
case Cursor::EastResize:
600+
return [NSCursor resizeRightCursor];
601+
case Cursor::Grab:
602+
return [NSCursor openHandCursor];
603+
case Cursor::Grabbing:
604+
return [NSCursor closedHandCursor];
605+
case Cursor::NorthResize:
606+
return [NSCursor resizeUpCursor];
607+
case Cursor::NoDrop:
608+
return [NSCursor operationNotAllowedCursor];
609+
case Cursor::NotAllowed:
610+
return [NSCursor operationNotAllowedCursor];
611+
case Cursor::Pointer:
612+
return [NSCursor pointingHandCursor];
613+
case Cursor::RowResize:
614+
return [NSCursor resizeUpDownCursor];
615+
case Cursor::SouthResize:
616+
return [NSCursor resizeDownCursor];
617+
case Cursor::Text:
618+
return [NSCursor IBeamCursor];
619+
case Cursor::VerticalText:
620+
return [NSCursor IBeamCursorForVerticalLayout];
621+
case Cursor::WestResize:
622+
return [NSCursor resizeLeftCursor];
623+
}
624+
}
625+
#endif // macOS]
626+
627+
562628
- (void)invalidateLayer
563629
{
564630
CALayer *layer = self.layer;
@@ -586,6 +652,33 @@ - (void)invalidateLayer
586652
} else {
587653
layer.shadowPath = nil;
588654
}
655+
656+
#if !TARGET_OS_OSX // [visionOS]
657+
// Stage 1.5. Cursor / Hover Effects
658+
if (@available(iOS 17.0, *)) {
659+
UIHoverStyle *hoverStyle = nil;
660+
if (_props->cursor == Cursor::Pointer) {
661+
const RCTCornerInsets cornerInsets =
662+
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
663+
#if TARGET_OS_IOS
664+
// Due to an Apple bug, it seems on iOS, UIShapes made with `[UIShape shapeWithBezierPath:]`
665+
// evaluate their shape on the superviews' coordinate space. This leads to the hover shape
666+
// rendering incorrectly on iOS, iOS apps in compatibility mode on visionOS, but not on visionOS.
667+
// To work around this, for iOS, we can calculate the border path based on `view.frame` (the
668+
// superview's coordinate space) instead of view.bounds.
669+
CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.frame, cornerInsets, NULL);
670+
#else // TARGET_OS_VISION
671+
CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL);
672+
#endif
673+
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath];
674+
CGPathRelease(borderPath);
675+
UIShape *shape = [UIShape shapeWithBezierPath:bezierPath];
676+
677+
hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverAutomaticEffect effect] shape:shape];
678+
}
679+
[self setHoverStyle:hoverStyle];
680+
}
681+
#endif // [visionOS]
589682

590683
// Stage 2. Border Rendering
591684
bool const useCoreAnimationBorderRendering =

0 commit comments

Comments
 (0)