From aa84a5277614e8a76f70bca2b461f58c6f20d418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Mon, 29 Dec 2025 18:07:22 +0530 Subject: [PATCH 1/5] url() function support in background image parser lint fix add local image example --- .../Libraries/StyleSheet/StyleSheetTypes.js | 10 +- .../__tests__/processBackgroundImage-itest.js | 108 ++++++++++++++++++ .../StyleSheet/processBackgroundImage.js | 57 ++++++++- .../view/BackgroundImagePropsConversions.cpp | 23 ++++ .../react/renderer/css/CSSBackgroundImage.h | 46 +++++++- .../css/tests/CSSBackgroundImageTest.cpp | 82 +++++++++++++ .../react/renderer/graphics/BackgroundImage.h | 26 ++++- .../BackgroundImage/BackgroundImageExample.js | 40 +++++++ 8 files changed, 384 insertions(+), 8 deletions(-) diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 5a1951a1e2c6..b9b1b6b921b9 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -769,7 +769,15 @@ type RadialGradientValue = { }>, }; -export type BackgroundImageValue = LinearGradientValue | RadialGradientValue; +type URLBackgroundImageValue = { + type: 'url', + uri: string | number, +}; + +export type BackgroundImageValue = + | LinearGradientValue + | RadialGradientValue + | URLBackgroundImageValue; export type BackgroundSizeValue = { x: string | number, diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js index 5dc05646b27e..019f11826ef5 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js @@ -363,6 +363,10 @@ describe('processBackgroundImage', () => { {color: 'blue', positions: ['100%']}, ], }, + { + type: 'url', + uri: 'https://example.com', + }, ]; const result = processBackgroundImage(input); expect(result).toEqual([ @@ -374,6 +378,10 @@ describe('processBackgroundImage', () => { {color: processColor('blue'), position: '100%'}, ], }, + { + type: 'url', + uri: 'https://example.com', + }, ]); }); @@ -1188,4 +1196,104 @@ describe('processBackgroundImage', () => { expect(result1).toEqual([]); expect(result2).toEqual([]); }); + + it('should parse url unquoted', () => { + const result = processBackgroundImage('url(https://example.com/image.png)'); + expect(result).toEqual([ + {type: 'url', uri: 'https://example.com/image.png'}, + ]); + }); + + it('should parse url double quoted', () => { + const result = processBackgroundImage( + 'url("https://example.com/image.png")', + ); + expect(result).toEqual([ + {type: 'url', uri: 'https://example.com/image.png'}, + ]); + }); + + it('should parse url single quoted', () => { + const result = processBackgroundImage( + "url('https://example.com/image.png')", + ); + expect(result).toEqual([ + {type: 'url', uri: 'https://example.com/image.png'}, + ]); + }); + + it('should parse url case insensitive', () => { + const result = processBackgroundImage('UrL(https://example.com/image.png)'); + expect(result).toEqual([ + {type: 'url', uri: 'https://example.com/image.png'}, + ]); + }); + + it('should parse url with query params', () => { + const result = processBackgroundImage( + 'url(https://example.com/image.png?size=Large&format=webp)', + ); + expect(result).toEqual([ + { + type: 'url', + uri: 'https://example.com/image.png?size=Large&format=webp', + }, + ]); + }); + + it('should parse url with whitespace', () => { + const result = processBackgroundImage( + 'url( https://example.com/image.png )', + ); + expect(result).toEqual([ + {type: 'url', uri: 'https://example.com/image.png'}, + ]); + }); + + it('should parse multiple urls', () => { + const result = processBackgroundImage( + 'url(https://example.com/bg1.png), url(https://example.com/bg2.png)', + ); + expect(result).toEqual([ + {type: 'url', uri: 'https://example.com/bg1.png'}, + {type: 'url', uri: 'https://example.com/bg2.png'}, + ]); + }); + + it('should parse url mixed with gradients', () => { + const result = processBackgroundImage( + 'radial-gradient(circle at top left, red, blue), url(https://example.com/image.png), linear-gradient(to bottom, green, yellow)', + ); + expect(result).toEqual([ + { + type: 'radial-gradient', + shape: 'circle', + size: 'farthest-corner', + position: {top: '0%', left: '0%'}, + colorStops: [ + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, + ], + }, + {type: 'url', uri: 'https://example.com/image.png'}, + { + type: 'linear-gradient', + direction: {type: 'angle', value: 180}, + colorStops: [ + {color: processColor('green'), position: null}, + {color: processColor('yellow'), position: null}, + ], + }, + ]); + }); + + it('should return empty for url empty', () => { + const result = processBackgroundImage('url()'); + expect(result).toEqual([]); + }); + + it('should return empty for url empty quoted', () => { + const result = processBackgroundImage('url("")'); + expect(result).toEqual([]); + }); }); diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index 23c0b20cc525..c67fae3e50aa 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -18,6 +18,7 @@ import type { RadialGradientSize, } from './StyleSheetTypes'; +const resolveAssetSource = require('../Image/resolveAssetSource').default; const processColor = require('./processColor').default; // Pre-compiled regex patterns for performance - avoids regex compilation on each call @@ -70,13 +71,20 @@ type RadialGradientBackgroundImage = { }>, }; +type URLBackgroundImage = { + type: 'url', + uri: string, +}; + // null color indicate that the transition hint syntax is used. e.g. red, 20%, blue type ColorStopColor = ProcessedColorValue | null; // percentage or pixel value type ColorStopPosition = number | string | null; type ParsedBackgroundImageValue = - LinearGradientBackgroundImage | RadialGradientBackgroundImage; + | LinearGradientBackgroundImage + | RadialGradientBackgroundImage + | URLBackgroundImage; export default function processBackgroundImage( backgroundImage: ?(ReadonlyArray | string), @@ -92,6 +100,28 @@ export default function processBackgroundImage( ); } else if (Array.isArray(backgroundImage)) { for (const bgImage of backgroundImage) { + if (bgImage.type === 'url') { + let uri: ?string = null; + if (typeof bgImage.uri === 'string') { + uri = bgImage.uri; + } else if (typeof bgImage.uri === 'number') { + const source = resolveAssetSource(bgImage.uri); + if (source != null && source.uri != null) { + uri = source.uri; + } + } + if (uri != null) { + result = result.concat({ + type: 'url', + uri, + }); + continue; + } else { + // If the URI is invalid, return an empty array. Same as web. + return []; + } + } + const processedColorStops = processColorStops(bgImage); if (processedColorStops == null) { // If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. @@ -257,11 +287,28 @@ function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{ function parseBackgroundImageCSSString( cssString: string, -): ReadonlyArray { - const gradients = []; +): ReadOnlyArray { + const backgroundImages = []; const bgImageStrings = splitGradients(cssString); for (const bgImageString of bgImageStrings) { + const urlRegex = /^url\((.*)\)$/i; + const urlMatch = urlRegex.exec(bgImageString); + if (urlMatch) { + let uri = urlMatch[1].trim(); + const first = uri[0]; + if ((first === '"' || first === "'") && uri.endsWith(first)) { + uri = uri.slice(1, -1); + } + if (uri.length > 0) { + backgroundImages.push({ + type: 'url', + uri, + }); + } + continue; + } + const bgImage = bgImageString.toLowerCase(); const match = GRADIENT_REGEX.exec(bgImage); if (match) { @@ -272,11 +319,11 @@ function parseBackgroundImageCSSString( : parseLinearGradientCSSString(gradientContent); if (gradient != null) { - gradients.push(gradient); + backgroundImages.push(gradient); } } } - return gradients; + return backgroundImages; } function parseRadialGradientCSSString( diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp index 7759acebe2a6..daaddf19c6a9 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp @@ -208,6 +208,14 @@ void parseProcessedBackgroundImage( } backgroundImage.emplace_back(std::move(radialGradient)); + } else if (type == "url") { + auto uriIt = rawBackgroundImageMap.find("uri"); + if (uriIt != rawBackgroundImageMap.end() && + uriIt->second.hasType()) { + URLBackgroundImage urlBackgroundImage; + urlBackgroundImage.uri = (std::string)(uriIt->second); + backgroundImage.emplace_back(std::move(urlBackgroundImage)); + } } } @@ -409,6 +417,14 @@ void parseUnprocessedBackgroundImageList( } backgroundImage.emplace_back(std::move(radialGradient)); + } else if (type == "url") { + auto uriIt = rawBackgroundImageMap.find("uri"); + if (uriIt != rawBackgroundImageMap.end() && + uriIt->second.hasType()) { + URLBackgroundImage urlBackgroundImage; + urlBackgroundImage.uri = (std::string)(uriIt->second); + backgroundImage.emplace_back(std::move(urlBackgroundImage)); + } } } @@ -472,6 +488,13 @@ void fromCSSColorStop( std::optional fromCSSBackgroundImage( const CSSBackgroundImage& cssBackgroundImage) { + if (std::holds_alternative(cssBackgroundImage)) { + const auto& urlFunc = std::get(cssBackgroundImage); + URLBackgroundImage urlBackgroundImage; + urlBackgroundImage.uri = urlFunc.url; + return BackgroundImage{urlBackgroundImage}; + } + if (std::holds_alternative(cssBackgroundImage)) { const auto& gradient = std::get(cssBackgroundImage); diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h b/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h index 75daa6853d85..a93b10830522 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h @@ -822,11 +822,55 @@ struct CSSDataTypeParser { static_assert(CSSDataType); +struct CSSURLFunction { + std::string url{}; + + bool operator==(const CSSURLFunction &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) + -> std::optional + { + if (!iequals(func.name, "url")) { + return {}; + } + + parser.syntaxParser().consumeWhitespace(); + std::string url; + while (auto token = parser.syntaxParser().consumeComponentValue>( + [](const CSSPreservedToken &t) -> std::optional { + return std::string(t.stringValue()); + })) { + if (!token->empty()) { + url += *token; + } + } + + if (url.size() >= 2) { + char first = url.front(); + char last = url.back(); + if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) { + url = url.substr(1, url.size() - 2); + } + } + + if (!url.empty()) { + return CSSURLFunction{url}; + } + + return {}; + } +}; + +static_assert(CSSDataType); + /** * Representation of * https://www.w3.org/TR/css-backgrounds-3/#background-image */ -using CSSBackgroundImage = CSSCompoundDataType; +using CSSBackgroundImage = CSSCompoundDataType; /** * Representation of diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp index 9851cc87277f..f5e28b738db8 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp @@ -557,4 +557,86 @@ TEST_F(CSSBackgroundImageTest, RadialGradientNegativeRadius) { } } +TEST_F(CSSBackgroundImageTest, URLBasicUnquoted) { + auto result = + parseCSSProperty("url(https://example.com/image.png)"); + decltype(result) expected = CSSURLFunction{.url = "https://example.com/image.png"}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, URLDoubleQuoted) { + auto result = + parseCSSProperty("url(\"https://example.com/image.png\")"); + decltype(result) expected = CSSURLFunction{.url = "https://example.com/image.png"}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, URLSingleQuoted) { + auto result = + parseCSSProperty("url('https://example.com/image.png')"); + decltype(result) expected = CSSURLFunction{.url = "https://example.com/image.png"}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, URLCaseInsensitive) { + auto result = + parseCSSProperty("UrL(https://example.com/image.png)"); + decltype(result) expected = CSSURLFunction{.url = "https://example.com/image.png"}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, URLWithQueryParams) { + auto result = + parseCSSProperty("url(https://example.com/image.png?size=Large&format=webp)"); + decltype(result) expected = CSSURLFunction{.url = "https://example.com/image.png?size=Large&format=webp"}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, URLWithWhitespace) { + auto result = + parseCSSProperty("url( https://example.com/image.png )"); + decltype(result) expected = CSSURLFunction{.url = "https://example.com/image.png"}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, MultipleURLs) { + auto result = parseCSSProperty( + "url(https://example.com/bg1.png), url(https://example.com/bg2.png)"); + decltype(result) expected = CSSBackgroundImageList{ + {CSSURLFunction{.url = "https://example.com/bg1.png"}, + CSSURLFunction{.url = "https://example.com/bg2.png"}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, URLMixedWithGradients) { + auto result = parseCSSProperty( + "radial-gradient(circle at top left, red, blue), url(https://example.com/image.png), linear-gradient(to bottom, green, yellow)"); + decltype(result) expected = CSSBackgroundImageList{ + {CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 0.0f}, + .left = CSSPercentage{.value = 0.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}, + CSSURLFunction{.url = "https://example.com/image.png"}, + CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + makeCSSColorStop(0, 128, 0), makeCSSColorStop(255, 255, 0)}}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, URLEmpty) { + auto result = parseCSSProperty("url()"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSBackgroundImageTest, URLEmptyQuoted) { + auto result = parseCSSProperty("url(\"\")"); + ASSERT_TRUE(std::holds_alternative(result)); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h index d156126f251c..487e58c7ea01 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h @@ -7,12 +7,34 @@ #pragma once +#include + #include #include namespace facebook::react { -using BackgroundImage = std::variant; +class ImageSource; + +struct URLBackgroundImage { + std::string uri{}; + + bool operator==(const URLBackgroundImage& rhs) const { + return uri == rhs.uri; + } + + bool operator!=(const URLBackgroundImage& rhs) const { + return !(*this == rhs); + } + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream& ss) const { + ss << "url(" << uri << ")"; + } +#endif +}; + +using BackgroundImage = std::variant; #ifdef RN_SERIALIZABLE_STATE folly::dynamic toDynamic(const BackgroundImage &backgroundImage); @@ -34,6 +56,8 @@ inline std::string toString(std::vector &value) std::get(backgroundImage).toString(ss); } else if (std::holds_alternative(backgroundImage)) { std::get(backgroundImage).toString(ss); + } else if (std::holds_alternative(backgroundImage)) { + std::get(backgroundImage).toString(ss); } } ss << "]"; diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js index e1ca7d92ef48..e4dbf6f66da1 100644 --- a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -440,4 +440,44 @@ exports.examples = [ ); }, }, + { + title: 'URL Image', + name: 'url-image', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'URL Image from local file', + name: 'local-image', + render(): React.Node { + return ( + + ); + }, + }, ] as Array; From 98a098127ec85cc1e88616b15fd815f8369951db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Mon, 29 Dec 2025 19:17:59 +0530 Subject: [PATCH 2/5] url() support in background image on ios --- packages/react-native/Package.swift | 1 + .../View/RCTBackgroundImageURLLoader.h | 30 ++++ .../View/RCTBackgroundImageURLLoader.mm | 148 ++++++++++++++++++ .../View/RCTViewComponentView.h | 4 +- .../View/RCTViewComponentView.mm | 62 ++++++-- .../ReactCommon/React-Fabric.podspec | 3 + .../view/ViewComponentDescriptor.cpp | 28 ++++ .../components/view/ViewComponentDescriptor.h | 15 +- .../components/view/ViewShadowNode.cpp | 67 ++++++++ .../renderer/components/view/ViewShadowNode.h | 10 +- .../renderer/components/view/ViewState.cpp | 17 ++ .../renderer/components/view/ViewState.h | 50 ++++++ 12 files changed, 417 insertions(+), 18 deletions(-) create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index 25a126e0cce7..fd6f41ca1213 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -455,6 +455,7 @@ let reactCore = RNTarget( let reactFabric = RNTarget( name: .reactFabric, path: "ReactCommon/react/renderer", + searchPaths: ["ReactCommon/react/renderer/imagemanager/platform/ios"], excludedPaths: [ "animated/tests", "animations/tests", diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h new file mode 100644 index 000000000000..ec99ac1b5cd3 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h @@ -0,0 +1,30 @@ +/* + * 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 + +@protocol RCTBackgroundImageURLLoaderDelegate + +- (void)backgroundImagesDidLoad; + +@end + +@interface RCTBackgroundImageURLLoader : NSObject + +@property (nonatomic, weak) id delegate; + +- (void)updateStateWithNewState:(facebook::react::ViewShadowNode::ConcreteState::Shared)state oldState:(facebook::react::ViewShadowNode::ConcreteState::Shared)oldState; +- (nullable UIImage *)loadedImageForUri:(NSString *)uri; +- (void)reset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm new file mode 100644 index 000000000000..4fac0c728278 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm @@ -0,0 +1,148 @@ +/* + * 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 "RCTBackgroundImageURLLoader.h" + +#import +#import +#import + +#include +#include +#include + +using namespace facebook::react; + +@implementation RCTBackgroundImageURLLoader { + ViewShadowNode::ConcreteState::Shared _state; + std::map> _uriToObserver; + NSMutableDictionary *_loadedImages; + NSMutableSet *_completedUris; +} + +- (instancetype)init +{ + if (self = [super init]) { + _loadedImages = [NSMutableDictionary new]; + _completedUris = [NSMutableSet new]; + } + return self; +} + +- (void)updateStateWithNewState:(ViewShadowNode::ConcreteState::Shared)state + oldState:(ViewShadowNode::ConcreteState::Shared)oldState +{ + const auto* oldRequests = oldState ? &oldState->getData().getBackgroundImageRequests() : nullptr; + const auto* newRequests = state ? &state->getData().getBackgroundImageRequests() : nullptr; + + if (oldRequests && newRequests && *oldRequests == *newRequests) { + return; + } + + if (oldRequests) { + for (const auto& request : *oldRequests) { + if (request.imageRequest) { + auto it = _uriToObserver.find(request.imageSource.uri); + if (it != _uriToObserver.end()) { + auto& observerCoordinator = request.imageRequest->getObserverCoordinator(); + observerCoordinator.removeObserver(it->second); + } + } + } + } + + _state = state; + _uriToObserver.clear(); + [_loadedImages removeAllObjects]; + [_completedUris removeAllObjects]; + + if (newRequests) { + for (const auto &request : *newRequests) { + if (request.imageRequest) { + const std::string &uri = request.imageSource.uri; + auto [it, inserted] = _uriToObserver.emplace(uri, std::make_shared(self)); + if (inserted) { + auto& observerCoordinator = request.imageRequest->getObserverCoordinator(); + observerCoordinator.addObserver(it->second); + } + } + } + } +} + +- (UIImage *)loadedImageForUri:(NSString *)uri +{ + return _loadedImages[uri]; +} + +- (void)reset +{ + if (_state) { + const auto &requests = _state->getData().getBackgroundImageRequests(); + for (const auto &request : requests) { + if (request.imageRequest) { + auto it = _uriToObserver.find(request.imageSource.uri); + if (it != _uriToObserver.end()) { + auto& observerCoordinator = request.imageRequest->getObserverCoordinator(); + observerCoordinator.removeObserver(it->second); + } + } + } + } + + _state = nullptr; + _uriToObserver.clear(); + [_loadedImages removeAllObjects]; + [_completedUris removeAllObjects]; +} + +#pragma mark - RCTImageResponseDelegate + +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer +{ + for (const auto& [uri, observerProxy] : _uriToObserver) { + if (observerProxy.get() == observer) { + NSString *nsUri = [NSString stringWithUTF8String:uri.c_str()]; + _loadedImages[nsUri] = image; + [_completedUris addObject:nsUri]; + break; + } + } + + [self notifyDelegateIfAllImagesLoaded]; +} + +- (void)didReceiveProgress:(float)progress + loaded:(int64_t)loaded + total:(int64_t)total + fromObserver:(const void *)observer +{ + // Progress tracking not needed for background images +} + +- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer +{ + for (const auto& [uri, observerProxy] : _uriToObserver) { + if (observerProxy.get() == observer) { + NSString *nsUri = [NSString stringWithUTF8String:uri.c_str()]; + RCTLogWarn(@"Failed to load background image: %@ - %@", nsUri, error); + [_completedUris addObject:nsUri]; + break; + } + } + + [self notifyDelegateIfAllImagesLoaded]; +} + +- (void)notifyDelegateIfAllImagesLoaded +{ + if (_completedUris.count == _uriToObserver.size()) { + [_delegate backgroundImagesDidLoad]; + } +} + +@end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index c9c965b9e2d7..a42546198edf 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -17,12 +17,14 @@ #import #import +#import "RCTBackgroundImageURLLoader.h" + NS_ASSUME_NONNULL_BEGIN /** * UIView class for component. */ -@interface RCTViewComponentView : UIView { +@interface RCTViewComponentView : UIView { @protected facebook::react::LayoutMetrics _layoutMetrics; facebook::react::SharedViewProps _props; 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 0912c50fec13..1a6760d34e25 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -122,6 +122,7 @@ @implementation RCTViewComponentView { NSMutableSet *_accessibilityOrderNativeIDs; RCTSwiftUIContainerViewWrapper *_swiftUIWrapper; BOOL _focusable; + RCTBackgroundImageURLLoader *_backgroundImageLoader; } #ifdef RCT_DYNAMIC_FRAMEWORKS @@ -136,6 +137,8 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { _props = ViewShadowNode::defaultSharedProps(); _reactSubviews = [NSMutableArray new]; + _backgroundImageLoader = [RCTBackgroundImageURLLoader new]; + _backgroundImageLoader.delegate = self; #if !TARGET_OS_TV self.multipleTouchEnabled = YES; #endif @@ -675,6 +678,13 @@ - (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter _eventEmitter = std::static_pointer_cast(eventEmitter); } +- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState +{ + auto newViewState = std::static_pointer_cast(state); + auto oldViewState = oldState ? std::static_pointer_cast(oldState) : nullptr; + [_backgroundImageLoader updateStateWithNewState:newViewState oldState:oldViewState]; +} + - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics { @@ -775,6 +785,9 @@ - (void)prepareForRecycle _filterLayer = nil; [self clearExistingBackgroundImageLayers]; + // Clean up background image observers + [_backgroundImageLoader reset]; + _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil; _eventEmitter.reset(); _isJSResponder = NO; @@ -1310,29 +1323,49 @@ - (void)invalidateLayer backgroundRepeat = _props->backgroundRepeat[imageIndex % _props->backgroundRepeat.size()]; } - CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea - itemIntrinsicSize:backgroundPositioningArea.size - backgroundSize:backgroundSize - backgroundRepeat:backgroundRepeat]; - - CALayer *gradientLayer; + CALayer *itemLayer = nil; if (std::holds_alternative(backgroundImage)) { + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea + itemIntrinsicSize:backgroundPositioningArea.size + backgroundSize:backgroundSize + backgroundRepeat:backgroundRepeat]; const auto &linearGradient = std::get(backgroundImage); - gradientLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient]; + itemLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient]; } else if (std::holds_alternative(backgroundImage)) { + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea + itemIntrinsicSize:backgroundPositioningArea.size + backgroundSize:backgroundSize + backgroundRepeat:backgroundRepeat]; const auto &radialGradient = std::get(backgroundImage); - gradientLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient]; + itemLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient]; + } else if (std::holds_alternative(backgroundImage)) { + const auto &urlBgImage = std::get(backgroundImage); + NSString *uri = [NSString stringWithUTF8String:urlBgImage.uri.c_str()]; + UIImage *loadedImage = [_backgroundImageLoader loadedImageForUri:uri]; + if (loadedImage != nil) { + CGSize intrinsicSize = loadedImage.size; + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea + itemIntrinsicSize:intrinsicSize + backgroundSize:backgroundSize + backgroundRepeat:backgroundRepeat]; + CALayer *imageLayer = [CALayer layer]; + imageLayer.frame = CGRectMake(0, 0, backgroundImageSize.width, backgroundImageSize.height); + imageLayer.contents = (__bridge id)loadedImage.CGImage; + imageLayer.contentsGravity = kCAGravityResizeAspectFill; + itemLayer = imageLayer; + } } - if (gradientLayer != nil) { + if (itemLayer != nil) { + CGSize itemSize = itemLayer.frame.size; CALayer *backgroundImageLayer = [RCTBackgroundImageUtils createBackgroundImageLayerWithSize:backgroundPositioningArea paintingArea:backgroundPaintingArea - itemSize:backgroundImageSize + itemSize:itemSize backgroundPosition:backgroundPosition backgroundRepeat:backgroundRepeat - itemLayer:gradientLayer]; + itemLayer:itemLayer]; [self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetricsBI]; backgroundImageLayer.masksToBounds = YES; backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; @@ -1446,6 +1479,13 @@ - (void)clearExistingBackgroundImageLayers [_backgroundImageLayers removeAllObjects]; } +#pragma mark - RCTBackgroundImageURLLoaderDelegate + +- (void)backgroundImagesDidLoad +{ + [self invalidateLayer]; +} + #pragma mark - Accessibility - (NSObject *)accessibilityElement diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 77046649be10..e709524aac56 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -149,6 +149,9 @@ Pod::Spec.new do |s| sss.dependency "Yoga" sss.source_files = podspec_sources(["react/renderer/components/view/*.{m,mm,cpp,h}", "react/renderer/components/view/platform/cxx/**/*.{m,mm,cpp,h}"], ["react/renderer/components/view/*.{h}", "react/renderer/components/view/platform/cxx/**/*.{h}"]) sss.header_dir = "react/renderer/components/view" + sss.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/react/renderer/imagemanager/platform/ios\"" + } end ss.subspec "viewUmbrella" do |sss| diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp new file mode 100644 index 000000000000..5becd215fe12 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp @@ -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. + */ + +#include "ViewComponentDescriptor.h" +#include + +namespace facebook::react { + +extern const char ImageManagerKey[]; + +ViewComponentDescriptor::ViewComponentDescriptor( + const ComponentDescriptorParameters& parameters) + : ConcreteComponentDescriptor(parameters), + imageManager_( + getManagerByName(contextContainer_, ImageManagerKey)) {} + +void ViewComponentDescriptor::adopt(ShadowNode& shadowNode) const { + ConcreteComponentDescriptor::adopt(shadowNode); + + auto& viewShadowNode = static_cast(shadowNode); + viewShadowNode.setImageManager(imageManager_); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h index d6cba8f4d448..ad1aa75eb49d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h @@ -14,12 +14,17 @@ namespace facebook::react { -class ViewComponentDescriptor : public ConcreteComponentDescriptor { +class ImageManager; + +class ViewComponentDescriptor + : public ConcreteComponentDescriptor { public: - ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters) - { - } + ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters); + + void adopt(ShadowNode &shadowNode) const override; + + private: + const std::shared_ptr imageManager_; }; } // 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 a166a90546c6..3377ec293873 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -8,6 +8,9 @@ #include "ViewShadowNode.h" #include #include +#include +#include +#include namespace facebook::react { @@ -87,4 +90,68 @@ void ViewShadowNode::initialize() noexcept { } } +void ViewShadowNode::setImageManager( + const std::shared_ptr& imageManager) { + ensureUnsealed(); + imageManager_ = imageManager; + updateStateIfNeeded(); +} + +void ViewShadowNode::updateStateIfNeeded() { + if (!imageManager_) { + return; + } + + ensureUnsealed(); + + const auto& viewProps = static_cast(*props_); + const auto& backgroundImages = viewProps.backgroundImage; + + std::vector newRequests; + for (const auto& bgImage : backgroundImages) { + if (std::holds_alternative(bgImage)) { + const auto& urlBgImage = std::get(bgImage); + if (!urlBgImage.uri.empty()) { + BackgroundImageURLRequest request; + request.imageSource.uri = urlBgImage.uri; + if (urlBgImage.uri.find("__packager_asset") != std::string::npos) { + request.imageSource.type = ImageSource::Type::Local; + } else { + request.imageSource.type = ImageSource::Type::Remote; + } + newRequests.push_back(std::move(request)); + } + } + } + + if (newRequests.empty()) { + return; + } + + const auto& savedState = getStateData(); + const auto& oldRequests = savedState.getBackgroundImageRequests(); + + bool requestsChanged = newRequests.size() != oldRequests.size(); + if (!requestsChanged) { + for (size_t i = 0; i < newRequests.size(); ++i) { + if (newRequests[i].imageSource != oldRequests[i].imageSource) { + requestsChanged = true; + break; + } + } + } + + if (!requestsChanged) { + return; + } + + for (auto& request : newRequests) { + request.imageRequest = std::make_shared( + imageManager_->requestImage(request.imageSource, getSurfaceId())); + } + + ViewState state{std::move(newRequests)}; + setStateData(std::move(state)); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h index 689dfbbf29bf..80c1776536a3 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h @@ -11,9 +11,12 @@ #include #include +#include namespace facebook::react { +class ImageManager; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) extern const char ViewComponentName[]; @@ -22,14 +25,19 @@ using ViewShadowNodeProps = ViewProps; /* * `ShadowNode` for component. */ -class ViewShadowNode final : public ConcreteViewShadowNode { +class ViewShadowNode final : public ConcreteViewShadowNode { public: ViewShadowNode(const ShadowNodeFragment &fragment, const ShadowNodeFamily::Shared &family, ShadowNodeTraits traits); ViewShadowNode(const ShadowNode &sourceShadowNode, const ShadowNodeFragment &fragment); + void setImageManager(const std::shared_ptr &imageManager); + private: void initialize() noexcept; + void updateStateIfNeeded(); + + std::shared_ptr imageManager_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp new file mode 100644 index 000000000000..01a4e024901f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp @@ -0,0 +1,17 @@ +/* + * 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 "ViewState.h" + +namespace facebook::react { + +const std::vector& ViewState::getBackgroundImageRequests() + const { + return backgroundImageRequests_; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h new file mode 100644 index 000000000000..cef0682714fe --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h @@ -0,0 +1,50 @@ +/* + * 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 + +#ifdef RN_SERIALIZABLE_STATE +#include +#endif + +namespace facebook::react { + +struct BackgroundImageURLRequest { + ImageSource imageSource{}; + std::shared_ptr imageRequest{}; + + bool operator==(const BackgroundImageURLRequest& rhs) const { + return imageSource == rhs.imageSource && imageRequest == rhs.imageRequest; + } +}; + +class ViewState final { + public: + ViewState() = default; + + explicit ViewState(std::vector backgroundImageRequests) + : backgroundImageRequests_(std::move(backgroundImageRequests)) {} + + const std::vector& getBackgroundImageRequests() const; + +#ifdef RN_SERIALIZABLE_STATE + ViewState(const ViewState& previousState, folly::dynamic data) {} + + folly::dynamic getDynamic() const { + return {}; + } +#endif + + private: + std::vector backgroundImageRequests_; +}; + +} // namespace facebook::react From 0f7d65ed44750efa785e75cbc4c60c0cc4be8fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Mon, 29 Dec 2025 18:07:22 +0530 Subject: [PATCH 3/5] url() function support in background on android copy bitmap on background thread --- .../drawable/BackgroundImageDrawable.kt | 51 +++++++++-- .../drawable/BackgroundImageURLLoader.kt | 87 +++++++++++++++++++ .../uimanager/style/BackgroundImageLayer.kt | 36 ++++---- .../renderer/components/view/CMakeLists.txt | 5 +- 4 files changed, 157 insertions(+), 22 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageURLLoader.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt index 66e04d3e7cbc..6218a4459d47 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt @@ -8,6 +8,7 @@ package com.facebook.react.uimanager.drawable import android.content.Context +import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Paint @@ -45,11 +46,13 @@ internal class BackgroundImageDrawable( private var backgroundImageClipPath: Path? = null private var backgroundPositioningArea: RectF? = null private var backgroundPaintingArea: RectF? = null + private val urlImageLoader = BackgroundImageURLLoader() var backgroundImageLayers: List? = null set(value) { if (field != value) { field = value + loadUrlImages(value) invalidateSelf() } } @@ -111,7 +114,7 @@ internal class BackgroundImageDrawable( } override fun draw(canvas: Canvas) { - if (backgroundImageLayers == null || backgroundImageLayers?.isEmpty() == true) { + if (backgroundImageLayers.isNullOrEmpty()) { return } @@ -140,13 +143,29 @@ internal class BackgroundImageDrawable( val position = backgroundPosition?.takeIf { it.isNotEmpty() }?.let { it.getOrNull(index % it.size) } + val urlBitmap: Bitmap? + val (intrinsicWidth, intrinsicHeight) = when (backgroundImageLayer) { + is BackgroundImageLayer.GradientLayer -> { + urlBitmap = null + backgroundPositioningArea.width() to backgroundPositioningArea.height() + } + is BackgroundImageLayer.URLImageLayer -> { + val bitmap = urlImageLoader.loadedBitmapForUri(backgroundImageLayer.uri) + if (bitmap == null) { + continue + } + urlBitmap = bitmap + bitmap.width.toFloat() to bitmap.height.toFloat() + } + } + // 2. Calculate the size of a single tile. val (tileWidth, tileHeight) = calculateBackgroundImageSize( backgroundPositioningArea.width(), backgroundPositioningArea.height(), - backgroundPositioningArea.width(), - backgroundPositioningArea.height(), + intrinsicWidth, + intrinsicHeight, size, repeat, ) @@ -155,8 +174,12 @@ internal class BackgroundImageDrawable( continue } - // 3. Set paint shader - backgroundPaint.setShader(backgroundImageLayer.getShader(tileWidth, tileHeight)) + // 3. Set paint shader for gradients (URL images don't use shaders) + if (backgroundImageLayer is BackgroundImageLayer.GradientLayer) { + backgroundPaint.setShader(backgroundImageLayer.getShader(tileWidth, tileHeight)) + } else { + backgroundPaint.setShader(null) + } // 4. Calculate spacing, x and y tiles count and position for tiles var (initialX, initialY) = calculateBackgroundPosition(tileWidth, tileHeight, position) @@ -255,7 +278,13 @@ internal class BackgroundImageDrawable( repeat(yTilesCount) { canvas.save() canvas.translate(translateX, translateY) - canvas.drawRect(0f, 0f, tileWidth, tileHeight, backgroundPaint) + if (urlBitmap != null) { + val srcRect = Rect(0, 0, urlBitmap.width, urlBitmap.height) + val dstRect = RectF(0f, 0f, tileWidth, tileHeight) + canvas.drawBitmap(urlBitmap, srcRect, dstRect, backgroundPaint) + } else { + canvas.drawRect(0f, 0f, tileWidth, tileHeight, backgroundPaint) + } canvas.restore() translateY += tileHeight + ySpacing } @@ -414,4 +443,14 @@ internal class BackgroundImageDrawable( return translateX to translateY } + + private fun loadUrlImages(layers: List?) { + val uris = layers?.filterIsInstance()?.map { it.uri } + if (uris.isNullOrEmpty()) { + urlImageLoader.cancelAllRequests() + return + } + + urlImageLoader.loadImages(uris) { invalidateSelf() } + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageURLLoader.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageURLLoader.kt new file mode 100644 index 000000000000..ea928930e70a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageURLLoader.kt @@ -0,0 +1,87 @@ +/* + * 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. + */ + +package com.facebook.react.uimanager.drawable + +import android.graphics.Bitmap +import android.net.Uri +import com.facebook.common.executors.CallerThreadExecutor +import com.facebook.common.logging.FLog +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSource +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.request.ImageRequestBuilder +import com.facebook.react.bridge.UiThreadUtil +import java.util.concurrent.ConcurrentHashMap + +internal class BackgroundImageURLLoader { + private companion object { + private const val TAG = "BackgroundImageURLLoader" + } + + private val pendingRequests = mutableMapOf>>() + private val loadedBitmaps = ConcurrentHashMap() + private var onComplete: (() -> Unit)? = null + + fun loadImages( + uris: List, + onComplete: () -> Unit + ) { + cancelAllRequests() + + if (uris.isEmpty()) { + onComplete() + return + } + + this.onComplete = onComplete + for (uri in uris) { + val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(uri)).build() + val imagePipeline = Fresco.getImagePipeline() + val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null) + + pendingRequests[uri] = dataSource + + dataSource.subscribe( + object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(bitmap: Bitmap?) { + if (bitmap != null) { + loadedBitmaps[uri] = bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false) + } + onRequestComplete(uri) + } + + override fun onFailureImpl(dataSource: DataSource>) { + FLog.w(TAG, "Failed to load background image: $uri-${dataSource.failureCause}") + onRequestComplete(uri) + } + }, + CallerThreadExecutor.getInstance() + ) + } + } + + fun loadedBitmapForUri(uri: String): Bitmap? = loadedBitmaps[uri] + + private fun onRequestComplete(uri: String) { + pendingRequests.remove(uri) + if (pendingRequests.isEmpty()) { + UiThreadUtil.runOnUiThread { onComplete?.invoke() } + } + } + + fun cancelAllRequests() { + for (dataSource in pendingRequests.values) { + dataSource.close() + } + pendingRequests.clear() + loadedBitmaps.clear() + onComplete = null + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt index 8e5cc7d8c2a4..3634f3c9b804 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt @@ -22,13 +22,13 @@ import com.facebook.react.bridge.ReadableType * @see LinearGradient * @see RadialGradient */ -public class BackgroundImageLayer() { - private lateinit var gradient: Gradient - - private constructor(gradient: Gradient) : this() { - this.gradient = gradient +public sealed class BackgroundImageLayer { + public class GradientLayer internal constructor(private val gradient: Gradient) : BackgroundImageLayer() { + public fun getShader(width: Float, height: Float): Shader = gradient.getShader(width, height) } + public class URLImageLayer(public val uri: String) : BackgroundImageLayer() + public companion object { /** * Parses a ReadableMap into a BackgroundImageLayer. @@ -40,22 +40,28 @@ public class BackgroundImageLayer() { * @param context Android context for resource resolution * @return A BackgroundImageLayer instance, or null if parsing fails */ - public fun parse(gradientMap: ReadableMap?, context: Context): BackgroundImageLayer? { - if (gradientMap == null) { + public fun parse(backgroundImageMap: ReadableMap?, context: Context): BackgroundImageLayer? { + if (backgroundImageMap == null) { return null } - val gradient = parseGradient(gradientMap, context) ?: return null - return BackgroundImageLayer(gradient) - } - private fun parseGradient(gradientMap: ReadableMap, context: Context): Gradient? { - if (!gradientMap.hasKey("type") || gradientMap.getType("type") != ReadableType.String) { + if (!backgroundImageMap.hasKey("type") || backgroundImageMap.getType("type") != ReadableType.String) { return null } - return when (gradientMap.getString("type")) { - "linear-gradient" -> LinearGradient.parse(gradientMap, context) - "radial-gradient" -> RadialGradient.parse(gradientMap, context) + return when (backgroundImageMap.getString("type")) { + "linear-gradient" -> { + val gradient = LinearGradient.parse(backgroundImageMap, context) ?: return null + GradientLayer(gradient) + } + "radial-gradient" -> { + val gradient = RadialGradient.parse(backgroundImageMap, context) ?: return null + GradientLayer(gradient) + } + "url" -> { + val uri = backgroundImageMap.getString("uri") ?: return null + URLImageLayer(uri) + } else -> null } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt index 3de2c10301c4..f922d13be77e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt @@ -22,7 +22,10 @@ add_library(rrc_view OBJECT ${rrc_view_SRC}) react_native_android_selector(platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platform/android/ ${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/) -target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR}) +react_native_android_selector(imagemanager_platform_DIR + ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/android/ + ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/cxx/) +target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR} ${imagemanager_platform_DIR}) target_link_libraries(rrc_view folly_runtime From f9e24ac1b58a2d49e526cb9c04078cb9e31dbadc Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 27 Aug 2026 14:37:00 +0200 Subject: [PATCH 4/5] feat: add JS support for CSS mask properties --- .../View/ReactNativeStyleAttributes.js | 17 +++ .../View/__tests__/View-mask-itest.js | 78 ++++++++++ .../NativeComponent/BaseViewConfig.android.js | 8 + .../Libraries/StyleSheet/StyleSheetTypes.js | 8 +- .../components/view/BaseViewProps.cpp | 42 ++++++ .../renderer/components/view/BaseViewProps.h | 6 + .../components/view/ViewShadowNode.cpp | 35 +++-- packages/react-native/ReactNativeApi.d.ts | 141 ++++++++++-------- .../api-snapshots/ReactAndroidDebugCxx.api | 41 ++++- .../api-snapshots/ReactAndroidNewarchCxx.api | 41 ++++- .../api-snapshots/ReactAndroidReleaseCxx.api | 41 ++++- .../api-snapshots/ReactAppleDebugCxx.api | 52 ++++++- .../api-snapshots/ReactAppleNewarchCxx.api | 52 ++++++- .../api-snapshots/ReactAppleReleaseCxx.api | 52 ++++++- .../api-snapshots/ReactCommonDebugCxx.api | 39 ++++- .../api-snapshots/ReactCommonNewarchCxx.api | 39 ++++- .../api-snapshots/ReactCommonReleaseCxx.api | 39 ++++- 17 files changed, 618 insertions(+), 113 deletions(-) create mode 100644 packages/react-native/Libraries/Components/View/__tests__/View-mask-itest.js diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index db39c72950ef..53e1f4e2295b 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -60,6 +60,15 @@ export const backgroundRepeatAttribute: AnyAttributeType = nativeCSSParsing ? true : {process: processBackgroundRepeat}; +// The `mask-*` longhands accept the same values as their `background-*` +// counterparts, so they reuse the same processing. +// https://www.w3.org/TR/css-masking-1/#the-mask-image +export const maskImageAttribute: AnyAttributeType = backgroundImageAttribute; +export const maskSizeAttribute: AnyAttributeType = backgroundSizeAttribute; +export const maskPositionAttribute: AnyAttributeType = + backgroundPositionAttribute; +export const maskRepeatAttribute: AnyAttributeType = backgroundRepeatAttribute; + export const transformAttribute: AnyAttributeType = nativeCSSParsing ? true : {process: processTransform}; @@ -223,6 +232,14 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { /** @deprecated Use `backgroundRepeat` instead. */ experimental_backgroundRepeat: backgroundRepeatAttribute, + /** + * Mask + */ + maskImage: maskImageAttribute, + maskSize: maskSizeAttribute, + maskPosition: maskPositionAttribute, + maskRepeat: maskRepeatAttribute, + /** * View */ diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-mask-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-mask-itest.js new file mode 100644 index 000000000000..7ecc9717b6d5 --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/View-mask-itest.js @@ -0,0 +1,78 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; + +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {View} from 'react-native'; + +// `mask-image` reuses the `background-image` value syntax, so these tests check +// that the style plumbing reaches the `maskImage` prop of the mounted view for +// each accepted form. `collapsable={false}` is not needed here: a non-empty +// `mask-image` makes the view form a stacking context on its own. +function mountedMaskImage(style: ViewStyleProp): string { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + return root.getRenderedOutput({props: ['maskImage']}).toJSONObject().props + .maskImage; +} + +describe(' mask-image', () => { + it('accepts a linear-gradient() shorthand', () => { + const maskImage = mountedMaskImage({ + maskImage: 'linear-gradient(#e66465, #9198e5)', + }); + expect(maskImage).toContain('linear-gradient'); + expect(maskImage).toContain('rgba(230, 100, 101, 1)'); + }); + + it('accepts a radial-gradient() shorthand', () => { + expect( + mountedMaskImage({maskImage: 'radial-gradient(circle, black, white)'}), + ).toContain('radial-gradient'); + }); + + it('accepts a url() shorthand', () => { + expect( + mountedMaskImage({maskImage: 'url(https://example.com/mask.png)'}), + ).toContain('https://example.com/mask.png'); + }); + + it('accepts an array of layers', () => { + const maskImage = mountedMaskImage({ + maskImage: [ + {type: 'url', uri: 'https://example.com/mask.png'}, + { + type: 'linear-gradient', + direction: 'to right', + colorStops: [{color: 'black'}, {color: 'white'}], + }, + ], + }); + expect(maskImage).toContain('https://example.com/mask.png'); + expect(maskImage).toContain('linear-gradient'); + }); + + it('is unset when no mask-image is given', () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + expect( + root.getRenderedOutput({props: ['maskImage']}).toJSONObject().props + .maskImage, + ).toBeUndefined(); + }); +}); diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 6e3ee698720d..aa9b829c0633 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -19,6 +19,10 @@ import { boxShadowAttribute, colorAttribute, filterAttribute, + maskImageAttribute, + maskPositionAttribute, + maskRepeatAttribute, + maskSizeAttribute, } from '../Components/View/ReactNativeStyleAttributes'; import {DynamicallyInjectedByGestureHandler} from './ViewConfigIgnore'; @@ -219,6 +223,10 @@ const validAttributesForNonEventProps = { experimental_backgroundPosition: backgroundPositionAttribute, backgroundRepeat: backgroundRepeatAttribute, experimental_backgroundRepeat: backgroundRepeatAttribute, + maskImage: maskImageAttribute, + maskSize: maskSizeAttribute, + maskPosition: maskPositionAttribute, + maskRepeat: maskRepeatAttribute, boxShadow: boxShadowAttribute, filter: filterAttribute, mixBlendMode: true, diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index b9b1b6b921b9..5048ea3990e6 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -775,9 +775,7 @@ type URLBackgroundImageValue = { }; export type BackgroundImageValue = - | LinearGradientValue - | RadialGradientValue - | URLBackgroundImageValue; + LinearGradientValue | RadialGradientValue | URLBackgroundImageValue; export type BackgroundSizeValue = { x: string | number, @@ -908,6 +906,10 @@ export type ____ViewStyle_InternalBase = Readonly<{ ReadonlyArray | string, backgroundRepeat?: ReadonlyArray | string, experimental_backgroundRepeat?: ReadonlyArray | string, + maskImage?: ReadonlyArray | string, + maskSize?: ReadonlyArray | string, + maskPosition?: ReadonlyArray | string, + maskRepeat?: ReadonlyArray | string, isolation?: 'auto' | 'isolate', }>; 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 1cb30b0ed6a8..a58b3b0b16b5 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -247,6 +247,42 @@ BaseViewProps::BaseViewProps( sourceProps.backgroundRepeat, {}), {})), + maskImage( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.maskImage + : convertRawProp( + context, + rawProps, + "maskImage", + sourceProps.maskImage, + {})), + maskSize( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.maskSize + : convertRawProp( + context, + rawProps, + "maskSize", + sourceProps.maskSize, + {})), + maskPosition( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.maskPosition + : convertRawProp( + context, + rawProps, + "maskPosition", + sourceProps.maskPosition, + {})), + maskRepeat( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.maskRepeat + : convertRawProp( + context, + rawProps, + "maskRepeat", + sourceProps.maskRepeat, + {})), mixBlendMode(convertRawProp( context, rawProps, @@ -384,6 +420,10 @@ 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(maskImage); + RAW_SET_PROP_SWITCH_CASE_BASIC(maskSize); + RAW_SET_PROP_SWITCH_CASE_BASIC(maskPosition); + RAW_SET_PROP_SWITCH_CASE_BASIC(maskRepeat); // events field VIEW_EVENT_CASE(PointerEnter); VIEW_EVENT_CASE(PointerEnterCapture); @@ -609,6 +649,8 @@ SharedDebugStringConvertibleList BaseViewProps::getDebugProps() const { "backgroundImage", backgroundImage, defaultBaseViewProps.backgroundImage), + debugStringConvertibleItem( + "maskImage", maskImage, defaultBaseViewProps.maskImage), }; } #endif 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 c78c4f38729b..6daf89016eaf 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -86,6 +86,12 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { // Background Repeat std::vector backgroundRepeat{}; + // Mask + std::vector maskImage{}; + std::vector maskSize{}; + std::vector maskPosition{}; + std::vector maskRepeat{}; + // MixBlendMode BlendMode mixBlendMode{BlendMode::Normal}; 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 3377ec293873..864042539f9e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -61,6 +61,7 @@ void ViewShadowNode::initialize() noexcept { !viewProps.filter.empty() || viewProps.mixBlendMode != BlendMode::Normal || viewProps.isolation == Isolation::Isolate || + !viewProps.maskImage.empty() || HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) || !viewProps.accessibilityOrder.empty(); @@ -105,24 +106,30 @@ void ViewShadowNode::updateStateIfNeeded() { ensureUnsealed(); const auto& viewProps = static_cast(*props_); - const auto& backgroundImages = viewProps.backgroundImage; std::vector newRequests; - for (const auto& bgImage : backgroundImages) { - if (std::holds_alternative(bgImage)) { - const auto& urlBgImage = std::get(bgImage); - if (!urlBgImage.uri.empty()) { - BackgroundImageURLRequest request; - request.imageSource.uri = urlBgImage.uri; - if (urlBgImage.uri.find("__packager_asset") != std::string::npos) { - request.imageSource.type = ImageSource::Type::Local; - } else { - request.imageSource.type = ImageSource::Type::Remote; - } - newRequests.push_back(std::move(request)); + auto collectRequests = [&](const std::vector& images) { + for (const auto& image : images) { + if (!std::holds_alternative(image)) { + continue; } + const auto& urlImage = std::get(image); + if (urlImage.uri.empty()) { + continue; + } + BackgroundImageURLRequest request; + request.imageSource.uri = urlImage.uri; + request.imageSource.type = + urlImage.uri.find("__packager_asset") != std::string::npos + ? ImageSource::Type::Local + : ImageSource::Type::Remote; + newRequests.push_back(std::move(request)); } - } + }; + + // `background-image` and `mask-image` share the same image loading pipeline. + collectRequests(viewProps.backgroundImage); + collectRequests(viewProps.maskImage); if (newRequests.empty()) { return; diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 1cda27553fbe..311732ac08d3 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -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<<0cdd169a97730ccd6c6fdb359f0eb30d>> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -891,6 +891,10 @@ declare type ____ViewStyle_InternalBase = { readonly elevation?: number readonly filter?: ReadonlyArray | string readonly isolation?: "auto" | "isolate" + readonly maskImage?: ReadonlyArray | string + readonly maskPosition?: ReadonlyArray | string + readonly maskRepeat?: ReadonlyArray | string + readonly maskSize?: ReadonlyArray | string readonly mixBlendMode?: ____BlendMode_Internal readonly opacity?: number readonly outlineColor?: ____ColorValue_Internal @@ -1586,7 +1590,8 @@ declare type AttributeType = readonly process?: (arg1: V) => T } declare type AutoCapitalize = "characters" | "none" | "sentences" | "words" -declare type BackgroundImageValue = LinearGradientValue | RadialGradientValue +declare type BackgroundImageValue = + LinearGradientValue | RadialGradientValue | URLBackgroundImageValue declare type BackgroundPositionValue = | { bottom: number | string @@ -5407,6 +5412,10 @@ declare type UnsafeEventObject = Object declare type UnsafeMixed = unknown declare type UnsafeNativeEventObject = Object declare type UnsafeObject = Object +declare type URLBackgroundImageValue = { + type: "url" + uri: number | string +} declare function useAnimatedColor( inputValue?: InputValue, config?: AnimatedColorConfig | null | undefined, @@ -5749,18 +5758,18 @@ export { AccessibilityValue, // cf8bcb74 ActionSheetIOS, // b558559e ActionSheetIOSOptions, // 1756eb5a - ActivityIndicator, // 7bb76795 + ActivityIndicator, // 78820c15 ActivityIndicatorInstance, // a82dd4e7 - ActivityIndicatorProps, // e3c81e37 + ActivityIndicatorProps, // ded6942c Alert, // a398a509 AlertButton, // bf1a3b60 AlertButtonStyle, // ec9fb242 AlertOptions, // 8a116d2a AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // e99db73c + Animated, // 1d694506 AppConfig, // 35c0ca70 - AppRegistry, // 5bc2bced + AppRegistry, // a6c11088 AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 @@ -5772,8 +5781,8 @@ export { BackPressEventName, // 4620fb76 BlurEvent, // 4ba4f941 BoxShadowValue, // b679703f - Button, // 78a75446 - ButtonInstance, // aecdca9d + Button, // a56892b1 + ButtonInstance, // 7cf7da53 ButtonProps, // 21c5780c Clipboard, // 41addb89 CodegenTypes, // ab4986cc @@ -5792,9 +5801,9 @@ export { DimensionsPayload, // 653bc26c DisplayMetrics, // 1dc35cef DisplayMetricsAndroid, // 872e62eb - DrawerLayoutAndroid, // a1445452 + DrawerLayoutAndroid, // 8efe0702 DrawerLayoutAndroidInstance, // c0694352 - DrawerLayoutAndroidProps, // 8144971b + DrawerLayoutAndroidProps, // 1034e28f DrawerSlideEvent, // c4ab8fba DropShadowValue, // e9df2606 DynamicColorIOS, // d96c228c @@ -5810,9 +5819,9 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // ff6fd4b9 - FlatListInstance, // 930e6e0d - FlatListProps, // b77a3695 + FlatList, // c0840ab3 + FlatListInstance, // c3b70ff5 + FlatListProps, // f95eb3a9 FocusEvent, // 850f1517 FontVariant, // 7c7558bb GestureResponderEvent, // 14d3e77a @@ -5823,17 +5832,17 @@ export { IEventEmitter, // fbef6131 IOSKeyboardEvent, // e67bfe3a IgnorePattern, // ec6f6ece - Image, // 70beaf51 - ImageBackground, // 125f114f - ImageBackgroundInstance, // b32a44b1 - ImageBackgroundProps, // 58c484ed + Image, // 7666419f + ImageBackground, // 5311e4e6 + ImageBackgroundInstance, // 8c498b53 + ImageBackgroundProps, // 2d7febd7 ImageErrorEvent, // 978933f4 ImageInstance, // 9a100753 ImageLoadEvent, // 77f0b718 ImageProgressEventIOS, // 445331a4 - ImageProps, // 0b057370 + ImageProps, // 2724e392 ImagePropsAndroid, // ee00e1d5 - ImagePropsBase, // 360c2972 + ImagePropsBase, // 0f4adb5c ImagePropsIOS, // 9e19c85d ImageRequireSource, // 681d683b ImageResizeMode, // d51106e2 @@ -5841,19 +5850,19 @@ export { ImageSize, // 1c47cf88 ImageSource, // ea31cf4a ImageSourcePropType, // f522e093 - ImageStyle, // 23278d38 + ImageStyle, // e9bb4356 ImageURISource, // 443d047c - InputAccessoryView, // 69da97d8 - InputAccessoryViewProps, // 6c4ba417 + InputAccessoryView, // 045c7220 + InputAccessoryViewProps, // 1b8359f0 InputModeOptions, // 4e8581b9 Insets, // e7fe432a KeyDownEvent, // 5b147614 KeyEvent, // 20fa4267 KeyUpEvent, // 57f832c5 Keyboard, // 49414c97 - KeyboardAvoidingView, // 86ce3237 - KeyboardAvoidingViewInstance, // 39e5e0cb - KeyboardAvoidingViewProps, // 5ea2df10 + KeyboardAvoidingView, // d5a54bde + KeyboardAvoidingViewInstance, // 475d4243 + KeyboardAvoidingViewProps, // d3d1f3bd KeyboardEvent, // c3f895d4 KeyboardEventEasing, // af4091c8 KeyboardEventName, // 59299ad6 @@ -5878,10 +5887,10 @@ export { MeasureInWindowOnSuccessCallback, // a285f598 MeasureLayoutOnSuccessCallback, // 3592502a MeasureOnSuccessCallback, // 82824e59 - Modal, // ddb43746 + Modal, // e486fa5b ModalBaseProps, // c294cc46 ModalInstance, // d466ce77 - ModalProps, // a073d560 + ModalProps, // bba8a717 ModalPropsAndroid, // 515fb173 ModalPropsIOS, // 664ecb7e ModeChangeEvent, // f64bf69d @@ -5917,15 +5926,15 @@ export { PointerEvent, // ff599afe PressabilityConfig, // fea539a5 PressabilityEventHandlers, // c222648b - Pressable, // 73cbe498 + Pressable, // 7735f120 PressableAndroidRippleConfig, // ee32eaca PressableInstance, // eebfe911 - PressableProps, // 573ac60a + PressableProps, // 7ec21d81 PressableStateCallbackType, // 5e0cfa78 ProcessedColorValue, // 33f74304 - ProgressBarAndroid, // db8f6a6b + ProgressBarAndroid, // 5e569ebb ProgressBarAndroidInstance, // ab545ef1 - ProgressBarAndroidProps, // 6484d368 + ProgressBarAndroidProps, // f7a46f43 PublicRootInstance, // 8040afd7 PublicTextInstance, // 6937c7bf PushNotificationEventName, // 84e7e150 @@ -5933,9 +5942,9 @@ export { PushNotificationPermissions, // c2e7ae4f Rationale, // 5df1b1c1 ReactNativeVersion, // abd76827 - RefreshControl, // 1fc5c032 - RefreshControlInstance, // 7cef228c - RefreshControlProps, // c03c6dd3 + RefreshControl, // 76a4295f + RefreshControlInstance, // f6f9eac4 + RefreshControlProps, // 3028e0a2 RefreshControlPropsAndroid, // 8ac931ca RefreshControlPropsIOS, // 72a36381 Registry, // 6c39216d @@ -5944,27 +5953,27 @@ export { Role, // af7b889d RootTag, // 3cd10504 RootTagContext, // 38bfc8f6 - RootViewStyleProvider, // a4547094 + RootViewStyleProvider, // 7e8587e0 Runnable, // 594dd93a Runnables, // 4367c557 - SafeAreaView, // 2a5620cd + SafeAreaView, // eed995bb SafeAreaViewInstance, // 21dba39c ScaledSize, // 07e417c7 ScrollEvent, // d7abdd0a - ScrollResponderType, // 4fb54e25 + ScrollResponderType, // c6236c7a ScrollToLocationParamsType, // d7ecdad1 - ScrollView, // 5b9149bf - ScrollViewImperativeMethods, // 308dd401 - ScrollViewInstance, // b86e49e8 - ScrollViewProps, // 40e3563d + ScrollView, // 08d27833 + ScrollViewImperativeMethods, // 3f896bcd + ScrollViewInstance, // 85906dc5 + ScrollViewProps, // ec0ca824 ScrollViewPropsAndroid, // 02f3df2e ScrollViewPropsIOS, // 807cb4f6 ScrollViewScrollToOptions, // 3313411e SectionBase, // 9f13db00 - SectionList, // 5ad78704 + SectionList, // ae79ae22 SectionListData, // 1a4de01a - SectionListInstance, // ce15f61c - SectionListProps, // 3b9f4d91 + SectionListInstance, // 9eb7aad3 + SectionListProps, // c1c257d6 SectionListRenderItem, // 715b2086 SectionListRenderItemInfo, // 4a48a922 Separators, // 6a45f7e3 @@ -5981,19 +5990,19 @@ export { StatusBarProps, // c2a44d88 StatusBarStyle, // 78f53eea StyleProp, // fa0e9b4a - StyleSheet, // f7fe407a + StyleSheet, // eb6ff476 SubmitBehavior, // c4ddf490 - Switch, // cf0d6ce5 + Switch, // 25d4a329 SwitchChangeEvent, // 899635b1 SwitchInstance, // 3c50eec5 - SwitchProps, // 9efb522c + SwitchProps, // fa4ce398 Systrace, // 626d178c TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 - Text, // 3ccd8020 + Text, // 9e776acd TextContentType, // 239b3ecc - TextInput, // 89af456b + TextInput, // 195a5343 TextInputAndroidProps, // 9ebbc103 TextInputBlurEvent, // b77af40e TextInputChangeEvent, // f55eef98 @@ -6003,44 +6012,44 @@ export { TextInputIOSProps, // fb3c9327 TextInputInstance, // 5a0c0e0d TextInputKeyPressEvent, // 546c5d07 - TextInputProps, // a93c2e69 + TextInputProps, // 0c66af6b TextInputSelectionChangeEvent, // e58f2abc TextInputSubmitEditingEvent, // 6bcb2aa5 TextInstance, // 05463a96 TextLayoutEvent, // 3f54186f - TextProps, // 2e3336ca - TextStyle, // d7678842 + TextProps, // 917f9bb7 + TextStyle, // 09b5821f ToastAndroid, // 88a8969a - TouchableHighlight, // edab1b07 + TouchableHighlight, // 04adf802 TouchableHighlightInstance, // b510c0eb - TouchableHighlightProps, // 08cd59e1 - TouchableNativeFeedback, // e2791ad5 - TouchableNativeFeedbackInstance, // ce1ad7e9 - TouchableNativeFeedbackProps, // ffc9c1d4 - TouchableOpacity, // fac1cc91 + TouchableHighlightProps, // 8ade4662 + TouchableNativeFeedback, // 66953b88 + TouchableNativeFeedbackInstance, // 9c598e5a + TouchableNativeFeedbackProps, // ab63f091 + TouchableOpacity, // 7f73b8f3 TouchableOpacityInstance, // b186055b - TouchableOpacityProps, // 9e3eeec9 - TouchableWithoutFeedback, // da544b16 - TouchableWithoutFeedbackProps, // a14626e7 + TouchableOpacityProps, // 3eb2de50 + TouchableWithoutFeedback, // acd854bf + TouchableWithoutFeedbackProps, // d14f0c38 TransformsStyle, // 65e70f18 TurboModule, // dfe29706 TurboModuleRegistry, // 4ace6db2 UIManager, // afbcdf05 UTFSequence, // ad625158 Vibration, // 31e4bbf8 - View, // 14779d0c + View, // 546bff6b ViewInstance, // ffde5573 - ViewProps, // 46cb2061 + ViewProps, // 514c6207 ViewPropsAndroid, // 55e81851 ViewPropsIOS, // 58ee19bf - ViewStyle, // e75eec1e + ViewStyle, // 05dd5366 VirtualViewMode, // 6be59722 VirtualizedList, // 68c7345e VirtualizedListInstance, // 423ee7c0 - VirtualizedListProps, // 5dded4a1 + VirtualizedListProps, // 1893b9cb VirtualizedSectionList, // 9fd9cd61 VirtualizedSectionListInstance, // 12b706d5 - VirtualizedSectionListProps, // ef311473 + VirtualizedSectionListProps, // 1a12ce90 WrapperComponentProvider, // 9ef54e61 codegenNativeCommands, // 628a7c0a codegenNativeComponent, // 520daa94 diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 2e6dc7e0b4e2..9eaad1424349 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -582,14 +582,14 @@ using facebook::react::AndroidSwipeRefreshLayoutState = facebook::react::StateDa using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -1858,9 +1858,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -5524,11 +5528,21 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(const facebook::react::ViewState& previousState, folly::dynamic data); + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; + public folly::dynamic getDynamic() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -6869,6 +6883,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -7219,6 +7239,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -8190,6 +8215,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -10020,6 +10051,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 7ec351405ee3..8e04211ed3ec 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -581,14 +581,14 @@ using facebook::react::AndroidSwipeRefreshLayoutState = facebook::react::StateDa using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -1852,9 +1852,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -5334,11 +5338,21 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(const facebook::react::ViewState& previousState, folly::dynamic data); + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; + public folly::dynamic getDynamic() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -6679,6 +6693,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -7029,6 +7049,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -7950,6 +7975,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -9633,6 +9664,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6843410835c3..dd49ffa45111 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -582,14 +582,14 @@ using facebook::react::AndroidSwipeRefreshLayoutState = facebook::react::StateDa using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -1856,9 +1856,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -5515,11 +5519,21 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(const facebook::react::ViewState& previousState, folly::dynamic data); + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; + public folly::dynamic getDynamic() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -6860,6 +6874,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -7210,6 +7230,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -8181,6 +8206,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -9864,6 +9895,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index d6dc3f80a6ad..d0216fbebf79 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -675,6 +675,13 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public @property (weak) id delegate; + public virtual _Nullable UIImage* loadedImageForUri:(NSString* uri); + public virtual void reset(); + public virtual void updateStateWithNewState:oldState:(facebook::react::ViewShadowNode::ConcreteState::Shared state, facebook::react::ViewShadowNode::ConcreteState::Shared oldState); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); @@ -2163,7 +2170,7 @@ interface RCTView : public UIView { public virtual void updateClippedSubviews(); } -interface RCTViewComponentView : public UIView { +interface RCTViewComponentView : public UIView { protected facebook::react::LayoutMetrics _layoutMetrics; protected facebook::react::SharedViewEventEmitter _eventEmitter; protected facebook::react::SharedViewProps _props; @@ -2662,6 +2669,10 @@ protocol RCTBackedTextInputViewProtocol : public UITextInput { public virtual void setSelectedTextRange:notifyDelegate:(_Nullable UITextRange* selectedTextRange, BOOL notifyDelegate); } +protocol RCTBackgroundImageURLLoaderDelegate : public RCTImageResponseDelegate { + public virtual void backgroundImagesDidLoad(); +} + protocol RCTBridgeDelegate : public NSObject { public virtual NSURL* _Nullable sourceURLForBridge:(RCTBridge* bridge); } @@ -3460,14 +3471,14 @@ using facebook::react::ActivityIndicatorViewState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -4458,9 +4469,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -7693,11 +7708,19 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -9047,6 +9070,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -9400,6 +9429,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -10205,6 +10239,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -11914,6 +11954,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index da7a542fd69f..de19f7f024e7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -675,6 +675,13 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public @property (weak) id delegate; + public virtual _Nullable UIImage* loadedImageForUri:(NSString* uri); + public virtual void reset(); + public virtual void updateStateWithNewState:oldState:(facebook::react::ViewShadowNode::ConcreteState::Shared state, facebook::react::ViewShadowNode::ConcreteState::Shared oldState); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); @@ -2156,7 +2163,7 @@ interface RCTView : public UIView { public virtual void updateClippedSubviews(); } -interface RCTViewComponentView : public UIView { +interface RCTViewComponentView : public UIView { protected facebook::react::LayoutMetrics _layoutMetrics; protected facebook::react::SharedViewEventEmitter _eventEmitter; protected facebook::react::SharedViewProps _props; @@ -2655,6 +2662,10 @@ protocol RCTBackedTextInputViewProtocol : public UITextInput { public virtual void setSelectedTextRange:notifyDelegate:(_Nullable UITextRange* selectedTextRange, BOOL notifyDelegate); } +protocol RCTBackgroundImageURLLoaderDelegate : public RCTImageResponseDelegate { + public virtual void backgroundImagesDidLoad(); +} + protocol RCTBridgeDelegate : public NSObject { } @@ -3451,14 +3462,14 @@ using facebook::react::ActivityIndicatorViewState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -4445,9 +4456,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -7535,11 +7550,19 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -8889,6 +8912,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -9242,6 +9271,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -10021,6 +10055,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -11593,6 +11633,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index a92742d8c14e..1d53dd6bbf0d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -675,6 +675,13 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public @property (weak) id delegate; + public virtual _Nullable UIImage* loadedImageForUri:(NSString* uri); + public virtual void reset(); + public virtual void updateStateWithNewState:oldState:(facebook::react::ViewShadowNode::ConcreteState::Shared state, facebook::react::ViewShadowNode::ConcreteState::Shared oldState); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); @@ -2163,7 +2170,7 @@ interface RCTView : public UIView { public virtual void updateClippedSubviews(); } -interface RCTViewComponentView : public UIView { +interface RCTViewComponentView : public UIView { protected facebook::react::LayoutMetrics _layoutMetrics; protected facebook::react::SharedViewEventEmitter _eventEmitter; protected facebook::react::SharedViewProps _props; @@ -2662,6 +2669,10 @@ protocol RCTBackedTextInputViewProtocol : public UITextInput { public virtual void setSelectedTextRange:notifyDelegate:(_Nullable UITextRange* selectedTextRange, BOOL notifyDelegate); } +protocol RCTBackgroundImageURLLoaderDelegate : public RCTImageResponseDelegate { + public virtual void backgroundImagesDidLoad(); +} + protocol RCTBridgeDelegate : public NSObject { public virtual NSURL* _Nullable sourceURLForBridge:(RCTBridge* bridge); } @@ -3460,14 +3471,14 @@ using facebook::react::ActivityIndicatorViewState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -4456,9 +4467,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -7684,11 +7699,19 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -9038,6 +9061,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -9391,6 +9420,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -10196,6 +10230,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -11768,6 +11808,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 763c902b2e27..8a96a9fc89e3 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -211,14 +211,14 @@ using facebook::react::ActivePointerRegistry = std::unordered_map; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -1181,9 +1181,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -3948,11 +3952,19 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -5185,6 +5197,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -5534,6 +5552,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -6305,6 +6328,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -7025,6 +7054,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index abad9815f5c4..a9e7cb259d13 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -210,14 +210,14 @@ using facebook::react::ActivePointerRegistry = std::unordered_map; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -1176,9 +1176,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -3798,11 +3802,19 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -5035,6 +5047,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -5384,6 +5402,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -6129,6 +6152,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -6849,6 +6878,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 0c58351b5c06..97359ab9dd0a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -211,14 +211,14 @@ using facebook::react::ActivePointerRegistry = std::unordered_map; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -1179,9 +1179,13 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public static facebook::react::Transform resolveTransform(const facebook::react::Size& frameSize, const facebook::react::Transform& transform, const facebook::react::TransformOrigin& transformOrigin); public std::optional zIndex; public std::vector backgroundImage; + public std::vector maskImage; public std::vector backgroundPosition; + public std::vector maskPosition; public std::vector backgroundRepeat; + public std::vector maskRepeat; public std::vector backgroundSize; + public std::vector maskSize; public std::vector boxShadow; public std::vector filter; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); @@ -3939,11 +3943,19 @@ class facebook::react::ValueFactoryEventPayload : public facebook::react::EventP class facebook::react::ViewComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { public ViewComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); + public virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; } -class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { +class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShadowNode { public ViewShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public ViewShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); + public void setImageManager(const std::shared_ptr& imageManager); +} + +class facebook::react::ViewState { + public ViewState() = default; + public ViewState(std::vector backgroundImageRequests); + public const std::vector& getBackgroundImageRequests() const; } class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook, public facebook::react::MountingOverrideDelegate { @@ -5176,6 +5188,12 @@ struct facebook::react::AnimationMutations { public std::vector batch; } +struct facebook::react::BackgroundImageURLRequest { + public bool operator==(const facebook::react::BackgroundImageURLRequest& rhs) const; + public facebook::react::ImageSource imageSource; + public std::shared_ptr imageRequest; +} + struct facebook::react::BackgroundPosition { public BackgroundPosition(); public bool operator==(const facebook::react::BackgroundPosition& other) const = default; @@ -5525,6 +5543,11 @@ struct facebook::react::CSSTranslateY { public std::variant value; } +struct facebook::react::CSSURLFunction { + public bool operator==(const facebook::react::CSSURLFunction& rhs) const = default; + public std::string url; +} + struct facebook::react::CSSZero { public constexpr bool operator==(const facebook::react::CSSZero& rhs) const = default; } @@ -6296,6 +6319,12 @@ struct facebook::react::TransformOrigin { public std::array xy; } +struct facebook::react::URLBackgroundImage { + public bool operator!=(const facebook::react::URLBackgroundImage& rhs) const; + public bool operator==(const facebook::react::URLBackgroundImage& rhs) const; + public std::string uri; +} + struct facebook::react::ValueUnit { public constexpr ValueUnit() = default; public constexpr ValueUnit(float v, facebook::react::UnitType u); @@ -7016,6 +7045,10 @@ struct facebook::react::CSSDataTypeParser : publ struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSVariantComponentTransformParser { } +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consumePreservedToken(const facebook::react::CSSPreservedToken& token); } From 7af27269c71848d744abdab65c9447521b8080e0 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 27 Aug 2026 13:32:12 +0200 Subject: [PATCH 5/5] feat: add iOS support for CSS mask properties --- .../View/RCTViewComponentView.mm | 117 +++++- .../rn-tester/js/examples/Mask/MaskExample.js | 389 ++++++++++++++++++ .../rn-tester/js/utils/RNTesterList.ios.js | 4 + 3 files changed, 507 insertions(+), 3 deletions(-) create mode 100644 packages/rn-tester/js/examples/Mask/MaskExample.js 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 1a6760d34e25..374ead417308 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -104,6 +104,18 @@ static BOOL RCTViewIsInteractiveAccessibilityElement(UIView *view, const ViewPro } #endif +// Core Animation supports a single mask per layer, so the border-radius +// clipping shape is nested as the mask of the `mask-image` layer. Nested masks +// multiply, which is exactly the intersection we want. +static CALayer *RCTIntersectMaskLayers(CALayer *maskImageLayer, CALayer *clippingLayer) +{ + if (maskImageLayer == nil) { + return clippingLayer; + } + maskImageLayer.mask = clippingLayer; + return maskImageLayer; +} + @implementation RCTViewComponentView { UIColor *_backgroundColor; CALayer *_backgroundColorLayer; @@ -662,6 +674,12 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & needsInvalidateLayer = YES; } + // `mask` + if (oldViewProps.maskImage != newViewProps.maskImage || oldViewProps.maskSize != newViewProps.maskSize || + oldViewProps.maskPosition != newViewProps.maskPosition || oldViewProps.maskRepeat != newViewProps.maskRepeat) { + needsInvalidateLayer = YES; + } + // `boxShadow` if (oldViewProps.boxShadow != newViewProps.boxShadow) { needsInvalidateLayer = YES; @@ -1399,7 +1417,8 @@ - (void)invalidateLayer } // clipping - self.currentContainerView.layer.mask = nil; + CALayer *maskImageLayer = [self createMaskImageLayer]; + self.currentContainerView.layer.mask = maskImageLayer; if (self.currentContainerView.clipsToBounds) { BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(); if (!clipToPaddingBox) { @@ -1410,7 +1429,7 @@ - (void)invalidateLayer [self createMaskLayer:self.bounds cornerInsets:RCTGetCornerInsets( RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)]; - self.currentContainerView.layer.mask = maskLayer; + self.currentContainerView.layer.mask = RCTIntersectMaskLayers(maskImageLayer, maskLayer); } for (UIView *subview in self.currentContainerView.subviews) { @@ -1431,7 +1450,7 @@ - (void)invalidateLayer cornerInsets:RCTGetCornerInsets( RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))]; - self.currentContainerView.layer.mask = maskLayer; + self.currentContainerView.layer.mask = RCTIntersectMaskLayers(maskImageLayer, maskLayer); } else { self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; } @@ -1467,6 +1486,98 @@ - (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)c return maskLayer; } +// Builds the layer used as `self.currentContainerView.layer.mask` for the +// `mask-image` style, or nil when no mask is set. Each mask layer is painted +// into a shared container so that multiple mask images composite together, +// matching how `background-image` stacks its layers. +// https://www.w3.org/TR/css-masking-1/#the-mask-image +- (nullable CALayer *)createMaskImageLayer +{ + if (_props->maskImage.empty()) { + return nil; + } + + // mask-origin: padding-box + CGRect positioningArea = RCTCGRectFromRect(_layoutMetrics.getPaddingFrame()); + // mask-clip: border-box + CGRect paintingArea = self.layer.bounds; + + CALayer *containerLayer = [CALayer layer]; + containerLayer.frame = paintingArea; + + size_t imageIndex = _props->maskImage.size() - 1; + // iterate in reverse to match CSS specification + for (const auto &maskImage : std::ranges::reverse_view(_props->maskImage)) { + BackgroundSize maskSize = BackgroundSizeLengthPercentage{}; + if (!_props->maskSize.empty()) { + maskSize = _props->maskSize[imageIndex % _props->maskSize.size()]; + } + + BackgroundPosition maskPosition; + if (!_props->maskPosition.empty()) { + maskPosition = _props->maskPosition[imageIndex % _props->maskPosition.size()]; + } + + BackgroundRepeat maskRepeat; + if (!_props->maskRepeat.empty()) { + maskRepeat = _props->maskRepeat[imageIndex % _props->maskRepeat.size()]; + } + + CALayer *itemLayer = nil; + + if (std::holds_alternative(maskImage)) { + CGSize itemSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:positioningArea + itemIntrinsicSize:positioningArea.size + backgroundSize:maskSize + backgroundRepeat:maskRepeat]; + itemLayer = [RCTLinearGradient gradientLayerWithSize:itemSize gradient:std::get(maskImage)]; + } else if (std::holds_alternative(maskImage)) { + CGSize itemSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:positioningArea + itemIntrinsicSize:positioningArea.size + backgroundSize:maskSize + backgroundRepeat:maskRepeat]; + itemLayer = [RCTRadialGradient gradientLayerWithSize:itemSize gradient:std::get(maskImage)]; + } else if (std::holds_alternative(maskImage)) { + const auto &urlMaskImage = std::get(maskImage); + NSString *uri = RCTNSStringFromString(urlMaskImage.uri); + // Loaded asynchronously by `RCTBackgroundImageURLLoader`, which triggers + // another `invalidateLayer` once the image is available. + UIImage *loadedImage = [_backgroundImageLoader loadedImageForUri:uri]; + if (loadedImage != nil) { + CGSize itemSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:positioningArea + itemIntrinsicSize:loadedImage.size + backgroundSize:maskSize + backgroundRepeat:maskRepeat]; + CALayer *imageLayer = [CALayer layer]; + imageLayer.frame = CGRectMake(0, 0, itemSize.width, itemSize.height); + imageLayer.contents = (__bridge id)loadedImage.CGImage; + // `itemSize` already resolves `mask-size`, so the image stretches to + // fill it rather than preserving its intrinsic aspect ratio. + imageLayer.contentsGravity = kCAGravityResize; + itemLayer = imageLayer; + } + } + + if (itemLayer != nil) { + CALayer *maskImageLayer = [RCTBackgroundImageUtils createBackgroundImageLayerWithSize:positioningArea + paintingArea:paintingArea + itemSize:itemLayer.frame.size + backgroundPosition:maskPosition + backgroundRepeat:maskRepeat + itemLayer:itemLayer]; + // The helper leaves the returned layer unpositioned; sizing it to the + // painting area is what applies `mask-clip: border-box`. + maskImageLayer.frame = paintingArea; + maskImageLayer.masksToBounds = YES; + [containerLayer addSublayer:maskImageLayer]; + } + + imageIndex--; + } + + return containerLayer; +} + - (void)clearExistingBackgroundImageLayers { if (_backgroundImageLayers == nil) { diff --git a/packages/rn-tester/js/examples/Mask/MaskExample.js b/packages/rn-tester/js/examples/Mask/MaskExample.js new file mode 100644 index 000000000000..d0e8a993a89e --- /dev/null +++ b/packages/rn-tester/js/examples/Mask/MaskExample.js @@ -0,0 +1,389 @@ +/** + * 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. + * + * @flow + * @format + */ + +'use strict'; + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; + +import * as React from 'react'; +import { + Image, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; + +const LOCAL_MASK = require('../../assets/imageMask.png'); + +const REMOTE_MASK = 'https://reactnative.dev/img/tiny_logo.png'; + +function MaskBox({ + style, + children, + testID, +}: { + style?: ViewStyleProp, + children?: React.Node, + testID: string, +}) { + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + box: { + width: 200, + height: 100, + backgroundColor: '#4ecdc4', + justifyContent: 'center', + alignItems: 'center', + marginVertical: 10, + }, + square: { + width: 150, + height: 150, + }, + text: { + color: 'white', + fontWeight: 'bold', + fontSize: 20, + }, + image: { + width: 150, + height: 150, + }, + textInput: { + width: 200, + borderWidth: 1, + borderColor: '#999', + padding: 8, + marginVertical: 10, + }, + scrollView: { + width: 200, + height: 160, + borderWidth: 1, + borderColor: '#999', + marginVertical: 10, + }, + scrollRow: { + padding: 8, + fontSize: 18, + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-evenly', + }, +}); + +exports.title = 'Mask'; +exports.category = 'UI'; +exports.description = + 'Examples of the mask-image, mask-size, mask-position and mask-repeat styles.'; +exports.examples = [ + { + title: 'Linear gradient mask', + description: 'Fades the view out towards its right edge.', + name: 'linear-gradient', + render(): React.Node { + return ( + + Fade + + ); + }, + }, + { + title: 'Radial gradient mask', + description: 'Reveals the view through a circular hole.', + name: 'radial-gradient', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Gradient stops', + description: + 'Multiple colour stops punch a transparent band out of the middle.', + name: 'gradient-stops', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Local image mask', + description: + 'A bundled PNG with an alpha channel, stretched over the whole view.', + name: 'local-image', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Remote image mask', + description: + 'A mask loaded over the network, centred without repeating. The view is ' + + 'unmasked until the image arrives.', + name: 'remote-image', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'mask-repeat', + description: 'The same image tiled across the view.', + name: 'repeat', + render(): React.Node { + return ( + + + + + ); + }, + }, + { + title: 'mask-size and mask-position', + description: 'A single tile sized and placed in the bottom right corner.', + name: 'size-position', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Multiple mask layers', + description: + 'Two gradients composite together, so the view shows through where either is opaque.', + name: 'multiple-layers', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Mask with border radius', + description: + 'The mask is clipped by the border box, so rounded corners still apply.', + name: 'border-radius', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Masked children', + description: + 'The mask applies to the whole subtree, not just the background.', + name: 'children', + render(): React.Node { + return ( + + + + ); + }, + }, + { + title: 'Masked scroll view', + description: + 'A gradient mask fades the top and bottom edges of a .', + name: 'scroll-view', + render(): React.Node { + return ( + + {Array.from({length: 12}, (_, i) => ( + + Row {i + 1} + + ))} + + ); + }, + }, + { + title: 'Masked text', + description: 'A gradient mask applied to a component.', + name: 'text', + render(): React.Node { + return ( + + Masked text + + ); + }, + }, + { + title: 'Masked image', + description: 'A gradient mask applied to an component.', + name: 'image', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Masked text input', + description: 'A gradient mask applied to a component.', + name: 'text-input', + render(): React.Node { + return ( + + ); + }, + }, +] as Array; diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index 1b588ba6d209..5d7ba414f584 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -329,6 +329,10 @@ const APIs: Array = ( category: 'UI', module: require('../examples/RadialGradient/RadialGradientExample'), }, + { + key: 'MaskExample', + module: require('../examples/Mask/MaskExample'), + }, { key: 'BackgroundImageExample', category: 'UI',