Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ GEM
specs:
CFPropertyList (3.0.6)
rexml
activesupport (7.0.8)
activesupport (6.1.7.10)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
Expand All @@ -17,12 +18,12 @@ GEM
benchmark (0.4.0)
bigdecimal (3.1.9)
claide (1.1.0)
cocoapods (1.14.2)
cocoapods (1.15.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.14.2)
cocoapods-core (= 1.15.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
Expand All @@ -35,7 +36,7 @@ GEM
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.14.2)
cocoapods-core (1.15.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
Expand All @@ -46,7 +47,7 @@ GEM
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.0)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
Expand Down Expand Up @@ -81,13 +82,14 @@ GEM
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
xcodeproj (1.23.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
zeitwerk (2.6.18)

PLATFORMS
ruby
Expand All @@ -106,4 +108,4 @@ RUBY VERSION
ruby 3.2.0p0

BUNDLED WITH
2.4.12
2.4.6
19 changes: 19 additions & 0 deletions packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ export interface TVViewPropsIOS {
}

export interface ViewPropsIOS extends TVViewPropsIOS {
/**
* Controls how iOS determines the shape used by the legacy `shadow*`
* properties. This does not affect `boxShadow`.
*
* - `auto`: Uses an optimized border-box path when the view has an opaque
* background. Otherwise derives the shadow from the composited content alpha.
* - `border-box`: Always uses the view's bounds and resolved border radii.
* This avoids content-alpha offscreen rendering, but callers must guarantee
* that the intended shadow silhouette matches the view's border box.
* - `content-alpha`: Always derives the shadow from the rendered alpha of the
* view and its children. This supports irregular silhouettes but may require
* offscreen rendering.
*
* When omitted, `auto` is used.
*
* @platform ios
*/
shadowPathIOS?: 'auto' | 'border-box' | 'content-alpha' | undefined;

/**
* Whether this view should be rendered as a bitmap before compositing.
*
Expand Down
19 changes: 19 additions & 0 deletions packages/react-native/Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,25 @@ export type TVViewPropsIOS = $ReadOnly<{
}>;

export type ViewPropsIOS = $ReadOnly<{
/**
* Controls how iOS determines the shape used by the legacy `shadow*`
* properties. This does not affect `boxShadow`.
*
* - `auto`: Uses an optimized border-box path when the view has an opaque
* background. Otherwise derives the shadow from the composited content alpha.
* - `border-box`: Always uses the view's bounds and resolved border radii.
* This avoids content-alpha offscreen rendering, but callers must guarantee
* that the intended shadow silhouette matches the view's border box.
* - `content-alpha`: Always derives the shadow from the rendered alpha of the
* view and its children. This supports irregular silhouettes but may require
* offscreen rendering.
*
* When omitted, `auto` is used.
*
* @platform ios
*/
shadowPathIOS?: ?('auto' | 'border-box' | 'content-alpha'),

/**
* Whether this `View` should be rendered as a bitmap before compositing.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ const validAttributesForNonEventProps = {
shadowColor: {process: require('../StyleSheet/processColor').default},
shadowOffset: {diff: require('../Utilities/differ/sizesDiffer').default},
shadowOpacity: true,
shadowPathIOS: true,
shadowRadius: true,
needsOffscreenAlphaCompositing: true,
overflow: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#import <React/RCTLinearGradient.h>
#import <React/RCTLocalizedString.h>
#import <React/RCTRadialGradient.h>
#import <react/debug/react_native_assert.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/view/ViewComponentDescriptor.h>
#import <react/renderer/components/view/ViewEventEmitter.h>
Expand Down Expand Up @@ -272,6 +273,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
needsInvalidateLayer = YES;
}

// `shadowPathIOS`
if (oldViewProps.shadowPathIOS != newViewProps.shadowPathIOS) {
needsInvalidateLayer = YES;
}

// `shadowRadius`
if (oldViewProps.shadowRadius != newViewProps.shadowRadius) {
self.layer.shadowRadius = (CGFloat)newViewProps.shadowRadius;
Expand Down Expand Up @@ -829,20 +835,36 @@ - (void)invalidateLayer

// Stage 1. Shadow Path
BOOL const layerHasShadow = layer.shadowOpacity > 0 && CGColorGetAlpha(layer.shadowColor) > 0;
if (layerHasShadow) {
if (CGColorGetAlpha(_backgroundColor.CGColor) > 0.999) {
// If view has a solid background color, calculate shadow path from border.
const RCTCornerInsets cornerInsets =
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil, NO);
layer.shadowPath = shadowPath;
CGPathRelease(shadowPath);
} else {
// Can't accurately calculate box shadow, so fall back to pixel-based shadow.
layer.shadowPath = nil;
}
} else {
if (!layerHasShadow) {
layer.shadowPath = nil;
} else {
ShadowPathMode shadowPathMode = resolveShadowPathMode(_props->shadowPathIOS);
RCTCornerRadii cornerRadii = RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii);
const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero);

switch (shadowPathMode) {
case ShadowPathMode::BorderBox: {
CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil, NO);
layer.shadowPath = shadowPath;
CGPathRelease(shadowPath);
break;
}
case ShadowPathMode::ContentAlpha:
layer.shadowPath = nil;
break;
case ShadowPathMode::Auto:
if (CGColorGetAlpha(_backgroundColor.CGColor) > 0.999) {
CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil, NO);
layer.shadowPath = shadowPath;
CGPathRelease(shadowPath);
} else {
layer.shadowPath = nil;
}
break;
case ShadowPathMode::Default:
react_native_assert(false);
break;
}
}

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 /* __IPHONE_17_0 */
Expand Down
7 changes: 7 additions & 0 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,13 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie
// filtered by view configs.
}

RCT_CUSTOM_VIEW_PROPERTY(shadowPathIOS, NSString *, RCTView)
{
// Property is only to be used in the new renderer.
// It is necessary to add it here, otherwise it gets
// filtered by view configs.
}

RCT_CUSTOM_VIEW_PROPERTY(mixBlendMode, NSString *, RCTView)
{
// Property is only to be used in the new renderer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ BaseViewProps::BaseViewProps(
"shadowOpacity",
sourceProps.shadowOpacity,
{})),
shadowPathIOS(
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
? sourceProps.shadowPathIOS
: convertRawProp(
context,
rawProps,
"shadowPathIOS",
sourceProps.shadowPathIOS,
ShadowPathMode::Default)),
shadowRadius(
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
? sourceProps.shadowRadius
Expand Down Expand Up @@ -379,6 +388,7 @@ void BaseViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowColor);
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOffset);
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOpacity);
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowPathIOS);
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowRadius);
RAW_SET_PROP_SWITCH_CASE_BASIC(transform);
RAW_SET_PROP_SWITCH_CASE_BASIC(backfaceVisibility);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
SharedColor shadowColor{};
Size shadowOffset{0, -3};
Float shadowOpacity{};
ShadowPathMode shadowPathIOS{ShadowPathMode::Default};
Float shadowRadius{3};

