LibWeb: Store GradientStyleValue color-stop positions as StyleValues

A few things fall out of this:
- We no longer need to templatize our color-stop list types.
- A bit more code is required to resolve gradient data.

This results in a slightly different rendering for a couple of the test
gradients, with a larger difference between macOS and Linux. I've
expanded the fuzziness factor to cover for it.
This commit is contained in:
Sam Atkins 2025-11-28 17:14:31 +00:00
parent 71397c876c
commit 73fbaaba77
Notes: github-actions[bot] 2025-12-01 11:10:16 +00:00
10 changed files with 103 additions and 85 deletions

View file

@ -18,8 +18,7 @@
namespace Web::CSS::Parser {
template<typename TElement>
Optional<Vector<TElement>> Parser::parse_color_stop_list(TokenStream<ComponentValue>& tokens, auto parse_position)
Optional<Vector<ColorStopListElement>> Parser::parse_color_stop_list(TokenStream<ComponentValue>& tokens, auto parse_position)
{
enum class ElementType {
Garbage,
@ -27,20 +26,20 @@ Optional<Vector<TElement>> Parser::parse_color_stop_list(TokenStream<ComponentVa
ColorHint
};
auto parse_color_stop_list_element = [&](TElement& element) -> ElementType {
auto parse_color_stop_list_element = [&](auto& element) -> ElementType {
tokens.discard_whitespace();
if (!tokens.has_next_token())
return ElementType::Garbage;
RefPtr<StyleValue const> color;
Optional<typename TElement::PositionType> position;
Optional<typename TElement::PositionType> second_position;
if (position = parse_position(tokens); position.has_value()) {
RefPtr<StyleValue const> position;
RefPtr<StyleValue const> second_position;
if (position = parse_position(tokens); position) {
// [<T-percentage> <color>] or [<T-percentage>]
tokens.discard_whitespace();
// <T-percentage>
if (!tokens.has_next_token() || tokens.next_token().is(Token::Type::Comma)) {
element.transition_hint = typename TElement::ColorHint { *position };
element.transition_hint = ColorStopListElement::ColorHint { *position };
return ElementType::ColorHint;
}
// <T-percentage> <color>
@ -60,24 +59,24 @@ Optional<Vector<TElement>> Parser::parse_color_stop_list(TokenStream<ComponentVa
for (auto stop_position : Array { &position, &second_position }) {
if (tokens.has_next_token() && !tokens.next_token().is(Token::Type::Comma)) {
*stop_position = parse_position(tokens);
if (!stop_position->has_value())
if (!stop_position)
return ElementType::Garbage;
tokens.discard_whitespace();
}
}
}
element.color_stop = typename TElement::ColorStop { color, position, second_position };
element.color_stop = ColorStopListElement::ColorStop { color, position, second_position };
return ElementType::ColorStop;
};
TElement first_element {};
ColorStopListElement first_element {};
if (parse_color_stop_list_element(first_element) != ElementType::ColorStop)
return {};
Vector<TElement> color_stops { first_element };
Vector<ColorStopListElement> color_stops { first_element };
while (tokens.has_next_token()) {
TElement list_element {};
ColorStopListElement list_element {};
tokens.discard_whitespace();
if (!tokens.consume_a_token().is(Token::Type::Comma))
return {};
@ -110,34 +109,34 @@ static StringView consume_if_starts_with(StringView str, StringView start, auto
return str;
}
Optional<Vector<LinearColorStopListElement>> Parser::parse_linear_color_stop_list(TokenStream<ComponentValue>& tokens)
Optional<Vector<ColorStopListElement>> Parser::parse_linear_color_stop_list(TokenStream<ComponentValue>& tokens)
{
// <color-stop-list> =
// <linear-color-stop> , [ <linear-color-hint>? , <linear-color-stop> ]#
return parse_color_stop_list<LinearColorStopListElement>(
return parse_color_stop_list(
tokens,
[&](auto& it) { return parse_length_percentage(it); });
[&](auto& it) { return parse_length_percentage_value(it); });
}
Optional<Vector<AngularColorStopListElement>> Parser::parse_angular_color_stop_list(TokenStream<ComponentValue>& tokens)
Optional<Vector<ColorStopListElement>> Parser::parse_angular_color_stop_list(TokenStream<ComponentValue>& tokens)
{
auto context_guard = push_temporary_value_parsing_context(SpecialContext::AngularColorStopList);
// <angular-color-stop-list> =
// <angular-color-stop> , [ <angular-color-hint>? , <angular-color-stop> ]#
return parse_color_stop_list<AngularColorStopListElement>(
return parse_color_stop_list(
tokens,
[&](TokenStream<ComponentValue>& it) -> Optional<AnglePercentage> {
[&](TokenStream<ComponentValue>& it) -> RefPtr<StyleValue const> {
if (tokens.next_token().is(Token::Type::Number)) {
auto transaction = tokens.begin_transaction();
auto numeric_value = tokens.consume_a_token().token().number_value();
if (numeric_value == 0) {
transaction.commit();
return Angle::make_degrees(0);
return AngleStyleValue::create(Angle::make_degrees(0));
}
}
return parse_angle_percentage(it);
return parse_angle_percentage_value(it);
});
}

View file

@ -356,10 +356,9 @@ private:
RefPtr<FitContentStyleValue const> parse_fit_content_value(TokenStream<ComponentValue>&);
template<typename TElement>
Optional<Vector<TElement>> parse_color_stop_list(TokenStream<ComponentValue>& tokens, auto parse_position);
Optional<Vector<LinearColorStopListElement>> parse_linear_color_stop_list(TokenStream<ComponentValue>&);
Optional<Vector<AngularColorStopListElement>> parse_angular_color_stop_list(TokenStream<ComponentValue>&);
Optional<Vector<ColorStopListElement>> parse_color_stop_list(TokenStream<ComponentValue>& tokens, auto parse_position);
Optional<Vector<ColorStopListElement>> parse_linear_color_stop_list(TokenStream<ComponentValue>&);
Optional<Vector<ColorStopListElement>> parse_angular_color_stop_list(TokenStream<ComponentValue>&);
Optional<InterpolationMethod> parse_interpolation_method(TokenStream<ComponentValue>&);
RefPtr<LinearGradientStyleValue const> parse_linear_gradient_function(TokenStream<ComponentValue>&);

View file

@ -16,4 +16,23 @@ GC::Ref<CSSStyleValue> AbstractImageStyleValue::reify(JS::Realm& realm, FlyStrin
return CSSImageValue::create(realm, *this);
}
void serialize_color_stop_list(StringBuilder& builder, Vector<ColorStopListElement> const& color_stop_list, SerializationMode mode)
{
bool first = true;
for (auto const& element : color_stop_list) {
if (!first)
builder.append(", "sv);
if (element.transition_hint.has_value())
builder.appendff("{}, "sv, element.transition_hint->value->to_string(mode));
builder.append(element.color_stop.color->to_string(mode));
if (element.color_stop.position)
builder.appendff(" {}"sv, element.color_stop.position->to_string(mode));
if (element.color_stop.second_position)
builder.appendff(" {}"sv, element.color_stop.second_position->to_string(mode));
first = false;
}
}
}

View file

@ -157,45 +157,22 @@ struct InterpolationMethod {
bool operator==(InterpolationMethod const&) const = default;
};
template<typename TPosition>
struct ColorStopListElement {
using PositionType = TPosition;
struct ColorHint {
TPosition value;
inline bool operator==(ColorHint const&) const = default;
NonnullRefPtr<StyleValue const> value;
bool operator==(ColorHint const&) const = default;
};
Optional<ColorHint> transition_hint;
struct ColorStop {
RefPtr<StyleValue const> color;
Optional<TPosition> position;
Optional<TPosition> second_position = {};
inline bool operator==(ColorStop const&) const = default;
RefPtr<StyleValue const> position;
RefPtr<StyleValue const> second_position {};
bool operator==(ColorStop const&) const = default;
} color_stop;
inline bool operator==(ColorStopListElement const&) const = default;
bool operator==(ColorStopListElement const&) const = default;
};
using LinearColorStopListElement = ColorStopListElement<LengthPercentage>;
using AngularColorStopListElement = ColorStopListElement<AnglePercentage>;
static void serialize_color_stop_list(StringBuilder& builder, auto const& color_stop_list, SerializationMode mode)
{
bool first = true;
for (auto const& element : color_stop_list) {
if (!first)
builder.append(", "sv);
if (element.transition_hint.has_value())
builder.appendff("{}, "sv, element.transition_hint->value.to_string(mode));
builder.append(element.color_stop.color->to_string(mode));
for (auto position : Array { &element.color_stop.position, &element.color_stop.second_position }) {
if (position->has_value())
builder.appendff(" {}"sv, (*position)->to_string(mode));
}
first = false;
}
}
void serialize_color_stop_list(StringBuilder&, Vector<ColorStopListElement> const&, SerializationMode);
}

View file

@ -9,8 +9,6 @@
#pragma once
#include <LibWeb/CSS/Angle.h>
#include <LibWeb/CSS/CalculatedOr.h>
#include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h>
#include <LibWeb/Painting/GradientPainting.h>
@ -18,7 +16,7 @@ namespace Web::CSS {
class ConicGradientStyleValue final : public AbstractImageStyleValue {
public:
static ValueComparingNonnullRefPtr<ConicGradientStyleValue const> create(ValueComparingRefPtr<StyleValue const> from_angle, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<AngularColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method)
static ValueComparingNonnullRefPtr<ConicGradientStyleValue const> create(ValueComparingRefPtr<StyleValue const> from_angle, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<ColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method)
{
VERIFY(!color_stop_list.is_empty());
bool any_non_legacy = color_stop_list.find_first_index_if([](auto const& stop) { return !stop.color_stop.color->is_keyword() && stop.color_stop.color->as_color().color_syntax() == ColorSyntax::Modern; }).has_value();
@ -31,7 +29,7 @@ public:
virtual bool equals(StyleValue const& other) const override;
Vector<AngularColorStopListElement> const& color_stop_list() const
Vector<ColorStopListElement> const& color_stop_list() const
{
return m_properties.color_stop_list;
}
@ -55,7 +53,7 @@ public:
bool is_repeating() const { return m_properties.repeating == GradientRepeating::Yes; }
private:
ConicGradientStyleValue(ValueComparingRefPtr<StyleValue const> from_angle, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<AngularColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method, ColorSyntax color_syntax)
ConicGradientStyleValue(ValueComparingRefPtr<StyleValue const> from_angle, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<ColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method, ColorSyntax color_syntax)
: AbstractImageStyleValue(Type::ConicGradient)
, m_properties { .from_angle = move(from_angle), .position = move(position), .color_stop_list = move(color_stop_list), .repeating = repeating, .interpolation_method = interpolation_method, .color_syntax = color_syntax }
{
@ -64,7 +62,7 @@ private:
struct Properties {
ValueComparingRefPtr<StyleValue const> from_angle;
ValueComparingNonnullRefPtr<PositionStyleValue const> position;
Vector<AngularColorStopListElement> color_stop_list;
Vector<ColorStopListElement> color_stop_list;
GradientRepeating repeating;
Optional<InterpolationMethod> interpolation_method;
ColorSyntax color_syntax;

View file

@ -39,7 +39,7 @@ public:
WebKit
};
static ValueComparingNonnullRefPtr<LinearGradientStyleValue const> create(GradientDirection direction, Vector<LinearColorStopListElement> color_stop_list, GradientType type, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method)
static ValueComparingNonnullRefPtr<LinearGradientStyleValue const> create(GradientDirection direction, Vector<ColorStopListElement> color_stop_list, GradientType type, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method)
{
VERIFY(!color_stop_list.is_empty());
bool any_non_legacy = color_stop_list.find_first_index_if([](auto const& stop) { return !stop.color_stop.color->is_keyword() && stop.color_stop.color->as_color().color_syntax() == ColorSyntax::Modern; }).has_value();
@ -50,7 +50,7 @@ public:
virtual ~LinearGradientStyleValue() override = default;
virtual bool equals(StyleValue const& other) const override;
Vector<LinearColorStopListElement> const& color_stop_list() const
Vector<ColorStopListElement> const& color_stop_list() const
{
return m_properties.color_stop_list;
}
@ -76,7 +76,7 @@ public:
void paint(DisplayListRecordingContext& context, DevicePixelRect const& dest_rect, CSS::ImageRendering image_rendering) const override;
private:
LinearGradientStyleValue(GradientDirection direction, Vector<LinearColorStopListElement> color_stop_list, GradientType type, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method, ColorSyntax color_syntax)
LinearGradientStyleValue(GradientDirection direction, Vector<ColorStopListElement> color_stop_list, GradientType type, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method, ColorSyntax color_syntax)
: AbstractImageStyleValue(Type::LinearGradient)
, m_properties { .direction = direction, .color_stop_list = move(color_stop_list), .gradient_type = type, .repeating = repeating, .interpolation_method = interpolation_method, .color_syntax = color_syntax }
{
@ -84,7 +84,7 @@ private:
struct Properties {
GradientDirection direction;
Vector<LinearColorStopListElement> color_stop_list;
Vector<ColorStopListElement> color_stop_list;
GradientType gradient_type;
GradientRepeating repeating;
Optional<InterpolationMethod> interpolation_method;

View file

@ -43,7 +43,7 @@ public:
using Size = Variant<Extent, CircleSize, EllipseSize>;
static ValueComparingNonnullRefPtr<RadialGradientStyleValue const> create(EndingShape ending_shape, Size size, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<LinearColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method)
static ValueComparingNonnullRefPtr<RadialGradientStyleValue const> create(EndingShape ending_shape, Size size, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<ColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method)
{
VERIFY(!color_stop_list.is_empty());
bool any_non_legacy = color_stop_list.find_first_index_if([](auto const& stop) { return !stop.color_stop.color->is_keyword() && stop.color_stop.color->as_color().color_syntax() == ColorSyntax::Modern; }).has_value();
@ -56,7 +56,7 @@ public:
virtual bool equals(StyleValue const& other) const override;
Vector<LinearColorStopListElement> const& color_stop_list() const
Vector<ColorStopListElement> const& color_stop_list() const
{
return m_properties.color_stop_list;
}
@ -80,7 +80,7 @@ public:
virtual ~RadialGradientStyleValue() override = default;
private:
RadialGradientStyleValue(EndingShape ending_shape, Size size, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<LinearColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method, ColorSyntax color_syntax)
RadialGradientStyleValue(EndingShape ending_shape, Size size, ValueComparingNonnullRefPtr<PositionStyleValue const> position, Vector<ColorStopListElement> color_stop_list, GradientRepeating repeating, Optional<InterpolationMethod> interpolation_method, ColorSyntax color_syntax)
: AbstractImageStyleValue(Type::RadialGradient)
, m_properties { .ending_shape = ending_shape, .size = size, .position = move(position), .color_stop_list = move(color_stop_list), .repeating = repeating, .interpolation_method = interpolation_method, .color_syntax = color_syntax }
{
@ -90,7 +90,7 @@ private:
EndingShape ending_shape;
Size size;
ValueComparingNonnullRefPtr<PositionStyleValue const> position;
Vector<LinearColorStopListElement> color_stop_list;
Vector<ColorStopListElement> color_stop_list;
GradientRepeating repeating;
Optional<InterpolationMethod> interpolation_method;
ColorSyntax color_syntax;

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -7,7 +8,10 @@
#include <AK/Math.h>
#include <LibGfx/Gradients.h>
#include <LibWeb/CSS/CalculationResolutionContext.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
#include <LibWeb/CSS/StyleValues/ConicGradientStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/LinearGradientStyleValue.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/RadialGradientStyleValue.h>
@ -16,13 +20,13 @@
namespace Web::Painting {
static ColorStopData resolve_color_stop_positions(Layout::NodeWithStyle const& node, auto const& color_stop_list, auto resolve_position_to_float, bool repeating)
static ColorStopData resolve_color_stop_positions(Layout::NodeWithStyle const& node, Vector<CSS::ColorStopListElement> const& color_stop_list, auto resolve_position_to_float, bool repeating)
{
VERIFY(!color_stop_list.is_empty());
ColorStopList resolved_color_stops;
auto color_stop_length = [&](auto& stop) {
return stop.color_stop.second_position.has_value() ? 2 : 1;
return stop.color_stop.second_position ? 2 : 1;
};
size_t expanded_size = 0;
@ -46,7 +50,7 @@ static ColorStopData resolve_color_stop_positions(Layout::NodeWithStyle const& n
// set its position to be equal to the largest specified position of any color stop
// or transition hint before it.
auto max_previous_color_stop_or_hint = resolved_color_stops[0].position;
auto resolve_stop_position = [&](auto& position) {
auto resolve_stop_position = [&](CSS::StyleValue const& position) {
float value = resolve_position_to_float(position);
value = max(value, max_previous_color_stop_or_hint);
max_previous_color_stop_or_hint = value;
@ -55,10 +59,10 @@ static ColorStopData resolve_color_stop_positions(Layout::NodeWithStyle const& n
size_t resolved_index = 0;
for (auto& stop : color_stop_list) {
if (stop.transition_hint.has_value())
resolved_color_stops[resolved_index].transition_hint = resolve_stop_position(stop.transition_hint->value);
if (stop.color_stop.position.has_value())
resolved_color_stops[resolved_index].transition_hint = resolve_stop_position(*stop.transition_hint->value);
if (stop.color_stop.position)
resolved_color_stops[resolved_index].position = resolve_stop_position(*stop.color_stop.position);
if (stop.color_stop.second_position.has_value())
if (stop.color_stop.second_position)
resolved_color_stops[++resolved_index].position = resolve_stop_position(*stop.color_stop.second_position);
++resolved_index;
}
@ -116,9 +120,17 @@ LinearGradientData resolve_linear_gradient_data(Layout::NodeWithStyle const& nod
auto gradient_angle = linear_gradient.angle_degrees(gradient_size);
auto gradient_length_px = Gfx::calculate_gradient_length(gradient_size.to_type<float>(), gradient_angle);
CSS::CalculationResolutionContext context {
.percentage_basis = CSS::Length::make_px(gradient_length_px),
.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(node),
};
auto resolved_color_stops = resolve_color_stop_positions(
node, linear_gradient.color_stop_list(), [&](auto const& length_percentage) {
return length_percentage.to_px(node, CSSPixels::nearest_value_for(gradient_length_px)).to_float() / static_cast<float>(gradient_length_px);
node, linear_gradient.color_stop_list(), [&](auto const& position) -> float {
if (position.is_length())
return position.as_length().length().to_px_without_rounding(*context.length_resolution_context) / gradient_length_px;
if (position.is_percentage())
return position.as_percentage().percentage().as_fraction();
return position.as_calculated().resolve_length(context)->to_px_without_rounding(*context.length_resolution_context) / gradient_length_px;
},
linear_gradient.is_repeating());
@ -127,24 +139,38 @@ LinearGradientData resolve_linear_gradient_data(Layout::NodeWithStyle const& nod
ConicGradientData resolve_conic_gradient_data(Layout::NodeWithStyle const& node, CSS::ConicGradientStyleValue const& conic_gradient)
{
CSS::Angle one_turn(360.0f, CSS::AngleUnit::Deg);
auto resolved_color_stops = resolve_color_stop_positions(
node, conic_gradient.color_stop_list(), [&](auto const& angle_percentage) {
return angle_percentage.resolved(node, one_turn).to_degrees() / one_turn.to_degrees();
},
conic_gradient.is_repeating());
CSS::Angle const one_turn { 360.0f, CSS::AngleUnit::Deg };
CSS::CalculationResolutionContext context {
.percentage_basis = one_turn,
.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(node),
};
auto resolved_color_stops = resolve_color_stop_positions(
node, conic_gradient.color_stop_list(), [&](auto const& position) -> float {
if (position.is_angle())
return position.as_angle().angle().to_degrees() / one_turn.to_degrees();
if (position.is_percentage())
return position.as_percentage().percentage().as_fraction();
return position.as_calculated().resolve_angle(context)->to_degrees() / one_turn.to_degrees();
},
conic_gradient.is_repeating());
return { conic_gradient.angle_degrees(context), resolved_color_stops, conic_gradient.interpolation_method() };
}
RadialGradientData resolve_radial_gradient_data(Layout::NodeWithStyle const& node, CSSPixelSize gradient_size, CSS::RadialGradientStyleValue const& radial_gradient)
{
CSS::CalculationResolutionContext context {
.percentage_basis = CSS::Length::make_px(gradient_size.width()),
.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(node),
};
// Start center, goes right to ending point, where the gradient line intersects the ending shape
auto resolved_color_stops = resolve_color_stop_positions(
node, radial_gradient.color_stop_list(), [&](auto const& length_percentage) {
return length_percentage.to_px(node, gradient_size.width()).to_float() / gradient_size.width().to_float();
node, radial_gradient.color_stop_list(), [&](auto const& position) -> float {
if (position.is_length())
return position.as_length().length().to_px_without_rounding(*context.length_resolution_context) / gradient_size.width().to_float();
if (position.is_percentage())
return position.as_percentage().percentage().as_fraction();
return position.as_calculated().resolve_length(context)->to_px_without_rounding(*context.length_resolution_context) / gradient_size.width().to_float();
},
radial_gradient.is_repeating());
return { resolved_color_stops, radial_gradient.interpolation_method() };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 488 KiB

Before After
Before After

View file

@ -1,6 +1,6 @@
<!DOCTYPE html>
<link rel="match" href="../expected/css-gradients-ref.html" />
<meta name="fuzzy" content="maxDifference=0-63;totalPixels=0-167">
<meta name="fuzzy" content="maxDifference=0-162;totalPixels=0-428">
<style>
body {
background-color: white;