From 508038abfd8452ff129907bc8bcc124fd9c7d018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 1 Jul 2026 11:23:57 +0200 Subject: [PATCH 1/6] perf(ios): only apply layer mask to UIImageView when none circular border --- .../View/RCTViewComponentView.mm | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 5571b5350bff..a1e82d07969c 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -995,6 +995,7 @@ - (void)invalidateLayer } const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); + BOOL const borderRadiiCircular = areBorderRadiiCircular(borderMetrics.borderRadii); // Stage 1. Shadow Path BOOL const layerHasShadow = layer.shadowOpacity > 0 && CGColorGetAlpha(layer.shadowColor) > 0; @@ -1044,7 +1045,7 @@ - (void)invalidateLayer const bool useCoreAnimationBorderRendering = borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() && borderMetrics.borderStyles.isUniform() && borderMetrics.borderStyles.left == BorderStyle::Solid && - areBorderRadiiCircular(borderMetrics.borderRadii) && + borderRadiiCircular && ( // iOS draws borders in front of the content whereas CSS draws them behind // the content. For this reason, only use iOS border drawing when clipping @@ -1126,7 +1127,7 @@ - (void)invalidateLayer _outlineLayer.frame = CGRectInset( layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth); - if (areBorderRadiiCircular(borderMetrics.borderRadii) && borderMetrics.borderRadii.topLeft.horizontal == 0) { + if (borderRadiiCircular && borderMetrics.borderRadii.topLeft.horizontal == 0) { UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); _outlineLayer.borderWidth = _props->outlineWidth; _outlineLayer.borderColor = outlineColor.CGColor; @@ -1302,7 +1303,7 @@ - (void)invalidateLayer if (self.currentContainerView.clipsToBounds) { BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(); if (!clipToPaddingBox) { - if (areBorderRadiiCircular(borderMetrics.borderRadii)) { + if (borderRadiiCircular) { self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; } else { CALayer *maskLayer = @@ -1312,20 +1313,26 @@ - (void)invalidateLayer self.currentContainerView.layer.mask = maskLayer; } - for (UIView *subview in self.currentContainerView.subviews) { - if ([subview isKindOfClass:[UIImageView class]]) { - RCTCornerInsets cornerInsets = RCTGetCornerInsets( - RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), - RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths)); - - // If the subview is an image view, we have to apply the mask directly to the image view's layer, - // otherwise the image might overflow with the border radius. - subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets]; + if (!borderRadiiCircular && + (borderMetrics.borderColors.left || borderMetrics.borderColors.right || borderMetrics.borderColors.top || + borderMetrics.borderColors.bottom)) { + for (UIView *subview in self.currentContainerView.subviews) { + if ([subview isKindOfClass:[UIImageView class]]) { + RCTCornerInsets cornerInsets = RCTGetCornerInsets( + RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), + RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths)); + + // If the subview is an image view, we have to apply the mask directly to the image view's layer, + // otherwise the image might overflow with the border radius. + // Applying a mask is rendering wise expensive so we only apply it when needed, which is only + // for none uniform border radii (that are actually visible by color). + subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets]; + } } } } else if ( !borderMetrics.borderWidths.isUniform() || borderMetrics.borderWidths.left != 0 || - !areBorderRadiiCircular(borderMetrics.borderRadii)) { + !borderRadiiCircular) { CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame()) cornerInsets:RCTGetCornerInsets( RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), From 8acdcafc638096e8c2ffe9c24fb9e27d2e87e2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 1 Jul 2026 11:29:14 +0200 Subject: [PATCH 2/6] the wrapping RCTViewComponentView does the clipping, this is unnecessary here --- .../Mounting/ComponentViews/Image/RCTImageComponentView.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm index 99104f0ec8fc..49a2b2d42a18 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm @@ -31,7 +31,6 @@ - (instancetype)initWithFrame:(CGRect)frame _props = defaultProps; _imageView = [RCTUIImageViewAnimated new]; - _imageView.clipsToBounds = YES; _imageView.contentMode = RCTContentModeFromImageResizeMode(defaultProps->resizeMode); _imageView.layer.minificationFilter = kCAFilterTrilinear; _imageView.layer.magnificationFilter = kCAFilterTrilinear; From 0238d1128a4b0480969fb9b9382858b46ef9dd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 1 Jul 2026 11:30:11 +0200 Subject: [PATCH 3/6] example: add example of none circular uniform border radius --- packages/rn-tester/js/examples/Image/ImageExample.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 55d4a8e7da26..b9cb2f08b609 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -1051,6 +1051,13 @@ const styles = StyleSheet.create({ flex: { flex: 1, }, + imageWithUniformButNoneCircularBorderRadius: { + width: 200, + height: 100, + borderRadius: '50%', + borderWidth: 4, + borderColor: 'blue', + }, imageWithBorderRadius: { borderRadius: 5, }, @@ -1420,6 +1427,10 @@ exports.examples = [ render: function (): React.Node { return ( + Date: Thu, 2 Jul 2026 13:11:09 +0200 Subject: [PATCH 4/6] option A --- .../View/RCTViewComponentView.mm | 142 +++++++++++++++--- 1 file changed, 124 insertions(+), 18 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index a1e82d07969c..c0f28ab9424f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -867,6 +867,98 @@ static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve) } } +struct RCTLayerCornerConfiguration { + CGFloat cornerRadius{0}; + CACornerMask maskedCorners{0}; + BorderCurve cornerCurve{BorderCurve::Circular}; + bool hasRoundedCorner{false}; +}; + +static bool RCTUpdateLayerCornerConfiguration( + const CornerRadii &cornerRadii, + BorderCurve cornerCurve, + CACornerMask cornerMask, + RCTLayerCornerConfiguration &cornerConfiguration) +{ + if (cornerRadii.horizontal != cornerRadii.vertical) { + return false; + } + + if (cornerRadii.horizontal == 0) { + return true; + } + + CGFloat cornerRadius = (CGFloat)cornerRadii.horizontal; + if (cornerConfiguration.hasRoundedCorner) { + if (cornerConfiguration.cornerRadius != cornerRadius) { + return false; + } + + if (cornerConfiguration.cornerCurve != cornerCurve) { + return false; + } + } else { + cornerConfiguration.cornerRadius = cornerRadius; + cornerConfiguration.cornerCurve = cornerCurve; + cornerConfiguration.hasRoundedCorner = true; + } + + cornerConfiguration.maskedCorners |= cornerMask; + return true; +} + +static bool RCTGetLayerCornerConfiguration( + const BorderMetrics &borderMetrics, + RCTLayerCornerConfiguration &cornerConfiguration) +{ + cornerConfiguration = RCTLayerCornerConfiguration{}; + + bool topLeftIsRepresentable = RCTUpdateLayerCornerConfiguration( + borderMetrics.borderRadii.topLeft, + borderMetrics.borderCurves.topLeft, + kCALayerMinXMinYCorner, + cornerConfiguration); + if (!topLeftIsRepresentable) { + return false; + } + + bool topRightIsRepresentable = RCTUpdateLayerCornerConfiguration( + borderMetrics.borderRadii.topRight, + borderMetrics.borderCurves.topRight, + kCALayerMaxXMinYCorner, + cornerConfiguration); + if (!topRightIsRepresentable) { + return false; + } + + bool bottomLeftIsRepresentable = RCTUpdateLayerCornerConfiguration( + borderMetrics.borderRadii.bottomLeft, + borderMetrics.borderCurves.bottomLeft, + kCALayerMinXMaxYCorner, + cornerConfiguration); + if (!bottomLeftIsRepresentable) { + return false; + } + + bool bottomRightIsRepresentable = RCTUpdateLayerCornerConfiguration( + borderMetrics.borderRadii.bottomRight, + borderMetrics.borderCurves.bottomRight, + kCALayerMaxXMaxYCorner, + cornerConfiguration); + if (!bottomRightIsRepresentable) { + return false; + } + + if (!cornerConfiguration.hasRoundedCorner) { + cornerConfiguration.maskedCorners = kCALayerMinXMinYCorner + | kCALayerMaxXMinYCorner + | kCALayerMinXMaxYCorner + | kCALayerMaxXMaxYCorner; + } + + return true; +} + static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) { switch (borderStyle) { @@ -995,7 +1087,6 @@ - (void)invalidateLayer } const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); - BOOL const borderRadiiCircular = areBorderRadiiCircular(borderMetrics.borderRadii); // Stage 1. Shadow Path BOOL const layerHasShadow = layer.shadowOpacity > 0 && CGColorGetAlpha(layer.shadowColor) > 0; @@ -1042,10 +1133,14 @@ - (void)invalidateLayer [self setHoverStyle:hoverStyle]; } #endif + + RCTLayerCornerConfiguration layerCornerConfiguration; + const bool layerCornersAreRepresentable = RCTGetLayerCornerConfiguration(borderMetrics, layerCornerConfiguration); + const bool useCoreAnimationBorderRendering = borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() && borderMetrics.borderStyles.isUniform() && borderMetrics.borderStyles.left == BorderStyle::Solid && - borderRadiiCircular && + layerCornersAreRepresentable && ( // iOS draws borders in front of the content whereas CSS draws them behind // the content. For this reason, only use iOS border drawing when clipping @@ -1086,8 +1181,9 @@ - (void)invalidateLayer layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; UIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); layer.borderColor = borderColor.CGColor; - layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal; - layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft); + layer.cornerRadius = layerCornerConfiguration.cornerRadius; + layer.maskedCorners = layerCornerConfiguration.maskedCorners; + layer.cornerCurve = CornerCurveFromBorderCurve(layerCornerConfiguration.cornerCurve); } else { if (!_borderLayer) { CALayer *borderLayer = [CALayer new]; @@ -1127,7 +1223,7 @@ - (void)invalidateLayer _outlineLayer.frame = CGRectInset( layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth); - if (borderRadiiCircular && borderMetrics.borderRadii.topLeft.horizontal == 0) { + if (layerCornersAreRepresentable && layerCornerConfiguration.cornerRadius == 0) { UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); _outlineLayer.borderWidth = _props->outlineWidth; _outlineLayer.borderColor = outlineColor.CGColor; @@ -1299,24 +1395,28 @@ - (void)invalidateLayer } // clipping - self.currentContainerView.layer.mask = nil; - if (self.currentContainerView.clipsToBounds) { + UIView *currentContainerView = self.currentContainerView; + currentContainerView.layer.mask = nil; + if (currentContainerView.clipsToBounds) { BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(); if (!clipToPaddingBox) { - if (borderRadiiCircular) { - self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; + if (layerCornersAreRepresentable) { + currentContainerView.layer.cornerRadius = layerCornerConfiguration.cornerRadius; + currentContainerView.layer.maskedCorners = layerCornerConfiguration.maskedCorners; + currentContainerView.layer.cornerCurve = CornerCurveFromBorderCurve(layerCornerConfiguration.cornerCurve); } else { CALayer *maskLayer = [self createMaskLayer:self.bounds cornerInsets:RCTGetCornerInsets( RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)]; - self.currentContainerView.layer.mask = maskLayer; + currentContainerView.layer.cornerRadius = 0; + currentContainerView.layer.mask = maskLayer; } - if (!borderRadiiCircular && + if (!layerCornersAreRepresentable && (borderMetrics.borderColors.left || borderMetrics.borderColors.right || borderMetrics.borderColors.top || borderMetrics.borderColors.bottom)) { - for (UIView *subview in self.currentContainerView.subviews) { + for (UIView *subview in currentContainerView.subviews) { if ([subview isKindOfClass:[UIImageView class]]) { RCTCornerInsets cornerInsets = RCTGetCornerInsets( RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), @@ -1332,14 +1432,16 @@ - (void)invalidateLayer } } else if ( !borderMetrics.borderWidths.isUniform() || borderMetrics.borderWidths.left != 0 || - !borderRadiiCircular) { + !layerCornersAreRepresentable) { CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame()) cornerInsets:RCTGetCornerInsets( RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))]; - self.currentContainerView.layer.mask = maskLayer; + currentContainerView.layer.mask = maskLayer; } else { - self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; + currentContainerView.layer.cornerRadius = layerCornerConfiguration.cornerRadius; + currentContainerView.layer.maskedCorners = layerCornerConfiguration.maskedCorners; + currentContainerView.layer.cornerCurve = CornerCurveFromBorderCurve(layerCornerConfiguration.cornerCurve); } } } @@ -1351,10 +1453,13 @@ - (void)shapeLayerToMatchView:(CALayer *)layer borderMetrics:(BorderMetrics)bord // Bounds is needed here to account for scaling transforms properly and ensure // we do not scale twice layer.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height); - if (areBorderRadiiCircular(borderMetrics.borderRadii)) { + RCTLayerCornerConfiguration cornerConfiguration; + const bool cornersAreRepresentable = RCTGetLayerCornerConfiguration(borderMetrics, cornerConfiguration); + if (cornersAreRepresentable) { layer.mask = nil; - layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; - layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft); + layer.cornerRadius = cornerConfiguration.cornerRadius; + layer.maskedCorners = cornerConfiguration.maskedCorners; + layer.cornerCurve = CornerCurveFromBorderCurve(cornerConfiguration.cornerCurve); } else { CAShapeLayer *maskLayer = [self createMaskLayer:self.bounds @@ -1721,6 +1826,7 @@ - (void)transferVisualPropertiesFromView:(UIView *)sourceView toView:(UIView *)d // corner destinationView.layer.cornerRadius = sourceView.layer.cornerRadius; sourceView.layer.cornerRadius = 0; + destinationView.layer.maskedCorners = sourceView.layer.maskedCorners; destinationView.layer.cornerCurve = sourceView.layer.cornerCurve; // custom layers From 92b5d3e606fc765f56fd3fa753e063809c284318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 2 Jul 2026 14:23:30 +0200 Subject: [PATCH 5/6] refactor: moved to seperate file + consolidate in loop + added helper RCTApplyLayerCornerConfiguration --- .../View/RCTViewComponentView.mm | 133 ++---------------- .../Utils/RCTLayerCornerConfiguration.h | 38 +++++ .../Utils/RCTLayerCornerConfiguration.mm | 91 ++++++++++++ 3 files changed, 140 insertions(+), 122 deletions(-) create mode 100644 packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.h create mode 100644 packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.mm diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index c0f28ab9424f..43511e4372b4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -11,6 +11,7 @@ #import #import #import +#import #import #import @@ -19,6 +20,7 @@ #import #import #import +#import #import #import #import @@ -855,110 +857,6 @@ static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors bor .right = RCTUIColorFromSharedColor(borderColors.right)}; } -static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve) -{ - // The constants are available only starting from iOS 13 - // CALayerCornerCurve is a typealias on NSString * - switch (borderCurve) { - case BorderCurve::Continuous: - return @"continuous"; // kCACornerCurveContinuous; - case BorderCurve::Circular: - return @"circular"; // kCACornerCurveCircular; - } -} - -struct RCTLayerCornerConfiguration { - CGFloat cornerRadius{0}; - CACornerMask maskedCorners{0}; - BorderCurve cornerCurve{BorderCurve::Circular}; - bool hasRoundedCorner{false}; -}; - -static bool RCTUpdateLayerCornerConfiguration( - const CornerRadii &cornerRadii, - BorderCurve cornerCurve, - CACornerMask cornerMask, - RCTLayerCornerConfiguration &cornerConfiguration) -{ - if (cornerRadii.horizontal != cornerRadii.vertical) { - return false; - } - - if (cornerRadii.horizontal == 0) { - return true; - } - - CGFloat cornerRadius = (CGFloat)cornerRadii.horizontal; - if (cornerConfiguration.hasRoundedCorner) { - if (cornerConfiguration.cornerRadius != cornerRadius) { - return false; - } - - if (cornerConfiguration.cornerCurve != cornerCurve) { - return false; - } - } else { - cornerConfiguration.cornerRadius = cornerRadius; - cornerConfiguration.cornerCurve = cornerCurve; - cornerConfiguration.hasRoundedCorner = true; - } - - cornerConfiguration.maskedCorners |= cornerMask; - return true; -} - -static bool RCTGetLayerCornerConfiguration( - const BorderMetrics &borderMetrics, - RCTLayerCornerConfiguration &cornerConfiguration) -{ - cornerConfiguration = RCTLayerCornerConfiguration{}; - - bool topLeftIsRepresentable = RCTUpdateLayerCornerConfiguration( - borderMetrics.borderRadii.topLeft, - borderMetrics.borderCurves.topLeft, - kCALayerMinXMinYCorner, - cornerConfiguration); - if (!topLeftIsRepresentable) { - return false; - } - - bool topRightIsRepresentable = RCTUpdateLayerCornerConfiguration( - borderMetrics.borderRadii.topRight, - borderMetrics.borderCurves.topRight, - kCALayerMaxXMinYCorner, - cornerConfiguration); - if (!topRightIsRepresentable) { - return false; - } - - bool bottomLeftIsRepresentable = RCTUpdateLayerCornerConfiguration( - borderMetrics.borderRadii.bottomLeft, - borderMetrics.borderCurves.bottomLeft, - kCALayerMinXMaxYCorner, - cornerConfiguration); - if (!bottomLeftIsRepresentable) { - return false; - } - - bool bottomRightIsRepresentable = RCTUpdateLayerCornerConfiguration( - borderMetrics.borderRadii.bottomRight, - borderMetrics.borderCurves.bottomRight, - kCALayerMaxXMaxYCorner, - cornerConfiguration); - if (!bottomRightIsRepresentable) { - return false; - } - - if (!cornerConfiguration.hasRoundedCorner) { - cornerConfiguration.maskedCorners = kCALayerMinXMinYCorner - | kCALayerMaxXMinYCorner - | kCALayerMinXMaxYCorner - | kCALayerMaxXMaxYCorner; - } - - return true; -} - static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) { switch (borderStyle) { @@ -1134,8 +1032,9 @@ - (void)invalidateLayer } #endif - RCTLayerCornerConfiguration layerCornerConfiguration; - const bool layerCornersAreRepresentable = RCTGetLayerCornerConfiguration(borderMetrics, layerCornerConfiguration); + const std::optional layerCornerConfiguration = + RCTGetLayerCornerConfiguration(borderMetrics); + const bool layerCornersAreRepresentable = layerCornerConfiguration.has_value(); const bool useCoreAnimationBorderRendering = borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() && @@ -1181,9 +1080,7 @@ - (void)invalidateLayer layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; UIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); layer.borderColor = borderColor.CGColor; - layer.cornerRadius = layerCornerConfiguration.cornerRadius; - layer.maskedCorners = layerCornerConfiguration.maskedCorners; - layer.cornerCurve = CornerCurveFromBorderCurve(layerCornerConfiguration.cornerCurve); + RCTApplyLayerCornerConfiguration(layer, *layerCornerConfiguration); } else { if (!_borderLayer) { CALayer *borderLayer = [CALayer new]; @@ -1223,7 +1120,7 @@ - (void)invalidateLayer _outlineLayer.frame = CGRectInset( layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth); - if (layerCornersAreRepresentable && layerCornerConfiguration.cornerRadius == 0) { + if (layerCornersAreRepresentable && layerCornerConfiguration->cornerRadius == 0) { UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); _outlineLayer.borderWidth = _props->outlineWidth; _outlineLayer.borderColor = outlineColor.CGColor; @@ -1401,9 +1298,7 @@ - (void)invalidateLayer BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(); if (!clipToPaddingBox) { if (layerCornersAreRepresentable) { - currentContainerView.layer.cornerRadius = layerCornerConfiguration.cornerRadius; - currentContainerView.layer.maskedCorners = layerCornerConfiguration.maskedCorners; - currentContainerView.layer.cornerCurve = CornerCurveFromBorderCurve(layerCornerConfiguration.cornerCurve); + RCTApplyLayerCornerConfiguration(currentContainerView.layer, *layerCornerConfiguration); } else { CALayer *maskLayer = [self createMaskLayer:self.bounds @@ -1439,9 +1334,7 @@ - (void)invalidateLayer RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))]; currentContainerView.layer.mask = maskLayer; } else { - currentContainerView.layer.cornerRadius = layerCornerConfiguration.cornerRadius; - currentContainerView.layer.maskedCorners = layerCornerConfiguration.maskedCorners; - currentContainerView.layer.cornerCurve = CornerCurveFromBorderCurve(layerCornerConfiguration.cornerCurve); + RCTApplyLayerCornerConfiguration(currentContainerView.layer, *layerCornerConfiguration); } } } @@ -1453,13 +1346,9 @@ - (void)shapeLayerToMatchView:(CALayer *)layer borderMetrics:(BorderMetrics)bord // Bounds is needed here to account for scaling transforms properly and ensure // we do not scale twice layer.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height); - RCTLayerCornerConfiguration cornerConfiguration; - const bool cornersAreRepresentable = RCTGetLayerCornerConfiguration(borderMetrics, cornerConfiguration); - if (cornersAreRepresentable) { + if (const auto cornerConfiguration = RCTGetLayerCornerConfiguration(borderMetrics)) { layer.mask = nil; - layer.cornerRadius = cornerConfiguration.cornerRadius; - layer.maskedCorners = cornerConfiguration.maskedCorners; - layer.cornerCurve = CornerCurveFromBorderCurve(cornerConfiguration.cornerCurve); + RCTApplyLayerCornerConfiguration(layer, *cornerConfiguration); } else { CAShapeLayer *maskLayer = [self createMaskLayer:self.bounds diff --git a/packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.h b/packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.h new file mode 100644 index 000000000000..d54f303ccc1c --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.h @@ -0,0 +1,38 @@ +/* + * 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 + +#import +#import + +/* + * Describes how a view's border radii can be rendered through CoreAnimation's + * `cornerRadius` / `maskedCorners` fast path instead of a `CAShapeLayer` mask. + */ +struct RCTLayerCornerConfiguration { + CGFloat cornerRadius{0}; + CACornerMask maskedCorners{0}; + facebook::react::BorderCurve cornerCurve{facebook::react::BorderCurve::Circular}; + bool hasRoundedCorner{false}; +}; + +/* + * Returns a corner configuration when `borderMetrics` can be represented with a + * single `cornerRadius` + `maskedCorners`, i.e. every rounded corner shares the + * same circular radius and curve. Returns `std::nullopt` when the radii require + * a mask layer instead (an elliptical corner, or differing radii/curves between + * rounded corners). + */ +std::optional RCTGetLayerCornerConfiguration( + const facebook::react::BorderMetrics &borderMetrics); + +/* + * Applies a corner configuration to a layer's `cornerRadius`, `maskedCorners` + * and `cornerCurve`. + */ +void RCTApplyLayerCornerConfiguration(CALayer *layer, const RCTLayerCornerConfiguration &cornerConfiguration); diff --git a/packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.mm b/packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.mm new file mode 100644 index 000000000000..7f7fdf849f15 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTLayerCornerConfiguration.mm @@ -0,0 +1,91 @@ +/* + * 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 "RCTLayerCornerConfiguration.h" + +using namespace facebook::react; + +static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve) +{ + switch (borderCurve) { + case BorderCurve::Continuous: + return kCACornerCurveContinuous; + case BorderCurve::Circular: + return kCACornerCurveCircular; + } +} + +static bool RCTUpdateLayerCornerConfiguration( + const CornerRadii &cornerRadii, + BorderCurve cornerCurve, + CACornerMask cornerMask, + RCTLayerCornerConfiguration &cornerConfiguration) +{ + if (cornerRadii.horizontal != cornerRadii.vertical) { + return false; + } + + if (cornerRadii.horizontal == 0) { + return true; + } + + CGFloat cornerRadius = (CGFloat)cornerRadii.horizontal; + if (cornerConfiguration.hasRoundedCorner) { + if (cornerConfiguration.cornerRadius != cornerRadius) { + return false; + } + + if (cornerConfiguration.cornerCurve != cornerCurve) { + return false; + } + } else { + cornerConfiguration.cornerRadius = cornerRadius; + cornerConfiguration.cornerCurve = cornerCurve; + cornerConfiguration.hasRoundedCorner = true; + } + + cornerConfiguration.maskedCorners |= cornerMask; + return true; +} + +std::optional RCTGetLayerCornerConfiguration(const BorderMetrics &borderMetrics) +{ + RCTLayerCornerConfiguration cornerConfiguration; + + const struct { + const CornerRadii &radii; + BorderCurve curve; + CACornerMask mask; + } corners[] = { + {borderMetrics.borderRadii.topLeft, borderMetrics.borderCurves.topLeft, kCALayerMinXMinYCorner}, + {borderMetrics.borderRadii.topRight, borderMetrics.borderCurves.topRight, kCALayerMaxXMinYCorner}, + {borderMetrics.borderRadii.bottomLeft, borderMetrics.borderCurves.bottomLeft, kCALayerMinXMaxYCorner}, + {borderMetrics.borderRadii.bottomRight, borderMetrics.borderCurves.bottomRight, kCALayerMaxXMaxYCorner}, + }; + + for (const auto &corner : corners) { + bool isRepresentable = + RCTUpdateLayerCornerConfiguration(corner.radii, corner.curve, corner.mask, cornerConfiguration); + if (!isRepresentable) { + return std::nullopt; + } + } + + if (!cornerConfiguration.hasRoundedCorner) { + cornerConfiguration.maskedCorners = + kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner | kCALayerMinXMaxYCorner | kCALayerMaxXMaxYCorner; + } + + return cornerConfiguration; +} + +void RCTApplyLayerCornerConfiguration(CALayer *layer, const RCTLayerCornerConfiguration &cornerConfiguration) +{ + layer.cornerRadius = cornerConfiguration.cornerRadius; + layer.maskedCorners = cornerConfiguration.maskedCorners; + layer.cornerCurve = CornerCurveFromBorderCurve(cornerConfiguration.cornerCurve); +} From 1cde52e0f4676aaa1c8cde54d1ab8a51fc59a402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 2 Jul 2026 16:32:12 +0200 Subject: [PATCH 6/6] chore(ios): update cxx api snapshots --- scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 7 +++++++ scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api | 7 +++++++ scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 4dd76e3aa931..eb8d5b2ec30d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -3189,6 +3189,13 @@ struct RCTFontProperties { public UIFontWeight weight; } +struct RCTLayerCornerConfiguration { + public CACornerMask maskedCorners; + public CGFloat cornerRadius; + public bool hasRoundedCorner; + public facebook::react::BorderCurve cornerCurve; +} + struct RCTLayoutContext { public CGPoint absolutePosition; public __unsafe_unretained NSHashTable* _Nonnull other; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 16e2dc5a0c80..4c495e04fe1d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -3177,6 +3177,13 @@ struct RCTFontProperties { public UIFontWeight weight; } +struct RCTLayerCornerConfiguration { + public CACornerMask maskedCorners; + public CGFloat cornerRadius; + public bool hasRoundedCorner; + public facebook::react::BorderCurve cornerCurve; +} + struct RCTLayoutContext { public CGPoint absolutePosition; public __unsafe_unretained NSHashTable* _Nonnull other; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 48d9cc57fdb4..843c5ef631b7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3189,6 +3189,13 @@ struct RCTFontProperties { public UIFontWeight weight; } +struct RCTLayerCornerConfiguration { + public CACornerMask maskedCorners; + public CGFloat cornerRadius; + public bool hasRoundedCorner; + public facebook::react::BorderCurve cornerCurve; +} + struct RCTLayoutContext { public CGPoint absolutePosition; public __unsafe_unretained NSHashTable* _Nonnull other;