Previously, and according to the spec, `a::part(foo)::before` would be a single CompoundSelector, even though it matches against 3 different targets. This meant some awkward swapping of targets in the middle of matching, and in particular it made `::part()` and `::slotted()` quite hacky, requiring them to track extra data on the MatchContext to then use later. This was scattered around and difficult to follow. Partly inspired by Gecko, this commit instead introduces an invisible PseudoElement combinator. After parsing a selector, we find any CompoundSelectors that contain a pseudo-element and split them up, so that each CompoundSelector only has a single target in the end. Where the pseudo-element was at the start of a CompoundSelector, we insert an invisible universal selector before it to represent its originating element. So now, a CompoundSelector deals with one target, and switching targets is done at the combinator. The one inconsistency is that we match the target of ::slotted() and ::part() in pseudo_element_transition_target(), instead of before then when processing the SimpleSelector. This is to avoid repeating the same computations twice. No outward-facing behaviour changes, though the invalidation metrics have changed.
94 lines
4.2 KiB
C++
94 lines
4.2 KiB
C++
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Function.h>
|
|
#include <AK/TemporaryChange.h>
|
|
#include <LibWeb/CSS/Invalidation/PseudoClassInvalidator.h>
|
|
#include <LibWeb/CSS/SelectorEngine.h>
|
|
#include <LibWeb/CSS/StyleComputer.h>
|
|
#include <LibWeb/CSS/StyleScope.h>
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/DOM/Element.h>
|
|
#include <LibWeb/DOM/ShadowRoot.h>
|
|
|
|
namespace Web::CSS::Invalidation {
|
|
|
|
template<typename StateSlot, typename NewState>
|
|
static void invalidate_style_after_pseudo_class_state_change_impl(CSS::PseudoClass pseudo_class, DOM::Document& document, StateSlot& state_slot, DOM::Node& invalidation_root, NewState new_state)
|
|
{
|
|
auto& root = invalidation_root.root();
|
|
auto shadow_root = is<DOM::ShadowRoot>(root) ? static_cast<DOM::ShadowRoot const*>(&root) : nullptr;
|
|
auto& style_scope = shadow_root ? shadow_root->style_scope() : document.style_scope();
|
|
|
|
auto const& rules = style_scope.get_pseudo_class_rule_cache(pseudo_class);
|
|
|
|
auto& style_computer = document.style_computer();
|
|
auto does_rule_match_on_element = [&](DOM::Element const& element, CSS::MatchingRule const& rule) {
|
|
auto const& selector = rule.selector;
|
|
if (selector.can_use_ancestor_filter() && style_computer.should_reject_with_ancestor_filter(selector))
|
|
return false;
|
|
|
|
SelectorEngine::MatchContext context;
|
|
auto target_pseudo = selector.target_pseudo_element();
|
|
return SelectorEngine::matches(selector, { element, target_pseudo }, {}, context);
|
|
};
|
|
|
|
auto matches_different_set_of_rules_after_state_change = [&](DOM::Element& element) {
|
|
bool result = false;
|
|
rules.for_each_matching_rules({ element }, [&](auto& rules) {
|
|
for (auto& rule : rules) {
|
|
bool before = does_rule_match_on_element(element, rule);
|
|
TemporaryChange change { state_slot, new_state };
|
|
bool after = does_rule_match_on_element(element, rule);
|
|
if (before != after) {
|
|
result = true;
|
|
return IterationDecision::Break;
|
|
}
|
|
}
|
|
return IterationDecision::Continue;
|
|
});
|
|
return result;
|
|
};
|
|
|
|
Function<void(DOM::Node&)> invalidate_affected_elements_recursively = [&](DOM::Node& node) -> void {
|
|
if (node.is_element()) {
|
|
auto& element = static_cast<DOM::Element&>(node);
|
|
style_computer.push_ancestor(element);
|
|
if (element.affected_by_pseudo_class(pseudo_class) && matches_different_set_of_rules_after_state_change(element))
|
|
element.set_needs_style_update(true);
|
|
}
|
|
|
|
node.for_each_child([&](auto& child) {
|
|
invalidate_affected_elements_recursively(child);
|
|
return IterationDecision::Continue;
|
|
});
|
|
|
|
if (node.is_element())
|
|
style_computer.pop_ancestor(static_cast<DOM::Element&>(node));
|
|
};
|
|
|
|
// Seed the ancestor filter with ancestors above the starting node,
|
|
// so that ancestor-dependent selectors can still be correctly rejected.
|
|
for (auto* ancestor = invalidation_root.parent_or_shadow_host_element(); ancestor; ancestor = ancestor->parent_or_shadow_host_element())
|
|
style_computer.push_ancestor(*ancestor);
|
|
|
|
invalidate_affected_elements_recursively(invalidation_root);
|
|
|
|
for (auto* ancestor = invalidation_root.parent_or_shadow_host_element(); ancestor; ancestor = ancestor->parent_or_shadow_host_element())
|
|
style_computer.pop_ancestor(*ancestor);
|
|
}
|
|
|
|
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass pseudo_class, DOM::Document& document, GC::Ptr<DOM::Node>& state_slot, DOM::Node& invalidation_root, GC::Ptr<DOM::Node> new_state)
|
|
{
|
|
invalidate_style_after_pseudo_class_state_change_impl(pseudo_class, document, state_slot, invalidation_root, new_state);
|
|
}
|
|
|
|
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass pseudo_class, DOM::Document& document, GC::Ptr<DOM::Element>& state_slot, DOM::Node& invalidation_root, GC::Ptr<DOM::Element> new_state)
|
|
{
|
|
invalidate_style_after_pseudo_class_state_change_impl(pseudo_class, document, state_slot, invalidation_root, new_state);
|
|
}
|
|
|
|
}
|