mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
meta-codesync[bot]
parent
4c4270d6c7
commit
a9780f9102
+3
-2
@@ -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);
|
||||
|
||||
+617
@@ -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
|
||||
+51
@@ -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
|
||||
+560
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user