diff --git a/React/Views/NSView+React.h b/React/Views/NSView+React.h index 3fee72d909..f9d469debf 100644 --- a/React/Views/NSView+React.h +++ b/React/Views/NSView+React.h @@ -113,5 +113,9 @@ * UIKit replacement */ @property (nonatomic, assign) BOOL clipsToBounds; +@property (nonatomic, assign) CATransform3D transform; + +/** Populate the `layer` ivar when nil */ +- (void)ensureLayerExists; @end diff --git a/React/Views/NSView+React.m b/React/Views/NSView+React.m index 382e3f3afe..daae6304b3 100644 --- a/React/Views/NSView+React.m +++ b/React/Views/NSView+React.m @@ -175,10 +175,10 @@ - (void)reactSetFrame:(CGRect)frame return; } + [self ensureLayerExists]; self.frame = frame; - - // TODO: why position matters? It's only produce bugs - // OSX requires position and anchor point to rotate from center + + // Ensure the anchorPoint is in the center. self.layer.position = position; self.layer.bounds = bounds; self.layer.anchorPoint = anchor; @@ -295,4 +295,26 @@ - (NSView *)reactAccessibilityElement return self; } +#pragma mark - Other + +- (void)ensureLayerExists +{ + if (!self.layer) { + self.wantsLayer = YES; + self.layer.delegate = (id)self; + } +} + +- (CATransform3D)transform +{ + return CATransform3DIdentity; +} + +- (void)setTransform:(__unused CATransform3D)transform +{ + // Do nothing by default. + // Native views must synthesize their own "transform" property, + // override "displayLayer:", and apply the transform there. +} + @end diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 5ed0ffd558..fb70bd8d55 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -46,8 +46,6 @@ */ + (NSEdgeInsets)contentInsetsForView:(NSView *)curView; -- (void)setBackgroundColor:(NSColor *)backgroundColor; - /** * Layout direction of the view. * This is inherited from UIView+React, but we override it here @@ -114,19 +112,17 @@ @property (nonatomic, assign) CGFloat borderWidth; /** - * Initial tranformation for a view which is not rendered yet + * Border styles. */ +@property (nonatomic, assign) RCTBorderStyle borderStyle; + + @property (nonatomic, assign) CATransform3D transform; -@property (nonatomic, assign) bool shouldBeTransformed; +@property (nonatomic, copy) NSColor *backgroundColor; @property (nonatomic, copy) RCTDirectEventBlock onDragEnter; @property (nonatomic, copy) RCTDirectEventBlock onDragLeave; @property (nonatomic, copy) RCTDirectEventBlock onDrop; @property (nonatomic, copy) RCTDirectEventBlock onContextMenuItemClick; -/** - * Border styles. - */ -@property (nonatomic, assign) RCTBorderStyle borderStyle; - @end diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 5446f0e680..90f42709c0 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -124,6 +124,7 @@ - (instancetype)initWithFrame:(CGRect)frame _borderBottomStartRadius = -1; _borderBottomEndRadius = -1; _borderStyle = RCTBorderStyleSolid; + _transform = CATransform3DIdentity; self.clipsToBounds = NO; } @@ -192,6 +193,7 @@ - (void)setPointerEvents:(RCTPointerEvents)pointerEvents - (void)setTransform:(CATransform3D)transform { _transform = transform; + [self setNeedsDisplay:YES]; } - (NSView *)hitTest:(CGPoint)point @@ -424,22 +426,11 @@ - (NSColor *)backgroundColor - (void)setBackgroundColor:(NSColor *)backgroundColor { - if ([_backgroundColor isEqual:backgroundColor]) { - return; - } - if (backgroundColor == nil) { - [self setWantsLayer:NO]; - self.layer = NULL; - return; - } - if (![self wantsLayer] || self.layer == nil) { - [self setWantsLayer:YES]; - self.layer.delegate = self; - } - [self.layer setBackgroundColor:[backgroundColor CGColor]]; - [self.layer setNeedsDisplay]; - [self setNeedsDisplay:YES]; _backgroundColor = backgroundColor; + + [self ensureLayerExists]; + self.layer.backgroundColor = backgroundColor.CGColor; + [self.layer setNeedsDisplay]; } static CGFloat RCTDefaultIfNegativeTo(CGFloat defaultValue, CGFloat x) { @@ -573,28 +564,29 @@ - (void)reactSetFrame:(CGRect)frame [super reactSetFrame:frame]; if (!CGSizeEqualToSize(self.bounds.size, oldSize)) { [self.layer setNeedsDisplay]; + } else if (!CATransform3DIsIdentity(_transform)) { + [self applyTransform:self.layer]; } } -- (void)ensureLayerExists +- (void)applyTransform:(CALayer *)layer { - if (!self.layer) { - // Set `wantsLayer` first to create a "layer-backed view" instead of a "layer-hosting view". - self.wantsLayer = YES; - - CALayer *layer = [CALayer layer]; - layer.delegate = self; - self.layer = layer; + if (!CATransform3DEqualToTransform(_transform, layer.transform)) { + layer.transform = _transform; + // Enable edge antialiasing in perspective transforms + layer.edgeAntialiasingMask = !(_transform.m34 == 0.0f); } } - (void)displayLayer:(CALayer *)layer { - if (self.shouldBeTransformed) { - self.layer.transform = self.transform; - self.shouldBeTransformed = NO; - } - + // Applying the transform here ensures it's not overridden by AppKit internals. + [self applyTransform:layer]; + + // Ensure the anchorPoint is in the center. + layer.position = (CGPoint){CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)}; + layer.anchorPoint = (CGPoint){0.5, 0.5}; + if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) { return; } diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index 23d50cd915..99c41555e9 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -119,4 +119,24 @@ RCT_REMAP_VIEW_PROPERTY(name, __custom__, type) \ RCT_REMAP_SHADOW_PROPERTY(name, __custom__, type) \ - (void)set_##name:(id)json forShadowView:(viewClass *)view +/** + * This macro maps a named property to an arbitrary key path in the view's layer. + */ +#define RCT_REMAP_LAYER_PROPERTY(name, keyPath, type) \ +RCT_CUSTOM_VIEW_PROPERTY(name, type, RCTView) \ +{ \ + if (json) { \ + [view ensureLayerExists]; \ + view.layer.keyPath = [RCTConvert type:json]; \ + } else { \ + view.layer.keyPath = defaultView.layer.keyPath; \ + } \ +} + +/** + * This handles the simple case, where JS and native property names match. + */ +#define RCT_EXPORT_LAYER_PROPERTY(name, type) \ +RCT_REMAP_LAYER_PROPERTY(name, name, type) + @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index b172e7a330..6605ae98c1 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -88,16 +88,6 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio return nil; } -- (void)checkLayerExists:(NSView *)view -{ - if (!view.layer) { - [view setWantsLayer:YES]; - CALayer *viewLayer = [CALayer layer]; - viewLayer.delegate = (id)view; - [view setLayer:viewLayer]; - } -} - #pragma mark - View properties #if TARGET_OS_TV @@ -121,11 +111,12 @@ - (void)checkLayerExists:(NSView *)view RCT_REMAP_VIEW_PROPERTY(testID, reactAccessibilityElement.accessibilityIdentifier, NSString) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, NSColor) -RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t) -RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor) -RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize) -RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float) -RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) +RCT_REMAP_LAYER_PROPERTY(backfaceVisibility, doubleSided, css_backface_visibility_t) +RCT_EXPORT_LAYER_PROPERTY(opacity, float) +RCT_EXPORT_LAYER_PROPERTY(shadowColor, CGColor) +RCT_EXPORT_LAYER_PROPERTY(shadowOffset, CGSize) +RCT_EXPORT_LAYER_PROPERTY(shadowOpacity, float) +RCT_EXPORT_LAYER_PROPERTY(shadowRadius, CGFloat) RCT_REMAP_VIEW_PROPERTY(toolTip, toolTip, NSString) RCT_CUSTOM_VIEW_PROPERTY(overflow, YGOverflow, RCTView) { @@ -137,23 +128,14 @@ - (void)checkLayerExists:(NSView *)view } RCT_CUSTOM_VIEW_PROPERTY(shouldRasterizeIOS, BOOL, RCTView) { + if (json) { + [view ensureLayerExists]; + } view.layer.shouldRasterize = json ? [RCTConvert BOOL:json] : defaultView.layer.shouldRasterize; view.layer.rasterizationScale = view.layer.shouldRasterize ? [NSScreen mainScreen].backingScaleFactor : defaultView.layer.rasterizationScale; } -RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RCTView) -{ - CATransform3D transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform; - if ([view respondsToSelector:@selector(shouldBeTransformed)] && !view.superview) { - view.shouldBeTransformed = YES; - view.transform = transform; - } else { - view.layer.transform = transform; - } - - // TODO: Improve this by enabling edge antialiasing only for transforms with rotation or skewing - view.layer.edgeAntialiasingMask = !CATransform3DIsIdentity(transform); -} +RCT_EXPORT_VIEW_PROPERTY(transform, CATransform3D) RCT_CUSTOM_VIEW_PROPERTY(draggedTypes, NSArray*, RCTView) { @@ -164,18 +146,6 @@ - (void)checkLayerExists:(NSView *)view [view registerForDraggedTypes:defaultView.registeredDraggedTypes]; } } - -RCT_CUSTOM_VIEW_PROPERTY(opacity, float, RCTView) -{ - if (json) { - [self checkLayerExists:view]; - [view.layer setOpacity:[RCTConvert float:json]]; - } else { - [view.layer setOpacity:1]; - } -} - - RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTPointerEvents, RCTView) { if ([view respondsToSelector:@selector(setPointerEvents:)]) { @@ -183,17 +153,18 @@ - (void)checkLayerExists:(NSView *)view return; } } - - RCT_CUSTOM_VIEW_PROPERTY(removeClippedSubviews, BOOL, RCTView) { if ([view respondsToSelector:@selector(setRemoveClippedSubviews:)]) { view.removeClippedSubviews = json ? [RCTConvert BOOL:json] : defaultView.removeClippedSubviews; } } -RCT_CUSTOM_VIEW_PROPERTY(borderRadius, CGFloat, RCTView) { +RCT_CUSTOM_VIEW_PROPERTY(borderRadius, CGFloat, RCTView) +{ + if (json) { + [view ensureLayerExists]; + } if ([view respondsToSelector:@selector(setBorderRadius:)]) { - [self checkLayerExists:view]; view.borderRadius = json ? [RCTConvert CGFloat:json] : defaultView.borderRadius; } else { view.layer.cornerRadius = json ? [RCTConvert CGFloat:json] : defaultView.layer.cornerRadius; @@ -201,8 +172,10 @@ - (void)checkLayerExists:(NSView *)view } RCT_CUSTOM_VIEW_PROPERTY(borderColor, CGColor, RCTView) { + if (json) { + [view ensureLayerExists]; + } if ([view respondsToSelector:@selector(setBorderColor:)]) { - [self checkLayerExists:view]; view.borderColor = json ? [RCTConvert CGColor:json] : defaultView.borderColor; } else { view.layer.borderColor = json ? [RCTConvert CGColor:json] : defaultView.layer.borderColor; @@ -210,8 +183,10 @@ - (void)checkLayerExists:(NSView *)view } RCT_CUSTOM_VIEW_PROPERTY(borderWidth, float, RCTView) { + if (json) { + [view ensureLayerExists]; + } if ([view respondsToSelector:@selector(setBorderWidth:)]) { - [self checkLayerExists:view]; view.borderWidth = json ? [RCTConvert CGFloat:json] : defaultView.borderWidth; } else { view.layer.borderWidth = json ? [RCTConvert CGFloat:json] : defaultView.layer.borderWidth; @@ -219,6 +194,9 @@ - (void)checkLayerExists:(NSView *)view } RCT_CUSTOM_VIEW_PROPERTY(borderStyle, RCTBorderStyle, RCTView) { + if (json) { + [view ensureLayerExists]; + } if ([view respondsToSelector:@selector(setBorderStyle:)]) { view.borderStyle = json ? [RCTConvert RCTBorderStyle:json] : defaultView.borderStyle; } @@ -262,14 +240,14 @@ - (void)checkLayerExists:(NSView *)view RCT_CUSTOM_VIEW_PROPERTY(border##SIDE##Width, float, RCTView) \ { \ if ([view respondsToSelector:@selector(setBorder##SIDE##Width:)]) { \ - [self checkLayerExists:view]; \ + [view ensureLayerExists]; \ view.border##SIDE##Width = json ? [RCTConvert CGFloat:json] : defaultView.border##SIDE##Width; \ } \ } \ RCT_CUSTOM_VIEW_PROPERTY(border##SIDE##Color, NSColor, RCTView) \ { \ if ([view respondsToSelector:@selector(setBorder##SIDE##Color:)]) { \ - [self checkLayerExists:view]; \ + [view ensureLayerExists]; \ view.border##SIDE##Color = json ? [RCTConvert CGColor:json] : defaultView.border##SIDE##Color; \ } \ }