ladybird/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.cpp

338 lines
15 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2024, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "BasicShapeStyleValue.h"
#include <LibGfx/Path.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/SVG/Path.h>
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 resolved_top = LengthPercentageOrAuto::from_style_value(top).to_px_or_zero(node, reference_box.height()).to_float();
auto resolved_right = reference_box.width().to_float() - LengthPercentageOrAuto::from_style_value(right).to_px_or_zero(node, reference_box.width()).to_float();
auto resolved_bottom = reference_box.height().to_float() - LengthPercentageOrAuto::from_style_value(bottom).to_px_or_zero(node, reference_box.height()).to_float();
auto resolved_left = LengthPercentageOrAuto::from_style_value(left).to_px_or_zero(node, reference_box.width()).to_float();
return path_from_resolved_rect(resolved_top, resolved_right, resolved_bottom, resolved_left);
}
String Inset::to_string(SerializationMode mode) const
{
return MUST(String::formatted("inset({} {} {} {})", top->to_string(mode), right->to_string(mode), bottom->to_string(mode), left->to_string(mode)));
}
Gfx::Path Xywh::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
auto top = LengthPercentage::from_style_value(y).to_px(node, reference_box.height()).to_float();
auto bottom = top + max(0.0f, LengthPercentage::from_style_value(height).to_px(node, reference_box.height()).to_float());
auto left = LengthPercentage::from_style_value(x).to_px(node, reference_box.width()).to_float();
auto right = left + max(0.0f, LengthPercentage::from_style_value(width).to_px(node, reference_box.width()).to_float());
return path_from_resolved_rect(top, right, bottom, left);
}
String Xywh::to_string(SerializationMode mode) const
{
return MUST(String::formatted("xywh({} {} {} {})", x->to_string(mode), y->to_string(mode), width->to_string(mode), height->to_string(mode)));
}
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:
// its equivalent to 0% as the first (top) or fourth (left) value, and equivalent to 100% as the second (right) or third (bottom) value.
auto resolved_top = top->has_auto() ? 0 : LengthPercentageOrAuto::from_style_value(top).to_px_or_zero(node, reference_box.height()).to_float();
auto resolved_right = right->has_auto() ? reference_box.width().to_float() : LengthPercentageOrAuto::from_style_value(right).to_px_or_zero(node, reference_box.width()).to_float();
auto resolved_bottom = bottom->has_auto() ? reference_box.height().to_float() : LengthPercentageOrAuto::from_style_value(bottom).to_px_or_zero(node, reference_box.height()).to_float();
auto resolved_left = left->has_auto() ? 0 : LengthPercentageOrAuto::from_style_value(left).to_px_or_zero(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(resolved_top, max(resolved_right, resolved_left), max(resolved_bottom, resolved_top), resolved_left);
}
String Rect::to_string(SerializationMode mode) const
{
return MUST(String::formatted("rect({} {} {} {})", top->to_string(mode), right->to_string(mode), bottom->to_string(mode), left->to_string(mode)));
}
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()));
auto radius_px = [&]() {
if (radius->is_keyword()) {
switch (*keyword_to_fit_side(radius->to_keyword())) {
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();
}
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, LengthPercentage::from_style_value(radius).to_px(node, CSSPixels(radius_ref)).to_float());
}();
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(SerializationMode mode) const
{
return MUST(String::formatted("circle({} at {})", radius->to_string(mode), position->to_string(mode)));
}
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()));
auto radius_x_px = [&]() {
if (radius_x->is_keyword()) {
switch (*keyword_to_fit_side(radius_x->to_keyword())) {
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();
}
return max(0.0f, LengthPercentage::from_style_value(radius_x).to_px(node, reference_box.width()).to_float());
}();
auto radius_y_px = [&]() {
if (radius_y->is_keyword()) {
switch (*keyword_to_fit_side(radius_y->to_keyword())) {
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();
}
return max(0.0f, LengthPercentage::from_style_value(radius_y).to_px(node, reference_box.height()).to_float());
}();
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(SerializationMode mode) const
{
return MUST(String::formatted("ellipse({} {} at {})", radius_x->to_string(mode), radius_y->to_string(mode), position->to_string(mode)));
}
Gfx::Path Polygon::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
Gfx::Path path;
path.set_fill_type(fill_rule);
bool first = true;
for (auto const& point : points) {
Gfx::FloatPoint resolved_point {
LengthPercentage::from_style_value(point.x).to_px(node, reference_box.width()).to_float(),
LengthPercentage::from_style_value(point.y).to_px(node, reference_box.height()).to_float()
};
if (first)
path.move_to(resolved_point);
else
path.line_to(resolved_point);
first = false;
}
path.close();
return path;
}
String Polygon::to_string(SerializationMode mode) const
{
StringBuilder builder;
builder.append("polygon("sv);
switch (fill_rule) {
case Gfx::WindingRule::Nonzero:
builder.append("nonzero"sv);
break;
case Gfx::WindingRule::EvenOdd:
builder.append("evenodd"sv);
}
for (auto const& point : points) {
builder.appendff(", {} {}", point.x->to_string(mode), point.y->to_string(mode));
}
builder.append(')');
return MUST(builder.to_string());
}
Gfx::Path Path::to_path(CSSPixelRect, Layout::Node const&) const
{
auto result = path_instructions.to_gfx_path();
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;
Gfx::Path BasicShapeStyleValue::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
return m_basic_shape.visit([&](auto const& shape) {
return shape.to_path(reference_box, node);
});
}
String BasicShapeStyleValue::to_string(SerializationMode mode) const
{
return m_basic_shape.visit([mode](auto const& shape) {
return shape.to_string(mode);
});
}
ValueComparingNonnullRefPtr<StyleValue const> BasicShapeStyleValue::absolutized(ComputationContext const& computation_context) const
{
auto absolutized_shape = m_basic_shape.visit(
[&](Inset const& shape) -> BasicShape {
auto absolutized_top = shape.top->absolutized(computation_context);
auto absolutized_right = shape.right->absolutized(computation_context);
auto absolutized_bottom = shape.bottom->absolutized(computation_context);
auto absolutized_left = shape.left->absolutized(computation_context);
if (absolutized_top == shape.top && absolutized_right == shape.right && absolutized_bottom == shape.bottom && absolutized_left == shape.left)
return shape;
return Inset { absolutized_top, absolutized_right, absolutized_bottom, absolutized_left };
},
[&](Xywh const& shape) -> BasicShape {
auto absolutized_x = shape.x->absolutized(computation_context);
auto absolutized_y = shape.y->absolutized(computation_context);
auto absolutized_width = shape.width->absolutized(computation_context);
auto absolutized_height = shape.height->absolutized(computation_context);
if (absolutized_x == shape.x && absolutized_y == shape.y && absolutized_width == shape.width && absolutized_height == shape.height)
return shape;
return Xywh { absolutized_x, absolutized_y, absolutized_width, absolutized_height };
},
[&](Rect const& shape) -> BasicShape {
auto absolutized_top = shape.top->absolutized(computation_context);
auto absolutized_right = shape.right->absolutized(computation_context);
auto absolutized_bottom = shape.bottom->absolutized(computation_context);
auto absolutized_left = shape.left->absolutized(computation_context);
if (absolutized_top == shape.top && absolutized_right == shape.right && absolutized_bottom == shape.bottom && absolutized_left == shape.left)
return shape;
return Rect { absolutized_top, absolutized_right, absolutized_bottom, absolutized_left };
},
[&](Circle const& shape) -> BasicShape {
auto absolutized_radius = shape.radius->absolutized(computation_context);
auto absolutized_position = shape.position->absolutized(computation_context);
if (absolutized_radius == shape.radius && absolutized_position->as_position() == *shape.position)
return shape;
return Circle { absolutized_radius, absolutized_position->as_position() };
},
[&](Ellipse const& shape) -> BasicShape {
auto absolutized_radius_x = shape.radius_x->absolutized(computation_context);
auto absolutized_radius_y = shape.radius_y->absolutized(computation_context);
auto absolutized_position = shape.position->absolutized(computation_context);
if (absolutized_radius_x == shape.radius_x && absolutized_radius_y == shape.radius_y && absolutized_position->as_position() == *shape.position)
return shape;
return Ellipse { absolutized_radius_x, absolutized_radius_y, absolutized_position->as_position() };
},
[&](Polygon const& shape) -> BasicShape {
Vector<Polygon::Point> absolutized_points;
absolutized_points.ensure_capacity(shape.points.size());
bool any_point_required_absolutization = false;
for (auto const& point : shape.points) {
auto absolutized_x = point.x->absolutized(computation_context);
auto absolutized_y = point.y->absolutized(computation_context);
if (absolutized_x == point.x && absolutized_y == point.y) {
absolutized_points.append(point);
continue;
}
any_point_required_absolutization = true;
absolutized_points.append({ absolutized_x, absolutized_y });
}
if (!any_point_required_absolutization)
return shape;
return Polygon { shape.fill_rule, absolutized_points };
},
[&](Path const& shape) -> BasicShape {
return shape;
});
if (absolutized_shape == m_basic_shape)
return *this;
return BasicShapeStyleValue::create(absolutized_shape);
}
}