LibWeb: Add dedicated overflow-clip-margin style value

Store overflow-clip-margin as a structured style value instead of
collapsing it into generic keyword/length lists during parsing.

Keep omitted visual-box as a distinct state so painting can apply
the spec default per element type (content-box for replaced,
padding-box otherwise), while preserving canonical serialization.
This commit is contained in:
mikiubo 2026-04-30 21:19:28 +02:00 committed by Jelle Raaijmakers
parent a3e420bb9e
commit b9ed64d1ce
11 changed files with 180 additions and 27 deletions

View file

@ -290,6 +290,7 @@ set(SOURCES
CSS/StyleValues/NumberStyleValue.cpp
CSS/StyleValues/OpacityValueStyleValue.cpp
CSS/StyleValues/OpenTypeTaggedStyleValue.cpp
CSS/StyleValues/OverflowClipMarginStyleValue.cpp
CSS/StyleValues/PositionStyleValue.cpp
CSS/StyleValues/RadialGradientStyleValue.cpp
CSS/StyleValues/RadialSizeStyleValue.cpp

View file

@ -146,6 +146,22 @@ private:
using CursorData = Variant<NonnullRefPtr<CursorStyleValue const>, CursorPredefined>;
struct OverflowClipMarginSide {
Optional<BackgroundBox> visual_box {};
Length offset { Length::make_px(0) };
bool operator==(OverflowClipMarginSide const&) const = default;
};
struct OverflowClipMarginData {
OverflowClipMarginSide left;
OverflowClipMarginSide top;
OverflowClipMarginSide right;
OverflowClipMarginSide bottom;
bool operator==(OverflowClipMarginData const&) const = default;
};
using ListStyleType = Variant<Empty, RefPtr<CounterStyle const>, String>;
class InitialValues {
@ -228,7 +244,7 @@ public:
static LengthBox inset() { return {}; }
static LengthBox margin() { return { Length::make_px(0), Length::make_px(0), Length::make_px(0), Length::make_px(0) }; }
static LengthBox padding() { return { Length::make_px(0), Length::make_px(0), Length::make_px(0), Length::make_px(0) }; }
static LengthBox overflow_clip_margin() { return { Length::make_px(0), Length::make_px(0), Length::make_px(0), Length::make_px(0) }; }
static OverflowClipMarginData overflow_clip_margin() { return {}; }
static Size width() { return Size::make_auto(); }
static Size min_width() { return Size::make_auto(); }
static Size max_width() { return Size::make_none(); }
@ -667,7 +683,7 @@ public:
LengthBox const& inset() const { return m_noninherited.inset; }
LengthBox const& margin() const { return m_noninherited.margin; }
LengthBox const& padding() const { return m_noninherited.padding; }
LengthBox const& overflow_clip_margin() const { return m_noninherited.overflow_clip_margin; }
OverflowClipMarginData const& overflow_clip_margin() const { return m_noninherited.overflow_clip_margin; }
BorderData const& border_left() const { return m_noninherited.border_left; }
BorderData const& border_top() const { return m_noninherited.border_top; }
@ -863,7 +879,7 @@ protected:
LengthBox inset { InitialValues::inset() };
LengthBox margin { InitialValues::margin() };
LengthBox padding { InitialValues::padding() };
LengthBox overflow_clip_margin { InitialValues::overflow_clip_margin() };
OverflowClipMarginData overflow_clip_margin { InitialValues::overflow_clip_margin() };
Filter backdrop_filter { InitialValues::backdrop_filter() };
Filter filter { InitialValues::filter() };
BorderData border_left;
@ -1051,7 +1067,7 @@ public:
void set_inset(LengthBox const& inset) { m_noninherited.inset = inset; }
void set_margin(LengthBox const& margin) { m_noninherited.margin = margin; }
void set_padding(LengthBox const& padding) { m_noninherited.padding = padding; }
void set_overflow_clip_margin(LengthBox const& overflow_clip_margin) { m_noninherited.overflow_clip_margin = overflow_clip_margin; }
void set_overflow_clip_margin(OverflowClipMarginData const& overflow_clip_margin) { m_noninherited.overflow_clip_margin = overflow_clip_margin; }
void set_overflow_x(Overflow value) { m_noninherited.overflow_x = value; }
void set_overflow_y(Overflow value) { m_noninherited.overflow_y = value; }
void set_list_style_type(ListStyleType value) { m_inherited.list_style_type = move(value); }

View file

@ -47,6 +47,7 @@
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/OpenTypeTaggedStyleValue.h>
#include <LibWeb/CSS/StyleValues/OverflowClipMarginStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/RepeatStyleStyleValue.h>
@ -3330,22 +3331,29 @@ RefPtr<StyleValue const> Parser::parse_math_depth_value(TokenStream<ComponentVal
RefPtr<StyleValue const> Parser::parse_overflow_clip_margin_value(TokenStream<ComponentValue>& tokens)
{
// <visual-box> || <length>
auto parse_visual_box = [this](auto& tokens) -> Optional<Keyword> {
auto parse_visual_box = [this](auto& tokens) -> Optional<BackgroundBox> {
auto transaction = tokens.begin_transaction();
auto maybe_visual_box = parse_keyword_value(tokens);
if (!maybe_visual_box)
return {};
auto keyword = maybe_visual_box->to_keyword();
if (keyword != Keyword::ContentBox && keyword != Keyword::PaddingBox && keyword != Keyword::BorderBox)
Optional<BackgroundBox> box;
if (keyword == Keyword::ContentBox)
box = BackgroundBox::ContentBox;
else if (keyword == Keyword::PaddingBox)
box = BackgroundBox::PaddingBox;
else if (keyword == Keyword::BorderBox)
box = BackgroundBox::BorderBox;
else
return {};
transaction.commit();
return keyword;
return box;
};
RefPtr<StyleValue const> length;
Optional<Keyword> visual_box;
Optional<BackgroundBox> visual_box;
for (size_t i = 0; i < 2; ++i) {
tokens.discard_whitespace();
@ -3360,7 +3368,7 @@ RefPtr<StyleValue const> Parser::parse_overflow_clip_margin_value(TokenStream<Co
}
if (!length) {
if (auto maybe_length = parse_length_value(tokens, infinite_range)) {
if (auto maybe_length = parse_length_value(tokens, non_negative_range)) {
length = maybe_length.release_nonnull();
continue;
}
@ -3376,13 +3384,7 @@ RefPtr<StyleValue const> Parser::parse_overflow_clip_margin_value(TokenStream<Co
if (!length)
length = LengthStyleValue::create(Length::make_px(0));
if (!visual_box.has_value() || *visual_box == Keyword::PaddingBox)
return length;
if (length->is_length() && length->as_length().length().raw_value() == 0)
return KeywordStyleValue::create(*visual_box);
return StyleValueList::create({ KeywordStyleValue::create(*visual_box), length.release_nonnull() }, StyleValueList::Separator::Space);
return OverflowClipMarginStyleValue::create(visual_box, length.release_nonnull());
}
RefPtr<StyleValue const> Parser::parse_overflow_clip_margin_shorthand(PropertyID property_id, TokenStream<ComponentValue>& tokens)

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2026, mikiubo <michele.uboldi@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "OverflowClipMarginStyleValue.h"
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
namespace Web::CSS {
ValueComparingNonnullRefPtr<OverflowClipMarginStyleValue const> OverflowClipMarginStyleValue::create(Optional<BackgroundBox> visual_box, NonnullRefPtr<StyleValue const> offset)
{
return adopt_ref(*new (nothrow) OverflowClipMarginStyleValue(visual_box, move(offset)));
}
OverflowClipMarginStyleValue::OverflowClipMarginStyleValue(Optional<BackgroundBox> visual_box, NonnullRefPtr<StyleValue const> offset)
: StyleValueWithDefaultOperators(Type::OverflowClipMargin)
, m_visual_box(visual_box)
, m_offset(move(offset))
{
}
OverflowClipMarginStyleValue::~OverflowClipMarginStyleValue() = default;
// https://drafts.csswg.org/css-overflow-4/#overflow-clip-margin
void OverflowClipMarginStyleValue::serialize(StringBuilder& builder, SerializationMode mode) const
{
bool has_explicit_box = m_visual_box.has_value();
bool is_default_box = has_explicit_box && *m_visual_box == BackgroundBox::PaddingBox;
bool is_zero_offset = m_offset->is_length() && m_offset->as_length().length().raw_value() == 0;
if (!has_explicit_box || is_default_box) {
m_offset->serialize(builder, mode);
} else if (is_zero_offset) {
builder.append(CSS::to_string(*m_visual_box));
} else {
builder.append(CSS::to_string(*m_visual_box));
builder.append(' ');
m_offset->serialize(builder, mode);
}
}
ValueComparingNonnullRefPtr<StyleValue const> OverflowClipMarginStyleValue::absolutized(ComputationContext const& context) const
{
auto new_offset = m_offset->absolutized(context);
if (new_offset->equals(m_offset))
return *this;
return create(m_visual_box, move(new_offset));
}
bool OverflowClipMarginStyleValue::properties_equal(OverflowClipMarginStyleValue const& other) const
{
return m_visual_box == other.m_visual_box
&& m_offset == other.m_offset;
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2026, mikiubo <michele.uboldi@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
namespace Web::CSS {
class OverflowClipMarginStyleValue final : public StyleValueWithDefaultOperators<OverflowClipMarginStyleValue> {
public:
static ValueComparingNonnullRefPtr<OverflowClipMarginStyleValue const> create(Optional<BackgroundBox> visual_box, NonnullRefPtr<StyleValue const> offset);
virtual ~OverflowClipMarginStyleValue() override;
Optional<BackgroundBox> visual_box() const { return m_visual_box; }
StyleValue const& offset() const { return m_offset; }
virtual void serialize(StringBuilder&, SerializationMode) const override;
virtual ValueComparingNonnullRefPtr<StyleValue const> absolutized(ComputationContext const&) const override;
bool properties_equal(OverflowClipMarginStyleValue const&) const;
virtual bool is_computationally_independent() const override { return m_offset->is_computationally_independent(); }
private:
OverflowClipMarginStyleValue(Optional<BackgroundBox> visual_box, NonnullRefPtr<StyleValue const> offset);
Optional<BackgroundBox> m_visual_box;
ValueComparingNonnullRefPtr<StyleValue const> m_offset;
};
}

View file

@ -181,8 +181,10 @@ void ShorthandStyleValue::serialize(StringBuilder& builder, SerializationMode mo
m_properties.values.first()->serialize(builder, mode);
};
// Then special cases
// FIXME: overflow-clip-margin needs a special case here for when its longhands aren't identical.
// Ref: https://github.com/w3c/csswg-drafts/issues/8381
// Then special cases
switch (m_properties.shorthand_property) {
case PropertyID::All: {
// NOTE: 'all' can only be serialized in the case all sub-properties share the same CSS-wide keyword, this is

View file

@ -57,6 +57,7 @@
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/OpacityValueStyleValue.h>
#include <LibWeb/CSS/StyleValues/OpenTypeTaggedStyleValue.h>
#include <LibWeb/CSS/StyleValues/OverflowClipMarginStyleValue.h>
#include <LibWeb/CSS/StyleValues/PendingSubstitutionStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h>

View file

@ -76,6 +76,7 @@ namespace Web::CSS {
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Number, number, NumberStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(OpacityValue, opacity_value, OpacityValueStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(OpenTypeTagged, open_type_tagged, OpenTypeTaggedStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(OverflowClipMargin, overflow_clip_margin, OverflowClipMarginStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(PendingSubstitution, pending_substitution, PendingSubstitutionStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Percentage, percentage, PercentageStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Position, position, PositionStyleValue) \

View file

@ -356,6 +356,7 @@ class NumberStyleValue;
class NumericType;
class OpacityValueStyleValue;
class OpenTypeTaggedStyleValue;
class OverflowClipMarginStyleValue;
class ParsedFontFace;
class PendingSubstitutionStyleValue;
class Percentage;

View file

@ -17,6 +17,7 @@
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/OverflowClipMarginStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
@ -828,7 +829,25 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_inset(computed_style.length_box(CSS::PropertyID::Left, CSS::PropertyID::Top, CSS::PropertyID::Right, CSS::PropertyID::Bottom, CSS::LengthPercentageOrAuto::make_auto()));
computed_values.set_margin(computed_style.length_box(CSS::PropertyID::MarginLeft, CSS::PropertyID::MarginTop, CSS::PropertyID::MarginRight, CSS::PropertyID::MarginBottom, CSS::Length::make_px(0)));
computed_values.set_padding(computed_style.length_box(CSS::PropertyID::PaddingLeft, CSS::PropertyID::PaddingTop, CSS::PropertyID::PaddingRight, CSS::PropertyID::PaddingBottom, CSS::Length::make_px(0)));
computed_values.set_overflow_clip_margin(computed_style.length_box(CSS::PropertyID::OverflowClipMarginLeft, CSS::PropertyID::OverflowClipMarginTop, CSS::PropertyID::OverflowClipMarginRight, CSS::PropertyID::OverflowClipMarginBottom, CSS::Length::make_px(0)));
{
auto extract_side = [&](CSS::PropertyID property_id) -> CSS::OverflowClipMarginSide {
auto const& value = computed_style.property(property_id);
if (value.is_overflow_clip_margin()) {
auto const& overflow_clip_margin = value.as_overflow_clip_margin();
CSS::Length offset = CSS::Length::make_px(0);
if (overflow_clip_margin.offset().is_length())
offset = overflow_clip_margin.offset().as_length().length();
return { overflow_clip_margin.visual_box(), offset };
}
return {};
};
CSS::OverflowClipMarginData data;
data.left = extract_side(CSS::PropertyID::OverflowClipMarginLeft);
data.top = extract_side(CSS::PropertyID::OverflowClipMarginTop);
data.right = extract_side(CSS::PropertyID::OverflowClipMarginRight);
data.bottom = extract_side(CSS::PropertyID::OverflowClipMarginBottom);
computed_values.set_overflow_clip_margin(data);
}
computed_values.set_box_shadow(computed_style.box_shadow(*this));

View file

@ -363,20 +363,36 @@ CSSPixelRect PaintableBox::overflow_clip_edge_rect() const
// '<visual-box>'
// Specifies the box edge to use as the overflow clip edge origin, i.e. when the specified offset is zero.
// If omitted, defaults to 'padding-box' on non-replaced elements, or 'content-box' on replaced elements.
// FIXME: We can't parse this yet so it's always omitted for now.
auto overflow_clip_edge = absolute_padding_box_rect();
if (layout_node().is_replaced_box()) {
overflow_clip_edge = absolute_rect();
}
auto const& overflow_clip_margin = computed_values().overflow_clip_margin();
auto resolve_box_edge = [&](CSS::OverflowClipMarginSide const& side) -> CSSPixelRect {
auto box = side.visual_box;
if (!box.has_value()) {
if (layout_node().is_replaced_box())
box = CSS::BackgroundBox::ContentBox;
else
box = CSS::BackgroundBox::PaddingBox;
}
switch (*box) {
case CSS::BackgroundBox::ContentBox:
return absolute_rect();
case CSS::BackgroundBox::BorderBox:
return absolute_border_box_rect();
case CSS::BackgroundBox::PaddingBox:
default:
return absolute_padding_box_rect();
}
};
auto overflow_clip_edge = resolve_box_edge(overflow_clip_margin.top);
// '<length [0,∞]>'
// The specified offset dictates how much the overflow clip edge is expanded from the specified box edge
// Negative values are invalid. Defaults to zero if omitted.
overflow_clip_edge.inflate(
computed_values().overflow_clip_margin().top().length().absolute_length_to_px(),
computed_values().overflow_clip_margin().right().length().absolute_length_to_px(),
computed_values().overflow_clip_margin().bottom().length().absolute_length_to_px(),
computed_values().overflow_clip_margin().left().length().absolute_length_to_px());
overflow_clip_margin.top.offset.absolute_length_to_px(),
overflow_clip_margin.right.offset.absolute_length_to_px(),
overflow_clip_margin.bottom.offset.absolute_length_to_px(),
overflow_clip_margin.left.offset.absolute_length_to_px());
return overflow_clip_edge;
}