Files
ladybird/Libraries/LibWeb/CSS/Invalidation/LanguageInvalidator.cpp
Andreas Kling 59ae7d934a LibWeb: Skip unchanged :has() mutation fanout
Collect concrete features from pending :has() mutations and use them to
avoid re-invalidating non-subject :has() anchors whose matching rules
cannot be affected by the mutation.

Keep conservative behavior for shadow-boundary fanout, structural
sibling changes, and selectors whose relevant features cannot be proven.
For sibling-combinator relative selectors, avoid marking an anchor as
handled until it is actually invalidated, and make the sibling scan
respect anchors skipped by the feature filter for the same mutation.
2026-04-30 00:24:04 +02:00

71 lines
3.1 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Invalidation/LanguageInvalidator.h>
#include <LibWeb/CSS/StyleScope.h>
#include <LibWeb/DOM/CharacterData.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/TraversalDecision.h>
namespace Web::CSS::Invalidation {
static void invalidate_descendants_affected_by_language_or_directionality(DOM::Element& element, bool is_directionality_change)
{
// dir and lang both inherit, so all descendants' :dir() / :lang() matches and direction-dependent layout/text
// need to be recomputed.
element.for_each_shadow_including_inclusive_descendant([is_directionality_change](auto& node) {
if (auto* element = as_if<DOM::Element>(node)) {
if (!is_directionality_change)
element->invalidate_lang_value();
element->set_needs_style_update(true);
}
return TraversalDecision::Continue;
});
// :has(:dir(...)) and :has(:lang(...)) on ancestors aren't keyed on any property the regular invalidation
// plan tracks, so explicitly schedule the :has() ancestor walk here.
element.for_each_style_scope_which_may_observe_the_node([&](CSS::StyleScope& scope) {
auto pseudo_class = is_directionality_change ? CSS::PseudoClass::Dir : CSS::PseudoClass::Lang;
Vector<CSS::InvalidationSet::Property> properties {
{ CSS::InvalidationSet::Property::Type::PseudoClass, pseudo_class },
};
scope.record_pending_has_invalidation_mutation_features(element, properties);
scope.schedule_ancestors_style_invalidation_due_to_presence_of_has(element);
});
}
void invalidate_style_after_language_change(DOM::Element& element)
{
invalidate_descendants_affected_by_language_or_directionality(element, false);
}
void invalidate_style_after_directionality_change(DOM::Element& element)
{
invalidate_descendants_affected_by_language_or_directionality(element, true);
}
void invalidate_style_after_text_directionality_change(DOM::CharacterData& character_data)
{
// dir=auto resolves an element's effective directionality from its text content, so any ancestor with dir=auto
// can flip its :dir() match when this text changes. Recompute style on each such ancestor's subtree and propagate
// :has(:dir(...)) invalidation up its ancestor chain.
for (auto ancestor = character_data.parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->dir() != DOM::Element::Dir::Auto)
continue;
Vector<CSS::InvalidationSet::Property> properties {
{ CSS::InvalidationSet::Property::Type::PseudoClass, CSS::PseudoClass::Dir },
{ CSS::InvalidationSet::Property::Type::PseudoClass, CSS::PseudoClass::Empty },
};
ancestor->for_each_style_scope_which_may_observe_the_node([&](CSS::StyleScope& scope) {
scope.record_pending_has_invalidation_mutation_features(*ancestor, properties);
});
invalidate_descendants_affected_by_language_or_directionality(*ancestor, true);
}
}
}