feat: background image native CSS parser (#53609)

Summary:
This PR adds native CSS parser for `backgroundImage` property. Currently, it supports linear-gradient and radial-gradient spec compliant CSS syntax.

## Changelog:

[GENERAL] [ADDED] - background image native parser.

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: https://github.com/facebook/react-native/pull/53609

Test Plan:
- Replicated existing testcases from JS. Currently i've added CSS syntax testcases. Checkout `CSSBackgroundImageTest.cpp`

### Verify example screens in RNTester
- Set `enableNativeCSSParsing` to true in `ReactNativeFeatureFlags.config.js` and run `yarn featureflags --update`
- Rebuild the project and verify `LinearGradientExample` and `RadialGradientExample` screens on both platforms.

### Notes
- Currently it is difficult to run CSS renderer tests. I made a custom cmake config to get it working, some steps would be helpful.
- Right now the new CSS renderer seems to be only working on iOS. NickGerleman mentioned there is some WIP to get it working on android. So please test this PR on iOS.

Reviewed By: mdvacca

Differential Revision: D83341309

Pulled By: javache

fbshipit-source-id: 91b88e3df164766c1f0021283697b1e5f9b44bfc
This commit is contained in:
nishan (o^▽^o)
2025-10-20 07:00:42 -07:00
committed by meta-codesync[bot]
parent 4c4270d6c7
commit a9780f9102
12 changed files with 2159 additions and 226 deletions
@@ -146,8 +146,9 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
/**
* Linear Gradient
*/
experimental_backgroundImage: {process: processBackgroundImage},
experimental_backgroundImage: ReactNativeFeatureFlags.enableNativeCSSParsing()
? true
: {process: processBackgroundImage},
/**
* View
*/
@@ -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},
@@ -267,6 +267,10 @@ static std::vector<ProcessedColorStop> processColorTransitionHints(const std::ve
+ (std::vector<ProcessedColorStop>)getFixedColorStops:(const std::vector<ColorStop> &)colorStops
gradientLineLength:(CGFloat)gradientLineLength
{
if (colorStops.empty()) {
return {};
}
std::vector<ProcessedColorStop> fixedColorStops(colorStops.size());
bool hasNullPositions = false;
auto maxPositionSoFar = resolveColorStopPosition(colorStops[0].position, gradientLineLength);
@@ -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 <glog/logging.h>
#include <react/debug/react_native_expect.h>
#include <react/renderer/components/view/CSSConversions.h>
#include <react/renderer/components/view/conversions.h>
#include <react/renderer/css/CSSBackgroundImage.h>
#include <react/renderer/css/CSSLengthUnit.h>
#include <react/renderer/css/CSSPercentage.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/renderer/graphics/ColorStop.h>
#include <react/renderer/graphics/LinearGradient.h>
#include <react/renderer/graphics/RadialGradient.h>
#include <react/renderer/graphics/ValueUnit.h>
namespace facebook::react {
using RawValueMap = std::unordered_map<std::string, RawValue>;
using RawValueList = std::vector<RawValue>;
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<BackgroundImage>& result) {
react_native_expect(value.hasType<RawValueList>());
if (!value.hasType<RawValueList>()) {
result = {};
return;
}
std::vector<BackgroundImage> backgroundImage{};
auto rawBackgroundImage = static_cast<RawValueList>(value);
for (const auto& rawBackgroundImageValue : rawBackgroundImage) {
bool isMap = rawBackgroundImageValue.hasType<RawValueMap>();
react_native_expect(isMap);
if (!isMap) {
result = {};
return;
}
auto rawBackgroundImageMap =
static_cast<RawValueMap>(rawBackgroundImageValue);
auto typeIt = rawBackgroundImageMap.find("type");
if (typeIt == rawBackgroundImageMap.end() ||
!typeIt->second.hasType<std::string>()) {
continue;
}
std::string type = (std::string)(typeIt->second);
std::vector<ColorStop> colorStops;
auto colorStopsIt = rawBackgroundImageMap.find("colorStops");
if (colorStopsIt != rawBackgroundImageMap.end() &&
colorStopsIt->second.hasType<RawValueList>()) {
auto rawColorStops = static_cast<RawValueList>(colorStopsIt->second);
for (const auto& stop : rawColorStops) {
if (stop.hasType<RawValueMap>()) {
auto stopMap = static_cast<RawValueMap>(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<RawValueMap>()) {
auto directionMap = static_cast<RawValueMap>(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<Float>()) {
linearGradient.direction.value = (Float)(valueIt->second);
}
} else if (directionType == "keyword") {
linearGradient.direction.type = GradientDirectionType::Keyword;
if (valueIt->second.hasType<std::string>()) {
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<std::string>()) {
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<std::string>()) {
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<RawValueMap>()) {
auto sizeMap = static_cast<RawValueMap>(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<RawValueMap>()) {
auto positionMap = static_cast<RawValueMap>(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<RawValue>& value,
std::vector<BackgroundImage>& result) {
std::vector<BackgroundImage> backgroundImage{};
for (const auto& rawBackgroundImageValue : value) {
bool isMap = rawBackgroundImageValue.hasType<RawValueMap>();
react_native_expect(isMap);
if (!isMap) {
result = {};
return;
}
auto rawBackgroundImageMap =
static_cast<RawValueMap>(rawBackgroundImageValue);
auto typeIt = rawBackgroundImageMap.find("type");
if (typeIt == rawBackgroundImageMap.end() ||
!typeIt->second.hasType<std::string>()) {
continue;
}
std::string type = (std::string)(typeIt->second);
std::vector<ColorStop> colorStops;
auto colorStopsIt = rawBackgroundImageMap.find("colorStops");
if (colorStopsIt != rawBackgroundImageMap.end() &&
colorStopsIt->second.hasType<RawValueList>()) {
auto rawColorStops = static_cast<RawValueList>(colorStopsIt->second);
for (const auto& stop : rawColorStops) {
if (stop.hasType<RawValueMap>()) {
auto stopMap = static_cast<RawValueMap>(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<RawValueList>() &&
static_cast<RawValueList>(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<RawValueList>()) {
auto positions = static_cast<RawValueList>(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>()) {
std::string directionStr = (std::string)(directionIt->second);
auto cssDirection =
parseCSSProperty<CSSLinearGradientDirection>(directionStr);
if (std::holds_alternative<CSSLinearGradientDirection>(
cssDirection)) {
const auto& direction =
std::get<CSSLinearGradientDirection>(cssDirection);
if (std::holds_alternative<CSSAngle>(direction.value)) {
linearGradient.direction.type = GradientDirectionType::Angle;
linearGradient.direction.value =
std::get<CSSAngle>(direction.value).degrees;
} else if (std::holds_alternative<
CSSLinearGradientDirectionKeyword>(
direction.value)) {
linearGradient.direction.type = GradientDirectionType::Keyword;
auto keyword =
std::get<CSSLinearGradientDirectionKeyword>(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<std::string>()) {
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<std::string>()) {
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<RawValueMap>()) {
auto sizeMap = static_cast<RawValueMap>(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<RawValueMap>()) {
auto positionMap = static_cast<RawValueMap>(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<CSSLength, CSSPercentage>& value) {
if (std::holds_alternative<CSSLength>(value)) {
return {std::get<CSSLength>(value).value, UnitType::Point};
} else {
return {std::get<CSSPercentage>(value).value, UnitType::Percent};
}
}
void fromCSSColorStop(
const std::variant<CSSColorStop, CSSColorHint>& item,
std::vector<ColorStop>& colorStops) {
if (std::holds_alternative<CSSColorStop>(item)) {
const auto& colorStop = std::get<CSSColorStop>(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<CSSColorHint>(item)) {
const auto& colorHint = std::get<CSSColorHint>(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<BackgroundImage> fromCSSBackgroundImage(
const CSSBackgroundImageVariant& cssBackgroundImage) {
if (std::holds_alternative<CSSLinearGradientFunction>(cssBackgroundImage)) {
const auto& gradient =
std::get<CSSLinearGradientFunction>(cssBackgroundImage);
LinearGradient linearGradient;
if (gradient.direction.has_value()) {
if (std::holds_alternative<CSSAngle>(gradient.direction->value)) {
const auto& angle = std::get<CSSAngle>(gradient.direction->value);
linearGradient.direction.type = GradientDirectionType::Angle;
linearGradient.direction.value = angle.degrees;
} else if (std::holds_alternative<CSSLinearGradientDirectionKeyword>(
gradient.direction->value)) {
const auto& dirKeyword = std::get<CSSLinearGradientDirectionKeyword>(
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<CSSRadialGradientFunction>(
cssBackgroundImage)) {
const auto& gradient =
std::get<CSSRadialGradientFunction>(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<CSSRadialGradientSizeKeyword>(
*gradient.size)) {
const auto& sizeKeyword =
std::get<CSSRadialGradientSizeKeyword>(*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<CSSRadialGradientExplicitSize>(
*gradient.size)) {
const auto& explicitSize =
std::get<CSSRadialGradientExplicitSize>(*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<BackgroundImage>& result) {
auto backgroundImageList = parseCSSProperty<CSSBackgroundImageList>(value);
if (!std::holds_alternative<CSSBackgroundImageList>(backgroundImageList)) {
result = {};
return;
}
std::vector<BackgroundImage> backgroundImages;
for (const auto& cssBackgroundImage :
std::get<CSSBackgroundImageList>(backgroundImageList)) {
if (auto backgroundImage = fromCSSBackgroundImage(cssBackgroundImage)) {
backgroundImages.push_back(*backgroundImage);
} else {
result = {};
return;
}
}
result = backgroundImages;
}
} // namespace facebook::react
@@ -0,0 +1,51 @@
/*
* 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 <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/RawProps.h>
#include <react/renderer/graphics/BackgroundImage.h>
#include <string>
namespace facebook::react {
void parseProcessedBackgroundImage(
const PropsParserContext& context,
const RawValue& value,
std::vector<BackgroundImage>& result);
void parseUnprocessedBackgroundImageList(
const PropsParserContext& context,
const std::vector<RawValue>& value,
std::vector<BackgroundImage>& result);
void parseUnprocessedBackgroundImageString(
const std::string& value,
std::vector<BackgroundImage>& result);
inline void fromRawValue(
const PropsParserContext& context,
const RawValue& value,
std::vector<BackgroundImage>& result) {
if (ReactNativeFeatureFlags::enableNativeCSSParsing()) {
if (value.hasType<std::string>()) {
parseUnprocessedBackgroundImageString((std::string)value, result);
} else if (value.hasType<std::vector<RawValue>>()) {
parseUnprocessedBackgroundImageList(
context, (std::vector<RawValue>)value, result);
} else {
result = {};
}
} else {
parseProcessedBackgroundImage(context, value, result);
}
}
} // namespace facebook::react
@@ -10,6 +10,7 @@
#include <algorithm>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/components/view/BackgroundImagePropsConversions.h>
#include <react/renderer/components/view/BoxShadowPropsConversions.h>
#include <react/renderer/components/view/FilterPropsConversions.h>
#include <react/renderer/components/view/conversions.h>
@@ -18,7 +18,6 @@
#include <react/renderer/css/CSSNumber.h>
#include <react/renderer/css/CSSPercentage.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/renderer/graphics/BackgroundImage.h>
#include <react/renderer/graphics/BlendMode.h>
#include <react/renderer/graphics/Isolation.h>
#include <react/renderer/graphics/LinearGradient.h>
@@ -1205,197 +1204,6 @@ inline void fromRawValue(
result = blendMode.value();
}
inline void fromRawValue(
const PropsParserContext& context,
const RawValue& value,
std::vector<BackgroundImage>& result) {
react_native_expect(value.hasType<std::vector<RawValue>>());
if (!value.hasType<std::vector<RawValue>>()) {
result = {};
return;
}
std::vector<BackgroundImage> backgroundImage{};
auto rawBackgroundImage = static_cast<std::vector<RawValue>>(value);
for (const auto& rawBackgroundImageValue : rawBackgroundImage) {
bool isMap = rawBackgroundImageValue
.hasType<std::unordered_map<std::string, RawValue>>();
react_native_expect(isMap);
if (!isMap) {
result = {};
return;
}
auto rawBackgroundImageMap =
static_cast<std::unordered_map<std::string, RawValue>>(
rawBackgroundImageValue);
auto typeIt = rawBackgroundImageMap.find("type");
if (typeIt == rawBackgroundImageMap.end() ||
!typeIt->second.hasType<std::string>()) {
continue;
}
std::string type = (std::string)(typeIt->second);
std::vector<ColorStop> colorStops;
auto colorStopsIt = rawBackgroundImageMap.find("colorStops");
if (colorStopsIt != rawBackgroundImageMap.end() &&
colorStopsIt->second.hasType<std::vector<RawValue>>()) {
auto rawColorStops =
static_cast<std::vector<RawValue>>(colorStopsIt->second);
for (const auto& stop : rawColorStops) {
if (stop.hasType<std::unordered_map<std::string, RawValue>>()) {
auto stopMap =
static_cast<std::unordered_map<std::string, RawValue>>(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<std::unordered_map<std::string, RawValue>>()) {
auto directionMap =
static_cast<std::unordered_map<std::string, RawValue>>(
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<Float>()) {
linearGradient.direction.value = (Float)(valueIt->second);
}
} else if (directionType == "keyword") {
linearGradient.direction.type = GradientDirectionType::Keyword;
if (valueIt->second.hasType<std::string>()) {
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<std::string>()) {
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<std::string>()) {
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<std::unordered_map<std::string, RawValue>>()) {
auto sizeMap = static_cast<std::unordered_map<std::string, RawValue>>(
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<std::unordered_map<std::string, RawValue>>()) {
auto positionMap =
static_cast<std::unordered_map<std::string, RawValue>>(
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,
@@ -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 <optional>
#include <variant>
#include <react/renderer/css/CSSAngle.h>
#include <react/renderer/css/CSSColor.h>
#include <react/renderer/css/CSSCompoundDataType.h>
#include <react/renderer/css/CSSDataType.h>
#include <react/renderer/css/CSSLength.h>
#include <react/renderer/css/CSSLengthPercentage.h>
#include <react/renderer/css/CSSList.h>
#include <react/renderer/css/CSSPercentage.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/utils/TemplateStringLiteral.h>
#include <react/utils/fnv1a.h>
#include <react/utils/iequals.h>
namespace facebook::react {
enum class CSSLinearGradientDirectionKeyword : uint8_t {
ToTopLeft,
ToTopRight,
ToBottomLeft,
ToBottomRight,
};
struct CSSLinearGradientDirection {
// angle or keyword like "to bottom"
std::variant<CSSAngle, CSSLinearGradientDirectionKeyword> value;
bool operator==(const CSSLinearGradientDirection& rhs) const = default;
};
template <>
struct CSSDataTypeParser<CSSLinearGradientDirection> {
static constexpr auto consume(CSSSyntaxParser& parser)
-> std::optional<CSSLinearGradientDirection> {
return parseLinearGradientDirection(parser);
}
private:
static constexpr std::optional<CSSLinearGradientDirection>
parseLinearGradientDirection(CSSSyntaxParser& parser) {
auto angle = parseNextCSSValue<CSSAngle>(parser);
if (std::holds_alternative<CSSAngle>(angle)) {
return CSSLinearGradientDirection{std::get<CSSAngle>(angle)};
}
auto toResult = parser.consumeComponentValue<bool>(
[](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<CSSKeyword> primaryDir;
auto primaryResult =
parser.consumeComponentValue<std::optional<CSSKeyword>>(
[](const CSSPreservedToken& token) -> std::optional<CSSKeyword> {
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<CSSKeyword> secondaryDir;
auto secondaryResult =
parser.consumeComponentValue<std::optional<CSSKeyword>>(
[&](const CSSPreservedToken& token) -> std::optional<CSSKeyword> {
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<CSSLinearGradientDirection>);
/**
* Representation of a color hint (interpolation hint)
*/
struct CSSColorHint {
std::variant<CSSLength, CSSPercentage>
position{}; // Support both lengths and percentages
bool operator==(const CSSColorHint& rhs) const {
return position == rhs.position;
}
};
template <>
struct CSSDataTypeParser<CSSColorHint> {
static auto consume(CSSSyntaxParser& parser) -> std::optional<CSSColorHint> {
return parseCSSColorHint(parser);
}
private:
static std::optional<CSSColorHint> parseCSSColorHint(
CSSSyntaxParser& parser) {
auto position = parseNextCSSValue<CSSLengthPercentage>(parser);
if (std::holds_alternative<CSSLength>(position)) {
return CSSColorHint{std::get<CSSLength>(position)};
} else if (std::holds_alternative<CSSPercentage>(position)) {
return CSSColorHint{std::get<CSSPercentage>(position)};
}
return {};
}
};
static_assert(CSSDataType<CSSColorHint>);
struct CSSColorStop {
CSSColor color{};
std::optional<std::variant<CSSLength, CSSPercentage>> startPosition{};
std::optional<std::variant<CSSLength, CSSPercentage>> 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<CSSColorStop> {
static constexpr auto consume(CSSSyntaxParser& parser)
-> std::optional<CSSColorStop> {
return parseCSSColorStop(parser);
}
private:
static constexpr std::optional<CSSColorStop> parseCSSColorStop(
CSSSyntaxParser& parser) {
auto color = parseNextCSSValue<CSSColor>(parser);
if (!std::holds_alternative<CSSColor>(color)) {
return {};
}
CSSColorStop colorStop;
colorStop.color = std::get<CSSColor>(color);
auto startPosition = parseNextCSSValue<CSSLengthPercentage>(
parser, CSSDelimiter::Whitespace);
if (std::holds_alternative<CSSLength>(startPosition)) {
colorStop.startPosition = std::get<CSSLength>(startPosition);
} else if (std::holds_alternative<CSSPercentage>(startPosition)) {
colorStop.startPosition = std::get<CSSPercentage>(startPosition);
}
if (colorStop.startPosition) {
// Try to parse second optional position (supports both lengths and
// percentages)
auto endPosition = parseNextCSSValue<CSSLengthPercentage>(
parser, CSSDelimiter::Whitespace);
if (std::holds_alternative<CSSLength>(endPosition)) {
colorStop.endPosition = std::get<CSSLength>(endPosition);
} else if (std::holds_alternative<CSSPercentage>(endPosition)) {
colorStop.endPosition = std::get<CSSPercentage>(endPosition);
}
}
return colorStop;
}
};
static_assert(CSSDataType<CSSColorStop>);
struct CSSLinearGradientFunction {
std::optional<CSSLinearGradientDirection> direction{};
std::vector<std::variant<CSSColorStop, CSSColorHint>>
items{}; // Color stops and color hints
bool operator==(const CSSLinearGradientFunction& rhs) const = default;
static std::pair<std::vector<std::variant<CSSColorStop, CSSColorHint>>, int>
parseGradientColorStopsAndHints(CSSSyntaxParser& parser) {
std::vector<std::variant<CSSColorStop, CSSColorHint>> items;
int colorStopCount = 0;
std::optional<CSSColorStop> prevColorStop = std::nullopt;
do {
auto colorStop = parseNextCSSValue<CSSColorStop>(parser);
if (std::holds_alternative<CSSColorStop>(colorStop)) {
auto parsedColorStop = std::get<CSSColorStop>(colorStop);
items.emplace_back(parsedColorStop);
prevColorStop = parsedColorStop;
colorStopCount++;
} else {
auto colorHint = parseNextCSSValue<CSSColorHint>(parser);
if (std::holds_alternative<CSSColorHint>(colorHint)) {
// color hint must be between two color stops
if (!prevColorStop) {
return {};
}
auto nextColorStop =
peekNextCSSValue<CSSColorStop>(parser, CSSDelimiter::Comma);
if (!std::holds_alternative<CSSColorStop>(nextColorStop)) {
return {};
}
items.emplace_back(std::get<CSSColorHint>(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<CSSRadialGradientShape> {
static constexpr auto consumePreservedToken(const CSSPreservedToken& token)
-> std::optional<CSSRadialGradientShape> {
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<CSSRadialGradientShape>);
enum class CSSRadialGradientSizeKeyword : uint8_t {
ClosestSide,
ClosestCorner,
FarthestSide,
FarthestCorner,
};
template <>
struct CSSDataTypeParser<CSSRadialGradientSizeKeyword> {
static constexpr auto consumePreservedToken(const CSSPreservedToken& token)
-> std::optional<CSSRadialGradientSizeKeyword> {
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<CSSRadialGradientSizeKeyword>);
struct CSSRadialGradientExplicitSize {
std::variant<CSSLength, CSSPercentage> sizeX{};
std::variant<CSSLength, CSSPercentage> sizeY{};
bool operator==(const CSSRadialGradientExplicitSize& rhs) const = default;
};
template <>
struct CSSDataTypeParser<CSSRadialGradientExplicitSize> {
static auto consume(CSSSyntaxParser& syntaxParser)
-> std::optional<CSSRadialGradientExplicitSize> {
auto sizeX = parseNextCSSValue<CSSLengthPercentage>(syntaxParser);
if (std::holds_alternative<std::monostate>(sizeX)) {
return {};
}
syntaxParser.consumeWhitespace();
auto sizeY = parseNextCSSValue<CSSLengthPercentage>(syntaxParser);
CSSRadialGradientExplicitSize result;
if (std::holds_alternative<CSSLength>(sizeX)) {
result.sizeX = std::get<CSSLength>(sizeX);
} else {
result.sizeX = std::get<CSSPercentage>(sizeX);
}
if (std::holds_alternative<CSSLength>(sizeY) ||
std::holds_alternative<CSSPercentage>(sizeY)) {
if (std::holds_alternative<CSSLength>(sizeY)) {
result.sizeY = std::get<CSSLength>(sizeY);
} else {
result.sizeY = std::get<CSSPercentage>(sizeY);
}
} else {
result.sizeY = result.sizeX;
}
return result;
}
};
static_assert(CSSDataType<CSSRadialGradientExplicitSize>);
using CSSRadialGradientSize =
std::variant<CSSRadialGradientSizeKeyword, CSSRadialGradientExplicitSize>;
struct CSSRadialGradientPosition {
std::optional<std::variant<CSSLength, CSSPercentage>> top{};
std::optional<std::variant<CSSLength, CSSPercentage>> bottom{};
std::optional<std::variant<CSSLength, CSSPercentage>> left{};
std::optional<std::variant<CSSLength, CSSPercentage>> right{};
bool operator==(const CSSRadialGradientPosition& rhs) const {
return top == rhs.top && bottom == rhs.bottom && left == rhs.left &&
right == rhs.right;
}
};
struct CSSRadialGradientFunction {
std::optional<CSSRadialGradientShape> shape{};
std::optional<CSSRadialGradientSize> size{};
std::optional<CSSRadialGradientPosition> position{};
std::vector<std::variant<CSSColorStop, CSSColorHint>>
items{}; // Color stops and color hints
bool operator==(const CSSRadialGradientFunction& rhs) const = default;
};
template <>
struct CSSDataTypeParser<CSSRadialGradientFunction> {
static auto consumeFunctionBlock(
const CSSFunctionBlock& func,
CSSSyntaxParser& parser) -> std::optional<CSSRadialGradientFunction> {
if (!iequals(func.name, "radial-gradient")) {
return {};
}
CSSRadialGradientFunction gradient;
auto hasExplicitShape = false;
auto hasExplicitSingleSize = false;
auto shapeResult = parseNextCSSValue<CSSRadialGradientShape>(parser);
if (std::holds_alternative<CSSRadialGradientShape>(shapeResult)) {
parser.consumeWhitespace();
}
std::optional<CSSRadialGradientSize> sizeResult;
auto sizeKeywordResult =
parseNextCSSValue<CSSRadialGradientSizeKeyword>(parser);
if (std::holds_alternative<CSSRadialGradientSizeKeyword>(
sizeKeywordResult)) {
sizeResult = CSSRadialGradientSize{
std::get<CSSRadialGradientSizeKeyword>(sizeKeywordResult)};
parser.consumeWhitespace();
} else {
auto explicitSizeResult =
parseNextCSSValue<CSSRadialGradientExplicitSize>(parser);
if (std::holds_alternative<CSSRadialGradientExplicitSize>(
explicitSizeResult)) {
auto explicitSize =
std::get<CSSRadialGradientExplicitSize>(explicitSizeResult);
// negative value validation
if (std::holds_alternative<CSSLength>(explicitSize.sizeX)) {
const auto& lengthX = std::get<CSSLength>(explicitSize.sizeX);
if (lengthX.value < 0) {
return {};
}
} else if (std::holds_alternative<CSSPercentage>(explicitSize.sizeX)) {
const auto& percentageX = std::get<CSSPercentage>(explicitSize.sizeX);
if (percentageX.value < 0) {
return {};
}
}
if (std::holds_alternative<CSSLength>(explicitSize.sizeY)) {
const auto& lengthY = std::get<CSSLength>(explicitSize.sizeY);
if (lengthY.value < 0) {
return {};
}
} else if (std::holds_alternative<CSSPercentage>(explicitSize.sizeY)) {
const auto& percentageY = std::get<CSSPercentage>(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<CSSRadialGradientShape>(shapeResult)) {
gradient.shape = std::get<CSSRadialGradientShape>(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<bool>(
[](const CSSPreservedToken& token) -> bool {
return token.type() == CSSTokenType::Ident &&
fnv1aLowercase(token.stringValue()) == fnv1a("at");
});
CSSRadialGradientPosition position;
if (atResult) {
parser.consumeWhitespace();
std::vector<std::variant<CSSLength, CSSPercentage, CSSKeyword>>
positionKeywordValues;
for (int i = 0; i < 2; i++) {
auto keywordFound = false;
auto valueFound = false;
auto positionKeyword =
parser.consumeComponentValue<std::optional<CSSKeyword>>(
[](const CSSPreservedToken& token)
-> std::optional<CSSKeyword> {
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<CSSKeyword>(existingValue)) {
if (std::get<CSSKeyword>(existingValue) == positionKeyword) {
return {};
}
}
}
positionKeywordValues.emplace_back(*positionKeyword);
keywordFound = true;
}
parser.consumeWhitespace();
auto lengthPercentageValue =
parseNextCSSValue<CSSLengthPercentage>(parser);
std::optional<decltype(positionKeywordValues)::value_type> value;
if (std::holds_alternative<CSSLength>(lengthPercentageValue)) {
value = std::get<CSSLength>(lengthPercentageValue);
} else if (std::holds_alternative<CSSPercentage>(
lengthPercentageValue)) {
value = std::get<CSSPercentage>(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 | <length-percentage> ]
if (positionKeywordValues.size() == 1) {
auto value = positionKeywordValues[0];
if (std::holds_alternative<CSSKeyword>(value)) {
auto keyword = std::get<CSSKeyword>(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<CSSLength>(value) ||
std::holds_alternative<CSSPercentage>(value))) {
if (std::holds_alternative<CSSLength>(value)) {
position.left = std::get<CSSLength>(value);
} else {
position.left = std::get<CSSPercentage>(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<CSSKeyword>(value1) &&
std::holds_alternative<CSSKeyword>(value2)) {
auto keyword1 = std::get<CSSKeyword>(value1);
auto keyword2 = std::get<CSSKeyword>(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 | <length-percentage> ] [ top | center |
// bottom | <length-percentage> ]
else {
if (std::holds_alternative<CSSKeyword>(value1)) {
auto keyword1 = std::get<CSSKeyword>(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<CSSLength>(value1) ||
std::holds_alternative<CSSPercentage>(value1))) {
if (std::holds_alternative<CSSLength>(value1)) {
position.left = std::get<CSSLength>(value1);
} else {
position.left = std::get<CSSPercentage>(value1);
}
} else {
return {};
}
if (std::holds_alternative<CSSKeyword>(value2)) {
auto keyword2 = std::get<CSSKeyword>(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<CSSLength>(value2) ||
std::holds_alternative<CSSPercentage>(value2))) {
if (std::holds_alternative<CSSLength>(value2)) {
position.top = std::get<CSSLength>(value2);
} else {
position.top = std::get<CSSPercentage>(value2);
}
} else {
return {};
}
}
}
// 4. [ [ left | right ] <length-percentage> ] && [ [ top | bottom ]
// <length-percentage> ]
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<CSSKeyword>(value1)) {
return {};
}
if (!std::holds_alternative<CSSKeyword>(value3)) {
return {};
}
if ((!std::holds_alternative<CSSLength>(value2) &&
!std::holds_alternative<CSSPercentage>(value2))) {
return {};
}
if ((!std::holds_alternative<CSSLength>(value4) &&
!std::holds_alternative<CSSPercentage>(value4))) {
return {};
}
auto parsedValue2 = std::holds_alternative<CSSLength>(value2)
? std::variant<CSSLength, CSSPercentage>{std::get<CSSLength>(
value2)}
: std::variant<CSSLength, CSSPercentage>{
std::get<CSSPercentage>(value2)};
auto parsedValue4 = std::holds_alternative<CSSLength>(value4)
? std::variant<CSSLength, CSSPercentage>{std::get<CSSLength>(
value4)}
: std::variant<CSSLength, CSSPercentage>{
std::get<CSSPercentage>(value4)};
auto keyword1 = std::get<CSSKeyword>(value1);
auto keyword3 = std::get<CSSKeyword>(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<CSSRadialGradientFunction>);
template <>
struct CSSDataTypeParser<CSSLinearGradientFunction> {
static auto consumeFunctionBlock(
const CSSFunctionBlock& func,
CSSSyntaxParser& parser) -> std::optional<CSSLinearGradientFunction> {
if (!iequals(func.name, "linear-gradient")) {
return {};
}
CSSLinearGradientFunction gradient;
auto parsedDirection =
parseNextCSSValue<CSSLinearGradientDirection>(parser);
if (!std::holds_alternative<CSSLinearGradientDirection>(parsedDirection)) {
return {};
}
parser.consumeDelimiter(CSSDelimiter::Comma);
gradient.direction = std::get<CSSLinearGradientDirection>(parsedDirection);
auto [items, colorStopCount] =
CSSLinearGradientFunction::parseGradientColorStopsAndHints(parser);
if (items.empty() || colorStopCount < 2) {
return {};
}
gradient.items = std::move(items);
return gradient;
}
};
static_assert(CSSDataType<CSSLinearGradientFunction>);
/**
* Representation of <background-image>
* https://www.w3.org/TR/css-backgrounds-3/#background-image
*/
using CSSBackgroundImage =
CSSCompoundDataType<CSSLinearGradientFunction, CSSRadialGradientFunction>;
/**
* Variant of possible CSS background image types
*/
using CSSBackgroundImageVariant = CSSVariantWithTypes<CSSBackgroundImage>;
/**
* Representation of <background-image-list>
*/
using CSSBackgroundImageList = CSSCommaSeparatedList<CSSBackgroundImage>;
} // namespace facebook::react
@@ -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 <gtest/gtest.h>
#include <react/renderer/css/CSSBackgroundImage.h>
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<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
"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<CSSBackgroundImageList>("");
ASSERT_TRUE(std::holds_alternative<std::monostate>(result));
}
TEST_F(CSSBackgroundImageTest, InvalidValueReturnsEmptyArray) {
auto result = parseCSSProperty<CSSBackgroundImageList>("linear-");
ASSERT_TRUE(std::holds_alternative<std::monostate>(result));
}
TEST_F(CSSBackgroundImageTest, LinearGradientWithWhitespacesInDirection) {
auto result = parseCSSProperty<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
" 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<CSSBackgroundImage>("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<CSSBackgroundImage>(
"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<CSSBackgroundImageList>(
"\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<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
"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<CSSBackgroundImage>("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<std::string> 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<CSSBackgroundImage>(input);
ASSERT_TRUE(std::holds_alternative<std::monostate>(result))
<< "Input should be invalid: " << input;
}
}
TEST_F(CSSBackgroundImageTest, LinearGradientWithMultipleTransitionHints) {
auto result = parseCSSProperty<CSSBackgroundImage>(
"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<std::string> 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<CSSBackgroundImageList>(input);
ASSERT_TRUE(std::holds_alternative<std::monostate>(result))
<< "Input should be invalid: " << input;
}
}
TEST_F(CSSBackgroundImageTest, LinearGradientWithMixedUnits) {
auto result = parseCSSProperty<CSSBackgroundImage>(
"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<CSSBackgroundImage>("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<CSSBackgroundImage>("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<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
"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 |
// <length-percentage> ]
TEST_F(CSSBackgroundImageTest, RadialGradientPositionLengthSyntax) {
auto result = parseCSSProperty<CSSBackgroundImage>(
"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<std::string> inputs = {
"radial-gradient(circle at left top, red, blue)",
"radial-gradient(circle at top left, red, blue)"};
decltype(parseCSSProperty<CSSBackgroundImage>(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<CSSBackgroundImage>(input);
ASSERT_EQ(result, expected) << "Failed for input: " << input;
}
}
// 3. position syntax: [ left | center | right | <length-percentage> ] [ top
// | center | bottom | <length-percentage> ]
TEST_F(CSSBackgroundImageTest, RadialGradientComplexPositionSyntax) {
const std::vector<std::pair<std::string, CSSRadialGradientPosition>>
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<CSSBackgroundImage>(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 ] <length-percentage> ] && [ [ top |
// bottom ] <length-percentage> ]
TEST_F(CSSBackgroundImageTest, RadialGradientSeparatePositionPercentages) {
auto result = parseCSSProperty<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
"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<CSSBackgroundImageList>(
"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<CSSBackgroundImage>(
"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<CSSBackgroundImage>(
"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<std::string> 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<CSSBackgroundImageList>(input);
ASSERT_TRUE(std::holds_alternative<std::monostate>(result))
<< "Input should be invalid: " << input;
}
}
TEST_F(CSSBackgroundImageTest, RadialGradientMultipleColorStops) {
auto result = parseCSSProperty<CSSBackgroundImage>(
"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<CSSBackgroundImageList>(input);
ASSERT_TRUE(std::holds_alternative<std::monostate>(result));
}
TEST_F(CSSBackgroundImageTest, RadialGradientNegativeRadius) {
const std::vector<std::string> invalidInputs = {
"radial-gradient(circle -100px, red, blue)",
"radial-gradient(ellipse 100px -40px, red, blue)"};
for (const auto& input : invalidInputs) {
const auto result = parseCSSProperty<CSSBackgroundImageList>(input);
ASSERT_TRUE(std::holds_alternative<std::monostate>(result))
<< "Input should be invalid: " << input;
}
}
} // namespace facebook::react
@@ -10,8 +10,6 @@
#include <react/renderer/graphics/ColorStop.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/graphics/ValueUnit.h>
#include <stdexcept>
#include <string>
#include <variant>
#include <vector>
@@ -52,16 +50,4 @@ struct LinearGradient {
#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);
}
}; // namespace facebook::react
@@ -37,6 +37,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 +50,7 @@ struct RadialGradientSize {
std::variant<SizeKeyword, Dimensions> value;
bool operator==(const RadialGradientSize& other) const {
if (std::holds_alternative<SizeKeyword>(value) &&
std::holds_alternative<SizeKeyword>(other.value)) {
return std::get<SizeKeyword>(value) == std::get<SizeKeyword>(other.value);
} else if (
std::holds_alternative<Dimensions>(value) &&
std::holds_alternative<Dimensions>(other.value)) {
return std::get<Dimensions>(value) == std::get<Dimensions>(other.value);
}
return false;
return value == other.value;
}
bool operator!=(const RadialGradientSize& other) const {
@@ -57,7 +57,7 @@ exports.examples = [
return (
<GradientBox
style={{
experimental_backgroundImage: 'linear-gradient(#e66465, #9198e5);',
experimental_backgroundImage: 'linear-gradient(#e66465, #9198e5)',
}}
testID="linear-gradient-basic">
<RNTesterText style={styles.text}>Linear Gradient</RNTesterText>
@@ -73,7 +73,7 @@ exports.examples = [
return (
<GradientBox
style={{
experimental_backgroundImage: 'linear-gradient(45deg, red, blue);',
experimental_backgroundImage: 'linear-gradient(45deg, red, blue)',
height: 300,
width: 140,
}}
@@ -94,7 +94,7 @@ exports.examples = [
linear-gradient(45deg, white, rgba(243, 119, 54, 0.8), rgba(243, 119, 54, 0) 70%),
linear-gradient(90deg, white, rgba(253, 244, 152, 0.8), rgba(253, 244, 152, 0) 70%),
linear-gradient(135deg, white, rgba(123, 192, 67, 0.8), rgba(123, 192, 67, 0) 70%),
linear-gradient(180deg, white, rgba(3, 146, 207, 0.8), rgba(3, 146, 207, 0) 70%);
linear-gradient(180deg, white, rgba(3, 146, 207, 0.8), rgba(3, 146, 207, 0) 70%)
`,
borderRadius: 16,
@@ -182,7 +182,7 @@ exports.examples = [
<GradientBox
style={{
experimental_backgroundImage:
'linear-gradient(to bottom right, yellow, green);',
'linear-gradient(to bottom right, yellow, green)',
borderRadius: 16,
}}
testID="linear-gradient-uniform-borders"
@@ -198,7 +198,7 @@ exports.examples = [
<GradientBox
style={{
experimental_backgroundImage:
'linear-gradient(to bottom right, yellow, green);',
'linear-gradient(to bottom right, yellow, green)',
borderTopRightRadius: 8,
borderTopLeftRadius: 80,
}}
@@ -267,7 +267,7 @@ exports.examples = [
#29abe2 65%,
180px,
#2e3192 100%
);`,
)`,
}}
testID="linear-gradient-px-and-percentage"
/>