Skip to content

Commit 0f029f7

Browse files
committed
LibWeb: Invalidate descendant paint caches on text decoration changes
Previously, changing a text decoration only invalidated the cached paint commands of the decorating box itself. Descendant boxes record propagated decorations in their own cached commands, so they kept painting the old decoration. Text decoration changes now also invalidate every descendant paint cache up to the propagation boundaries. Underline offset and position changes take this path as well: they are repaints rather than relayouts, and a descendant that overrides either inherited property keeps an unchanged style when the decorating box changes them, so nothing else would repaint it.
1 parent ae35f6a commit 0f029f7

9 files changed

Lines changed: 123 additions & 6 deletions

File tree

Libraries/LibWeb/Animations/AnimationEffect.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -932,10 +932,21 @@ AnimationUpdateContext::~AnimationUpdateContext()
932932

933933
if (invalidation.needs_repaint()) {
934934
if (element.pseudo_element().has_value()) {
935-
if (auto pseudo_element_node = target->pseudo_element_unsafe_layout_node(*element.pseudo_element()); pseudo_element_node && pseudo_element_node->paintable())
936-
pseudo_element_node->paintable()->set_needs_repaint();
935+
if (auto pseudo_element_node = target->pseudo_element_unsafe_layout_node(*element.pseudo_element())) {
936+
if (auto paintable = pseudo_element_node->paintable()) {
937+
paintable->set_needs_repaint();
938+
if (invalidation.repaint_propagated_text_decorations)
939+
paintable->invalidate_propagated_text_decoration_caches();
940+
}
941+
}
937942
} else {
938943
target->set_needs_repaint();
944+
if (invalidation.repaint_propagated_text_decorations) {
945+
if (auto* layout_node = target->unsafe_layout_node()) {
946+
if (auto paintable = layout_node->paintable())
947+
paintable->invalidate_propagated_text_decoration_caches();
948+
}
949+
}
939950
}
940951
}
941952
if (invalidation.needs_stacking_context_tree_rebuild())

