Files
ladybird/Libraries/LibWeb/CSS/Invalidation/HasMutationInvalidator.cpp
Andreas Kling 85e33738f5 LibWeb: Move :has() element invalidation into the helper
Element exposed a small method that encoded how :has()-affected elements
are marked dirty. Move that policy into CSS::Invalidation alongside the
rest of the :has() mutation invalidation helpers.

This keeps Element focused on DOM state while preserving the existing
subject and non-subject :has() invalidation behavior.
2026-04-29 15:47:23 +02:00

112 lines
4.6 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Invalidation/HasMutationFeatureCollector.h>
#include <LibWeb/CSS/Invalidation/HasMutationInvalidator.h>
#include <LibWeb/CSS/StyleScope.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Node.h>
namespace Web::CSS::Invalidation {
static bool reason_may_affect_has_selectors(DOM::StyleInvalidationReason reason)
{
// :has() selectors match based on DOM state only (structure, attributes, pseudo-classes). Reasons that don't change
// any DOM state can't affect :has() matching, so we can skip scheduling :has() ancestor invalidation.
switch (reason) {
case DOM::StyleInvalidationReason::BaseURLChanged:
case DOM::StyleInvalidationReason::CSSFontLoaded:
case DOM::StyleInvalidationReason::HTMLIFrameElementGeometryChange:
case DOM::StyleInvalidationReason::HTMLObjectElementUpdateLayoutAndChildObjects:
case DOM::StyleInvalidationReason::NavigableSetViewportSize:
case DOM::StyleInvalidationReason::SettingsChange:
return false;
default:
return true;
}
}
static void invalidate_children_affected_by_has_sibling_combinators(DOM::Node& parent)
{
parent.for_each_child_of_type<DOM::Element>([&](auto& element) {
if (element.affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator())
invalidate_element_if_affected_by_has(element);
return IterationDecision::Continue;
});
}
void invalidate_element_if_affected_by_has(DOM::Element& element)
{
if (element.affected_by_has_pseudo_class_in_subject_position())
element.set_needs_style_update(true);
if (element.affected_by_has_pseudo_class_in_non_subject_position())
element.invalidate_style(DOM::StyleInvalidationReason::Other, { { InvalidationSet::Property::Type::PseudoClass, PseudoClass::Has } }, {});
}
static void schedule_has_invalidation_for_child_list_mutation(DOM::Node& parent, DOM::Node& mutation_root, StyleScope& scope)
{
if (!scope.may_have_has_selectors())
return;
auto has_sibling_combinator_has_selectors = scope.may_have_has_selectors_with_relative_selector_that_has_sibling_combinator();
// Sibling-combinator :has() selectors are sensitive to featureless insertions/removals because a plain node can
// still change adjacency and following-sibling relationships.
auto may_affect_has_match = mutation_root.is_character_data()
|| subtree_has_feature_used_in_has_selector(mutation_root, scope)
|| has_sibling_combinator_has_selectors;
if (!may_affect_has_match)
return;
scope.record_pending_has_invalidation_mutation_features(parent, mutation_root, true);
scope.schedule_ancestors_style_invalidation_due_to_presence_of_has(parent);
if (has_sibling_combinator_has_selectors)
invalidate_children_affected_by_has_sibling_combinators(parent);
}
void schedule_has_invalidation_for_node(DOM::Node& node, DOM::StyleInvalidationReason reason)
{
auto is_child_list_mutation = reason == DOM::StyleInvalidationReason::NodeRemove
|| reason == DOM::StyleInvalidationReason::NodeInsertBefore;
// On insertion and removal the mutated node itself is uninteresting to the
// :has() walker (a freshly inserted node has no :has() scope flags yet, and
// a removed node is about to leave the tree). Start the walk at the parent,
// which was in scope before and reliably carries the correct flags.
if (is_child_list_mutation) {
auto* parent = node.parent_or_shadow_host();
if (!parent)
return;
// Walk every scope that can observe the parent, including enclosing and hosted shadow roots, so :has() in
// :host(), ::slotted(), and ::part() selectors can react to the mutation.
parent->for_each_style_scope_which_may_observe_the_node([&](StyleScope& scope) {
schedule_has_invalidation_for_child_list_mutation(*parent, node, scope);
});
return;
}
auto& style_scope = node.style_scope();
if (!style_scope.may_have_has_selectors() || !reason_may_affect_has_selectors(reason))
return;
style_scope.record_pending_has_invalidation_mutation_features(node, node, false);
style_scope.schedule_ancestors_style_invalidation_due_to_presence_of_has(node);
}
void schedule_has_invalidation_for_same_parent_move(DOM::Node& node)
{
auto* parent = node.parent_or_shadow_host();
if (!parent)
return;
parent->for_each_style_scope_which_may_observe_the_node([&](StyleScope& scope) {
schedule_has_invalidation_for_child_list_mutation(*parent, node, scope);
});
}
}