LibWeb/CSS: Implement path() basic shape

This commit is contained in:
Sam Atkins 2025-07-17 16:39:45 +01:00 committed by Alexander Kalenik
commit 0ff61e5e7b
Notes: github-actions[bot] 2025-07-17 18:00:31 +00:00
5 changed files with 115 additions and 22 deletions

View file

@ -2965,6 +2965,25 @@ RefPtr<CSSStyleValue const> Parser::parse_basic_shape_value(TokenStream<Componen
auto function_name = component_value.function().name.bytes_as_string_view(); auto function_name = component_value.function().name.bytes_as_string_view();
auto parse_fill_rule_argument = [](Vector<ComponentValue> const& component_values) -> Optional<Gfx::WindingRule> {
TokenStream tokens { component_values };
tokens.discard_whitespace();
auto& maybe_ident = tokens.consume_a_token();
tokens.discard_whitespace();
if (tokens.has_next_token())
return {};
if (maybe_ident.is_ident("nonzero"sv))
return Gfx::WindingRule::Nonzero;
if (maybe_ident.is_ident("evenodd"sv))
return Gfx::WindingRule::EvenOdd;
return {};
};
// FIXME: Implement path(). See: https://www.w3.org/TR/css-shapes-1/#basic-shape-functions // FIXME: Implement path(). See: https://www.w3.org/TR/css-shapes-1/#basic-shape-functions
if (function_name.equals_ignoring_ascii_case("inset"sv)) { if (function_name.equals_ignoring_ascii_case("inset"sv)) {
// inset() = inset( <length-percentage>{1,4} [ round <'border-radius'> ]? ) // inset() = inset( <length-percentage>{1,4} [ round <'border-radius'> ]? )
@ -3147,21 +3166,9 @@ RefPtr<CSSStyleValue const> Parser::parse_basic_shape_value(TokenStream<Componen
return nullptr; return nullptr;
Optional<Gfx::WindingRule> fill_rule; Optional<Gfx::WindingRule> fill_rule;
auto const& first_argument = arguments[0]; fill_rule = parse_fill_rule_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 = Gfx::WindingRule::Nonzero;
} else if (first_argument_tokens.next_token().is_ident("evenodd"sv)) {
fill_rule = Gfx::WindingRule::EvenOdd;
}
if (fill_rule.has_value()) { if (fill_rule.has_value()) {
first_argument_tokens.discard_a_token();
if (first_argument_tokens.has_next_token())
return nullptr;
arguments.remove(0); arguments.remove(0);
} else { } else {
fill_rule = Gfx::WindingRule::Nonzero; fill_rule = Gfx::WindingRule::Nonzero;
@ -3195,6 +3202,39 @@ RefPtr<CSSStyleValue const> Parser::parse_basic_shape_value(TokenStream<Componen
return BasicShapeStyleValue::create(Polygon { fill_rule.value(), move(points) }); return BasicShapeStyleValue::create(Polygon { fill_rule.value(), move(points) });
} }
if (function_name.equals_ignoring_ascii_case("path"sv)) {
// <path()> = path( <'fill-rule'>?, <string> )
auto arguments_tokens = TokenStream { component_value.function().value };
auto arguments = parse_a_comma_separated_list_of_component_values(arguments_tokens);
if (arguments.size() < 1 || arguments.size() > 2)
return nullptr;
// <'fill-rule'>?
Gfx::WindingRule fill_rule { Gfx::WindingRule::Nonzero };
if (arguments.size() == 2) {
auto maybe_fill_rule = parse_fill_rule_argument(arguments[0]);
if (!maybe_fill_rule.has_value())
return nullptr;
fill_rule = maybe_fill_rule.release_value();
}
// <string>, which is a path string
TokenStream path_argument_tokens { arguments.last() };
path_argument_tokens.discard_whitespace();
auto& maybe_string = path_argument_tokens.consume_a_token();
path_argument_tokens.discard_whitespace();
if (!maybe_string.is(Token::Type::String) || path_argument_tokens.has_next_token())
return nullptr;
auto path_data = SVG::AttributeParser::parse_path_data(maybe_string.token().string().to_string());
if (path_data.instructions().is_empty())
return nullptr;
transaction.commit();
return BasicShapeStyleValue::create(Path { fill_rule, move(path_data) });
}
return nullptr; return nullptr;
} }

View file

@ -1,11 +1,14 @@
/* /*
* Copyright (c) 2024, MacDue <macdue@dueutil.tech> * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include "BasicShapeStyleValue.h" #include "BasicShapeStyleValue.h"
#include <LibGfx/Path.h> #include <LibGfx/Path.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/SVG/Path.h>
namespace Web::CSS { namespace Web::CSS {
@ -211,6 +214,43 @@ String Polygon::to_string(SerializationMode) const
return MUST(builder.to_string()); return MUST(builder.to_string());
} }
Gfx::Path Path::to_path(CSSPixelRect, Layout::Node const&) const
{
auto result = path_instructions.to_gfx_path();
// The UA must close a path with an implicit closepath command ("z" or "Z") if it is not present in the string for
// properties that require a closed loop (such as shape-outside and clip-path).
// https://drafts.csswg.org/css-shapes/#funcdef-basic-shape-path
// FIXME: For now, all users want a closed path, so we'll always close it.
result.close_all_subpaths();
result.set_fill_type(fill_rule);
return result;
}
// https://drafts.csswg.org/css-shapes/#basic-shape-serialization
String Path::to_string(SerializationMode mode) const
{
StringBuilder builder;
builder.append("path("sv);
// For serializing computed values, component values are computed, and omitted when possible without changing the meaning.
// NB: So, we don't include `nonzero` in that case.
if (!(mode == SerializationMode::ResolvedValue && fill_rule == Gfx::WindingRule::Nonzero)) {
switch (fill_rule) {
case Gfx::WindingRule::Nonzero:
builder.append("nonzero, "sv);
break;
case Gfx::WindingRule::EvenOdd:
builder.append("evenodd, "sv);
}
}
serialize_a_string(builder, path_instructions.serialize());
builder.append(')');
return builder.to_string_without_validation();
}
BasicShapeStyleValue::~BasicShapeStyleValue() = default; BasicShapeStyleValue::~BasicShapeStyleValue() = default;
Gfx::Path BasicShapeStyleValue::to_path(CSSPixelRect reference_box, Layout::Node const& node) const Gfx::Path BasicShapeStyleValue::to_path(CSSPixelRect reference_box, Layout::Node const& node) const

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2024, MacDue <macdue@dueutil.tech> * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -12,6 +13,7 @@
#include <LibWeb/CSS/LengthBox.h> #include <LibWeb/CSS/LengthBox.h>
#include <LibWeb/CSS/PercentageOr.h> #include <LibWeb/CSS/PercentageOr.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h> #include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
#include <LibWeb/SVG/AttributeParser.h>
namespace Web::CSS { namespace Web::CSS {
@ -89,8 +91,19 @@ struct Polygon {
Vector<Point> points; Vector<Point> points;
}; };
// FIXME: Implement path(). See: https://www.w3.org/TR/css-shapes-1/#basic-shape-functions // https://drafts.csswg.org/css-shapes/#funcdef-basic-shape-path
using BasicShape = Variant<Inset, Xywh, Rect, Circle, Ellipse, Polygon>; struct Path {
Gfx::Path to_path(CSSPixelRect reference_box, Layout::Node const&) const;
String to_string(SerializationMode) const;
bool operator==(Path const&) const = default;
Gfx::WindingRule fill_rule;
SVG::Path path_instructions;
};
// https://www.w3.org/TR/css-shapes-1/#basic-shape-functions
using BasicShape = Variant<Inset, Xywh, Rect, Circle, Ellipse, Polygon, Path>;
class BasicShapeStyleValue : public StyleValueWithDefaultOperators<BasicShapeStyleValue> { class BasicShapeStyleValue : public StyleValueWithDefaultOperators<BasicShapeStyleValue> {
public: public:

View file

@ -2,7 +2,7 @@ Harness status: OK
Found 3 tests Found 3 tests
3 Fail 3 Pass
Fail Property clip-path value 'path(nonzero, 'M10,10h80v80h-80zM25,25h50v50h-50z')' Pass Property clip-path value 'path(nonzero, 'M10,10h80v80h-80zM25,25h50v50h-50z')'
Fail Property clip-path value 'path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50z')' Pass Property clip-path value 'path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50z')'
Fail Property clip-path value 'path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50')' Pass Property clip-path value 'path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50')'

View file

@ -2,6 +2,6 @@ Harness status: OK
Found 2 tests Found 2 tests
2 Fail 2 Pass
Fail e.style['clip-path'] = "path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50z')" should set the property value Pass e.style['clip-path'] = "path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50z')" should set the property value
Fail e.style['clip-path'] = "path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50')" should set the property value Pass e.style['clip-path'] = "path(evenodd, 'M10,10h80v80h-80zM25,25h50v50h-50')" should set the property value