Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement cursor: pointer for iOS / visionOS #2080

Merged
merged 1 commit into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,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 @@ -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 =
Expand Down Expand Up @@ -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;
}
41 changes: 20 additions & 21 deletions packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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`:
Expand Down Expand Up @@ -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<{
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.mm
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 @@ -84,6 +85,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 @@ -549,7 +549,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 @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 =
Expand Down
Loading
Loading