LibWeb: Don't resolve colors with unresolved components

`CSSColorValue`s which have unresolved `calc` components should be able
to be resolved. Previously we would always resolve them but with
incorrect values.

This is useful as we will now be able to now whether we should serialize
colors in their normalized form or not.

Slight regression in that we now serialize (RGB, HSL and HWB) colors
with components that rely on compute-time information as an empty
string, but that will be fixed in the next commit.
This commit is contained in:
Callum Law 2025-07-03 12:59:24 +12:00 committed by Sam Atkins
commit 6a9c8d7767
Notes: github-actions[bot] 2025-07-16 12:06:47 +00:00
30 changed files with 278 additions and 129 deletions

View file

@ -393,7 +393,7 @@ public:
virtual ValueComparingNonnullRefPtr<CSSStyleValue const> absolutized(CSSPixelRect const& viewport_rect, Length::FontMetrics const& font_metrics, Length::FontMetrics const& root_font_metrics) const;
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const { return {}; }
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const { return {}; }
Keyword to_keyword() const;
virtual String to_string(SerializationMode) const = 0;

View file

@ -226,7 +226,7 @@ Color ComputedProperties::color_or_fallback(PropertyID id, Layout::NodeWithStyle
auto const& value = property(id);
if (!value.has_color())
return fallback;
return value.to_color(node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(node) });
return value.to_color(node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(node) }).value();
}
// https://drafts.csswg.org/css-color-adjust-1/#determine-the-used-color-scheme
@ -447,7 +447,7 @@ Color ComputedProperties::flood_color(Layout::NodeWithStyle const& node) const
{
auto const& value = property(PropertyID::FloodColor);
if (value.has_color()) {
return value.to_color(node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(node) });
return value.to_color(node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(node) }).value();
}
return InitialValues::flood_color();
@ -940,7 +940,7 @@ Color ComputedProperties::caret_color(Layout::NodeWithStyle const& node) const
return node.computed_values().color();
if (value.has_color())
return value.to_color(node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(node) });
return value.to_color(node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(node) }).value();
return InitialValues::caret_color();
}
@ -1203,7 +1203,7 @@ Vector<ShadowData> ComputedProperties::shadow(PropertyID property_id, Layout::No
maybe_offset_y.release_value(),
maybe_blur_radius.release_value(),
maybe_spread_distance.release_value(),
value.color()->to_color(as<Layout::NodeWithStyle>(layout_node), { .length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node) }),
value.color()->to_color(as<Layout::NodeWithStyle>(layout_node), { .length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node) }).value(),
value.placement()
};
};
@ -1826,7 +1826,7 @@ Color ComputedProperties::stop_color() const
// FIXME: This is used by the SVGStopElement, which does not participate in layout, so we can't pass a layout
// node or CalculationResolutionContext. This means we don't support all valid colors (e.g. palette
// colors, calculated values which depend on length resolution, etc)
return value->to_color({}, {});
return value->to_color({}, {}).value_or(Color::Black);
}
return Color::Black;
}
@ -1910,8 +1910,8 @@ ScrollbarColorData ComputedProperties::scrollbar_color(Layout::NodeWithStyle con
if (value.is_scrollbar_color()) {
auto& scrollbar_color_value = value.as_scrollbar_color();
auto thumb_color = scrollbar_color_value.thumb_color()->to_color(layout_node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node) });
auto track_color = scrollbar_color_value.track_color()->to_color(layout_node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node) });
auto thumb_color = scrollbar_color_value.thumb_color()->to_color(layout_node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node) }).value();
auto track_color = scrollbar_color_value.track_color()->to_color(layout_node, { .length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node) }).value();
return { thumb_color, track_color };
}

View file

