diff --git a/Tests/LibWeb/Screenshot/clip-path-basic-shapes.html b/Tests/LibWeb/Screenshot/clip-path-basic-shapes.html new file mode 100644 index 00000000000..3aa88ff9475 --- /dev/null +++ b/Tests/LibWeb/Screenshot/clip-path-basic-shapes.html @@ -0,0 +1,126 @@ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Screenshot/images/clip-path-basic-shapes-ref.png b/Tests/LibWeb/Screenshot/images/clip-path-basic-shapes-ref.png new file mode 100644 index 00000000000..e8ec2cb0f86 Binary files /dev/null and b/Tests/LibWeb/Screenshot/images/clip-path-basic-shapes-ref.png differ diff --git a/Tests/LibWeb/Screenshot/reference/clip-path-basic-shapes-ref.html b/Tests/LibWeb/Screenshot/reference/clip-path-basic-shapes-ref.html new file mode 100644 index 00000000000..9db24d323e4 --- /dev/null +++ b/Tests/LibWeb/Screenshot/reference/clip-path-basic-shapes-ref.html @@ -0,0 +1,9 @@ + + diff --git a/Userland/Libraries/LibWeb/CSS/LengthBox.h b/Userland/Libraries/LibWeb/CSS/LengthBox.h index bdd7be3641c..f97e507cd1d 100644 --- a/Userland/Libraries/LibWeb/CSS/LengthBox.h +++ b/Userland/Libraries/LibWeb/CSS/LengthBox.h @@ -27,6 +27,8 @@ public: LengthPercentage const& bottom() const { return m_bottom; } LengthPercentage const& left() const { return m_left; } + bool operator==(LengthBox const&) const = default; + private: LengthPercentage m_top; LengthPercentage m_right; diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 89c4d543f5f..072497de9b0 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -1236,6 +1236,37 @@ RefPtr Parser::parse_url_value(TokenStream& token return URLStyleValue::create(*url); } +// https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius +Optional Parser::parse_shape_radius(TokenStream& tokens) +{ + auto transaction = tokens.begin_transaction(); + tokens.discard_whitespace(); + auto maybe_radius = parse_length_percentage(tokens); + if (maybe_radius.has_value()) { + // Negative radius is invalid. + auto radius = maybe_radius.value(); + if ((radius.is_length() && radius.length().raw_value() < 0) || (radius.is_percentage() && radius.percentage().value() < 0)) + return {}; + + transaction.commit(); + return radius; + } + + if (tokens.next_token().is_ident("closest-side"sv)) { + tokens.discard_a_token(); + transaction.commit(); + return FitSide::ClosestSide; + } + + if (tokens.next_token().is_ident("farthest-side"sv)) { + tokens.discard_a_token(); + transaction.commit(); + return FitSide::FarthestSide; + } + + return {}; +} + RefPtr Parser::parse_basic_shape_value(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); @@ -1245,38 +1276,237 @@ RefPtr Parser::parse_basic_shape_value(TokenStream{1,4} [ round <'border-radius'> ]? ) + // FIXME: Parse the border-radius. + auto arguments_tokens = TokenStream { component_value.function().value }; - // polygon() = polygon( <'fill-rule'>? , [ ]# ) - // FIXME: Parse the fill-rule. - auto arguments_tokens = TokenStream { component_value.function().value }; - auto arguments = parse_a_comma_separated_list_of_component_values(arguments_tokens); + // If less than four values are provided, + // the omitted values default in the same way as the margin shorthand: + // an omitted second or third value defaults to the first, and an omitted fourth value defaults to the second. - Vector points; - for (auto& argument : arguments) { - TokenStream argument_tokens { argument }; + // The four s define the position of the top, right, bottom, and left edges of a rectangle. - argument_tokens.discard_whitespace(); - auto x_pos = parse_length_percentage(argument_tokens); - if (!x_pos.has_value()) + arguments_tokens.discard_whitespace(); + auto top = parse_length_percentage(arguments_tokens); + if (!top.has_value()) return nullptr; - argument_tokens.discard_whitespace(); - auto y_pos = parse_length_percentage(argument_tokens); - if (!y_pos.has_value()) + arguments_tokens.discard_whitespace(); + auto right = parse_length_percentage(arguments_tokens); + if (!right.has_value()) + right = top; + + arguments_tokens.discard_whitespace(); + auto bottom = parse_length_percentage(arguments_tokens); + if (!bottom.has_value()) + bottom = top; + + arguments_tokens.discard_whitespace(); + auto left = parse_length_percentage(arguments_tokens); + if (!left.has_value()) + left = right; + + arguments_tokens.discard_whitespace(); + if (arguments_tokens.has_next_token()) return nullptr; - argument_tokens.discard_whitespace(); - if (argument_tokens.has_next_token()) - return nullptr; - - points.append(Polygon::Point { *x_pos, *y_pos }); + transaction.commit(); + return BasicShapeStyleValue::create(Inset { LengthBox(top.value(), right.value(), bottom.value(), left.value()) }); } - transaction.commit(); - return BasicShapeStyleValue::create(Polygon { FillRule::Nonzero, move(points) }); + if (function_name.equals_ignoring_ascii_case("xywh"sv)) { + // xywh() = xywh( {2} {2} [ round <'border-radius'> ]? ) + // FIXME: Parse the border-radius. + auto arguments_tokens = TokenStream { component_value.function().value }; + + arguments_tokens.discard_whitespace(); + auto x = parse_length_percentage(arguments_tokens); + if (!x.has_value()) + return nullptr; + + arguments_tokens.discard_whitespace(); + auto y = parse_length_percentage(arguments_tokens); + if (!y.has_value()) + return nullptr; + + arguments_tokens.discard_whitespace(); + auto width = parse_length_percentage(arguments_tokens); + if (!width.has_value()) + return nullptr; + + arguments_tokens.discard_whitespace(); + auto height = parse_length_percentage(arguments_tokens); + if (!height.has_value()) + return nullptr; + + arguments_tokens.discard_whitespace(); + if (arguments_tokens.has_next_token()) + return nullptr; + + // Negative width or height is invalid. + if ((width->is_length() && width->length().raw_value() < 0) + || (width->is_percentage() && width->percentage().value() < 0) + || (height->is_length() && height->length().raw_value() < 0) + || (height->is_percentage() && height->percentage().value() < 0)) + return nullptr; + + transaction.commit(); + return BasicShapeStyleValue::create(Xywh { x.value(), y.value(), width.value(), height.value() }); + } + + if (function_name.equals_ignoring_ascii_case("rect"sv)) { + // rect() = rect( [ | auto ]{4} [ round <'border-radius'> ]? ) + // FIXME: Parse the border-radius. + auto arguments_tokens = TokenStream { component_value.function().value }; + + auto parse_length_percentage_or_auto = [this](TokenStream& tokens) -> Optional { + tokens.discard_whitespace(); + auto value = parse_length_percentage(tokens); + if (!value.has_value()) { + if (tokens.consume_a_token().is_ident("auto"sv)) { + value = Length::make_auto(); + } + } + return value; + }; + + auto top = parse_length_percentage_or_auto(arguments_tokens); + auto right = parse_length_percentage_or_auto(arguments_tokens); + auto bottom = parse_length_percentage_or_auto(arguments_tokens); + auto left = parse_length_percentage_or_auto(arguments_tokens); + + if (!top.has_value() || !right.has_value() || !bottom.has_value() || !left.has_value()) + return nullptr; + + arguments_tokens.discard_whitespace(); + if (arguments_tokens.has_next_token()) + return nullptr; + + transaction.commit(); + return BasicShapeStyleValue::create(Rect { LengthBox(top.value(), right.value(), bottom.value(), left.value()) }); + } + + if (function_name.equals_ignoring_ascii_case("circle"sv)) { + // circle() = circle( ? [ at ]? ) + auto arguments_tokens = TokenStream { component_value.function().value }; + + auto radius = parse_shape_radius(arguments_tokens).value_or(FitSide::ClosestSide); + + auto position = PositionStyleValue::create_center(); + arguments_tokens.discard_whitespace(); + if (arguments_tokens.next_token().is_ident("at"sv)) { + arguments_tokens.discard_a_token(); + arguments_tokens.discard_whitespace(); + auto maybe_position = parse_position_value(arguments_tokens); + if (maybe_position.is_null()) + return nullptr; + + position = maybe_position.release_nonnull(); + } + + arguments_tokens.discard_whitespace(); + if (arguments_tokens.has_next_token()) + return nullptr; + + transaction.commit(); + return BasicShapeStyleValue::create(Circle { radius, position }); + } + + if (function_name.equals_ignoring_ascii_case("ellipse"sv)) { + // ellipse() = ellipse( [ {2} ]? [ at ]? ) + auto arguments_tokens = TokenStream { component_value.function().value }; + + Optional radius_x = parse_shape_radius(arguments_tokens); + Optional radius_y = parse_shape_radius(arguments_tokens); + + if (radius_x.has_value() && !radius_y.has_value()) + return nullptr; + + if (!radius_x.has_value()) { + radius_x = FitSide::ClosestSide; + radius_y = FitSide::ClosestSide; + } + + auto position = PositionStyleValue::create_center(); + arguments_tokens.discard_whitespace(); + if (arguments_tokens.next_token().is_ident("at"sv)) { + arguments_tokens.discard_a_token(); + arguments_tokens.discard_whitespace(); + auto maybe_position = parse_position_value(arguments_tokens); + if (maybe_position.is_null()) + return nullptr; + + position = maybe_position.release_nonnull(); + } + + arguments_tokens.discard_whitespace(); + if (arguments_tokens.has_next_token()) + return nullptr; + + transaction.commit(); + return BasicShapeStyleValue::create(Ellipse { radius_x.value(), radius_y.value(), position }); + } + + if (function_name.equals_ignoring_ascii_case("polygon"sv)) { + // polygon() = polygon( <'fill-rule'>? , [ ]# ) + auto arguments_tokens = TokenStream { component_value.function().value }; + auto arguments = parse_a_comma_separated_list_of_component_values(arguments_tokens); + + if (arguments.size() < 1) + return nullptr; + + Optional fill_rule; + auto first_argument = arguments[0]; + TokenStream first_argument_tokens { first_argument }; + + first_argument_tokens.discard_whitespace(); + if (first_argument_tokens.next_token().is_ident("nonzero"sv)) { + fill_rule = FillRule::Nonzero; + } else if (first_argument_tokens.next_token().is_ident("evenodd"sv)) { + fill_rule = FillRule::Evenodd; + } + + if (fill_rule.has_value()) { + first_argument_tokens.discard_a_token(); + if (first_argument_tokens.has_next_token()) + return nullptr; + + arguments.remove(0); + } else { + fill_rule = FillRule::Nonzero; + } + + if (arguments.size() < 1) + return nullptr; + + Vector points; + for (auto& argument : arguments) { + TokenStream argument_tokens { argument }; + + argument_tokens.discard_whitespace(); + auto x_pos = parse_length_percentage(argument_tokens); + if (!x_pos.has_value()) + return nullptr; + + argument_tokens.discard_whitespace(); + auto y_pos = parse_length_percentage(argument_tokens); + if (!y_pos.has_value()) + return nullptr; + + argument_tokens.discard_whitespace(); + if (argument_tokens.has_next_token()) + return nullptr; + + points.append(Polygon::Point { *x_pos, *y_pos }); + } + + transaction.commit(); + return BasicShapeStyleValue::create(Polygon { fill_rule.value(), move(points) }); + } + + return nullptr; } Optional Parser::parse_layer_name(TokenStream& tokens, AllowBlankLayerName allow_blank_layer_name) diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index 9316341c507..763fae505df 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -221,6 +221,7 @@ private: Optional parse_url_function(TokenStream&); RefPtr parse_url_value(TokenStream&); + Optional parse_shape_radius(TokenStream&); RefPtr parse_basic_shape_value(TokenStream&); template diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.cpp index e26a2ddb48f..a1091c556ae 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.cpp @@ -9,14 +9,178 @@ namespace Web::CSS { +static Gfx::Path path_from_resolved_rect(float top, float right, float bottom, float left) +{ + Gfx::Path path; + path.move_to(Gfx::FloatPoint { left, top }); + path.line_to(Gfx::FloatPoint { right, top }); + path.line_to(Gfx::FloatPoint { right, bottom }); + path.line_to(Gfx::FloatPoint { left, bottom }); + path.close(); + return path; +} + +Gfx::Path Inset::to_path(CSSPixelRect reference_box, Layout::Node const& node) const +{ + // FIXME: A pair of insets in either dimension that add up to more than the used dimension + // (such as left and right insets of 75% apiece) use the CSS Backgrounds 3 § 4.5 Overlapping Curves rules + // to proportionally reduce the inset effect to 100%. + + auto top = inset_box.top().to_px(node, reference_box.height()).to_float(); + auto right = reference_box.width().to_float() - inset_box.right().to_px(node, reference_box.width()).to_float(); + auto bottom = reference_box.height().to_float() - inset_box.bottom().to_px(node, reference_box.height()).to_float(); + auto left = inset_box.left().to_px(node, reference_box.width()).to_float(); + + return path_from_resolved_rect(top, right, bottom, left); +} + +String Inset::to_string() const +{ + return MUST(String::formatted("inset({} {} {} {})", inset_box.top(), inset_box.right(), inset_box.bottom(), inset_box.left())); +} + +Gfx::Path Xywh::to_path(CSSPixelRect reference_box, Layout::Node const& node) const +{ + auto top = y.to_px(node, reference_box.height()).to_float(); + auto bottom = top + max(0.0f, height.to_px(node, reference_box.height()).to_float()); + auto left = x.to_px(node, reference_box.width()).to_float(); + auto right = left + max(0.0f, width.to_px(node, reference_box.width()).to_float()); + + return path_from_resolved_rect(top, right, bottom, left); +} + +String Xywh::to_string() const +{ + return MUST(String::formatted("xywh({} {} {} {})", x, y, width, height)); +} + +Gfx::Path Rect::to_path(CSSPixelRect reference_box, Layout::Node const& node) const +{ + // An auto value makes the edge of the box coincide with the corresponding edge of the reference box: + // it’s equivalent to 0% as the first (top) or fourth (left) value, and equivalent to 100% as the second (right) or third (bottom) value. + + auto top = box.top().is_auto() ? 0 : box.top().to_px(node, reference_box.height()).to_float(); + auto right = box.right().is_auto() ? reference_box.width().to_float() : box.right().to_px(node, reference_box.width()).to_float(); + auto bottom = box.bottom().is_auto() ? reference_box.height().to_float() : box.bottom().to_px(node, reference_box.height()).to_float(); + auto left = box.left().is_auto() ? 0 : box.left().to_px(node, reference_box.width()).to_float(); + + // The second (right) and third (bottom) values are floored by the fourth (left) and second (top) values, respectively. + return path_from_resolved_rect(top, max(right, left), max(bottom, top), left); +} + +String Rect::to_string() const +{ + return MUST(String::formatted("rect({} {} {} {})", box.top(), box.right(), box.bottom(), box.left())); +} + +static String radius_to_string(ShapeRadius radius) +{ + return radius.visit( + [](LengthPercentage const& length_percentage) { return length_percentage.to_string(); }, + [](FitSide const& side) { + switch (side) { + case FitSide::ClosestSide: + return "closest-side"_string; + case FitSide::FarthestSide: + return "farthest-side"_string; + } + VERIFY_NOT_REACHED(); + }); +} + +Gfx::Path Circle::to_path(CSSPixelRect reference_box, Layout::Node const& node) const +{ + // Translating the reference box because PositionStyleValues are resolved to an absolute position. + auto center = position->resolved(node, reference_box.translated(-reference_box.x(), -reference_box.y())); + + float radius_px = radius.visit( + [&](LengthPercentage const& length_percentage) { + auto radius_ref = sqrt(pow(reference_box.width().to_float(), 2) + pow(reference_box.height().to_float(), 2)) / AK::Sqrt2; + return max(0.0f, length_percentage.to_px(node, CSSPixels(radius_ref)).to_float()); + }, + [&](FitSide const& side) { + switch (side) { + case FitSide::ClosestSide: + float closest; + closest = min(abs(center.x()), abs(center.y())).to_float(); + closest = min(closest, abs(reference_box.width() - center.x()).to_float()); + closest = min(closest, abs(reference_box.height() - center.y()).to_float()); + return closest; + case FitSide::FarthestSide: + float farthest; + farthest = max(abs(center.x()), abs(center.y())).to_float(); + farthest = max(farthest, abs(reference_box.width() - center.x()).to_float()); + farthest = max(farthest, abs(reference_box.height() - center.y()).to_float()); + return farthest; + } + VERIFY_NOT_REACHED(); + }); + + Gfx::Path path; + path.move_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + radius_px }); + path.arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() - radius_px }, radius_px, true, true); + path.arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + radius_px }, radius_px, true, true); + return path; +} + +String Circle::to_string() const +{ + return MUST(String::formatted("circle({} at {})", radius_to_string(radius), position->to_string())); +} + +Gfx::Path Ellipse::to_path(CSSPixelRect reference_box, Layout::Node const& node) const +{ + // Translating the reference box because PositionStyleValues are resolved to an absolute position. + auto center = position->resolved(node, reference_box.translated(-reference_box.x(), -reference_box.y())); + + float radius_x_px = radius_x.visit( + [&](LengthPercentage const& length_percentage) { + return max(0.0f, length_percentage.to_px(node, reference_box.width()).to_float()); + }, + [&](FitSide const& side) { + switch (side) { + case FitSide::ClosestSide: + return min(abs(center.x()), abs(reference_box.width() - center.x())).to_float(); + case FitSide::FarthestSide: + return max(abs(center.x()), abs(reference_box.width() - center.x())).to_float(); + } + VERIFY_NOT_REACHED(); + }); + + float radius_y_px = radius_y.visit( + [&](LengthPercentage const& length_percentage) { + return max(0.0f, length_percentage.to_px(node, reference_box.height()).to_float()); + }, + [&](FitSide const& side) { + switch (side) { + case FitSide::ClosestSide: + return min(abs(center.y()), abs(reference_box.height() - center.y())).to_float(); + case FitSide::FarthestSide: + return max(abs(center.y()), abs(reference_box.height() - center.y())).to_float(); + } + VERIFY_NOT_REACHED(); + }); + + Gfx::Path path; + path.move_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + radius_y_px }); + path.elliptical_arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() - radius_y_px }, Gfx::FloatSize { radius_x_px, radius_y_px }, 0, true, true); + path.elliptical_arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + radius_y_px }, Gfx::FloatSize { radius_x_px, radius_y_px }, 0, true, true); + return path; +} + +String Ellipse::to_string() const +{ + return MUST(String::formatted("ellipse({} {} at {})", radius_to_string(radius_x), radius_to_string(radius_y), position->to_string())); +} + Gfx::Path Polygon::to_path(CSSPixelRect reference_box, Layout::Node const& node) const { Gfx::Path path; bool first = true; for (auto const& point : points) { Gfx::FloatPoint resolved_point { - static_cast(point.x.to_px(node, reference_box.width())), - static_cast(point.y.to_px(node, reference_box.height())) + point.x.to_px(node, reference_box.width()).to_float(), + point.y.to_px(node, reference_box.height()).to_float() }; if (first) path.move_to(resolved_point); diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.h index 02436566be1..b046648ccbf 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.h @@ -7,12 +7,71 @@ #pragma once #include -#include #include +#include #include +#include namespace Web::CSS { +struct Inset { + Gfx::Path to_path(CSSPixelRect reference_box, Layout::Node const&) const; + String to_string() const; + + bool operator==(Inset const&) const = default; + + LengthBox inset_box; +}; + +struct Xywh { + Gfx::Path to_path(CSSPixelRect reference_box, Layout::Node const&) const; + String to_string() const; + + bool operator==(Xywh const&) const = default; + + LengthPercentage x; + LengthPercentage y; + LengthPercentage width; + LengthPercentage height; +}; + +struct Rect { + Gfx::Path to_path(CSSPixelRect reference_box, Layout::Node const&) const; + String to_string() const; + + bool operator==(Rect const&) const = default; + + LengthBox box; +}; + +enum class FitSide { + ClosestSide, + FarthestSide, +}; + +using ShapeRadius = Variant; + +struct Circle { + Gfx::Path to_path(CSSPixelRect reference_box, Layout::Node const&) const; + String to_string() const; + + bool operator==(Circle const&) const = default; + + ShapeRadius radius; + ValueComparingNonnullRefPtr position; +}; + +struct Ellipse { + Gfx::Path to_path(CSSPixelRect reference_box, Layout::Node const&) const; + String to_string() const; + + bool operator==(Ellipse const&) const = default; + + ShapeRadius radius_x; + ShapeRadius radius_y; + ValueComparingNonnullRefPtr position; +}; + struct Polygon { struct Point { bool operator==(Point const&) const = default; @@ -25,12 +84,13 @@ struct Polygon { bool operator==(Polygon const&) const = default; + // FIXME: Actually use the fill rule FillRule fill_rule; Vector points; }; -// FIXME: Implement other basic shapes. See: https://www.w3.org/TR/css-shapes-1/#basic-shape-functions -using BasicShape = Variant; +// FIXME: Implement path(). See: https://www.w3.org/TR/css-shapes-1/#basic-shape-functions +using BasicShape = Variant; class BasicShapeStyleValue : public StyleValueWithDefaultOperators { public: