From 34b8947ca0ab7918114270626f082927e68690dc Mon Sep 17 00:00:00 2001 From: Callum Law Date: Sun, 14 Sep 2025 16:04:26 +1200 Subject: [PATCH] LibWeb: Support `text-underline-position: under` --- Libraries/LibWeb/CSS/ComputedProperties.cpp | 11 ++++++ Libraries/LibWeb/CSS/ComputedProperties.h | 1 + Libraries/LibWeb/CSS/ComputedValues.h | 9 +++++ Libraries/LibWeb/Layout/Node.cpp | 1 + Libraries/LibWeb/Painting/PaintableBox.cpp | 36 +++++++++++++++++-- .../expected/css/text-underline-position.txt | 12 +++++++ .../input/css/text-underline-position.html | 18 ++++++++++ 7 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/css/text-underline-position.txt create mode 100644 Tests/LibWeb/Text/input/css/text-underline-position.html diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index aec4225ad4d..a4aa9ec52a6 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -884,6 +885,16 @@ CSSPixels ComputedProperties::text_underline_offset() const VERIFY_NOT_REACHED(); } +TextUnderlinePosition ComputedProperties::text_underline_position() const +{ + auto const& computed_text_underline_position = property(PropertyID::TextUnderlinePosition).as_text_underline_position(); + + return { + .horizontal = computed_text_underline_position.horizontal(), + .vertical = computed_text_underline_position.vertical() + }; +} + 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 c265363ba2b..4179a93ee86 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.h +++ b/Libraries/LibWeb/CSS/ComputedProperties.h @@ -92,6 +92,7 @@ public: TextOverflow text_overflow() const; TextRendering text_rendering() const; CSSPixels text_underline_offset() const; + TextUnderlinePosition text_underline_position() 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 a7b1b1a321b..15beec2d3b5 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -87,6 +87,11 @@ struct ScrollbarColorData { Color track_color { Color::Transparent }; }; +struct TextUnderlinePosition { + TextUnderlinePositionHorizontal horizontal { TextUnderlinePositionHorizontal::Auto }; + TextUnderlinePositionVertical vertical { TextUnderlinePositionVertical::Auto }; +}; + struct WillChange { enum class Type : u8 { Contents, @@ -153,6 +158,7 @@ public: 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 TextUnderlinePosition text_underline_position() { return { .horizontal = TextUnderlinePositionHorizontal::Auto, .vertical = TextUnderlinePositionVertical::Auto }; } 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; } @@ -483,6 +489,7 @@ public: 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; } + TextUnderlinePosition text_underline_position() const { return m_inherited.text_underline_position; } 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; } @@ -701,6 +708,7 @@ protected: CSS::TextWrapMode text_wrap_mode { InitialValues::text_wrap_mode() }; CSS::TextRendering text_rendering { InitialValues::text_rendering() }; CSSPixels text_underline_offset { InitialValues::text_underline_offset() }; + TextUnderlinePosition text_underline_position { InitialValues::text_underline_position() }; CSS::WhiteSpaceCollapse white_space_collapse { InitialValues::white_space_collapse() }; CSS::WordBreak word_break { InitialValues::word_break() }; CSSPixels word_spacing { InitialValues::word_spacing() }; @@ -919,6 +927,7 @@ public: 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_text_underline_position(TextUnderlinePosition value) { m_inherited.text_underline_position = 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 d4ae0a15bbb..90a7a20d7bd 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -609,6 +609,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style) 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()); + computed_values.set_text_underline_position(computed_style.text_underline_position()); 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 a9f62abb974..7088ece4829 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -815,6 +815,7 @@ void paint_text_decoration(DisplayListRecordingContext& context, TextPaintable c 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(); + auto text_underline_position = paintable.computed_values().text_underline_position(); for (auto line : text_decoration_lines) { DevicePixelPoint line_start_point {}; DevicePixelPoint line_end_point {}; @@ -850,10 +851,39 @@ void paint_text_decoration(DisplayListRecordingContext& context, TextPaintable c 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 + text_underline_offset)); - line_end_point = context.rounded_device_point(fragment_box.top_right().translated(0, baseline + text_underline_offset)); + case CSS::TextDecorationLine::Underline: { + // https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property + auto underline_position_without_offset = [&]() { + // FIXME: Support text-decoration: underline on vertical text + switch (text_underline_position.horizontal) { + case Web::CSS::TextUnderlinePositionHorizontal::Auto: + // The user agent may use any algorithm to determine the underline’s position; however it must be + // placed at or under the alphabetic baseline. + + // Spec Note: It is suggested that the default underline position be close to the alphabetic + // baseline, + // FIXME: unless that would either cross subscripted (or otherwise lowered) text or draw over + // glyphs from Asian scripts such as Han or Tibetan for which an alphabetic underline is + // too high: in such cases, shifting the underline lower or aligning to the em box edge + // as described for under may be more appropriate. + return fragment.baseline(); + case Web::CSS::TextUnderlinePositionHorizontal::FromFont: + // FIXME: If the first available font has metrics indicating a preferred underline offset, use that + // offset, otherwise behaves as auto. + return fragment.baseline(); + case Web::CSS::TextUnderlinePositionHorizontal::Under: + // The underline is positioned under the element’s text content. In this case the underline usually + // does not cross the descenders. (This is sometimes called “accounting” underline.) + return fragment.baseline() + CSSPixels { font.pixel_metrics().descent }; + } + + VERIFY_NOT_REACHED(); + }(); + + line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, underline_position_without_offset + text_underline_offset)); + line_end_point = context.rounded_device_point(fragment_box.top_right().translated(0, underline_position_without_offset + text_underline_offset)); break; + } case CSS::TextDecorationLine::Overline: line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, baseline - glyph_height)); line_end_point = context.rounded_device_point(fragment_box.top_right().translated(0, baseline - glyph_height)); diff --git a/Tests/LibWeb/Text/expected/css/text-underline-position.txt b/Tests/LibWeb/Text/expected/css/text-underline-position.txt new file mode 100644 index 00000000000..9b86f1c14dd --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/text-underline-position.txt @@ -0,0 +1,12 @@ +SaveLayer + PushStackingContext opacity=1 isolate=false has_clip_path=false transform=[1 0 0 1 0 0] + PushStackingContext opacity=1 isolate=false has_clip_path=false transform=[1 0 0 1 0 0] + DrawGlyphRun rect=[35,8 8x18] translation=[35.15625,21.796875] color=rgb(0, 0, 0) scale=1 + DrawGlyphRun rect=[8,8 28x18] translation=[8,21.796875] color=rgb(0, 0, 0) scale=1 + DrawLine from=[8,24] to=[35,24] color=rgb(0, 0, 0) thickness=2 + DrawGlyphRun rect=[43,8 28x18] translation=[43.15625,21.796875] color=rgb(0, 0, 0) scale=1 + DrawLine from=[43,27] to=[71,27] color=rgb(0, 0, 0) thickness=2 + PopStackingContext + PopStackingContext +Restore + diff --git a/Tests/LibWeb/Text/input/css/text-underline-position.html b/Tests/LibWeb/Text/input/css/text-underline-position.html new file mode 100644 index 00000000000..7cf91ba1543 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/text-underline-position.html @@ -0,0 +1,18 @@ + + + +
+ foo + bar +
+ + +