Skip to content

Commit

Permalink
Implement cursor style prop for iOS/visionOS
Browse files Browse the repository at this point in the history
  • Loading branch information
Saadnajmi committed Mar 1, 2024
1 parent 3669173 commit 7faae99
Show file tree
Hide file tree
Showing 21 changed files with 631 additions and 177 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderTopStartRadius: true,
cursor: true,
cursor: true, // [macOS] [visionOS]
opacity: true,
pointerEvents: true,

Expand Down
26 changes: 26 additions & 0 deletions packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -267,6 +291,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 =
Expand Down Expand Up @@ -349,4 +374,5 @@ export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
tintColor?: ColorValue | undefined;
opacity?: AnimatableNumericValue | undefined;
objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | undefined;
cursor?: CursorValue | undefined;
}
56 changes: 36 additions & 20 deletions packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,41 @@ 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;

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'
Expand All @@ -42,26 +75,9 @@ export type CursorValue = ?(
| 's-resize'
| 'text'
| 'vertical-text'
| 'w-resize'
);
| 'w-resize';
// macOS]

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`:
Expand Down Expand Up @@ -678,7 +694,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<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
*/

#import <React/RCTBaseTextViewManager.h>
#if TARGET_OS_OSX // [macOS
#import <React/RCTCursor.h>
#endif // macOS]
#import <React/RCTCursor.h> // [macOS]

@implementation RCTBaseTextViewManager

Expand Down
5 changes: 1 addition & 4 deletions packages/react-native/Libraries/Text/RCTTextAttributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,12 @@

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

#import <React/RCTCursor.h> // [macOS]
#import <React/RCTDynamicTypeRamp.h>
#import <React/RCTTextDecorationLineType.h>

#import "RCTTextTransform.h"

#if TARGET_OS_OSX // [macOS
#import <React/RCTCursor.h>
#endif // macOS]

NS_ASSUME_NONNULL_BEGIN

extern NSString *const RCTTextAttributesIsHighlightedAttributeName;
Expand Down
7 changes: 2 additions & 5 deletions packages/react-native/Libraries/Text/RCTTextAttributes.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,10 @@
#import <React/RCTTextAttributes.h>

#import <React/RCTAssert.h>
#import <React/RCTCursor.h> // [macOS]
#import <React/RCTFont.h>
#import <React/RCTLog.h>

#if TARGET_OS_OSX // [macOS
#import <React/RCTCursor.h>
#endif // macOS]

NSString *const RCTTextAttributesIsHighlightedAttributeName = @"RCTTextAttributesIsHighlightedAttributeName";
NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttributeName";

Expand Down Expand Up @@ -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]

Expand Down
3 changes: 3 additions & 0 deletions packages/react-native/React/Base/RCTConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import <React/RCTAnimationType.h>
#import <React/RCTBorderCurve.h>
#import <React/RCTBorderStyle.h>
#import <React/RCTCursor.h> // [macOS] [visionOS]
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
#import <React/RCTPointerEvents.h>
Expand Down Expand Up @@ -89,6 +90,8 @@ typedef NSURL RCTFileURL;
#endif
#endif // [macOS]

+ (RCTCursor)RCTCursor:(id)json; // [macOS] [visionOS]

#if TARGET_OS_OSX // [macOS
+ (NSTextCheckingTypes)NSTextCheckingTypes:(id)json;
#endif // macOS]
Expand Down
33 changes: 32 additions & 1 deletion packages/react-native/React/Base/RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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
Expand Down Expand Up @@ -250,6 +262,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
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) {
Expand Down Expand Up @@ -559,6 +576,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;
Expand Down Expand Up @@ -586,6 +652,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
bool const useCoreAnimationBorderRendering =
Expand Down
Loading

0 comments on commit 7faae99

Please sign in to comment.