Skip to content

Commit bb88e9f

Browse files
committed
LibWeb: Paint text decorations from each decorating box
Previously, decoration lines reached descendants by treating `text-decoration-line` as an inherited property. Descendants painted the lines with their own color, style, thickness, underline offset, and underline position rather than the values specified on the decorating box, and derived the line geometry from their own font, so one decoration changed appearance wherever its text changed font size or was shifted by `vertical-align`. Now each text fragment walks its ancestor chain and paints the decorations of every decorating box that applies to it, using that box's computed values and font. Lines are anchored at the decorating box's own baseline, so vertical alignment moves a box's decorations together with it but never with its descendants. Decorations that propagate into a block container derive their geometry from that block.
1 parent de1b6c9 commit bb88e9f

33 files changed

Lines changed: 428 additions & 66 deletions

Libraries/LibWeb/CSS/Properties.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4330,8 +4330,7 @@
43304330
"text-decoration-line": {
43314331
"affects-layout": false,
43324332
"animation-type": "discrete",
4333-
"__comment": "FIXME: This property is not supposed to be inherited, but we currently rely on inheritance to propagate decorations into line boxes.",
4334-
"inherited": true,
4333+
"inherited": false,
43354334
"initial": "none",
43364335
"requires-computation": "never",
43374336
"valid-types": [

Libraries/LibWeb/Layout/InlineFormattingContext.cpp

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ void InlineFormattingContext::compute_inline_box_pieces()
265265

266266
struct StagedPiece {
267267
Painting::InlineBoxPiece piece;
268-
u32 line_index { 0 };
269268
u32 depth { 0 };
270269
size_t discovery_index { 0 };
271270
};
@@ -330,17 +329,21 @@ void InlineFormattingContext::compute_inline_box_pieces()
330329
}
331330

332331
for (auto& line : per_node.lines) {
332+
auto const& line_box = line_boxes[line.line_index];
333+
auto line_box_top = line_box.bottom() - line_box.block_length();
334+
auto inline_box_baseline = line_box.inline_box_baseline(node).value_or(line_box_top + line_box.baseline());
333335
if (!line.has_contributions) {
334336
if (line.interrupting_block_position.has_value()) {
335337
staged_pieces.append({ .piece = {
336338
.node = node,
337339
.first_fragment_index = line.first_fragment_index.value_or(0),
338340
.fragment_count = line.fragment_count,
341+
.line_index = static_cast<u32>(line.line_index),
339342
.border_box_rect = { *line.interrupting_block_position, {} },
343+
.baseline = inline_box_baseline,
340344
.present_edges = edge_bits(true, true),
341345
.is_geometry_only_placeholder = true,
342346
},
343-
.line_index = static_cast<u32>(line.line_index),
344347
.depth = per_node.depth,
345348
.discovery_index = node_index });
346349
}
@@ -369,10 +372,11 @@ void InlineFormattingContext::compute_inline_box_pieces()
369372
.node = node,
370373
.first_fragment_index = line.first_fragment_index.value_or(0),
371374
.fragment_count = line.fragment_count,
375+
.line_index = static_cast<u32>(line.line_index),
372376
.border_box_rect = border_box_rect,
377+
.baseline = inline_box_baseline,
373378
.present_edges = edge_bits(has_low_edge, has_high_edge),
374379
},
375-
.line_index = static_cast<u32>(line.line_index),
376380
.depth = per_node.depth,
377381
.discovery_index = node_index });
378382

@@ -399,14 +403,13 @@ void InlineFormattingContext::compute_inline_box_pieces()
399403
.present_edges = edge_bits(true, true),
400404
.is_geometry_only_placeholder = true,
401405
},
402-
.line_index = 0,
403406
.depth = fragmented_inline_nesting_depth(node),
404407
.discovery_index = per_node_data.size() + i });
405408
}
406409

