diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index 4b1763143fc86f..5828d9cae031e1 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -146,8 +146,9 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { /** * Linear Gradient */ - experimental_backgroundImage: {process: processBackgroundImage}, - + experimental_backgroundImage: ReactNativeFeatureFlags.enableNativeCSSParsing() + ? true + : {process: processBackgroundImage}, /** * View */ diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js index 2c2779ecd0de64..7eaa402ec24e11 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @flow strict-local + * @fantom_flags enableNativeCSSParsing:* * @format */ @@ -229,6 +230,56 @@ describe('', () => { }); }); }); + + describe('background-image', () => { + it('it parses CSS and object syntax', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + <> + + + , + ); + }); + + const expectedProps = { + backgroundImage: + '[radial-gradient(ellipse farthest-corner at 50% 50% , rgba(230, 100, 101, 1), rgba(145, 152, 229, 1))]', + }; + + expect(root.getRenderedOutput().toJSON()).toEqual([ + { + children: [], + props: expectedProps, + type: 'View', + }, + { + children: [], + props: expectedProps, + type: 'View', + }, + ]); + }); + }); }); describe('pointerEvents', () => { diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 8dcad92e61de09..262d2b6ffc9cc3 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -179,9 +179,9 @@ const validAttributesForNonEventProps = { backgroundColor: {process: require('../StyleSheet/processColor').default}, transform: true, transformOrigin: true, - experimental_backgroundImage: { - process: require('../StyleSheet/processBackgroundImage').default, - }, + experimental_backgroundImage: ReactNativeFeatureFlags.enableNativeCSSParsing() + ? (true as const) + : {process: require('../StyleSheet/processBackgroundImage').default}, boxShadow: ReactNativeFeatureFlags.enableNativeCSSParsing() ? (true as const) : {process: require('../StyleSheet/processBoxShadow').default}, diff --git a/packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm b/packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm index 1ced502a797f8f..ee0f5ff8e0c98d 100644 --- a/packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm +++ b/packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm @@ -267,6 +267,10 @@ @implementation RCTGradientUtils + (std::vector)getFixedColorStops:(const std::vector &)colorStops gradientLineLength:(CGFloat)gradientLineLength { + if (colorStops.empty()) { + return {}; + } + std::vector fixedColorStops(colorStops.size()); bool hasNullPositions = false; auto maxPositionSoFar = resolveColorStopPosition(colorStops[0].position, gradientLineLength); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp new file mode 100644 index 00000000000000..56ac84fc5b42eb --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp @@ -0,0 +1,617 @@ +/* + * 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 "BackgroundImagePropsConversions.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +using RawValueMap = std::unordered_map; +using RawValueList = std::vector; + +inline GradientKeyword parseGradientKeyword(const std::string& keyword) { + if (keyword == "to top right") { + return GradientKeyword::ToTopRight; + } else if (keyword == "to bottom right") { + return GradientKeyword::ToBottomRight; + } else if (keyword == "to top left") { + return GradientKeyword::ToTopLeft; + } else if (keyword == "to bottom left") { + return GradientKeyword::ToBottomLeft; + } else { + throw std::invalid_argument("Invalid gradient keyword: " + keyword); + } +} + +void parseProcessedBackgroundImage( + const PropsParserContext& context, + const RawValue& value, + std::vector& result) { + react_native_expect(value.hasType()); + if (!value.hasType()) { + result = {}; + return; + } + + std::vector backgroundImage{}; + auto rawBackgroundImage = static_cast(value); + for (const auto& rawBackgroundImageValue : rawBackgroundImage) { + bool isMap = rawBackgroundImageValue.hasType(); + react_native_expect(isMap); + if (!isMap) { + result = {}; + return; + } + + auto rawBackgroundImageMap = + static_cast(rawBackgroundImageValue); + auto typeIt = rawBackgroundImageMap.find("type"); + if (typeIt == rawBackgroundImageMap.end() || + !typeIt->second.hasType()) { + continue; + } + + std::string type = (std::string)(typeIt->second); + std::vector colorStops; + + auto colorStopsIt = rawBackgroundImageMap.find("colorStops"); + if (colorStopsIt != rawBackgroundImageMap.end() && + colorStopsIt->second.hasType()) { + auto rawColorStops = static_cast(colorStopsIt->second); + for (const auto& stop : rawColorStops) { + if (stop.hasType()) { + auto stopMap = static_cast(stop); + auto positionIt = stopMap.find("position"); + auto colorIt = stopMap.find("color"); + + if (positionIt != stopMap.end() && colorIt != stopMap.end()) { + ColorStop colorStop; + if (positionIt->second.hasValue()) { + auto valueUnit = toValueUnit(positionIt->second); + if (!valueUnit) { + result = {}; + return; + } + colorStop.position = valueUnit; + } + if (colorIt->second.hasValue()) { + fromRawValue( + context.contextContainer, + context.surfaceId, + colorIt->second, + colorStop.color); + } + colorStops.push_back(colorStop); + } + } + } + } + + if (type == "linear-gradient") { + LinearGradient linearGradient; + + auto directionIt = rawBackgroundImageMap.find("direction"); + if (directionIt != rawBackgroundImageMap.end() && + directionIt->second.hasType()) { + auto directionMap = static_cast(directionIt->second); + + auto directionTypeIt = directionMap.find("type"); + auto valueIt = directionMap.find("value"); + + if (directionTypeIt != directionMap.end() && + valueIt != directionMap.end()) { + std::string directionType = (std::string)(directionTypeIt->second); + + if (directionType == "angle") { + linearGradient.direction.type = GradientDirectionType::Angle; + if (valueIt->second.hasType()) { + linearGradient.direction.value = (Float)(valueIt->second); + } + } else if (directionType == "keyword") { + linearGradient.direction.type = GradientDirectionType::Keyword; + if (valueIt->second.hasType()) { + linearGradient.direction.value = + parseGradientKeyword((std::string)(valueIt->second)); + } + } + } + } + + if (!colorStops.empty()) { + linearGradient.colorStops = colorStops; + } + + backgroundImage.emplace_back(std::move(linearGradient)); + } else if (type == "radial-gradient") { + RadialGradient radialGradient; + auto shapeIt = rawBackgroundImageMap.find("shape"); + if (shapeIt != rawBackgroundImageMap.end() && + shapeIt->second.hasType()) { + auto shape = (std::string)(shapeIt->second); + radialGradient.shape = shape == "circle" ? RadialGradientShape::Circle + : RadialGradientShape::Ellipse; + } + + auto sizeIt = rawBackgroundImageMap.find("size"); + if (sizeIt != rawBackgroundImageMap.end()) { + if (sizeIt->second.hasType()) { + auto sizeStr = (std::string)(sizeIt->second); + if (sizeStr == "closest-side") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::ClosestSide; + } else if (sizeStr == "farthest-side") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::FarthestSide; + } else if (sizeStr == "closest-corner") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::ClosestCorner; + } else if (sizeStr == "farthest-corner") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::FarthestCorner; + } + } else if (sizeIt->second.hasType()) { + auto sizeMap = static_cast(sizeIt->second); + auto xIt = sizeMap.find("x"); + auto yIt = sizeMap.find("y"); + if (xIt != sizeMap.end() && yIt != sizeMap.end()) { + radialGradient.size = RadialGradientSize{ + .value = RadialGradientSize::Dimensions{ + .x = toValueUnit(xIt->second), + .y = toValueUnit(yIt->second)}}; + } + } + + auto positionIt = rawBackgroundImageMap.find("position"); + if (positionIt != rawBackgroundImageMap.end() && + positionIt->second.hasType()) { + auto positionMap = static_cast(positionIt->second); + + auto topIt = positionMap.find("top"); + auto bottomIt = positionMap.find("bottom"); + auto leftIt = positionMap.find("left"); + auto rightIt = positionMap.find("right"); + + if (topIt != positionMap.end()) { + auto topValue = toValueUnit(topIt->second); + radialGradient.position.top = topValue; + } else if (bottomIt != positionMap.end()) { + auto bottomValue = toValueUnit(bottomIt->second); + radialGradient.position.bottom = bottomValue; + } + + if (leftIt != positionMap.end()) { + auto leftValue = toValueUnit(leftIt->second); + radialGradient.position.left = leftValue; + } else if (rightIt != positionMap.end()) { + auto rightValue = toValueUnit(rightIt->second); + radialGradient.position.right = rightValue; + } + } + } + + if (!colorStops.empty()) { + radialGradient.colorStops = colorStops; + } + + backgroundImage.emplace_back(std::move(radialGradient)); + } + } + + result = backgroundImage; +} + +void parseUnprocessedBackgroundImageList( + const PropsParserContext& context, + const std::vector& value, + std::vector& result) { + std::vector backgroundImage{}; + for (const auto& rawBackgroundImageValue : value) { + bool isMap = rawBackgroundImageValue.hasType(); + react_native_expect(isMap); + if (!isMap) { + result = {}; + return; + } + + auto rawBackgroundImageMap = + static_cast(rawBackgroundImageValue); + + auto typeIt = rawBackgroundImageMap.find("type"); + if (typeIt == rawBackgroundImageMap.end() || + !typeIt->second.hasType()) { + continue; + } + + std::string type = (std::string)(typeIt->second); + std::vector colorStops; + + auto colorStopsIt = rawBackgroundImageMap.find("colorStops"); + if (colorStopsIt != rawBackgroundImageMap.end() && + colorStopsIt->second.hasType()) { + auto rawColorStops = static_cast(colorStopsIt->second); + for (const auto& stop : rawColorStops) { + if (stop.hasType()) { + auto stopMap = static_cast(stop); + auto positionsIt = stopMap.find("positions"); + auto colorIt = stopMap.find("color"); + // has only color. e.g. (red, green) + if (positionsIt == stopMap.end() || + (positionsIt->second.hasType() && + static_cast(positionsIt->second).empty())) { + auto color = coerceColor(colorIt->second, context); + if (!color) { + // invalid color + result = {}; + return; + } + colorStops.push_back( + ColorStop{.color = std::move(color), .position = ValueUnit()}); + continue; + } + + // Color hint (red, 20%, blue) + // or Color Stop with positions (red, 20% 30%) + if (positionsIt != stopMap.end() && + positionsIt->second.hasType()) { + auto positions = static_cast(positionsIt->second); + for (const auto& position : positions) { + auto positionValue = toValueUnit(position); + if (!positionValue) { + // invalid position + result = {}; + return; + } + + ColorStop colorStop; + colorStop.position = positionValue; + if (colorIt != stopMap.end()) { + auto color = coerceColor(colorIt->second, context); + if (color) { + colorStop.color = color; + } + } + colorStops.emplace_back(colorStop); + } + } + } + } + } + + if (type == "linear-gradient") { + LinearGradient linearGradient; + + auto directionIt = rawBackgroundImageMap.find("direction"); + if (directionIt != rawBackgroundImageMap.end()) { + if (directionIt->second.hasType()) { + std::string directionStr = (std::string)(directionIt->second); + auto cssDirection = + parseCSSProperty(directionStr); + + if (std::holds_alternative( + cssDirection)) { + const auto& direction = + std::get(cssDirection); + + if (std::holds_alternative(direction.value)) { + linearGradient.direction.type = GradientDirectionType::Angle; + linearGradient.direction.value = + std::get(direction.value).degrees; + } else if (std::holds_alternative< + CSSLinearGradientDirectionKeyword>( + direction.value)) { + linearGradient.direction.type = GradientDirectionType::Keyword; + auto keyword = + std::get(direction.value); + + switch (keyword) { + case CSSLinearGradientDirectionKeyword::ToTopLeft: + linearGradient.direction.value = GradientKeyword::ToTopLeft; + break; + case CSSLinearGradientDirectionKeyword::ToTopRight: + linearGradient.direction.value = GradientKeyword::ToTopRight; + break; + case CSSLinearGradientDirectionKeyword::ToBottomLeft: + linearGradient.direction.value = + GradientKeyword::ToBottomLeft; + break; + case CSSLinearGradientDirectionKeyword::ToBottomRight: + linearGradient.direction.value = + GradientKeyword::ToBottomRight; + break; + } + } + } + } + } + + if (!colorStops.empty()) { + linearGradient.colorStops = colorStops; + } + + backgroundImage.emplace_back(std::move(linearGradient)); + } else if (type == "radial-gradient") { + RadialGradient radialGradient; + auto shapeIt = rawBackgroundImageMap.find("shape"); + if (shapeIt != rawBackgroundImageMap.end() && + shapeIt->second.hasType()) { + auto shape = (std::string)(shapeIt->second); + radialGradient.shape = shape == "circle" ? RadialGradientShape::Circle + : RadialGradientShape::Ellipse; + } + + auto sizeIt = rawBackgroundImageMap.find("size"); + if (sizeIt != rawBackgroundImageMap.end()) { + if (sizeIt->second.hasType()) { + auto sizeStr = (std::string)(sizeIt->second); + if (sizeStr == "closest-side") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::ClosestSide; + } else if (sizeStr == "farthest-side") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::FarthestSide; + } else if (sizeStr == "closest-corner") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::ClosestCorner; + } else if (sizeStr == "farthest-corner") { + radialGradient.size.value = + RadialGradientSize::SizeKeyword::FarthestCorner; + } + } else if (sizeIt->second.hasType()) { + auto sizeMap = static_cast(sizeIt->second); + auto xIt = sizeMap.find("x"); + auto yIt = sizeMap.find("y"); + if (xIt != sizeMap.end() && yIt != sizeMap.end()) { + radialGradient.size = {RadialGradientSize::Dimensions{ + .x = toValueUnit(xIt->second), .y = toValueUnit(yIt->second)}}; + } + } + + auto positionIt = rawBackgroundImageMap.find("position"); + if (positionIt != rawBackgroundImageMap.end() && + positionIt->second.hasType()) { + auto positionMap = static_cast(positionIt->second); + + auto topIt = positionMap.find("top"); + auto bottomIt = positionMap.find("bottom"); + auto leftIt = positionMap.find("left"); + auto rightIt = positionMap.find("right"); + + if (topIt != positionMap.end()) { + auto topValue = toValueUnit(topIt->second); + radialGradient.position.top = topValue; + } else if (bottomIt != positionMap.end()) { + auto bottomValue = toValueUnit(bottomIt->second); + radialGradient.position.bottom = bottomValue; + } + + if (leftIt != positionMap.end()) { + auto leftValue = toValueUnit(leftIt->second); + radialGradient.position.left = leftValue; + } else if (rightIt != positionMap.end()) { + auto rightValue = toValueUnit(rightIt->second); + radialGradient.position.right = rightValue; + } + } + } + + if (!colorStops.empty()) { + radialGradient.colorStops = colorStops; + } + + backgroundImage.emplace_back(std::move(radialGradient)); + } + } + + result = backgroundImage; +} + +namespace { +ValueUnit convertLengthPercentageToValueUnit( + const std::variant& value) { + if (std::holds_alternative(value)) { + return {std::get(value).value, UnitType::Point}; + } else { + return {std::get(value).value, UnitType::Percent}; + } +} + +void fromCSSColorStop( + const std::variant& item, + std::vector& colorStops) { + if (std::holds_alternative(item)) { + const auto& colorStop = std::get(item); + + // handle two positions case: [color, position, position] -> push two + // stops + if (colorStop.startPosition.has_value() && + colorStop.endPosition.has_value()) { + // first stop with start position + colorStops.push_back(ColorStop{ + .color = fromCSSColor(colorStop.color), + .position = + convertLengthPercentageToValueUnit(*colorStop.startPosition)}); + + // second stop with end position (same color) + colorStops.push_back(ColorStop{ + .color = fromCSSColor(colorStop.color), + .position = + convertLengthPercentageToValueUnit(*colorStop.endPosition)}); + } else { + // single color stop + ColorStop stop; + stop.color = fromCSSColor(colorStop.color); + + // handle start position if present + if (colorStop.startPosition.has_value()) { + stop.position = + convertLengthPercentageToValueUnit(*colorStop.startPosition); + } + + colorStops.push_back(stop); + } + } else if (std::holds_alternative(item)) { + const auto& colorHint = std::get(item); + // color hint: add a stop with null color and the hint position + ColorStop hintStop; + hintStop.position = convertLengthPercentageToValueUnit(colorHint.position); + colorStops.push_back(hintStop); + } +} + +std::optional fromCSSBackgroundImage( + const CSSBackgroundImageVariant& cssBackgroundImage) { + if (std::holds_alternative(cssBackgroundImage)) { + const auto& gradient = + std::get(cssBackgroundImage); + LinearGradient linearGradient; + + if (gradient.direction.has_value()) { + if (std::holds_alternative(gradient.direction->value)) { + const auto& angle = std::get(gradient.direction->value); + linearGradient.direction.type = GradientDirectionType::Angle; + linearGradient.direction.value = angle.degrees; + } else if (std::holds_alternative( + gradient.direction->value)) { + const auto& dirKeyword = std::get( + gradient.direction->value); + linearGradient.direction.type = GradientDirectionType::Keyword; + + switch (dirKeyword) { + case CSSLinearGradientDirectionKeyword::ToTopLeft: + linearGradient.direction.value = GradientKeyword::ToTopLeft; + break; + case CSSLinearGradientDirectionKeyword::ToTopRight: + linearGradient.direction.value = GradientKeyword::ToTopRight; + break; + case CSSLinearGradientDirectionKeyword::ToBottomLeft: + linearGradient.direction.value = GradientKeyword::ToBottomLeft; + break; + case CSSLinearGradientDirectionKeyword::ToBottomRight: + linearGradient.direction.value = GradientKeyword::ToBottomRight; + break; + } + } + } + + for (const auto& item : gradient.items) { + fromCSSColorStop(item, linearGradient.colorStops); + } + + return BackgroundImage{linearGradient}; + + } else if (std::holds_alternative( + cssBackgroundImage)) { + const auto& gradient = + std::get(cssBackgroundImage); + RadialGradient radialGradient; + + if (gradient.shape.has_value()) { + radialGradient.shape = (*gradient.shape == CSSRadialGradientShape::Circle) + ? RadialGradientShape::Circle + : RadialGradientShape::Ellipse; + } + + if (gradient.size.has_value()) { + if (std::holds_alternative( + *gradient.size)) { + const auto& sizeKeyword = + std::get(*gradient.size); + switch (sizeKeyword) { + case CSSRadialGradientSizeKeyword::ClosestSide: + radialGradient.size.value = + RadialGradientSize::SizeKeyword::ClosestSide; + break; + case CSSRadialGradientSizeKeyword::ClosestCorner: + radialGradient.size.value = + RadialGradientSize::SizeKeyword::ClosestCorner; + break; + case CSSRadialGradientSizeKeyword::FarthestSide: + radialGradient.size.value = + RadialGradientSize::SizeKeyword::FarthestSide; + break; + case CSSRadialGradientSizeKeyword::FarthestCorner: + radialGradient.size.value = + RadialGradientSize::SizeKeyword::FarthestCorner; + break; + } + } else if (std::holds_alternative( + *gradient.size)) { + const auto& explicitSize = + std::get(*gradient.size); + radialGradient.size.value = RadialGradientSize::Dimensions{ + .x = convertLengthPercentageToValueUnit(explicitSize.sizeX), + .y = convertLengthPercentageToValueUnit(explicitSize.sizeY)}; + } + } + + if (gradient.position.has_value()) { + const auto& pos = *gradient.position; + if (pos.top.has_value()) { + radialGradient.position.top = + convertLengthPercentageToValueUnit(*pos.top); + } + if (pos.bottom.has_value()) { + radialGradient.position.bottom = + convertLengthPercentageToValueUnit(*pos.bottom); + } + if (pos.left.has_value()) { + radialGradient.position.left = + convertLengthPercentageToValueUnit(*pos.left); + } + if (pos.right.has_value()) { + radialGradient.position.right = + convertLengthPercentageToValueUnit(*pos.right); + } + } + + for (const auto& item : gradient.items) { + fromCSSColorStop(item, radialGradient.colorStops); + } + + return BackgroundImage{radialGradient}; + } + + return std::nullopt; +} +} // namespace + +void parseUnprocessedBackgroundImageString( + const std::string& value, + std::vector& result) { + auto backgroundImageList = parseCSSProperty(value); + if (!std::holds_alternative(backgroundImageList)) { + result = {}; + return; + } + + std::vector backgroundImages; + for (const auto& cssBackgroundImage : + std::get(backgroundImageList)) { + if (auto backgroundImage = fromCSSBackgroundImage(cssBackgroundImage)) { + backgroundImages.push_back(*backgroundImage); + } else { + result = {}; + return; + } + } + + result = backgroundImages; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.h new file mode 100644 index 00000000000000..d33c701c01213b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +void parseProcessedBackgroundImage( + const PropsParserContext& context, + const RawValue& value, + std::vector& result); + +void parseUnprocessedBackgroundImageList( + const PropsParserContext& context, + const std::vector& value, + std::vector& result); + +void parseUnprocessedBackgroundImageString( + const std::string& value, + std::vector& result); + +inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + std::vector& result) { + if (ReactNativeFeatureFlags::enableNativeCSSParsing()) { + if (value.hasType()) { + parseUnprocessedBackgroundImageString((std::string)value, result); + } else if (value.hasType>()) { + parseUnprocessedBackgroundImageList( + context, (std::vector)value, result); + } else { + result = {}; + } + } else { + parseProcessedBackgroundImage(context, value, result); + } +} + +} // namespace facebook::react 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 dc9d79534e7986..99c2f822979b59 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -611,6 +612,10 @@ SharedDebugStringConvertibleList BaseViewProps::getDebugProps() const { defaultBaseViewProps.pointerEvents), debugStringConvertibleItem( "transform", transform, defaultBaseViewProps.transform), + debugStringConvertibleItem( + "backgroundImage", + backgroundImage, + defaultBaseViewProps.backgroundImage), }; } #endif diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index 4132d578701028..2f6f9dbe1f7658 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -18,7 +18,7 @@ #include #include #include -#include +#include #include #include #include @@ -566,7 +566,7 @@ inline void fromRawValue( } inline void fromRawValue( - const PropsParserContext& context, + const PropsParserContext& /*context*/, const RawValue& value, Transform& result) { auto transformMatrix = Transform{}; @@ -1205,197 +1205,6 @@ inline void fromRawValue( result = blendMode.value(); } -inline void fromRawValue( - const PropsParserContext& context, - const RawValue& value, - std::vector& result) { - react_native_expect(value.hasType>()); - if (!value.hasType>()) { - result = {}; - return; - } - - std::vector backgroundImage{}; - auto rawBackgroundImage = static_cast>(value); - for (const auto& rawBackgroundImageValue : rawBackgroundImage) { - bool isMap = rawBackgroundImageValue - .hasType>(); - react_native_expect(isMap); - if (!isMap) { - result = {}; - return; - } - - auto rawBackgroundImageMap = - static_cast>( - rawBackgroundImageValue); - - auto typeIt = rawBackgroundImageMap.find("type"); - if (typeIt == rawBackgroundImageMap.end() || - !typeIt->second.hasType()) { - continue; - } - - std::string type = (std::string)(typeIt->second); - std::vector colorStops; - auto colorStopsIt = rawBackgroundImageMap.find("colorStops"); - - if (colorStopsIt != rawBackgroundImageMap.end() && - colorStopsIt->second.hasType>()) { - auto rawColorStops = - static_cast>(colorStopsIt->second); - - for (const auto& stop : rawColorStops) { - if (stop.hasType>()) { - auto stopMap = - static_cast>(stop); - auto positionIt = stopMap.find("position"); - auto colorIt = stopMap.find("color"); - - if (positionIt != stopMap.end() && colorIt != stopMap.end()) { - ColorStop colorStop; - if (positionIt->second.hasValue()) { - auto valueUnit = toValueUnit(positionIt->second); - if (!valueUnit) { - result = {}; - return; - } - colorStop.position = valueUnit; - } - if (colorIt->second.hasValue()) { - fromRawValue( - context.contextContainer, - context.surfaceId, - colorIt->second, - colorStop.color); - } - colorStops.push_back(colorStop); - } - } - } - } - - if (type == "linear-gradient") { - LinearGradient linearGradient; - - auto directionIt = rawBackgroundImageMap.find("direction"); - if (directionIt != rawBackgroundImageMap.end() && - directionIt->second - .hasType>()) { - auto directionMap = - static_cast>( - directionIt->second); - - auto directionTypeIt = directionMap.find("type"); - auto valueIt = directionMap.find("value"); - - if (directionTypeIt != directionMap.end() && - valueIt != directionMap.end()) { - std::string directionType = (std::string)(directionTypeIt->second); - - if (directionType == "angle") { - linearGradient.direction.type = GradientDirectionType::Angle; - if (valueIt->second.hasType()) { - linearGradient.direction.value = (Float)(valueIt->second); - } - } else if (directionType == "keyword") { - linearGradient.direction.type = GradientDirectionType::Keyword; - if (valueIt->second.hasType()) { - linearGradient.direction.value = - parseGradientKeyword((std::string)(valueIt->second)); - } - } - } - } - - if (!colorStops.empty()) { - linearGradient.colorStops = colorStops; - } - - backgroundImage.emplace_back(std::move(linearGradient)); - } else if (type == "radial-gradient") { - RadialGradient radialGradient; - auto shapeIt = rawBackgroundImageMap.find("shape"); - if (shapeIt != rawBackgroundImageMap.end() && - shapeIt->second.hasType()) { - auto shape = (std::string)(shapeIt->second); - radialGradient.shape = shape == "circle" ? RadialGradientShape::Circle - : RadialGradientShape::Ellipse; - } - - auto sizeIt = rawBackgroundImageMap.find("size"); - if (sizeIt != rawBackgroundImageMap.end()) { - if (sizeIt->second.hasType()) { - auto sizeStr = (std::string)(sizeIt->second); - if (sizeStr == "closest-side") { - radialGradient.size.value = - RadialGradientSize::SizeKeyword::ClosestSide; - } else if (sizeStr == "farthest-side") { - radialGradient.size.value = - RadialGradientSize::SizeKeyword::FarthestSide; - } else if (sizeStr == "closest-corner") { - radialGradient.size.value = - RadialGradientSize::SizeKeyword::ClosestCorner; - } else if (sizeStr == "farthest-corner") { - radialGradient.size.value = - RadialGradientSize::SizeKeyword::FarthestCorner; - } - } else if (sizeIt->second - .hasType>()) { - auto sizeMap = static_cast>( - sizeIt->second); - auto xIt = sizeMap.find("x"); - auto yIt = sizeMap.find("y"); - if (xIt != sizeMap.end() && yIt != sizeMap.end()) { - RadialGradientSize sizeObj; - sizeObj.value = RadialGradientSize::Dimensions{ - .x = toValueUnit(xIt->second), .y = toValueUnit(yIt->second)}; - radialGradient.size = sizeObj; - } - } - - auto positionIt = rawBackgroundImageMap.find("position"); - if (positionIt != rawBackgroundImageMap.end() && - positionIt->second - .hasType>()) { - auto positionMap = - static_cast>( - positionIt->second); - - auto topIt = positionMap.find("top"); - auto bottomIt = positionMap.find("bottom"); - auto leftIt = positionMap.find("left"); - auto rightIt = positionMap.find("right"); - - if (topIt != positionMap.end()) { - auto topValue = toValueUnit(topIt->second); - radialGradient.position.top = topValue; - } else if (bottomIt != positionMap.end()) { - auto bottomValue = toValueUnit(bottomIt->second); - radialGradient.position.bottom = bottomValue; - } - - if (leftIt != positionMap.end()) { - auto leftValue = toValueUnit(leftIt->second); - radialGradient.position.left = leftValue; - } else if (rightIt != positionMap.end()) { - auto rightValue = toValueUnit(rightIt->second); - radialGradient.position.right = rightValue; - } - } - } - - if (!colorStops.empty()) { - radialGradient.colorStops = colorStops; - } - - backgroundImage.emplace_back(std::move(radialGradient)); - } - } - - result = backgroundImage; -} - inline void fromRawValue( const PropsParserContext& /*context*/, const RawValue& value, @@ -1417,6 +1226,7 @@ inline void fromRawValue( result = isolation.value(); } +#if RN_DEBUG_STRING_CONVERTIBLE template inline std::string toString(const std::array vec) { std::string s; @@ -1614,5 +1424,6 @@ inline std::string toString(const Transform& transform) { result += "]"; return result; } +#endif } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h b/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h index 126c289798d2e5..5f3327407d2f90 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h @@ -46,17 +46,7 @@ inline int toAndroidRepr(const SharedColor& color) { #endif inline std::string toString(const SharedColor& value) { - ColorComponents components = colorComponentsFromColor(value); - std::array buffer{}; - std::snprintf( - buffer.data(), - buffer.size(), - "rgba(%.0f, %.0f, %.0f, %g)", - components.red * 255.f, - components.green * 255.f, - components.blue * 255.f, - components.alpha); - return buffer.data(); + return value.toString(); } #pragma mark - Geometry diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h b/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h new file mode 100644 index 00000000000000..250308773b57bf --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h @@ -0,0 +1,912 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +enum class CSSLinearGradientDirectionKeyword : uint8_t { + ToTopLeft, + ToTopRight, + ToBottomLeft, + ToBottomRight, +}; + +struct CSSLinearGradientDirection { + // angle or keyword like "to bottom" + std::variant value; + + bool operator==(const CSSLinearGradientDirection& rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consume(CSSSyntaxParser& parser) + -> std::optional { + return parseLinearGradientDirection(parser); + } + + private: + static constexpr std::optional + parseLinearGradientDirection(CSSSyntaxParser& parser) { + auto angle = parseNextCSSValue(parser); + if (std::holds_alternative(angle)) { + return CSSLinearGradientDirection{std::get(angle)}; + } + auto toResult = parser.consumeComponentValue( + [](const CSSPreservedToken& token) -> bool { + return token.type() == CSSTokenType::Ident && + fnv1aLowercase(token.stringValue()) == fnv1a("to"); + }); + + if (!toResult) { + // no direction found, default to 180 degrees (to bottom) + return CSSLinearGradientDirection{CSSAngle{180.0f}}; + } + + parser.consumeWhitespace(); + + std::optional primaryDir; + auto primaryResult = + parser.consumeComponentValue>( + [](const CSSPreservedToken& token) -> std::optional { + if (token.type() == CSSTokenType::Ident) { + switch (fnv1aLowercase(token.stringValue())) { + case fnv1a("top"): + return CSSKeyword::Top; + case fnv1a("bottom"): + return CSSKeyword::Bottom; + case fnv1a("left"): + return CSSKeyword::Left; + case fnv1a("right"): + return CSSKeyword::Right; + } + } + return {}; + }); + + if (!primaryResult) { + return {}; + } + + primaryDir = primaryResult; + parser.consumeWhitespace(); + + std::optional secondaryDir; + auto secondaryResult = + parser.consumeComponentValue>( + [&](const CSSPreservedToken& token) -> std::optional { + if (token.type() == CSSTokenType::Ident) { + auto hash = fnv1aLowercase(token.stringValue()); + // validate compatible combinations + if (primaryDir == CSSKeyword::Top || + primaryDir == CSSKeyword::Bottom) { + if (hash == fnv1a("left")) { + return CSSKeyword::Left; + } + if (hash == fnv1a("right")) { + return CSSKeyword::Right; + } + } + if (primaryDir == CSSKeyword::Left || + primaryDir == CSSKeyword::Right) { + if (hash == fnv1a("top")) { + return CSSKeyword::Top; + } + if (hash == fnv1a("bottom")) { + return CSSKeyword::Bottom; + } + } + } + return {}; + }); + + if (secondaryResult) { + secondaryDir = secondaryResult; + } + + if (primaryDir == CSSKeyword::Top) { + if (secondaryDir == CSSKeyword::Left) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToTopLeft}; + } else if (secondaryDir == CSSKeyword::Right) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToTopRight}; + } else { + // "to top" = 0 degrees + return CSSLinearGradientDirection{CSSAngle{0.0f}}; + } + } else if (primaryDir == CSSKeyword::Bottom) { + if (secondaryDir == CSSKeyword::Left) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToBottomLeft}; + } else if (secondaryDir == CSSKeyword::Right) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToBottomRight}; + } else { + // "to bottom" = 180 degrees + return CSSLinearGradientDirection{CSSAngle{180.0f}}; + } + } else if (primaryDir == CSSKeyword::Left) { + if (secondaryDir == CSSKeyword::Top) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToTopLeft}; + } else if (secondaryDir == CSSKeyword::Bottom) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToBottomLeft}; + } else { + // "to left" = 270 degrees + return CSSLinearGradientDirection{CSSAngle{270.0f}}; + } + } else if (primaryDir == CSSKeyword::Right) { + if (secondaryDir == CSSKeyword::Top) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToTopRight}; + } else if (secondaryDir == CSSKeyword::Bottom) { + return CSSLinearGradientDirection{ + CSSLinearGradientDirectionKeyword::ToBottomRight}; + } else { + // "to right" = 90 degrees + return CSSLinearGradientDirection{CSSAngle{90.0f}}; + } + } + + return {}; + } +}; + +static_assert(CSSDataType); + +/** + * Representation of a color hint (interpolation hint) + */ +struct CSSColorHint { + std::variant + position{}; // Support both lengths and percentages + + bool operator==(const CSSColorHint& rhs) const { + return position == rhs.position; + } +}; + +template <> +struct CSSDataTypeParser { + static auto consume(CSSSyntaxParser& parser) -> std::optional { + return parseCSSColorHint(parser); + } + + private: + static std::optional parseCSSColorHint( + CSSSyntaxParser& parser) { + auto position = parseNextCSSValue(parser); + if (std::holds_alternative(position)) { + return CSSColorHint{std::get(position)}; + } else if (std::holds_alternative(position)) { + return CSSColorHint{std::get(position)}; + } + return {}; + } +}; + +static_assert(CSSDataType); + +struct CSSColorStop { + CSSColor color{}; + std::optional> startPosition{}; + std::optional> endPosition{}; + + bool operator==(const CSSColorStop& rhs) const { + if (color != rhs.color) { + return false; + } + + if (startPosition.has_value() != rhs.startPosition.has_value()) { + return false; + } + if (startPosition.has_value()) { + if (startPosition->index() != rhs.startPosition->index()) { + return false; + } + if (*startPosition != *rhs.startPosition) { + return false; + } + } + + if (endPosition.has_value() != rhs.endPosition.has_value()) { + return false; + } + if (endPosition.has_value()) { + if (endPosition->index() != rhs.endPosition->index()) { + return false; + } + if (*endPosition != *rhs.endPosition) { + return false; + } + } + + return true; + } +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consume(CSSSyntaxParser& parser) + -> std::optional { + return parseCSSColorStop(parser); + } + + private: + static constexpr std::optional parseCSSColorStop( + CSSSyntaxParser& parser) { + auto color = parseNextCSSValue(parser); + if (!std::holds_alternative(color)) { + return {}; + } + + CSSColorStop colorStop; + colorStop.color = std::get(color); + + auto startPosition = parseNextCSSValue( + parser, CSSDelimiter::Whitespace); + if (std::holds_alternative(startPosition)) { + colorStop.startPosition = std::get(startPosition); + } else if (std::holds_alternative(startPosition)) { + colorStop.startPosition = std::get(startPosition); + } + + if (colorStop.startPosition) { + // Try to parse second optional position (supports both lengths and + // percentages) + auto endPosition = parseNextCSSValue( + parser, CSSDelimiter::Whitespace); + if (std::holds_alternative(endPosition)) { + colorStop.endPosition = std::get(endPosition); + } else if (std::holds_alternative(endPosition)) { + colorStop.endPosition = std::get(endPosition); + } + } + return colorStop; + } +}; + +static_assert(CSSDataType); + +struct CSSLinearGradientFunction { + std::optional direction{}; + std::vector> + items{}; // Color stops and color hints + + bool operator==(const CSSLinearGradientFunction& rhs) const = default; + + static std::pair>, int> + parseGradientColorStopsAndHints(CSSSyntaxParser& parser) { + std::vector> items; + int colorStopCount = 0; + + std::optional prevColorStop = std::nullopt; + do { + auto colorStop = parseNextCSSValue(parser); + if (std::holds_alternative(colorStop)) { + auto parsedColorStop = std::get(colorStop); + items.emplace_back(parsedColorStop); + prevColorStop = parsedColorStop; + colorStopCount++; + } else { + auto colorHint = parseNextCSSValue(parser); + if (std::holds_alternative(colorHint)) { + // color hint must be between two color stops + if (!prevColorStop) { + return {}; + } + auto nextColorStop = + peekNextCSSValue(parser, CSSDelimiter::Comma); + if (!std::holds_alternative(nextColorStop)) { + return {}; + } + items.emplace_back(std::get(colorHint)); + } else { + break; // No more valid items + } + } + } while (parser.consumeDelimiter(CSSDelimiter::Comma)); + + return {items, colorStopCount}; + } +}; + +enum class CSSRadialGradientShape : uint8_t { + Circle, + Ellipse, +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consumePreservedToken(const CSSPreservedToken& token) + -> std::optional { + if (token.type() == CSSTokenType::Ident) { + auto lowercase = fnv1aLowercase(token.stringValue()); + if (lowercase == fnv1a("circle")) { + return CSSRadialGradientShape::Circle; + } else if (lowercase == fnv1a("ellipse")) { + return CSSRadialGradientShape::Ellipse; + } + } + return {}; + } +}; + +static_assert(CSSDataType); + +enum class CSSRadialGradientSizeKeyword : uint8_t { + ClosestSide, + ClosestCorner, + FarthestSide, + FarthestCorner, +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consumePreservedToken(const CSSPreservedToken& token) + -> std::optional { + if (token.type() == CSSTokenType::Ident) { + auto lowercase = fnv1aLowercase(token.stringValue()); + if (lowercase == fnv1a("closest-side")) { + return CSSRadialGradientSizeKeyword::ClosestSide; + } else if (lowercase == fnv1a("closest-corner")) { + return CSSRadialGradientSizeKeyword::ClosestCorner; + } else if (lowercase == fnv1a("farthest-side")) { + return CSSRadialGradientSizeKeyword::FarthestSide; + } else if (lowercase == fnv1a("farthest-corner")) { + return CSSRadialGradientSizeKeyword::FarthestCorner; + } + } + return {}; + } +}; + +static_assert(CSSDataType); + +struct CSSRadialGradientExplicitSize { + std::variant sizeX{}; + std::variant sizeY{}; + + bool operator==(const CSSRadialGradientExplicitSize& rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consume(CSSSyntaxParser& syntaxParser) + -> std::optional { + auto sizeX = parseNextCSSValue(syntaxParser); + if (std::holds_alternative(sizeX)) { + return {}; + } + + syntaxParser.consumeWhitespace(); + + auto sizeY = parseNextCSSValue(syntaxParser); + + CSSRadialGradientExplicitSize result; + if (std::holds_alternative(sizeX)) { + result.sizeX = std::get(sizeX); + } else { + result.sizeX = std::get(sizeX); + } + + if (std::holds_alternative(sizeY) || + std::holds_alternative(sizeY)) { + if (std::holds_alternative(sizeY)) { + result.sizeY = std::get(sizeY); + } else { + result.sizeY = std::get(sizeY); + } + } else { + result.sizeY = result.sizeX; + } + + return result; + } +}; + +static_assert(CSSDataType); + +using CSSRadialGradientSize = + std::variant; + +struct CSSRadialGradientPosition { + std::optional> top{}; + std::optional> bottom{}; + std::optional> left{}; + std::optional> right{}; + + bool operator==(const CSSRadialGradientPosition& rhs) const { + return top == rhs.top && bottom == rhs.bottom && left == rhs.left && + right == rhs.right; + } +}; + +struct CSSRadialGradientFunction { + std::optional shape{}; + std::optional size{}; + std::optional position{}; + std::vector> + items{}; // Color stops and color hints + + bool operator==(const CSSRadialGradientFunction& rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock( + const CSSFunctionBlock& func, + CSSSyntaxParser& parser) -> std::optional { + if (!iequals(func.name, "radial-gradient")) { + return {}; + } + + CSSRadialGradientFunction gradient; + + auto hasExplicitShape = false; + auto hasExplicitSingleSize = false; + auto shapeResult = parseNextCSSValue(parser); + if (std::holds_alternative(shapeResult)) { + parser.consumeWhitespace(); + } + + std::optional sizeResult; + + auto sizeKeywordResult = + parseNextCSSValue(parser); + + if (std::holds_alternative( + sizeKeywordResult)) { + sizeResult = CSSRadialGradientSize{ + std::get(sizeKeywordResult)}; + parser.consumeWhitespace(); + } else { + auto explicitSizeResult = + parseNextCSSValue(parser); + if (std::holds_alternative( + explicitSizeResult)) { + auto explicitSize = + std::get(explicitSizeResult); + // negative value validation + if (std::holds_alternative(explicitSize.sizeX)) { + const auto& lengthX = std::get(explicitSize.sizeX); + if (lengthX.value < 0) { + return {}; + } + } else if (std::holds_alternative(explicitSize.sizeX)) { + const auto& percentageX = std::get(explicitSize.sizeX); + if (percentageX.value < 0) { + return {}; + } + } + if (std::holds_alternative(explicitSize.sizeY)) { + const auto& lengthY = std::get(explicitSize.sizeY); + if (lengthY.value < 0) { + return {}; + } + } else if (std::holds_alternative(explicitSize.sizeY)) { + const auto& percentageY = std::get(explicitSize.sizeY); + if (percentageY.value < 0) { + return {}; + } + } + + // check if it's a single size (both X and Y are the same), we use it + // to set shape to circle + if (explicitSize.sizeX == explicitSize.sizeY) { + hasExplicitSingleSize = true; + } + + sizeResult = CSSRadialGradientSize{explicitSize}; + parser.consumeWhitespace(); + } + } + + if (std::holds_alternative(shapeResult)) { + gradient.shape = std::get(shapeResult); + hasExplicitShape = true; + } else { + // default to ellipse + gradient.shape = CSSRadialGradientShape::Ellipse; + } + + if (sizeResult.has_value()) { + gradient.size = *sizeResult; + } else { + // default to farthest corner + gradient.size = + CSSRadialGradientSize{CSSRadialGradientSizeKeyword::FarthestCorner}; + } + + if (!hasExplicitShape && hasExplicitSingleSize) { + gradient.shape = CSSRadialGradientShape::Circle; + } + + if (hasExplicitSingleSize && hasExplicitShape && + gradient.shape.value() == CSSRadialGradientShape::Ellipse) { + // if a single size is explicitly set and the shape is an ellipse do not + // apply any gradient. Same as web. + return {}; + } + + auto atResult = parser.consumeComponentValue( + [](const CSSPreservedToken& token) -> bool { + return token.type() == CSSTokenType::Ident && + fnv1aLowercase(token.stringValue()) == fnv1a("at"); + }); + + CSSRadialGradientPosition position; + + if (atResult) { + parser.consumeWhitespace(); + std::vector> + positionKeywordValues; + for (int i = 0; i < 2; i++) { + auto keywordFound = false; + auto valueFound = false; + + auto positionKeyword = + parser.consumeComponentValue>( + [](const CSSPreservedToken& token) + -> std::optional { + if (token.type() == CSSTokenType::Ident) { + auto keyword = std::string(token.stringValue()); + auto hash = fnv1aLowercase(keyword); + if (hash == fnv1a("top")) { + return CSSKeyword::Top; + } else if (hash == fnv1a("bottom")) { + return CSSKeyword::Bottom; + } else if (hash == fnv1a("left")) { + return CSSKeyword::Left; + } else if (hash == fnv1a("right")) { + return CSSKeyword::Right; + } else if (hash == fnv1a("center")) { + return CSSKeyword::Center; + } + } + return {}; + }); + + if (positionKeyword) { + // invalid position declaration of same keyword "at top 10% top 20%" + for (const auto& existingValue : positionKeywordValues) { + if (std::holds_alternative(existingValue)) { + if (std::get(existingValue) == positionKeyword) { + return {}; + } + } + } + positionKeywordValues.emplace_back(*positionKeyword); + keywordFound = true; + } + + parser.consumeWhitespace(); + + auto lengthPercentageValue = + parseNextCSSValue(parser); + + std::optional value; + if (std::holds_alternative(lengthPercentageValue)) { + value = std::get(lengthPercentageValue); + } else if (std::holds_alternative( + lengthPercentageValue)) { + value = std::get(lengthPercentageValue); + } + if (value.has_value()) { + positionKeywordValues.emplace_back(*value); + valueFound = true; + } + + parser.consumeWhitespace(); + + if (!keywordFound && !valueFound) { + break; + } + } + + if (positionKeywordValues.empty()) { + return {}; + } + + // 1. [ left | center | right | top | bottom | ] + if (positionKeywordValues.size() == 1) { + auto value = positionKeywordValues[0]; + if (std::holds_alternative(value)) { + auto keyword = std::get(value); + if (keyword == CSSKeyword::Left) { + position.top = CSSPercentage{50.0f}; + position.left = CSSPercentage{0.0f}; + } else if (keyword == CSSKeyword::Right) { + position.top = CSSPercentage{50.0f}; + position.left = CSSPercentage{100.0f}; + } else if (keyword == CSSKeyword::Top) { + position.top = CSSPercentage{0.0f}; + position.left = CSSPercentage{50.0f}; + } else if (keyword == CSSKeyword::Bottom) { + position.top = CSSPercentage{100.0f}; + position.left = CSSPercentage{50.0f}; + } else if (keyword == CSSKeyword::Center) { + position.left = CSSPercentage{50.0f}; + position.top = CSSPercentage{50.0f}; + } else { + return {}; + } + } else if ((std::holds_alternative(value) || + std::holds_alternative(value))) { + if (std::holds_alternative(value)) { + position.left = std::get(value); + } else { + position.left = std::get(value); + } + position.top = CSSPercentage{50.0f}; + } else { + return {}; + } + } + + else if (positionKeywordValues.size() == 2) { + auto value1 = positionKeywordValues[0]; + auto value2 = positionKeywordValues[1]; + // 2. [ left | center | right ] && [ top | center | bottom ] + if (std::holds_alternative(value1) && + std::holds_alternative(value2)) { + auto keyword1 = std::get(value1); + auto keyword2 = std::get(value2); + auto isHorizontal = [](CSSKeyword kw) { + return kw == CSSKeyword::Left || kw == CSSKeyword::Center || + kw == CSSKeyword::Right; + }; + auto isVertical = [](CSSKeyword kw) { + return kw == CSSKeyword::Top || kw == CSSKeyword::Center || + kw == CSSKeyword::Bottom; + }; + if (isHorizontal(keyword1) && isVertical(keyword2)) { + // First horizontal, second vertical + if (keyword1 == CSSKeyword::Left) { + position.left = CSSPercentage{0.0f}; + } else if (keyword1 == CSSKeyword::Right) { + position.right = CSSPercentage{0.0f}; + } else if (keyword1 == CSSKeyword::Center) { + position.left = CSSPercentage{50.0f}; + } + + if (keyword2 == CSSKeyword::Top) { + position.top = CSSPercentage{0.0f}; + } else if (keyword2 == CSSKeyword::Bottom) { + position.bottom = CSSPercentage{0.0f}; + } else if (keyword2 == CSSKeyword::Center) { + position.top = CSSPercentage{50.0f}; + } + } else if (isVertical(keyword1) && isHorizontal(keyword2)) { + // First vertical, second horizontal + if (keyword1 == CSSKeyword::Top) { + position.top = CSSPercentage{0.0f}; + } else if (keyword1 == CSSKeyword::Bottom) { + position.bottom = CSSPercentage{0.0f}; + } else if (keyword1 == CSSKeyword::Center) { + position.top = CSSPercentage{50.0f}; + } + + if (keyword2 == CSSKeyword::Left) { + position.left = CSSPercentage{0.0f}; + } else if (keyword2 == CSSKeyword::Right) { + position.left = CSSPercentage{100.0f}; + } else if (keyword2 == CSSKeyword::Center) { + position.left = CSSPercentage{50.0f}; + } + } else { + return {}; + } + } + // 3. [ left | center | right | ] [ top | center | + // bottom | ] + else { + if (std::holds_alternative(value1)) { + auto keyword1 = std::get(value1); + if (keyword1 == CSSKeyword::Left) { + position.left = CSSPercentage{0.0f}; + } else if (keyword1 == CSSKeyword::Right) { + position.right = CSSPercentage{0.0f}; + } else if (keyword1 == CSSKeyword::Center) { + position.left = CSSPercentage{50.0f}; + } else { + return {}; + } + } else if ((std::holds_alternative(value1) || + std::holds_alternative(value1))) { + if (std::holds_alternative(value1)) { + position.left = std::get(value1); + } else { + position.left = std::get(value1); + } + } else { + return {}; + } + + if (std::holds_alternative(value2)) { + auto keyword2 = std::get(value2); + if (keyword2 == CSSKeyword::Top) { + position.top = CSSPercentage{0.0f}; + } else if (keyword2 == CSSKeyword::Bottom) { + position.bottom = CSSPercentage{0.f}; + } else if (keyword2 == CSSKeyword::Center) { + position.top = CSSPercentage{50.0f}; + } else { + return {}; + } + } else if ((std::holds_alternative(value2) || + std::holds_alternative(value2))) { + if (std::holds_alternative(value2)) { + position.top = std::get(value2); + } else { + position.top = std::get(value2); + } + } else { + return {}; + } + } + } + + // 4. [ [ left | right ] ] && [ [ top | bottom ] + // ] + else if (positionKeywordValues.size() == 4) { + auto value1 = positionKeywordValues[0]; + auto value2 = positionKeywordValues[1]; + auto value3 = positionKeywordValues[2]; + auto value4 = positionKeywordValues[3]; + + if (!std::holds_alternative(value1)) { + return {}; + } + if (!std::holds_alternative(value3)) { + return {}; + } + if ((!std::holds_alternative(value2) && + !std::holds_alternative(value2))) { + return {}; + } + if ((!std::holds_alternative(value4) && + !std::holds_alternative(value4))) { + return {}; + } + + auto parsedValue2 = std::holds_alternative(value2) + ? std::variant{std::get( + value2)} + : std::variant{ + std::get(value2)}; + auto parsedValue4 = std::holds_alternative(value4) + ? std::variant{std::get( + value4)} + : std::variant{ + std::get(value4)}; + auto keyword1 = std::get(value1); + auto keyword3 = std::get(value3); + + if (keyword1 == CSSKeyword::Left) { + position.left = parsedValue2; + } else if (keyword1 == CSSKeyword::Right) { + position.right = parsedValue2; + } else if (keyword1 == CSSKeyword::Top) { + position.top = parsedValue2; + } else if (keyword1 == CSSKeyword::Bottom) { + position.bottom = parsedValue2; + } else { + return {}; + } + + if (keyword3 == CSSKeyword::Left) { + position.left = parsedValue4; + } else if (keyword3 == CSSKeyword::Right) { + position.right = parsedValue4; + } else if (keyword3 == CSSKeyword::Top) { + position.top = parsedValue4; + } else if (keyword3 == CSSKeyword::Bottom) { + position.bottom = parsedValue4; + } else { + return {}; + } + } else { + return {}; + } + + gradient.position = position; + } else { + // Default position + position.top = CSSPercentage{50.0f}; + position.left = CSSPercentage{50.0f}; + gradient.position = position; + } + + parser.consumeDelimiter(CSSDelimiter::Comma); + auto [items, colorStopCount] = + CSSLinearGradientFunction::parseGradientColorStopsAndHints(parser); + + if (items.empty() || colorStopCount < 2) { + return {}; + } + + gradient.items = std::move(items); + return gradient; + } +}; + +static_assert(CSSDataType); + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock( + const CSSFunctionBlock& func, + CSSSyntaxParser& parser) -> std::optional { + if (!iequals(func.name, "linear-gradient")) { + return {}; + } + + CSSLinearGradientFunction gradient; + + auto parsedDirection = + parseNextCSSValue(parser); + if (!std::holds_alternative(parsedDirection)) { + return {}; + } + + parser.consumeDelimiter(CSSDelimiter::Comma); + + gradient.direction = std::get(parsedDirection); + + auto [items, colorStopCount] = + CSSLinearGradientFunction::parseGradientColorStopsAndHints(parser); + + if (items.empty() || colorStopCount < 2) { + return {}; + } + + gradient.items = std::move(items); + + return gradient; + } +}; + +static_assert(CSSDataType); + +/** + * Representation of + * https://www.w3.org/TR/css-backgrounds-3/#background-image + */ +using CSSBackgroundImage = + CSSCompoundDataType; + +/** + * Variant of possible CSS background image types + */ +using CSSBackgroundImageVariant = CSSVariantWithTypes; + +/** + * Representation of + */ +using CSSBackgroundImageList = CSSCommaSeparatedList; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp new file mode 100644 index 00000000000000..9851cc87277f5e --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp @@ -0,0 +1,560 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +namespace facebook::react { + +namespace { + +CSSColorStop +makeCSSColorStop(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) { + return CSSColorStop{.color = CSSColor{.r = r, .g = g, .b = b, .a = a}}; +} + +} // namespace + +class CSSBackgroundImageTest : public ::testing::Test {}; + +TEST_F(CSSBackgroundImageTest, LinearGradientToRight) { + auto result = parseCSSProperty( + "linear-gradient(to right, red, blue)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 90.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientToBottomRight) { + auto result = parseCSSProperty( + "linear-gradient(to bottom right, red, blue)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{ + .value = CSSLinearGradientDirectionKeyword::ToBottomRight}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, EmptyStringReturnsEmptyArray) { + auto result = parseCSSProperty(""); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSBackgroundImageTest, InvalidValueReturnsEmptyArray) { + auto result = parseCSSProperty("linear-"); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithWhitespacesInDirection) { + auto result = parseCSSProperty( + "linear-gradient(to bottom right, red, blue)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{ + .value = CSSLinearGradientDirectionKeyword::ToBottomRight}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithRandomWhitespaces) { + auto result = parseCSSProperty( + " linear-gradient(to bottom right, red 30%, blue 80%) "); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{ + .value = CSSLinearGradientDirectionKeyword::ToBottomRight}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 30.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 80.0f}}, + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithAngle) { + auto result = + parseCSSProperty("linear-gradient(45deg, red, blue)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 45.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientCaseInsensitive) { + auto result = parseCSSProperty( + "LiNeAr-GradieNt(To Bottom, Red, Blue)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, MultipleLinearGradientsWithNewlines) { + auto result = parseCSSProperty( + "\n linear-gradient(to top, red, blue),\n linear-gradient(to bottom, green, yellow)"); + decltype(result) expected = CSSBackgroundImageList{ + {CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 0.0f}}, + .items = + {CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}}}}, + CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 128, .b = 0, .a = 255}}, + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 255, .b = 0, .a = 255}}, + }}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithMultipleColorStops) { + auto result = parseCSSProperty( + "linear-gradient(to bottom, red 0%, green 50%, blue 100%)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 0.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 128, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 50.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 100.0f}}, + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithColorStopEndPosition) { + auto result = parseCSSProperty( + "linear-gradient(red 10% 30%, blue 50%)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 10.0f}, + .endPosition = CSSPercentage{.value = 30.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 50.0f}}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientMixedPositionedStops) { + auto result = parseCSSProperty( + "linear-gradient(to right, red, green, blue 60%, yellow, purple)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 90.0f}}, + .items = { + makeCSSColorStop(255, 0, 0), + makeCSSColorStop(0, 128, 0), + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 60.0f}}, + makeCSSColorStop(255, 255, 0), + makeCSSColorStop(128, 0, 128), + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithHslColors) { + auto result = parseCSSProperty( + "linear-gradient(hsl(330, 100%, 45.1%), hsl(0, 100%, 50%))"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = {makeCSSColorStop(230, 0, 115), makeCSSColorStop(255, 0, 0)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithoutDirection) { + auto result = + parseCSSProperty("linear-gradient(#e66465, #9198e5)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + makeCSSColorStop(230, 100, 101), makeCSSColorStop(145, 152, 229)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientInvalidCases) { + const std::vector invalidInputs = { + "linear-gradient(45deg, rede, blue)", + "linear-gradient(45 deg, red, blue)", + "linear-gradient(to left2, red, blue)", + "linear-gradient(to left, red 5, blue)"}; + for (const auto& input : invalidInputs) { + const auto result = parseCSSProperty(input); + ASSERT_TRUE(std::holds_alternative(result)) + << "Input should be invalid: " << input; + } +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithMultipleTransitionHints) { + auto result = parseCSSProperty( + "linear-gradient(red, 20%, blue, 60%, green, 80%, yellow)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + makeCSSColorStop(255, 0, 0), + CSSColorHint{.position = CSSPercentage{.value = 20.0f}}, + makeCSSColorStop(0, 0, 255), + CSSColorHint{.position = CSSPercentage{.value = 60.0f}}, + makeCSSColorStop(0, 128, 0), + CSSColorHint{.position = CSSPercentage{.value = 80.0f}}, + makeCSSColorStop(255, 255, 0), + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, LinearGradientInvalidTransitionHints) { + const std::vector invalidInputs = { + // color hints must be between two color stops + "linear-gradient(red, 30%, blue, 60%, green, 80%)", + "linear-gradient(red, 30%, 60%, green)", + "linear-gradient(20%, red, green)"}; + for (const auto& input : invalidInputs) { + const auto result = parseCSSProperty(input); + ASSERT_TRUE(std::holds_alternative(result)) + << "Input should be invalid: " << input; + } +} + +TEST_F(CSSBackgroundImageTest, LinearGradientWithMixedUnits) { + auto result = parseCSSProperty( + "linear-gradient(red 10%, 20px, blue 30%, purple 40px)"); + decltype(result) expected = CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 10.0f}}, + CSSColorHint{ + .position = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 30.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 128, .g = 0, .b = 128, .a = 255}, + .startPosition = + CSSLength{.value = 40.0f, .unit = CSSLengthUnit::Px}}, + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientBasic) { + auto result = + parseCSSProperty("radial-gradient(red, blue)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Ellipse, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSPercentage{.value = 50.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientInferCircleFromSingleLength) { + auto result = + parseCSSProperty("radial-gradient(100px, red, blue)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = + CSSRadialGradientExplicitSize{ + .sizeX = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + .sizeY = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}}, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSPercentage{.value = 50.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientInferEllipseFromDoubleLength) { + auto result = parseCSSProperty( + "radial-gradient(100px 50px, red, blue)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Ellipse, + .size = + CSSRadialGradientExplicitSize{ + .sizeX = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + .sizeY = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}}, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSPercentage{.value = 50.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientExplicitShapeWithSize) { + auto result = parseCSSProperty( + "radial-gradient(circle 100px at center, red, blue 80%)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = + CSSRadialGradientExplicitSize{ + .sizeX = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}, + .sizeY = CSSLength{.value = 100.0f, .unit = CSSLengthUnit::Px}}, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSPercentage{.value = 50.0f}}, + .items = { + makeCSSColorStop(255, 0, 0), + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 80.0f}}}}; + ASSERT_EQ(result, expected); +} + +// 1. position syntax: [ left | center | right | top | bottom | +// ] +TEST_F(CSSBackgroundImageTest, RadialGradientPositionLengthSyntax) { + auto result = parseCSSProperty( + "radial-gradient(circle at 20px, red, blue)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} +// 2. position syntax: [ left | center | right ] && [ top | center | bottom ] +TEST_F(CSSBackgroundImageTest, RadialGradientPositionKeywordCombinations) { + const std::vector inputs = { + "radial-gradient(circle at left top, red, blue)", + "radial-gradient(circle at top left, red, blue)"}; + decltype(parseCSSProperty(inputs[0])) expected = + 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)}}; + for (const auto& input : inputs) { + auto result = parseCSSProperty(input); + ASSERT_EQ(result, expected) << "Failed for input: " << input; + } +} + +// 3. position syntax: [ left | center | right | ] [ top +// | center | bottom | ] +TEST_F(CSSBackgroundImageTest, RadialGradientComplexPositionSyntax) { + const std::vector> + testCases = { + { + "radial-gradient(circle at left 20px, red, blue)", + {.top = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .left = CSSPercentage{.value = 0.f}}, + }, + { + "radial-gradient(circle at 20px 20px, red, blue)", + {.top = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}}, + }, + { + "radial-gradient(circle at right 50px, red, blue)", + {.top = CSSLength{.value = 50.0f, .unit = CSSLengthUnit::Px}, + .right = CSSPercentage{.value = 0.f}}, + }}; + for (const auto& [input, expectedPosition] : testCases) { + const auto result = parseCSSProperty(input); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = expectedPosition, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); + } +} + +// 4. position syntax: [ [ left | right ] ] && [ [ top | +// bottom ] ] +TEST_F(CSSBackgroundImageTest, RadialGradientSeparatePositionPercentages) { + auto result = parseCSSProperty( + "radial-gradient(at top 0% right 10%, red, blue)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Ellipse, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 0.0f}, + .right = CSSPercentage{.value = 10.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientWithTransitionHints) { + auto result = parseCSSProperty( + "radial-gradient(circle, red 0%, 25%, blue 50%, 75%, green 100%)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSPercentage{.value = 50.0f}}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 0.0f}}, + CSSColorHint{.position = CSSPercentage{.value = 25.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 50.0f}}, + CSSColorHint{.position = CSSPercentage{.value = 75.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 128, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 100.0f}}, + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, MultipleGradientsRadialAndLinear) { + auto result = parseCSSProperty( + "radial-gradient(circle at top left, red, blue), 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)}}, + CSSLinearGradientFunction{ + .direction = + CSSLinearGradientDirection{.value = CSSAngle{.degrees = 180.0f}}, + .items = { + makeCSSColorStop(0, 128, 0), makeCSSColorStop(255, 255, 0)}}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientMixedCase) { + auto result = parseCSSProperty( + "RaDiAl-GrAdIeNt(CiRcLe ClOsEsT-sIdE aT cEnTeR, rEd, bLuE)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = CSSRadialGradientSizeKeyword::ClosestSide, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSPercentage{.value = 50.0f}}, + .items = {makeCSSColorStop(255, 0, 0), makeCSSColorStop(0, 0, 255)}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientWhitespaceVariations) { + auto result = parseCSSProperty( + "radial-gradient( circle farthest-corner at 25% 75% , red 0% , blue 100% )"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Circle, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 75.0f}, + .left = CSSPercentage{.value = 25.0f}}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 0.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 100.0f}}, + }}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientInvalidCases) { + const std::vector invalidInputs = { + "radial-gradient(circle at top leftt, red, blue)", + "radial-gradient(circle at, red, blue)", + "radial-gradient(ellipse 100px, red, blue)", + "radial-gradient(ellipse at top 20% top 50%, red, blue)"}; + for (const auto& input : invalidInputs) { + const auto result = parseCSSProperty(input); + ASSERT_TRUE(std::holds_alternative(result)) + << "Input should be invalid: " << input; + } +} + +TEST_F(CSSBackgroundImageTest, RadialGradientMultipleColorStops) { + auto result = parseCSSProperty( + "radial-gradient(red 0%, yellow 30%, green 60%, blue 100%)"); + decltype(result) expected = CSSRadialGradientFunction{ + .shape = CSSRadialGradientShape::Ellipse, + .size = CSSRadialGradientSizeKeyword::FarthestCorner, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 50.0f}, + .left = CSSPercentage{.value = 50.0f}}, + .items = { + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 0.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 255, .g = 255, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 30.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 128, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 60.0f}}, + CSSColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSPercentage{.value = 100.0f}}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, InvalidGradientFunctionName) { + const std::string input = + "aoeusntial-gradient(red 0%, yellow 30%, green 60%, blue 100%)"; + const auto result = parseCSSProperty(input); + ASSERT_TRUE(std::holds_alternative(result)); +} + +TEST_F(CSSBackgroundImageTest, RadialGradientNegativeRadius) { + const std::vector invalidInputs = { + "radial-gradient(circle -100px, red, blue)", + "radial-gradient(ellipse 100px -40px, red, blue)"}; + for (const auto& input : invalidInputs) { + const auto result = parseCSSProperty(input); + ASSERT_TRUE(std::holds_alternative(result)) + << "Input should be invalid: " << input; + } +} + +} // 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 a0b4d47210a6b1..1945f5e31e0d96 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h @@ -7,7 +7,6 @@ #pragma once -#include #include #include @@ -19,4 +18,27 @@ using BackgroundImage = std::variant; folly::dynamic toDynamic(const BackgroundImage& backgroundImage); #endif +#if RN_DEBUG_STRING_CONVERTIBLE +inline std::string toString(std::vector& value) { + std::stringstream ss; + + ss << "["; + for (size_t i = 0; i < value.size(); i++) { + if (i > 0) { + ss << ", "; + } + + const auto& backgroundImage = value[i]; + if (std::holds_alternative(backgroundImage)) { + std::get(backgroundImage).toString(ss); + } else if (std::holds_alternative(backgroundImage)) { + std::get(backgroundImage).toString(ss); + } + } + ss << "]"; + + return ss.str(); +} +#endif + }; // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Color.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/Color.cpp index 33cec4f0173b6b..22361dd9d02532 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Color.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Color.cpp @@ -7,8 +7,24 @@ #include "Color.h" +#include + namespace facebook::react { +std::string SharedColor::toString() const noexcept { + ColorComponents components = colorComponentsFromColor(*this); + std::array buffer{}; + std::snprintf( + buffer.data(), + buffer.size(), + "rgba(%.0f, %.0f, %.0f, %g)", + components.red * 255.f, + components.green * 255.f, + components.blue * 255.f, + components.alpha); + return buffer.data(); +} + bool isColorMeaningful(const SharedColor& color) noexcept { if (!color) { return false; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Color.h b/packages/react-native/ReactCommon/react/renderer/graphics/Color.h index ec614c55cdcd81..76445126b8b590 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Color.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Color.h @@ -7,11 +7,12 @@ #pragma once -#include - #include #include +#include +#include + #ifdef RN_SERIALIZABLE_STATE #include #endif @@ -52,6 +53,8 @@ class SharedColor { return color_ != HostPlatformColor::UndefinedColor; } + std::string toString() const noexcept; + private: Color color_; }; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h b/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h index ad1986fa50fc3e..1ff771bbf91a58 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h @@ -7,10 +7,12 @@ #pragma once +#include #include #include #include #include +#include namespace facebook::react { @@ -22,6 +24,16 @@ struct ColorStop { #ifdef RN_SERIALIZABLE_STATE folly::dynamic toDynamic() const; #endif + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream& ss) const { + ss << color.toString(); + if (position.unit != UnitType::Undefined) { + ss << " "; + ss << position.toString(); + } + } +#endif }; struct ProcessedColorStop { diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/DoubleConversions.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/DoubleConversions.cpp index 488f56bed235df..ce9d3435412b53 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/DoubleConversions.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/DoubleConversions.cpp @@ -29,7 +29,9 @@ std::string toString(double doubleValue, char suffix) { // Serialize infinite and NaN as 0 builder.AddCharacter('0'); } - builder.AddCharacter(suffix); + if (suffix != '\0') { + builder.AddCharacter(suffix); + } return builder.Finalize(); } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.cpp index 9097b2d8263902..6e590b28471f97 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.cpp @@ -56,4 +56,37 @@ folly::dynamic LinearGradient::toDynamic() const { } #endif +#if RN_DEBUG_STRING_CONVERTIBLE +void LinearGradient::toString(std::stringstream& ss) const { + ss << "linear-gradient("; + + if (direction.type == GradientDirectionType::Angle) { + ss << std::get(direction.value) << "deg"; + } else { + auto keyword = std::get(direction.value); + switch (keyword) { + case GradientKeyword::ToTopRight: + ss << "to top right"; + break; + case GradientKeyword::ToBottomRight: + ss << "to bottom right"; + break; + case GradientKeyword::ToTopLeft: + ss << "to top left"; + break; + case GradientKeyword::ToBottomLeft: + ss << "to bottom left"; + break; + } + } + + for (const auto& colorStop : colorStops) { + ss << ", "; + colorStop.toString(ss); + } + + ss << ")"; +} +#endif + }; // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h index 368237530d898f..5d356f7e6ed57d 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h @@ -7,11 +7,12 @@ #pragma once +#include #include #include #include -#include -#include + +#include #include #include @@ -50,18 +51,10 @@ struct LinearGradient { #ifdef RN_SERIALIZABLE_STATE folly::dynamic toDynamic() const; #endif -}; -inline GradientKeyword parseGradientKeyword(const std::string& keyword) { - if (keyword == "to top right") - return GradientKeyword::ToTopRight; - if (keyword == "to bottom right") - return GradientKeyword::ToBottomRight; - if (keyword == "to top left") - return GradientKeyword::ToTopLeft; - if (keyword == "to bottom left") - return GradientKeyword::ToBottomLeft; - throw std::invalid_argument("Invalid gradient keyword: " + keyword); -} +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream& ss) const; +#endif +}; }; // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.cpp index f5c2a2053ca809..1194543a26411c 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.cpp @@ -7,6 +7,10 @@ #include "RadialGradient.h" +#if RN_DEBUG_STRING_CONVERTIBLE +#include +#endif + namespace facebook::react { #ifdef RN_SERIALIZABLE_STATE @@ -80,4 +84,55 @@ folly::dynamic RadialGradient::toDynamic() const { return result; } #endif + +#if RN_DEBUG_STRING_CONVERTIBLE +void RadialGradient::toString(std::stringstream& ss) const { + ss << "radial-gradient(" + << (shape == RadialGradientShape::Circle ? "circle" : "ellipse") << " "; + + if (std::holds_alternative(size.value)) { + auto& keyword = std::get(size.value); + switch (keyword) { + case RadialGradientSize::SizeKeyword::ClosestSide: + ss << "closest-side"; + break; + case RadialGradientSize::SizeKeyword::FarthestSide: + ss << "farthest-side"; + break; + case RadialGradientSize::SizeKeyword::ClosestCorner: + ss << "closest-corner"; + break; + case RadialGradientSize::SizeKeyword::FarthestCorner: + ss << "farthest-corner"; + break; + } + } else { + auto& dimensions = std::get(size.value); + ss << react::toString(dimensions.x) << " " << react::toString(dimensions.y); + } + + ss << " at "; + + if (position.left.has_value()) { + ss << position.left->toString() << " "; + } + if (position.top.has_value()) { + ss << position.top->toString() << " "; + } + if (position.right.has_value()) { + ss << position.right->toString() << " "; + } + if (position.bottom.has_value()) { + ss << position.bottom->toString() << " "; + } + + for (const auto& colorStop : colorStops) { + ss << ", "; + colorStop.toString(ss); + } + + ss << ")"; +} +#endif + }; // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h b/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h index 28e01df250117e..20ff53f48c6459 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h @@ -7,10 +7,12 @@ #pragma once +#include #include #include #include #include +#include #include #include @@ -37,6 +39,7 @@ struct RadialGradientSize { bool operator==(const Dimensions& other) const { return x == other.x && y == other.y; } + bool operator!=(const Dimensions& other) const { return !(*this == other); } @@ -49,15 +52,7 @@ struct RadialGradientSize { std::variant value; bool operator==(const RadialGradientSize& other) const { - if (std::holds_alternative(value) && - std::holds_alternative(other.value)) { - return std::get(value) == std::get(other.value); - } else if ( - std::holds_alternative(value) && - std::holds_alternative(other.value)) { - return std::get(value) == std::get(other.value); - } - return false; + return value == other.value; } bool operator!=(const RadialGradientSize& other) const { @@ -106,6 +101,9 @@ struct RadialGradient { #ifdef RN_SERIALIZABLE_STATE folly::dynamic toDynamic() const; #endif -}; +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream& ss) const; +#endif +}; }; // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.cpp index a8eabd5546883d..a44cadc6e95025 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.cpp @@ -7,14 +7,11 @@ #include "ValueUnit.h" -#ifdef RN_SERIALIZABLE_STATE #include "DoubleConversions.h" -#endif namespace facebook::react { #ifdef RN_SERIALIZABLE_STATE - folly::dynamic ValueUnit::toDynamic() const { switch (unit) { case UnitType::Undefined: @@ -22,11 +19,23 @@ folly::dynamic ValueUnit::toDynamic() const { case UnitType::Point: return value; case UnitType::Percent: - return toString(value, '%'); + return react::toString(value, '%'); default: return nullptr; } } #endif +#if RN_DEBUG_STRING_CONVERTIBLE +std::string ValueUnit::toString() const { + if (unit == UnitType::Percent) { + return react::toString(value, '%'); + } else if (unit == UnitType::Point) { + return react::toString(value, '\0') + "px"; + } else { + return "undefined"; + } +} +#endif + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h index 11936e97950041..a7d68fc18ce0d2 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h @@ -7,6 +7,9 @@ #pragma once +#include +#include + #ifdef RN_SERIALIZABLE_STATE #include #endif @@ -29,6 +32,7 @@ struct ValueUnit { bool operator==(const ValueUnit& other) const { return value == other.value && unit == other.unit; } + bool operator!=(const ValueUnit& other) const { return !(*this == other); } @@ -52,5 +56,9 @@ struct ValueUnit { #ifdef RN_SERIALIZABLE_STATE folly::dynamic toDynamic() const; #endif + +#if RN_DEBUG_STRING_CONVERTIBLE + std::string toString() const; +#endif }; } // namespace facebook::react diff --git a/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js b/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js index ed787dfc4e2371..ed63414a933a79 100644 --- a/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js +++ b/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js @@ -57,7 +57,7 @@ exports.examples = [ return ( Linear Gradient @@ -73,7 +73,7 @@ exports.examples = [ return (