Skip to content

Commit

Permalink
Decouple clipping logic from border drawing (facebook#46191)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#46191

Borders should not have to deal with clipping logic, that is fairly independent.

Changelog: [Internal]

Differential Revision: D61418470
  • Loading branch information
joevilches authored and facebook-github-bot committed Aug 28, 2024
1 parent 32126c6 commit 853a7cf
Show file tree
Hide file tree
Showing 20 changed files with 217 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import <React/RCTBoxShadow.h>
#import <React/RCTConversions.h>
#import <React/RCTLocalizedString.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/view/ViewComponentDescriptor.h>
#import <react/renderer/components/view/ViewEventEmitter.h>
#import <react/renderer/components/view/ViewProps.h>
Expand Down Expand Up @@ -308,7 +309,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &

// `overflow`
if (oldViewProps.getClipsContentToBounds() != newViewProps.getClipsContentToBounds()) {
self.clipsToBounds = newViewProps.getClipsContentToBounds();
self.currentContainerView.clipsToBounds = newViewProps.getClipsContentToBounds();
needsInvalidateLayer = YES;
}

Expand Down Expand Up @@ -577,7 +578,6 @@ - (void)prepareForRecycle
_eventEmitter.reset();
_isJSResponder = NO;
_removeClippedSubviews = NO;
_useCustomContainerView = NO;
_reactSubviews = [NSMutableArray new];
}

Expand Down Expand Up @@ -605,7 +605,7 @@ - (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event

BOOL isPointInside = [self pointInside:point withEvent:event];

BOOL clipsToBounds = self.clipsToBounds;
BOOL clipsToBounds = self.currentContainerView.clipsToBounds;

clipsToBounds = clipsToBounds || _layoutMetrics.overflowInset == EdgeInsets{};

Expand Down Expand Up @@ -696,7 +696,8 @@ - (BOOL)styleWouldClipOverflowInk
{
const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
BOOL nonZeroBorderWidth = !(borderMetrics.borderWidths.isUniform() && borderMetrics.borderWidths.left == 0);
return _props->getClipsContentToBounds() && (!_props->boxShadow.empty() || nonZeroBorderWidth);
BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
return _props->getClipsContentToBounds() && (!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth));
}

// This UIView is the UIView that holds all subviews. It is sometimes not self
Expand All @@ -710,6 +711,10 @@ - (UIView *)currentContainerView
for (UIView *subview in self.subviews) {
[_containerView addSubview:subview];
}
_containerView.clipsToBounds = self.clipsToBounds;
self.clipsToBounds = NO;
_containerView.layer.mask = self.layer.mask;
self.layer.mask = nil;
[self addSubview:_containerView];
}

Expand All @@ -719,6 +724,8 @@ - (UIView *)currentContainerView
for (UIView *subview in _containerView.subviews) {
[self addSubview:subview];
}
self.clipsToBounds = _containerView.clipsToBounds;
self.layer.mask = _containerView.layer.mask;
[_containerView removeFromSuperview];
_containerView = nil;
}
Expand Down Expand Up @@ -788,7 +795,7 @@ - (void)invalidateLayer
// iOS draws borders in front of the content whereas CSS draws them behind
// the content. For this reason, only use iOS border drawing when clipping
// or when the border is hidden.
borderMetrics.borderWidths.left == 0 || self.clipsToBounds ||
borderMetrics.borderWidths.left == 0 || self.currentContainerView.clipsToBounds ||
(colorComponentsFromColor(borderMetrics.borderColors.left).alpha == 0 &&
(*borderMetrics.borderColors.left).getUIColor() != nullptr));

Expand All @@ -813,17 +820,25 @@ - (void)invalidateLayer
[self.layer addSublayer:_backgroundColorLayer];
}

