Skip to content
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 @@ -536,7 +536,7 @@ - (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event //
}

for (RCTPlatformView *subview in [_containerView.subviews reverseObjectEnumerator]) { // [macOS]
RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, [subview convertPoint:point fromView:self], event); // [macOS]
RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, point, self, event); // [macOS]
if (hitView) {
return hitView;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,17 +367,12 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
auto newTransform = newViewProps.resolveTransform(_layoutMetrics);
CATransform3D caTransform = RCTCATransform3DFromTransformMatrix(newTransform);
#if TARGET_OS_OSX // [macOS
CGPoint anchorPoint = self.layer.anchorPoint;
if (CGPointEqualToPoint(anchorPoint, CGPointZero) && !CATransform3DEqualToTransform(caTransform, CATransform3DIdentity)) {
// https://developer.apple.com/documentation/quartzcore/calayer/1410817-anchorpoint
// This compensates for the fact that layer.anchorPoint is {0, 0} instead of {0.5, 0.5} on macOS for some reason.
CATransform3D originAdjust = CATransform3DTranslate(CATransform3DIdentity, self.frame.size.width / 2, self.frame.size.height / 2, 0);
caTransform = CATransform3DConcat(CATransform3DConcat(CATransform3DInvert(originAdjust), caTransform), originAdjust);
}
#if !TARGET_OS_OSX // [macOS]
self.layer.transform = caTransform;
#else // [macOS
self.transform3D = caTransform;
#endif // macOS]

self.layer.transform = caTransform;
// Enable edge antialiasing in rotation, skew, or perspective transforms
self.layer.allowsEdgeAntialiasing = caTransform.m12 != 0.0f || caTransform.m21 != 0.0f || caTransform.m34 != 0.0f;
}
Expand Down Expand Up @@ -718,7 +713,11 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
if ((_props->transformOrigin.isSet() || _props->transform.operations.size() > 0) &&
layoutMetrics.frame.size != oldLayoutMetrics.frame.size) {
auto newTransform = _props->resolveTransform(layoutMetrics);
#if !TARGET_OS_OSX // [macOS]
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
#else // [macOS
self.transform3D = RCTCATransform3DFromTransformMatrix(newTransform);
#endif // macOS]
}
}

Expand Down Expand Up @@ -812,7 +811,7 @@ - (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event //
}

