LibWeb: Support text-underline-position: under

This commit is contained in:
Callum Law 2025-09-14 16:04:26 +12:00 committed by Tim Ledbetter
commit 34b8947ca0
Notes: github-actions[bot] 2025-09-15 14:25:23 +00:00
7 changed files with 85 additions and 3 deletions

View file

@ -35,6 +35,7 @@
#include <LibWeb/CSS/StyleValues/ShadowStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/TextUnderlinePositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/Node.h>
@ -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);

View file

@ -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;

View file

@ -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<CSS::TextDecorationLine> 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; }

View file

@ -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());

View file

@ -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 underlines 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 elements 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));

View file

@ -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

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<style>
span {
text-decoration: underline;
}
</style>
<div>
<span style="text-underline-position: auto">foo</span>
<span style="text-underline-position: under">bar</span>
</div>
<script src="../include.js"></script>
<script>
test(() => {
println(internals.dumpDisplayList());
});
</script>
</html>