2024-05-25 23:06:47 +01:00
/*
* Copyright ( c ) 2024 , MacDue < macdue @ dueutil . tech >
2025-07-17 16:39:45 +01:00
* Copyright ( c ) 2025 , Sam Atkins < sam @ ladybird . org >
2024-05-25 23:06:47 +01:00
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include "BasicShapeStyleValue.h"
2024-08-20 17:27:08 +02:00
# include <LibGfx/Path.h>
2025-07-17 16:39:45 +01:00
# include <LibWeb/CSS/Serialize.h>
2026-01-05 22:29:49 +13:00
# include <LibWeb/CSS/StyleValues/BorderRadiusRectStyleValue.h>
2025-10-02 22:57:13 +13:00
# include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
2025-12-11 17:45:49 +13:00
# include <LibWeb/CSS/StyleValues/RadialSizeStyleValue.h>
2025-10-28 21:11:17 +13:00
# include <LibWeb/CSS/ValueType.h>
2025-07-17 16:39:45 +01:00
# include <LibWeb/SVG/Path.h>
2024-05-25 23:06:47 +01:00
namespace Web : : CSS {
2024-10-27 11:58:52 +11:00
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
{
2025-10-28 23:23:00 +13:00
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 ( ) ;
2026-01-05 22:29:49 +13:00
// FIXME: Respect border radius
2025-10-28 23:23:00 +13:00
// A pair of insets in either dimension that add up to more than the used dimension
2024-10-27 11:58:52 +11:00
// (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%.
2025-10-28 23:23:00 +13:00
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.
2024-10-27 11:58:52 +11:00
2025-10-28 23:23:00 +13:00
// 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 ;
}
2024-10-27 11:58:52 +11:00
2025-10-28 23:23:00 +13:00
return path_from_resolved_rect ( resolved_top , reference_box . width ( ) . to_float ( ) - resolved_right , reference_box . height ( ) . to_float ( ) - resolved_bottom , resolved_left ) ;
2024-10-27 11:58:52 +11:00
}
2025-10-02 22:57:13 +13:00
String Inset : : to_string ( SerializationMode mode ) const
2024-10-27 11:58:52 +11:00
{
2026-01-05 22:29:49 +13:00
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())) ;
2024-10-27 11:58:52 +11:00
}
2025-10-02 22:57:13 +13:00
String Xywh : : to_string ( SerializationMode mode ) const
2024-10-27 11:58:52 +11:00
{
2025-10-02 22:57:13 +13:00
return MUST ( String : : formatted ( " xywh({} {} {} {}) " , x->to_string(mode), y->to_string(mode), width->to_string(mode), height->to_string(mode))) ;
2024-10-27 11:58:52 +11:00
}
2025-10-02 22:57:13 +13:00
String Rect : : to_string ( SerializationMode mode ) const
2024-10-27 11:58:52 +11:00
{
2026-01-05 22:44:11 +13:00
StringBuilder builder ;
builder . appendff ( " {} {} {} {} " , top - > to_string ( mode ) , right - > to_string ( mode ) , bottom - > to_string ( mode ) , left - > to_string ( 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 ( " rect({}) " , builder.to_string_without_validation())) ;
2024-10-27 11:58:52 +11:00
}
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.
2025-12-11 17:45:49 +13:00
auto translated_reference_box = reference_box . translated ( - reference_box . x ( ) , - reference_box . y ( ) ) ;
2024-10-27 11:58:52 +11:00
2025-12-10 13:33:22 +13:00
// 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 ) ;
2025-12-11 17:45:49 +13:00
auto radius_px = radius - > as_radial_size ( ) . resolve_circle_size ( center , translated_reference_box , node ) . to_float ( ) ;
2024-10-27 11:58:52 +11:00
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 ;
}
2025-05-16 19:20:24 +01:00
String Circle : : to_string ( SerializationMode mode ) const
2024-10-27 11:58:52 +11:00
{
2025-12-10 13:33:22 +13:00
StringBuilder arguments_builder ;
2025-12-10 13:44:03 +13:00
auto serialized_radius = radius - > to_string ( mode ) ;
2025-12-10 13:33:22 +13:00
2025-12-10 13:44:03 +13:00
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 ) ) ;
}
2025-12-10 13:33:22 +13:00
return MUST ( String : : formatted ( " circle({}) " , arguments_builder.to_string_without_validation())) ;
2024-10-27 11:58:52 +11:00
}
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.
2025-12-12 14:21:35 +13:00
auto translated_reference_box = reference_box . translated ( - reference_box . x ( ) , - reference_box . y ( ) ) ;
2025-12-12 14:42:40 +13:00
// 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 ) ;
2025-12-12 14:21:35 +13:00
auto size = radius - > as_radial_size ( ) . resolve_ellipse_size ( center , translated_reference_box , node ) ;
2024-10-27 11:58:52 +11:00
Gfx : : Path path ;
2025-12-12 14:21:35 +13:00
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 ) ;
2024-10-27 11:58:52 +11:00
return path ;
}
2025-05-16 19:20:24 +01:00
String Ellipse : : to_string ( SerializationMode mode ) const
2024-10-27 11:58:52 +11:00
{
2025-12-12 14:42:40 +13:00
StringBuilder arguments_builder ;
2025-12-12 14:46:19 +13:00
auto serialized_radius = radius - > to_string ( mode ) ;
2025-12-12 14:42:40 +13:00
2025-12-12 14:46:19 +13:00
if ( serialized_radius ! = " closest-side closest-side " sv )
arguments_builder . append ( serialized_radius ) ;
2025-12-12 14:42:40 +13:00
2025-12-12 14:46:19 +13:00
if ( position ) {
if ( ! arguments_builder . is_empty ( ) )
arguments_builder . append ( ' ' ) ;
arguments_builder . appendff ( " at {} " , position - > to_string ( mode ) ) ;
}
2025-12-12 14:42:40 +13:00
return MUST ( String : : formatted ( " ellipse({}) " , arguments_builder.to_string_without_validation())) ;
2024-10-27 11:58:52 +11:00
}
2024-08-20 17:27:08 +02:00
Gfx : : Path Polygon : : to_path ( CSSPixelRect reference_box , Layout : : Node const & node ) const
2024-05-25 23:06:47 +01:00
{
2024-08-20 17:27:08 +02:00
Gfx : : Path path ;
2024-10-28 13:51:23 +11:00
path . set_fill_type ( fill_rule ) ;
2024-05-25 23:06:47 +01:00
bool first = true ;
for ( auto const & point : points ) {
Gfx : : FloatPoint resolved_point {
2025-10-02 22:57:13 +13:00
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 ( )
2024-05-25 23:06:47 +01:00
} ;
if ( first )
path . move_to ( resolved_point ) ;
else
path . line_to ( resolved_point ) ;
first = false ;
}
path . close ( ) ;
return path ;
}
2025-10-02 22:57:13 +13:00
String Polygon : : to_string ( SerializationMode mode ) const
2024-05-25 23:06:47 +01:00
{
StringBuilder builder ;
builder . append ( " polygon( " sv ) ;
2024-10-28 13:51:23 +11:00
switch ( fill_rule ) {
case Gfx : : WindingRule : : Nonzero :
builder . append ( " nonzero " sv ) ;
break ;
case Gfx : : WindingRule : : EvenOdd :
builder . append ( " evenodd " sv ) ;
}
2024-05-25 23:06:47 +01:00
for ( auto const & point : points ) {
2025-10-02 22:57:13 +13:00
builder . appendff ( " , {} {} " , point . x - > to_string ( mode ) , point . y - > to_string ( mode ) ) ;
2024-05-25 23:06:47 +01:00
}
builder . append ( ' ) ' ) ;
return MUST ( builder . to_string ( ) ) ;
}
2025-07-17 16:39:45 +01:00
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 ( ) ;
}
2024-05-25 23:06:47 +01:00
BasicShapeStyleValue : : ~ BasicShapeStyleValue ( ) = default ;
2024-08-20 17:27:08 +02:00
Gfx : : Path BasicShapeStyleValue : : to_path ( CSSPixelRect reference_box , Layout : : Node const & node ) const
2024-05-25 23:06:47 +01:00
{
2025-10-28 21:11:17 +13:00
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 ( ) ;
2024-05-25 23:06:47 +01:00
} ) ;
}
2024-11-29 22:44:14 +11:00
String BasicShapeStyleValue : : to_string ( SerializationMode mode ) const
2024-05-25 23:06:47 +01:00
{
2024-11-29 22:44:14 +11:00
return m_basic_shape . visit ( [ mode ] ( auto const & shape ) {
return shape . to_string ( mode ) ;
2024-05-25 23:06:47 +01:00
} ) ;
}
2025-10-28 21:11:17 +13:00
// https://www.w3.org/TR/css-shapes-1/#basic-shape-computed-values
2025-10-02 23:19:17 +13:00
ValueComparingNonnullRefPtr < StyleValue const > BasicShapeStyleValue : : absolutized ( ComputationContext const & computation_context ) const
{
2025-10-28 21:11:17 +13:00
// 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 ) ;
} ;
2025-12-10 13:33:22 +13:00
auto const absolutize_if_nonnull = [ & ] ( RefPtr < StyleValue const > const & value ) - > ValueComparingRefPtr < StyleValue const > {
if ( ! value )
return nullptr ;
return value - > absolutized ( computation_context ) ;
} ;
2025-10-02 23:19:17 +13:00
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 ) ;
2026-01-05 22:29:49 +13:00
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 )
2025-10-02 23:19:17 +13:00
return shape ;
2026-01-05 22:29:49 +13:00
return Inset { absolutized_top , absolutized_right , absolutized_bottom , absolutized_left , absolutized_border_radius } ;
2025-10-02 23:19:17 +13:00
} ,
[ & ] ( Xywh const & shape ) - > BasicShape {
2025-10-28 21:11:17 +13:00
// 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 ) ;
2025-10-02 23:19:17 +13:00
2026-01-05 22:29:49 +13:00
// FIXME: Pass actual border radius once we parse it
return Inset { * absolutized_top , * absolutized_right , * absolutized_bottom , * absolutized_left , BorderRadiusRectStyleValue : : create_zero ( ) } ;
2025-10-02 23:19:17 +13:00
} ,
[ & ] ( Rect const & shape ) - > BasicShape {
2025-10-28 21:11:17 +13:00
// 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:
// 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.
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 ) ;
2026-01-05 22:44:11 +13:00
auto absolutized_border_radius = shape . border_radius - > absolutized ( computation_context ) ;
2025-10-28 21:11:17 +13:00
2026-01-05 22:44:11 +13:00
return Inset { * absolutized_top , * absolutized_right , * absolutized_bottom , * absolutized_left , absolutized_border_radius } ;
2025-10-02 23:19:17 +13:00
} ,
[ & ] ( Circle const & shape ) - > BasicShape {
auto absolutized_radius = shape . radius - > absolutized ( computation_context ) ;
2025-12-10 13:33:22 +13:00
auto absolutized_position = absolutize_if_nonnull ( shape . position ) ;
2025-10-02 23:19:17 +13:00
2025-12-10 13:33:22 +13:00
if ( absolutized_radius = = shape . radius & & absolutized_position = = shape . position )
2025-10-02 23:19:17 +13:00
return shape ;
2025-12-10 13:33:22 +13:00
return Circle { absolutized_radius , absolutized_position } ;
2025-10-02 23:19:17 +13:00
} ,
[ & ] ( Ellipse const & shape ) - > BasicShape {
2025-12-12 14:21:35 +13:00
auto absolutized_radius = shape . radius - > absolutized ( computation_context ) ;
2025-12-12 14:42:40 +13:00
auto absolutized_position = absolutize_if_nonnull ( shape . position ) ;
2025-10-02 23:19:17 +13:00
2025-12-12 14:42:40 +13:00
if ( absolutized_radius = = shape . radius & & absolutized_position = = shape . position )
2025-10-02 23:19:17 +13:00
return shape ;
2025-12-12 14:42:40 +13:00
return Ellipse { absolutized_radius , absolutized_position } ;
2025-10-02 23:19:17 +13:00
} ,
[ & ] ( 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 ) ;
}
2024-05-25 23:06:47 +01:00
}