From 4075418c14daf3e4343b9611a34a56163a569e90 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Thu, 22 Aug 2024 21:03:28 -0700 Subject: [PATCH] Add support for assymetrical border radii when using % (#46009) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46009 as title Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D61148739 fbshipit-source-id: 7cc37f98ab441df2e0df7cfa0415791cb642caaf --- .../View/RCTViewComponentView.mm | 18 ++- .../React/Fabric/Utils/RCTBoxShadow.mm | 12 +- .../React/Views/RCTBorderDrawing.h | 14 +- .../React/Views/RCTBorderDrawing.m | 37 +++-- packages/react-native/React/Views/RCTView.m | 10 +- .../components/view/BaseViewProps.cpp | 142 +++++++++--------- .../renderer/components/view/primitives.h | 9 +- .../react/renderer/graphics/ValueUnit.h | 2 +- .../rn-tester/js/examples/View/ViewExample.js | 8 + 9 files changed, 143 insertions(+), 109 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 0c4b7c1d1ddd77..646a91082c70fe 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -624,10 +624,14 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii) { return RCTCornerRadii{ - .topLeft = (CGFloat)borderRadii.topLeft, - .topRight = (CGFloat)borderRadii.topRight, - .bottomLeft = (CGFloat)borderRadii.bottomLeft, - .bottomRight = (CGFloat)borderRadii.bottomRight}; + .topLeftHorizontal = (CGFloat)borderRadii.topLeft.horizontal, + .topLeftVertical = (CGFloat)borderRadii.topLeft.vertical, + .topRightHorizontal = (CGFloat)borderRadii.topRight.horizontal, + .topRightVertical = (CGFloat)borderRadii.topRight.vertical, + .bottomLeftHorizontal = (CGFloat)borderRadii.bottomLeft.horizontal, + .bottomLeftVertical = (CGFloat)borderRadii.bottomLeft.vertical, + .bottomRightHorizontal = (CGFloat)borderRadii.bottomRight.horizontal, + .bottomRightVertical = (CGFloat)borderRadii.bottomRight.vertical}; } static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors) @@ -748,7 +752,7 @@ - (void)invalidateLayer CGColorRef borderColor = RCTCreateCGColorRefFromSharedColor(borderMetrics.borderColors.left); layer.borderColor = borderColor; CGColorRelease(borderColor); - layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft; + layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal; layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft); @@ -810,7 +814,7 @@ - (void)invalidateLayer if (self.clipsToBounds) { if (borderMetrics.borderRadii.isUniform()) { // In this case we can simply use `cornerRadius` exclusively. - cornerRadius = borderMetrics.borderRadii.topLeft; + cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; } else { RCTCornerInsets cornerInsets = RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero); @@ -856,7 +860,7 @@ - (void)invalidateLayer alpha:self.layer.opacity] .CGColor; if (borderMetrics.borderRadii.isUniform()) { - _filterLayer.cornerRadius = borderMetrics.borderRadii.topLeft; + _filterLayer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; } else { RCTCornerInsets cornerInsets = RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero); diff --git a/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm b/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm index 25d04732343930..b37c2e246ca2d3 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBoxShadow.mm @@ -32,10 +32,14 @@ static CGFloat adjustedCornerRadius(CGFloat cornerRadius, CGFloat spreadDistance static RCTCornerRadii cornerRadiiForBoxShadow(RCTCornerRadii cornerRadii, CGFloat spreadDistance) { return { - adjustedCornerRadius(cornerRadii.topLeft, spreadDistance), - adjustedCornerRadius(cornerRadii.topRight, spreadDistance), - adjustedCornerRadius(cornerRadii.bottomLeft, spreadDistance), - adjustedCornerRadius(cornerRadii.bottomRight, spreadDistance)}; + adjustedCornerRadius(cornerRadii.topLeftHorizontal, spreadDistance), + adjustedCornerRadius(cornerRadii.topLeftVertical, spreadDistance), + adjustedCornerRadius(cornerRadii.topRightHorizontal, spreadDistance), + adjustedCornerRadius(cornerRadii.topRightVertical, spreadDistance), + adjustedCornerRadius(cornerRadii.bottomLeftHorizontal, spreadDistance), + adjustedCornerRadius(cornerRadii.bottomLeftVertical, spreadDistance), + adjustedCornerRadius(cornerRadii.bottomRightHorizontal, spreadDistance), + adjustedCornerRadius(cornerRadii.bottomRightVertical, spreadDistance)}; } // Returns the smallest CGRect that will contain all shadows and the layer itself. diff --git a/packages/react-native/React/Views/RCTBorderDrawing.h b/packages/react-native/React/Views/RCTBorderDrawing.h index 4c92868f7c607e..39105666f63daf 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.h +++ b/packages/react-native/React/Views/RCTBorderDrawing.h @@ -11,10 +11,14 @@ #import typedef struct { - CGFloat topLeft; - CGFloat topRight; - CGFloat bottomLeft; - CGFloat bottomRight; + CGFloat topLeftHorizontal; + CGFloat topLeftVertical; + CGFloat topRightHorizontal; + CGFloat topRightVertical; + CGFloat bottomLeftHorizontal; + CGFloat bottomLeftVertical; + CGFloat bottomRightHorizontal; + CGFloat bottomRightVertical; } RCTCornerRadii; typedef struct { @@ -35,7 +39,7 @@ typedef struct { * Determine if the border widths, colors and radii are all equal. */ RCT_EXTERN BOOL RCTBorderInsetsAreEqual(UIEdgeInsets borderInsets); -RCT_EXTERN BOOL RCTCornerRadiiAreEqual(RCTCornerRadii cornerRadii); +RCT_EXTERN BOOL RCTCornerRadiiAreEqualAndSymmetrical(RCTCornerRadii cornerRadii); RCT_EXTERN BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors); /** diff --git a/packages/react-native/React/Views/RCTBorderDrawing.m b/packages/react-native/React/Views/RCTBorderDrawing.m index fa2f5e50cb59dc..fa760f6a56605d 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.m +++ b/packages/react-native/React/Views/RCTBorderDrawing.m @@ -17,11 +17,15 @@ BOOL RCTBorderInsetsAreEqual(UIEdgeInsets borderInsets) ABS(borderInsets.left - borderInsets.top) < RCTViewBorderThreshold; } -BOOL RCTCornerRadiiAreEqual(RCTCornerRadii cornerRadii) +BOOL RCTCornerRadiiAreEqualAndSymmetrical(RCTCornerRadii cornerRadii) { - return ABS(cornerRadii.topLeft - cornerRadii.topRight) < RCTViewBorderThreshold && - ABS(cornerRadii.topLeft - cornerRadii.bottomLeft) < RCTViewBorderThreshold && - ABS(cornerRadii.topLeft - cornerRadii.bottomRight) < RCTViewBorderThreshold; + return cornerRadii.topLeftHorizontal == cornerRadii.topLeftHorizontal && + cornerRadii.topRightHorizontal == cornerRadii.topRightVertical && + cornerRadii.bottomLeftHorizontal == cornerRadii.bottomLeftVertical && + cornerRadii.bottomRightHorizontal == cornerRadii.bottomRightVertical && + ABS(cornerRadii.topLeftHorizontal - cornerRadii.topRightHorizontal) < RCTViewBorderThreshold && + ABS(cornerRadii.topLeftHorizontal - cornerRadii.bottomLeftHorizontal) < RCTViewBorderThreshold && + ABS(cornerRadii.topLeftHorizontal - cornerRadii.bottomRightHorizontal) < RCTViewBorderThreshold; } BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors) @@ -35,20 +39,20 @@ RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, UIEdgeInsets edge { return (RCTCornerInsets){ { - MAX(0, cornerRadii.topLeft - edgeInsets.left), - MAX(0, cornerRadii.topLeft - edgeInsets.top), + MAX(0, cornerRadii.topLeftHorizontal - edgeInsets.left), + MAX(0, cornerRadii.topLeftVertical - edgeInsets.top), }, { - MAX(0, cornerRadii.topRight - edgeInsets.right), - MAX(0, cornerRadii.topRight - edgeInsets.top), + MAX(0, cornerRadii.topRightHorizontal - edgeInsets.right), + MAX(0, cornerRadii.topRightVertical - edgeInsets.top), }, { - MAX(0, cornerRadii.bottomLeft - edgeInsets.left), - MAX(0, cornerRadii.bottomLeft - edgeInsets.bottom), + MAX(0, cornerRadii.bottomLeftHorizontal - edgeInsets.left), + MAX(0, cornerRadii.bottomLeftVertical - edgeInsets.bottom), }, { - MAX(0, cornerRadii.bottomRight - edgeInsets.right), - MAX(0, cornerRadii.bottomRight - edgeInsets.bottom), + MAX(0, cornerRadii.bottomRightHorizontal - edgeInsets.right), + MAX(0, cornerRadii.bottomRightVertical - edgeInsets.bottom), }}; } @@ -159,8 +163,13 @@ CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, RCTCornerInsets cornerInse NS_INLINE BOOL RCTCornerRadiiAreAboveThreshold(RCTCornerRadii cornerRadii) { return ( - cornerRadii.topLeft > RCTViewBorderThreshold || cornerRadii.topRight > RCTViewBorderThreshold || - cornerRadii.bottomLeft > RCTViewBorderThreshold || cornerRadii.bottomRight > RCTViewBorderThreshold); + cornerRadii.topLeftHorizontal > RCTViewBorderThreshold || cornerRadii.topLeftVertical > RCTViewBorderThreshold || + cornerRadii.topRightHorizontal > RCTViewBorderThreshold || + cornerRadii.topRightVertical > RCTViewBorderThreshold || + cornerRadii.bottomLeftHorizontal > RCTViewBorderThreshold || + cornerRadii.bottomLeftVertical > RCTViewBorderThreshold || + cornerRadii.bottomRightHorizontal > RCTViewBorderThreshold || + cornerRadii.bottomRightVertical > RCTViewBorderThreshold); } static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCornerRadii cornerRadii) diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 7b6f5e7a6d43e8..be122dd4cb633b 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -803,8 +803,8 @@ - (void)displayLayer:(CALayer *)layer const UIEdgeInsets borderInsets = [self bordersAsInsets]; const RCTBorderColors borderColors = [self borderColorsWithTraitCollection:self.traitCollection]; - BOOL useIOSBorderRendering = RCTCornerRadiiAreEqual(cornerRadii) && RCTBorderInsetsAreEqual(borderInsets) && - RCTBorderColorsAreEqual(borderColors) && + BOOL useIOSBorderRendering = RCTCornerRadiiAreEqualAndSymmetrical(cornerRadii) && + RCTBorderInsetsAreEqual(borderInsets) && RCTBorderColorsAreEqual(borderColors) && // 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 @@ -821,7 +821,7 @@ - (void)displayLayer:(CALayer *)layer backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor; if (useIOSBorderRendering) { - layer.cornerRadius = cornerRadii.topLeft; + layer.cornerRadius = cornerRadii.topLeftHorizontal; layer.borderColor = borderColors.left; layer.borderWidth = borderInsets.left; layer.backgroundColor = backgroundColor; @@ -928,8 +928,8 @@ - (void)updateClippingForLayer:(CALayer *)layer if (self.clipsToBounds) { const RCTCornerRadii cornerRadii = [self cornerRadii]; - if (RCTCornerRadiiAreEqual(cornerRadii)) { - cornerRadius = cornerRadii.topLeft; + if (RCTCornerRadiiAreEqualAndSymmetrical(cornerRadii)) { + cornerRadius = cornerRadii.topLeftHorizontal; } else { CAShapeLayer *shapeLayer = [CAShapeLayer layer]; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 8228b501f65982..4b9149b5c6790e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -388,36 +388,53 @@ static BorderRadii ensureNoOverlap(const BorderRadii& radii, const Size& size) { // Source: https://www.w3.org/TR/css-backgrounds-3/#corner-overlap auto insets = EdgeInsets{ - /* .left = */ radii.topLeft + radii.bottomLeft, - /* .top = */ radii.topLeft + radii.topRight, - /* .right = */ radii.topRight + radii.bottomRight, - /* .bottom = */ radii.bottomLeft + radii.bottomRight, + .left = radii.topLeft.horizontal + radii.bottomLeft.horizontal, + .top = radii.topLeft.vertical + radii.topRight.vertical, + .right = radii.topRight.horizontal + radii.bottomRight.horizontal, + .bottom = radii.bottomLeft.vertical + radii.bottomRight.vertical, }; auto insetsScale = EdgeInsets{ - /* .left = */ - insets.left > 0 ? std::min((Float)1.0, size.height / insets.left) : 0, - /* .top = */ - insets.top > 0 ? std::min((Float)1.0, size.width / insets.top) : 0, - /* .right = */ - insets.right > 0 ? std::min((Float)1.0, size.height / insets.right) : 0, - /* .bottom = */ - insets.bottom > 0 ? std::min((Float)1.0, size.width / insets.bottom) : 0, + .left = + insets.left > 0 ? std::min((Float)1.0, size.height / insets.left) : 0, + .top = insets.top > 0 ? std::min((Float)1.0, size.width / insets.top) : 0, + .right = insets.right > 0 + ? std::min((Float)1.0, size.height / insets.right) + : 0, + .bottom = insets.bottom > 0 + ? std::min((Float)1.0, size.width / insets.bottom) + : 0, }; return BorderRadii{ - /* topLeft = */ - static_cast( - radii.topLeft * std::min(insetsScale.top, insetsScale.left)), - /* topRight = */ - static_cast( - radii.topRight * std::min(insetsScale.top, insetsScale.right)), - /* bottomLeft = */ - static_cast( - radii.bottomLeft * std::min(insetsScale.bottom, insetsScale.left)), - /* bottomRight = */ - static_cast( - radii.bottomRight * std::min(insetsScale.bottom, insetsScale.right)), + .topLeft = + {static_cast( + radii.topLeft.horizontal * + std::min(insetsScale.top, insetsScale.left)), + static_cast( + radii.topLeft.vertical * + std::min(insetsScale.top, insetsScale.left))}, + .topRight = + {static_cast( + radii.topRight.horizontal * + std::min(insetsScale.top, insetsScale.right)), + static_cast( + radii.topRight.vertical * + std::min(insetsScale.top, insetsScale.right))}, + .bottomLeft = + {static_cast( + radii.bottomLeft.horizontal * + std::min(insetsScale.bottom, insetsScale.left)), + static_cast( + radii.bottomLeft.vertical * + std::min(insetsScale.bottom, insetsScale.left))}, + .bottomRight = + {static_cast( + radii.bottomRight.horizontal * + std::min(insetsScale.bottom, insetsScale.right)), + static_cast( + radii.bottomRight.vertical * + std::min(insetsScale.bottom, insetsScale.right))}, }; } @@ -425,52 +442,35 @@ static BorderRadii radiiPercentToPoint( const RectangleCorners& radii, const Size& size) { return BorderRadii{ - /* topLeft = */ - (radii.topLeft.unit == UnitType::Percent) - ? static_cast( - (radii.topLeft.value / 100) * std::max(size.width, size.height)) - : static_cast(radii.topLeft.value), - /* topRight = */ - (radii.topRight.unit == UnitType::Percent) - ? static_cast( - (radii.topRight.value / 100) * - std::max(size.width, size.height)) - : static_cast(radii.topRight.value), - /* bottomLeft = */ - (radii.bottomLeft.unit == UnitType::Percent) - ? static_cast( - (radii.bottomLeft.value / 100) * - std::max(size.width, size.height)) - : static_cast(radii.bottomLeft.value), - /* bottomRight = */ - (radii.bottomRight.unit == UnitType::Percent) - ? static_cast( - (radii.bottomRight.value / 100) * - std::max(size.width, size.height)) - : static_cast(radii.bottomRight.value), + .topLeft = + {radii.topLeft.resolve(size.width), + radii.topLeft.resolve(size.height)}, + .topRight = + {radii.topRight.resolve(size.width), + radii.topRight.resolve(size.height)}, + .bottomLeft = + {radii.bottomLeft.resolve(size.width), + radii.bottomLeft.resolve(size.height)}, + .bottomRight = + {radii.bottomRight.resolve(size.width), + radii.bottomRight.resolve(size.height)}, }; } CascadedBorderWidths BaseViewProps::getBorderWidths() const { return CascadedBorderWidths{ - /* .left = */ optionalFloatFromYogaValue( - yogaStyle.border(yoga::Edge::Left)), - /* .top = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Top)), - /* .right = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Right)), - /* .bottom = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Bottom)), - /* .start = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Start)), - /* .end = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::End)), - /* .horizontal = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Horizontal)), - /* .vertical = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Vertical)), - /* .all = */ - optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::All)), + .left = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Left)), + .top = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Top)), + .right = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Right)), + .bottom = + optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Bottom)), + .start = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Start)), + .end = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::End)), + .horizontal = + optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Horizontal)), + .vertical = + optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Vertical)), + .all = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::All)), }; } @@ -486,13 +486,11 @@ BorderMetrics BaseViewProps::resolveBorderMetrics( layoutMetrics.frame.size); return { - /* .borderColors = */ borderColors.resolve(isRTL, {}), - /* .borderWidths = */ borderWidths.resolve(isRTL, 0), - /* .borderRadii = */ - ensureNoOverlap(radii, layoutMetrics.frame.size), - /* .borderCurves = */ - borderCurves.resolve(isRTL, BorderCurve::Circular), - /* .borderStyles = */ borderStyles.resolve(isRTL, BorderStyle::Solid), + .borderColors = borderColors.resolve(isRTL, {}), + .borderWidths = borderWidths.resolve(isRTL, 0), + .borderRadii = ensureNoOverlap(radii, layoutMetrics.frame.size), + .borderCurves = borderCurves.resolve(isRTL, BorderCurve::Circular), + .borderStyles = borderStyles.resolve(isRTL, BorderStyle::Solid), }; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index 4e38d898cff77a..42f2cbe2d211ed 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -92,6 +92,13 @@ enum class BorderCurve : uint8_t { Circular, Continuous }; enum class BorderStyle : uint8_t { Solid, Dotted, Dashed }; +struct CornerRadii { + float vertical{0.0f}; + float horizontal{0.0f}; + + bool operator==(const CornerRadii& other) const = default; +}; + enum class Cursor : uint8_t { Auto, Alias, @@ -289,7 +296,7 @@ using BorderWidths = RectangleEdges; using BorderCurves = RectangleCorners; using BorderStyles = RectangleEdges; using BorderColors = RectangleEdges; -using BorderRadii = RectangleCorners; +using BorderRadii = RectangleCorners; using CascadedBorderWidths = CascadedRectangleEdges; using CascadedBorderCurves = CascadedRectangleCorners; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h index c9d3b6141d862c..d64703895b9610 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h @@ -29,7 +29,7 @@ struct ValueUnit { return !(*this == other); } - constexpr float resolve(float referenceLength) { + constexpr float resolve(float referenceLength) const { switch (unit) { case UnitType::Point: return value; diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index 6657ae7d4c4c85..de44881b2acde0 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -833,6 +833,14 @@ export default ({ borderBottomLeftRadius: '100%', }} /> + ); },