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

236 lines
11 KiB
C++
Raw Permalink Normal View History

/*
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "RadialSizeStyleValue.h"
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/CSS/PercentageOr.h>
namespace Web::CSS {
ValueComparingNonnullRefPtr<StyleValue const> RadialSizeStyleValue::absolutized(ComputationContext const& computation_context) const
{
bool any_component_required_absolutization = false;
Vector<Component> absolutized_components;
for (auto const& component : m_components) {
if (component.has<RadialExtent>()) {
absolutized_components.append(component);
} else {
auto const& absolutized_length_percentage = component.get<NonnullRefPtr<StyleValue const>>()->absolutized(computation_context);
if (!absolutized_length_percentage->equals(component.get<NonnullRefPtr<StyleValue const>>()))
any_component_required_absolutization = true;
absolutized_components.append(absolutized_length_percentage);
}
}
if (!any_component_required_absolutization)
return *this;
return RadialSizeStyleValue::create(move(absolutized_components));
}
void RadialSizeStyleValue::serialize(StringBuilder& builder, SerializationMode serialization_mode) const
{
bool first = true;
for (auto const& component : m_components) {
if (!first)
builder.append(' ');
first = false;
component.visit(
[&](RadialExtent extent) {
builder.append(CSS::to_string(extent));
},
[&](NonnullRefPtr<StyleValue const> const& length_percentage) {
length_percentage->serialize(builder, serialization_mode);
});
}
}
static CSSPixelSize side_shape(CSSPixelPoint const& center, CSSPixelRect const& reference_box, Function<CSSPixels(CSSPixels, CSSPixels)> distance_function)
{
auto x_dist = distance_function(abs(reference_box.left() - center.x()), abs(reference_box.right() - center.x()));
auto y_dist = distance_function(abs(reference_box.top() - center.y()), abs(reference_box.bottom() - center.y()));
return CSSPixelSize { x_dist, y_dist };
}
static CSSPixelSize closest_side_shape(CSSPixelPoint const& center, CSSPixelRect const& size)
{
return side_shape(center, size, AK::min<CSSPixels>);
}
static CSSPixelSize farthest_side_shape(CSSPixelPoint const& center, CSSPixelRect const& size)
{
return side_shape(center, size, AK::max<CSSPixels>);
}
static CSSPixels corner_distance(CSSPixelPoint const& center, CSSPixelRect const& reference_box, Function<bool(CSSPixels, CSSPixels)> distance_compare_function, CSSPixelPoint& corner)
{
auto top_left_distance_squared = square_distance_between(reference_box.top_left(), center);
auto top_right_distance_squared = square_distance_between(reference_box.top_right(), center);
auto bottom_right_distance_squared = square_distance_between(reference_box.bottom_right(), center);
auto bottom_left_distance_squared = square_distance_between(reference_box.bottom_left(), center);
auto distance_squared = top_left_distance_squared;
corner = reference_box.top_left();
if (distance_compare_function(top_right_distance_squared, distance_squared)) {
corner = reference_box.top_right();
distance_squared = top_right_distance_squared;
}
if (distance_compare_function(bottom_right_distance_squared, distance_squared)) {
corner = reference_box.bottom_right();
distance_squared = bottom_right_distance_squared;
}
if (distance_compare_function(bottom_left_distance_squared, distance_squared)) {
corner = reference_box.bottom_left();
distance_squared = bottom_left_distance_squared;
}
return sqrt(distance_squared);
}
static CSSPixels closest_corner_distance(CSSPixelPoint const& center, CSSPixelRect const& reference_box, CSSPixelPoint& corner)
{
return corner_distance(center, reference_box, [](CSSPixels a, CSSPixels b) { return a < b; }, corner);
}
static CSSPixels farthest_corner_distance(CSSPixelPoint const& center, CSSPixelRect const& reference_box, CSSPixelPoint& corner)
{
return corner_distance(center, reference_box, [](CSSPixels a, CSSPixels b) { return a > b; }, corner);
}
CSSPixels RadialSizeStyleValue::resolve_circle_size(CSSPixelPoint const& center, CSSPixelRect const& reference_box, Layout::Node const& node) const
{
VERIFY(m_components.size() == 1);
auto resolved_size = m_components[0].visit(
[&](RadialExtent const& radial_extent) {
switch (radial_extent) {
case RadialExtent::ClosestSide: {
auto side_distances = closest_side_shape(center, reference_box);
return AK::min(side_distances.width(), side_distances.height());
}
case RadialExtent::FarthestSide: {
auto side_distances = farthest_side_shape(center, reference_box);
return AK::max(side_distances.width(), side_distances.height());
}
case RadialExtent::ClosestCorner: {
CSSPixelPoint corner {};
return closest_corner_distance(center, reference_box, corner);
}
case RadialExtent::FarthestCorner: {
CSSPixelPoint corner {};
return farthest_corner_distance(center, reference_box, corner);
}
}
VERIFY_NOT_REACHED();
},
[&](NonnullRefPtr<StyleValue const> const& length_percentage) {
auto radius_ref = sqrt(pow(reference_box.width().to_float(), 2) + pow(reference_box.height().to_float(), 2)) / AK::Sqrt2<float>;
// FIXME: We don't need to pass `node` here since we know that all relative lengths have already been absolutized
return CSSPixels::nearest_value_for(max(0.0f, LengthPercentage::from_style_value(length_percentage).to_px(node, CSSPixels::nearest_value_for(radius_ref)).to_float()));
});
// https://w3c.github.io/csswg-drafts/css-images/#degenerate-radials
// If the ending shape is a circle with zero radius:
if (resolved_size == 0) {
// Render as if the ending shape was a circle whose radius was an arbitrary very small number greater than zero.
// This will make the gradient continue to look like a circle.
return CSSPixels::smallest_positive_value();
}
return resolved_size;
}
static CSSPixelSize ellipse_corner_shape(CSSPixelPoint const& center, CSSPixelRect const& reference_box, Function<CSSPixels(CSSPixelPoint const&, CSSPixelRect const&, CSSPixelPoint&)> get_corner, Function<CSSPixelSize(CSSPixelPoint const&, CSSPixelRect const&)> get_shape)
{
CSSPixelPoint corner {};
get_corner(center, reference_box, corner);
auto shape = get_shape(center, reference_box);
CSSPixels height = shape.height();
CSSPixels width = shape.width();
// Prevent division by zero
// https://w3c.github.io/csswg-drafts/css-images/#degenerate-radials
if (height == 0) {
// Render as if the ending shape was an ellipse whose width was an arbitrary very large number and whose height
// was an arbitrary very small number greater than zero. This will make the gradient look like a solid-color image equal
// to the color of the last color-stop, or equal to the average color of the gradient if its repeating.
constexpr auto arbitrary_small_number = CSSPixels::smallest_positive_value();
constexpr auto arbitrary_large_number = CSSPixels::max();
return CSSPixelSize { arbitrary_large_number, arbitrary_small_number };
}
auto aspect_ratio = width / height;
auto p = corner - center;
auto radius_a = sqrt(p.y() * p.y() * aspect_ratio * aspect_ratio + p.x() * p.x());
auto radius_b = radius_a / aspect_ratio;
return CSSPixelSize { radius_a, radius_b };
}
CSSPixelSize RadialSizeStyleValue::resolve_ellipse_size(CSSPixelPoint const& center, CSSPixelRect const& reference_box, Layout::Node const& node) const
{
VERIFY(m_components.size() == 1 || m_components.size() == 2);
auto const resolve_component = [&](Component const& component, CSSPixels const& reference_size) -> CSSPixelSize {
return component.visit(
[&](RadialExtent const& radial_extent) {
switch (radial_extent) {
case RadialExtent::ClosestSide:
return closest_side_shape(center, reference_box);
case RadialExtent::FarthestSide:
return farthest_side_shape(center, reference_box);
case RadialExtent::ClosestCorner:
return ellipse_corner_shape(center, reference_box, closest_corner_distance, closest_side_shape);
case RadialExtent::FarthestCorner:
return ellipse_corner_shape(center, reference_box, farthest_corner_distance, farthest_side_shape);
}
VERIFY_NOT_REACHED();
},
[&](NonnullRefPtr<StyleValue const> const& length_percentage) {
// FIXME: We don't need to pass `node` here since we know that all relative lengths have already been absolutized
auto value = LengthPercentage::from_style_value(length_percentage).to_px(node, reference_size);
return CSSPixelSize { value, value };
});
};
CSSPixelSize resolved_size = CSSPixelSize {
resolve_component(m_components[0], reference_box.width()).width(),
resolve_component(m_components.size() == 1 ? m_components[0] : m_components[1], reference_box.height()).height()
};
// Handle degenerate cases
// https://w3c.github.io/csswg-drafts/css-images/#degenerate-radials
constexpr auto arbitrary_small_number = CSSPixels::smallest_positive_value();
constexpr auto arbitrary_large_number = CSSPixels::max();
// If the ending shape has zero width (regardless of the height):
if (resolved_size.width() <= 0) {
// Render as if the ending shape was an ellipse whose height was an arbitrary very large number
// and whose width was an arbitrary very small number greater than zero.
// This will make the gradient look similar to a horizontal linear gradient that is mirrored across the center of the ellipse.
// It also means that all color-stop positions specified with a percentage resolve to 0px.
return CSSPixelSize { arbitrary_small_number, arbitrary_large_number };
}
// Otherwise, if the ending shape has zero height:
if (resolved_size.height() <= 0) {
// Render as if the ending shape was an ellipse whose width was an arbitrary very large number and whose height
// was an arbitrary very small number greater than zero. This will make the gradient look like a solid-color image equal
// to the color of the last color-stop, or equal to the average color of the gradient if its repeating.
return CSSPixelSize { arbitrary_large_number, arbitrary_small_number };
}
return resolved_size;
}
}