@ -688,13 +688,25 @@ RefPtr<CSSStyleValue const> interpolate_box_shadow(DOM::Element& element, Calcul
auto interpolated_spread_distance = interpolate_value(element, calculation_context, from_shadow.spread_distance(), to_shadow.spread_distance(), delta, allow_discrete);
if (!interpolated_offset_x || !interpolated_offset_y || !interpolated_blur_radius || !interpolated_spread_distance)
return {};
auto color_syntax = ColorSyntax::Legacy;
if ((!from_shadow.color()->is_keyword() && from_shadow.color()->as_color().color_syntax() == ColorSyntax::Modern)
|| (!to_shadow.color()->is_keyword() && to_shadow.color()->as_color().color_syntax() == ColorSyntax::Modern)) {
color_syntax = ColorSyntax::Modern;
}
// FIXME: If we aren't able to resolve the colors here, we should postpone interpolation until we can (perhaps
// by creating something similar to a ColorMixStyleValue).
auto from_color = from_shadow.color()->to_color(layout_node, resolution_context);
auto to_color = to_shadow.color()->to_color(layout_node, resolution_context);
Color interpolated_color = Color::Black;
if (from_color.has_value() && to_color.has_value())
interpolated_color = interpolate_color(from_color.value(), to_color.value(), delta, color_syntax);
auto result_shadow = ShadowStyleValue::create(
CSSColorValue::create_from_color(interpolate_color(from_shadow.color()->to_color(layout_node, resolution_context), to_shadow.color()->to_color(layout_node, resolution_context), delta, color_syntax), ColorSyntax::Modern),
CSSColorValue::create_from_color(interpolated_color, ColorSyntax::Modern),
*interpolated_offset_x,
*interpolated_offset_y,
*interpolated_blur_radius,
@ -817,7 +829,18 @@ static RefPtr<CSSStyleValue const> interpolate_value_impl(DOM::Element& element,
|| (!to.is_keyword() && to.as_color().color_syntax() == ColorSyntax::Modern)) {
color_syntax = ColorSyntax::Modern;
}
return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node, resolution_context), to.to_color(layout_node, resolution_context), delta, color_syntax), ColorSyntax::Modern);
// FIXME: If we aren't able to resolve the colors here, we should postpone interpolation until we can (perhaps
// by creating something similar to a ColorMixStyleValue).
auto from_color = from.to_color(layout_node, resolution_context);
auto to_color = to.to_color(layout_node, resolution_context);
Color interpolated_color = Color::Black;
if (from_color.has_value() && to_color.has_value())
interpolated_color = interpolate_color(from_color.value(), to_color.value(), delta, color_syntax);
return CSSColorValue::create_from_color(interpolated_color, ColorSyntax::Modern);
}
case CSSStyleValue::Type::Edge: {
auto resolved_from = from.as_edge().resolved_value(calculation_context);

View file

@ -52,16 +52,26 @@ Optional<double> CSSColorValue::resolve_hue(CSSStyleValue const& style_value, Ca
return normalized(style_value.as_angle().angle().to_degrees());
if (style_value.is_calculated()) {
if (style_value.as_calculated().resolves_to_number())
return normalized(style_value.as_calculated().resolve_number_deprecated(resolution_context).value());
if (style_value.as_calculated().resolves_to_number()) {
auto maybe_number = style_value.as_calculated().resolve_number(resolution_context);
if (style_value.as_calculated().resolves_to_angle())
return normalized(style_value.as_calculated().resolve_angle_deprecated(resolution_context).value().to_degrees());
if (!maybe_number.has_value())
return {};
return normalized(maybe_number.value());
}
if (style_value.as_calculated().resolves_to_angle()) {
auto maybe_angle = style_value.as_calculated().resolve_angle(resolution_context);
if (!maybe_angle.has_value())
return {};
return normalized(maybe_angle.value().to_degrees());
}
}
if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
return 0;
return {};
return 0;
}
Optional<double> CSSColorValue::resolve_with_reference_value(CSSStyleValue const& style_value, float one_hundred_percent_value, CalculationResolutionContext const& resolution_context)
@ -79,16 +89,26 @@ Optional<double> CSSColorValue::resolve_with_reference_value(CSSStyleValue const
if (style_value.is_calculated()) {
auto const& calculated = style_value.as_calculated();
if (calculated.resolves_to_number())
return calculated.resolve_number_deprecated(resolution_context).value();
if (calculated.resolves_to_percentage())
return normalize_percentage(calculated.resolve_percentage_deprecated(resolution_context).value());
if (calculated.resolves_to_number()) {
auto maybe_number = calculated.resolve_number(resolution_context);
if (!maybe_number.has_value())
return {};
return maybe_number.value();
}
if (calculated.resolves_to_percentage()) {
auto percentage = calculated.resolve_percentage(resolution_context);
if (!percentage.has_value())
return {};
return normalize_percentage(percentage.value());
}
}
if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
return 0;
return {};
return 0;
}
Optional<double> CSSColorValue::resolve_alpha(CSSStyleValue const& style_value, CalculationResolutionContext const& resolution_context)
@ -108,16 +128,29 @@ Optional<double> CSSColorValue::resolve_alpha(CSSStyleValue const& style_value,
if (style_value.is_calculated()) {
auto const& calculated = style_value.as_calculated();
if (calculated.resolves_to_number())
return normalized(calculated.resolve_number_deprecated(resolution_context).value());
if (calculated.resolves_to_percentage())
return normalized(calculated.resolve_percentage_deprecated(resolution_context).value().as_fraction());
if (calculated.resolves_to_number()) {
auto maybe_number = calculated.resolve_number(resolution_context);
if (!maybe_number.has_value())
return {};
return normalized(maybe_number.value());
}
if (calculated.resolves_to_percentage()) {
auto percentage = calculated.resolve_percentage(resolution_context);
if (!percentage.has_value())
return {};
return normalized(percentage.value().as_fraction());
}
}
if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
return 0;
return {};
return 1;
}
void CSSColorValue::serialize_color_component(StringBuilder& builder, SerializationMode mode, CSSStyleValue const& component, float one_hundred_percent_value, Optional<double> clamp_min, Optional<double> clamp_max) const
@ -130,7 +163,16 @@ void CSSColorValue::serialize_color_component(StringBuilder& builder, Serializat
builder.append(component.to_string(mode));
return;
}
auto resolved_value = resolve_with_reference_value(component, one_hundred_percent_value, {}).value_or(0);
auto maybe_resolved_value = resolve_with_reference_value(component, one_hundred_percent_value, {});
if (!maybe_resolved_value.has_value()) {
builder.append(component.to_string(mode));
return;
}
auto resolved_value = maybe_resolved_value.value();
if (clamp_min.has_value() && resolved_value < *clamp_min)
resolved_value = *clamp_min;
if (clamp_max.has_value() && resolved_value > *clamp_max)
@ -153,8 +195,15 @@ void CSSColorValue::serialize_alpha_component(StringBuilder& builder, Serializat
builder.append(component.to_string(mode));
return;
}
auto resolved_value = resolve_alpha(component, {}).value_or(0);
builder.appendff("{}", resolved_value);
auto maybe_resolved_value = resolve_alpha(component, {});
if (!maybe_resolved_value.has_value()) {
builder.append(component.to_string(mode));
return;
}
builder.appendff("{}", maybe_resolved_value.value());
}
void CSSColorValue::serialize_hue_component(StringBuilder& builder, SerializationMode mode, CSSStyleValue const& component) const
@ -167,7 +216,15 @@ void CSSColorValue::serialize_hue_component(StringBuilder& builder, Serializatio
builder.append(component.to_string(mode));
return;
}
builder.appendff("{:.4}", resolve_hue(component, {}).value_or(0));
auto maybe_resolved_value = resolve_hue(component, {});
if (!maybe_resolved_value.has_value()) {
builder.append(component.to_string(mode));
return;
}
builder.appendff("{:.4}", maybe_resolved_value.value());
}
}

