Skip to content
Draft
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
30 changes: 20 additions & 10 deletions packages/react-native/React/Base/RCTUIKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ extern "C" {

// UIGraphics.h
CGContextRef UIGraphicsGetCurrentContext(void);
CGImageRef UIImageGetCGImageRef(NSImage *image);

#ifdef __cplusplus
}
Expand Down Expand Up @@ -330,28 +329,39 @@ NS_INLINE NSEdgeInsets UIEdgeInsetsMake(CGFloat top, CGFloat left, CGFloat botto
#define UIApplication NSApplication

// UIImage
@compatibility_alias UIImage NSImage;
// RCTUIImage is a subclass of NSImage that caches its CGImage representation.
// This is needed because NSImage's CGImageForProposedRect: returns a new autoreleased
// CGImage each time, which causes issues when used with CALayer.contents.
@interface RCTUIImage : NSImage
@property (nonatomic, readonly, nullable) CGImageRef CGImage;
@end

@compatibility_alias UIImage RCTUIImage;

typedef NS_ENUM(NSInteger, UIImageRenderingMode) {
UIImageRenderingModeAlwaysOriginal,
UIImageRenderingModeAlwaysTemplate,
};

#ifdef __cplusplus
extern "C"
extern "C" {
#endif
CGFloat UIImageGetScale(NSImage *image);

CGImageRef UIImageGetCGImageRef(NSImage *image);
CGFloat UIImageGetScale(RCTUIImage *image);
CGImageRef UIImageGetCGImageRef(RCTUIImage *image);

#ifdef __cplusplus
}
#endif

NS_INLINE UIImage *UIImageWithContentsOfFile(NSString *filePath)
NS_INLINE RCTUIImage *UIImageWithContentsOfFile(NSString *filePath)
{
return [[NSImage alloc] initWithContentsOfFile:filePath];
return [[RCTUIImage alloc] initWithContentsOfFile:filePath];
}

NS_INLINE UIImage *UIImageWithData(NSData *imageData)
NS_INLINE RCTUIImage *UIImageWithData(NSData *imageData)
{
return [[NSImage alloc] initWithData:imageData];
return [[RCTUIImage alloc] initWithData:imageData];
}

NSData *UIImagePNGRepresentation(NSImage *image);
Expand Down Expand Up @@ -624,7 +634,7 @@ typedef void (^RCTUIGraphicsImageDrawingActions)(RCTUIGraphicsImageRendererConte

- (instancetype)initWithSize:(CGSize)size;
- (instancetype)initWithSize:(CGSize)size format:(RCTUIGraphicsImageRendererFormat *)format;
- (NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions;
- (RCTUIImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions;

@end
NS_ASSUME_NONNULL_END
Expand Down
92 changes: 76 additions & 16 deletions packages/react-native/React/Base/macOS/RCTUIKit.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ CGContextRef UIGraphicsGetCurrentContext(void)

// UIImage

CGFloat UIImageGetScale(NSImage *image)
CGFloat UIImageGetScale(RCTUIImage *image)
{
if (image == nil) {
return 0.0;
Expand All @@ -76,9 +76,33 @@ CGFloat UIImageGetScale(NSImage *image)
return 1.0;
}

CGImageRef __nullable UIImageGetCGImageRef(NSImage *image)
// RCTUIImage - NSImage subclass with cached CGImage

@implementation RCTUIImage {
CGImageRef _cachedCGImage;
}

- (void)dealloc {
if (_cachedCGImage != NULL) {
CGImageRelease(_cachedCGImage);
}
}

- (CGImageRef)CGImage {
if (_cachedCGImage == NULL) {
CGImageRef cgImage = [self CGImageForProposedRect:NULL context:NULL hints:NULL];
if (cgImage != NULL) {
_cachedCGImage = CGImageRetain(cgImage);
}
}
return _cachedCGImage;
}

@end

CGImageRef __nullable UIImageGetCGImageRef(RCTUIImage *image)
{
return [image CGImageForProposedRect:NULL context:NULL hints:NULL];
return image.CGImage;
}

static NSData *NSImageDataForFileType(NSImage *image, NSBitmapImageFileType fileType, NSDictionary<NSString *, id> *properties)
Expand Down Expand Up @@ -825,19 +849,55 @@ - (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull RCTUIGraphicsI
return self;
}

- (nonnull NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions {

NSImage *image = [NSImage imageWithSize:_size
flipped:YES
drawingHandler:^BOOL(NSRect dstRect) {

RCTUIGraphicsImageRendererContext *context = [NSGraphicsContext currentContext];
if (self->_format.opaque) {
CGContextSetAlpha([context CGContext], 1.0);
}
actions(context);
return YES;
}];
- (nonnull RCTUIImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions {
// Create an RCTUIImage which caches its CGImage for efficient layer.contents usage.
// We draw into a bitmap context and create the image from that.

CGFloat scale = _format.scale > 0 ? _format.scale : [[NSScreen mainScreen] backingScaleFactor];
NSInteger pixelWidth = (NSInteger)(_size.width * scale);
NSInteger pixelHeight = (NSInteger)(_size.height * scale);

if (pixelWidth <= 0 || pixelHeight <= 0) {
return [[RCTUIImage alloc] initWithSize:_size];
}

// Create a bitmap context
NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:pixelWidth
pixelsHigh:pixelHeight
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bytesPerRow:0
bitsPerPixel:0];

bitmapRep.size = _size;

NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapRep];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:context];

// Flip the context to match iOS coordinate system (origin at top-left)
CGContextRef cgContext = [context CGContext];
CGContextTranslateCTM(cgContext, 0, _size.height);
CGContextScaleCTM(cgContext, 1.0, -1.0);

if (_format.opaque) {
CGContextSetAlpha(cgContext, 1.0);
}

// Execute the drawing actions
actions(context);

[NSGraphicsContext restoreGraphicsState];

// Create an RCTUIImage from the bitmap representation
RCTUIImage *image = [[RCTUIImage alloc] initWithSize:_size];
[image addRepresentation:bitmapRep];

return image;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,9 @@ static void RCTAddContourEffectToLayer(
layer.contents = (id)image.CGImage;
layer.contentsScale = image.scale;
#else // [macOS
layer.contents = (__bridge id) UIImageGetCGImageRef(image);
// RCTUIImage caches its CGImage, so it stays valid as long as the image is alive.
// The image is retained by the layer.contents assignment.
layer.contents = (__bridge id)image.CGImage;
layer.contentsScale = UIImageGetScale(image);
#endif // macOS]

Expand Down Expand Up @@ -1015,10 +1017,20 @@ - (RCTUIView *)currentContainerView // [macOS]
if (_useCustomContainerView) {
if (!_containerView) {
_containerView = [[RCTPlatformView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; // [macOS]
#if TARGET_OS_OSX // [macOS
_containerView.wantsLayer = YES;
#endif // macOS]
for (RCTPlatformView *subview in self.subviews) { // [macOS]
[_containerView addSubview:subview];
}
#if !TARGET_OS_OSX // [macOS]
_containerView.clipsToBounds = self.clipsToBounds;
#else // [macOS
// On macOS, clipsToBounds doesn't automatically set layer.masksToBounds
// like it does on iOS, so we need to set it directly.
_containerView.clipsToBounds = _props->getClipsContentToBounds();
_containerView.layer.masksToBounds = _props->getClipsContentToBounds();
#endif // macOS]
self.clipsToBounds = NO;
_containerView.layer.mask = self.layer.mask;
self.layer.mask = nil;
Expand Down Expand Up @@ -1050,8 +1062,15 @@ - (void)invalidateLayer
}

#if TARGET_OS_OSX // [macOS
// clipsToBounds is stubbed out on macOS because it's not part of NSView
layer.masksToBounds = self.clipsToBounds;
// On macOS, clipsToBounds doesn't automatically set layer.masksToBounds like iOS does.
// When _useCustomContainerView is true (boxShadow + overflow:hidden), the container
// view handles clipping children while the main layer stays unclipped for the shadow.
// The container view's masksToBounds is set in currentContainerView getter.
if (_useCustomContainerView) {
layer.masksToBounds = NO;
} else {
layer.masksToBounds = _props->getClipsContentToBounds();
}
#endif // macOS]

const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
Expand Down Expand Up @@ -1189,6 +1208,10 @@ - (void)invalidateLayer
[layer addSublayer:borderLayer];
_borderLayer = borderLayer;
}
#if TARGET_OS_OSX // [macOS
// Update frame on every call in case view was resized
_borderLayer.frame = layer.bounds;
#endif // macOS]

layer.borderWidth = 0;
layer.borderColor = nil;
Expand Down Expand Up @@ -1289,7 +1312,8 @@ - (void)invalidateLayer
if (!_props->boxShadow.empty()) {
_boxShadowLayer = [CALayer layer];
[self.layer addSublayer:_boxShadowLayer];
_boxShadowLayer.zPosition = _borderLayer.zPosition;
// Box shadow should be behind all content but still visible
_boxShadowLayer.zPosition = BACKGROUND_COLOR_ZPOSITION - 1;
_boxShadowLayer.frame = RCTGetBoundingRect(_props->boxShadow, self.layer.bounds.size);

UIImage *boxShadowImage = RCTGetBoxShadowImage(
Expand All @@ -1301,7 +1325,8 @@ - (void)invalidateLayer
#if !TARGET_OS_OSX // [macOS]
_boxShadowLayer.contents = (id)boxShadowImage.CGImage;
#else // [macOS
_boxShadowLayer.contents = (__bridge id)UIImageGetCGImageRef(boxShadowImage);
// RCTUIImage caches its CGImage, so it stays valid as long as the image is alive.
_boxShadowLayer.contents = (__bridge id)boxShadowImage.CGImage;
#endif // macOS]
}

Expand Down
Loading