ladybird/Libraries/LibWeb/CSS/StyleValues/BasicShapeStyleValue.cpp
Callum Law 1b7567cc86 LibWeb: Support parsing of border radius in inset() function
Respecting this value during basic shape path resolution is yet to be
implemented
2026-01-06 10:50:06 +01:00

381 lines
17 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/BorderRadiusRectStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/RadialSizeStyleValue.h>
#include <LibWeb/CSS/ValueType.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
{
auto resolved_top = LengthPercentageOrAuto::from_style_value(top).to_px_or_zero(node, reference_box.height()).to_float();
auto resolved_right = LengthPercentageOrAuto::from_style_value(right).to_px_or_zero(node, reference_box.width()).to_float();
auto resolved_bottom = 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();
// FIXME: Respect border radius
// 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%.
if (resolved_top + resolved_bottom > reference_box.height().to_float() || resolved_left + resolved_right > reference_box.width().to_float()) {
// https://drafts.csswg.org/css-backgrounds-3/#corner-overlap
// Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, Si is the sum of the two corresponding radii of the
// corners on side i, and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box. If
// f < 1, then all corner radii are reduced by multiplying them by f.
// NB: We only care about vertical and horizontal here as top = bottom and left = right
auto s_vertical = resolved_top + resolved_bottom;
auto s_horizontal = resolved_left + resolved_right;
auto f = min(reference_box.height() / s_vertical, reference_box.width() / s_horizontal);
resolved_top *= f;
resolved_right *= f;
resolved_bottom *= f;
resolved_left *= f;
}
return path_from_resolved_rect(resolved_top, reference_box.width().to_float() - resolved_right, reference_box.height().to_float() - resolved_bottom, resolved_left);
}
String Inset::to_string(SerializationMode mode) const
{
StringBuilder builder;
builder.append(serialize_a_positional_value_list({ top, right, bottom, left }, mode));
auto serialized_border_radius = border_radius->to_string(mode);
if (serialized_border_radius != "0px"sv)
builder.appendff(" round {}", serialized_border_radius);
return MUST(String::formatted("inset({})", builder.to_string_without_validation()));
}
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)));
}
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 translated_reference_box = reference_box.translated(-reference_box.x(), -reference_box.y());
// https://www.w3.org/TR/css-shapes/#funcdef-basic-shape-circle
// The <position> argument defines the center of the circle. Unless otherwise specified, this defaults to center if omitted.
RefPtr<PositionStyleValue const> resolved_position = PositionStyleValue::create_computed_center();
if (position)
resolved_position = position->as_position();
auto center = resolved_position->resolved(node, translated_reference_box);
auto radius_px = radius->as_radial_size().resolve_circle_size(center, translated_reference_box, node).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
{
StringBuilder arguments_builder;
auto serialized_radius = radius->to_string(mode);
if (serialized_radius != "closest-side"sv)
arguments_builder.append(serialized_radius);
if (position) {
if (!arguments_builder.is_empty())
arguments_builder.append(' ');
arguments_builder.appendff("at {}", position->to_string(mode));
}
return MUST(String::formatted("circle({})", arguments_builder.to_string_without_validation()));
}
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 translated_reference_box = reference_box.translated(-reference_box.x(), -reference_box.y());
// https://www.w3.org/TR/css-shapes/#funcdef-basic-shape-circle
// The <position> argument defines the center of the ellipse. Unless otherwise specified, this defaults to center if omitted.
RefPtr<PositionStyleValue const> resolved_position = PositionStyleValue::create_computed_center();
if (position)
resolved_position = position->as_position();
auto center = resolved_position->resolved(node, translated_reference_box);
auto size = radius->as_radial_size().resolve_ellipse_size(center, translated_reference_box, node);
Gfx::Path path;
path.move_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + size.height().to_float() });
path.elliptical_arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() - size.height().to_float() }, Gfx::FloatSize { size.width().to_float(), size.height().to_float() }, 0, true, true);
path.elliptical_arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + size.height().to_float() }, Gfx::FloatSize { size.width().to_float(), size.height().to_float() }, 0, true, true);
return path;
}
String Ellipse::to_string(SerializationMode mode) const
{
StringBuilder arguments_builder;
auto serialized_radius = radius->to_string(mode);
if (serialized_radius != "closest-side closest-side"sv)
arguments_builder.append(serialized_radius);
if (position) {
if (!arguments_builder.is_empty())
arguments_builder.append(' ');
arguments_builder.appendff("at {}", position->to_string(mode));
}
return MUST(String::formatted("ellipse({})", arguments_builder.to_string_without_validation()));
}
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) -> Gfx::Path {
// NB: Xywh and Rect don't require to_path functions as we should have already converted them to their
// respective Inset equivalents during absolutization
if constexpr (requires { shape.to_path(reference_box, node); }) {
return shape.to_path(reference_box, node);
}
VERIFY_NOT_REACHED();
});
}
String BasicShapeStyleValue::to_string(SerializationMode mode) const
{
return m_basic_shape.visit([mode](auto const& shape) {
return shape.to_string(mode);
});
}
// https://www.w3.org/TR/css-shapes-1/#basic-shape-computed-values
ValueComparingNonnullRefPtr<StyleValue const> BasicShapeStyleValue::absolutized(ComputationContext const& computation_context) const
{
// The values in a <basic-shape> function are computed as specified, with these exceptions:
// - Omitted values are included and compute to their defaults.
// FIXME: - A <position> value in circle() or ellipse() is computed as a pair of offsets (horizontal then vertical) from the top left origin, each given as a <length-percentage>.
// FIXME: - A <'border-radius'> value in a <basic-shape-rect> function is computed as an expanded list of all eight <length-percentage> values.
// - All <basic-shape-rect> functions compute to the equivalent inset() function.
CalculationContext calculation_context { .percentages_resolve_as = ValueType::Length };
auto const one_hundred_percent_minus = [&](Vector<NonnullRefPtr<StyleValue const>> const& values, CalculationContext const& calculation_context) {
Vector<NonnullRefPtr<CalculationNode const>> sum_components = { NumericCalculationNode::create(Percentage { 100 }, calculation_context) };
for (auto const& value : values)
sum_components.append(NegateCalculationNode::create(CalculationNode::from_style_value(value, calculation_context)));
return CalculatedStyleValue::create(SumCalculationNode::create(sum_components), NumericType { NumericType::BaseType::Length, 1 }, calculation_context);
};
auto const absolutize_if_nonnull = [&](RefPtr<StyleValue const> const& value) -> ValueComparingRefPtr<StyleValue const> {
if (!value)
return nullptr;
return value->absolutized(computation_context);
};
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);
auto absolutized_border_radius = shape.border_radius->absolutized(computation_context);
if (absolutized_top == shape.top && absolutized_right == shape.right && absolutized_bottom == shape.bottom && absolutized_left == shape.left && absolutized_border_radius == shape.border_radius)
return shape;
return Inset { absolutized_top, absolutized_right, absolutized_bottom, absolutized_left, absolutized_border_radius };
},
[&](Xywh const& shape) -> BasicShape {
// Note: Given xywh(x y w h), the equivalent function is inset(y calc(100% - x - w) calc(100% - y - h) x).
auto absolutized_top = shape.y->absolutized(computation_context);
auto absolutized_right = one_hundred_percent_minus({ shape.x, shape.width }, calculation_context)->absolutized(computation_context);
auto absolutized_bottom = one_hundred_percent_minus({ shape.y, shape.height }, calculation_context)->absolutized(computation_context);
auto absolutized_left = shape.x->absolutized(computation_context);
// FIXME: Pass actual border radius once we parse it
return Inset { *absolutized_top, *absolutized_right, *absolutized_bottom, *absolutized_left, BorderRadiusRectStyleValue::create_zero() };
},
[&](Rect const& shape) -> BasicShape {
// Note: Given rect(t r b l), the equivalent function is inset(t calc(100% - r) calc(100% - b) l).
auto resolve_auto = [](ValueComparingNonnullRefPtr<StyleValue const> const& style_value, Percentage value_of_auto) -> ValueComparingNonnullRefPtr<StyleValue 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.
if (style_value->is_keyword()) {
VERIFY(style_value->to_keyword() == Keyword::Auto);
return PercentageStyleValue::create(value_of_auto);
}
return style_value;
};
auto absolutized_top = resolve_auto(shape.top, Percentage { 0 })->absolutized(computation_context);
auto absolutized_right = one_hundred_percent_minus({ resolve_auto(shape.right, Percentage { 100 }) }, calculation_context)->absolutized(computation_context);
auto absolutized_bottom = one_hundred_percent_minus({ resolve_auto(shape.bottom, Percentage { 100 }) }, calculation_context)->absolutized(computation_context);
auto absolutized_left = resolve_auto(shape.left, Percentage { 0 })->absolutized(computation_context);
// FIXME: Pass actual border radius once we parse it
return Inset { *absolutized_top, *absolutized_right, *absolutized_bottom, *absolutized_left, BorderRadiusRectStyleValue::create_zero() };
},
[&](Circle const& shape) -> BasicShape {
auto absolutized_radius = shape.radius->absolutized(computation_context);
auto absolutized_position = absolutize_if_nonnull(shape.position);
if (absolutized_radius == shape.radius && absolutized_position == shape.position)
return shape;
return Circle { absolutized_radius, absolutized_position };
},
[&](Ellipse const& shape) -> BasicShape {
auto absolutized_radius = shape.radius->absolutized(computation_context);
auto absolutized_position = absolutize_if_nonnull(shape.position);
if (absolutized_radius == shape.radius && absolutized_position == shape.position)
return shape;
return Ellipse { absolutized_radius, absolutized_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);
}
}