View file

@ -11,14 +11,17 @@
namespace Web::CSS {
Color CSSHSL::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSHSL::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto const h_val = resolve_hue(m_properties.h, resolution_context).value_or(0);
auto const s_val = resolve_with_reference_value(m_properties.s, 100.0, resolution_context).value_or(0);
auto const l_val = resolve_with_reference_value(m_properties.l, 100.0, resolution_context).value_or(0);
auto const alpha_val = resolve_alpha(m_properties.alpha, resolution_context).value_or(1);
auto h_val = resolve_hue(m_properties.h, resolution_context);
auto s_val = resolve_with_reference_value(m_properties.s, 100.0, resolution_context);
auto l_val = resolve_with_reference_value(m_properties.l, 100.0, resolution_context);
auto alpha_val = resolve_alpha(m_properties.alpha, resolution_context);
return Color::from_hsla(h_val, s_val / 100.0f, l_val / 100.0f, alpha_val);
if (!h_val.has_value() || !s_val.has_value() || !l_val.has_value() || !alpha_val.has_value())
return {};
return Color::from_hsla(h_val.value(), s_val.value() / 100.0f, l_val.value() / 100.0f, alpha_val.value());
}
bool CSSHSL::equals(CSSStyleValue const& other) const
@ -35,8 +38,11 @@ bool CSSHSL::equals(CSSStyleValue const& other) const
// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
String CSSHSL::to_string(SerializationMode) const
{
if (auto color = to_color({}, {}); color.has_value())
return serialize_a_srgb_value(color.value());
// FIXME: Do this properly, taking unresolved calculated values into account.
return serialize_a_srgb_value(to_color({}, {}));
return ""_string;
}
}

View file

@ -29,7 +29,7 @@ public:
CSSStyleValue const& l() const { return *m_properties.l; }
CSSStyleValue const& alpha() const { return *m_properties.alpha; }
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;

View file