407410
quick_sort(staged_pieces, [](auto const& a, auto const& b) {
408-
if (a.line_index != b.line_index)
409-
return a.line_index < b.line_index;
411+
if (a.piece.line_index != b.piece.line_index)
412+
return a.piece.line_index < b.piece.line_index;
410413
if (a.depth != b.depth)
411414
return a.depth < b.depth;
412415
return a.discovery_index < b.discovery_index;

Libraries/LibWeb/Layout/LayoutState.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,10 @@ void LayoutState::resolve_relative_positions()
222222
auto offset = accumulated_offset_per_node.ensure(piece_node, [&] {
223223
return accumulated_relative_insets_from_inline_ancestor_chain(piece_node, nullptr).offset;
224224
});
225-
if (!offset.is_zero())
225+
if (!offset.is_zero()) {
226226
piece.border_box_rect.translate_by(offset);
227+
piece.baseline += offset.y();
228+
}
227229
}
228230
});
229231
}
@@ -385,6 +387,7 @@ void LayoutState::commit_used_values_and_build_paint_tree(Box& root, RefPtr<Pain
385387

386388
lines.append({
387389
.rect = rect_for_line_box(line_box, used_values.content_width()),
390+
.baseline = line_box.bottom() - line_box.block_length() + line_box.baseline(),
388391
.fragment_count = static_cast<u32>(paintable_with_lines->fragments().size() - first_fragment_index),
389392
});
390393
}

Libraries/LibWeb/Layout/LineBox.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ CSSPixels LineBox::bottom() const
3737
return m_bottom;
3838
}
3939

40+
void LineBox::set_inline_box_baseline(NodeWithStyleAndBoxModelMetrics const& box, CSSPixels baseline)
41+
{
42+
if (any_of(m_inline_box_baselines, [&](auto const& entry) { return entry.box == &box; }))
43+
return;
44+
m_inline_box_baselines.append({ &box, baseline });
45+
}
46+
47+
Optional<CSSPixels> LineBox::inline_box_baseline(NodeWithStyleAndBoxModelMetrics const& box) const
48+
{
49+
auto entry = m_inline_box_baselines.first_matching([&](auto const& entry) { return entry.box == &box; });
50+
if (!entry.has_value())
51+
return {};
52+
return entry->baseline;
53+
}
54+
4055
void LineBox::add_fragment(Node const& layout_node, size_t start, size_t length, CSSPixels leading_size,
4156
CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width,
4257
CSSPixels content_height, CSSPixels border_box_top, CSSPixels border_box_bottom, RefPtr<Gfx::GlyphRun> glyph_run)

