diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h b/packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h index 24a29fee9f7..29f7ef66969 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h @@ -104,6 +104,31 @@ hslToRgb(float h, float s, float l) { }; } +inline std::tuple +hwbToRgb(float h, float w, float b) { + w = std::clamp(w / 100.0f, 0.0f, 1.0f); + b = std::clamp(b / 100.0f, 0.0f, 1.0f); + + if (w + b >= 1.0f) { + auto gray = w / (w + b); + return { + static_cast(std::round(gray * 255.0f)), + static_cast(std::round(gray * 255.0f)), + static_cast(std::round(gray * 255.0f)), + }; + } + + auto red = hueToRgb(0.0f, 1.0f, h + 1.0f / 3.0f) * (1.0f - w - b) + w; + auto green = hueToRgb(0.0f, 1.0f, h) * (1.0f - w - b) + w; + auto blue = hueToRgb(0.0f, 1.0f, h - 1.0f / 3.0f) * (1.0f - w - b) + w; + + return { + static_cast(std::round(red * 255.0f)), + static_cast(std::round(green * 255.0f)), + static_cast(std::round(blue * 255.0f)), + }; +} + template requires( (std::is_same_v || @@ -269,6 +294,7 @@ inline std::optional parseLegacyHslFunction(CSSSyntaxParser& parser) { if (!l.has_value()) { return {}; } + auto a = normalizeComponent( parseNextCSSValue(parser, CSSDelimiter::Comma), 1.0f); @@ -310,6 +336,7 @@ inline std::optional parseModernHslFunction(CSSSyntaxParser& parser) { if (!l.has_value()) { return {}; } + auto a = normalizeComponent( parseNextCSSValue( parser, CSSDelimiter::SolidusOrWhitespace), @@ -337,6 +364,50 @@ inline std::optional parseHslFunction(CSSSyntaxParser& parser) { return parseModernHslFunction(parser); } } + +/** + * Parses an hwb() function and returns a CSSColor if it is valid. + * https://www.w3.org/TR/css-color-4/#funcdef-hwb + */ +template +inline std::optional parseHwbFunction(CSSSyntaxParser& parser) { + auto h = + normalizeHueComponent(parseNextCSSValue(parser)); + if (!h.has_value()) { + return {}; + } + + auto w = normalizeComponent( + parseNextCSSValue( + parser, CSSDelimiter::Whitespace), + 100.0f); + if (!w.has_value()) { + return {}; + } + + auto b = normalizeComponent( + parseNextCSSValue( + parser, CSSDelimiter::Whitespace), + 100.0f); + if (!b.has_value()) { + return {}; + } + + auto a = normalizeComponent( + parseNextCSSValue( + parser, CSSDelimiter::SolidusOrWhitespace), + 1.0f); + + auto [red, green, blue] = hwbToRgb(*h, *w, *b); + + return CSSColor{ + .r = red, + .g = green, + .b = blue, + .a = clampAlpha(a), + }; +} + } // namespace detail /** @@ -360,20 +431,11 @@ constexpr std::optional parseCSSColorFunction( return detail::parseHslFunction(parser); break; case fnv1a("hwb"): - case fnv1a("hwba"): - // TODO - break; - case fnv1a("lab"): - break; - case fnv1a("lch"): - break; - case fnv1a("oklab"): - break; - case fnv1a("oklch"): - break; - case fnv1a("color"): - // TODO T213000437: Support `color()` functions and wide-gamut colors. + return detail::parseHwbFunction(parser); break; + + // TODO T213000437: support lab(), lch(), oklab(), oklch(), color(), + // color-mix() default: return {}; } diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp index ebdd363afde..b579f4af274 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp @@ -394,6 +394,92 @@ TEST(CSSColor, hsl_hsla_values) { EXPECT_TRUE(std::holds_alternative(valueEndingWithComma)); } +TEST(CSSColor, hwb_values) { + auto simpleValue = parseCSSProperty("hwb(208 14% 42%)"); + EXPECT_TRUE(std::holds_alternative(simpleValue)); + EXPECT_EQ(std::get(simpleValue).r, 36); + EXPECT_EQ(std::get(simpleValue).g, 96); + EXPECT_EQ(std::get(simpleValue).b, 148); + EXPECT_EQ(std::get(simpleValue).a, 255); + + auto grayValue = parseCSSProperty("hwb(208 100 100)"); + EXPECT_TRUE(std::holds_alternative(grayValue)); + EXPECT_EQ(std::get(grayValue).r, 128); + EXPECT_EQ(std::get(grayValue).g, 128); + EXPECT_EQ(std::get(grayValue).b, 128); + EXPECT_EQ(std::get(grayValue).a, 255); + + auto angleValue = parseCSSProperty("hwb(36.3028E-1rad 14% 42%)"); + EXPECT_TRUE(std::holds_alternative(angleValue)); + EXPECT_EQ(std::get(angleValue).r, 36); + EXPECT_EQ(std::get(angleValue).g, 96); + EXPECT_EQ(std::get(angleValue).b, 148); + EXPECT_EQ(std::get(angleValue).a, 255); + + auto legacySyntaxValue = parseCSSProperty("hwb(208, 14%, 42%)"); + EXPECT_TRUE(std::holds_alternative(legacySyntaxValue)); + + auto alphaValue = parseCSSProperty("hwb(208 14% 42% 0.5)"); + EXPECT_TRUE(std::holds_alternative(alphaValue)); + EXPECT_EQ(std::get(alphaValue).r, 36); + EXPECT_EQ(std::get(alphaValue).g, 96); + EXPECT_EQ(std::get(alphaValue).b, 148); + EXPECT_EQ(std::get(alphaValue).a, 128); + + auto alphaPercentageValue = + parseCSSProperty("hwb(208 14% 42% 50%)"); + EXPECT_TRUE(std::holds_alternative(alphaPercentageValue)); + EXPECT_EQ(std::get(alphaPercentageValue).r, 36); + EXPECT_EQ(std::get(alphaPercentageValue).g, 96); + EXPECT_EQ(std::get(alphaPercentageValue).b, 148); + EXPECT_EQ(std::get(alphaPercentageValue).a, 128); + + auto alphaSolidusValue = parseCSSProperty("hwb(208 14% 42% / 0.5)"); + EXPECT_TRUE(std::holds_alternative(alphaSolidusValue)); + EXPECT_EQ(std::get(alphaSolidusValue).r, 36); + EXPECT_EQ(std::get(alphaSolidusValue).g, 96); + EXPECT_EQ(std::get(alphaSolidusValue).b, 148); + EXPECT_EQ(std::get(alphaSolidusValue).a, 128); + + auto mixedWhitespaceValue = + parseCSSProperty(" hwb( 208 14% 42% /0.5 ) "); + EXPECT_TRUE(std::holds_alternative(mixedWhitespaceValue)); + EXPECT_EQ(std::get(mixedWhitespaceValue).r, 36); + EXPECT_EQ(std::get(mixedWhitespaceValue).g, 96); + EXPECT_EQ(std::get(mixedWhitespaceValue).b, 148); + EXPECT_EQ(std::get(mixedWhitespaceValue).a, 128); + + auto extraDegreesValue = parseCSSProperty("hwb(568 14% 42% / 0.5)"); + EXPECT_TRUE(std::holds_alternative(extraDegreesValue)); + EXPECT_EQ(std::get(extraDegreesValue).r, 36); + EXPECT_EQ(std::get(extraDegreesValue).g, 96); + EXPECT_EQ(std::get(extraDegreesValue).b, 148); + EXPECT_EQ(std::get(extraDegreesValue).a, 128); + + auto negativeDegreesValue = + parseCSSProperty("hwb(-152 14% 42% / 0.5)"); + EXPECT_TRUE(std::holds_alternative(negativeDegreesValue)); + EXPECT_EQ(std::get(negativeDegreesValue).r, 36); + EXPECT_EQ(std::get(negativeDegreesValue).g, 96); + EXPECT_EQ(std::get(negativeDegreesValue).b, 148); + EXPECT_EQ(std::get(negativeDegreesValue).a, 128); + + auto missingComponentsValue = parseCSSProperty("hwb(208 14%)"); + EXPECT_TRUE(std::holds_alternative(missingComponentsValue)); + + auto tooManyComponentsValue = + parseCSSProperty("hwb(208 14% 42% 0.5 0.5)"); + EXPECT_TRUE(std::holds_alternative(tooManyComponentsValue)); + + auto valueStartingWithComma = + parseCSSProperty("hwb(,208 14% 42% / 0.5)"); + EXPECT_TRUE(std::holds_alternative(valueStartingWithComma)); + + auto valueEndingWithComma = + parseCSSProperty("hwb(208 14% 42% / 0.5,)"); + EXPECT_TRUE(std::holds_alternative(valueEndingWithComma)); +} + TEST(CSSColor, constexpr_values) { [[maybe_unused]] constexpr auto emptyValue = parseCSSProperty("");