From 5c79e99ce8f9105d2fe77c38795b786bc9746811 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 7 May 2024 12:43:59 -0700 Subject: [PATCH] Add CSSValue support for data type (#44430) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44430 Let's add support for angles so we can correctly represent things like rotation/skew transforms or hue-rotate filter. This should replace `ValueUnit` in prop related transform code. A couple implementation notes: 1. RN currently uses radians internally, but CSS says the cannonical angle unit is degrees, so we keep to that 2. We have all the information to convert to cannonical value type at parsing layer, so we do that, and clients can only see degreee values instead of units. Less flexible, but simpler/more efficient for now, where higher levels don't care. Changelog: [Internal] Reviewed By: sammy-SC Differential Revision: D57029378 fbshipit-source-id: 91341f1bf4686d9016823ac8cf91897e933345f9 --- .../react/renderer/css/CSSAngleUnit.h | 66 +++++++++++++++++++ .../react/renderer/css/CSSDeclaredStyle.h | 3 +- .../react/renderer/css/CSSParser.h | 7 ++ .../ReactCommon/react/renderer/css/CSSValue.h | 23 +++++++ .../renderer/css/tests/CSSParserTest.cpp | 28 ++++++++ 5 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSAngleUnit.h diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSAngleUnit.h b/packages/react-native/ReactCommon/react/renderer/css/CSSAngleUnit.h new file mode 100644 index 00000000000..1261fcef660 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSAngleUnit.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace facebook::react { + +/** + * Unit for the CSS type. + * https://www.w3.org/TR/css-values-4/#angles + */ +enum class CSSAngleUnit : uint8_t { + Deg, + Grad, + Rad, + Turn, +}; + +/** + * Parses a unit from a dimension token into a CSS angle unit. + */ +constexpr std::optional parseCSSAngleUnit(std::string_view unit) { + switch (fnv1a(unit)) { + case fnv1a("deg"): + return CSSAngleUnit::Deg; + case fnv1a("grad"): + return CSSAngleUnit::Grad; + case fnv1a("rad"): + return CSSAngleUnit::Rad; + case fnv1a("turn"): + return CSSAngleUnit::Turn; + default: + return std::nullopt; + } +} + +/** + * Converts a specified CSS angle to its cannonical unit (degrees) + */ +constexpr float canonicalize(float value, CSSAngleUnit unit) { + switch (unit) { + case CSSAngleUnit::Deg: + return value; + case CSSAngleUnit::Grad: + return value * 0.9f; + case CSSAngleUnit::Rad: + return value * 57.295779513f; + case CSSAngleUnit::Turn: + return value * 360.0f; + default: + return std::numeric_limits::quiet_NaN(); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSDeclaredStyle.h b/packages/react-native/ReactCommon/react/renderer/css/CSSDeclaredStyle.h index c52bf22e679..d3c02343bbd 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSDeclaredStyle.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSDeclaredStyle.h @@ -93,7 +93,8 @@ class CSSDeclaredStyle { CSSLength, CSSNumber, CSSPercentage, - CSSRatio>)> + CSSRatio, + CSSAngle>)> value; constexpr bool operator<(const PropMapping& rhs) const { diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSParser.h b/packages/react-native/ReactCommon/react/renderer/css/CSSParser.h index c2ea7d9bc94..7724ddb34ae 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSParser.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSParser.h @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -107,6 +108,12 @@ class CSSParser { return CSSValueT::length(consumeToken().numericValue(), *unit); } } + if constexpr (traits::containsType()) { + if (auto unit = parseCSSAngleUnit(peek().unit())) { + return CSSValueT::angle( + canonicalize(consumeToken().numericValue(), *unit)); + } + } return {}; } diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSValue.h b/packages/react-native/ReactCommon/react/renderer/css/CSSValue.h index fd7dfb3575c..3d18b971f79 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSValue.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSValue.h @@ -28,6 +28,7 @@ enum class CSSValueType : uint8_t { Number, Percentage, Ratio, + Angle, }; /** @@ -74,6 +75,14 @@ struct CSSRatio { float denominator{}; }; +/** + * Representation of CSS data type + * https://www.w3.org/TR/css-values-4/#angles + */ +struct CSSAngle { + float degrees{}; +}; + /** * CSSValueVariant represents a CSS component value: * https://www.w3.org/TR/css-values-4/#component-types @@ -161,6 +170,12 @@ class CSSValueVariant { CSSValueType::Ratio, CSSRatio{numerator, denominator}); } + static constexpr CSSValueVariant angle(float degrees) + requires(canRepresent()) + { + return CSSValueVariant(CSSValueType::Angle, CSSAngle{degrees}); + } + constexpr CSSValueType type() const { return type_; } @@ -201,6 +216,12 @@ class CSSValueVariant { return getIf(); } + constexpr CSSAngle getAngle() const + requires(canRepresent()) + { + return getIf(); + } + constexpr operator bool() const requires(canRepresent()) { @@ -225,6 +246,8 @@ class CSSValueVariant { return getPercentage() == other.getPercentage(); case CSSValueType::Ratio: return getRatio() == other.getRatio(); + case CSSValueType::Angle: + return getAngle() == other.getAngle(); } return false; diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSParserTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSParserTest.cpp index 2ff8760e0dc..26482354da2 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSParserTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSParserTest.cpp @@ -267,6 +267,34 @@ TEST(CSSParser, number_ratio_values) { EXPECT_EQ(degenerateRatio.getNumber().value, 0.0f); } +TEST(CSSParser, angle_values) { + auto emptyValue = parseCSSComponentValue(""); + EXPECT_EQ(emptyValue.type(), CSSValueType::CSSWideKeyword); + EXPECT_EQ(emptyValue.getCSSWideKeyword(), CSSWideKeyword::Unset); + + auto degreeValue = parseCSSComponentValue("10deg"); + EXPECT_EQ(degreeValue.type(), CSSValueType::Angle); + EXPECT_EQ(degreeValue.getAngle().degrees, 10.0f); + + auto radianValue = parseCSSComponentValue("10rad"); + EXPECT_EQ(radianValue.type(), CSSValueType::Angle); + EXPECT_NEAR(radianValue.getAngle().degrees, 572.958f, 0.001f); + + auto negativeRadianValue = + parseCSSComponentValue("-10rad"); + EXPECT_EQ(negativeRadianValue.type(), CSSValueType::Angle); + EXPECT_NEAR(negativeRadianValue.getAngle().degrees, -572.958f, 0.001f); + + auto gradianValue = + parseCSSComponentValue("10grad"); + EXPECT_EQ(gradianValue.type(), CSSValueType::Angle); + ASSERT_NEAR(gradianValue.getAngle().degrees, 9.0f, 0.001f); + + auto turnValue = parseCSSComponentValue("1turn"); + EXPECT_EQ(turnValue.type(), CSSValueType::Angle); + EXPECT_EQ(turnValue.getAngle().degrees, 360.0f); +} + TEST(CSSParser, parse_prop) { auto emptyValue = parseCSSProp(""); EXPECT_EQ(emptyValue.type(), CSSValueType::CSSWideKeyword);