LibWeb: Respect text-underline-offset when rendering underlines

This commit is contained in:
Callum Law 2025-09-12 12:21:16 +12:00 committed by Tim Ledbetter
commit 815e77c04d
Notes: github-actions[bot] 2025-09-12 06:08:11 +00:00
11 changed files with 226 additions and 2 deletions

View file

@ -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();
// <length>
if (computed_text_underline_offset.is_length())
return computed_text_underline_offset.as_length().length().absolute_length_to_px();
// <percentage>
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 <length-percentage>
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);

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Non-reference case for text-underline-offset</title>
<link rel="stylesheet" type="text/css" href="../../..../../../fonts/ahem.css" />
<style>
#main {
margin: 2em;
display:flex
}
div span {
text-decoration: green underline;
text-decoration-skip-ink: none;
font: 20px/1 Ahem;
color: transparent;
padding-bottom: 20px;
border: 1px dotted transparent;
border-bottom-color: cyan;
}
</style>
</head>
<body>
<p class="instructions">Test passes if the box on the right has a lower underline than the box on the left</p>
<div id="main">
<div>
<p>left<span>XXXX</span></p>
</div>
<div>
<p><span id="rightbox">XXXX</span>right</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Reference case for text-underline-offset</title>
<link rel="stylesheet" type="text/css" href="../../..../../../fonts/ahem.css" />
<style>
#main{
border-bottom: 1px solid cyan;
display: flex;
}
#text, #norm{
text-decoration: green underline;
text-decoration-skip-ink: none;
text-underline-offset: 0px;
font: 20px/1 Ahem;
color: transparent;
position: relative;
top: 21px;
margin-right: 10px;
}
</style>
</head>
<body >
<p class="instructions">Test passes if the lines are at the same level</p>
<div id="main">
<div>
<p>left<span id="text">XXXX</span></p>
</div>
<div>
<p><span id="norm">XXXX</span>right</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test case for negative values of text-underline-offset</title>
</head>
<body>
<div style="font-size: 48px; text-decoration: underline; text-underline-offset: 0px; text-decoration-skip: none;">Hello</div>
</body>
</html>

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>text-underline-offset test case</title>
<meta name="assert" content="text-decoration:underline; there is a line at or under the alphabetic baseline">
<link rel="author" title="Charlie Marlow" href="mailto:cmarlow@mozilla.com">
<link rel="author" title="Mozilla" href="https://www.mozilla.org">
<link rel="help" href="https://drafts.csswg.org/css-text-decor-4/#underline-offset">
<link rel="mismatch" href="../../../../expected/wpt-import/css/css-text-decor/reference/text-underline-offset-001-notref.html">
<link rel="stylesheet" type="text/css" href="../..../../fonts/ahem.css" />
<style>
#main {
margin: 2em;
display:flex
}
div span {
text-decoration: green underline;
text-decoration-skip-ink: none;
font: 20px/1 Ahem;
color: transparent;
padding-bottom: 20px;
border: 1px dotted transparent;
border-bottom-color: cyan;
}
#rightbox {
text-underline-offset: 24px;
}
</style>
</head>
<body>
<p class="instructions">Test passes if the box on the right has a lower underline than the box on the left</p>
<div id="main">
<div>
<p>left<span>XXXX</span></p>
</div>
<div>
<p><span id="rightbox">XXXX</span>right</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test case for text-underline-offset</title>
<link rel="author" title="Charlie Marlow" href="mailto:cmarlow@mozilla.com">
<link rel="author" title="Mozilla" href="https://www.mozilla.org">
<link rel="help" href="https://drafts.csswg.org/css-text-decor-4/#underline-offset">
<link rel="match" href="../../../../expected/wpt-import/css/css-text-decor/reference/text-underline-offset-002-ref.html">
<link rel="stylesheet" type="text/css" href="../..../../fonts/ahem.css" />
<style>
#main{
border-bottom: 1px solid cyan;
display: flex;
}
#text, #norm{
text-decoration-color: green;
text-decoration-line: underline;
text-decoration-skip-ink: none;
font: 20px/1 Ahem;
color: transparent;
position: relative;
margin-right: 10px;
}
#text{
top: 10px;
text-underline-offset: 11px;
}
#norm{
top: 21px;
text-underline-offset: 0px;
}
</style>
</head>
<body >
<p class="instructions">Test passes if the lines are at the same level</p>
<div id="main">
<div>
<p>left<span id="text">XXXX</span></p>
</div>
<div>
<p><span id="norm">XXXX</span>right</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test case for negative values of text-underline-offset</title>
<meta name="assert" content="text-underline-offset: Negative values should not be clamped to zero">
<link rel="author" title="Myles C. Maxfield" href="mmaxfield@apple.com">
<link rel="author" title="Apple Inc." href="http://www.apple.com/">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/4059">
<link rel="mismatch" href="../../../../expected/wpt-import/css/css-text-decor/reference/text-underline-offset-negative-notref.html">
</head>
<body>
<div style="font-size: 48px; text-decoration: underline; text-underline-offset: -20px; text-decoration-skip: none;">Hello</div>
</body>
</html>