diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn index 357b217cff..be1a3363f6 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn @@ -8,7 +8,12 @@ source_set("StyleValues") { "BasicShapeStyleValue.cpp", "BorderRadiusStyleValue.cpp", "CSSColorValue.cpp", + "CSSHSL.cpp", + "CSSHWB.cpp", "CSSKeywordValue.cpp", + "CSSOKLCH.cpp", + "CSSOKLab.cpp", + "CSSRGB.cpp", "CalculatedStyleValue.cpp", "ConicGradientStyleValue.cpp", "ContentStyleValue.cpp", diff --git a/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html b/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html index 88b5936b27..f600333880 100644 --- a/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html +++ b/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html @@ -30,8 +30,6 @@ ctx.fillRect(0, 300, 500, 100); // Calc - // FIXME: The CSS parser currently chokes on calc expressions, which will - // leave the fillStyle as it was (green). ctx.fillStyle = "rgb(calc(infinity), 0, 0)"; ctx.fillRect(0, 400, 500, 100); diff --git a/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png b/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png index 7cdb91b126..98df06fd15 100644 Binary files a/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png and b/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png differ diff --git a/Tests/LibWeb/Screenshot/images/css-color-functions-ref.png b/Tests/LibWeb/Screenshot/images/css-color-functions-ref.png index f5e9846098..464bcb783f 100644 Binary files a/Tests/LibWeb/Screenshot/images/css-color-functions-ref.png and b/Tests/LibWeb/Screenshot/images/css-color-functions-ref.png differ diff --git a/Tests/LibWeb/Text/expected/canvas/fillstyle.txt b/Tests/LibWeb/Text/expected/canvas/fillstyle.txt index b342d5b0d5..df993731f8 100644 --- a/Tests/LibWeb/Text/expected/canvas/fillstyle.txt +++ b/Tests/LibWeb/Text/expected/canvas/fillstyle.txt @@ -2,4 +2,4 @@ 2. "#ff0000ff" 3. "#0000ffff" 4. "#00ff00ff" -5. "#00ff00ff" +5. "#ff0000ff" diff --git a/Tests/LibWeb/Text/input/canvas/fillstyle.html b/Tests/LibWeb/Text/input/canvas/fillstyle.html index 87a3e851d4..1b2830effd 100644 --- a/Tests/LibWeb/Text/input/canvas/fillstyle.html +++ b/Tests/LibWeb/Text/input/canvas/fillstyle.html @@ -33,10 +33,8 @@ return context.fillStyle; }); - // 5. Percentages + // 5. Calc, with out-of-range values testPart(() => { - // FIXME: The CSS parser currently chokes on calc expressions, which will - // leave the fillStyle as it was (green). context.fillStyle = "rgb(calc(infinity), 0, 0)"; return context.fillStyle; }); diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 7171fcc95f..87114f9cab 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -113,7 +113,12 @@ set(SOURCES CSS/StyleValues/CounterDefinitionsStyleValue.cpp CSS/StyleValues/CounterStyleValue.cpp CSS/StyleValues/CSSColorValue.cpp + CSS/StyleValues/CSSHSL.cpp + CSS/StyleValues/CSSHWB.cpp CSS/StyleValues/CSSKeywordValue.cpp + CSS/StyleValues/CSSOKLab.cpp + CSS/StyleValues/CSSOKLCH.cpp + CSS/StyleValues/CSSRGB.cpp CSS/StyleValues/DisplayStyleValue.cpp CSS/StyleValues/EasingStyleValue.cpp CSS/StyleValues/EdgeStyleValue.cpp diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index ddd9960e4f..b0b233fc39 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -1,7 +1,7 @@ /* * Copyright (c) 2018-2022, Andreas Kling * Copyright (c) 2020-2021, the SerenityOS developers. - * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2021-2024, Sam Atkins * Copyright (c) 2021, Tobias Christiansen * Copyright (c) 2022, MacDue * Copyright (c) 2024, Shannon Booth @@ -43,7 +43,12 @@ #include #include #include +#include +#include #include +#include +#include +#include #include #include #include @@ -2622,474 +2627,454 @@ RefPtr Parser::parse_rect_value(TokenStream& toke return RectStyleValue::create(EdgeRect { params[0], params[1], params[2], params[3] }); } -Optional Parser::parse_rgb_color(Vector const& component_values) +// https://www.w3.org/TR/css-color-4/#typedef-hue +RefPtr Parser::parse_hue_value(TokenStream& tokens) { - u8 r_val = 0; - u8 g_val = 0; - u8 b_val = 0; + // = | + if (auto number = parse_number_value(tokens)) + return number; + if (auto angle = parse_angle_value(tokens)) + return angle; - auto tokens = TokenStream { component_values }; - - tokens.skip_whitespace(); - auto const& red = tokens.next_token(); - - if (!red.is(Token::Type::Number) - && !red.is(Token::Type::Percentage)) - return {}; - - tokens.skip_whitespace(); - bool legacy_syntax = tokens.peek_token().is(Token::Type::Comma); - if (legacy_syntax) { - // Legacy syntax. - tokens.next_token(); - tokens.skip_whitespace(); - - auto const& green = tokens.next_token(); - tokens.skip_whitespace(); - - tokens.next_token(); - tokens.skip_whitespace(); - - auto const& blue = tokens.next_token(); - - if (red.is(Token::Type::Percentage)) { - // Percentage components. - if (!green.is(Token::Type::Percentage) || !blue.is(Token::Type::Percentage)) - return {}; - - r_val = lround(clamp(red.token().percentage() * 2.55, 0, 255)); - g_val = lround(clamp(green.token().percentage() * 2.55, 0, 255)); - b_val = lround(clamp(blue.token().percentage() * 2.55, 0, 255)); - } else { - // Number components. - if (!green.is(Token::Type::Number) || !blue.is(Token::Type::Number)) - return {}; - - r_val = clamp(llroundf(red.token().number_value()), 0, 255); - g_val = clamp(llroundf(green.token().number_value()), 0, 255); - b_val = clamp(llroundf(blue.token().number_value()), 0, 255); - } - } else { - // Modern syntax. - - if (red.is(Token::Type::Number)) { - r_val = clamp(llroundf(red.token().number_value()), 0, 255); - } else { - r_val = lround(clamp(red.token().percentage() * 2.55, 0, 255)); - } - - auto const& green = tokens.next_token(); - if (green.is(Token::Type::Number)) { - g_val = clamp(llroundf(green.token().number_value()), 0, 255); - } else if (green.is(Token::Type::Percentage)) { - g_val = lround(clamp(green.token().percentage() * 2.55, 0, 255)); - } else { - return {}; - } - - tokens.skip_whitespace(); - auto const& blue = tokens.next_token(); - if (blue.is(Token::Type::Number)) { - b_val = clamp(llroundf(blue.token().number_value()), 0, 255); - } else if (blue.is(Token::Type::Percentage)) { - b_val = lround(clamp(blue.token().percentage() * 2.55, 0, 255)); - } else { - return {}; - } - } - - u8 alpha_val = 255; - tokens.skip_whitespace(); - if (tokens.has_next_token()) { - auto const& separator = tokens.next_token(); - bool is_comma = separator.is(Token::Type::Comma); - bool is_slash = separator.is_delim('/'); - if (legacy_syntax ? !is_comma : !is_slash) - return {}; - - tokens.skip_whitespace(); - auto const& alpha = tokens.next_token(); - - if (alpha.is(Token::Type::Number)) - alpha_val = clamp(lround(alpha.token().number_value() * 255.0), 0, 255); - else if (alpha.is(Token::Type::Percentage)) - alpha_val = clamp(lround(alpha.token().percentage() * 2.55), 0, 255); - else - return {}; - - tokens.skip_whitespace(); - if (tokens.has_next_token()) - return {}; // should have consumed all arguments. - } - - return Color(r_val, g_val, b_val, alpha_val); + return nullptr; } -Optional Parser::parse_hsl_color(Vector const& component_values) +RefPtr Parser::parse_solidus_and_alpha_value(TokenStream& tokens) { - float h_val = 0.0; - float s_val = 0.0; - float l_val = 0.0; + // [ / [ | none] ]? + // Common to the modern-syntax color functions. + // TODO: Parse `none` - auto tokens = TokenStream { component_values }; - - tokens.skip_whitespace(); - auto const& hue = tokens.next_token(); - - if (!hue.is(Token::Type::Number) - && !hue.is(Token::Type::Dimension)) - return {}; - - if (hue.is(Token::Type::Number)) { - h_val = fmod(hue.token().number_value(), 360.0); - } else { - auto numeric_value = hue.token().dimension_value(); - auto unit_string = hue.token().dimension_unit(); - auto angle_type = Angle::unit_from_name(unit_string); - - if (!angle_type.has_value()) - return {}; - - auto angle = Angle { numeric_value, angle_type.release_value() }; - - h_val = fmod(angle.to_degrees(), 360.0); - } - - tokens.skip_whitespace(); - bool legacy_syntax = tokens.peek_token().is(Token::Type::Comma); - if (legacy_syntax) { - // legacy syntax. - tokens.next_token(); - tokens.skip_whitespace(); - - auto const& saturation = tokens.next_token(); - if (!saturation.is(Token::Type::Percentage)) - return {}; - s_val = max(saturation.token().percentage() / 100.0, 0); - - tokens.skip_whitespace(); - tokens.next_token(); - tokens.skip_whitespace(); - - auto const& lightness = tokens.next_token(); - if (!lightness.is(Token::Type::Percentage)) - return {}; - l_val = lightness.token().percentage() / 100.0; - } else { - // Modern syntax. - - auto const& saturation = tokens.next_token(); - if (saturation.is(Token::Type::Number)) { - s_val = saturation.token().number_value() / 100.0; - } else if (saturation.is(Token::Type::Percentage)) { - s_val = saturation.token().percentage() / 100.0; - } else { - return {}; - } - s_val = max(s_val, 0); - - tokens.skip_whitespace(); - auto const& lightness = tokens.next_token(); - if (lightness.is(Token::Type::Number)) { - l_val = lightness.token().number_value() / 100.0; - } else if (lightness.is(Token::Type::Percentage)) { - l_val = lightness.token().percentage() / 100.0; - } else { - return {}; - } - } - - float alpha_val = 1.0; - tokens.skip_whitespace(); - if (tokens.has_next_token()) { - auto const& separator = tokens.next_token(); - bool is_comma = separator.is(Token::Type::Comma); - bool is_slash = separator.is_delim('/'); - if (legacy_syntax ? !is_comma : !is_slash) - return {}; - - tokens.skip_whitespace(); - auto const& alpha = tokens.next_token(); - - if (alpha.is(Token::Type::Number)) - alpha_val = alpha.token().number_value(); - else if (alpha.is(Token::Type::Percentage)) - alpha_val = alpha.token().percentage() / 100.0; - else - return {}; - - tokens.skip_whitespace(); - if (tokens.has_next_token()) - return {}; // should have consumed all arguments. - } - - return Color::from_hsla(h_val, s_val, l_val, alpha_val); -} - -Optional Parser::parse_hwb_color(Vector const& component_values) -{ - float h_val = 0.0; - float w_val = 0.0; - float b_val = 0.0; - - auto tokens = TokenStream { component_values }; - - tokens.skip_whitespace(); - auto const& hue = tokens.next_token(); - - if (!hue.is(Token::Type::Number) - && !hue.is(Token::Type::Dimension)) - return {}; - - if (hue.is(Token::Type::Number)) { - h_val = fmod(hue.token().number_value(), 360.0); - } else { - auto numeric_value = hue.token().dimension_value(); - auto unit_string = hue.token().dimension_unit(); - auto angle_type = Angle::unit_from_name(unit_string); - - if (!angle_type.has_value()) - return {}; - - auto angle = Angle { numeric_value, angle_type.release_value() }; - - h_val = fmod(angle.to_degrees(), 360); - } - - tokens.skip_whitespace(); - auto const& whiteness = tokens.next_token(); - if (whiteness.is(Token::Type::Number)) { - w_val = whiteness.token().number_value() / 100.0; - } else if (whiteness.is(Token::Type::Percentage)) { - w_val = whiteness.token().percentage() / 100.0; - } else { - return {}; - } - - tokens.skip_whitespace(); - auto const& blackness = tokens.next_token(); - if (blackness.is(Token::Type::Number)) { - b_val = blackness.token().number_value() / 100.0; - } else if (blackness.is(Token::Type::Percentage)) { - b_val = blackness.token().percentage() / 100.0; - } else { - return {}; - } - - float alpha_val = 1.0; - tokens.skip_whitespace(); - if (tokens.has_next_token()) { - auto const& separator = tokens.next_token(); - if (!separator.is_delim('/')) - return {}; - - tokens.skip_whitespace(); - auto const& alpha = tokens.next_token(); - - if (alpha.is(Token::Type::Number)) - alpha_val = alpha.token().number_value(); - else if (alpha.is(Token::Type::Percentage)) - alpha_val = alpha.token().percentage() / 100.0; - else - return {}; - - tokens.skip_whitespace(); - if (tokens.has_next_token()) - return {}; // should have consumed all arguments. - } - - if (w_val + b_val >= 1.0) { - u8 gray = clamp(llroundf((w_val / (w_val + b_val)) * 255), 0, 255); - return Color(gray, gray, gray, clamp(llroundf(alpha_val * 255), 0, 255)); - } - - float value = 1 - b_val; - float saturation = 1 - (w_val / value); - return Color::from_hsv(h_val, saturation, value).with_opacity(alpha_val); -} - -Optional Parser::parse_oklab_color(Vector const& component_values) -{ - float L_val = 0.0; - float a_val = 0.0; - float b_val = 0.0; - - auto tokens = TokenStream { component_values }; - - tokens.skip_whitespace(); - auto const& lightness = tokens.next_token(); - if (lightness.is(Token::Type::Number)) { - L_val = lightness.token().number_value(); - } else if (lightness.is(Token::Type::Percentage)) { - L_val = lightness.token().percentage() / 100.0; - } else { - return {}; - } - L_val = clamp(L_val, 0.0, 1.0); - - tokens.skip_whitespace(); - auto const& a = tokens.next_token(); - if (a.is(Token::Type::Number)) { - a_val = a.token().number_value(); - } else if (a.is(Token::Type::Percentage)) { - a_val = a.token().percentage() / 100.0 * 0.4; - } else { - return {}; - } - - tokens.skip_whitespace(); - auto const& b = tokens.next_token(); - if (b.is(Token::Type::Number)) { - b_val = b.token().number_value(); - } else if (a.is(Token::Type::Percentage)) { - b_val = b.token().percentage() / 100.0 * 0.4; - } else { - return {}; - } - - float alpha_val = 1.0; - tokens.skip_whitespace(); - if (tokens.has_next_token()) { - auto const& separator = tokens.next_token(); - if (!separator.is_delim('/')) - return {}; - - tokens.skip_whitespace(); - auto const& alpha = tokens.next_token(); - - if (alpha.is(Token::Type::Number)) - alpha_val = alpha.token().number_value(); - else if (alpha.is(Token::Type::Percentage)) - alpha_val = alpha.token().percentage() / 100.0; - else - return {}; - - tokens.skip_whitespace(); - if (tokens.has_next_token()) - return {}; // should have consumed all arguments. - } - - return Color::from_oklab(L_val, a_val, b_val, alpha_val); -} - -Optional Parser::parse_oklch_color(Vector const& component_values) -{ - float L_val = 0.0; - float c_val = 0.0; - float h_val = 0.0; - - auto tokens = TokenStream { component_values }; - - tokens.skip_whitespace(); - auto const& lightness = tokens.next_token(); - if (lightness.is(Token::Type::Number)) { - L_val = lightness.token().number_value(); - } else if (lightness.is(Token::Type::Percentage)) { - L_val = lightness.token().percentage() / 100.0; - } else { - return {}; - } - L_val = clamp(L_val, 0.0, 1.0); - - tokens.skip_whitespace(); - auto const& chroma = tokens.next_token(); - if (chroma.is(Token::Type::Number)) { - c_val = chroma.token().number_value(); - } else if (chroma.is(Token::Type::Percentage)) { - c_val = chroma.token().percentage() / 100.0 * 0.4; - } else { - return {}; - } - c_val = max(c_val, 0.0); - - tokens.skip_whitespace(); - auto const& hue = tokens.next_token(); - - if (!hue.is(Token::Type::Number) - && !hue.is(Token::Type::Dimension)) - return {}; - - if (hue.is(Token::Type::Number)) { - h_val = hue.token().number_value() * AK::Pi / 180; - } else { - auto numeric_value = hue.token().dimension_value(); - auto unit_string = hue.token().dimension_unit(); - auto angle_type = Angle::unit_from_name(unit_string); - - if (!angle_type.has_value()) - return {}; - - auto angle = Angle { numeric_value, angle_type.release_value() }; - - h_val = angle.to_radians(); - } - - float alpha_val = 1.0; - tokens.skip_whitespace(); - if (tokens.has_next_token()) { - auto const& separator = tokens.next_token(); - if (!separator.is_delim('/')) - return {}; - - tokens.skip_whitespace(); - auto const& alpha = tokens.next_token(); - - if (alpha.is(Token::Type::Number)) - alpha_val = alpha.token().number_value(); - else if (alpha.is(Token::Type::Percentage)) - alpha_val = alpha.token().percentage() / 100.0; - else - return {}; - - tokens.skip_whitespace(); - if (tokens.has_next_token()) - return {}; // should have consumed all arguments. - } - - return Color::from_oklab(L_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val); -} - -Optional Parser::parse_color(TokenStream& tokens) -{ auto transaction = tokens.begin_transaction(); - auto commit_if_valid = [&](Optional color) { - if (color.has_value()) - transaction.commit(); - return color; - }; + tokens.skip_whitespace(); + if (!tokens.next_token().is_delim('/')) + return {}; + tokens.skip_whitespace(); + auto alpha = parse_number_percentage_value(tokens); + if (!alpha) + return {}; + tokens.skip_whitespace(); + transaction.commit(); + return alpha; +} + +// https://www.w3.org/TR/css-color-4/#funcdef-rgb +RefPtr Parser::parse_rgb_color_value(TokenStream& outer_tokens) +{ + // rgb() = [ | ] + // rgba() = [ | ] + // = rgb( #{3} , ? ) | + // rgb( #{3} , ? ) + // = rgba( #{3} , ? ) | + // rgba( #{3} , ? ) + // = rgb( + // [ | | none]{3} + // [ / [ | none] ]? ) + // = rgba( + // [ | | none]{3} + // [ / [ | none] ]? ) + // TODO: Handle none values + + auto transaction = outer_tokens.begin_transaction(); + outer_tokens.skip_whitespace(); + + auto& function_token = outer_tokens.next_token(); + if (!function_token.is_function("rgb"sv) && !function_token.is_function("rgba"sv)) + return {}; + + RefPtr red; + RefPtr green; + RefPtr blue; + RefPtr alpha; + + auto inner_tokens = TokenStream { function_token.function().values() }; + inner_tokens.skip_whitespace(); + + red = parse_number_percentage_value(inner_tokens); + if (!red) + return {}; + + inner_tokens.skip_whitespace(); + bool legacy_syntax = inner_tokens.peek_token().is(Token::Type::Comma); + if (legacy_syntax) { + // Legacy syntax + // #{3} , ? + // | #{3} , ? + // So, r/g/b can be numbers or percentages, as long as they're all the same type. + + inner_tokens.next_token(); // comma + inner_tokens.skip_whitespace(); + + green = parse_number_percentage_value(inner_tokens); + if (!green) + return {}; + inner_tokens.skip_whitespace(); + + if (!inner_tokens.next_token().is(Token::Type::Comma)) + return {}; + inner_tokens.skip_whitespace(); + + blue = parse_number_percentage_value(inner_tokens); + if (!blue) + return {}; + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) { + // Try and read comma and alpha + if (!inner_tokens.next_token().is(Token::Type::Comma)) + return {}; + inner_tokens.skip_whitespace(); + + alpha = parse_number_percentage_value(inner_tokens); + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) + return {}; + } + + // Verify we're all percentages or all numbers + auto is_percentage = [](CSSStyleValue const& style_value) { + return style_value.is_percentage() + || (style_value.is_calculated() && style_value.as_calculated().resolves_to_percentage()); + }; + bool red_is_percentage = is_percentage(*red); + bool green_is_percentage = is_percentage(*green); + bool blue_is_percentage = is_percentage(*blue); + if (red_is_percentage != green_is_percentage || red_is_percentage != blue_is_percentage) + return {}; + + } else { + // Modern syntax + // [ | | none]{3} [ / [ | none] ]? + + green = parse_number_percentage_value(inner_tokens); + if (!green) + return {}; + inner_tokens.skip_whitespace(); + + blue = parse_number_percentage_value(inner_tokens); + if (!blue) + return {}; + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) { + alpha = parse_solidus_and_alpha_value(inner_tokens); + if (!alpha || inner_tokens.has_next_token()) + return {}; + } + } + + if (!alpha) + alpha = NumberStyleValue::create(1); + + transaction.commit(); + return CSSRGB::create(red.release_nonnull(), green.release_nonnull(), blue.release_nonnull(), alpha.release_nonnull()); +} + +// https://www.w3.org/TR/css-color-4/#funcdef-hsl +RefPtr Parser::parse_hsl_color_value(TokenStream& outer_tokens) +{ + // hsl() = [ | ] + // hsla() = [ | ] + // = hsl( + // [ | none] + // [ | | none] + // [ | | none] + // [ / [ | none] ]? ) + // = hsla( + // [ | none] + // [ | | none] + // [ | | none] + // [ / [ | none] ]? ) + // = hsl( , , , ? ) + // = hsla( , , , ? ) + // TODO: Handle none values + + auto transaction = outer_tokens.begin_transaction(); + outer_tokens.skip_whitespace(); + + auto& function_token = outer_tokens.next_token(); + if (!function_token.is_function("hsl"sv) && !function_token.is_function("hsla"sv)) + return {}; + + RefPtr h; + RefPtr s; + RefPtr l; + RefPtr alpha; + + auto inner_tokens = TokenStream { function_token.function().values() }; + inner_tokens.skip_whitespace(); + + h = parse_hue_value(inner_tokens); + if (!h) + return {}; + + inner_tokens.skip_whitespace(); + bool legacy_syntax = inner_tokens.peek_token().is(Token::Type::Comma); + if (legacy_syntax) { + // Legacy syntax + // , , , ? + (void)inner_tokens.next_token(); // comma + inner_tokens.skip_whitespace(); + + s = parse_percentage_value(inner_tokens); + if (!s) + return {}; + inner_tokens.skip_whitespace(); + + if (!inner_tokens.next_token().is(Token::Type::Comma)) + return {}; + inner_tokens.skip_whitespace(); + + l = parse_percentage_value(inner_tokens); + if (!l) + return {}; + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) { + // Try and read comma and alpha + if (!inner_tokens.next_token().is(Token::Type::Comma)) + return {}; + inner_tokens.skip_whitespace(); + + alpha = parse_number_percentage_value(inner_tokens); + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) + return {}; + } + } else { + // Modern syntax + // [ | none] + // [ | | none] + // [ | | none] + // [ / [ | none] ]? + + s = parse_number_percentage_value(inner_tokens); + if (!s) + return {}; + inner_tokens.skip_whitespace(); + + l = parse_number_percentage_value(inner_tokens); + if (!l) + return {}; + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) { + alpha = parse_solidus_and_alpha_value(inner_tokens); + if (!alpha || inner_tokens.has_next_token()) + return {}; + } + } + + if (!alpha) + alpha = NumberStyleValue::create(1); + + transaction.commit(); + return CSSHSL::create(h.release_nonnull(), s.release_nonnull(), l.release_nonnull(), alpha.release_nonnull()); +} + +// https://www.w3.org/TR/css-color-4/#funcdef-hwb +RefPtr Parser::parse_hwb_color_value(TokenStream& outer_tokens) +{ + // hwb() = hwb( + // [ | none] + // [ | | none] + // [ | | none] + // [ / [ | none] ]? ) + + auto transaction = outer_tokens.begin_transaction(); + outer_tokens.skip_whitespace(); + + auto& function_token = outer_tokens.next_token(); + if (!function_token.is_function("hwb"sv)) + return {}; + + RefPtr h; + RefPtr w; + RefPtr b; + RefPtr alpha; + + auto inner_tokens = TokenStream { function_token.function().values() }; + inner_tokens.skip_whitespace(); + + h = parse_hue_value(inner_tokens); + if (!h) + return {}; + inner_tokens.skip_whitespace(); + + w = parse_number_percentage_value(inner_tokens); + if (!w) + return {}; + inner_tokens.skip_whitespace(); + + b = parse_number_percentage_value(inner_tokens); + if (!b) + return {}; + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) { + alpha = parse_solidus_and_alpha_value(inner_tokens); + if (!alpha || inner_tokens.has_next_token()) + return {}; + } + + if (!alpha) + alpha = NumberStyleValue::create(1); + + transaction.commit(); + return CSSHWB::create(h.release_nonnull(), w.release_nonnull(), b.release_nonnull(), alpha.release_nonnull()); +} + +// https://www.w3.org/TR/css-color-4/#funcdef-oklab +RefPtr Parser::parse_oklab_color_value(TokenStream& outer_tokens) +{ + // oklab() = oklab( [ | | none] + // [ | | none] + // [ | | none] + // [ / [ | none] ]? ) + + auto transaction = outer_tokens.begin_transaction(); + outer_tokens.skip_whitespace(); + + auto& function_token = outer_tokens.next_token(); + if (!function_token.is_function("oklab"sv)) + return {}; + + RefPtr l; + RefPtr a; + RefPtr b; + RefPtr alpha; + + auto inner_tokens = TokenStream { function_token.function().values() }; + inner_tokens.skip_whitespace(); + + l = parse_number_percentage_value(inner_tokens); + if (!l) + return {}; + inner_tokens.skip_whitespace(); + + a = parse_number_percentage_value(inner_tokens); + if (!a) + return {}; + inner_tokens.skip_whitespace(); + + b = parse_number_percentage_value(inner_tokens); + if (!b) + return {}; + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) { + alpha = parse_solidus_and_alpha_value(inner_tokens); + if (!alpha || inner_tokens.has_next_token()) + return {}; + } + + if (!alpha) + alpha = NumberStyleValue::create(1); + + transaction.commit(); + return CSSOKLab::create(l.release_nonnull(), a.release_nonnull(), b.release_nonnull(), alpha.release_nonnull()); +} + +// https://www.w3.org/TR/css-color-4/#funcdef-oklch +RefPtr Parser::parse_oklch_color_value(TokenStream& outer_tokens) +{ + // oklch() = oklch( [ | | none] + // [ | | none] + // [ | none] + // [ / [ | none] ]? ) + + auto transaction = outer_tokens.begin_transaction(); + outer_tokens.skip_whitespace(); + + auto& function_token = outer_tokens.next_token(); + if (!function_token.is_function("oklch"sv)) + return {}; + + RefPtr l; + RefPtr c; + RefPtr h; + RefPtr alpha; + + auto inner_tokens = TokenStream { function_token.function().values() }; + inner_tokens.skip_whitespace(); + + l = parse_number_percentage_value(inner_tokens); + if (!l) + return {}; + inner_tokens.skip_whitespace(); + + c = parse_number_percentage_value(inner_tokens); + if (!c) + return {}; + inner_tokens.skip_whitespace(); + + h = parse_hue_value(inner_tokens); + if (!h) + return {}; + inner_tokens.skip_whitespace(); + + if (inner_tokens.has_next_token()) { + alpha = parse_solidus_and_alpha_value(inner_tokens); + if (!alpha || inner_tokens.has_next_token()) + return {}; + } + + if (!alpha) + alpha = NumberStyleValue::create(1); + + transaction.commit(); + return CSSOKLCH::create(l.release_nonnull(), c.release_nonnull(), h.release_nonnull(), alpha.release_nonnull()); +} + +// https://www.w3.org/TR/css-color-4/#color-syntax +RefPtr Parser::parse_color_value(TokenStream& tokens) +{ + + // Keywords: | | currentColor + { + auto transaction = tokens.begin_transaction(); + if (auto keyword = parse_keyword_value(tokens); keyword && keyword->has_color()) { + transaction.commit(); + return keyword; + } + } + + // Functions + if (auto rgb = parse_rgb_color_value(tokens)) + return rgb; + if (auto hsl = parse_hsl_color_value(tokens)) + return hsl; + if (auto hwb = parse_hwb_color_value(tokens)) + return hwb; + if (auto oklab = parse_oklab_color_value(tokens)) + return oklab; + if (auto oklch = parse_oklch_color_value(tokens)) + return oklch; + + auto transaction = tokens.begin_transaction(); tokens.skip_whitespace(); auto component_value = tokens.next_token(); - // https://www.w3.org/TR/css-color-4/ if (component_value.is(Token::Type::Ident)) { auto ident = component_value.token().ident(); auto color = Color::from_string(ident); if (color.has_value()) { transaction.commit(); - return color; + return CSSColorValue::create_from_color(color.release_value()); } // Otherwise, fall through to the hashless-hex-color case - } else if (component_value.is(Token::Type::Hash)) { + } + + if (component_value.is(Token::Type::Hash)) { auto color = Color::from_string(MUST(String::formatted("#{}", component_value.token().hash_value()))); - return commit_if_valid(color); - } else if (component_value.is_function()) { - auto const& function = component_value.function(); - auto const& values = function.values(); - auto const function_name = function.name(); - - if (function_name.equals_ignoring_ascii_case("rgb"sv) || function_name.equals_ignoring_ascii_case("rgba"sv)) - return commit_if_valid(parse_rgb_color(values)); - if (function_name.equals_ignoring_ascii_case("hsl"sv) || function_name.equals_ignoring_ascii_case("hsla"sv)) - return commit_if_valid(parse_hsl_color(values)); - if (function_name.equals_ignoring_ascii_case("hwb"sv)) - return commit_if_valid(parse_hwb_color(values)); - if (function_name.equals_ignoring_ascii_case("oklab"sv)) - return commit_if_valid(parse_oklab_color(values)); - if (function_name.equals_ignoring_ascii_case("oklch"sv)) - return commit_if_valid(parse_oklch_color(values)); - + if (color.has_value()) { + transaction.commit(); + return CSSColorValue::create_from_color(color.release_value()); + } return {}; } @@ -3149,26 +3134,16 @@ Optional Parser::parse_color(TokenStream& tokens) } // 6. Return the concatenation of "#" (U+0023) and serialization. - return commit_if_valid(Color::from_string(MUST(String::formatted("#{}", serialization)))); + auto color = Color::from_string(MUST(String::formatted("#{}", serialization))); + if (color.has_value()) { + transaction.commit(); + return CSSColorValue::create_from_color(color.release_value()); + } } return {}; } -RefPtr Parser::parse_color_value(TokenStream& tokens) -{ - if (auto color = parse_color(tokens); color.has_value()) - return CSSColorValue::create_from_color(color.value()); - - auto transaction = tokens.begin_transaction(); - if (auto keyword = parse_keyword_value(tokens); keyword && keyword->has_color()) { - transaction.commit(); - return keyword; - } - - return nullptr; -} - // https://drafts.csswg.org/css-lists-3/#counter-functions RefPtr Parser::parse_counter_value(TokenStream& tokens) { diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index 52fa1dcb19..e1e45fb183 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2020-2021, the SerenityOS developers. - * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2021-2024, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -194,12 +194,6 @@ private: Optional parse_time(TokenStream&); Optional parse_time_percentage(TokenStream&); - Optional parse_rgb_color(Vector const&); - Optional parse_hsl_color(Vector const&); - Optional parse_hwb_color(Vector const&); - Optional parse_oklab_color(Vector const&); - Optional parse_oklch_color(Vector const&); - Optional parse_color(TokenStream&); Optional parse_source_size_value(TokenStream&); Optional parse_ratio(TokenStream&); Optional parse_unicode_range(TokenStream&); @@ -239,6 +233,13 @@ private: OwnPtr parse_math_function(PropertyID, Function const&); OwnPtr parse_a_calc_function_node(Function const&); RefPtr parse_keyword_value(TokenStream&); + RefPtr parse_hue_value(TokenStream&); + RefPtr parse_solidus_and_alpha_value(TokenStream&); + RefPtr parse_rgb_color_value(TokenStream&); + RefPtr parse_hsl_color_value(TokenStream&); + RefPtr parse_hwb_color_value(TokenStream&); + RefPtr parse_oklab_color_value(TokenStream&); + RefPtr parse_oklch_color_value(TokenStream&); RefPtr parse_color_value(TokenStream&); RefPtr parse_counter_value(TokenStream&); enum class AllowReversed { diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 28c7db6419..837d7f5514 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -1431,8 +1431,12 @@ static NonnullRefPtr interpolate_value(DOM::Element& elemen switch (from.type()) { case CSSStyleValue::Type::Angle: return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta))); - case CSSStyleValue::Type::Color: - return CSSColorValue::create_from_color(interpolate_color(from.as_color().color(), to.as_color().color(), delta)); + case CSSStyleValue::Type::Color: { + Optional layout_node; + if (auto node = element.layout_node()) + layout_node = *node; + return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta)); + } case CSSStyleValue::Type::Integer: return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta)); case CSSStyleValue::Type::Length: { diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp index ca99b9beb8..860b993ab6 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp @@ -9,32 +9,116 @@ #include "CSSColorValue.h" #include +#include +#include +#include +#include +#include namespace Web::CSS { ValueComparingNonnullRefPtr CSSColorValue::create_from_color(Color color) { + auto make_rgb_color = [](Color const& color) { + return CSSRGB::create( + NumberStyleValue::create(color.red()), + NumberStyleValue::create(color.green()), + NumberStyleValue::create(color.blue()), + NumberStyleValue::create(color.alpha() / 255.0)); + }; + if (color.value() == 0) { - static auto transparent = adopt_ref(*new (nothrow) CSSColorValue(color)); + static auto transparent = make_rgb_color(color); return transparent; } if (color == Color::from_rgb(0x000000)) { - static auto black = adopt_ref(*new (nothrow) CSSColorValue(color)); + static auto black = make_rgb_color(color); return black; } if (color == Color::from_rgb(0xffffff)) { - static auto white = adopt_ref(*new (nothrow) CSSColorValue(color)); + static auto white = make_rgb_color(color); return white; } - return adopt_ref(*new (nothrow) CSSColorValue(color)); + return make_rgb_color(color); } -String CSSColorValue::to_string() const +Optional CSSColorValue::resolve_hue(CSSStyleValue const& style_value) { - return serialize_a_srgb_value(m_color); + // | | none + auto normalized = [](double number) { + return fmod(number, 360.0); + }; + + if (style_value.is_number()) + return normalized(style_value.as_number().number()); + + if (style_value.is_angle()) + return normalized(style_value.as_angle().angle().to_degrees()); + + if (style_value.is_calculated() && style_value.as_calculated().resolves_to_angle()) + return normalized(style_value.as_calculated().resolve_angle().value().to_degrees()); + + if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None) + return 0; + + return {}; +} + +Optional CSSColorValue::resolve_with_reference_value(CSSStyleValue const& style_value, float one_hundred_percent_value) +{ + // | | none + auto normalize_percentage = [one_hundred_percent_value](Percentage const& percentage) { + return percentage.as_fraction() * one_hundred_percent_value; + }; + + if (style_value.is_percentage()) + return normalize_percentage(style_value.as_percentage().percentage()); + + if (style_value.is_number()) + return style_value.as_number().number(); + + if (style_value.is_calculated()) { + auto const& calculated = style_value.as_calculated(); + if (calculated.resolves_to_number()) + return calculated.resolve_number().value(); + if (calculated.resolves_to_percentage()) + return normalize_percentage(calculated.resolve_percentage().value()); + } + + if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None) + return 0; + + return {}; +} + +Optional CSSColorValue::resolve_alpha(CSSStyleValue const& style_value) +{ + // | | none + auto normalized = [](double number) { + return clamp(number, 0.0, 1.0); + }; + + if (style_value.is_number()) + return normalized(style_value.as_number().number()); + + if (style_value.is_percentage()) + return normalized(style_value.as_percentage().percentage().as_fraction()); + + if (style_value.is_calculated()) { + auto const& calculated = style_value.as_calculated(); + if (calculated.resolves_to_number()) + return normalized(calculated.resolve_number().value()); + if (calculated.resolves_to_percentage()) + return normalized(calculated.resolve_percentage().value().as_fraction()); + } + + if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None) + return 0; + + return {}; } } diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h index 72add309fa..fc72fa29dd 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h @@ -15,26 +15,35 @@ namespace Web::CSS { // https://drafts.css-houdini.org/css-typed-om-1/#csscolorvalue -class CSSColorValue : public StyleValueWithDefaultOperators { +class CSSColorValue : public CSSStyleValue { public: static ValueComparingNonnullRefPtr create_from_color(Color color); virtual ~CSSColorValue() override = default; - Color color() const { return m_color; } - virtual String to_string() const override; virtual bool has_color() const override { return true; } - virtual Color to_color(Optional) const override { return m_color; } - bool properties_equal(CSSColorValue const& other) const { return m_color == other.m_color; } + enum class ColorType { + RGB, + HSL, + HWB, + OKLab, + OKLCH, + }; + ColorType color_type() const { return m_color_type; } -private: - explicit CSSColorValue(Color color) - : StyleValueWithDefaultOperators(Type::Color) - , m_color(color) +protected: + explicit CSSColorValue(ColorType color_type) + : CSSStyleValue(Type::Color) + , m_color_type(color_type) { } - Color m_color; + static Optional resolve_hue(CSSStyleValue const&); + static Optional resolve_with_reference_value(CSSStyleValue const&, float one_hundred_percent_value); + static Optional resolve_alpha(CSSStyleValue const&); + +private: + ColorType m_color_type; }; } diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp new file mode 100644 index 0000000000..a36ee11bb4 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CSSHSL.h" +#include +#include + +namespace Web::CSS { + +Color CSSHSL::to_color(Optional) const +{ + auto const h_val = resolve_hue(m_properties.h).value_or(0); + auto const s_val = resolve_with_reference_value(m_properties.s, 100.0).value_or(0); + auto const l_val = resolve_with_reference_value(m_properties.l, 100.0).value_or(0); + auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1); + + return Color::from_hsla(h_val, s_val / 100.0f, l_val / 100.0f, alpha_val); +} + +bool CSSHSL::equals(CSSStyleValue const& other) const +{ + if (type() != other.type()) + return false; + auto const& other_color = other.as_color(); + if (color_type() != other_color.color_type()) + return false; + auto const& other_hsl = verify_cast(other_color); + return m_properties == other_hsl.m_properties; +} + +// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values +String CSSHSL::to_string() const +{ + // FIXME: Do this properly, taking unresolved calculated values into account. + return serialize_a_srgb_value(to_color({})); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h new file mode 100644 index 0000000000..edf0603da4 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +// https://drafts.css-houdini.org/css-typed-om-1/#csshsl +class CSSHSL final : public CSSColorValue { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr h, ValueComparingNonnullRefPtr s, ValueComparingNonnullRefPtr l, ValueComparingRefPtr alpha = {}) + { + // alpha defaults to 1 + if (!alpha) + return adopt_ref(*new (nothrow) CSSHSL(move(h), move(s), move(l), NumberStyleValue::create(1))); + + return adopt_ref(*new (nothrow) CSSHSL(move(h), move(s), move(l), alpha.release_nonnull())); + } + virtual ~CSSHSL() override = default; + + CSSStyleValue const& h() const { return *m_properties.h; } + CSSStyleValue const& s() const { return *m_properties.s; } + CSSStyleValue const& l() const { return *m_properties.l; } + CSSStyleValue const& alpha() const { return *m_properties.alpha; } + + virtual Color to_color(Optional) const override; + + String to_string() const override; + + virtual bool equals(CSSStyleValue const& other) const override; + +private: + CSSHSL(ValueComparingNonnullRefPtr h, ValueComparingNonnullRefPtr s, ValueComparingNonnullRefPtr l, ValueComparingNonnullRefPtr alpha) + : CSSColorValue(ColorType::HSL) + , m_properties { .h = move(h), .s = move(s), .l = move(l), .alpha = move(alpha) } + { + } + + struct Properties { + ValueComparingNonnullRefPtr h; + ValueComparingNonnullRefPtr s; + ValueComparingNonnullRefPtr l; + ValueComparingNonnullRefPtr alpha; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.cpp new file mode 100644 index 0000000000..8f72011aee --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CSSHWB.h" +#include +#include + +namespace Web::CSS { + +Color CSSHWB::to_color(Optional) const +{ + auto const h_val = resolve_hue(m_properties.h).value_or(0); + auto const w_val = clamp(resolve_with_reference_value(m_properties.w, 100.0).value_or(0), 0, 100) / 100.0; + auto const b_val = clamp(resolve_with_reference_value(m_properties.b, 100.0).value_or(0), 0, 100) / 100.0; + auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1); + + if (w_val + b_val >= 1.0) { + auto to_byte = [](double value) { + return round_to(clamp(value * 255.0, 0.0, 255.0)); + }; + u8 gray = to_byte(w_val / (w_val + b_val)); + return Color(gray, gray, gray, to_byte(alpha_val)); + } + + float value = 1 - b_val; + float saturation = 1 - (w_val / value); + return Color::from_hsv(h_val, saturation, value).with_opacity(alpha_val); +} + +bool CSSHWB::equals(CSSStyleValue const& other) const +{ + if (type() != other.type()) + return false; + auto const& other_color = other.as_color(); + if (color_type() != other_color.color_type()) + return false; + auto const& other_hwb = verify_cast(other_color); + return m_properties == other_hwb.m_properties; +} + +// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values +String CSSHWB::to_string() const +{ + // FIXME: Do this properly, taking unresolved calculated values into account. + return serialize_a_srgb_value(to_color({})); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.h new file mode 100644 index 0000000000..c2753db2c7 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +// https://drafts.css-houdini.org/css-typed-om-1/#csshwb +class CSSHWB final : public CSSColorValue { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr h, ValueComparingNonnullRefPtr w, ValueComparingNonnullRefPtr b, ValueComparingRefPtr alpha = {}) + { + // alpha defaults to 1 + if (!alpha) + return adopt_ref(*new (nothrow) CSSHWB(move(h), move(w), move(b), NumberStyleValue::create(1))); + + return adopt_ref(*new (nothrow) CSSHWB(move(h), move(w), move(b), alpha.release_nonnull())); + } + virtual ~CSSHWB() override = default; + + CSSStyleValue const& h() const { return *m_properties.h; } + CSSStyleValue const& w() const { return *m_properties.w; } + CSSStyleValue const& b() const { return *m_properties.b; } + CSSStyleValue const& alpha() const { return *m_properties.alpha; } + + virtual Color to_color(Optional) const override; + + String to_string() const override; + + virtual bool equals(CSSStyleValue const& other) const override; + +private: + CSSHWB(ValueComparingNonnullRefPtr h, ValueComparingNonnullRefPtr w, ValueComparingNonnullRefPtr b, ValueComparingNonnullRefPtr alpha) + : CSSColorValue(ColorType::HWB) + , m_properties { .h = move(h), .w = move(w), .b = move(b), .alpha = move(alpha) } + { + } + + struct Properties { + ValueComparingNonnullRefPtr h; + ValueComparingNonnullRefPtr w; + ValueComparingNonnullRefPtr b; + ValueComparingNonnullRefPtr alpha; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.cpp new file mode 100644 index 0000000000..7be858c9a0 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CSSOKLCH.h" +#include +#include +#include +#include +#include +#include + +namespace Web::CSS { + +Color CSSOKLCH::to_color(Optional) const +{ + auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 1.0).value_or(0), 0, 1); + auto const c_val = max(resolve_with_reference_value(m_properties.c, 0.4).value_or(0), 0); + auto const h_val = AK::to_radians(resolve_hue(m_properties.h).value_or(0)); + auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1); + + return Color::from_oklab(l_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val); +} + +bool CSSOKLCH::equals(CSSStyleValue const& other) const +{ + if (type() != other.type()) + return false; + auto const& other_color = other.as_color(); + if (color_type() != other_color.color_type()) + return false; + auto const& other_oklch = verify_cast(other_color); + return m_properties == other_oklch.m_properties; +} + +// https://www.w3.org/TR/css-color-4/#serializing-oklab-oklch +String CSSOKLCH::to_string() const +{ + // FIXME: Do this properly, taking unresolved calculated values into account. + return serialize_a_srgb_value(to_color({})); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.h new file mode 100644 index 0000000000..b66e145076 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +// https://drafts.css-houdini.org/css-typed-om-1/#cssoklch +class CSSOKLCH final : public CSSColorValue { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr l, ValueComparingNonnullRefPtr c, ValueComparingNonnullRefPtr h, ValueComparingRefPtr alpha = {}) + { + // alpha defaults to 1 + if (!alpha) + return adopt_ref(*new (nothrow) CSSOKLCH(move(l), move(c), move(h), NumberStyleValue::create(1))); + + return adopt_ref(*new (nothrow) CSSOKLCH(move(l), move(c), move(h), alpha.release_nonnull())); + } + virtual ~CSSOKLCH() override = default; + + CSSStyleValue const& l() const { return *m_properties.l; } + CSSStyleValue const& c() const { return *m_properties.c; } + CSSStyleValue const& h() const { return *m_properties.h; } + CSSStyleValue const& alpha() const { return *m_properties.alpha; } + + virtual Color to_color(Optional) const override; + + String to_string() const override; + + virtual bool equals(CSSStyleValue const& other) const override; + +private: + CSSOKLCH(ValueComparingNonnullRefPtr l, ValueComparingNonnullRefPtr c, ValueComparingNonnullRefPtr h, ValueComparingNonnullRefPtr alpha) + : CSSColorValue(ColorType::OKLCH) + , m_properties { .l = move(l), .c = move(c), .h = move(h), .alpha = move(alpha) } + { + } + + struct Properties { + ValueComparingNonnullRefPtr l; + ValueComparingNonnullRefPtr c; + ValueComparingNonnullRefPtr h; + ValueComparingNonnullRefPtr alpha; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.cpp new file mode 100644 index 0000000000..2288c6750c --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CSSOKLab.h" +#include +#include +#include +#include +#include + +namespace Web::CSS { + +Color CSSOKLab::to_color(Optional) const +{ + auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 1.0).value_or(0), 0, 1); + auto const a_val = resolve_with_reference_value(m_properties.a, 0.4).value_or(0); + auto const b_val = resolve_with_reference_value(m_properties.b, 0.4).value_or(0); + auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1); + + return Color::from_oklab(l_val, a_val, b_val, alpha_val); +} + +bool CSSOKLab::equals(CSSStyleValue const& other) const +{ + if (type() != other.type()) + return false; + auto const& other_color = other.as_color(); + if (color_type() != other_color.color_type()) + return false; + auto const& other_oklab = verify_cast(other_color); + return m_properties == other_oklab.m_properties; +} + +// https://www.w3.org/TR/css-color-4/#serializing-oklab-oklch +String CSSOKLab::to_string() const +{ + // FIXME: Do this properly, taking unresolved calculated values into account. + return serialize_a_srgb_value(to_color({})); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.h new file mode 100644 index 0000000000..2d5b4ea0e6 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +// https://drafts.css-houdini.org/css-typed-om-1/#cssoklab +class CSSOKLab final : public CSSColorValue { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr l, ValueComparingNonnullRefPtr a, ValueComparingNonnullRefPtr b, ValueComparingRefPtr alpha = {}) + { + // alpha defaults to 1 + if (!alpha) + return adopt_ref(*new (nothrow) CSSOKLab(move(l), move(a), move(b), NumberStyleValue::create(1))); + + return adopt_ref(*new (nothrow) CSSOKLab(move(l), move(a), move(b), alpha.release_nonnull())); + } + virtual ~CSSOKLab() override = default; + + CSSStyleValue const& l() const { return *m_properties.l; } + CSSStyleValue const& a() const { return *m_properties.a; } + CSSStyleValue const& b() const { return *m_properties.b; } + CSSStyleValue const& alpha() const { return *m_properties.alpha; } + + virtual Color to_color(Optional) const override; + + String to_string() const override; + + virtual bool equals(CSSStyleValue const& other) const override; + +private: + CSSOKLab(ValueComparingNonnullRefPtr l, ValueComparingNonnullRefPtr a, ValueComparingNonnullRefPtr b, ValueComparingNonnullRefPtr alpha) + : CSSColorValue(ColorType::OKLab) + , m_properties { .l = move(l), .a = move(a), .b = move(b), .alpha = move(alpha) } + { + } + + struct Properties { + ValueComparingNonnullRefPtr l; + ValueComparingNonnullRefPtr a; + ValueComparingNonnullRefPtr b; + ValueComparingNonnullRefPtr alpha; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.cpp new file mode 100644 index 0000000000..483a244c53 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CSSRGB.h" +#include +#include +#include +#include +#include + +namespace Web::CSS { + +Color CSSRGB::to_color(Optional) const +{ + auto resolve_rgb_to_u8 = [](CSSStyleValue const& style_value) -> Optional { + // | | none + auto normalized = [](double number) { + return llround(clamp(number, 0.0, 255.0)); + }; + + if (style_value.is_number()) + return normalized(style_value.as_number().number()); + + if (style_value.is_percentage()) + return normalized(style_value.as_percentage().value() * 2.55); + + if (style_value.is_calculated()) { + auto const& calculated = style_value.as_calculated(); + if (calculated.resolves_to_number()) + return normalized(calculated.resolve_number().value()); + if (calculated.resolves_to_percentage()) + return normalized(calculated.resolve_percentage().value().value() * 2.55); + } + + if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None) + return 0; + + return {}; + }; + + auto resolve_alpha_to_u8 = [](CSSStyleValue const& style_value) -> Optional { + auto alpha_0_1 = resolve_alpha(style_value); + if (alpha_0_1.has_value()) + return llround(clamp(alpha_0_1.value() * 255.0, 0.0, 255.0)); + return {}; + }; + + u8 const r_val = resolve_rgb_to_u8(m_properties.r).value_or(0); + u8 const g_val = resolve_rgb_to_u8(m_properties.g).value_or(0); + u8 const b_val = resolve_rgb_to_u8(m_properties.b).value_or(0); + u8 const alpha_val = resolve_alpha_to_u8(m_properties.alpha).value_or(255); + + return Color(r_val, g_val, b_val, alpha_val); +} + +bool CSSRGB::equals(CSSStyleValue const& other) const +{ + if (type() != other.type()) + return false; + auto const& other_color = other.as_color(); + if (color_type() != other_color.color_type()) + return false; + auto const& other_rgb = verify_cast(other_color); + return m_properties == other_rgb.m_properties; +} + +// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values +String CSSRGB::to_string() const +{ + // FIXME: Do this properly, taking unresolved calculated values into account. + return serialize_a_srgb_value(to_color({})); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.h new file mode 100644 index 0000000000..6fcfcdc843 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +// https://drafts.css-houdini.org/css-typed-om-1/#cssrgb +class CSSRGB final : public CSSColorValue { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr r, ValueComparingNonnullRefPtr g, ValueComparingNonnullRefPtr b, ValueComparingRefPtr alpha = {}) + { + // alpha defaults to 1 + if (!alpha) + return adopt_ref(*new (nothrow) CSSRGB(move(r), move(g), move(b), NumberStyleValue::create(1))); + + return adopt_ref(*new (nothrow) CSSRGB(move(r), move(g), move(b), alpha.release_nonnull())); + } + virtual ~CSSRGB() override = default; + + CSSStyleValue const& r() const { return *m_properties.r; } + CSSStyleValue const& g() const { return *m_properties.g; } + CSSStyleValue const& b() const { return *m_properties.b; } + CSSStyleValue const& alpha() const { return *m_properties.alpha; } + + virtual Color to_color(Optional) const override; + + String to_string() const override; + + virtual bool equals(CSSStyleValue const& other) const override; + +private: + CSSRGB(ValueComparingNonnullRefPtr r, ValueComparingNonnullRefPtr g, ValueComparingNonnullRefPtr b, ValueComparingNonnullRefPtr alpha) + : CSSColorValue(ColorType::RGB) + , m_properties { .r = move(r), .g = move(g), .b = move(b), .alpha = move(alpha) } + { + } + + struct Properties { + ValueComparingNonnullRefPtr r; + ValueComparingNonnullRefPtr g; + ValueComparingNonnullRefPtr b; + ValueComparingNonnullRefPtr alpha; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 90c6a2d733..9e4e14e665 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -106,11 +106,16 @@ class CSSColorValue; class CSSConditionRule; class CSSFontFaceRule; class CSSGroupingRule; +class CSSHSL; +class CSSHWB; class CSSImportRule; class CSSKeyframeRule; class CSSKeyframesRule; class CSSKeywordValue; class CSSMediaRule; +class CSSOKLab; +class CSSOKLCH; +class CSSRGB; class CSSRule; class CSSRuleList; class CSSStyleDeclaration; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp index e062412c9c..3fec539293 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp @@ -76,7 +76,7 @@ void HTMLMetaElement::inserted() auto css_value = parse_css_value(context, value, CSS::PropertyID::Color); if (css_value.is_null() || !css_value->is_color()) return; - auto color = css_value->as_color().color(); + auto color = css_value->to_color({}); // TODO: Pass a layout node? // 4. If color is not failure, then return color. document().page().client().page_did_change_theme_color(color);