Skip to content

Commit

Permalink
LibWeb: Invalidate layout if pseudo-element style changes
Browse files Browse the repository at this point in the history
Pseudo-elements' style is only computed while building the layout tree.
This meant that previously, they would not have their style recomputed
in some cases. (Such as when :hover is applied to an ancestor.)

Now, when recomputing an element's style, we also return a full
invalidation if one or more pseudo-elements would exist either before or
after style recomputation.

This heuristic produces some false positives, but no false negatives.
Because pseudo-elements' style is computed during layout building, any
computation done here is then thrown away. So this approach minimises
the amount of wasted style computation. Plus it's simple, until we have
data on what approach would be faster.

This fixes the Acid2 nose becoming blue when the .nose div is hovered.
  • Loading branch information
AtkinsSJ committed Jul 29, 2024
1 parent 98a447c commit 559ecc2
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Hi Not hovering: 16
Hovering: 78
Not hovering: 16
33 changes: 33 additions & 0 deletions Tests/LibWeb/Text/input/css/update-pseudo-elements-on-hover.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<style>
.outer {
height: 100px;
}
.inner {
display: inline-block;
}
.inner::before {
content: "Hi";
background-color: red;
}
.outer:hover .inner::before {
content: "Long text";
background-color: lime;
}
</style>
<script src="../include.js"></script>
<div class="outer"><div class="inner"></div></div>
<script>
test(() => {
const inner = document.querySelector('.inner');
println('Not hovering: ' + inner.clientWidth);

// Move mouse over .outer
internals.movePointerTo(80, 80);
println('Hovering: ' + inner.clientWidth);

// Move mouse away again
internals.movePointerTo(200, 200);
println('Not hovering: ' + inner.clientWidth);
});
</script>
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/CSS/StyleInvalidation.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct RequiredInvalidationAfterStyleChange {
}

[[nodiscard]] bool is_none() const { return !repaint && !rebuild_stacking_context_tree && !relayout && !rebuild_layout_tree; }
[[nodiscard]] bool is_full() const { return repaint && rebuild_stacking_context_tree && relayout && rebuild_layout_tree; }
static RequiredInvalidationAfterStyleChange full() { return { true, true, true, true }; }
};

Expand Down
32 changes: 31 additions & 1 deletion Userland/Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@ CSS::RequiredInvalidationAfterStyleChange Element::recompute_style()
set_needs_style_update(false);
VERIFY(parent());

auto new_computed_css_values = document().style_computer().compute_style(*this);
auto& style_computer = document().style_computer();
auto new_computed_css_values = style_computer.compute_style(*this);

// Tables must not inherit -libweb-* values for text-align.
// FIXME: Find the spec for this.
Expand All @@ -550,6 +551,35 @@ CSS::RequiredInvalidationAfterStyleChange Element::recompute_style()
else
invalidation = CSS::RequiredInvalidationAfterStyleChange::full();

// Any document change that can cause this element's style to change, could also affect its pseudo-elements.
// So determine if any pseudo-elements currently exist, or should now exist, and if so, invalidate everything.
// (If we're already invalidating everything, we don't need to do further checks for this.)
if (!invalidation.is_full()) {
bool pseudo_elements_dirty = false;

if (m_pseudo_element_data) {
for (auto& pseudo_element : *m_pseudo_element_data) {
if (pseudo_element.layout_node) {
pseudo_elements_dirty = true;
break;
}
}
}

if (!pseudo_elements_dirty) {
for (auto i = 0; i < to_underlying(CSS::Selector::PseudoElement::Type::KnownPseudoElementCount); i++) {
auto style = style_computer.compute_pseudo_element_style_if_needed(*this, static_cast<CSS::Selector::PseudoElement::Type>(i));
if (style) {
pseudo_elements_dirty = true;
break;
}
}
}

if (pseudo_elements_dirty)
invalidation = CSS::RequiredInvalidationAfterStyleChange::full();
}

if (invalidation.is_none())
return invalidation;

Expand Down

0 comments on commit 559ecc2

Please sign in to comment.