Skip to content

Commit

Permalink
feat(iOS): Implement cursor style prop
Browse files Browse the repository at this point in the history
  • Loading branch information
Saadnajmi committed Mar 1, 2024
1 parent 8ff05b5 commit 7c857a1
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderTopStartRadius: true,
cursor: true,
opacity: true,
pointerEvents: true,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type DimensionValue =
type AnimatableNumericValue = number | Animated.AnimatedNode;
type AnimatableStringValue = string | Animated.AnimatedNode;

export type CursorValue = 'auto' | 'pointer';

/**
* Flex Prop Types
* @see https://reactnative.dev/docs/flexbox
Expand Down Expand Up @@ -274,6 +276,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 @@ -403,4 +406,5 @@ export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
tintColor?: ColorValue | undefined;
opacity?: AnimatableNumericValue | undefined;
objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | undefined;
cursor?: CursorValue | undefined;
}
3 changes: 3 additions & 0 deletions packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export type EdgeInsetsValue = {
export type DimensionValue = number | string | 'auto' | AnimatedNode | null;
export type AnimatableNumericValue = number | AnimatedNode;

export type CursorValue = 'auto' | 'pointer';

/**
* 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 @@ -729,6 +731,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
opacity?: AnimatableNumericValue,
elevation?: number,
pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only',
cursor?: CursorValue,
}>;

export type ____ViewStyle_Internal = $ReadOnly<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7417,6 +7417,7 @@ export type EdgeInsetsValue = {
};
export type DimensionValue = number | string | \\"auto\\" | AnimatedNode | null;
export type AnimatableNumericValue = number | AnimatedNode;
export type CursorValue = \\"auto\\" | \\"pointer\\";
type ____LayoutStyle_Internal = $ReadOnly<{
display?: \\"none\\" | \\"flex\\",
width?: DimensionValue,
Expand Down Expand Up @@ -7567,6 +7568,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
opacity?: AnimatableNumericValue,
elevation?: number,
pointerEvents?: \\"auto\\" | \\"none\\" | \\"box-none\\" | \\"box-only\\",
cursor?: CursorValue,
}>;
export type ____ViewStyle_Internal = $ReadOnly<{
...____ViewStyle_InternalCore,
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>
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
#import <React/RCTPointerEvents.h>
Expand Down Expand Up @@ -80,6 +81,8 @@ typedef NSURL RCTFileURL;
+ (UIBarStyle)UIBarStyle:(id)json __deprecated;
#endif

+ (RCTCursor)RCTCursor:(id)json;

+ (CGFloat)CGFloat:(id)json;
+ (CGPoint)CGPoint:(id)json;
+ (CGSize)CGSize:(id)json;
Expand Down
9 changes: 9 additions & 0 deletions packages/react-native/React/Base/RCTConvert.mm
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,15 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC
UIBarStyleDefault,
integerValue)

RCT_ENUM_CONVERTER(
RCTCursor,
(@{
@"auto" : @(RCTCursorAuto),
@"pointer" : @(RCTCursorPointer),
}),
RCTCursorAuto,
integerValue)

static void convertCGStruct(const char *type, NSArray *fields, CGFloat *result, id json)
{
NSUInteger count = fields.count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,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 @@ -591,6 +596,31 @@ - (void)invalidateLayer
} else {
layer.shadowPath = nil;
}

// 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];
}

// Stage 2. Border Rendering
const bool useCoreAnimationBorderRendering =
Expand Down
14 changes: 14 additions & 0 deletions packages/react-native/React/Views/RCTCursor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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.
*/

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, RCTCursor) {
RCTCursorAuto,
RCTCursorPointer,
};

3 changes: 3 additions & 0 deletions packages/react-native/React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#import <React/RCTBorderCurve.h>
#import <React/RCTBorderStyle.h>
#import <React/RCTCursor.h>
#import <React/RCTComponent.h>
#import <React/RCTPointerEvents.h>

Expand Down Expand Up @@ -120,6 +121,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
*/
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;

@property (nonatomic, assign) RCTCursor cursor;

/**
* (Experimental and unused for Paper) Pointer event handlers.
*/
Expand Down
28 changes: 28 additions & 0 deletions packages/react-native/React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_borderCurve = RCTBorderCurveCircular;
_borderStyle = RCTBorderStyleSolid;
_hitTestEdgeInsets = UIEdgeInsetsZero;
_cursor = RCTCursorAuto;

_backgroundColor = super.backgroundColor;
}
Expand Down Expand Up @@ -796,6 +797,8 @@ - (void)displayLayer:(CALayer *)layer

RCTUpdateShadowPathForView(self);

RCTUpdateHoverStyleForView(self);

const RCTCornerRadii cornerRadii = [self cornerRadii];
const UIEdgeInsets borderInsets = [self bordersAsInsets];
const RCTBorderColors borderColors = [self borderColorsWithTraitCollection:self.traitCollection];
Expand Down Expand Up @@ -891,6 +894,31 @@ static void RCTUpdateShadowPathForView(RCTView *view)
}
}

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];
}
}

- (void)updateClippingForLayer:(CALayer *)layer
{
CALayer *mask = nil;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#import "RCTBridge.h"
#import "RCTConvert+Transform.h"
#import "RCTConvert.h"
#import "RCTCursor.h"
#import "RCTLog.h"
#import "RCTShadowView.h"
#import "RCTUIManager.h"
Expand Down Expand Up @@ -195,6 +196,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)
RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat)
RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor)
RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
Size shadowOffset{0, -3};
Float shadowOpacity{};
Float shadowRadius{3};

Cursor cursor{};

// Transform
Transform transform{};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,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 ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,28 @@ inline void fromRawValue(
react_native_expect(false);
}

inline void fromRawValue(
const PropsParserContext& context,
const RawValue& value,
Cursor& result) {
result = Cursor::Auto;
react_native_expect(value.hasType<std::string>());
if (!value.hasType<std::string>()) {
return;
}
auto stringValue = (std::string)value;
if (stringValue == "auto") {
result = Cursor::Auto;
return;
}
if (stringValue == "pointer") {
result = Cursor::Pointer;
return;
}
LOG(ERROR) << "Could not parse Cursor:" << stringValue;
react_native_expect(false);
}

inline void fromRawValue(
const PropsParserContext& /*context*/,
const RawValue& value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ enum class BorderCurve : uint8_t { Circular, Continuous };

enum class BorderStyle : uint8_t { Solid, Dotted, Dashed };

enum class Cursor : uint8_t { Auto, Pointer };

enum class LayoutConformance : uint8_t { Undefined, Classic, Strict };

template <typename T>
Expand Down
Loading

0 comments on commit 7c857a1

Please sign in to comment.