Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import <React/RCTBackgroundImageUtils.h>
#import <React/RCTBorderDrawing.h>
#import <React/RCTBoxShadow.h>
#import <React/RCTClipPathUtils.h>
#import <React/RCTConversions.h>
#import <React/RCTLinearGradient.h>
#import <React/RCTLocalizedString.h>
Expand Down Expand Up @@ -676,6 +677,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<const ViewProps>(props);
Expand Down Expand Up @@ -1464,7 +1470,28 @@ - (void)invalidateLayer

// clipping
self.currentContainerView.layer.mask = nil;
if (self.currentContainerView.clipsToBounds) {

// Handle clip-path property
if (_props->clipPath != nullptr) {
if (auto yogaStylableProps = std::static_pointer_cast<const YogaStylableProps>(_props)) {
CALayer *maskLayer =
[RCTClipPathUtils createClipPathLayer:*_props->clipPath
layoutMetrics:_layoutMetrics
yogaStylableProps:*yogaStylableProps.get()
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 (areBorderRadiiCircular(borderMetrics.borderRadii)) {
Expand Down
28 changes: 28 additions & 0 deletions packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <react/renderer/graphics/ClipPath.h>

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
164 changes: 164 additions & 0 deletions packages/react-native/React/Fabric/Utils/RCTBasicShapeUtils.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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 <React/RCTConversions.h>

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<CircleShape>(basicShape)) {
return [self createCirclePath:std::get<CircleShape>(basicShape) bounds:bounds];
} else if (std::holds_alternative<EllipseShape>(basicShape)) {
return [self createEllipsePath:std::get<EllipseShape>(basicShape) bounds:bounds];
} else if (std::holds_alternative<InsetShape>(basicShape)) {
return [self createInsetPath:std::get<InsetShape>(basicShape) bounds:bounds];
} else if (std::holds_alternative<PolygonShape>(basicShape)) {
return [self createPolygonPath:std::get<PolygonShape>(basicShape) bounds:bounds];
} else if (std::holds_alternative<RectShape>(basicShape)) {
return [self createRectPath:std::get<RectShape>(basicShape) bounds:bounds];
} else if (std::holds_alternative<XywhShape>(basicShape)) {
return [self createXywhPath:std::get<XywhShape>(basicShape) bounds:bounds];
}

return nil;
}

@end
39 changes: 39 additions & 0 deletions packages/react-native/React/Fabric/Utils/RCTClipPathUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
#import <React/RCTBorderDrawing.h>
#import <react/renderer/components/view/YogaStylableProps.h>
#import <react/renderer/components/view/primitives.h>
#import <react/renderer/core/LayoutMetrics.h>
#import <react/renderer/graphics/ClipPath.h>
#import <yoga/style/Style.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTClipPathUtils : NSObject

+ (RCTCornerRadii)adjustCornerRadiiForGeometryBox:(facebook::react::GeometryBox)geometryBox
cornerRadii:(RCTCornerRadii)cornerRadii
layoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
yogaStylableProps:(const facebook::react::YogaStylableProps &)yogaStylableProps;

+ (CGRect)getGeometryBoxRect:(facebook::react::GeometryBox)geometryBox
layoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
yogaStylableProps:(const facebook::react::YogaStylableProps &)yogaStylableProps
bounds:(CGRect)bounds;

+ (CALayer *_Nullable)createClipPathLayer:(const facebook::react::ClipPath &)clipPath
layoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
yogaStylableProps:(const facebook::react::YogaStylableProps &)yogaStylableProps
bounds:(CGRect)bounds
cornerRadii:(RCTCornerRadii)cornerRadii;

@end

NS_ASSUME_NONNULL_END
Loading
Loading