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 29c543c5809827..0e68c92940a737 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -17,6 +17,7 @@ #import #import #import +#import #import #import #import @@ -542,6 +543,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & needsInvalidateLayer = YES; } + // `clipPath` + if (oldViewProps.clipPath != newViewProps.clipPath) { + needsInvalidateLayer = YES; + } + _needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer; _props = std::static_pointer_cast(props); @@ -1223,7 +1229,25 @@ - (void)invalidateLayer // clipping self.currentContainerView.layer.mask = nil; - if (self.currentContainerView.clipsToBounds) { + + // Handle clip-path property + if (_props->clipPath.has_value()) { + CALayer *maskLayer = [RCTClipPathUtils createClipPathLayer:_props->clipPath.value() + layoutMetrics:_layoutMetrics + yogaStyle:_props->yogaStyle + bounds:layer.bounds + cornerRadii:RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii)]; + if (maskLayer != nil) { + self.currentContainerView.layer.mask = maskLayer; + + for (UIView *subview in self.currentContainerView.subviews) { + if ([subview isKindOfClass:[UIImageView class]]) { + subview.layer.mask = maskLayer; + } + } + } + } else if (self.currentContainerView.clipsToBounds) { + // Handle regular clipsToBounds clipping when no clip-path is specified BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(); if (!clipToPaddingBox) { if (borderMetrics.borderRadii.isUniform()) { diff --git a/packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.h b/packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.h new file mode 100644 index 00000000000000..053e40f583b398 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.h @@ -0,0 +1,28 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTBasicShapeUtils : NSObject + ++ (UIBezierPath *_Nullable)createPathFromBasicShape:(const facebook::react::BasicShape &)basicShape + bounds:(CGRect)bounds; + ++ (UIBezierPath *_Nullable)createCirclePath:(const facebook::react::CircleShape &)circle bounds:(CGRect)bounds; ++ (UIBezierPath *_Nullable)createEllipsePath:(const facebook::react::EllipseShape &)ellipse bounds:(CGRect)bounds; ++ (UIBezierPath *_Nullable)createInsetPath:(const facebook::react::InsetShape &)inset bounds:(CGRect)bounds; ++ (UIBezierPath *_Nullable)createPolygonPath:(const facebook::react::PolygonShape &)polygon bounds:(CGRect)bounds; ++ (UIBezierPath *_Nullable)createRectPath:(const facebook::react::RectShape &)rect bounds:(CGRect)bounds; ++ (UIBezierPath *_Nullable)createXywhPath:(const facebook::react::XywhShape &)xywh bounds:(CGRect)bounds; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.mm b/packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.mm new file mode 100644 index 00000000000000..8b795f1412f3c3 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.mm @@ -0,0 +1,151 @@ +/* + * 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 "RCTBasicShapeUtils.h" + +#import + +using namespace facebook::react; + +static CGFloat RCTResolveValueUnit(const ValueUnit &unit, CGFloat referenceDimension) +{ + if (unit.unit == UnitType::Percent) { + return (CGFloat)unit.value * referenceDimension / 100.0f; + } + return (CGFloat)unit.value; +} + +@implementation RCTBasicShapeUtils + ++ (UIBezierPath *)createCirclePath:(const CircleShape &)circle bounds:(CGRect)bounds +{ + // Resolve radius (use smaller dimension as reference for percentages, matching CSS closest-side) + // Default to 50% of closest-side if radius is not specified + CGFloat referenceDimension = MIN(bounds.size.width, bounds.size.height); + CGFloat radius = circle.r.has_value() ? RCTResolveValueUnit(circle.r.value(), referenceDimension) : referenceDimension / 2.0f; + CGFloat cx = bounds.origin.x + (circle.cx.has_value() ? RCTResolveValueUnit(circle.cx.value(), bounds.size.width) : bounds.size.width / 2.0f); + CGFloat cy = bounds.origin.y + (circle.cy.has_value() ? RCTResolveValueUnit(circle.cy.value(), bounds.size.height) : bounds.size.height / 2.0f); + + CGFloat diameter = radius * 2.0f; + CGRect circleRect = CGRectMake(cx - radius, cy - radius, diameter, diameter); + + return [UIBezierPath bezierPathWithOvalInRect:circleRect]; +} + ++ (UIBezierPath *)createEllipsePath:(const EllipseShape &)ellipse bounds:(CGRect)bounds +{ + // Resolve radii (default to 50% if not specified) + CGFloat rx = ellipse.rx.has_value() ? RCTResolveValueUnit(ellipse.rx.value(), bounds.size.width) : bounds.size.width / 2.0f; + CGFloat ry = ellipse.ry.has_value() ? RCTResolveValueUnit(ellipse.ry.value(), bounds.size.height) : bounds.size.height / 2.0f; + CGFloat cx = bounds.origin.x + (ellipse.cx.has_value() ? RCTResolveValueUnit(ellipse.cx.value(), bounds.size.width) : bounds.size.width / 2.0f); + CGFloat cy = bounds.origin.y + (ellipse.cy.has_value() ? RCTResolveValueUnit(ellipse.cy.value(), bounds.size.height) : bounds.size.height / 2.0f); + + CGFloat diameterX = rx * 2.0f; + CGFloat diameterY = ry * 2.0f; + CGRect ellipseRect = CGRectMake(cx - rx, cy - ry, diameterX, diameterY); + + return [UIBezierPath bezierPathWithOvalInRect:ellipseRect]; +} + ++ (UIBezierPath *)createInsetPath:(const InsetShape &)inset bounds:(CGRect)bounds +{ + CGFloat top = bounds.origin.y + RCTResolveValueUnit(inset.top, bounds.size.height); + CGFloat right = bounds.origin.x + RCTResolveValueUnit(inset.right, bounds.size.width); + CGFloat bottom = bounds.origin.y + RCTResolveValueUnit(inset.bottom, bounds.size.height); + CGFloat left = bounds.origin.x + RCTResolveValueUnit(inset.left, bounds.size.width); + + CGRect insetRect = CGRectMake( + left, top, bounds.size.width - left - right, bounds.size.height - top - bottom); + + if (insetRect.size.width < 0 || insetRect.size.height < 0) { + return nil; + } + + CGFloat borderRadius = inset.borderRadius.has_value() ? RCTResolveValueUnit(inset.borderRadius.value(), MIN(insetRect.size.width, insetRect.size.height)) : 0.0f; + + return [UIBezierPath bezierPathWithRoundedRect:insetRect cornerRadius:borderRadius]; +} + ++ (UIBezierPath *)createPolygonPath:(const PolygonShape &)polygon bounds:(CGRect)bounds +{ + if (polygon.points.empty()) { + return nil; + } + + UIBezierPath *path = [UIBezierPath bezierPath]; + + const auto &firstPoint = polygon.points[0]; + [path moveToPoint:CGPointMake( + bounds.origin.x + RCTResolveValueUnit(firstPoint.first, bounds.size.width), + bounds.origin.y + RCTResolveValueUnit(firstPoint.second, bounds.size.height))]; + + for (size_t i = 1; i < polygon.points.size(); i++) { + const auto &point = polygon.points[i]; + [path addLineToPoint:CGPointMake( + bounds.origin.x + RCTResolveValueUnit(point.first, bounds.size.width), + bounds.origin.y + RCTResolveValueUnit(point.second, bounds.size.height))]; + } + + path.usesEvenOddFillRule = polygon.fillRule == FillRule::EvenOdd; + [path closePath]; + return path; +} + ++ (UIBezierPath *)createRectPath:(const RectShape &)rect bounds:(CGRect)bounds +{ + CGFloat top = bounds.origin.y + RCTResolveValueUnit(rect.top, bounds.size.height); + CGFloat right = bounds.origin.x + RCTResolveValueUnit(rect.right, bounds.size.width); + CGFloat bottom = bounds.origin.y + RCTResolveValueUnit(rect.bottom, bounds.size.height); + CGFloat left = bounds.origin.x + RCTResolveValueUnit(rect.left, bounds.size.width); + + CGRect clipRect = CGRectMake(left, top, right - left, bottom - top); + + if (clipRect.size.width < 0 || clipRect.size.height < 0) { + return nil; + } + + CGFloat borderRadius = rect.borderRadius.has_value() ? RCTResolveValueUnit(rect.borderRadius.value(), MIN(clipRect.size.width, clipRect.size.height)) : 0.0f; + return [UIBezierPath bezierPathWithRoundedRect:clipRect cornerRadius:borderRadius]; +} + ++ (UIBezierPath *)createXywhPath:(const XywhShape &)xywh bounds:(CGRect)bounds +{ + CGFloat x = bounds.origin.x + RCTResolveValueUnit(xywh.x, bounds.size.width); + CGFloat y = bounds.origin.y + RCTResolveValueUnit(xywh.y, bounds.size.height); + CGFloat width = RCTResolveValueUnit(xywh.width, bounds.size.width); + CGFloat height = RCTResolveValueUnit(xywh.height, bounds.size.height); + + CGRect xywhRect = CGRectMake(x, y, width, height); + + if (xywhRect.size.width < 0 || xywhRect.size.height < 0) { + return nil; + } + + CGFloat borderRadius = xywh.borderRadius.has_value() ? RCTResolveValueUnit(xywh.borderRadius.value(), MIN(xywhRect.size.width, xywhRect.size.height)) : 0.0f; + return [UIBezierPath bezierPathWithRoundedRect:xywhRect cornerRadius:borderRadius]; +} + ++ (UIBezierPath *)createPathFromBasicShape:(const BasicShape &)basicShape bounds:(CGRect)bounds +{ + if (std::holds_alternative(basicShape)) { + return [self createCirclePath:std::get(basicShape) bounds:bounds]; + } else if (std::holds_alternative(basicShape)) { + return [self createEllipsePath:std::get(basicShape) bounds:bounds]; + } else if (std::holds_alternative(basicShape)) { + return [self createInsetPath:std::get(basicShape) bounds:bounds]; + } else if (std::holds_alternative(basicShape)) { + return [self createPolygonPath:std::get(basicShape) bounds:bounds]; + } else if (std::holds_alternative(basicShape)) { + return [self createRectPath:std::get(basicShape) bounds:bounds]; + } else if (std::holds_alternative(basicShape)) { + return [self createXywhPath:std::get(basicShape) bounds:bounds]; + } + + return nil; +} + +@end diff --git a/packages/react-native/React/Fabric/Utils/RCTClipPathUtils.h b/packages/react-native/React/Fabric/Utils/RCTClipPathUtils.h new file mode 100644 index 00000000000000..66c8d88c744c16 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTClipPathUtils.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 +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTClipPathUtils : NSObject + ++ (RCTCornerRadii)adjustCornerRadiiForGeometryBox:(facebook::react::GeometryBox)geometryBox + cornerRadii:(RCTCornerRadii)cornerRadii + layoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics + yogaStyle:(const facebook::yoga::Style &)yogaStyle; + ++ (CGRect)getGeometryBoxRect:(facebook::react::GeometryBox)geometryBox + layoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics + yogaStyle:(const facebook::yoga::Style &)yogaStyle + bounds:(CGRect)bounds; + ++ (CALayer *_Nullable)createClipPathLayer:(const facebook::react::ClipPath &)clipPath + layoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics + yogaStyle:(const facebook::yoga::Style &)yogaStyle + bounds:(CGRect)bounds + cornerRadii:(RCTCornerRadii)cornerRadii; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTClipPathUtils.mm b/packages/react-native/React/Fabric/Utils/RCTClipPathUtils.mm new file mode 100644 index 00000000000000..8c1f918accae45 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTClipPathUtils.mm @@ -0,0 +1,154 @@ +/* + * 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 "RCTClipPathUtils.h" + +#import "RCTBasicShapeUtils.h" +#import + +using namespace facebook::react; + +@implementation RCTClipPathUtils + ++ (RCTCornerRadii)adjustCornerRadiiForGeometryBox:(GeometryBox)geometryBox + cornerRadii:(RCTCornerRadii)cornerRadii + layoutMetrics:(const LayoutMetrics &)layoutMetrics + yogaStyle:(const facebook::yoga::Style &)yogaStyle +{ + auto marginLeft = yogaStyle.margin(facebook::yoga::Edge::Left).value().unwrapOrDefault(0.0f); + auto marginRight = yogaStyle.margin(facebook::yoga::Edge::Right).value().unwrapOrDefault(0.0f); + auto marginTop = yogaStyle.margin(facebook::yoga::Edge::Top).value().unwrapOrDefault(0.0f); + auto marginBottom = yogaStyle.margin(facebook::yoga::Edge::Bottom).value().unwrapOrDefault(0.0f); + + RCTCornerRadii adjustedRadii = cornerRadii; + + switch (geometryBox) { + case GeometryBox::MarginBox: { + // margin-box: extend border-radius by margin amount + adjustedRadii.topLeftHorizontal += marginLeft; + adjustedRadii.topLeftVertical += marginTop; + adjustedRadii.topRightHorizontal += marginRight; + adjustedRadii.topRightVertical += marginTop; + adjustedRadii.bottomLeftHorizontal += marginLeft; + adjustedRadii.bottomLeftVertical += marginBottom; + adjustedRadii.bottomRightHorizontal += marginRight; + adjustedRadii.bottomRightVertical += marginBottom; + break; + } + case GeometryBox::BorderBox: + case GeometryBox::StrokeBox: + case GeometryBox::ViewBox: + // border-box: use border-radius as-is (this is the reference) + break; + + case GeometryBox::PaddingBox: { + // padding-box: reduce border-radius by border width + // Formula: max(0, border-radius - border-width) + adjustedRadii.topLeftHorizontal = MAX(0.0f, cornerRadii.topLeftHorizontal - layoutMetrics.borderWidth.left); + adjustedRadii.topLeftVertical = MAX(0.0f, cornerRadii.topLeftVertical - layoutMetrics.borderWidth.top); + adjustedRadii.topRightHorizontal = MAX(0.0f, cornerRadii.topRightHorizontal - layoutMetrics.borderWidth.right); + adjustedRadii.topRightVertical = MAX(0.0f, cornerRadii.topRightVertical - layoutMetrics.borderWidth.top); + adjustedRadii.bottomLeftHorizontal = MAX(0.0f, cornerRadii.bottomLeftHorizontal - layoutMetrics.borderWidth.left); + adjustedRadii.bottomLeftVertical = MAX(0.0f, cornerRadii.bottomLeftVertical - layoutMetrics.borderWidth.bottom); + adjustedRadii.bottomRightHorizontal = MAX(0.0f, cornerRadii.bottomRightHorizontal - layoutMetrics.borderWidth.right); + adjustedRadii.bottomRightVertical = MAX(0.0f, cornerRadii.bottomRightVertical - layoutMetrics.borderWidth.bottom); + break; + } + case GeometryBox::ContentBox: + case GeometryBox::FillBox: { + // content-box: reduce border-radius by border width + padding + // contentInsets = border + padding, so we reduce by full contentInsets + adjustedRadii.topLeftHorizontal = MAX(0.0f, cornerRadii.topLeftHorizontal - layoutMetrics.contentInsets.left); + adjustedRadii.topLeftVertical = MAX(0.0f, cornerRadii.topLeftVertical - layoutMetrics.contentInsets.top); + adjustedRadii.topRightHorizontal = MAX(0.0f, cornerRadii.topRightHorizontal - layoutMetrics.contentInsets.right); + adjustedRadii.topRightVertical = MAX(0.0f, cornerRadii.topRightVertical - layoutMetrics.contentInsets.top); + adjustedRadii.bottomLeftHorizontal = MAX(0.0f, cornerRadii.bottomLeftHorizontal - layoutMetrics.contentInsets.left); + adjustedRadii.bottomLeftVertical = MAX(0.0f, cornerRadii.bottomLeftVertical - layoutMetrics.contentInsets.bottom); + adjustedRadii.bottomRightHorizontal = MAX(0.0f, cornerRadii.bottomRightHorizontal - layoutMetrics.contentInsets.right); + adjustedRadii.bottomRightVertical = MAX(0.0f, cornerRadii.bottomRightVertical - layoutMetrics.contentInsets.bottom); + break; + } + } + + return adjustedRadii; +} + ++ (CGRect)getGeometryBoxRect:(GeometryBox)geometryBox + layoutMetrics:(const LayoutMetrics &)layoutMetrics + yogaStyle:(const facebook::yoga::Style &)yogaStyle + bounds:(CGRect)bounds +{ + auto marginLeft = yogaStyle.margin(facebook::yoga::Edge::Left).value().unwrapOrDefault(0.0f); + auto marginRight = yogaStyle.margin(facebook::yoga::Edge::Right).value().unwrapOrDefault(0.0f); + auto marginTop = yogaStyle.margin(facebook::yoga::Edge::Top).value().unwrapOrDefault(0.0f); + auto marginBottom = yogaStyle.margin(facebook::yoga::Edge::Bottom).value().unwrapOrDefault(0.0f); + + switch (geometryBox) { + case GeometryBox::ContentBox: + case GeometryBox::FillBox: + return RCTCGRectFromRect(layoutMetrics.getContentFrame()); + case GeometryBox::PaddingBox: + return RCTCGRectFromRect(layoutMetrics.getPaddingFrame()); + case GeometryBox::BorderBox: + case GeometryBox::StrokeBox: + case GeometryBox::ViewBox: + return bounds; + case GeometryBox::MarginBox: + return CGRectMake( + bounds.origin.x - marginLeft, + bounds.origin.y - marginTop, + bounds.size.width + marginLeft + marginRight, + bounds.size.height + marginTop + marginBottom + ); + } + + return bounds; +} + ++ (CALayer *)createClipPathLayer:(const ClipPath &)clipPath + layoutMetrics:(const LayoutMetrics &)layoutMetrics + yogaStyle:(const facebook::yoga::Style &)yogaStyle + bounds:(CGRect)bounds + cornerRadii:(RCTCornerRadii)cornerRadii +{ + CGRect box = bounds; + if (clipPath.geometryBox.has_value()) { + box = [self getGeometryBoxRect:clipPath.geometryBox.value() + layoutMetrics:layoutMetrics + yogaStyle:yogaStyle + bounds:bounds]; + } + + UIBezierPath *path = nil; + if (clipPath.shape.has_value()) { + path = [RCTBasicShapeUtils createPathFromBasicShape:clipPath.shape.value() bounds:box]; + } else if (clipPath.geometryBox.has_value()) { + // For geometry box only (no shape), create a rounded rectangle using border radius + // Adjust corner radii based on the geometry box type + RCTCornerRadii adjustedRadii = [self adjustCornerRadiiForGeometryBox:clipPath.geometryBox.value() + cornerRadii:cornerRadii + layoutMetrics:layoutMetrics + yogaStyle:yogaStyle]; + RCTCornerInsets cornerInsets = RCTGetCornerInsets(adjustedRadii, UIEdgeInsetsZero); + CGPathRef cgPath = RCTPathCreateWithRoundedRect(box, cornerInsets, nil, NO); + path = [UIBezierPath bezierPathWithCGPath:cgPath]; + CGPathRelease(cgPath); + } + + if (path == nil) { + return nil; + } + + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + maskLayer.path = path.CGPath; + if (path.usesEvenOddFillRule) { + maskLayer.fillRule = kCAFillRuleEvenOdd; + } + return maskLayer; +} + +@end 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 1b3de652825e79..af24df851cecf5 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -373,7 +374,16 @@ BaseViewProps::BaseViewProps( rawProps, "removeClippedSubviews", sourceProps.removeClippedSubviews, - false)) {} + false)), + clipPath( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.clipPath + : convertRawProp( + context, + rawProps, + "clipPath", + sourceProps.clipPath, + {})) {} #define VIEW_EVENT_CASE(eventType) \ case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ @@ -431,6 +441,7 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(filter); RAW_SET_PROP_SWITCH_CASE_BASIC(boxShadow); RAW_SET_PROP_SWITCH_CASE_BASIC(mixBlendMode); + RAW_SET_PROP_SWITCH_CASE_BASIC(clipPath); // events field VIEW_EVENT_CASE(PointerEnter); VIEW_EVENT_CASE(PointerEnterCapture); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index 7554ba7cad86ab..fab60ac0588de8 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,8 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { bool removeClippedSubviews{false}; + std::optional clipPath{}; + #pragma mark - Convenience Methods CascadedBorderWidths getBorderWidths() const; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.cpp new file mode 100644 index 00000000000000..d5a4751299f364 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.cpp @@ -0,0 +1,364 @@ +/* + * 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. + */ + +#include "ClipPathPropsConversions.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { +ValueUnit convertLengthPercentageToValueUnit( + const std::variant& value) { + if (std::holds_alternative(value)) { + return {std::get(value).value, UnitType::Point}; + } else { + return {std::get(value).value, UnitType::Percent}; + } +} + +GeometryBox convertCSSGeometryBox(CSSGeometryBox cssBox) { + switch (cssBox) { + case CSSGeometryBox::MarginBox: + return GeometryBox::MarginBox; + case CSSGeometryBox::BorderBox: + return GeometryBox::BorderBox; + case CSSGeometryBox::ContentBox: + return GeometryBox::ContentBox; + case CSSGeometryBox::PaddingBox: + return GeometryBox::PaddingBox; + case CSSGeometryBox::FillBox: + return GeometryBox::FillBox; + case CSSGeometryBox::StrokeBox: + return GeometryBox::StrokeBox; + case CSSGeometryBox::ViewBox: + return GeometryBox::ViewBox; + } +} + +std::optional getOptionalValueUnit( + const std::unordered_map& rawShape, + const std::string& key) { + auto it = rawShape.find(key); + if (it != rawShape.end()) { + return toValueUnit(it->second); + } + return std::nullopt; +} +} // namespace + +std::optional fromCSSClipPath(const CSSClipPath& cssClipPath) { + ClipPath result; + + if (cssClipPath.shape) { + const auto& cssShape = *cssClipPath.shape; + + if (std::holds_alternative(cssShape)) { + auto cssCircle = std::get(cssShape); + CircleShape circle; + if (cssCircle.radius) { + circle.r = convertLengthPercentageToValueUnit(*cssCircle.radius); + } + if (cssCircle.cx) { + circle.cx = convertLengthPercentageToValueUnit(*cssCircle.cx); + } + if (cssCircle.cy) { + circle.cy = convertLengthPercentageToValueUnit(*cssCircle.cy); + } + result.shape = circle; + } else if (std::holds_alternative(cssShape)) { + auto cssEllipse = std::get(cssShape); + EllipseShape ellipse; + if (cssEllipse.rx) { + ellipse.rx = convertLengthPercentageToValueUnit(*cssEllipse.rx); + } + if (cssEllipse.ry) { + ellipse.ry = convertLengthPercentageToValueUnit(*cssEllipse.ry); + } + if (cssEllipse.cx) { + ellipse.cx = convertLengthPercentageToValueUnit(*cssEllipse.cx); + } + if (cssEllipse.cy) { + ellipse.cy = convertLengthPercentageToValueUnit(*cssEllipse.cy); + } + result.shape = ellipse; + } else if (std::holds_alternative(cssShape)) { + auto cssInset = std::get(cssShape); + InsetShape inset; + if (cssInset.top) { + inset.top = convertLengthPercentageToValueUnit(*cssInset.top); + } + if (cssInset.right) { + inset.right = convertLengthPercentageToValueUnit(*cssInset.right); + } + if (cssInset.bottom) { + inset.bottom = convertLengthPercentageToValueUnit(*cssInset.bottom); + } + if (cssInset.left) { + inset.left = convertLengthPercentageToValueUnit(*cssInset.left); + } + if (cssInset.borderRadius) { + inset.borderRadius = + convertLengthPercentageToValueUnit(*cssInset.borderRadius); + } + result.shape = inset; + } else if (std::holds_alternative(cssShape)) { + auto cssPolygon = std::get(cssShape); + PolygonShape polygon; + for (const auto& point : cssPolygon.points) { + polygon.points.push_back( + {convertLengthPercentageToValueUnit(point.first), + convertLengthPercentageToValueUnit(point.second)}); + } + if (cssPolygon.fillRule == CSSFillRule::NonZero) { + polygon.fillRule = FillRule::NonZero; + } else if (cssPolygon.fillRule == CSSFillRule::EvenOdd) { + polygon.fillRule = FillRule::EvenOdd; + } + result.shape = polygon; + } else if (std::holds_alternative(cssShape)) { + auto cssRect = std::get(cssShape); + RectShape rect; + rect.top = convertLengthPercentageToValueUnit(cssRect.top); + rect.right = convertLengthPercentageToValueUnit(cssRect.right); + rect.bottom = convertLengthPercentageToValueUnit(cssRect.bottom); + rect.left = convertLengthPercentageToValueUnit(cssRect.left); + if (cssRect.borderRadius) { + rect.borderRadius = + convertLengthPercentageToValueUnit(*cssRect.borderRadius); + } + result.shape = rect; + } else if (std::holds_alternative(cssShape)) { + auto cssXywh = std::get(cssShape); + XywhShape xywh; + xywh.x = convertLengthPercentageToValueUnit(cssXywh.x); + xywh.y = convertLengthPercentageToValueUnit(cssXywh.y); + xywh.width = convertLengthPercentageToValueUnit(cssXywh.width); + xywh.height = convertLengthPercentageToValueUnit(cssXywh.height); + if (cssXywh.borderRadius) { + xywh.borderRadius = + convertLengthPercentageToValueUnit(*cssXywh.borderRadius); + } + result.shape = xywh; + } + } + + if (cssClipPath.geometryBox) { + result.geometryBox = convertCSSGeometryBox(*cssClipPath.geometryBox); + } + + return result; +} + +void parseProcessedClipPath( + const PropsParserContext& context, + const RawValue& value, + std::optional& result) { + if (!value.hasType>()) { + result = {}; + return; + } + + auto rawClipPath = + static_cast>(value); + ClipPath clipPath; + + auto shapeIt = rawClipPath.find("shape"); + if (shapeIt != rawClipPath.end() && + shapeIt->second.hasType>()) { + auto rawShape = + static_cast>(shapeIt->second); + + auto typeIt = rawShape.find("type"); + if (typeIt == rawShape.end() || !typeIt->second.hasType()) { + result = {}; + return; + } + + auto type = (std::string)(typeIt->second); + + if (type == "inset") { + InsetShape inset; + + if (auto top = getOptionalValueUnit(rawShape, "top")) { + inset.top = *top; + } + if (auto right = getOptionalValueUnit(rawShape, "right")) { + inset.right = *right; + } + if (auto bottom = getOptionalValueUnit(rawShape, "bottom")) { + inset.bottom = *bottom; + } + if (auto left = getOptionalValueUnit(rawShape, "left")) { + inset.left = *left; + } + if (auto borderRadius = getOptionalValueUnit(rawShape, "borderRadius")) { + inset.borderRadius = *borderRadius; + } + + clipPath.shape = inset; + } else if (type == "circle") { + CircleShape circle; + + // r is optional - defaults to 50% handled in rendering + if (auto r = getOptionalValueUnit(rawShape, "r")) { + circle.r = *r; + } + if (auto cx = getOptionalValueUnit(rawShape, "cx")) { + circle.cx = *cx; + } + if (auto cy = getOptionalValueUnit(rawShape, "cy")) { + circle.cy = *cy; + } + + clipPath.shape = circle; + } else if (type == "ellipse") { + EllipseShape ellipse; + + if (auto rx = getOptionalValueUnit(rawShape, "rx")) { + ellipse.rx = *rx; + } + // rx is optional - defaults to 50% handled in rendering + if (auto ry = getOptionalValueUnit(rawShape, "ry")) { + ellipse.ry = *ry; + } + // ry is optional - defaults to 50% handled in rendering + if (auto cx = getOptionalValueUnit(rawShape, "cx")) { + ellipse.cx = *cx; + } + if (auto cy = getOptionalValueUnit(rawShape, "cy")) { + ellipse.cy = *cy; + } + + clipPath.shape = ellipse; + } else if (type == "polygon") { + PolygonShape polygon; + + auto pointsIt = rawShape.find("points"); + if (pointsIt != rawShape.end() && + pointsIt->second.hasType>()) { + auto rawPoints = static_cast>(pointsIt->second); + for (const auto& rawPoint : rawPoints) { + if (rawPoint.hasType>()) { + auto pointMap = + static_cast>( + rawPoint); + auto xIt = pointMap.find("x"); + auto yIt = pointMap.find("y"); + + if (xIt != pointMap.end() && yIt != pointMap.end()) { + polygon.points.push_back( + {toValueUnit(xIt->second), toValueUnit(yIt->second)}); + } + } + } + } + + auto fillRuleIt = rawShape.find("fillRule"); + if (fillRuleIt != rawShape.end() && + fillRuleIt->second.hasType()) { + auto fillRule = (std::string)(fillRuleIt->second); + if (fillRule == "nonzero") { + polygon.fillRule = FillRule::NonZero; + } else if (fillRule == "evenodd") { + polygon.fillRule = FillRule::EvenOdd; + } + } + + clipPath.shape = polygon; + } else if (type == "rect") { + RectShape rect; + + if (auto top = getOptionalValueUnit(rawShape, "top")) { + rect.top = *top; + } + if (auto right = getOptionalValueUnit(rawShape, "right")) { + rect.right = *right; + } + if (auto bottom = getOptionalValueUnit(rawShape, "bottom")) { + rect.bottom = *bottom; + } + if (auto left = getOptionalValueUnit(rawShape, "left")) { + rect.left = *left; + } + if (auto borderRadius = getOptionalValueUnit(rawShape, "borderRadius")) { + rect.borderRadius = *borderRadius; + } + + clipPath.shape = rect; + } else if (type == "xywh") { + XywhShape xywh; + + if (auto x = getOptionalValueUnit(rawShape, "x")) { + xywh.x = *x; + } + if (auto y = getOptionalValueUnit(rawShape, "y")) { + xywh.y = *y; + } + if (auto width = getOptionalValueUnit(rawShape, "width")) { + xywh.width = *width; + } + if (auto height = getOptionalValueUnit(rawShape, "height")) { + xywh.height = *height; + } + if (auto borderRadius = getOptionalValueUnit(rawShape, "borderRadius")) { + xywh.borderRadius = *borderRadius; + } + + clipPath.shape = xywh; + } else { + result = {}; + return; + } + } + + auto geometryBoxIt = rawClipPath.find("geometryBox"); + if (geometryBoxIt != rawClipPath.end() && + geometryBoxIt->second.hasType()) { + auto geometryBox = (std::string)(geometryBoxIt->second); + + if (geometryBox == "border-box") { + clipPath.geometryBox = GeometryBox::BorderBox; + } else if (geometryBox == "padding-box") { + clipPath.geometryBox = GeometryBox::PaddingBox; + } else if (geometryBox == "content-box") { + clipPath.geometryBox = GeometryBox::ContentBox; + } else if (geometryBox == "margin-box") { + clipPath.geometryBox = GeometryBox::MarginBox; + } else if (geometryBox == "fill-box") { + clipPath.geometryBox = GeometryBox::FillBox; + } else if (geometryBox == "stroke-box") { + clipPath.geometryBox = GeometryBox::StrokeBox; + } else if (geometryBox == "view-box") { + clipPath.geometryBox = GeometryBox::ViewBox; + } + } + + result = clipPath; +} + +void parseUnprocessedClipPath( + std::string&& value, + std::optional& result) { + auto clipPath = parseCSSProperty((std::string)value); + if (std::holds_alternative(clipPath)) { + result = {}; + return; + } + + result = fromCSSClipPath(std::get(clipPath)); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.h new file mode 100644 index 00000000000000..95498227b2cd93 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.h @@ -0,0 +1,32 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +void parseProcessedClipPath(const PropsParserContext &context, const RawValue &value, std::optional &result); + +void parseUnprocessedClipPath(std::string &&value, std::optional &result); + +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, std::optional &result) +{ + if (ReactNativeFeatureFlags::enableNativeCSSParsing()) { + parseUnprocessedClipPath((std::string)value, result); + } else { + parseProcessedClipPath(context, value, result); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index 641f544bee6209..7d4c444bb376b1 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -64,7 +64,7 @@ void ViewShadowNode::initialize() noexcept { viewProps.mixBlendMode != BlendMode::Normal || viewProps.isolation == Isolation::Isolate || HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) || - !viewProps.accessibilityOrder.empty(); + !viewProps.accessibilityOrder.empty() || viewProps.clipPath.has_value(); bool formsView = formsStackingContext || isColorMeaningful(viewProps.backgroundColor) || hasBorder() || diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp index b54b60430f7bca..f7c5d047474164 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp @@ -8,6 +8,7 @@ #include #include +#include #include namespace facebook::react { @@ -239,4 +240,309 @@ TEST(ConversionsTest, unprocessed_filter_objects_multiple_objects) { EXPECT_TRUE(filters.empty()); } +TEST(ConversionsTest, unprocessed_clip_path_string_inset) { + std::optional clipPath; + parseUnprocessedClipPath("inset(10px 20% 30px 5%)", clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto inset = std::get(*clipPath->shape); + EXPECT_EQ(inset.top.value, 10); + EXPECT_EQ(inset.top.unit, UnitType::Point); + EXPECT_EQ(inset.right.value, 20); + EXPECT_EQ(inset.right.unit, UnitType::Percent); + EXPECT_EQ(inset.bottom.value, 30); + EXPECT_EQ(inset.bottom.unit, UnitType::Point); + EXPECT_EQ(inset.left.value, 5); + EXPECT_EQ(inset.left.unit, UnitType::Percent); + EXPECT_FALSE(inset.borderRadius.has_value()); +} + +TEST(ConversionsTest, unprocessed_clip_path_string_circle) { + std::optional clipPath; + parseUnprocessedClipPath("circle(50% at 25% 75%)", clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto circle = std::get(*clipPath->shape); + EXPECT_EQ(circle.r->value, 50); + EXPECT_EQ(circle.r->unit, UnitType::Percent); + ASSERT_TRUE(circle.cx.has_value()); + EXPECT_EQ(circle.cx->value, 25); + EXPECT_EQ(circle.cx->unit, UnitType::Percent); + ASSERT_TRUE(circle.cy.has_value()); + EXPECT_EQ(circle.cy->value, 75); + EXPECT_EQ(circle.cy->unit, UnitType::Percent); +} + +TEST(ConversionsTest, unprocessed_clip_path_string_ellipse) { + std::optional clipPath; + parseUnprocessedClipPath("ellipse(100px 50px at 10% 20%)", clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto ellipse = std::get(*clipPath->shape); + EXPECT_EQ(ellipse.rx->value, 100); + EXPECT_EQ(ellipse.rx->unit, UnitType::Point); + EXPECT_EQ(ellipse.ry->value, 50); + EXPECT_EQ(ellipse.ry->unit, UnitType::Point); + ASSERT_TRUE(ellipse.cx.has_value()); + EXPECT_EQ(ellipse.cx->value, 10); + EXPECT_EQ(ellipse.cx->unit, UnitType::Percent); + ASSERT_TRUE(ellipse.cy.has_value()); + EXPECT_EQ(ellipse.cy->value, 20); + EXPECT_EQ(ellipse.cy->unit, UnitType::Percent); +} + +TEST(ConversionsTest, unprocessed_clip_path_string_polygon) { + std::optional clipPath; + parseUnprocessedClipPath("polygon(0 0, 100% 0, 50% 100%)", clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto polygon = std::get(*clipPath->shape); + ASSERT_EQ(polygon.points.size(), 3); + + EXPECT_EQ(polygon.points[0].first.value, 0); + EXPECT_EQ(polygon.points[0].first.unit, UnitType::Point); + EXPECT_EQ(polygon.points[0].second.value, 0); + EXPECT_EQ(polygon.points[0].second.unit, UnitType::Point); + + EXPECT_EQ(polygon.points[1].first.value, 100); + EXPECT_EQ(polygon.points[1].first.unit, UnitType::Percent); + EXPECT_EQ(polygon.points[1].second.value, 0); + EXPECT_EQ(polygon.points[1].second.unit, UnitType::Point); + + EXPECT_EQ(polygon.points[2].first.value, 50); + EXPECT_EQ(polygon.points[2].first.unit, UnitType::Percent); + EXPECT_EQ(polygon.points[2].second.value, 100); + EXPECT_EQ(polygon.points[2].second.unit, UnitType::Percent); +} + +TEST(ConversionsTest, unprocessed_clip_path_string_with_geometry_box) { + std::optional clipPath; + parseUnprocessedClipPath("inset(10px) border-box", clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(clipPath->geometryBox.has_value()); + EXPECT_EQ(*clipPath->geometryBox, GeometryBox::BorderBox); +} + +TEST(ConversionsTest, processed_clip_path_inset) { + RawValue value{folly::dynamic::object( + "shape", + folly::dynamic::object("type", "inset")("top", 10)("right", "20%")( + "bottom", 30)("left", "5%"))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto inset = std::get(*clipPath->shape); + EXPECT_EQ(inset.top.value, 10); + EXPECT_EQ(inset.top.unit, UnitType::Point); + EXPECT_EQ(inset.right.value, 20); + EXPECT_EQ(inset.right.unit, UnitType::Percent); + EXPECT_EQ(inset.bottom.value, 30); + EXPECT_EQ(inset.bottom.unit, UnitType::Point); + EXPECT_EQ(inset.left.value, 5); + EXPECT_EQ(inset.left.unit, UnitType::Percent); +} + +TEST(ConversionsTest, processed_clip_path_circle) { + RawValue value{folly::dynamic::object( + "shape", + folly::dynamic::object("type", "circle")("r", "50%")("cx", "25%")( + "cy", "75%"))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto circle = std::get(*clipPath->shape); + EXPECT_EQ(circle.r->value, 50); + EXPECT_EQ(circle.r->unit, UnitType::Percent); + ASSERT_TRUE(circle.cx.has_value()); + EXPECT_EQ(circle.cx->value, 25); + EXPECT_EQ(circle.cx->unit, UnitType::Percent); + ASSERT_TRUE(circle.cy.has_value()); + EXPECT_EQ(circle.cy->value, 75); + EXPECT_EQ(circle.cy->unit, UnitType::Percent); +} + +TEST(ConversionsTest, processed_clip_path_ellipse) { + RawValue value{folly::dynamic::object( + "shape", + folly::dynamic::object("type", "ellipse")("rx", 100)("ry", 50)( + "cx", "10%")("cy", "20%"))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto ellipse = std::get(*clipPath->shape); + EXPECT_EQ(ellipse.rx->value, 100); + EXPECT_EQ(ellipse.rx->unit, UnitType::Point); + EXPECT_EQ(ellipse.ry->value, 50); + EXPECT_EQ(ellipse.ry->unit, UnitType::Point); + ASSERT_TRUE(ellipse.cx.has_value()); + EXPECT_EQ(ellipse.cx->value, 10); + EXPECT_EQ(ellipse.cx->unit, UnitType::Percent); + ASSERT_TRUE(ellipse.cy.has_value()); + EXPECT_EQ(ellipse.cy->value, 20); + EXPECT_EQ(ellipse.cy->unit, UnitType::Percent); +} + +TEST(ConversionsTest, processed_clip_path_polygon) { + RawValue value{folly::dynamic::object( + "shape", + folly::dynamic::object("type", "polygon")( + "points", + folly::dynamic::array( + folly::dynamic::object("x", 0)("y", 0), + folly::dynamic::object("x", "100%")("y", 0), + folly::dynamic::object("x", "50%")("y", "100%")))( + "fillRule", "evenodd"))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto polygon = std::get(*clipPath->shape); + ASSERT_EQ(polygon.points.size(), 3); + + EXPECT_EQ(polygon.points[0].first.value, 0); + EXPECT_EQ(polygon.points[0].first.unit, UnitType::Point); + EXPECT_EQ(polygon.points[0].second.value, 0); + EXPECT_EQ(polygon.points[0].second.unit, UnitType::Point); + + EXPECT_EQ(polygon.points[1].first.value, 100); + EXPECT_EQ(polygon.points[1].first.unit, UnitType::Percent); + EXPECT_EQ(polygon.points[1].second.value, 0); + EXPECT_EQ(polygon.points[1].second.unit, UnitType::Point); + + EXPECT_EQ(polygon.points[2].first.value, 50); + EXPECT_EQ(polygon.points[2].first.unit, UnitType::Percent); + EXPECT_EQ(polygon.points[2].second.value, 100); + EXPECT_EQ(polygon.points[2].second.unit, UnitType::Percent); + + ASSERT_TRUE(polygon.fillRule.has_value()); + EXPECT_EQ(*polygon.fillRule, FillRule::EvenOdd); +} + +TEST(ConversionsTest, processed_clip_path_rect) { + RawValue value{folly::dynamic::object( + "shape", + folly::dynamic::object("type", "rect")("top", 10)("right", "90%")( + "bottom", "80%")("left", 5))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto rect = std::get(*clipPath->shape); + EXPECT_EQ(rect.top.value, 10); + EXPECT_EQ(rect.top.unit, UnitType::Point); + EXPECT_EQ(rect.right.value, 90); + EXPECT_EQ(rect.right.unit, UnitType::Percent); + EXPECT_EQ(rect.bottom.value, 80); + EXPECT_EQ(rect.bottom.unit, UnitType::Percent); + EXPECT_EQ(rect.left.value, 5); + EXPECT_EQ(rect.left.unit, UnitType::Point); +} + +TEST(ConversionsTest, processed_clip_path_xywh) { + RawValue value{folly::dynamic::object( + "shape", + folly::dynamic::object("type", "xywh")("x", 10)("y", "20%")("width", 100)( + "height", "50%")("borderRadius", 5))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(std::holds_alternative(*clipPath->shape)); + + auto xywh = std::get(*clipPath->shape); + EXPECT_EQ(xywh.x.value, 10); + EXPECT_EQ(xywh.x.unit, UnitType::Point); + EXPECT_EQ(xywh.y.value, 20); + EXPECT_EQ(xywh.y.unit, UnitType::Percent); + EXPECT_EQ(xywh.width.value, 100); + EXPECT_EQ(xywh.width.unit, UnitType::Point); + EXPECT_EQ(xywh.height.value, 50); + EXPECT_EQ(xywh.height.unit, UnitType::Percent); + ASSERT_TRUE(xywh.borderRadius.has_value()); + EXPECT_EQ(xywh.borderRadius->value, 5); + EXPECT_EQ(xywh.borderRadius->unit, UnitType::Point); +} + +TEST(ConversionsTest, processed_clip_path_with_geometry_box) { + RawValue value{folly::dynamic::object( + "shape", folly::dynamic::object("type", "inset")("top", 10))( + "geometryBox", "padding-box")}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + ASSERT_TRUE(clipPath.has_value()); + ASSERT_TRUE(clipPath->shape.has_value()); + ASSERT_TRUE(clipPath->geometryBox.has_value()); + EXPECT_EQ(*clipPath->geometryBox, GeometryBox::PaddingBox); +} + +TEST(ConversionsTest, processed_clip_path_invalid_type) { + RawValue value{folly::dynamic::object( + "shape", folly::dynamic::object("type", "invalid"))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + EXPECT_FALSE(clipPath.has_value()); +} + +TEST(ConversionsTest, processed_clip_path_missing_shape_type) { + RawValue value{ + folly::dynamic::object("shape", folly::dynamic::object("top", 10))}; + + std::optional clipPath; + parseProcessedClipPath( + PropsParserContext{-1, ContextContainer{}}, value, clipPath); + + EXPECT_FALSE(clipPath.has_value()); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSCircleShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSCircleShape.h new file mode 100644 index 00000000000000..69289929fd31d1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSCircleShape.h @@ -0,0 +1,73 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSCircleShape { + std::optional> radius; + std::optional> cx; + std::optional> cy; + + bool operator==(const CSSCircleShape &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) + -> std::optional + { + if (!iequals(func.name, "circle")) { + return {}; + } + + CSSCircleShape shape; + + auto radius = parseNextCSSValue(parser); + if (std::holds_alternative(radius)) { + shape.radius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.radius = std::get(radius); + } + parser.consumeWhitespace(); + auto atResult = parser.consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("at"); + }); + + if (atResult) { + parser.consumeWhitespace(); + auto cx = parseNextCSSValue(parser); + if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } else if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } + parser.consumeWhitespace(); + auto cy = parseNextCSSValue(parser); + if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } else if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSClipPath.h b/packages/react-native/ReactCommon/react/renderer/css/CSSClipPath.h new file mode 100644 index 00000000000000..b6d52a986f0206 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSClipPath.h @@ -0,0 +1,162 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +enum class CSSGeometryBox : uint8_t { + BorderBox, + PaddingBox, + ContentBox, + MarginBox, + FillBox, + StrokeBox, + ViewBox, +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consumePreservedToken(const CSSPreservedToken &token) -> std::optional + { + if (token.type() == CSSTokenType::Ident) { + auto lowercase = fnv1aLowercase(token.stringValue()); + if (lowercase == fnv1a("border-box")) { + return CSSGeometryBox::BorderBox; + } else if (lowercase == fnv1a("padding-box")) { + return CSSGeometryBox::PaddingBox; + } else if (lowercase == fnv1a("content-box")) { + return CSSGeometryBox::ContentBox; + } else if (lowercase == fnv1a("margin-box")) { + return CSSGeometryBox::MarginBox; + } else if (lowercase == fnv1a("fill-box")) { + return CSSGeometryBox::FillBox; + } else if (lowercase == fnv1a("stroke-box")) { + return CSSGeometryBox::StrokeBox; + } else if (lowercase == fnv1a("view-box")) { + return CSSGeometryBox::ViewBox; + } + } + return {}; + } +}; + +static_assert(CSSDataType); + +/** + * Compound type for parsing basic shapes + */ +using CSSBasicShapeTypes = + CSSCompoundDataType; + +/** + * Variant type for basic shapes in clip-path + */ +using CSSBasicShape = CSSVariantWithTypes; + + +/** + * Representation of + * https://www.w3.org/TR/css-masking-1/#the-clip-path + * + * Supports: + * - + * - + * - + * - + */ +struct CSSClipPath { + std::optional shape; + std::optional geometryBox; + + bool operator==(const CSSClipPath &rhs) const + { + return shape == rhs.shape && geometryBox == rhs.geometryBox; + } +}; + +template <> +struct CSSDataTypeParser { + static auto consume(CSSSyntaxParser &parser) -> std::optional + { + auto shape = parseNextCSSValue(parser); + + if (!std::holds_alternative(shape)) { + auto geometryBox = parseNextCSSValue(parser, CSSDelimiter::Whitespace); + + CSSClipPath result; + if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } + + if (std::holds_alternative(geometryBox)) { + result.geometryBox = std::get(geometryBox); + } + + return result; + } + + auto geometryBox = parseNextCSSValue(parser); + + if (!std::holds_alternative(geometryBox)) { + auto shapeAfter = parseNextCSSValue(parser, CSSDelimiter::Whitespace); + + CSSClipPath result; + result.geometryBox = std::get(geometryBox); + + if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } + + return result; + } + + return {}; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSEllipseShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSEllipseShape.h new file mode 100644 index 00000000000000..0663035d10b11f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSEllipseShape.h @@ -0,0 +1,85 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSEllipseShape { + std::optional> rx; + std::optional> ry; + std::optional> cx; + std::optional> cy; + + bool operator==(const CSSEllipseShape &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) + -> std::optional + { + if (!iequals(func.name, "ellipse")) { + return {}; + } + + CSSEllipseShape shape; + + auto rx = parseNextCSSValue(parser); + if (std::holds_alternative(rx)) { + shape.rx = std::get(rx); + } else if (std::holds_alternative(rx)) { + shape.rx = std::get(rx); + } + parser.consumeWhitespace(); + auto ry = parseNextCSSValue(parser); + if (std::holds_alternative(ry)) { + shape.ry = std::get(ry); + } else if (std::holds_alternative(ry)) { + shape.ry = std::get(ry); + } else { + shape.ry = shape.rx; + } + + parser.consumeWhitespace(); + + auto atResult = parser.consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("at"); + }); + + if (atResult) { + parser.consumeWhitespace(); + auto cx = parseNextCSSValue(parser); + if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } else if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } + parser.consumeWhitespace(); + auto cy = parseNextCSSValue(parser); + if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } else if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSInsetShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSInsetShape.h new file mode 100644 index 00000000000000..75aa890b25da8a --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSInsetShape.h @@ -0,0 +1,105 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSInsetShape { + std::optional> top{}; + std::optional> bottom{}; + std::optional> left{}; + std::optional> right{}; + std::optional> borderRadius{}; + + bool operator==(const CSSInsetShape &rhs) const + { + return top == rhs.top && bottom == rhs.bottom && left == rhs.left && right == rhs.right && + borderRadius == rhs.borderRadius; + } +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) + -> std::optional + { + if (!iequals(func.name, "inset")) { + return {}; + } + + CSSInsetShape shape; + + std::vector> lengths; + for (int i = 0; i < 4; ++i) { + auto length = parseNextCSSValue(parser); + if (std::holds_alternative(length)) { + lengths.push_back(std::get(length)); + } else if (std::holds_alternative(length)) { + lengths.push_back(std::get(length)); + } else { + break; + } + + parser.consumeWhitespace(); + } + + if (lengths.empty()) { + return {}; + } + + if (lengths.size() == 1) { + shape.top = shape.right = shape.bottom = shape.left = lengths[0]; + } else if (lengths.size() == 2) { + shape.top = shape.bottom = lengths[0]; + shape.right = shape.left = lengths[1]; + } else if (lengths.size() == 3) { + shape.top = lengths[0]; + shape.right = shape.left = lengths[1]; + shape.bottom = lengths[2]; + } else if (lengths.size() == 4) { + shape.top = lengths[0]; + shape.right = lengths[1]; + shape.bottom = lengths[2]; + shape.left = lengths[3]; + } + + parser.consumeWhitespace(); + + auto roundResult = parser.consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("round"); + }); + + if (roundResult) { + parser.consumeWhitespace(); + auto radius = parseNextCSSValue(parser); + if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSPolygonShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSPolygonShape.h new file mode 100644 index 00000000000000..f068430c362700 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSPolygonShape.h @@ -0,0 +1,113 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +enum class CSSFillRule : uint8_t { + NonZero, + EvenOdd, +}; + +template <> +struct CSSDataTypeParser { + static auto consumePreservedToken(const CSSPreservedToken &token) -> std::optional + { + if (token.type() == CSSTokenType::Ident) { + auto lowercase = fnv1aLowercase(token.stringValue()); + if (lowercase == fnv1a("nonzero")) { + return CSSFillRule::NonZero; + } else if (lowercase == fnv1a("evenodd")) { + return CSSFillRule::EvenOdd; + } + } + return {}; + } +}; + +static_assert(CSSDataType); + +struct CSSPolygonShape { + std::vector, std::variant>> points; + std::optional fillRule; + + bool operator==(const CSSPolygonShape &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) + -> std::optional + { + if (!iequals(func.name, "polygon")) { + return {}; + } + + CSSPolygonShape shape; + + auto firstValue = parseNextCSSValue(parser); + if (std::holds_alternative(firstValue)) { + shape.fillRule = std::get(firstValue); + parser.consumeDelimiter(CSSDelimiter::Comma); + parser.consumeWhitespace(); + } + + do { + auto x = parseNextCSSValue(parser); + if (std::holds_alternative(x)) { + break; + } + + parser.consumeWhitespace(); + + auto y = parseNextCSSValue(parser); + if (std::holds_alternative(y)) { + return {}; + } + + std::variant xValue; + std::variant yValue; + + if (std::holds_alternative(x)) { + xValue = std::get(x); + } else if (std::holds_alternative(x)) { + xValue = std::get(x); + } + + if (std::holds_alternative(y)) { + yValue = std::get(y); + } else if (std::holds_alternative(y)) { + yValue = std::get(y); + } + + shape.points.emplace_back(xValue, yValue); + } while (parser.consumeDelimiter(CSSDelimiter::Comma)); + + if (shape.points.size() < 3) { + return {}; + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSRectShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSRectShape.h new file mode 100644 index 00000000000000..c674de6a0fe169 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSRectShape.h @@ -0,0 +1,122 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSRectShape { + std::variant top; + std::variant right; + std::variant bottom; + std::variant left; + std::optional> borderRadius; + + bool operator==(const CSSRectShape &other) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) -> std::optional + { + if (!iequals(func.name, "rect")) { + return {}; + } + + auto top = parseNextCSSValue(parser); + if (std::holds_alternative(top)) { + return std::nullopt; + } + + parser.consumeWhitespace(); + + auto right = parseNextCSSValue(parser); + if (std::holds_alternative(right)) { + return std::nullopt; + } + + parser.consumeWhitespace(); + + auto bottom = parseNextCSSValue(parser); + if (std::holds_alternative(bottom)) { + return std::nullopt; + } + + parser.consumeWhitespace(); + + auto left = parseNextCSSValue(parser); + if (std::holds_alternative(left)) { + return std::nullopt; + } + + CSSRectShape shape; + + if (std::holds_alternative(top)) { + shape.top = std::get(top); + } else if (std::holds_alternative(top)) { + shape.top = std::get(top); + } else if (std::holds_alternative(top)) { + shape.top = CSSPercentage{0.0f}; + } + + if (std::holds_alternative(right)) { + shape.right = std::get(right); + } else if (std::holds_alternative(right)) { + shape.right = std::get(right); + } else if (std::holds_alternative(right)) { + shape.right = CSSPercentage{100.0f}; + } + + if (std::holds_alternative(bottom)) { + shape.bottom = std::get(bottom); + } else if (std::holds_alternative(bottom)) { + shape.bottom = std::get(bottom); + } else if (std::holds_alternative(bottom)) { + shape.bottom = CSSPercentage{100.0f}; + } + + if (std::holds_alternative(left)) { + shape.left = std::get(left); + } else if (std::holds_alternative(left)) { + shape.left = std::get(left); + } else if (std::holds_alternative(left)) { + shape.left = CSSPercentage{0.0f}; + } + + parser.consumeWhitespace(); + + auto roundResult = parser.consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("round"); + }); + + if (roundResult) { + parser.consumeWhitespace(); + auto radius = parseNextCSSValue(parser); + if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSXywhShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSXywhShape.h new file mode 100644 index 00000000000000..364509ad043059 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSXywhShape.h @@ -0,0 +1,119 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSXywhShape { + std::variant x; + std::variant y; + std::variant width; + std::variant height; + std::optional> borderRadius; + + bool operator==(const CSSXywhShape &other) const + { + return x == other.x && y == other.y && width == other.width && height == other.height && + borderRadius == other.borderRadius; + } +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) -> std::optional + { + if (!iequals(func.name, "xywh")) { + return {}; + } + + auto x = parseNextCSSValue(parser); + if (std::holds_alternative(x)) { + return std::nullopt; + } + + parser.consumeWhitespace(); + + auto y = parseNextCSSValue(parser); + if (std::holds_alternative(y)) { + return std::nullopt; + } + + parser.consumeWhitespace(); + + auto width = parseNextCSSValue(parser); + if (std::holds_alternative(width)) { + return std::nullopt; + } + + parser.consumeWhitespace(); + + auto height = parseNextCSSValue(parser); + if (std::holds_alternative(height)) { + return std::nullopt; + } + + CSSXywhShape shape; + + if (std::holds_alternative(x)) { + shape.x = std::get(x); + } else if (std::holds_alternative(x)) { + shape.x = std::get(x); + } + + if (std::holds_alternative(y)) { + shape.y = std::get(y); + } else if (std::holds_alternative(y)) { + shape.y = std::get(y); + } + + if (std::holds_alternative(width)) { + shape.width = std::get(width); + } else if (std::holds_alternative(width)) { + shape.width = std::get(width); + } + + if (std::holds_alternative(height)) { + shape.height = std::get(height); + } else if (std::holds_alternative(height)) { + shape.height = std::get(height); + } + + parser.consumeWhitespace(); + + auto roundResult = parser.consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("round"); + }); + + if (roundResult) { + parser.consumeWhitespace(); + auto radius = parseNextCSSValue(parser); + if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSClipPathTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSClipPathTest.cpp new file mode 100644 index 00000000000000..6001147432eab1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSClipPathTest.cpp @@ -0,0 +1,456 @@ +/* + * 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. + */ + +#include +#include + +namespace facebook::react { + +class CSSClipPathTest : public ::testing::Test {}; + +TEST_F(CSSClipPathTest, InsetSingleValue) { + auto result = parseCSSProperty("inset(10px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InsetTwoValues) { + auto result = parseCSSProperty("inset(10px 20px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InsetThreeValues) { + auto result = parseCSSProperty("inset(10px 20px 30px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 30.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InsetFourValues) { + auto result = parseCSSProperty("inset(10px 20px 30px 40px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 30.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 40.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InsetWithPercentage) { + auto result = parseCSSProperty("inset(10%)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSPercentage{.value = 10.0f}, + .right = CSSPercentage{.value = 10.0f}, + .bottom = CSSPercentage{.value = 10.0f}, + .left = CSSPercentage{.value = 10.0f}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InsetWithBorderRadius) { + auto result = parseCSSProperty("inset(10px round 5px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .borderRadius = CSSLength{.value = 5.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, CircleWithoutRadius) { + auto result = parseCSSProperty("circle()"); + decltype(result) expected = + CSSClipPath{.shape = CSSCircleShape{.radius = std::nullopt}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, CircleWithRadius) { + auto result = parseCSSProperty("circle(50px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSCircleShape{ + .radius = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, CircleWithPercentageRadius) { + auto result = parseCSSProperty("circle(25%)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSCircleShape{.radius = CSSPercentage{.value = 25.0f}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, CircleWithPosition) { + auto result = parseCSSProperty("circle(50px at 100px 100px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSCircleShape{ + .radius = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .cx = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + .cy = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, CircleWithPercentagePosition) { + auto result = parseCSSProperty("circle(50px at 25% 75%)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSCircleShape{ + .radius = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .cx = CSSPercentage{.value = 25.0f}, + .cy = CSSPercentage{.value = 75.0f}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, EllipseWithoutRadii) { + auto result = parseCSSProperty("ellipse()"); + decltype(result) expected = CSSClipPath{ + .shape = CSSEllipseShape{.rx = std::nullopt, .ry = std::nullopt}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, EllipseWithOneRadius) { + auto result = parseCSSProperty("ellipse(50px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSEllipseShape{ + .rx = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .ry = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, EllipseWithTwoRadii) { + auto result = parseCSSProperty("ellipse(50px 25px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSEllipseShape{ + .rx = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .ry = CSSLength{.value = 25.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, EllipseWithPosition) { + auto result = parseCSSProperty("ellipse(50px 25px at 100px 100px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSEllipseShape{ + .rx = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .ry = CSSLength{.value = 25.0f, .unit = CSSLengthUnit::Px}, + .cx = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + .cy = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, EllipseWithPercentagePosition) { + auto result = parseCSSProperty("ellipse(50px 25px at 10% 20%)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSEllipseShape{ + .rx = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .ry = CSSLength{.value = 25.0f, .unit = CSSLengthUnit::Px}, + .cx = CSSPercentage{.value = 10.0f}, + .cy = CSSPercentage{.value = 20.0f}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, PolygonBasic) { + auto result = + parseCSSProperty("polygon(0px 0px, 100px 0px, 100px 100px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSPolygonShape{ + .points = { + {CSSLength{.value = 0.0f, .unit = CSSLengthUnit::Px}, + CSSLength{.value = 0.0f, .unit = CSSLengthUnit::Px}}, + {CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + CSSLength{.value = 0.0f, .unit = CSSLengthUnit::Px}}, + {CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}}, + }}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, PolygonWithPercentages) { + auto result = parseCSSProperty( + "polygon(0% 0%, 100% 0%, 50% 100%) border-box"); + decltype(result) expected = CSSClipPath{ + .shape = + CSSPolygonShape{ + .points = + { + {CSSPercentage{.value = 0.0f}, + CSSPercentage{.value = 0.0f}}, + {CSSPercentage{.value = 100.0f}, + CSSPercentage{.value = 0.0f}}, + {CSSPercentage{.value = 50.0f}, + CSSPercentage{.value = 100.0f}}, + }}, + .geometryBox = CSSGeometryBox::BorderBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, PolygonWithEvenOdd) { + auto result = parseCSSProperty( + "polygon(evenodd, 0px 0px, 100px 0px, 100px 100px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSPolygonShape{ + .points = + { + {CSSLength{.value = 0.0f, .unit = CSSLengthUnit::Px}, + CSSLength{.value = 0.0f, .unit = CSSLengthUnit::Px}}, + {CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + CSSLength{.value = 0.0f, .unit = CSSLengthUnit::Px}}, + {CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}}, + }, + .fillRule = CSSFillRule::EvenOdd, + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, PolygonWithInvalidFillRule) { + auto result = parseCSSProperty( + "polygon(invalid, 0px 0px, 100px 0px, 100px 100px)"); + ASSERT_TRUE(std::holds_alternative(result)); +} +TEST_F(CSSClipPathTest, GeometryBoxBorderBox) { + auto result = parseCSSProperty("border-box"); + decltype(result) expected = + CSSClipPath{.geometryBox = CSSGeometryBox::BorderBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxPaddingBox) { + auto result = parseCSSProperty("padding-box"); + decltype(result) expected = + CSSClipPath{.geometryBox = CSSGeometryBox::PaddingBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxContentBox) { + auto result = parseCSSProperty("content-box"); + decltype(result) expected = + CSSClipPath{.geometryBox = CSSGeometryBox::ContentBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxMarginBox) { + auto result = parseCSSProperty("margin-box"); + decltype(result) expected = + CSSClipPath{.geometryBox = CSSGeometryBox::MarginBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxFillBox) { + auto result = parseCSSProperty("fill-box"); + decltype(result) expected = + CSSClipPath{.geometryBox = CSSGeometryBox::FillBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxStrokeBox) { + auto result = parseCSSProperty("stroke-box"); + decltype(result) expected = + CSSClipPath{.geometryBox = CSSGeometryBox::StrokeBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxViewBox) { + auto result = parseCSSProperty("view-box"); + decltype(result) expected = + CSSClipPath{.geometryBox = CSSGeometryBox::ViewBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InvalidInsetTooManyValues) { + auto result = + parseCSSProperty("inset(10px 20px 30px 40px 50px)"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, InvalidCircleWithInvalidRadius) { + auto result = parseCSSProperty("circle(invalid)"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, InvalidPolygonTooFewPoints) { + auto result = parseCSSProperty("polygon(0px 0px)"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, InvalidPolygonOddValues) { + auto result = parseCSSProperty("polygon(0px 0px, 100px)"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, RectBasic) { + auto result = parseCSSProperty("rect(10px 20px 30px 40px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSRectShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 30.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 40.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, RectWithBorderRadius) { + auto result = parseCSSProperty("rect(10px 20px 30px 40px round 5px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSRectShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 30.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 40.0f, .unit = CSSLengthUnit::Px}, + .borderRadius = CSSLength{.value = 5.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, RectWithAutoKeyword) { + auto result = parseCSSProperty("rect(auto auto auto auto)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSRectShape{ + .top = CSSPercentage{.value = 0.0f}, + .right = CSSPercentage{.value = 100.0f}, + .bottom = CSSPercentage{.value = 100.0f}, + .left = CSSPercentage{.value = 0.0f}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, XywhBasic) { + auto result = parseCSSProperty("xywh(10px 20px 100px 50px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSXywhShape{ + .x = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .y = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .width = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + .height = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, XywhWithBorderRadius) { + auto result = parseCSSProperty("xywh(10px 20px 100px 50px round 5px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSXywhShape{ + .x = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .y = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .width = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + .height = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .borderRadius = CSSLength{.value = 5.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, XywhWithPercentages) { + auto result = parseCSSProperty("xywh(10% 20% 100% 50%)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSXywhShape{ + .x = CSSPercentage{.value = 10.0f}, + .y = CSSPercentage{.value = 20.0f}, + .width = CSSPercentage{.value = 100.0f}, + .height = CSSPercentage{.value = 50.0f}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InvalidGeometryBox) { + auto result = parseCSSProperty("invalid-box"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, InvalidRectWrongNumberOfValues) { + auto result = parseCSSProperty("rect(10px 20px)"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, InvalidXywhWrongNumberOfValues) { + auto result = parseCSSProperty("xywh(10px 20px)"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, InvalidEllipseWithInvalidRadii) { + auto result = parseCSSProperty("ellipse(invalid)"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSClipPathTest, CaseInsensitive) { + auto result = parseCSSProperty("InSeT(10Px)"); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, WhitespaceHandling) { + auto result = parseCSSProperty(" inset( 10px 20px ) "); + decltype(result) expected = CSSClipPath{ + .shape = CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, CircleWithGeometryBox) { + auto result = parseCSSProperty("circle(50px) padding-box"); + decltype(result) expected = CSSClipPath{ + .shape = + CSSCircleShape{ + .radius = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}}, + .geometryBox = CSSGeometryBox::PaddingBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxThenCircle) { + auto result = parseCSSProperty("content-box circle(50px)"); + decltype(result) expected = CSSClipPath{ + .shape = + CSSCircleShape{ + .radius = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}}, + .geometryBox = CSSGeometryBox::ContentBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, InsetWithGeometryBox) { + auto result = parseCSSProperty("inset(10px 20px) border-box"); + decltype(result) expected = CSSClipPath{ + .shape = + CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}, + .geometryBox = CSSGeometryBox::BorderBox}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSClipPathTest, GeometryBoxThenInset) { + auto result = parseCSSProperty("margin-box inset(10px 20px)"); + decltype(result) expected = CSSClipPath{ + .shape = + CSSInsetShape{ + .top = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .right = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .bottom = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}, + .geometryBox = CSSGeometryBox::MarginBox}; + ASSERT_EQ(result, expected); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.cpp new file mode 100644 index 00000000000000..ddfa9973b183ea --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.cpp @@ -0,0 +1,287 @@ +/* + * 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. + */ + +#include "ClipPath.h" + +#include +#include +#include +#include + +namespace facebook::react { + +namespace { +std::string geometryBoxToString(facebook::react::GeometryBox box) { + switch (box) { + case facebook::react::GeometryBox::MarginBox: + return "margin-box"; + case facebook::react::GeometryBox::BorderBox: + return "border-box"; + case facebook::react::GeometryBox::ContentBox: + return "content-box"; + case facebook::react::GeometryBox::PaddingBox: + return "padding-box"; + case facebook::react::GeometryBox::FillBox: + return "fill-box"; + case facebook::react::GeometryBox::StrokeBox: + return "stroke-box"; + case facebook::react::GeometryBox::ViewBox: + return "view-box"; + } +} +} // namespace + +bool CircleShape::operator==(const CircleShape& other) const { + return cx == other.cx && cy == other.cy && r == other.r; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void CircleShape::toString(std::stringstream& ss) const { + ss << "circle("; + if (r) { + ss << r->toString(); + } + if (cx || cy) { + ss << " at "; + if (cx) { + ss << cx->toString(); + } + if (cy) { + ss << " " << cy->toString(); + } + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic CircleShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + if (r) { + result["r"] = r->toDynamic(); + } + if (cx) { + result["cx"] = cx->toDynamic(); + } + if (cy) { + result["cy"] = cy->toDynamic(); + } + return result; +} +#endif + +bool EllipseShape::operator==(const EllipseShape& other) const { + return cx == other.cx && cy == other.cy && rx == other.rx && ry == other.ry; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void EllipseShape::toString(std::stringstream& ss) const { + ss << "ellipse("; + if (rx) { + ss << rx->toString(); + } + if (ry) { + ss << " " << ry->toString(); + } + if (cx || cy) { + ss << " at "; + if (cx) { + ss << cx->toString(); + } + if (cy) { + ss << " " << cy->toString(); + } + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic EllipseShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + if (rx) { + result["rx"] = rx->toDynamic(); + } + if (ry) { + result["ry"] = ry->toDynamic(); + } + if (cx) { + result["cx"] = cx->toDynamic(); + } + if (cy) { + result["cy"] = cy->toDynamic(); + } + return result; +} +#endif + +bool InsetShape::operator==(const InsetShape& other) const { + return top == other.top && right == other.right && bottom == other.bottom && + left == other.left && borderRadius == other.borderRadius; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void InsetShape::toString(std::stringstream& ss) const { + ss << "inset(" << top.toString() << " " << right.toString() << " " + << bottom.toString() << " " << left.toString(); + if (borderRadius) { + ss << " round " << borderRadius->toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic InsetShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + result["top"] = top.toDynamic(); + result["right"] = right.toDynamic(); + result["bottom"] = bottom.toDynamic(); + result["left"] = left.toDynamic(); + if (borderRadius) { + result["borderRadius"] = borderRadius->toDynamic(); + } + return result; +} +#endif + +bool PolygonShape::operator==(const PolygonShape& other) const { + return points == other.points && fillRule == other.fillRule; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void PolygonShape::toString(std::stringstream& ss) const { + ss << "polygon("; + for (size_t i = 0; i < points.size(); i++) { + if (i > 0) { + ss << ", "; + } + ss << points[i].first.toString() << " " << points[i].second.toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic PolygonShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + folly::dynamic pointsArray = folly::dynamic::array(); + for (const auto& point : points) { + folly::dynamic pointObj = folly::dynamic::object(); + pointObj["x"] = point.first.toDynamic(); + pointObj["y"] = point.second.toDynamic(); + pointsArray.push_back(pointObj); + } + result["points"] = pointsArray; + if (fillRule) { + result["fillRule"] = fillRule == FillRule::EvenOdd ? "evenodd" : "nonzero"; + } + return result; +} +#endif + +bool RectShape::operator==(const RectShape& other) const { + return top == other.top && right == other.right && bottom == other.bottom && + left == other.left && borderRadius == other.borderRadius; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void RectShape::toString(std::stringstream& ss) const { + ss << "rect(" << top.toString() << " " << right.toString() << " " + << bottom.toString() << " " << left.toString() << " "; + if (borderRadius) { + ss << "round " << borderRadius->toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic RectShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + result["top"] = top.toDynamic(); + result["right"] = right.toDynamic(); + result["bottom"] = bottom.toDynamic(); + result["left"] = left.toDynamic(); + if (borderRadius) { + result["borderRadius"] = borderRadius->toDynamic(); + } + return result; +} +#endif + +bool XywhShape::operator==(const XywhShape& other) const { + return x == other.x && y == other.y && width == other.width && + height == other.height && borderRadius == other.borderRadius; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void XywhShape::toString(std::stringstream& ss) const { + ss << "xywh(" << x.toString() << " " << y.toString() << " " + << width.toString() << " " << height.toString(); + if (borderRadius) { + ss << " round " << borderRadius->toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic XywhShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + result["x"] = x.toDynamic(); + result["y"] = y.toDynamic(); + result["width"] = width.toDynamic(); + result["height"] = height.toDynamic(); + if (borderRadius) { + result["borderRadius"] = borderRadius->toDynamic(); + } + return result; +} +#endif + +bool ClipPath::operator==(const ClipPath& other) const { + return shape == other.shape && geometryBox == other.geometryBox; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +std::string ClipPath::toString() const { + std::stringstream ss; + + if (shape) { + std::visit([&](const auto& s) { s.toString(ss); }, *shape); + } + + if (geometryBox) { + if (shape) { + ss << " "; + } + ss << geometryBoxToString(*geometryBox); + } + + return ss.str(); +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic ClipPath::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + + if (shape) { + result["shape"] = + std::visit([](const auto& s) { return s.toDynamic(); }, *shape); + } + + if (geometryBox) { + result["geometryBox"] = geometryBoxToString(*geometryBox); + } + + return result; +} +#endif + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.h b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.h new file mode 100644 index 00000000000000..94895b9c96ba15 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.h @@ -0,0 +1,156 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef RN_SERIALIZABLE_STATE +#include +#endif + +namespace facebook::react { + +struct CircleShape { + std::optional r{}; + std::optional cx{}; + std::optional cy{}; + + bool operator==(const CircleShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct EllipseShape { + std::optional rx{}; + std::optional ry{}; + std::optional cx{}; + std::optional cy{}; + + bool operator==(const EllipseShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct InsetShape { + ValueUnit top{}; + ValueUnit right{}; + ValueUnit bottom{}; + ValueUnit left{}; + std::optional borderRadius{}; + + bool operator==(const InsetShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +enum class FillRule : uint8_t { + NonZero, + EvenOdd, +}; + +struct PolygonShape { + std::vector> points; + std::optional fillRule; + + bool operator==(const PolygonShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct RectShape { + ValueUnit top{}; + ValueUnit right{}; + ValueUnit bottom{}; + ValueUnit left{}; + std::optional borderRadius{}; + + bool operator==(const RectShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct XywhShape { + ValueUnit x{}; + ValueUnit y{}; + ValueUnit width{}; + ValueUnit height{}; + std::optional borderRadius{}; + + bool operator==(const XywhShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +using BasicShape = std::variant; + +enum class GeometryBox : uint8_t { + MarginBox, + BorderBox, + ContentBox, + PaddingBox, + FillBox, + StrokeBox, + ViewBox, +}; + +struct ClipPath { + std::optional shape; + std::optional geometryBox; + + bool operator==(const ClipPath &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + std::string toString() const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +} // namespace facebook::react