CAShapeLayer *maskLayer = [self
createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
_backgroundColorLayer.backgroundColor = backgroundColor;
_backgroundColorLayer.mask = maskLayer;
if (borderMetrics.borderRadii.isUniform()) {
_backgroundColorLayer.mask = nil;
_backgroundColorLayer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
_backgroundColorLayer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
} else {
CAShapeLayer *maskLayer =
[self createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
_backgroundColorLayer.mask = maskLayer;
_backgroundColorLayer.cornerRadius = 0;
}
}

// Stage 2. Border Rendering
// borders
if (useCoreAnimationBorderRendering) {
layer.mask = nil;
[_borderLayer removeFromSuperlayer];
_borderLayer = nil;

layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left;
CGColorRef borderColor = RCTCreateCGColorRefFromSharedColor(borderMetrics.borderColors.left);
Expand Down Expand Up @@ -880,37 +895,9 @@ - (void)invalidateLayer
// If mutations are applied inside of Animation block, it may cause _borderLayer to be animated.
// To stop that, imperatively remove all animations from _borderLayer.
[_borderLayer removeAllAnimations];

// Stage 2.5. Custom Clipping Mask
CAShapeLayer *maskLayer = nil;
CGFloat cornerRadius = 0;
if (self.clipsToBounds) {
if (borderMetrics.borderRadii.isUniform()) {
// In this case we can simply use `cornerRadius` exclusively.
cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
} else {
RCTCornerInsets cornerInsets =
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
maskLayer = [self createMaskLayer:self.bounds cornerInsets:cornerInsets];
}
}

layer.cornerRadius = cornerRadius;
layer.mask = maskLayer;

for (UIView *subview in self.currentContainerView.subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
RCTCornerInsets cornerInsets = RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths));

// If the subview is an image view, we have to apply the mask directly to the image view's layer,
// otherwise the image might overflow with the border radius.
subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets];
}
}
}

// filter
[_filterLayer removeFromSuperlayer];
_filterLayer = nil;
self.layer.opacity = (float)_props->opacity;
Expand Down Expand Up @@ -947,6 +934,7 @@ - (void)invalidateLayer
[self.layer addSublayer:_filterLayer];
}

// background image
[self clearExistingGradientLayers];
if (!_props->backgroundImage.empty()) {
for (const auto &gradient : _props->backgroundImage) {
Expand Down Expand Up @@ -994,6 +982,7 @@ - (void)invalidateLayer
}
}

// box shadow
[_boxShadowLayer removeFromSuperlayer];
_boxShadowLayer = nil;
if (!_props->boxShadow.empty()) {
Expand All @@ -1010,6 +999,42 @@ - (void)invalidateLayer

_boxShadowLayer.contents = (id)boxShadowImage.CGImage;
}

// clipping
if (self.currentContainerView.clipsToBounds) {
BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
if (clipToPaddingBox) {
CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame())
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))];
self.currentContainerView.layer.mask = maskLayer;
} else {
if (borderMetrics.borderRadii.isUniform()) {
self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
} else {
CALayer *maskLayer =
[self createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
self.currentContainerView.layer.mask = maskLayer;
}

for (UIView *subview in self.currentContainerView.subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
RCTCornerInsets cornerInsets = RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths));

// If the subview is an image view, we have to apply the mask directly to the image view's layer,
// otherwise the image might overflow with the border radius.
subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets];
}
}
}
} else {
self.currentContainerView.layer.mask = nil;
}
}

- (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)cornerInsets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<b66656cc0c4c1556986bcb765b1a4717>>
* @generated SignedSource<<8fa471e86dbaac07b3bee229de063392>>
*/

/**
Expand Down Expand Up @@ -112,6 +112,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableGranularShadowTreeStateReconciliation(): Boolean = accessor.enableGranularShadowTreeStateReconciliation()

/**
* iOS Views will clip to their padding box vs border box
*/
@JvmStatic
public fun enableIOSViewClipToPaddingBox(): Boolean = accessor.enableIOSViewClipToPaddingBox()

/**
* When enabled, LayoutAnimations API will animate state changes on iOS.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ffc9e4785820aa65fad28f79d27a85d2>>
* @generated SignedSource<<9ed448a2c92f8494ac23b6d8e69e220d>>
*/

/**
Expand Down Expand Up @@ -34,6 +34,7 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
private var enableFabricLogsCache: Boolean? = null
private var enableFabricRendererExclusivelyCache: Boolean? = null
private var enableGranularShadowTreeStateReconciliationCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
private var enableLongTaskAPICache: Boolean? = null
private var enableMicrotasksCache: Boolean? = null
Expand Down Expand Up @@ -193,6 +194,15 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
return cached
}

override fun enableIOSViewClipToPaddingBox(): Boolean {
var cached = enableIOSViewClipToPaddingBoxCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableIOSViewClipToPaddingBox()
enableIOSViewClipToPaddingBoxCache = cached
}
return cached
}

override fun enableLayoutAnimationsOnIOS(): Boolean {
var cached = enableLayoutAnimationsOnIOSCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<580db8acae42a65aeb678d2d6ef4630c>>
* @generated SignedSource<<eb4afd665292a96aa9f2ea754ce64e35>>
*/