Libraries/LibWeb/CSS/Properties.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4445,6 +4445,7 @@
44454445
]
44464446
},
44474447
"text-underline-offset": {
4448+
"affects-layout": false,
44484449
"animation-type": "by-computed-value",
44494450
"inherited": true,
44504451
"initial": "auto",
@@ -4459,6 +4460,7 @@
44594460
"percentages-resolve-to": "length"
44604461
},
44614462
"text-underline-position": {
4463+
"affects-layout": false,
44624464
"animation-type": "discrete",
44634465
"inherited": true,
44644466
"initial": "auto",

Libraries/LibWeb/CSS/StyleInvalidation.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,13 @@ RequiredInvalidationAfterStyleChange compute_property_invalidation(CSS::Property
345345
if (AK::first_is_one_of(property_id, CSS::PropertyID::ContainerName, CSS::PropertyID::ContainerType))
346346
invalidation.recompute_descendant_styles = true;
347347

348+
// Text decorations propagate to descendant boxes, which paint them from this element's computed
349+
// values, so their cached paint commands must be discarded even though their style is unchanged.
350+
if (AK::first_is_one_of(property_id, CSS::PropertyID::TextDecorationLine, CSS::PropertyID::TextDecorationColor,
351+
CSS::PropertyID::TextDecorationStyle, CSS::PropertyID::TextDecorationThickness,
352+
CSS::PropertyID::TextUnderlineOffset, CSS::PropertyID::TextUnderlinePosition))
353+
invalidation.repaint_propagated_text_decorations = true;
354+
348355
// OPTIMIZATION: Special handling for CSS `visibility`:
349356
if (property_id == CSS::PropertyID::Visibility) {
350357
// We don't need to relayout if the visibility changes from visible to hidden or vice versa. Only collapse requires relayout.

Libraries/LibWeb/CSS/StyleInvalidation.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ struct RequiredInvalidationAfterStyleChange {
5757
// descendants. Containing block pointers are only recomputed by a full layout pass, so
5858
// partial relayout boundary qualification cannot be trusted until one runs.
5959
bool changes_containing_block_establishment : 1 { false };
60+
// A property controlling decorations originated by this box changed. Descendant boxes paint
61+
// the propagated decorations from these values, so their cached paint commands are stale.
62+
bool repaint_propagated_text_decorations : 1 { false };
6063

6164
void operator|=(RequiredInvalidationAfterStyleChange const& other)
6265
{
@@ -67,6 +70,7 @@ struct RequiredInvalidationAfterStyleChange {
6770
recompute_descendant_styles |= other.recompute_descendant_styles;
6871
inherited_style_changed |= other.inherited_style_changed;
6972
changes_containing_block_establishment |= other.changes_containing_block_establishment;
73+
repaint_propagated_text_decorations |= other.repaint_propagated_text_decorations;
7074
}
7175

7276
[[nodiscard]] bool is_none() const
@@ -76,7 +80,8 @@ struct RequiredInvalidationAfterStyleChange {
7680
&& !m_needs_scrollable_overflow_recalculation
7781
&& !recompute_descendant_styles
7882
&& !inherited_style_changed
79-
&& !changes_containing_block_establishment;
83+
&& !changes_containing_block_establishment
84+
&& !repaint_propagated_text_decorations;
8085
}
8186

8287
static RequiredInvalidationAfterStyleChange full()

Libraries/LibWeb/DOM/Element.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,8 +1154,13 @@ void Element::apply_computed_style_to_layout_node_if_needed(CSS::RequiredInvalid
11541154
auto element_computed_values = m_computed_values;
11551155
VERIFY(element_computed_values);
11561156
unsafe_layout_node()->apply_style(element_computed_values.release_nonnull());
1157-
if (invalidation.needs_repaint())
1157+
if (invalidation.needs_repaint()) {
11581158
set_needs_repaint();
1159+
if (invalidation.repaint_propagated_text_decorations) {
1160+
if (auto paintable = unsafe_layout_node()->paintable())
1161+
paintable->invalidate_propagated_text_decoration_caches();
1162+
}
1163+
}
11591164

11601165
// Do the same for pseudo-elements.
11611166
for_each_synthetic_pseudo_element([&](CSS::PseudoElement pseudo_element_type, SyntheticPseudoElement const& pseudo_element) {
@@ -1165,8 +1170,11 @@ void Element::apply_computed_style_to_layout_node_if_needed(CSS::RequiredInvalid
11651170

11661171
if (auto node_with_style = pseudo_element.unsafe_layout_node()) {
11671172
node_with_style->apply_style(pseudo_element_style.release_nonnull());
1168-
if (invalidation.needs_repaint() && node_with_style->paintable())
1169-
node_with_style->paintable()->set_needs_repaint();
1173+
if (auto paintable = node_with_style->paintable(); paintable && invalidation.needs_repaint()) {
1174+
paintable->set_needs_repaint();
1175+
if (invalidation.repaint_propagated_text_decorations)
1176+
paintable->invalidate_propagated_text_decoration_caches();
1177+
}
11701178
}
11711179
});
11721180
}

Libraries/LibWeb/Painting/Paintable.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,18 @@ void Paintable::invalidate_paint_cache() const
10411041
m_cached_paint_data = nullptr;
10421042
}
10431043

1044+
// Descendant boxes paint this box's propagated text decorations from its computed values, so their
1045+
// cached commands go stale when those values change even though the descendants' style is unchanged.
1046+
void Paintable::invalidate_propagated_text_decoration_caches() const
1047+
{
1048+
for_each_in_subtree([](Paintable const& descendant) {
1049+
if (descendant.layout_node().is_text_decoration_propagation_boundary())
1050+
return TraversalDecision::SkipChildrenAndContinue;
1051+
descendant.invalidate_paint_cache();
1052+
return TraversalDecision::Continue;
1053+
});
1054+
}
1055+
10441056
void Paintable::set_cached_commands(PaintPhase phase, ByteBuffer const& commands) const
10451057
{
10461058
if (!m_cached_paint_data)

Libraries/LibWeb/Painting/Paintable.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ class WEB_API Paintable
409409
static constexpr size_t paint_phase_count = to_underlying(PaintPhase::Overlay) + 1;
410410

411411
void invalidate_paint_cache() const;
412+
void invalidate_propagated_text_decoration_caches() const;
412413

413414
bool has_cached_commands(PaintPhase) const;
414415
ReadonlyBytes cached_commands(PaintPhase) const;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<style>
3+
#color { text-decoration: underline red; }
4+
5+
#offset {
6+
text-decoration: underline;
7+
text-decoration-skip-ink: none;
8+
text-underline-offset: 12px;
9+
}
10+
11+
#offset-descendant { text-underline-offset: 0; }
12+
13+
#position {
14+
text-decoration: underline;
15+
text-decoration-skip-ink: none;
16+
text-underline-position: under;
17+
}
18+
19+
#position-descendant { text-underline-position: auto; }
20+
</style>
21+
<span id="color"><div>This text must have a red underline.</div></span>
22+
<span><div>This text must not have an underline.</div></span>
23+
<span id="offset">before <div id="offset-descendant">nested</div> after</span>
24+
<span id="position">before <div id="position-descendant">nested</div> after</span>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<!DOCTYPE html>
2+
<html class="reftest-wait">
3+
<link rel="match" href="../expected/text-decoration-propagation-repaint-ref.html">
4+
<style>
5+
#color { text-decoration: underline transparent; }
6+
#color.on { text-decoration: underline red; }
7+
8+
#removal.on { text-decoration: underline red; }
9+
10+
#offset {
11+
text-decoration: underline;
12+
text-decoration-skip-ink: none;
13+
text-underline-offset: 0;
14+
}
15+
16+
#offset.on { text-underline-offset: 12px; }
17+
#offset-descendant { text-underline-offset: 0; }
18+
19+
#position {
20+
text-decoration: underline;
21+
text-decoration-skip-ink: none;
22+
text-underline-position: auto;
23+
}
24+
25+
#position.on { text-underline-position: under; }
26+
#position-descendant { text-underline-position: auto; }
27+
</style>
28+
<span id="color"><div>This text must have a red underline.</div></span>
29+
<span id="removal" class="on"><div>This text must not have an underline.</div></span>
30+
<span id="offset">before <div id="offset-descendant">nested</div> after</span>
31+
<span id="position">before <div id="position-descendant">nested</div> after</span>
32+
<script>
33+
requestAnimationFrame(() => {
34+
requestAnimationFrame(() => {
35+
for (let id of ["color", "offset", "position"])
36+
document.getElementById(id).classList.add("on");
37+
document.getElementById("removal").classList.remove("on");
38+
requestAnimationFrame(() => {
39+
if (window.internals)
40+
internals.signalTestIsDone("PASS");
41+
else
42+
document.documentElement.classList.remove("reftest-wait");
43+
});
44+
});
45+
});
46+
</script>
47+
</html>

0 commit comments

Comments
 (0)