@ -11,24 +11,30 @@
namespace Web::CSS {
Color CSSHWB::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSHWB::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto const h_val = resolve_hue(m_properties.h, resolution_context).value_or(0);
auto const w_val = clamp(resolve_with_reference_value(m_properties.w, 100.0, resolution_context).value_or(0), 0, 100) / 100.0f;
auto const b_val = clamp(resolve_with_reference_value(m_properties.b, 100.0, resolution_context).value_or(0), 0, 100) / 100.0f;
auto const alpha_val = resolve_alpha(m_properties.alpha, resolution_context).value_or(1);
auto h_val = resolve_hue(m_properties.h, resolution_context);
auto raw_w_value = resolve_with_reference_value(m_properties.w, 100.0, resolution_context);
auto raw_b_value = resolve_with_reference_value(m_properties.b, 100.0, resolution_context);
auto alpha_val = resolve_alpha(m_properties.alpha, resolution_context);
if (!h_val.has_value() || !raw_w_value.has_value() || !raw_b_value.has_value() || !alpha_val.has_value())
return {};
auto w_val = clamp(raw_w_value.value(), 0, 100) / 100.0f;
auto b_val = clamp(raw_b_value.value(), 0, 100) / 100.0f;
if (w_val + b_val >= 1.0f) {
auto to_byte = [](float value) {
return round_to<u8>(clamp(value * 255.0f, 0.0f, 255.0f));
};
u8 gray = to_byte(w_val / (w_val + b_val));
return Color(gray, gray, gray, to_byte(alpha_val));
return Color(gray, gray, gray, to_byte(alpha_val.value()));
}
auto value = 1 - b_val;
auto saturation = 1 - (w_val / value);
return Color::from_hsv(h_val, saturation, value).with_opacity(alpha_val);
return Color::from_hsv(h_val.value(), saturation, value).with_opacity(alpha_val.value());
}
bool CSSHWB::equals(CSSStyleValue const& other) const
@ -45,8 +51,11 @@ bool CSSHWB::equals(CSSStyleValue const& other) const
// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
String CSSHWB::to_string(SerializationMode) const
{
if (auto color = to_color({}, {}); color.has_value())
return serialize_a_srgb_value(color.value());
// FIXME: Do this properly, taking unresolved calculated values into account.
return serialize_a_srgb_value(to_color({}, {}));
return ""_string;
}
}

View file

@ -29,7 +29,7 @@ public:
CSSStyleValue const& b() const { return *m_properties.b; }
CSSStyleValue const& alpha() const { return *m_properties.alpha; }
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;

View file

@ -135,7 +135,7 @@ bool CSSKeywordValue::has_color() const
return is_color(keyword());
}
Color CSSKeywordValue::to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const&) const
Optional<Color> CSSKeywordValue::to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const&) const
{
if (keyword() == Keyword::Currentcolor) {
if (!node.has_value() || !node->has_style())

View file

@ -51,7 +51,7 @@ public:
static bool is_color(Keyword);
virtual bool has_color() const override;
virtual Color to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;
bool properties_equal(CSSKeywordValue const& other) const { return m_keyword == other.m_keyword; }

View file

@ -27,14 +27,20 @@ bool CSSLCHLike::equals(CSSStyleValue const& other) const
return m_properties == other_oklch_like.m_properties;
}
Color CSSLCH::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSLCH::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 100, resolution_context).value_or(0), 0, 100);
auto const c_val = resolve_with_reference_value(m_properties.c, 150, resolution_context).value_or(0);
auto const h_val = AK::to_radians(resolve_hue(m_properties.h, resolution_context).value_or(0));
auto const alpha_val = resolve_alpha(m_properties.alpha, resolution_context).value_or(1);
auto raw_l_val = resolve_with_reference_value(m_properties.l, 100, resolution_context);
auto c_val = resolve_with_reference_value(m_properties.c, 150, resolution_context);
auto raw_h_val = resolve_hue(m_properties.h, resolution_context);
auto alpha_val = resolve_alpha(m_properties.alpha, resolution_context);
return Color::from_lab(l_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val);
if (!raw_l_val.has_value() || !c_val.has_value() || !raw_h_val.has_value() || !alpha_val.has_value())
return {};
auto l_val = clamp(raw_l_val.value(), 0, 100);
auto h_val = AK::to_radians(raw_h_val.value());
return Color::from_lab(l_val, c_val.value() * cos(h_val), c_val.value() * sin(h_val), alpha_val.value());
}
// https://www.w3.org/TR/css-color-4/#serializing-lab-lch
@ -57,14 +63,21 @@ String CSSLCH::to_string(SerializationMode mode) const
return MUST(builder.to_string());
}
Color CSSOKLCH::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSOKLCH::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 1.0, resolution_context).value_or(0), 0, 1);
auto const c_val = max(resolve_with_reference_value(m_properties.c, 0.4, resolution_context).value_or(0), 0);
auto const h_val = AK::to_radians(resolve_hue(m_properties.h, resolution_context).value_or(0));
auto const alpha_val = resolve_alpha(m_properties.alpha, resolution_context).value_or(1);
auto raw_l_val = resolve_with_reference_value(m_properties.l, 1.0, resolution_context);
auto raw_c_val = resolve_with_reference_value(m_properties.c, 0.4, resolution_context);
auto raw_h_val = resolve_hue(m_properties.h, resolution_context);
auto alpha_val = resolve_alpha(m_properties.alpha, resolution_context);
return Color::from_oklab(l_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val);
if (!raw_l_val.has_value() || !raw_c_val.has_value() || !raw_h_val.has_value() || !alpha_val.has_value())
return {};
auto l_val = clamp(raw_l_val.value(), 0, 1);
auto c_val = max(raw_c_val.value(), 0);
auto h_val = AK::to_radians(raw_h_val.value());
return Color::from_oklab(l_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val.value());
}
// https://www.w3.org/TR/css-color-4/#serializing-oklab-oklch

