LibWeb: Allow multiple text-decoration-lines

The spec grammar for `text-decoration-line` is:

`none | [ underline || overline || line-through || blink ]`

Which means that it's either `none`, or any combination of the other
values. This patch makes that parse for `text-decoration-line` and
`text-decoration`, stores the results as a Vector, and adjusts
`paint_text_decoration()` to run as a loop over all the values that are
provided.

As noted, storing a Vector of values is a bit wasteful, as they could be
stored as flags in a single `u8`. But I was getting too confused trying
to do that in a nice way.
This commit is contained in:
Sam Atkins 2022-04-14 16:22:35 +01:00 committed by Andreas Kling
parent 85da8cbb07
commit 7c91fda088
Notes: sideshowbarker 2024-07-17 11:48:28 +09:00
9 changed files with 142 additions and 66 deletions

View File

@ -8,6 +8,7 @@
.strikethrough { text-decoration: line-through dotted green; }
.blink { text-decoration: blink; }
.current-color { color: #8B4513; text-decoration: underline; }
.overboard { text-decoration: double overline underline line-through magenta; }
</style>
</head>
<body>
@ -16,5 +17,6 @@
<p class="strikethrough">Wombling</p>
<p class="blink">FREE!</p>
<p class="current-color">This underline should match the text color</p>
<p class="overboard">This should have an underline, overline and line-through, all in glorious magenta.</p>
</body>
</html>

View File

@ -121,7 +121,7 @@ public:
Optional<int> const& z_index() const { return m_noninherited.z_index; }
CSS::TextAlign text_align() const { return m_inherited.text_align; }
CSS::TextJustify text_justify() const { return m_inherited.text_justify; }
CSS::TextDecorationLine text_decoration_line() const { return m_noninherited.text_decoration_line; }
Vector<CSS::TextDecorationLine> text_decoration_line() const { return m_noninherited.text_decoration_line; }
CSS::LengthPercentage text_decoration_thickness() const { return m_noninherited.text_decoration_thickness; }
CSS::TextDecorationStyle text_decoration_style() const { return m_noninherited.text_decoration_style; }
Color text_decoration_color() const { return m_noninherited.text_decoration_color; }
@ -217,7 +217,8 @@ protected:
CSS::Clear clear { InitialValues::clear() };
CSS::Display display { InitialValues::display() };
Optional<int> z_index;
CSS::TextDecorationLine text_decoration_line { InitialValues::text_decoration_line() };
// FIXME: Store this as flags in a u8.
Vector<CSS::TextDecorationLine> text_decoration_line { InitialValues::text_decoration_line() };
CSS::LengthPercentage text_decoration_thickness { InitialValues::text_decoration_thickness() };
CSS::TextDecorationStyle text_decoration_style { InitialValues::text_decoration_style() };
Color text_decoration_color { InitialValues::color() };
@ -282,7 +283,7 @@ public:
void set_z_index(Optional<int> value) { m_noninherited.z_index = value; }
void set_text_align(CSS::TextAlign text_align) { m_inherited.text_align = text_align; }
void set_text_justify(CSS::TextJustify text_justify) { m_inherited.text_justify = text_justify; }
void set_text_decoration_line(CSS::TextDecorationLine value) { m_noninherited.text_decoration_line = value; }
void set_text_decoration_line(Vector<CSS::TextDecorationLine> value) { m_noninherited.text_decoration_line = move(value); }
void set_text_decoration_thickness(CSS::LengthPercentage value) { m_noninherited.text_decoration_thickness = value; }
void set_text_decoration_style(CSS::TextDecorationStyle value) { m_noninherited.text_decoration_style = value; }
void set_text_decoration_color(Color value) { m_noninherited.text_decoration_color = value; }

View File

@ -4673,15 +4673,15 @@ RefPtr<StyleValue> Parser::parse_overflow_value(Vector<ComponentValue> const& co
RefPtr<StyleValue> Parser::parse_text_decoration_value(Vector<ComponentValue> const& component_values)
{
if (component_values.size() > 4)
return nullptr;
RefPtr<StyleValue> decoration_line;
RefPtr<StyleValue> decoration_thickness;
RefPtr<StyleValue> decoration_style;
RefPtr<StyleValue> decoration_color;
for (auto& part : component_values) {
auto tokens = TokenStream { component_values };
while (tokens.has_next_token()) {
auto& part = tokens.next_token();
auto value = parse_css_value(part);
if (!value)
return nullptr;
@ -4695,7 +4695,11 @@ RefPtr<StyleValue> Parser::parse_text_decoration_value(Vector<ComponentValue> co
if (property_accepts_value(PropertyID::TextDecorationLine, *value)) {
if (decoration_line)
return nullptr;
decoration_line = value.release_nonnull();
tokens.reconsume_current_input_token();
auto parsed_decoration_line = parse_text_decoration_line_value(tokens);
if (!parsed_decoration_line)
return nullptr;
decoration_line = parsed_decoration_line.release_nonnull();
continue;
}
if (property_accepts_value(PropertyID::TextDecorationThickness, *value)) {
@ -4726,6 +4730,45 @@ RefPtr<StyleValue> Parser::parse_text_decoration_value(Vector<ComponentValue> co
return TextDecorationStyleValue::create(decoration_line.release_nonnull(), decoration_thickness.release_nonnull(), decoration_style.release_nonnull(), decoration_color.release_nonnull());
}
RefPtr<StyleValue> Parser::parse_text_decoration_line_value(TokenStream<ComponentValue>& tokens)
{
NonnullRefPtrVector<StyleValue> style_values;
while (tokens.has_next_token()) {
auto& token = tokens.next_token();
auto maybe_value = parse_css_value(token);
if (!maybe_value || !property_accepts_value(PropertyID::TextDecorationLine, *maybe_value)) {
tokens.reconsume_current_input_token();
break;
}
auto value = maybe_value.release_nonnull();
if (auto maybe_line = value_id_to_text_decoration_line(value->to_identifier()); maybe_line.has_value()) {
auto line = maybe_line.release_value();
if (line == TextDecorationLine::None) {
if (!style_values.is_empty()) {
tokens.reconsume_current_input_token();
break;
}
return value;
}
if (style_values.contains_slow(value)) {
tokens.reconsume_current_input_token();
break;
}
style_values.append(move(value));
continue;
}
tokens.reconsume_current_input_token();
break;
}
if (style_values.is_empty())
return nullptr;
return StyleValueList::create(move(style_values), StyleValueList::Separator::Space);
}
static Optional<CSS::TransformFunction> parse_transform_function_name(StringView name)
{
if (name == "matrix")
@ -5076,6 +5119,13 @@ Result<NonnullRefPtr<StyleValue>, Parser::ParsingResult> Parser::parse_css_value
if (auto parsed_value = parse_text_decoration_value(component_values))
return parsed_value.release_nonnull();
return ParsingResult::SyntaxError;
case PropertyID::TextDecorationLine: {
TokenStream tokens { component_values };
auto parsed_value = parse_text_decoration_line_value(tokens);
if (parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParsingResult::SyntaxError;
}
case PropertyID::TextShadow:
if (auto parsed_value = parse_shadow_value(component_values, AllowInsetKeyword::No))
return parsed_value.release_nonnull();

View File

@ -319,6 +319,7 @@ private:
RefPtr<StyleValue> parse_shadow_value(Vector<ComponentValue> const&, AllowInsetKeyword);
RefPtr<StyleValue> parse_single_shadow_value(TokenStream<ComponentValue>&, AllowInsetKeyword);
RefPtr<StyleValue> parse_text_decoration_value(Vector<ComponentValue> const&);
RefPtr<StyleValue> parse_text_decoration_line_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue> parse_transform_value(Vector<ComponentValue> const&);
RefPtr<StyleValue> parse_transform_origin_value(Vector<ComponentValue> const&);

View File

@ -140,8 +140,16 @@ RefPtr<StyleValue> ResolvedCSSStyleDeclaration::style_value_for_property(Layout:
}
case CSS::PropertyID::TextAlign:
return IdentifierStyleValue::create(to_value_id(layout_node.computed_values().text_align()));
case CSS::PropertyID::TextDecorationLine:
return IdentifierStyleValue::create(to_value_id(layout_node.computed_values().text_decoration_line()));
case CSS::PropertyID::TextDecorationLine: {
auto text_decoration_lines = layout_node.computed_values().text_decoration_line();
if (text_decoration_lines.is_empty())
return IdentifierStyleValue::create(ValueID::None);
NonnullRefPtrVector<StyleValue> style_values;
for (auto const& line : text_decoration_lines) {
style_values.append(IdentifierStyleValue::create(to_value_id(line)));
}
return StyleValueList::create(move(style_values), StyleValueList::Separator::Space);
}
case CSS::PropertyID::TextDecorationStyle:
return IdentifierStyleValue::create(to_value_id(layout_node.computed_values().text_decoration_style()));
case CSS::PropertyID::TextTransform:

View File

@ -490,10 +490,23 @@ CSS::Display StyleProperties::display() const
}
}
Optional<CSS::TextDecorationLine> StyleProperties::text_decoration_line() const
Vector<CSS::TextDecorationLine> StyleProperties::text_decoration_line() const
{
auto value = property(CSS::PropertyID::TextDecorationLine);
return value_id_to_text_decoration_line(value->to_identifier());
if (value->is_value_list()) {
Vector<CSS::TextDecorationLine> lines;
auto& values = value->as_value_list().values();
for (auto const& item : values) {
lines.append(value_id_to_text_decoration_line(item.to_identifier()).value());
}
return lines;
}
if (value->is_identifier() && value->to_identifier() == ValueID::None)
return {};
VERIFY_NOT_REACHED();
}
Optional<CSS::TextDecorationStyle> StyleProperties::text_decoration_style() const

View File

@ -55,7 +55,7 @@ public:
Optional<CSS::Cursor> cursor() const;
Optional<CSS::WhiteSpace> white_space() const;
Optional<CSS::LineStyle> line_style(CSS::PropertyID) const;
Optional<CSS::TextDecorationLine> text_decoration_line() const;
Vector<CSS::TextDecorationLine> text_decoration_line() const;
Optional<CSS::TextDecorationStyle> text_decoration_style() const;
Optional<CSS::TextTransform> text_transform() const;
Vector<CSS::ShadowData> text_shadow() const;

View File

@ -453,9 +453,7 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style)
if (pointer_events.has_value())
computed_values.set_pointer_events(pointer_events.value());
auto text_decoration_line = computed_style.text_decoration_line();
if (text_decoration_line.has_value())
computed_values.set_text_decoration_line(text_decoration_line.value());
computed_values.set_text_decoration_line(computed_style.text_decoration_line());
auto text_decoration_style = computed_style.text_decoration_style();
if (text_decoration_style.has_value())

View File

@ -289,36 +289,11 @@ static void paint_cursor_if_needed(PaintContext& context, Layout::TextNode const
static void paint_text_decoration(Gfx::Painter& painter, Layout::Node const& text_node, Layout::LineBoxFragment const& fragment)
{
Gfx::IntPoint line_start_point {};
Gfx::IntPoint line_end_point {};
auto& font = fragment.layout_node().font();
auto fragment_box = enclosing_int_rect(fragment.absolute_rect());
auto glyph_height = font.pixel_size();
auto baseline = fragment_box.height() / 2 - (glyph_height + 4) / 2 + glyph_height;
switch (text_node.computed_values().text_decoration_line()) {
case CSS::TextDecorationLine::None:
return;
case CSS::TextDecorationLine::Underline:
line_start_point = fragment_box.top_left().translated(0, baseline + 2);
line_end_point = fragment_box.top_right().translated(0, baseline + 2);
break;
case CSS::TextDecorationLine::Overline:
line_start_point = fragment_box.top_left().translated(0, baseline - glyph_height);
line_end_point = fragment_box.top_right().translated(0, baseline - glyph_height);
break;
case CSS::TextDecorationLine::LineThrough: {
auto x_height = font.x_height();
line_start_point = fragment_box.top_left().translated(0, baseline - x_height / 2);
line_end_point = fragment_box.top_right().translated(0, baseline - x_height / 2);
break;
}
case CSS::TextDecorationLine::Blink:
// Conforming user agents may simply not blink the text
return;
}
auto line_color = text_node.computed_values().text_decoration_color();
int line_thickness = [&] {
@ -329,38 +304,66 @@ static void paint_text_decoration(Gfx::Painter& painter, Layout::Node const& tex
return computed_thickness.to_px(text_node);
}();
switch (text_node.computed_values().text_decoration_style()) {
case CSS::TextDecorationStyle::Solid:
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Solid);
break;
case CSS::TextDecorationStyle::Double:
switch (text_node.computed_values().text_decoration_line()) {
auto text_decoration_lines = text_node.computed_values().text_decoration_line();
for (auto line : text_decoration_lines) {
Gfx::IntPoint line_start_point {};
Gfx::IntPoint line_end_point {};
switch (line) {
case CSS::TextDecorationLine::None:
return;
case CSS::TextDecorationLine::Underline:
line_start_point = fragment_box.top_left().translated(0, baseline + 2);
line_end_point = fragment_box.top_right().translated(0, baseline + 2);
break;
case CSS::TextDecorationLine::Overline:
line_start_point.translate_by(0, -line_thickness - 1);
line_end_point.translate_by(0, -line_thickness - 1);
line_start_point = fragment_box.top_left().translated(0, baseline - glyph_height);
line_end_point = fragment_box.top_right().translated(0, baseline - glyph_height);
break;
case CSS::TextDecorationLine::LineThrough:
line_start_point.translate_by(0, -line_thickness / 2);
line_end_point.translate_by(0, -line_thickness / 2);
case CSS::TextDecorationLine::LineThrough: {
auto x_height = font.x_height();
line_start_point = fragment_box.top_left().translated(0, baseline - x_height / 2);
line_end_point = fragment_box.top_right().translated(0, baseline - x_height / 2);
break;
default:
VERIFY_NOT_REACHED();
}
case CSS::TextDecorationLine::Blink:
// Conforming user agents may simply not blink the text
return;
}
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness);
painter.draw_line(line_start_point.translated(0, line_thickness + 1), line_end_point.translated(0, line_thickness + 1), line_color, line_thickness);
break;
case CSS::TextDecorationStyle::Dashed:
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Dashed);
break;
case CSS::TextDecorationStyle::Dotted:
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Dotted);
break;
case CSS::TextDecorationStyle::Wavy:
painter.draw_triangle_wave(line_start_point, line_end_point, line_color, line_thickness + 1, line_thickness);
break;
switch (text_node.computed_values().text_decoration_style()) {
case CSS::TextDecorationStyle::Solid:
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Solid);
break;
case CSS::TextDecorationStyle::Double:
switch (line) {
case CSS::TextDecorationLine::Underline:
break;
case CSS::TextDecorationLine::Overline:
line_start_point.translate_by(0, -line_thickness - 1);
line_end_point.translate_by(0, -line_thickness - 1);
break;
case CSS::TextDecorationLine::LineThrough:
line_start_point.translate_by(0, -line_thickness / 2);
line_end_point.translate_by(0, -line_thickness / 2);
break;
default:
VERIFY_NOT_REACHED();
}
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness);
painter.draw_line(line_start_point.translated(0, line_thickness + 1), line_end_point.translated(0, line_thickness + 1), line_color, line_thickness);
break;
case CSS::TextDecorationStyle::Dashed:
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Dashed);
break;
case CSS::TextDecorationStyle::Dotted:
painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Dotted);
break;
case CSS::TextDecorationStyle::Wavy:
painter.draw_triangle_wave(line_start_point, line_end_point, line_color, line_thickness + 1, line_thickness);
break;
}
}
}