From 815e77c04db88452c0184cdd8f871233d6811cc0 Mon Sep 17 00:00:00 2001 From: Callum Law Date: Fri, 12 Sep 2025 12:21:16 +1200 Subject: [PATCH] LibWeb: Respect `text-underline-offset` when rendering underlines --- Libraries/LibWeb/CSS/ComputedProperties.cpp | 25 ++++++++++ Libraries/LibWeb/CSS/ComputedProperties.h | 1 + Libraries/LibWeb/CSS/ComputedValues.h | 4 ++ Libraries/LibWeb/Layout/Node.cpp | 1 + Libraries/LibWeb/Painting/PaintableBox.cpp | 15 +++++- .../text-underline-offset-001-notref.html | 34 ++++++++++++++ .../text-underline-offset-002-ref.html | 35 ++++++++++++++ ...text-underline-offset-negative-notref.html | 10 ++++ .../text-underline-offset-001.html | 42 +++++++++++++++++ .../text-underline-offset-002.html | 46 +++++++++++++++++++ .../text-underline-offset-negative.html | 15 ++++++ 11 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-001-notref.html create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-002-ref.html create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-negative-notref.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-001.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-002.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-negative.html diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index 11232e0bd50..aec4225ad4d 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -859,6 +859,31 @@ TextRendering ComputedProperties::text_rendering() const return keyword_to_text_rendering(value.to_keyword()).release_value(); } +CSSPixels ComputedProperties::text_underline_offset() const +{ + auto const& computed_text_underline_offset = property(PropertyID::TextUnderlineOffset); + + // auto + if (computed_text_underline_offset.to_keyword() == Keyword::Auto) + return InitialValues::text_underline_offset(); + + // + if (computed_text_underline_offset.is_length()) + return computed_text_underline_offset.as_length().length().absolute_length_to_px(); + + // + if (computed_text_underline_offset.is_percentage()) + return font_size().scaled(computed_text_underline_offset.as_percentage().percentage().as_fraction()); + + // NOTE: We also support calc()'d + if (computed_text_underline_offset.is_calculated()) + // NOTE: We don't need to pass a length resolution context here as lengths have already been absolutized in + // StyleComputer::compute_text_underline_offset + return computed_text_underline_offset.as_calculated().resolve_length({ .percentage_basis = Length::make_px(font_size()), .length_resolution_context = {} })->absolute_length_to_px(); + + VERIFY_NOT_REACHED(); +} + PointerEvents ComputedProperties::pointer_events() const { auto const& value = property(PropertyID::PointerEvents); diff --git a/Libraries/LibWeb/CSS/ComputedProperties.h b/Libraries/LibWeb/CSS/ComputedProperties.h index 38515cfc5f2..c265363ba2b 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.h +++ b/Libraries/LibWeb/CSS/ComputedProperties.h @@ -91,6 +91,7 @@ public: TextJustify text_justify() const; TextOverflow text_overflow() const; TextRendering text_rendering() const; + CSSPixels text_underline_offset() const; Length border_spacing_horizontal(Layout::Node const&) const; Length border_spacing_vertical(Layout::Node const&) const; CaptionSide caption_side() const; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index e466a1c1305..a7b1b1a321b 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -152,6 +152,7 @@ public: static CSS::LengthPercentage text_indent() { return CSS::Length::make_px(0); } static CSS::TextWrapMode text_wrap_mode() { return CSS::TextWrapMode::Wrap; } static CSS::TextRendering text_rendering() { return CSS::TextRendering::Auto; } + static CSSPixels text_underline_offset() { return 2; } static CSS::Display display() { return CSS::Display { CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow }; } static Color color() { return Color::Black; } static Color stop_color() { return Color::Black; } @@ -481,6 +482,7 @@ public: CSS::LengthPercentage const& text_indent() const { return m_inherited.text_indent; } CSS::TextWrapMode text_wrap_mode() const { return m_inherited.text_wrap_mode; } CSS::TextRendering text_rendering() const { return m_inherited.text_rendering; } + CSSPixels text_underline_offset() const { return m_inherited.text_underline_offset; } Vector const& text_decoration_line() const { return m_noninherited.text_decoration_line; } TextDecorationThickness const& text_decoration_thickness() const { return m_noninherited.text_decoration_thickness; } CSS::TextDecorationStyle text_decoration_style() const { return m_noninherited.text_decoration_style; } @@ -698,6 +700,7 @@ protected: CSS::LengthPercentage text_indent { InitialValues::text_indent() }; CSS::TextWrapMode text_wrap_mode { InitialValues::text_wrap_mode() }; CSS::TextRendering text_rendering { InitialValues::text_rendering() }; + CSSPixels text_underline_offset { InitialValues::text_underline_offset() }; CSS::WhiteSpaceCollapse white_space_collapse { InitialValues::white_space_collapse() }; CSS::WordBreak word_break { InitialValues::word_break() }; CSSPixels word_spacing { InitialValues::word_spacing() }; @@ -915,6 +918,7 @@ public: void set_text_wrap_mode(CSS::TextWrapMode value) { m_inherited.text_wrap_mode = value; } void set_text_overflow(CSS::TextOverflow value) { m_noninherited.text_overflow = value; } void set_text_rendering(CSS::TextRendering value) { m_inherited.text_rendering = value; } + void set_text_underline_offset(CSSPixels value) { m_inherited.text_underline_offset = value; } void set_webkit_text_fill_color(Color value) { m_inherited.webkit_text_fill_color = value; } void set_position(CSS::Positioning position) { m_noninherited.position = position; } void set_white_space_collapse(CSS::WhiteSpaceCollapse value) { m_inherited.white_space_collapse = value; } diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index b8fdc9576e1..d4ae0a15bbb 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -608,6 +608,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style) computed_values.set_text_justify(computed_style.text_justify()); computed_values.set_text_overflow(computed_style.text_overflow()); computed_values.set_text_rendering(computed_style.text_rendering()); + computed_values.set_text_underline_offset(computed_style.text_underline_offset()); if (auto text_indent = computed_style.length_percentage(CSS::PropertyID::TextIndent, *this, CSS::ComputedProperties::ClampNegativeLengths::No); text_indent.has_value()) computed_values.set_text_indent(text_indent.release_value()); diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index f198d6886c4..b0ef89cc5b2 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -814,6 +814,7 @@ void paint_text_decoration(DisplayListRecordingContext& context, TextPaintable c auto line_style = paintable.computed_values().text_decoration_style(); auto device_line_thickness = context.rounded_device_pixels(fragment.text_decoration_thickness()); auto text_decoration_lines = paintable.computed_values().text_decoration_line(); + auto text_underline_offset = paintable.computed_values().text_underline_offset(); for (auto line : text_decoration_lines) { DevicePixelPoint line_start_point {}; DevicePixelPoint line_end_point {}; @@ -826,6 +827,11 @@ void paint_text_decoration(DisplayListRecordingContext& context, TextPaintable c device_line_thickness = context.rounded_device_pixels(1); line_style = CSS::TextDecorationStyle::Wavy; line = CSS::TextDecorationLine::Underline; + + // https://drafts.csswg.org/css-text-decor-4/#underline-offset + // When the value of the text-decoration-line property is either spelling-error or grammar-error, the UA + // must ignore the value of text-underline-position. + text_underline_offset = CSS::InitialValues::text_underline_offset(); } else if (line == CSS::TextDecorationLine::GrammarError) { // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-line-grammar-error // This value indicates the type of text decoration used by the user agent to highlight grammar mistakes. @@ -834,14 +840,19 @@ void paint_text_decoration(DisplayListRecordingContext& context, TextPaintable c device_line_thickness = context.rounded_device_pixels(1); line_style = CSS::TextDecorationStyle::Wavy; line = CSS::TextDecorationLine::Underline; + + // https://drafts.csswg.org/css-text-decor-4/#underline-offset + // When the value of the text-decoration-line property is either spelling-error or grammar-error, the UA + // must ignore the value of text-underline-position. + text_underline_offset = CSS::InitialValues::text_underline_offset(); } switch (line) { case CSS::TextDecorationLine::None: return; case CSS::TextDecorationLine::Underline: - line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, baseline + 2)); - line_end_point = context.rounded_device_point(fragment_box.top_right().translated(0, baseline + 2)); + line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, baseline + text_underline_offset)); + line_end_point = context.rounded_device_point(fragment_box.top_right().translated(0, baseline + text_underline_offset)); break; case CSS::TextDecorationLine::Overline: line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, baseline - glyph_height)); diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-001-notref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-001-notref.html new file mode 100644 index 00000000000..864c7e60855 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-001-notref.html @@ -0,0 +1,34 @@ + + + + + Non-reference case for text-underline-offset + + + + +

Test passes if the box on the right has a lower underline than the box on the left

+
+
+

leftXXXX

+
+
+

XXXXright

+
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-002-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-002-ref.html new file mode 100644 index 00000000000..2061ea8bdff --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-002-ref.html @@ -0,0 +1,35 @@ + + + + + Reference case for text-underline-offset + + + + +

Test passes if the lines are at the same level

+
+
+

leftXXXX

+
+
+

XXXXright

+
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-negative-notref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-negative-notref.html new file mode 100644 index 00000000000..4b0b285a797 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-text-decor/reference/text-underline-offset-negative-notref.html @@ -0,0 +1,10 @@ + + + + +Test case for negative values of text-underline-offset + + +
Hello
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-001.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-001.html new file mode 100644 index 00000000000..d24b0235a36 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-001.html @@ -0,0 +1,42 @@ + + + + + text-underline-offset test case + + + + + + + + + +

Test passes if the box on the right has a lower underline than the box on the left

+
+
+

leftXXXX

+
+
+

XXXXright

+
+
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-002.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-002.html new file mode 100644 index 00000000000..619490c12fe --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-002.html @@ -0,0 +1,46 @@ + + + + + Test case for text-underline-offset + + + + + + + + +

Test passes if the lines are at the same level

+
+
+

leftXXXX

+
+
+

XXXXright

+
+
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-negative.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-negative.html new file mode 100644 index 00000000000..74d8b00602a --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-text-decor/text-underline-offset-negative.html @@ -0,0 +1,15 @@ + + + + +Test case for negative values of text-underline-offset + + + + + + + +
Hello
+ +