for (RCTPlatformView *subview in [self.subviews reverseObjectEnumerator]) { // [macOS]
RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, [subview convertPoint:point fromView:self], event); // [macOS]
RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, point, self, event); // [macOS]
if (hitView) {
return hitView;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Modules/RCTUIManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1186,7 +1186,7 @@ - (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag viewName:(NSStrin
{
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTPlatformView *> *viewRegistry) { // [macOS]
RCTPlatformView *view = viewRegistry[reactTag]; // [macOS]
RCTPlatformView *target = RCTUIViewHitTestWithEvent(view, point, nil); // [macOS]
RCTPlatformView *target = RCTUIViewHitTestWithEvent(view, point, view, nil); // [macOS]
CGRect frame = [target convertRect:target.bounds toView:view];

while (target.reactTag == nil && target.superview != nil) {
Expand Down
16 changes: 10 additions & 6 deletions packages/react-native/React/RCTUIKit/RCTUIView.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ NS_ASSUME_NONNULL_BEGIN

@property (nonatomic, copy) NSColor *backgroundColor;
@property (nonatomic) CGAffineTransform transform;
@property (nonatomic) CATransform3D transform3D;

/**
* Specifies whether the view should receive the mouse down event when the
Expand All @@ -75,7 +76,6 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, assign) BOOL enableFocusRing;

// [macOS
/**
* iOS compatibility shim. On macOS, this forwards to accessibilityChildren.
*/
Expand All @@ -90,9 +90,9 @@ NS_ASSUME_NONNULL_BEGIN

#if !TARGET_OS_OSX

UIKIT_STATIC_INLINE RCTPlatformView *RCTUIViewHitTestWithEvent(RCTPlatformView *view, CGPoint point, __unused UIEvent *__nullable event)
UIKIT_STATIC_INLINE RCTPlatformView *RCTUIViewHitTestWithEvent(RCTPlatformView *view, CGPoint point, RCTPlatformView *fromView, __unused UIEvent *__nullable event)
{
return [view hitTest:point withEvent:event];
return [view hitTest:[view convertPoint:point fromView:fromView] withEvent:event];
}

UIKIT_STATIC_INLINE void RCTUIViewSetContentModeRedraw(UIView *view)
Expand All @@ -107,11 +107,15 @@ UIKIT_STATIC_INLINE BOOL RCTUIViewIsDescendantOfView(RCTPlatformView *view, RCTP

#else // TARGET_OS_OSX

NS_INLINE RCTPlatformView *RCTUIViewHitTestWithEvent(RCTPlatformView *view, CGPoint point, __unused UIEvent *__nullable event)
// Use CALayer coordinate conversion which correctly accounts for layer.transform.
// NSView's convertPoint:fromView: does not account for layer transforms on macOS.
// IMPORTANT -- NSView's hitTest: expects a point in the superview's coordinate space,
// so we convert from fromView → superview using CALayer, which handles layer transforms correctly.
// This allows hit testing to work correctly between nested RCTUIViews and plain NSViews.
NS_INLINE RCTPlatformView *RCTUIViewHitTestWithEvent(RCTPlatformView *view, CGPoint point, RCTPlatformView *fromView, __unused UIEvent *__nullable event)
{
// [macOS IMPORTANT -- point is in local coordinate space, but OSX expects super coordinate space for hitTest:
NSView *superview = [view superview];
NSPoint pointInSuperview = superview != nil ? [view convertPoint:point toView:superview] : point;
NSPoint pointInSuperview = superview != nil ? [superview.layer convertPoint:point fromLayer:fromView.layer] : point;
return [view hitTest:pointInSuperview];
}

Expand Down
52 changes: 44 additions & 8 deletions packages/react-native/React/RCTUIKit/RCTUIView.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#if TARGET_OS_OSX

#import <QuartzCore/QuartzCore.h>
#import <React/RCTUIView.h>

// UIView
Expand All @@ -21,6 +22,8 @@ @implementation RCTUIView
BOOL _userInteractionEnabled;
BOOL _mouseDownCanMoveWindow;
BOOL _respondsToDisplayLayer;
CATransform3D _transform3D;
BOOL _hasCustomTransform3D;
}

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
Expand Down Expand Up @@ -51,6 +54,8 @@ @implementation RCTUIView
self->_enableFocusRing = YES;
self->_mouseDownCanMoveWindow = YES;
self->_respondsToDisplayLayer = [self respondsToSelector:@selector(displayLayer:)];
self->_transform3D = CATransform3DIdentity;
self->_hasCustomTransform3D = NO;
}
return self;
}
Expand Down Expand Up @@ -127,20 +132,46 @@ - (CGAffineTransform)transform

- (void)setTransform:(CGAffineTransform)transform
{
self.layer.affineTransform = transform;
self.transform3D = CATransform3DMakeAffineTransform(transform);
}

- (CATransform3D)transform3D
{
return _transform3D;
}

- (void)setTransform3D:(CATransform3D)transform3D
{
// On macOS, layer.anchorPoint defaults to {0, 0} instead of {0.5, 0.5} on iOS.
// Compensate so transforms are applied from the view's center as expected.
CGPoint anchorPoint = self.layer.anchorPoint;
if (CGPointEqualToPoint(anchorPoint, CGPointZero) && !CATransform3DEqualToTransform(transform3D, CATransform3DIdentity)) {
CATransform3D originAdjust = CATransform3DTranslate(CATransform3DIdentity, self.frame.size.width / 2, self.frame.size.height / 2, 0);
transform3D = CATransform3DConcat(CATransform3DConcat(CATransform3DInvert(originAdjust), transform3D), originAdjust);
}

_transform3D = transform3D;
_hasCustomTransform3D = !CATransform3DEqualToTransform(transform3D, CATransform3DIdentity);
self.layer.transform = transform3D;
}

- (NSView *)hitTest:(NSPoint)point
{
// IMPORTANT point is passed in super coordinates by OSX, but expected to be passed in local coordinates
NSView *superview = [self superview];
NSPoint pointInSelf = superview != nil ? [self convertPoint:point fromView:superview] : point;
return [self hitTest:pointInSelf withEvent:nil];
// NSView's hitTest: receives a point in superview coordinates. Convert to local
// coordinates using CALayer, which correctly accounts for layer.transform.
// NSView's convertPoint:fromView: does NOT account for layer transforms.
CGPoint localPoint;
if (self.layer.superlayer) {
localPoint = [self.layer convertPoint:point fromLayer:self.layer.superlayer];
} else {
localPoint = point;
}
return [self hitTest:localPoint withEvent:nil];
}

- (BOOL)wantsUpdateLayer
{
return [self respondsToSelector:@selector(displayLayer:)];
return _respondsToDisplayLayer || _hasCustomTransform3D;
}

- (void)updateLayer
Expand All @@ -153,8 +184,13 @@ - (void)updateLayer
[layer setBackgroundColor:[_backgroundColor CGColor]];
}

// In Fabric, wantsUpdateLayer is always enabled and doesn't guarantee that
// the instance has a displayLayer method.
// On macOS, AppKit's layer-backed view system resets layer.transform to identity
// during its layout/display cycle because NSView has no built-in transform property
// (unlike UIView on iOS). We must re-apply the stored transform after each cycle.
if (_hasCustomTransform3D && !CATransform3DEqualToTransform(layer.transform, _transform3D)) {
layer.transform = _transform3D;
}

if (_respondsToDisplayLayer) {
[(id<CALayerDelegate>)self displayLayer:layer];
}
Expand Down
3 changes: 1 addition & 2 deletions packages/react-native/React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ - (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS
// of the hit view will return YES from -pointInside:withEvent:). See:
// - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
for (RCTUIView *subview in [sortedSubviews reverseObjectEnumerator]) { // [macOS]
CGPoint pointForHitTest = [subview convertPoint:point fromView:self];
hitSubview = RCTUIViewHitTestWithEvent(subview, pointForHitTest, event); // macOS]
hitSubview = RCTUIViewHitTestWithEvent(subview, point, self, event); // [macOS]
if (hitSubview != nil) {
break;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/UIView+React.m
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ - (BOOL)isReactRootView

- (NSNumber *)reactTagAtPoint:(CGPoint)point
{
RCTPlatformView *view = RCTUIViewHitTestWithEvent(self, point, nil); // [macOS]
RCTPlatformView *view = RCTUIViewHitTestWithEvent(self, point, self, nil); // [macOS]
while (view && !view.reactTag) {
view = view.superview;
}
Expand Down
Loading