View file

@ -56,7 +56,7 @@ public:
}
virtual ~CSSLCH() override = default;
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;
};
@ -70,7 +70,7 @@ public:
}
virtual ~CSSOKLCH() override = default;
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;
};

View file

@ -26,14 +26,17 @@ bool CSSLabLike::equals(CSSStyleValue const& other) const
return m_properties == other_lab_like.m_properties;
}
Color CSSOKLab::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSOKLab::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 1.0, resolution_context).value_or(0), 0, 1);
auto const a_val = resolve_with_reference_value(m_properties.a, 0.4, resolution_context).value_or(0);
auto const b_val = resolve_with_reference_value(m_properties.b, 0.4, resolution_context).value_or(0);
auto const alpha_val = resolve_alpha(m_properties.alpha, resolution_context).value_or(1);
auto const l_val = resolve_with_reference_value(m_properties.l, 1.0, resolution_context);
auto const a_val = resolve_with_reference_value(m_properties.a, 0.4, resolution_context);
auto const b_val = resolve_with_reference_value(m_properties.b, 0.4, resolution_context);
auto const alpha_val = resolve_alpha(m_properties.alpha, resolution_context);
return Color::from_oklab(l_val, a_val, b_val, alpha_val);
if (!l_val.has_value() || !a_val.has_value() || !b_val.has_value() || !alpha_val.has_value())
return {};
return Color::from_oklab(clamp(l_val.value(), 0, 1), a_val.value(), b_val.value(), alpha_val.value());
}
// https://www.w3.org/TR/css-color-4/#serializing-oklab-oklch
@ -56,14 +59,17 @@ String CSSOKLab::to_string(SerializationMode mode) const
return MUST(builder.to_string());
}
Color CSSLab::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSLab::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 100, resolution_context).value_or(0), 0, 100);
auto const a_val = resolve_with_reference_value(m_properties.a, 125, resolution_context).value_or(0);
auto const b_val = resolve_with_reference_value(m_properties.b, 125, resolution_context).value_or(0);
auto const alpha_val = resolve_alpha(m_properties.alpha, resolution_context).value_or(1);
auto l_val = resolve_with_reference_value(m_properties.l, 100, resolution_context);
auto a_val = resolve_with_reference_value(m_properties.a, 125, resolution_context);
auto b_val = resolve_with_reference_value(m_properties.b, 125, resolution_context);
auto alpha_val = resolve_alpha(m_properties.alpha, resolution_context);
return Color::from_lab(l_val, a_val, b_val, alpha_val);
if (!l_val.has_value() || !a_val.has_value() || !b_val.has_value() || !alpha_val.has_value())
return {};
return Color::from_lab(clamp(l_val.value(), 0, 100), a_val.value(), b_val.value(), alpha_val.value());
}
// https://www.w3.org/TR/css-color-4/#serializing-lab-lch

View file

