mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-19 15:32:31 +00:00
LibWeb: Implement missing basic shapes
This commit is contained in:
parent
76e638b4ca
commit
a7e83c38ee
Notes:
github-actions[bot]
2024-10-31 10:49:39 +00:00
Author: https://github.com/Gingeh
Commit: a7e83c38ee
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1996
Reviewed-by: https://github.com/AtkinsSJ ✅
8 changed files with 620 additions and 28 deletions
126
Tests/LibWeb/Screenshot/clip-path-basic-shapes.html
Normal file
126
Tests/LibWeb/Screenshot/clip-path-basic-shapes.html
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<link rel="match" href="reference/clip-path-basic-shapes-ref.html" />
|
||||||
|
<style>
|
||||||
|
.outer {
|
||||||
|
margin: 5px;
|
||||||
|
width: 130px;
|
||||||
|
height: 80px;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
height: 100%;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: inset(30px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: inset(10px 20px 30px 40px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: inset(20% 30%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: inset(40px 20%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: inset(60% 0 40% 0)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: xywh(10px 20px 30px 40px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: xywh(10% 20% 30% 40%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: xywh(10% 20px 30px 40%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: xywh(10px 20px -30px 40px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: xywh(10px -20px 30px 40px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: rect(0 30% auto 10%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: rect(50px 70px 80% 20%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: rect(10px 0 0 20px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: rect(10px 20px 10px 20px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(50px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(60px at right center)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(10% at 20px 90%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(closest-side at 40px 30px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(farthest-side)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle()"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(at 30% 40px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(farthest-side at -10px -10px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: circle(-10px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(20px 50px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(40px 50% at right center)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(closest-side closest-side at 50px 60px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(farthest-side closest-side at 20% 70%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(closest-side farthest-side at 20% 70%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(closest-side 20% at 20% 70%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(at 40% 70%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(closest-side farthest-side)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(closest-side)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: ellipse(-10% -10px at right)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: polygon(100% 0%, 50% 50%, 100% 100%)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: polygon(65px 0px, 35px 80px, 105px 30px, 25px 30px, 95px 80px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: polygon(nonzero, 65px 0px, 35px 80px, 105px 30px, 25px 30px, 95px 80px)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner" style="clip-path: polygon(100px 0%, 50% 50px, 100% 100%)"></div>
|
||||||
|
</div>
|
BIN
Tests/LibWeb/Screenshot/images/clip-path-basic-shapes-ref.png
Normal file
BIN
Tests/LibWeb/Screenshot/images/clip-path-basic-shapes-ref.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,9 @@
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<img src="../images/clip-path-basic-shapes-ref.png">
|
|
@ -27,6 +27,8 @@ public:
|
||||||
LengthPercentage const& bottom() const { return m_bottom; }
|
LengthPercentage const& bottom() const { return m_bottom; }
|
||||||
LengthPercentage const& left() const { return m_left; }
|
LengthPercentage const& left() const { return m_left; }
|
||||||
|
|
||||||
|
bool operator==(LengthBox const&) const = default;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
LengthPercentage m_top;
|
LengthPercentage m_top;
|
||||||
LengthPercentage m_right;
|
LengthPercentage m_right;
|
||||||
|
|
|
@ -1236,6 +1236,37 @@ RefPtr<CSSStyleValue> Parser::parse_url_value(TokenStream<ComponentValue>& token
|
||||||
return URLStyleValue::create(*url);
|
return URLStyleValue::create(*url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius
|
||||||
|
Optional<ShapeRadius> Parser::parse_shape_radius(TokenStream<ComponentValue>& 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<CSSStyleValue> Parser::parse_basic_shape_value(TokenStream<ComponentValue>& tokens)
|
RefPtr<CSSStyleValue> Parser::parse_basic_shape_value(TokenStream<ComponentValue>& tokens)
|
||||||
{
|
{
|
||||||
auto transaction = tokens.begin_transaction();
|
auto transaction = tokens.begin_transaction();
|
||||||
|
@ -1245,38 +1276,237 @@ RefPtr<CSSStyleValue> Parser::parse_basic_shape_value(TokenStream<ComponentValue
|
||||||
|
|
||||||
auto function_name = component_value.function().name.bytes_as_string_view();
|
auto function_name = component_value.function().name.bytes_as_string_view();
|
||||||
|
|
||||||
// FIXME: Implement other shapes. 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("polygon"sv))
|
if (function_name.equals_ignoring_ascii_case("inset"sv)) {
|
||||||
return nullptr;
|
// inset() = inset( <length-percentage>{1,4} [ round <'border-radius'> ]? )
|
||||||
|
// FIXME: Parse the border-radius.
|
||||||
|
auto arguments_tokens = TokenStream { component_value.function().value };
|
||||||
|
|
||||||
// polygon() = polygon( <'fill-rule'>? , [<length-percentage> <length-percentage>]# )
|
// If less than four <length-percentage> values are provided,
|
||||||
// FIXME: Parse the fill-rule.
|
// the omitted values default in the same way as the margin shorthand:
|
||||||
auto arguments_tokens = TokenStream { component_value.function().value };
|
// an omitted second or third value defaults to the first, and an omitted fourth value defaults to the second.
|
||||||
auto arguments = parse_a_comma_separated_list_of_component_values(arguments_tokens);
|
|
||||||
|
|
||||||
Vector<Polygon::Point> points;
|
// The four <length-percentage>s define the position of the top, right, bottom, and left edges of a rectangle.
|
||||||
for (auto& argument : arguments) {
|
|
||||||
TokenStream argument_tokens { argument };
|
|
||||||
|
|
||||||
argument_tokens.discard_whitespace();
|
arguments_tokens.discard_whitespace();
|
||||||
auto x_pos = parse_length_percentage(argument_tokens);
|
auto top = parse_length_percentage(arguments_tokens);
|
||||||
if (!x_pos.has_value())
|
if (!top.has_value())
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
argument_tokens.discard_whitespace();
|
arguments_tokens.discard_whitespace();
|
||||||
auto y_pos = parse_length_percentage(argument_tokens);
|
auto right = parse_length_percentage(arguments_tokens);
|
||||||
if (!y_pos.has_value())
|
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;
|
return nullptr;
|
||||||
|
|
||||||
argument_tokens.discard_whitespace();
|
transaction.commit();
|
||||||
if (argument_tokens.has_next_token())
|
return BasicShapeStyleValue::create(Inset { LengthBox(top.value(), right.value(), bottom.value(), left.value()) });
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
points.append(Polygon::Point { *x_pos, *y_pos });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.commit();
|
if (function_name.equals_ignoring_ascii_case("xywh"sv)) {
|
||||||
return BasicShapeStyleValue::create(Polygon { FillRule::Nonzero, move(points) });
|
// xywh() = xywh( <length-percentage>{2} <length-percentage [0,∞]>{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( [ <length-percentage> | 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<ComponentValue>& tokens) -> Optional<LengthPercentage> {
|
||||||
|
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( <shape-radius>? [ at <position> ]? )
|
||||||
|
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( [ <shape-radius>{2} ]? [ at <position> ]? )
|
||||||
|
auto arguments_tokens = TokenStream { component_value.function().value };
|
||||||
|
|
||||||
|
Optional<ShapeRadius> radius_x = parse_shape_radius(arguments_tokens);
|
||||||
|
Optional<ShapeRadius> 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'>? , [<length-percentage> <length-percentage>]# )
|
||||||
|
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<FillRule> 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<Polygon::Point> 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<FlyString> Parser::parse_layer_name(TokenStream<ComponentValue>& tokens, AllowBlankLayerName allow_blank_layer_name)
|
Optional<FlyString> Parser::parse_layer_name(TokenStream<ComponentValue>& tokens, AllowBlankLayerName allow_blank_layer_name)
|
||||||
|
|
|
@ -221,6 +221,7 @@ private:
|
||||||
Optional<URL::URL> parse_url_function(TokenStream<ComponentValue>&);
|
Optional<URL::URL> parse_url_function(TokenStream<ComponentValue>&);
|
||||||
RefPtr<CSSStyleValue> parse_url_value(TokenStream<ComponentValue>&);
|
RefPtr<CSSStyleValue> parse_url_value(TokenStream<ComponentValue>&);
|
||||||
|
|
||||||
|
Optional<ShapeRadius> parse_shape_radius(TokenStream<ComponentValue>&);
|
||||||
RefPtr<CSSStyleValue> parse_basic_shape_value(TokenStream<ComponentValue>&);
|
RefPtr<CSSStyleValue> parse_basic_shape_value(TokenStream<ComponentValue>&);
|
||||||
|
|
||||||
template<typename TElement>
|
template<typename TElement>
|
||||||
|
|
|
@ -9,14 +9,178 @@
|
||||||
|
|
||||||
namespace Web::CSS {
|
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<float>;
|
||||||
|
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 Polygon::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
|
||||||
{
|
{
|
||||||
Gfx::Path path;
|
Gfx::Path path;
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (auto const& point : points) {
|
for (auto const& point : points) {
|
||||||
Gfx::FloatPoint resolved_point {
|
Gfx::FloatPoint resolved_point {
|
||||||
static_cast<float>(point.x.to_px(node, reference_box.width())),
|
point.x.to_px(node, reference_box.width()).to_float(),
|
||||||
static_cast<float>(point.y.to_px(node, reference_box.height()))
|
point.y.to_px(node, reference_box.height()).to_float()
|
||||||
};
|
};
|
||||||
if (first)
|
if (first)
|
||||||
path.move_to(resolved_point);
|
path.move_to(resolved_point);
|
||||||
|
|
|
@ -7,12 +7,71 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <AK/Variant.h>
|
#include <AK/Variant.h>
|
||||||
#include <LibGfx/DeprecatedPath.h>
|
|
||||||
#include <LibWeb/CSS/CSSStyleValue.h>
|
#include <LibWeb/CSS/CSSStyleValue.h>
|
||||||
|
#include <LibWeb/CSS/LengthBox.h>
|
||||||
#include <LibWeb/CSS/PercentageOr.h>
|
#include <LibWeb/CSS/PercentageOr.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
|
||||||
|
|
||||||
namespace Web::CSS {
|
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<LengthPercentage, FitSide>;
|
||||||
|
|
||||||
|
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<PositionStyleValue> 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<PositionStyleValue> position;
|
||||||
|
};
|
||||||
|
|
||||||
struct Polygon {
|
struct Polygon {
|
||||||
struct Point {
|
struct Point {
|
||||||
bool operator==(Point const&) const = default;
|
bool operator==(Point const&) const = default;
|
||||||
|
@ -25,12 +84,13 @@ struct Polygon {
|
||||||
|
|
||||||
bool operator==(Polygon const&) const = default;
|
bool operator==(Polygon const&) const = default;
|
||||||
|
|
||||||
|
// FIXME: Actually use the fill rule
|
||||||
FillRule fill_rule;
|
FillRule fill_rule;
|
||||||
Vector<Point> points;
|
Vector<Point> points;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: Implement other basic shapes. 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
|
||||||
using BasicShape = Variant<Polygon>;
|
using BasicShape = Variant<Inset, Xywh, Rect, Circle, Ellipse, Polygon>;
|
||||||
|
|
||||||
class BasicShapeStyleValue : public StyleValueWithDefaultOperators<BasicShapeStyleValue> {
|
class BasicShapeStyleValue : public StyleValueWithDefaultOperators<BasicShapeStyleValue> {
|
||||||
public:
|
public:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue