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 005667f4c38..aaf3a498ec2 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -38,7 +38,7 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f; CALayer *_backgroundColorLayer; __weak CALayer *_borderLayer; CALayer *_outlineLayer; - CALayer *_boxShadowLayer; + NSMutableArray *_boxShadowLayers; CALayer *_filterLayer; NSMutableArray *_backgroundImageLayers; BOOL _needsInvalidateLayer; @@ -828,7 +828,7 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle) // If view has a solid background color, calculate shadow path from border. const RCTCornerInsets cornerInsets = RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero); - CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil); + CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil, NO); layer.shadowPath = shadowPath; CGPathRelease(shadowPath); } else { @@ -852,9 +852,9 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle) // 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); + CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.frame, cornerInsets, NULL, NO); #else // TARGET_OS_VISION - CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL); + CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL, NO); #endif UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath]; CGPathRelease(borderPath); @@ -1016,21 +1016,24 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle) } // box shadow - [_boxShadowLayer removeFromSuperlayer]; - _boxShadowLayer = nil; + for (CALayer *boxShadowLayer in _boxShadowLayers) { + [boxShadowLayer removeFromSuperlayer]; + } + [_boxShadowLayers removeAllObjects]; if (!_props->boxShadow.empty()) { - _boxShadowLayer = [CALayer layer]; - [self.layer addSublayer:_boxShadowLayer]; - _boxShadowLayer.zPosition = _borderLayer.zPosition; - _boxShadowLayer.frame = RCTGetBoundingRect(_props->boxShadow, self.layer.bounds.size); - - UIImage *boxShadowImage = RCTGetBoxShadowImage( - _props->boxShadow, - RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), - RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths), - self.layer.bounds.size); - - _boxShadowLayer.contents = (id)boxShadowImage.CGImage; + if (!_boxShadowLayers) { + _boxShadowLayers = [NSMutableArray new]; + } + for (auto it = _props->boxShadow.rbegin(); it != _props->boxShadow.rend(); ++it) { + CALayer *shadowLayer = RCTGetBoxShadowLayer( + *it, + RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), + RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths), + self.layer.bounds.size); + shadowLayer.zPosition = _borderLayer.zPosition; + [self.layer addSublayer:shadowLayer]; + [_boxShadowLayers addObject:shadowLayer]; + } } // clipping @@ -1095,7 +1098,7 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle) - (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)cornerInsets { - CGPathRef path = RCTPathCreateWithRoundedRect(bounds, cornerInsets, nil); + CGPathRef path = RCTPathCreateWithRoundedRect(bounds, cornerInsets, nil, NO); CAShapeLayer *maskLayer = [CAShapeLayer layer]; maskLayer.path = path; CGPathRelease(path); diff --git a/packages/react-native/React/Fabric/Utils/RCTBoxShadow.h b/packages/react-native/React/Fabric/Utils/RCTBoxShadow.h index 47ed6f69305..36125c02d3c 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBoxShadow.h +++ b/packages/react-native/React/Fabric/Utils/RCTBoxShadow.h @@ -12,10 +12,8 @@ #import #import -RCT_EXTERN UIImage *RCTGetBoxShadowImage( - const std::vector &shadows, +RCT_EXTERN CALayer *RCTGetBoxShadowLayer( + const facebook::react::BoxShadow &shadow, RCTCornerRadii cornerRadii, UIEdgeInsets edgeInsets, CGSize layerSize); - -RCT_EXTERN CGRect RCTGetBoundingRect(const std::vector &boxShadows, CGSize layerSize); diff --git a/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm b/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm index fe6518ae1cf..54f3b79894c 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm @@ -7,7 +7,6 @@ #import "RCTBoxShadow.h" -#import #import #import @@ -42,46 +41,6 @@ static RCTCornerRadii cornerRadiiForBoxShadow(RCTCornerRadii cornerRadii, CGFloa adjustedCornerRadius(cornerRadii.bottomRightVertical, spreadDistance)}; } -// Returns the smallest CGRect that will contain all shadows and the layer itself. -// The origin represents the location of this box relative to the layer the shadows -// are attached to. -CGRect RCTGetBoundingRect(const std::vector &boxShadows, CGSize layerSize) -{ - CGFloat smallestX = 0; - CGFloat smallestY = 0; - CGFloat largestX = layerSize.width; - CGFloat largestY = layerSize.height; - for (const auto &boxShadow : boxShadows) { - if (!boxShadow.inset) { - CGFloat negativeXExtent = boxShadow.offsetX - boxShadow.spreadDistance - boxShadow.blurRadius; - smallestX = MIN(smallestX, negativeXExtent); - CGFloat negativeYExtent = boxShadow.offsetY - boxShadow.spreadDistance - boxShadow.blurRadius; - smallestY = MIN(smallestY, negativeYExtent); - CGFloat positiveXExtent = boxShadow.offsetX + boxShadow.spreadDistance + boxShadow.blurRadius + layerSize.width; - largestX = MAX(largestX, positiveXExtent); - CGFloat positiveYExtent = boxShadow.offsetY + boxShadow.spreadDistance + boxShadow.blurRadius + layerSize.height; - largestY = MAX(largestY, positiveYExtent); - } - } - - return CGRectMake(smallestX, smallestY, largestX - smallestX, largestY - smallestY); -} - -static std::pair, std::vector> splitBoxShadowsByInset( - const std::vector &allShadows) -{ - std::vector outsetShadows, insetShadows; - - std::partition_copy( - allShadows.begin(), - allShadows.end(), - std::back_inserter(insetShadows), - std::back_inserter(outsetShadows), - [](BoxShadow shadow) { return shadow.inset; }); - - return std::make_pair(outsetShadows, insetShadows); -} - static CGRect insetRect(CGRect rect, CGFloat left, CGFloat top, CGFloat right, CGFloat bottom) { return CGRectMake( @@ -94,208 +53,117 @@ static CGColorRef colorRefFromSharedColor(const SharedColor &color) return colorRef ? colorRef : [UIColor blackColor].CGColor; } -// Core graphics has support for shadows that looks similar to web and are very -// fast to apply. The only issue is that this shadow does not take a spread -// radius like on web. To get around this, we draw the shadow rect (the rect -// that casts the shadow, not the shadow itself) offscreen. This shadow rect -// is correctly sized to account for spread radius. Then, when setting the -// shadow itself, we modify the offsetX/Y to account for the fact that our -// shadow rect is offscreen and position it where it needs to be in the image. -static void renderOutsetShadows( - std::vector &outsetShadows, - RCTCornerRadii cornerRadii, - CGSize layerSize, - CGRect boundingRect, - CGContextRef context) +static CALayer *initBoxShadowLayer(const BoxShadow &shadow, CGSize layerSize) { - if (outsetShadows.empty()) { - return; - } - // Save state before doing any work so that we can restore it after we have - // drawn all of our shadows. This ensures that we do not need to worry about - // graphical state carrying over after this function returns - CGContextSaveGState(context); - // Reverse iterator as shadows are stacked back to front - for (auto it = outsetShadows.rbegin(); it != outsetShadows.rend(); ++it) { - CGContextSaveGState(context); - CGFloat offsetX = it->offsetX; - CGFloat offsetY = it->offsetY; - CGFloat blurRadius = it->blurRadius; - CGFloat spreadDistance = it->spreadDistance; - CGColorRef color = colorRefFromSharedColor(it->color); + CALayer *shadowLayer = [CALayer layer]; + shadowLayer.frame = CGRectMake(0, 0, layerSize.width, layerSize.height); + shadowLayer.shadowColor = colorRefFromSharedColor(shadow.color); + // Default is (0, -3) believe it or not + shadowLayer.shadowOffset = CGSizeMake(0, 0); + shadowLayer.shadowOpacity = 1; + // Apple's blur is not quite what we want and seems to be a bit overbearing + // with the radius. This is an eyeballed adjustment that has the blur looking + // more like the web. + shadowLayer.shadowRadius = shadow.blurRadius / 2; + shadowLayer.contentsScale = [UIScreen mainScreen].scale; - // First, define the shadow rect. This is the rect that will be filled - // and _cast_ the shadow. As a result, the size does not incorporate - // the blur radius since this rect is not the shadow itself. - const RCTCornerInsets shadowRectCornerInsets = - RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, spreadDistance), UIEdgeInsetsZero); - CGSize shadowRectSize = - CGSizeMake(fmax(layerSize.width + 2 * spreadDistance, 0), fmax(layerSize.height + 2 * spreadDistance, 0)); - // Ensure this is drawn offscreen and will not show in the image - CGRect shadowRect = CGRectMake(-shadowRectSize.width, 0, shadowRectSize.width, shadowRectSize.height); - CGPathRef shadowRectPath = RCTPathCreateWithRoundedRect(shadowRect, shadowRectCornerInsets, nil); - - // Second, set the shadow as graphics state so that when we fill our - // shadow rect it will actually cast a shadow. The offset of this - // shadow needs to compensate for the fact that the shadow rect is - // offscreen. Additionally, we take away the spread radius since spread - // grows in all directions but the origin of our shadow rect is just - // the negative width, which accounts for 2*spread radius. - CGContextSetShadowWithColor( - context, - CGSizeMake( - offsetX - boundingRect.origin.x - spreadDistance - shadowRect.origin.x, - offsetY - boundingRect.origin.y - spreadDistance), - blurRadius, - color); - - // Third, the Core Graphics functions to actually draw the shadow rect - // and thus the shadow itself. - CGContextAddPath(context, shadowRectPath); - // Color here does not matter, we just need something that has 1 for - // alpha so that the shadow is visible. The rect is purposely rendered - // outside of the context so it should not be visible. - CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor); - CGContextFillPath(context); - - CGPathRelease(shadowRectPath); - CGContextRestoreGState(context); - } - // Lastly, clear out the region inside the view so that the shadows do - // not cover its content - const RCTCornerInsets layerCornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero); - CGPathRef shadowPathAlignedWithLayer = RCTPathCreateWithRoundedRect( - CGRectMake(-boundingRect.origin.x, -boundingRect.origin.y, layerSize.width, layerSize.height), - layerCornerInsets, - nil); - CGContextAddPath(context, shadowPathAlignedWithLayer); - CGContextSetBlendMode(context, kCGBlendModeClear); - CGContextFillPath(context); - - CGPathRelease(shadowPathAlignedWithLayer); - CGContextRestoreGState(context); + return shadowLayer; } -// Just like with outset shadows, we need to draw inset shadow rects offscreen -// then offset the shadow it casts to the proper place. We can replicate the shape -// of the inset shadow by using 2 rects. One is the same size as the view it is -// attached to, plus some padding. The other represents a cropping region of the -// first rect, with the exact postion and size of this region depending on the -// shadow's params. We make this cropping region by using the EO fill pattern so -// that the interection of the 2 rects is clear, and thus no shadow is cast. -static void renderInsetShadows( - std::vector &insetShadows, - RCTCornerRadii cornerRadii, - UIEdgeInsets edgeInsets, - CGSize layerSize, - CGRect boundingRect, - CGContextRef context) +static CALayer * +RCTGetOutsetBoxShadowLayer(const facebook::react::BoxShadow &shadow, RCTCornerRadii cornerRadii, CGSize layerSize) { - if (insetShadows.empty()) { - return; - } - // Save state before doing any work so that we can restore it after we have - // drawn all of our shadows. This ensures that we do not need to worry about - // graphical state carrying over after this function returns - CGContextSaveGState(context); + CALayer *shadowLayer = initBoxShadowLayer(shadow, layerSize); - CGRect layerFrameRelativeToBoundingRect = - CGRectMake(-boundingRect.origin.x, -boundingRect.origin.y, layerSize.width, layerSize.height); - CGRect shadowFrame = - insetRect(layerFrameRelativeToBoundingRect, edgeInsets.left, edgeInsets.top, edgeInsets.right, edgeInsets.bottom); + const RCTCornerInsets shadowRectCornerInsets = + RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, shadow.spreadDistance), UIEdgeInsetsZero); - // First, create a clipping area so we only draw within the view's bounds. - // If we do not do this, blur artifacts will show up outside the view. - CGRect outerClippingRect = CGRectMake(0, 0, boundingRect.size.width, boundingRect.size.height); - // Add the path twice so we only draw inside the view with the EO crop rule - CGContextAddRect(context, outerClippingRect); - CGContextAddRect(context, outerClippingRect); - const RCTCornerInsets cornerInsetsForLayer = RCTGetCornerInsets(cornerRadii, edgeInsets); - CGPathRef layerPath = RCTPathCreateWithRoundedRect(shadowFrame, cornerInsetsForLayer, nil); - CGContextAddPath(context, layerPath); - CGContextEOClip(context); + CGRect shadowRect = CGRectInset(shadowLayer.bounds, -shadow.spreadDistance, -shadow.spreadDistance); + shadowRect = CGRectOffset(shadowRect, shadow.offsetX, shadow.offsetY); + CGPathRef shadowRectPath = RCTPathCreateWithRoundedRect(shadowRect, shadowRectCornerInsets, nil, NO); + shadowLayer.shadowPath = shadowRectPath; + + CAShapeLayer *mask = [CAShapeLayer new]; + [mask setContentsScale:[UIScreen mainScreen].scale]; + CGMutablePathRef path = CGPathCreateMutable(); + CGPathRef layerPath = + RCTPathCreateWithRoundedRect(shadowLayer.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), nil, NO); + CGPathAddPath(path, NULL, layerPath); + CGPathRef paddedShadowRectPath = RCTPathCreateWithRoundedRect( + CGRectInset(shadowRect, -2 * (shadow.blurRadius + 1), -2 * (shadow.blurRadius + 1)), + shadowRectCornerInsets, + nil, + NO); + CGPathAddPath(path, NULL, paddedShadowRectPath); + mask.fillRule = kCAFillRuleEvenOdd; + mask.path = path; + shadowLayer.mask = mask; + + CGPathRelease(path); + CGPathRelease(shadowRectPath); CGPathRelease(layerPath); + CGPathRelease(paddedShadowRectPath); - // Reverse iterator as shadows are stacked back to front - for (auto it = insetShadows.rbegin(); it != insetShadows.rend(); ++it) { - CGContextSaveGState(context); - CGFloat offsetX = it->offsetX; - CGFloat offsetY = it->offsetY; - CGFloat blurRadius = it->blurRadius; - CGFloat spreadDistance = it->spreadDistance; - CGColorRef color = colorRefFromSharedColor(it->color); - - // Second, create the two offscreen rects we will use to create the correct - // inset shadow shape. shadowRect has an originX such that it AND the clear - // region are both guaranteed to be offscreen. We do not want some combination - // of shadow params to allow the clear region to showup inside our context. - // We also pad the size of the shadow rect by the blur radius so that the - // edges of the shadow remain a solid color and do not blend with outside - // of the view. - CGRect shadowCastingRect = CGRectInset(shadowFrame, -blurRadius, -blurRadius); - CGRect clearRegionRect = CGRectInset(shadowFrame, spreadDistance, spreadDistance); - // This happens if the spread causes the height/width to be negative. A null - // rect breaks a lot of the logic, so lets just keep it as a point - if (CGRectIsNull(clearRegionRect)) { - clearRegionRect = CGRectMake(0, 0, 0, 0); - } - CGPoint offsetToMoveOffscreen = CGPointMake( - -fmax( - shadowCastingRect.size.width, - clearRegionRect.size.width + offsetX + (clearRegionRect.origin.x - shadowCastingRect.origin.x)) - - shadowCastingRect.origin.x, - 0); - shadowCastingRect = CGRectOffset(shadowCastingRect, offsetToMoveOffscreen.x, offsetToMoveOffscreen.y); - clearRegionRect = - CGRectOffset(clearRegionRect, offsetToMoveOffscreen.x + offsetX, offsetToMoveOffscreen.y + offsetY); - CGContextAddRect(context, shadowCastingRect); - - const RCTCornerInsets cornerInsetsForClearRegion = - RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, -spreadDistance), edgeInsets); - CGPathRef clearRegionPath = RCTPathCreateWithRoundedRect(clearRegionRect, cornerInsetsForClearRegion, nil); - CGContextAddPath(context, clearRegionPath); - - // Third, set the shadow graphics state with the appropriate offset such that - // it is positioned on top of the view. We subtract blurRadius because the - // shadow rect is padded. - CGContextSetShadowWithColor( - context, CGSizeMake(-offsetToMoveOffscreen.x, -offsetToMoveOffscreen.y), blurRadius, color); - - // Fourth, the Core Graphics functions to actually draw the shadow rect - // and thus the shadow itself. Note we use an EO fill path so that the - // intersection between our two rects will be clear. The disjoint parts of - // the rect will be colored, but because of the clipping area, we only see - // the shadow projected from the shadow rect, not the clear region rect. - CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor); - CGContextEOFillPath(context); - - CGContextRestoreGState(context); - } - - CGContextRestoreGState(context); + return shadowLayer; } -UIImage *RCTGetBoxShadowImage( - const std::vector &shadows, +static CALayer *RCTGetInsetBoxShadowLayer( + const facebook::react::BoxShadow &shadow, RCTCornerRadii cornerRadii, UIEdgeInsets edgeInsets, CGSize layerSize) { - CGRect boundingRect = RCTGetBoundingRect(shadows, layerSize); - UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat]; - UIGraphicsImageRenderer *const renderer = [[UIGraphicsImageRenderer alloc] initWithSize:boundingRect.size - format:rendererFormat]; - UIImage *const boxShadowImage = - [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { - auto [outsetShadows, insetShadows] = splitBoxShadowsByInset(shadows); - const CGContextRef context = rendererContext.CGContext; - // Outset shadows should be before inset shadows since outset needs to - // clear out a region in the view so we do not block its contents. - // Inset shadows could draw over those outset shadows but if the shadow - // colors have alpha < 1 then we will have inaccurate alpha compositing - renderOutsetShadows(outsetShadows, cornerRadii, layerSize, boundingRect, context); - renderInsetShadows(insetShadows, cornerRadii, edgeInsets, layerSize, boundingRect, context); - }]; + CALayer *shadowLayer = initBoxShadowLayer(shadow, layerSize); - return boxShadowImage; + CGMutablePathRef path = CGPathCreateMutable(); + // This shadow is padded by the blur to make sure blur artifacts can be cast + // if the clear region is right on the border of the layer + CGRect shadowRect = + insetRect(shadowLayer.bounds, edgeInsets.left, edgeInsets.top, edgeInsets.right, edgeInsets.bottom); + if (CGRectIsNull(shadowRect)) { + shadowRect = CGRectMake(0, 0, 0, 0); + } + CGPathRef shadowPath = + RCTPathCreateWithRoundedRect(CGRectInset(shadowRect, -shadow.blurRadius, -shadow.blurRadius), {}, nil, NO); + + CGPathRef layerPath = RCTPathCreateWithRoundedRect(shadowRect, RCTGetCornerInsets(cornerRadii, edgeInsets), nil, NO); + CGPathAddPath(path, NULL, shadowPath); + + CGRect clearRegionRect = CGRectOffset(shadowRect, shadow.offsetX, shadow.offsetY); + clearRegionRect = CGRectInset(clearRegionRect, shadow.spreadDistance, shadow.spreadDistance); + if (CGRectIsNull(clearRegionRect)) { + clearRegionRect = CGRectMake(0, 0, 0, 0); + } + CGPathRef clearRegionPath = RCTPathCreateWithRoundedRect( + clearRegionRect, + RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, -shadow.spreadDistance), edgeInsets), + nil, + YES); + CGPathAddPath(path, NULL, clearRegionPath); + + shadowLayer.shadowPath = path; + + CAShapeLayer *mask = [CAShapeLayer new]; + mask.path = layerPath; + shadowLayer.mask = mask; + + CGPathRelease(path); + CGPathRelease(layerPath); + CGPathRelease(shadowPath); + CGPathRelease(clearRegionPath); + + return shadowLayer; +} + +CALayer *RCTGetBoxShadowLayer( + const facebook::react::BoxShadow &shadow, + RCTCornerRadii cornerRadii, + UIEdgeInsets edgeInsets, + CGSize layerSize) +{ + if (shadow.inset) { + return RCTGetInsetBoxShadowLayer(shadow, cornerRadii, edgeInsets, layerSize); + } else { + return RCTGetOutsetBoxShadowLayer(shadow, cornerRadii, layerSize); + } } diff --git a/packages/react-native/React/Views/RCTBorderDrawing.h b/packages/react-native/React/Views/RCTBorderDrawing.h index b13ffdf6fba..fbc8407a535 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.h +++ b/packages/react-native/React/Views/RCTBorderDrawing.h @@ -52,8 +52,11 @@ RCT_EXTERN RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, UIEdge * Create a CGPath representing a rounded rectangle with the specified bounds * and corner insets. Note that the CGPathRef must be released by the caller. */ -RCT_EXTERN CGPathRef -RCTPathCreateWithRoundedRect(CGRect bounds, RCTCornerInsets cornerInsets, const CGAffineTransform *transform); +RCT_EXTERN CGPathRef RCTPathCreateWithRoundedRect( + CGRect bounds, + RCTCornerInsets cornerInsets, + const CGAffineTransform *transform, + const BOOL inverted); /** * Draw a CSS-compliant border as an image. You can determine if it's scalable diff --git a/packages/react-native/React/Views/RCTBorderDrawing.m b/packages/react-native/React/Views/RCTBorderDrawing.m index c6a0338c5c0..a2d9ba27995 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.m +++ b/packages/react-native/React/Views/RCTBorderDrawing.m @@ -95,7 +95,11 @@ static void RCTPathAddEllipticArc( CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise); } -CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, RCTCornerInsets cornerInsets, const CGAffineTransform *transform) +CGPathRef RCTPathCreateWithRoundedRect( + CGRect bounds, + RCTCornerInsets cornerInsets, + const CGAffineTransform *transform, + const BOOL inverted) { const CGFloat minX = CGRectGetMinX(bounds); const CGFloat minY = CGRectGetMinY(bounds); @@ -120,14 +124,25 @@ CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, RCTCornerInsets cornerInse }; CGMutablePathRef path = CGPathCreateMutable(); - RCTPathAddEllipticArc( - path, transform, (CGPoint){minX + topLeft.width, minY + topLeft.height}, topLeft, M_PI, 3 * M_PI_2, NO); - RCTPathAddEllipticArc( - path, transform, (CGPoint){maxX - topRight.width, minY + topRight.height}, topRight, 3 * M_PI_2, 0, NO); - RCTPathAddEllipticArc( - path, transform, (CGPoint){maxX - bottomRight.width, maxY - bottomRight.height}, bottomRight, 0, M_PI_2, NO); - RCTPathAddEllipticArc( - path, transform, (CGPoint){minX + bottomLeft.width, maxY - bottomLeft.height}, bottomLeft, M_PI_2, M_PI, NO); + if (inverted) { + RCTPathAddEllipticArc( + path, transform, (CGPoint){minX + bottomLeft.width, maxY - bottomLeft.height}, bottomLeft, M_PI, M_PI_2, YES); + RCTPathAddEllipticArc( + path, transform, (CGPoint){maxX - bottomRight.width, maxY - bottomRight.height}, bottomRight, M_PI_2, 0, YES); + RCTPathAddEllipticArc( + path, transform, (CGPoint){maxX - topRight.width, minY + topRight.height}, topRight, 0, 3 * M_PI_2, YES); + RCTPathAddEllipticArc( + path, transform, (CGPoint){minX + topLeft.width, minY + topLeft.height}, topLeft, 3 * M_PI_2, M_PI, YES); + } else { + RCTPathAddEllipticArc( + path, transform, (CGPoint){minX + topLeft.width, minY + topLeft.height}, topLeft, M_PI, 3 * M_PI_2, NO); + RCTPathAddEllipticArc( + path, transform, (CGPoint){maxX - topRight.width, minY + topRight.height}, topRight, 3 * M_PI_2, 0, NO); + RCTPathAddEllipticArc( + path, transform, (CGPoint){maxX - bottomRight.width, maxY - bottomRight.height}, bottomRight, 0, M_PI_2, NO); + RCTPathAddEllipticArc( + path, transform, (CGPoint){minX + bottomLeft.width, maxY - bottomLeft.height}, bottomLeft, M_PI_2, M_PI, NO); + } CGPathCloseSubpath(path); return path; } @@ -178,7 +193,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn return CGPathCreateWithRect(rect, NULL); } - return RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + return RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL, NO); } static UIGraphicsImageRenderer * @@ -247,7 +262,8 @@ static UIImage *RCTGetSolidBorderImage( CGContextAddPath(context, path); CGPathRelease(path); - CGPathRef insetPath = RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, borderInsets), cornerInsets, NULL); + CGPathRef insetPath = + RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, borderInsets), cornerInsets, NULL, NO); CGContextAddPath(context, insetPath); CGContextEOClip(context); @@ -500,7 +516,8 @@ static UIImage *RCTGetDashedOrDottedBorderImage( // perpendicular to the path, that's why we inset by half the width, so that it // reaches the edge of the rect. CGRect pathRect = CGRectInset(rect, lineWidth / 2.0, lineWidth / 2.0); - CGPathRef path = RCTPathCreateWithRoundedRect(pathRect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + CGPathRef path = + RCTPathCreateWithRoundedRect(pathRect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL, NO); CGFloat dashLengths[2]; dashLengths[0] = dashLengths[1] = (borderStyle == RCTBorderStyleDashed ? 3 : 1) * lineWidth; diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 0a2fa36aeb9..7fd1f3453b0 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -883,7 +883,7 @@ static void RCTUpdateShadowPathForView(RCTView *view) // If view has a solid background color, calculate shadow path from border const RCTCornerRadii cornerRadii = [view cornerRadii]; const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero); - CGPathRef shadowPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL); + CGPathRef shadowPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL, NO); view.layer.shadowPath = shadowPath; CGPathRelease(shadowPath); @@ -913,9 +913,9 @@ static void RCTUpdateHoverStyleForView(RCTView *view) // 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); + CGPathRef borderPath = RCTPathCreateWithRoundedRect(view.frame, cornerInsets, NULL, NO); #else // TARGET_OS_VISION - CGPathRef borderPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL); + CGPathRef borderPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL, NO); #endif UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath]; CGPathRelease(borderPath); @@ -941,7 +941,7 @@ static void RCTUpdateHoverStyleForView(RCTView *view) } else { CAShapeLayer *shapeLayer = [CAShapeLayer layer]; CGPathRef path = - RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL, NO); shapeLayer.path = path; CGPathRelease(path); mask = shapeLayer;