@ -51,7 +51,7 @@ protected:
// https://drafts.css-houdini.org/css-typed-om-1/#cssoklab
class CSSOKLab final : public CSSLabLike {
public:
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;
CSSOKLab(Badge<CSSLabLike>, ValueComparingNonnullRefPtr<CSSStyleValue const> l, ValueComparingNonnullRefPtr<CSSStyleValue const> a, ValueComparingNonnullRefPtr<CSSStyleValue const> b, ValueComparingNonnullRefPtr<CSSStyleValue const> alpha)
@ -63,7 +63,7 @@ public:
// https://drafts.css-houdini.org/css-typed-om-1/#csslab
class CSSLab final : public CSSLabLike {
public:
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;
CSSLab(Badge<CSSLabLike>, ValueComparingNonnullRefPtr<CSSStyleValue const> l, ValueComparingNonnullRefPtr<CSSStyleValue const> a, ValueComparingNonnullRefPtr<CSSStyleValue const> b, ValueComparingNonnullRefPtr<CSSStyleValue const> alpha)

View file

@ -9,7 +9,7 @@
namespace Web::CSS {
Color CSSLightDark::to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSLightDark::to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const& resolution_context) const
{
if (node.has_value() && node.value().computed_values().color_scheme() == PreferredColorScheme::Dark)
return m_properties.dark->to_color(node, resolution_context);

View file

@ -21,7 +21,7 @@ public:
}
virtual bool equals(CSSStyleValue const&) const override;
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;
private:

View file

@ -14,7 +14,7 @@
namespace Web::CSS {
Color CSSRGB::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> CSSRGB::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto resolve_rgb_to_u8 = [&resolution_context](CSSStyleValue const& style_value) -> Optional<u8> {
// <number> | <percentage> | none
@ -32,16 +32,26 @@ Color CSSRGB::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolu
if (style_value.is_calculated()) {
auto const& calculated = style_value.as_calculated();
if (calculated.resolves_to_number())
return normalized(calculated.resolve_number_deprecated(resolution_context).value());
if (calculated.resolves_to_percentage())
return normalized(calculated.resolve_percentage_deprecated(resolution_context).value().value() * 255 / 100);
if (calculated.resolves_to_number()) {
auto maybe_number = calculated.resolve_number(resolution_context);
if (!maybe_number.has_value())
return {};
return normalized(maybe_number.value());
}
if (calculated.resolves_to_percentage()) {
auto maybe_percentage = calculated.resolve_percentage(resolution_context);
if (!maybe_percentage.has_value())
return {};
return normalized(maybe_percentage.value().value() * 255 / 100);
}
}
if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
return 0;
return {};
return 0;
};
auto resolve_alpha_to_u8 = [&resolution_context](CSSStyleValue const& style_value) -> Optional<u8> {
@ -51,12 +61,15 @@ Color CSSRGB::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolu
return {};
};
u8 const r_val = resolve_rgb_to_u8(m_properties.r).value_or(0);
u8 const g_val = resolve_rgb_to_u8(m_properties.g).value_or(0);
u8 const b_val = resolve_rgb_to_u8(m_properties.b).value_or(0);
u8 const alpha_val = resolve_alpha_to_u8(m_properties.alpha).value_or(255);
auto r_val = resolve_rgb_to_u8(m_properties.r);
auto g_val = resolve_rgb_to_u8(m_properties.g);
auto b_val = resolve_rgb_to_u8(m_properties.b);
auto alpha_val = resolve_alpha_to_u8(m_properties.alpha);
return Color(r_val, g_val, b_val, alpha_val);
if (!r_val.has_value() || !g_val.has_value() || !b_val.has_value() || !alpha_val.has_value())
return {};
return Color(r_val.value(), g_val.value(), b_val.value(), alpha_val.value());
}
bool CSSRGB::equals(CSSStyleValue const& other) const
@ -73,10 +86,14 @@ bool CSSRGB::equals(CSSStyleValue const& other) const
// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
String CSSRGB::to_string(SerializationMode mode) const
{
// FIXME: Do this properly, taking unresolved calculated values into account.
if (mode != SerializationMode::ResolvedValue && m_properties.name.has_value())
return m_properties.name.value().to_string().to_ascii_lowercase();
return serialize_a_srgb_value(to_color({}, {}));
if (auto color = to_color({}, {}); color.has_value())
return serialize_a_srgb_value(color.value());
// FIXME: Do this properly, taking unresolved calculated values into account.
return ""_string;
}
}

View file

@ -29,7 +29,7 @@ public:
CSSStyleValue const& b() const { return *m_properties.b; }
CSSStyleValue const& alpha() const { return *m_properties.alpha; }
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;

View file

@ -82,13 +82,22 @@ bool ColorFunctionStyleValue::equals(CSSStyleValue const& other) const
return m_properties == other_lab_like.m_properties;
}
ColorFunctionStyleValue::Resolved ColorFunctionStyleValue::resolve_properties(CalculationResolutionContext const& resolution_context) const
Optional<ColorFunctionStyleValue::Resolved> ColorFunctionStyleValue::resolve_properties(CalculationResolutionContext const& resolution_context) const
{
float const c1 = resolve_with_reference_value(m_properties.channels[0], 1, resolution_context).value_or(0);
float const c2 = resolve_with_reference_value(m_properties.channels[1], 1, resolution_context).value_or(0);
float const c3 = resolve_with_reference_value(m_properties.channels[2], 1, resolution_context).value_or(0);
float const alpha_val = resolve_alpha(m_properties.alpha, resolution_context).value_or(1);
return { .channels = { c1, c2, c3 }, .alpha = alpha_val };
auto c1 = resolve_with_reference_value(m_properties.channels[0], 1, resolution_context);
auto c2 = resolve_with_reference_value(m_properties.channels[1], 1, resolution_context);
auto c3 = resolve_with_reference_value(m_properties.channels[2], 1, resolution_context);
auto alpha = resolve_alpha(m_properties.alpha, resolution_context);
if (!c1.has_value() || !c2.has_value() || !c3.has_value() || !alpha.has_value())
return {};
float const c1_value = c1.value();
float const c2_value = c2.value();
float const c3_value = c3.value();
float const alpha_value = alpha.value();
return ColorFunctionStyleValue::Resolved { .channels = { c1_value, c2_value, c3_value }, .alpha = alpha_value };
}
// https://www.w3.org/TR/css-color-4/#serializing-color-function-values
@ -102,14 +111,14 @@ String ColorFunctionStyleValue::to_string(SerializationMode mode) const
CalculationResolutionContext context {};
auto const& calculated = value->as_calculated();
if (calculated.resolves_to_percentage()) {
if (auto resolved_percentage = calculated.resolve_percentage_deprecated(context); resolved_percentage.has_value()) {
if (auto resolved_percentage = calculated.resolve_percentage(context); resolved_percentage.has_value()) {
auto resolved_number = resolved_percentage->value() / 100;
if (!isfinite(resolved_number))
resolved_number = 0;
return NumberStyleValue::create(resolved_number);
}
} else if (calculated.resolves_to_number()) {
if (auto resolved_number = calculated.resolve_number_deprecated(context); resolved_number.has_value())
if (auto resolved_number = calculated.resolve_number(context); resolved_number.has_value())
return NumberStyleValue::create(*resolved_number);
}
}
@ -144,9 +153,14 @@ String ColorFunctionStyleValue::to_string(SerializationMode mode) const
convert_percentage(m_properties.channels[2])->to_string(mode)));
}
Color ColorFunctionStyleValue::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
Optional<Color> ColorFunctionStyleValue::to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const
{
auto [channels, alpha_val] = resolve_properties(resolution_context);
auto properties = resolve_properties(resolution_context);
if (!properties.has_value())
return {};
auto [channels, alpha_val] = properties.value();
auto c1 = channels[0];
auto c2 = channels[1];
auto c3 = channels[2];

View file

@ -18,7 +18,7 @@ public:
static ValueComparingNonnullRefPtr<ColorFunctionStyleValue const> create(StringView color_space, ValueComparingNonnullRefPtr<CSSStyleValue const> c1, ValueComparingNonnullRefPtr<CSSStyleValue const> c2, ValueComparingNonnullRefPtr<CSSStyleValue const> c3, ValueComparingRefPtr<CSSStyleValue const> alpha = {});
virtual bool equals(CSSStyleValue const&) const override;
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const& resolution_context) const override;
virtual String to_string(SerializationMode) const override;
virtual bool is_color_function() const override { return true; }
@ -43,7 +43,7 @@ private:
float alpha {};
};
Resolved resolve_properties(CalculationResolutionContext const& resolution_context) const;
Optional<Resolved> resolve_properties(CalculationResolutionContext const& resolution_context) const;
Properties m_properties;
};