Cursor cursor{};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ void ViewShadowNode::initialize() noexcept {
isColorMeaningful(viewProps.backgroundColor) || hasBorder() ||
!viewProps.testId.empty() || !viewProps.boxShadow.empty() ||
!viewProps.backgroundImage.empty() ||
viewProps.shadowPathIOS != ShadowPathMode::Default ||
HostPlatformViewTraitsInitializer::formsView(viewProps) ||
viewProps.outlineWidth > 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,34 @@ inline void fromRawValue(
react_native_expect(false);
}

inline void fromRawValue(
const PropsParserContext& context,
const RawValue& value,
ShadowPathMode& result) {
result = ShadowPathMode::Default;
react_native_expect(value.hasType<std::string>());
if (!value.hasType<std::string>()) {
return;
}

auto stringValue = (std::string)value;
if (stringValue == "auto") {
result = ShadowPathMode::Auto;
return;
}
if (stringValue == "border-box") {
result = ShadowPathMode::BorderBox;
return;
}
if (stringValue == "content-alpha") {
result = ShadowPathMode::ContentAlpha;
return;
}

LOG(ERROR) << "Could not parse ShadowPathMode:" << stringValue;
react_native_expect(false);
}

inline void fromRawValue(
const PropsParserContext& context,
const RawValue& value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ enum class BorderStyle : uint8_t { Solid, Dotted, Dashed };

enum class OutlineStyle : uint8_t { Solid, Dotted, Dashed };

enum class ShadowPathMode : uint8_t {
Default,
Auto,
BorderBox,
ContentAlpha,
};

inline ShadowPathMode resolveShadowPathMode(ShadowPathMode propMode) {
if (propMode == ShadowPathMode::Default) {
return ShadowPathMode::Auto;
}

return propMode;
}

struct CornerRadii {
float vertical{0.0f};
float horizontal{0.0f};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,38 @@

#include <react/renderer/components/view/BoxShadowPropsConversions.h>
#include <react/renderer/components/view/FilterPropsConversions.h>
#include <react/renderer/components/view/conversions.h>

namespace facebook::react {

TEST(ConversionsTest, shadow_path_modes) {
PropsParserContext context{-1, ContextContainer{}};
ShadowPathMode result = ShadowPathMode::Default;
RawValue autoValue{folly::dynamic("auto")};
RawValue borderBoxValue{folly::dynamic("border-box")};
RawValue contentAlphaValue{folly::dynamic("content-alpha")};

fromRawValue(context, autoValue, result);
EXPECT_EQ(result, ShadowPathMode::Auto);

fromRawValue(context, borderBoxValue, result);
EXPECT_EQ(result, ShadowPathMode::BorderBox);

fromRawValue(context, contentAlphaValue, result);
EXPECT_EQ(result, ShadowPathMode::ContentAlpha);
}

TEST(ConversionsTest, resolves_default_shadow_path_mode) {
EXPECT_EQ(
resolveShadowPathMode(ShadowPathMode::Default),
ShadowPathMode::Auto);
EXPECT_EQ(
resolveShadowPathMode(ShadowPathMode::Auto), ShadowPathMode::Auto);
EXPECT_EQ(
resolveShadowPathMode(ShadowPathMode::ContentAlpha),
ShadowPathMode::ContentAlpha);
}

TEST(ConversionsTest, unprocessed_box_shadow_string) {
RawValue value{
folly::dynamic("10px 2px 0 5px #fff, inset 20px 10px 5px 0 #000")};
Expand Down
1 change: 1 addition & 0 deletions packages/rn-tester/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'
gem 'nkf'
4 changes: 2 additions & 2 deletions packages/rn-tester/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2752,7 +2752,7 @@ SPEC CHECKSUMS:
React-timing: 1e6a8acb66e2b7ac9d418956617fd1fdb19322fd
React-utils: 52bbb03f130319ef82e4c3bc7a85eaacdb1fec87
ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3
ReactCodegen: cb2a3dea7ad220568d9453e17d275dbd9f9bc044
ReactCodegen: f9a16823d5e01de57cc449c9451d6ddc2a424f64
ReactCommon: 394c6b92765cf6d211c2c3f7f6bc601dffb316a6
ReactCommon-Samples: d083c8092a5ad0df33b1ca589cb2d909fbd6914e
ScreenshotManager: cd9e43b0c749ba38101effc22be58f4a69440d5f
Expand All @@ -2761,4 +2761,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: b0abc972ac4c3a335250548a31500196b1a2c163

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';

import Playground from './RNTesterPlayground';
import ShadowPathPlayground from './RNTesterShadowPathPlayground';

export const title = Playground.title;
export const framework = 'React';
export const description = 'Test out new features and ideas.';
export const examples: Array<RNTesterModuleExample> = [Playground];
export const examples: Array<RNTesterModuleExample> = [
Playground,
ShadowPathPlayground,
];
Loading
Loading