Libraries/LibWeb/Layout/LineBox.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Web::Layout {
1313

1414
class Box;
15+
class NodeWithStyleAndBoxModelMetrics;
1516

1617
class LineBox {
1718
public:
@@ -44,6 +45,9 @@ class LineBox {
4445
CSSPixels block_length() const { return m_block_length; }
4546
CSSPixels baseline() const { return m_baseline; }
4647

48+
void set_inline_box_baseline(NodeWithStyleAndBoxModelMetrics const&, CSSPixels);
49+
Optional<CSSPixels> inline_box_baseline(NodeWithStyleAndBoxModelMetrics const&) const;
50+
4751
void add_fragment(Node const& layout_node, size_t start, size_t length, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height, CSSPixels border_box_top, CSSPixels border_box_bottom, RefPtr<Gfx::GlyphRun> glyph_run = {});
4852
void add_static_position_marker(Box const&, bool preceded_by_inline_box_start_edges);
4953

@@ -78,6 +82,11 @@ class LineBox {
7882

7983
Vector<LineBoxFragment> m_fragments;
8084
Vector<StaticPositionMarker> m_static_position_markers;
85+
struct InlineBoxBaseline {
86+
NodeWithStyleAndBoxModelMetrics const* box { nullptr };
87+
CSSPixels baseline { 0 };
88+
};
89+
Vector<InlineBoxBaseline> m_inline_box_baselines;
8190
CSSPixels m_inline_length { 0 };
8291
CSSPixels m_block_length { 0 };
8392
CSSPixels m_bottom { 0 };

Libraries/LibWeb/Layout/LineBuilder.cpp

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@ void LineBuilder::update_last_line()
455455

456456
Optional<CSSPixels> first_unshifted_text_baseline;
457457

458+
struct InlineBoxAlignment {
459+
NodeWithStyleAndBoxModelMetrics const* box { nullptr };
460+
CSSPixels vertical_shift { 0 };
461+
};
462+
458463
for (auto& fragment : line_box.fragments()) {
459464
CSSPixels new_fragment_inline_offset = inline_offset + fragment.inline_offset();
460465

@@ -476,29 +481,41 @@ void LineBuilder::update_last_line()
476481
CSSPixels new_fragment_block_offset = block_offset_value_for_alignment(own_vertical_align, fragment_metrics);
477482
auto baseline_aligned_fragment_block_offset = block_offset_value_for_alignment(CSS::VerticalAlign::Baseline, fragment_metrics);
478483

484+
Vector<InlineBoxAlignment, 4> inline_box_alignments;
485+
479486
// A 'top'- or 'bottom'-aligned box forms the root of its own aligned subtree and is positioned relative to the
480487
// line box, so it must ignore the vertical-align of its ancestors.
481488
auto* own_alignment = own_vertical_align.get_pointer<CSS::VerticalAlign>();
482489
bool own_alignment_is_line_relative = own_alignment && first_is_one_of(*own_alignment, CSS::VerticalAlign::Top, CSS::VerticalAlign::Bottom);
483490

484491
auto const& node = fragment.style_source();
485492
auto const* containing_block = &m_context.containing_block();
493+
if (&node != containing_block && node.is_fragmented_inline()) {
494+
inline_box_alignments.append({
495+
.box = &as<NodeWithStyleAndBoxModelMetrics>(node),
496+
.vertical_shift = new_fragment_block_offset - baseline_aligned_fragment_block_offset,
497+
});
498+
}
486499
auto const* first_ancestor = &node == containing_block ? nullptr : node.parent();
487500
for (auto const* ancestor = first_ancestor;
488501
!own_alignment_is_line_relative && ancestor && ancestor->is_fragmented_inline() && ancestor != containing_block;
489502
ancestor = ancestor->parent()) {
490503
auto const& ancestor_vertical_align = ancestor->computed_values().vertical_align();
491504
if (ancestor_vertical_align.has<CSS::VerticalAlign>()) {
492505
auto keyword = ancestor_vertical_align.get<CSS::VerticalAlign>();
493-
if (keyword == CSS::VerticalAlign::Baseline)
506+
if (keyword == CSS::VerticalAlign::Baseline) {
507+
inline_box_alignments.append({ &as<NodeWithStyleAndBoxModelMetrics>(*ancestor), 0 });
494508
continue;
509+
}
495510
// FIXME: Implement aligning a 'top'- or 'bottom'-aligned ancestor's aligned subtree to the line box
496511
if (first_is_one_of(keyword, CSS::VerticalAlign::Top, CSS::VerticalAlign::Bottom))
497512
break;
498513
}
499514
auto ancestor_metrics = inline_box_alignment_metrics(*ancestor);
500-
new_fragment_block_offset += block_offset_value_for_alignment(ancestor_vertical_align, ancestor_metrics)
515+
auto ancestor_vertical_shift = block_offset_value_for_alignment(ancestor_vertical_align, ancestor_metrics)
501516
- block_offset_value_for_alignment(CSS::VerticalAlign::Baseline, ancestor_metrics);
517+
new_fragment_block_offset += ancestor_vertical_shift;
518+
inline_box_alignments.append({ &as<NodeWithStyleAndBoxModelMetrics>(*ancestor), ancestor_vertical_shift });
502519
}
503520

504521
fragment.set_inline_offset(new_fragment_inline_offset);
@@ -546,6 +563,21 @@ void LineBuilder::update_last_line()
546563
if (!first_unshifted_text_baseline.has_value() && fragment_is_unshifted
547564
&& fragment.layout_node().is_text_node() && !fragment.is_fully_truncated())
548565
first_unshifted_text_baseline = fragment.block_offset() + fragment.baseline() - m_current_block_offset;
566+
567+
// https://drafts.csswg.org/css-text-decor-4/#text-line-constancy
568+
// UAs must adjust line positions to match the shifted metrics of decorating boxes shifted with
569+
// vertical-align values other than baseline [CSS2] or subscripted/superscripted via
570+
// font-variant-position [CSS-FONTS-3], but must not adjust the line position or thickness in response to
571+
// descendants of a decorating box that are so styled.
572+
// NB: Walk from the innermost box outward, removing each box's shift before recording its parent's
573+
// baseline.
574+
if (fragment.layout_node().is_text_node() && !fragment.is_fully_truncated()) {
575+
auto inline_box_baseline = fragment.block_offset() + fragment.baseline();
576+
for (auto const& alignment : inline_box_alignments) {
577+
line_box.set_inline_box_baseline(*alignment.box, inline_box_baseline);
578+
inline_box_baseline -= alignment.vertical_shift;
579+
}
580+
}
549581
}
550582

551583
for (auto& marker : line_box.static_position_markers()) {

Libraries/LibWeb/Layout/Node.cpp

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ Node* Node::topmost_layout_node_of_top_layer_placement()
8585
return direct_viewport_child_candidate;
8686
}
8787

88+
bool Node::is_pseudo_element_principal_box() const
89+
{
90+
auto pseudo_element = generated_for_pseudo_element();
91+
return pseudo_element.has_value() && pseudo_element_generator()->pseudo_element_unsafe_layout_node(*pseudo_element) == this;
92+
}
93+
8894
// https://www.w3.org/TR/css-display-3/#out-of-flow
8995
bool Node::is_out_of_flow(FormattingContext const& formatting_context) const
9096
{
@@ -588,6 +594,20 @@ bool NodeWithStyle::is_sticky_position() const
588594
return position == CSS::Positioning::Sticky;
589595
}
590596

597+
bool NodeWithStyle::is_text_decoration_propagation_boundary() const
598+
{
599+
// NB: Anonymous wrappers must stay transparent to propagation so an element's own decorations still reach
600+
// its text. The principal box of a pseudo-element is not a wrapper and must be checked like any other
601+
// element.
602+
if (is_anonymous() && !is_pseudo_element_principal_box())
603+
return false;
604+
605+
// https://drafts.csswg.org/css-text-decor-4/#decorating-box
606+
// NOTE: Note that text decorations are not propagated to any out-of-flow descendants, nor to the contents
607+
// of atomic inline-level descendants such as inline blocks and inline tables.
608+
return is_out_of_flow() || is_atomic_inline();
609+
}
610+
591611
NodeWithStyle::NodeWithStyle(DOM::Document& document, DOM::Node* node, NonnullRefPtr<CSS::ComputedValues const> computed_values)
592612
: Node(document, node)
593613
, m_computed_values(move(computed_values))
@@ -725,15 +745,6 @@ CSS::StyleScope const& NodeWithStyle::style_scope() const
725745
return document().style_scope();
726746
}
727747

728-
void NodeWithStyle::propagate_non_inherit_values(CSS::ComputedValues::Builder& builder) const
729-
{
730-
// NOTE: These properties are not inherited, but we still have to propagate them to anonymous wrappers.
731-
builder->set_text_decoration_line(computed_values().text_decoration_line());
732-
builder->set_text_decoration_thickness(computed_values().text_decoration_thickness());
733-
builder->set_text_decoration_color(computed_values().text_decoration_color());
734-
builder->set_text_decoration_style(computed_values().text_decoration_style());
735-
}
736-
737748
void NodeWithStyle::propagate_style_to_anonymous_wrappers()
738749
{
739750
// Update the style of any anonymous wrappers that inherit from this node.
@@ -752,16 +763,13 @@ void NodeWithStyle::propagate_style_to_anonymous_wrappers()
752763
// Propagate style to all anonymous children (except table wrappers!)
753764
for_each_child_of_type<NodeWithStyle>([&](NodeWithStyle& child) {
754765
if (child.is_anonymous() && !is<TableWrapper>(child)) {
755-
// NB: The principal box of a pseudo-element (::before, ::after, ::marker, etc) is anonymous in the
756-
// sense that it has no DOM node, but it's not an anonymous wrapper: it has its own computed style,
757-
// which is applied to it separately. Don't clobber that style with inherited values from this node.
758-
if (auto pseudo_element = child.generated_for_pseudo_element(); pseudo_element.has_value()
759-
&& child.pseudo_element_generator()->pseudo_element_unsafe_layout_node(*pseudo_element) == &child) {
766+
// NB: The principal box of a pseudo-element (::before, ::after, ::marker, etc) has its own computed
767+
// style, which is applied to it separately. Don't clobber that style with inherited values from
768+
// this node.
769+
if (child.is_pseudo_element_principal_box())
760770
return IterationDecision::Continue;
761-
}
762771
CSS::ComputedValues::Builder builder(child.computed_values());
763772
builder->inherit_from(computed_values());
764-
propagate_non_inherit_values(builder);
765773
child.set_computed_values(move(builder).build());
766774
child.propagate_style_to_anonymous_wrappers();
767775
}
@@ -901,7 +909,6 @@ NonnullRefPtr<NodeWithStyle> NodeWithStyle::create_anonymous_wrapper() const
901909
{
902910
auto builder = CSS::ComputedValues::Builder::create_inheriting_from(computed_values());
903911
builder->set_display(CSS::Display(CSS::DisplayOutside::Block, CSS::DisplayInside::Flow));
904-
propagate_non_inherit_values(builder);
905912
// CSS 2.2 9.2.1.1 creates anonymous block boxes, but 9.4.1 states inline-block creates a BFC.
906913
// Set wrapper to inline-block to participate correctly in the IFC within the parent inline-block.
907914
if (display().is_inline_block() && !has_children())

Libraries/LibWeb/Layout/Node.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ class WEB_API Node
8787

8888
bool is_generated_for_pseudo_element() const { return m_generated_for.has_value(); }
8989
Optional<CSS::PseudoElement> generated_for_pseudo_element() const { return m_generated_for; }
90+
// The principal box of a pseudo-element has no DOM node, but unlike an anonymous wrapper it has its own
91+
// computed style.
92+
bool is_pseudo_element_principal_box() const;
9093
bool is_generated_for_before_pseudo_element() const { return m_generated_for == CSS::PseudoElement::Before; }
9194
bool is_generated_for_after_pseudo_element() const { return m_generated_for == CSS::PseudoElement::After; }
9295
bool is_generated_for_backdrop_pseudo_element() const { return m_generated_for == CSS::PseudoElement::Backdrop; }
@@ -322,6 +325,8 @@ class WEB_API NodeWithStyle : public Node {
322325
bool is_fixed_position() const;
323326
bool is_sticky_position() const;
324327

328+
bool is_text_decoration_propagation_boundary() const;
329+
325330
// https://www.w3.org/TR/css-display-3/#out-of-flow
326331
bool is_out_of_flow(FormattingContext const&) const;
327332

@@ -389,7 +394,6 @@ class WEB_API NodeWithStyle : public Node {
389394
virtual bool is_node_with_style() const final { return true; }
390395

391396
void reset_table_box_computed_values_used_by_wrapper_to_init_values();
392-
void propagate_non_inherit_values(CSS::ComputedValues::Builder&) const;
393397
void propagate_style_to_anonymous_wrappers();
394398

395399
void rebuild_image_observers();

Libraries/LibWeb/Painting/PaintableTypes.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,20 @@ enum class SelectionState : u8 {
3232
};
3333

3434
// One record per line box of a block container with inline children, including lines without
35-
// fragments (e.g. blank lines between consecutive forced breaks in a textarea). The rect is
36-
// relative to the containing block's content-box origin, and fragment_count only counts
37-
// committed fragments (fully truncated ones are never committed).
35+
// fragments (e.g. blank lines between consecutive forced breaks in a textarea). The rect and
36+
// baseline are relative to the containing block's content-box origin, and fragment_count only
37+
// counts committed fragments (fully truncated ones are never committed).
3838
struct LineRecord {
3939
CSSPixelRect rect;
40+
CSSPixels baseline;
4041
u32 fragment_count { 0 };
4142
};
4243

4344
// One line's slice of an inline box (InlineNode or inline-flow ListItemBox) fragmented across
4445
// lines; box edges cut at a line boundary are absent, per box-decoration-break: slice. Fragment
45-
// indexes point into the containing block paintable's fragment list, and border_box_rect is
46-
// relative to that block's content-box origin. The committed piece list is ordered by (line,
47-
// nesting depth), so outer boxes' pieces precede nested ones on the same line.
46+
// indexes point into the containing block paintable's fragment list, and border_box_rect and
47+
// baseline are relative to that block's content-box origin. The committed piece list is ordered
48+
// by (line_index, nesting depth), so outer boxes' pieces precede nested ones on the same line.
4849
struct InlineBoxPiece {
4950
enum class Edge : u8 {
5051
Top = 1 << 0,
@@ -56,7 +57,9 @@ struct InlineBoxPiece {
5657
WeakPtr<Layout::Node const> node;
5758
u32 first_fragment_index { 0 };
5859
u32 fragment_count { 0 };
60+
u32 line_index { 0 };
5961
CSSPixelRect border_box_rect;
62+
CSSPixels baseline { 0 };
6063
u8 present_edges { 0 };
6164
bool is_geometry_only_placeholder { false };
6265

0 commit comments

Comments
 (0)