View file

@ -175,15 +175,19 @@ ColorMixStyleValue::PercentageNormalizationResult ColorMixStyleValue::normalize_
}
// https://drafts.csswg.org/css-color-5/#color-mix-result
Color ColorMixStyleValue::to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const& resolution_context) const
Optional<Color> ColorMixStyleValue::to_color(Optional<Layout::NodeWithStyle const&> node, CalculationResolutionContext const& resolution_context) const
{
// FIXME: Take the color space and hue interpolation method into account.
// The current implementation only uses oklab interpolation.
auto normalized_percentages = normalize_percentages();
auto from_color = m_properties.first_component.color;
auto to_color = m_properties.second_component.color;
auto from_color = m_properties.first_component.color->to_color(node, resolution_context);
auto to_color = m_properties.second_component.color->to_color(node, resolution_context);
auto delta = normalized_percentages.p2.value() / 100;
return interpolate_color(from_color->to_color(node, resolution_context), to_color->to_color(node, resolution_context), delta, ColorSyntax::Modern);
if (!from_color.has_value() || !to_color.has_value())
return {};
return interpolate_color(from_color.value(), to_color.value(), delta, ColorSyntax::Modern);
}
}

View file

@ -30,7 +30,7 @@ public:
static ValueComparingNonnullRefPtr<ColorMixStyleValue const> create(ColorInterpolationMethod, ColorMixComponent first_component, ColorMixComponent second_component);
virtual bool equals(CSSStyleValue const&) const override;
virtual Color to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual Optional<Color> to_color(Optional<Layout::NodeWithStyle const&>, CalculationResolutionContext const&) const override;
virtual String to_string(SerializationMode) const override;
private:

View file

@ -1677,7 +1677,7 @@ void Document::obtain_theme_color()
resolution_context.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*html_element()->layout_node());
}
theme_color = css_value->to_color(root_node, resolution_context);
theme_color = css_value->to_color(root_node, resolution_context).value();
return TraversalDecision::Break;
}
}

View file

@ -1175,7 +1175,7 @@ Optional<String> effective_command_value(GC::Ptr<DOM::Node> node, FlyString cons
return NumericLimits<u8>::max();
VERIFY(is<Layout::NodeWithStyle>(node->layout_node()));
auto& layout_node = *static_cast<Layout::NodeWithStyle*>(node->layout_node());
return background_color.value()->to_color(layout_node, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(layout_node) }).alpha();
return background_color.value()->to_color(layout_node, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(layout_node) }).value().alpha();
};
while (resolved_background_alpha() == 0 && node->parent() && is<DOM::Element>(*node->parent()))
node = node->parent();

View file