/**
Expand Down Expand Up @@ -56,6 +56,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableGranularShadowTreeStateReconciliation(): Boolean

@DoNotStrip @JvmStatic public external fun enableIOSViewClipToPaddingBox(): Boolean

@DoNotStrip @JvmStatic public external fun enableLayoutAnimationsOnIOS(): Boolean

@DoNotStrip @JvmStatic public external fun enableLongTaskAPI(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<00aae631e9d9b146fae9be838e04e165>>
* @generated SignedSource<<22f05fff5c867324d44729c5bf02dddb>>
*/

/**
Expand Down Expand Up @@ -51,6 +51,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableGranularShadowTreeStateReconciliation(): Boolean = false

override fun enableIOSViewClipToPaddingBox(): Boolean = false

override fun enableLayoutAnimationsOnIOS(): Boolean = true

override fun enableLongTaskAPI(): Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ead344838f30d7e3f1205d7bbabbc803>>
* @generated SignedSource<<cf4fd4fb9656bebf07d7c9bbdc5df5a1>>
*/

/**
Expand Down Expand Up @@ -38,6 +38,7 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
private var enableFabricLogsCache: Boolean? = null
private var enableFabricRendererExclusivelyCache: Boolean? = null
private var enableGranularShadowTreeStateReconciliationCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
private var enableLongTaskAPICache: Boolean? = null
private var enableMicrotasksCache: Boolean? = null
Expand Down Expand Up @@ -211,6 +212,16 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableIOSViewClipToPaddingBox(): Boolean {
var cached = enableIOSViewClipToPaddingBoxCache
if (cached == null) {
cached = currentProvider.enableIOSViewClipToPaddingBox()
accessedFeatureFlags.add("enableIOSViewClipToPaddingBox")
enableIOSViewClipToPaddingBoxCache = cached
}
return cached
}

override fun enableLayoutAnimationsOnIOS(): Boolean {
var cached = enableLayoutAnimationsOnIOSCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<2c8c28515aa6929b975ef7193c8bf72e>>
* @generated SignedSource<<46e8ddef352182c1c6a9669caa7d5111>>
*/

/**
Expand Down Expand Up @@ -51,6 +51,8 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableGranularShadowTreeStateReconciliation(): Boolean

@DoNotStrip public fun enableIOSViewClipToPaddingBox(): Boolean

@DoNotStrip public fun enableLayoutAnimationsOnIOS(): Boolean

@DoNotStrip public fun enableLongTaskAPI(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<35f5003b77104e68e32741e4626e2ba6>>
* @generated SignedSource<<07d3bcf171990d2976f611a82d8fcd32>>
*/

/**
Expand Down Expand Up @@ -123,6 +123,12 @@ class ReactNativeFeatureFlagsProviderHolder
return method(javaProvider_);
}

bool enableIOSViewClipToPaddingBox() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableIOSViewClipToPaddingBox");
return method(javaProvider_);
}

bool enableLayoutAnimationsOnIOS() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableLayoutAnimationsOnIOS");
Expand Down Expand Up @@ -389,6 +395,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableGranularShadowTreeStateReconcilia
return ReactNativeFeatureFlags::enableGranularShadowTreeStateReconciliation();
}

bool JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
}

bool JReactNativeFeatureFlagsCxxInterop::enableLayoutAnimationsOnIOS(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableLayoutAnimationsOnIOS();
Expand Down Expand Up @@ -608,6 +619,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enableGranularShadowTreeStateReconciliation",
JReactNativeFeatureFlagsCxxInterop::enableGranularShadowTreeStateReconciliation),
makeNativeMethod(
"enableIOSViewClipToPaddingBox",
JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox),
makeNativeMethod(
"enableLayoutAnimationsOnIOS",
JReactNativeFeatureFlagsCxxInterop::enableLayoutAnimationsOnIOS),
Expand Down
Loading

0 comments on commit 853a7cf

Please sign in to comment.