@ -52,7 +52,7 @@ public:
resolution_context.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*context->layout_node());
}
auto parsedValue = style_value->to_color(layout_node, resolution_context);
auto parsedValue = style_value->to_color(layout_node, resolution_context).value_or(Color::Black);
// 4. Set this's fill style to parsedValue.
my_drawing_state().fill_style = parsedValue;
@ -105,7 +105,7 @@ public:
resolution_context.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*context->layout_node());
}
auto parsedValue = style_value->to_color(layout_node, resolution_context);
auto parsedValue = style_value->to_color(layout_node, resolution_context).value_or(Color::Black);
// 4. Set this's stroke style to parsedValue.
my_drawing_state().stroke_style = parsedValue;

View file

@ -950,7 +950,7 @@ void CanvasRenderingContext2D::set_shadow_color(String color)
resolution_context.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*node);
}
auto parsedValue = style_value->to_color(layout_node, resolution_context);
auto parsedValue = style_value->to_color(layout_node, resolution_context).value_or(Color::Black);
// 4. Set this's shadow color to parsedValue.
drawing_state().shadow_color = parsedValue;

View file

@ -288,7 +288,7 @@ void OffscreenCanvasRenderingContext2D::set_shadow_color(String color)
// 2. Let parsedValue be the result of parsing the given value with context if non-null.
auto style_value = parse_css_value(CSS::Parser::ParsingParams(), color, CSS::PropertyID::Color);
if (style_value && style_value->has_color()) {
auto parsedValue = style_value->to_color({}, {});
auto parsedValue = style_value->to_color({}, {}).value_or(Color::Black);
// 4. Set this's shadow color to parsedValue.
drawing_state().shadow_color = parsedValue;

View file

@ -835,7 +835,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
do_border_style(computed_values.border_bottom(), CSS::PropertyID::BorderBottomWidth, CSS::PropertyID::BorderBottomColor, CSS::PropertyID::BorderBottomStyle);
if (auto const& outline_color = computed_style.property(CSS::PropertyID::OutlineColor); outline_color.has_color())
computed_values.set_outline_color(outline_color.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }));
computed_values.set_outline_color(outline_color.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }).value());
if (auto const& outline_offset = computed_style.property(CSS::PropertyID::OutlineOffset); outline_offset.is_length())
computed_values.set_outline_offset(outline_offset.as_length().length());
computed_values.set_outline_style(computed_style.outline_style());
@ -875,16 +875,16 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
auto const& fill = computed_style.property(CSS::PropertyID::Fill);
if (fill.has_color())
computed_values.set_fill(fill.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }));
computed_values.set_fill(fill.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }).value());
else if (fill.is_url())
computed_values.set_fill(fill.as_url().url());
auto const& stroke = computed_style.property(CSS::PropertyID::Stroke);
if (stroke.has_color())
computed_values.set_stroke(stroke.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }));
computed_values.set_stroke(stroke.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }).value());
else if (stroke.is_url())
computed_values.set_stroke(stroke.as_url().url());
if (auto const& stop_color = computed_style.property(CSS::PropertyID::StopColor); stop_color.has_color())
computed_values.set_stop_color(stop_color.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }));
computed_values.set_stop_color(stop_color.to_color(*this, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*this) }).value());
auto const& stroke_width = computed_style.property(CSS::PropertyID::StrokeWidth);
// FIXME: Converting to pixels isn't really correct - values should be in "user units"
// https://svgwg.org/svg2-draft/coords.html#TermUserUnits

View file

@ -587,7 +587,7 @@ Optional<BordersData> borders_data_for_outline(Layout::Node const& layout_node,
// `auto` lets us do whatever we want for the outline. 2px of the accent colour seems reasonable.
line_style = CSS::LineStyle::Solid;
// NOTE: CalculationResolutionContext is not required here as Accentcolor keyword value is guaranteed to not rely on it to resolve.
outline_color = CSS::CSSKeywordValue::create(CSS::Keyword::Accentcolor)->to_color(*static_cast<Layout::NodeWithStyle const*>(&layout_node), {});
outline_color = CSS::CSSKeywordValue::create(CSS::Keyword::Accentcolor)->to_color(*static_cast<Layout::NodeWithStyle const*>(&layout_node), {}).value();
outline_width = 2;
} else {
line_style = CSS::keyword_to_line_style(CSS::to_keyword(outline_style)).value_or(CSS::LineStyle::None);

View file

@ -30,7 +30,7 @@ static ColorStopData resolve_color_stop_positions(Layout::NodeWithStyle const& n
resolved_color_stops.ensure_capacity(expanded_size);
for (auto& stop : color_stop_list) {
auto resolved_stop = Gfx::ColorStop { .color = stop.color_stop.color->to_color(node, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(node) }) };
auto resolved_stop = Gfx::ColorStop { .color = stop.color_stop.color->to_color(node, { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(node) }).value() };
for (int i = 0; i < color_stop_length(stop); i++)
resolved_color_stops.append(resolved_stop);
}