mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-19 00:01:59 +00:00
When @media rules change match state, avoid treating that as a whole-document stylesheet change. Record the rules under the query that became effective or ineffective, build the normal stylesheet invalidation set from those rules, and invalidate only matching elements. Keep broad invalidation for rule kinds whose effects are not selector-targetable. Preserve cascade-wide fallout by reusing stylesheet-change shadow effect analysis for broad invalidations inside shadow roots. This keeps :host and slotted content current when active rules in the same shadow scope can match there. Also report an imported stylesheet's owner rule when the imported sheet's own media gate changes. Layered @import rules can affect layer ordering even when the imported sheet contributes no rules, so they need the same broad invalidation treatment as other cascade-wide rules. Add viewport resize coverage for media query breakpoints, broad shadow-root media invalidation, and empty layered imports whose media gate changes layer order.
924 lines
43 KiB
C++
924 lines
43 KiB
C++
/*
|
|
* Copyright (c) 2026, Andreas Kling <andreas@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/CSS/CSSImportRule.h>
|
|
#include <LibWeb/CSS/CSSKeyframesRule.h>
|
|
#include <LibWeb/CSS/CSSNestedDeclarations.h>
|
|
#include <LibWeb/CSS/CSSStyleRule.h>
|
|
#include <LibWeb/CSS/CSSStyleSheet.h>
|
|
#include <LibWeb/CSS/ComputedProperties.h>
|
|
#include <LibWeb/CSS/Invalidation/InvalidationSetMatcher.h>
|
|
#include <LibWeb/CSS/Parser/Parser.h>
|
|
#include <LibWeb/CSS/SelectorEngine.h>
|
|
#include <LibWeb/CSS/StyleScope.h>
|
|
#include <LibWeb/CSS/StyleSheetInvalidation.h>
|
|
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/DOM/Element.h>
|
|
#include <LibWeb/DOM/ShadowRoot.h>
|
|
#include <LibWeb/HTML/HTMLSlotElement.h>
|
|
#include <LibWeb/HTML/HTMLTableCellElement.h>
|
|
#include <LibWeb/HTML/HTMLTableElement.h>
|
|
|
|
namespace Web::CSS {
|
|
|
|
bool selector_may_match_light_dom_under_shadow_host(Selector const& selector)
|
|
{
|
|
if (selector.is_slotted())
|
|
return true;
|
|
|
|
if (!selector.contains_pseudo_class(PseudoClass::Host))
|
|
return false;
|
|
|
|
// A bare :host selector only targets the host itself, and pseudo-element transitions such as :host::part() stay
|
|
// anchored on that same host. We only escape to light-DOM once the selector keeps walking through a real DOM
|
|
// combinator after matching :host.
|
|
for (size_t i = 1; i < selector.compound_selectors().size(); ++i) {
|
|
if (selector.compound_selectors()[i].combinator != Selector::Combinator::PseudoElement)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool selector_may_match_shadow_host(Selector const&);
|
|
static bool selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(Selector const&);
|
|
|
|
static bool pseudo_class_has_positive_selector_list_arguments(PseudoClass pseudo_class)
|
|
{
|
|
return first_is_one_of(pseudo_class, PseudoClass::Is, PseudoClass::Where);
|
|
}
|
|
|
|
static bool simple_selector_may_match_shadow_host(Selector::SimpleSelector const& simple_selector)
|
|
{
|
|
if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass)
|
|
return false;
|
|
|
|
auto const& pseudo_class = simple_selector.pseudo_class();
|
|
if (pseudo_class.type == PseudoClass::Host)
|
|
return true;
|
|
|
|
if (!pseudo_class_has_positive_selector_list_arguments(pseudo_class.type))
|
|
return false;
|
|
|
|
return any_of(pseudo_class.argument_selector_list, selector_may_match_shadow_host);
|
|
}
|
|
|
|
static bool selector_may_match_shadow_host(Selector const& selector)
|
|
{
|
|
return any_of(selector.compound_selectors(), [](auto const& compound_selector) {
|
|
return any_of(compound_selector.simple_selectors, [](auto const& simple_selector) {
|
|
return simple_selector_may_match_shadow_host(simple_selector);
|
|
});
|
|
});
|
|
}
|
|
|
|
static bool compound_selector_may_match_shadow_host(Selector::CompoundSelector const& compound_selector)
|
|
{
|
|
return any_of(compound_selector.simple_selectors, simple_selector_may_match_shadow_host);
|
|
}
|
|
|
|
static bool simple_selector_may_match_light_dom_outside_shadow_host(Selector::SimpleSelector const& simple_selector)
|
|
{
|
|
if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass)
|
|
return false;
|
|
|
|
auto const& pseudo_class = simple_selector.pseudo_class();
|
|
if (!pseudo_class_has_positive_selector_list_arguments(pseudo_class.type))
|
|
return false;
|
|
|
|
return any_of(pseudo_class.argument_selector_list, selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list);
|
|
}
|
|
|
|
static bool compound_selector_may_match_light_dom_outside_shadow_host(Selector::CompoundSelector const& compound_selector)
|
|
{
|
|
return any_of(compound_selector.simple_selectors, simple_selector_may_match_light_dom_outside_shadow_host);
|
|
}
|
|
|
|
static bool selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(Selector const& selector)
|
|
{
|
|
auto const& compound_selectors = selector.compound_selectors();
|
|
for (size_t i = 0; i < compound_selectors.size(); ++i) {
|
|
if (compound_selector_may_match_light_dom_outside_shadow_host(compound_selectors[i]))
|
|
return true;
|
|
|
|
if (!compound_selector_may_match_shadow_host(compound_selectors[i]))
|
|
continue;
|
|
|
|
for (size_t j = i + 1; j < compound_selectors.size(); ++j) {
|
|
if (first_is_one_of(compound_selectors[j].combinator, Selector::Combinator::NextSibling, Selector::Combinator::SubsequentSibling))
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool selector_may_match_light_dom_outside_shadow_host(Selector const& selector)
|
|
{
|
|
return selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(selector);
|
|
}
|
|
|
|
bool selector_may_match_light_dom_under_shadow_host(StringView selector_text)
|
|
{
|
|
CSS::Parser::ParsingParams parsing_params;
|
|
auto selectors = parse_selector(parsing_params, selector_text);
|
|
if (!selectors.has_value() || selectors->size() != 1)
|
|
return false;
|
|
return selector_may_match_light_dom_under_shadow_host(selectors->first());
|
|
}
|
|
|
|
bool selector_may_match_light_dom_outside_shadow_host(StringView selector_text)
|
|
{
|
|
CSS::Parser::ParsingParams parsing_params;
|
|
auto selectors = parse_selector(parsing_params, selector_text);
|
|
if (!selectors.has_value() || selectors->size() != 1)
|
|
return false;
|
|
return selector_may_match_light_dom_outside_shadow_host(selectors->first());
|
|
}
|
|
|
|
static bool is_universal_only_compound(Selector::CompoundSelector const& compound_selector)
|
|
{
|
|
if (compound_selector.simple_selectors.is_empty())
|
|
return false;
|
|
for (auto const& simple_selector : compound_selector.simple_selectors) {
|
|
if (simple_selector.type != Selector::SimpleSelector::Type::Universal)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool is_pseudo_element_targeting_compound(Selector::CompoundSelector const& compound_selector)
|
|
{
|
|
return !compound_selector.simple_selectors.is_empty()
|
|
&& compound_selector.simple_selectors.first().type == Selector::SimpleSelector::Type::PseudoElement;
|
|
}
|
|
|
|
static bool rule_requires_broad_add_or_remove_invalidation(CSSRule const&);
|
|
|
|
// :has() in the rightmost set filters by an element's previously recorded "affected by :has()" flags. Those flags
|
|
// are not yet set for a brand-new rule that has never been matched, so :has() on its own can't narrow the walk.
|
|
// Treat a rightmost set whose only targetable feature is :has() as if it had no targetable features, so we fall
|
|
// back to whole-subtree invalidation.
|
|
static bool rightmost_set_has_targetable_features(InvalidationSet const& rightmost_set)
|
|
{
|
|
bool found_targetable = false;
|
|
rightmost_set.for_each_property([&](InvalidationSet::Property const& property) {
|
|
if (property.type == InvalidationSet::Property::Type::PseudoClass
|
|
&& property.value.get<PseudoClass>() == PseudoClass::Has) {
|
|
return IterationDecision::Continue;
|
|
}
|
|
found_targetable = true;
|
|
return IterationDecision::Break;
|
|
});
|
|
return found_targetable;
|
|
}
|
|
|
|
struct AnchorInvalidationRule {
|
|
InvalidationSet anchor_set;
|
|
RefPtr<Selector> anchor_selector;
|
|
GC::Ptr<CSSStyleSheet const> style_sheet_for_rule;
|
|
};
|
|
|
|
enum class AllowSkippingUntargetablePseudoElementCompounds {
|
|
No,
|
|
Yes,
|
|
};
|
|
|
|
// If the leading compounds (everything except the rightmost) of `compound_selectors` produce a usable invalidation
|
|
// set, return an anchor rule built from them.
|
|
static Optional<AnchorInvalidationRule> try_build_anchor_for_leading_compounds(Vector<Selector::CompoundSelector> const& compound_selectors, StyleInvalidationData& throwaway_data, GC::Ptr<CSSStyleSheet const> style_sheet_for_rule, AllowSkippingUntargetablePseudoElementCompounds allow_skipping_untargetable_pseudo_element_compounds = AllowSkippingUntargetablePseudoElementCompounds::No)
|
|
{
|
|
if (compound_selectors.size() < 2)
|
|
return {};
|
|
|
|
auto anchor_compound_count = compound_selectors.size() - 1;
|
|
while (anchor_compound_count > 0) {
|
|
auto const& rightmost_anchor = compound_selectors[anchor_compound_count - 1];
|
|
|
|
InvalidationSet anchor_set;
|
|
for (auto const& simple : rightmost_anchor.simple_selectors)
|
|
build_invalidation_sets_for_simple_selector(simple, anchor_set, ExcludePropertiesNestedInNotPseudoClass::No, throwaway_data, InsideNthChildPseudoClass::No);
|
|
|
|
if (anchor_set.has_properties()) {
|
|
Vector<Selector::CompoundSelector> anchor_compound_selectors;
|
|
anchor_compound_selectors.ensure_capacity(anchor_compound_count);
|
|
for (size_t i = 0; i < anchor_compound_count; ++i)
|
|
anchor_compound_selectors.append(compound_selectors[i]);
|
|
|
|
RefPtr<Selector> anchor_selector;
|
|
if (anchor_compound_selectors.size() > 1)
|
|
anchor_selector = Selector::create(move(anchor_compound_selectors));
|
|
|
|
return AnchorInvalidationRule { move(anchor_set), move(anchor_selector), style_sheet_for_rule };
|
|
}
|
|
|
|
if (allow_skipping_untargetable_pseudo_element_compounds == AllowSkippingUntargetablePseudoElementCompounds::No
|
|
|| !is_pseudo_element_targeting_compound(rightmost_anchor)) {
|
|
return {};
|
|
}
|
|
|
|
--anchor_compound_count;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
static void extend_style_sheet_invalidation_set_with_selectors(StyleSheetInvalidationSet& result, SelectorList const& selectors, GC::Ptr<CSSStyleSheet const> style_sheet_for_rule)
|
|
{
|
|
StyleInvalidationData throwaway_data;
|
|
|
|
for (auto const& selector : selectors) {
|
|
result.may_match_light_dom_under_shadow_host |= selector_may_match_light_dom_under_shadow_host(*selector);
|
|
result.may_match_light_dom_outside_shadow_host |= selector_may_match_light_dom_outside_shadow_host(*selector);
|
|
result.may_match_shadow_host |= selector->contains_pseudo_class(PseudoClass::Host);
|
|
|
|
auto const& compound_selectors = selector->compound_selectors();
|
|
if (compound_selectors.is_empty())
|
|
continue;
|
|
|
|
auto const& rightmost = compound_selectors.last();
|
|
|
|
InvalidationSet rightmost_set;
|
|
for (auto const& simple : rightmost.simple_selectors)
|
|
build_invalidation_sets_for_simple_selector(simple, rightmost_set, ExcludePropertiesNestedInNotPseudoClass::No, throwaway_data, InsideNthChildPseudoClass::No);
|
|
|
|
if (rightmost_set_has_targetable_features(rightmost_set)) {
|
|
result.invalidation_set.include_all_from(rightmost_set);
|
|
continue;
|
|
}
|
|
|
|
// OPTIMIZATION: When the rightmost compound has no class/tag/id/attr feature but does have a
|
|
// structural-position pseudo-class (:first-child, :last-child, or :only-child)
|
|
// as its only "feature", we can target the boundary children instead of falling
|
|
// back to a whole-subtree invalidation. We restrict this to the case with no
|
|
// other targetable feature so that selectors like `.foo:first-child` keep
|
|
// matching only `.foo` elements rather than every first-child in the document.
|
|
bool rightmost_has_only_structural_pseudo = false;
|
|
for (auto const& simple : rightmost.simple_selectors) {
|
|
if (simple.type != Selector::SimpleSelector::Type::PseudoClass)
|
|
continue;
|
|
auto pseudo_class_type = simple.pseudo_class().type;
|
|
if (pseudo_class_type == PseudoClass::FirstChild
|
|
|| pseudo_class_type == PseudoClass::LastChild
|
|
|| pseudo_class_type == PseudoClass::OnlyChild) {
|
|
rightmost_has_only_structural_pseudo = true;
|
|
break;
|
|
}
|
|
}
|
|
if (rightmost_has_only_structural_pseudo) {
|
|
for (auto const& simple : rightmost.simple_selectors) {
|
|
if (simple.type != Selector::SimpleSelector::Type::PseudoClass)
|
|
continue;
|
|
auto pseudo_class_type = simple.pseudo_class().type;
|
|
if (pseudo_class_type == PseudoClass::FirstChild
|
|
|| pseudo_class_type == PseudoClass::LastChild
|
|
|| pseudo_class_type == PseudoClass::OnlyChild) {
|
|
result.invalidation_set.set_needs_invalidate_pseudo_class(pseudo_class_type);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// The rightmost compound has no targetable properties on its own, but we can still avoid a whole-subtree
|
|
// invalidation if the leading compounds carry an anchor invalidation set that we can match against.
|
|
if (is_pseudo_element_targeting_compound(rightmost)) {
|
|
// Pseudo-element normalization can leave untargetable pseudo-element-transition compounds before the
|
|
// final pseudo-element, e.g. `x-host::part(foo):dir(rtl)::before`. Anchor those rules on the nearest
|
|
// targetable leading compound instead of broadening to the whole subtree.
|
|
if (auto anchor = try_build_anchor_for_leading_compounds(compound_selectors, throwaway_data, style_sheet_for_rule, AllowSkippingUntargetablePseudoElementCompounds::Yes); anchor.has_value()) {
|
|
result.pseudo_element_rules.append({
|
|
.anchor_set = move(anchor->anchor_set),
|
|
.anchor_selector = move(anchor->anchor_selector),
|
|
.style_sheet_for_rule = anchor->style_sheet_for_rule,
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (is_universal_only_compound(rightmost)) {
|
|
if (auto anchor = try_build_anchor_for_leading_compounds(compound_selectors, throwaway_data, style_sheet_for_rule); anchor.has_value()) {
|
|
result.trailing_universal_rules.append({
|
|
.anchor_set = move(anchor->anchor_set),
|
|
.anchor_selector = move(anchor->anchor_selector),
|
|
.combinator = rightmost.combinator,
|
|
.style_sheet_for_rule = anchor->style_sheet_for_rule,
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
|
|
result.invalidation_set.set_needs_invalidate_whole_subtree();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void extend_style_sheet_invalidation_set_with_style_rule(StyleSheetInvalidationSet& result, CSSStyleRule const& style_rule)
|
|
{
|
|
auto* style_sheet_for_rule = const_cast<CSSStyleRule&>(style_rule).parent_style_sheet();
|
|
extend_style_sheet_invalidation_set_with_selectors(result, style_rule.absolutized_selectors(), style_sheet_for_rule);
|
|
}
|
|
|
|
void extend_style_sheet_invalidation_set_with_rule(StyleSheetInvalidationSet& result, CSSRule const& rule)
|
|
{
|
|
if (auto const* style_rule = as_if<CSSStyleRule>(rule)) {
|
|
extend_style_sheet_invalidation_set_with_style_rule(result, *style_rule);
|
|
return;
|
|
}
|
|
|
|
if (auto const* nested_declarations = as_if<CSSNestedDeclarations>(rule)) {
|
|
auto* style_sheet_for_rule = const_cast<CSSNestedDeclarations&>(*nested_declarations).parent_style_sheet();
|
|
extend_style_sheet_invalidation_set_with_selectors(result, nested_declarations->absolutized_selectors(), style_sheet_for_rule);
|
|
return;
|
|
}
|
|
|
|
if (rule_requires_broad_add_or_remove_invalidation(rule))
|
|
result.invalidation_set.set_needs_invalidate_whole_subtree();
|
|
}
|
|
|
|
static GC::Ptr<DOM::Element const> shadow_host_for_targeted_shadow_root_invalidation(DOM::Node const& root)
|
|
{
|
|
if (auto const* shadow_root = as_if<DOM::ShadowRoot>(root))
|
|
return shadow_root->host();
|
|
return nullptr;
|
|
}
|
|
|
|
template<typename Rule>
|
|
static bool element_matches_anchor_rule(DOM::Element const& element, Rule const& rule, GC::Ptr<DOM::Element const> shadow_host, GC::Ptr<DOM::ShadowRoot const> rule_shadow_root)
|
|
{
|
|
if (!Invalidation::element_matches_any_invalidation_set_property(element, rule.anchor_set))
|
|
return false;
|
|
if (rule.anchor_selector) {
|
|
SelectorEngine::MatchContext context {
|
|
.style_sheet_for_rule = rule.style_sheet_for_rule,
|
|
.rule_shadow_root = rule_shadow_root,
|
|
};
|
|
if (!SelectorEngine::matches(*rule.anchor_selector, element, shadow_host, context))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void apply_trailing_universal_combinator(Selector::Combinator combinator, DOM::Element& anchor, DOM::Node& target_root)
|
|
{
|
|
switch (combinator) {
|
|
case Selector::Combinator::ImmediateChild:
|
|
target_root.for_each_child_of_type<DOM::Element>([](DOM::Element& child) {
|
|
child.set_needs_style_update(true);
|
|
return IterationDecision::Continue;
|
|
});
|
|
break;
|
|
case Selector::Combinator::Descendant:
|
|
target_root.for_each_in_subtree_of_type<DOM::Element>([](DOM::Element& descendant) {
|
|
descendant.set_needs_style_update(true);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
break;
|
|
case Selector::Combinator::NextSibling:
|
|
if (auto* sibling = anchor.next_element_sibling())
|
|
sibling->set_needs_style_update(true);
|
|
break;
|
|
case Selector::Combinator::SubsequentSibling:
|
|
for (auto* sibling = anchor.next_element_sibling(); sibling; sibling = sibling->next_element_sibling())
|
|
sibling->set_needs_style_update(true);
|
|
break;
|
|
case Selector::Combinator::Column:
|
|
// The column combinator relates a <col>/<colgroup> anchor to the table cells in the represented column. We do
|
|
// not yet have a more precise invalidation primitive for column membership, so keep the work bounded to the
|
|
// current table instead of dropping the invalidation.
|
|
for (auto* ancestor = &anchor; ancestor; ancestor = ancestor->parent_element()) {
|
|
if (auto* table = as_if<HTML::HTMLTableElement>(*ancestor)) {
|
|
table->for_each_in_subtree_of_type<HTML::HTMLTableCellElement>([](HTML::HTMLTableCellElement& cell) {
|
|
cell.set_needs_style_update(true);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case Selector::Combinator::None:
|
|
break;
|
|
case Selector::Combinator::PseudoElement:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Walk `root` once and, for each element, check the primary invalidation set plus every anchor rule in one pass. Doing
|
|
// this as a single traversal instead of one walk per rule kind keeps the dirtying cost proportional to the tree size
|
|
// rather than to tree_size * rule_count.
|
|
static void invalidate_elements_matching_invalidation_set_and_anchor_rules(
|
|
DOM::Node& root,
|
|
StyleSheetInvalidationSet const& result,
|
|
GC::Ptr<DOM::Element const> shadow_host,
|
|
GC::Ptr<DOM::ShadowRoot const> rule_shadow_root)
|
|
{
|
|
auto const& invalidation_set = result.invalidation_set;
|
|
bool const has_primary_set = invalidation_set.has_properties();
|
|
bool const has_anchor_rules = !result.pseudo_element_rules.is_empty() || !result.trailing_universal_rules.is_empty();
|
|
if (!has_primary_set && !has_anchor_rules)
|
|
return;
|
|
|
|
root.for_each_in_inclusive_subtree_of_type<DOM::Element>([&](DOM::Element& element) {
|
|
if (has_primary_set && !element.needs_style_update()
|
|
&& Invalidation::element_matches_any_invalidation_set_property(element, invalidation_set)) {
|
|
element.set_needs_style_update(true);
|
|
}
|
|
|
|
if (!element.needs_style_update()) {
|
|
for (auto const& rule : result.pseudo_element_rules) {
|
|
if (element_matches_anchor_rule(element, rule, shadow_host, rule_shadow_root)) {
|
|
element.set_needs_style_update(true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trailing-universal rules dirty *other* elements (siblings or children of the anchor), so we still have to
|
|
// check them even if `element` is already marked dirty.
|
|
for (auto const& rule : result.trailing_universal_rules) {
|
|
if (element_matches_anchor_rule(element, rule, shadow_host, rule_shadow_root))
|
|
apply_trailing_universal_combinator(rule.combinator, element, element);
|
|
}
|
|
|
|
return TraversalDecision::Continue;
|
|
});
|
|
}
|
|
|
|
static bool slot_or_ancestor_needs_style_update(HTML::HTMLSlotElement const& slot, DOM::ShadowRoot const& shadow_root)
|
|
{
|
|
for (auto const* node = static_cast<DOM::Node const*>(&slot); node && node != &shadow_root; node = node->parent_node()) {
|
|
if (node->needs_style_update())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Slotted light-DOM nodes inherit from their assigned <slot>. If a targeted shadow-root invalidation dirties the slot
|
|
// or one of its ancestors, we must also dirty the flattened assignees outside the shadow subtree.
|
|
void invalidate_assigned_elements_for_dirty_slots(DOM::ShadowRoot& shadow_root)
|
|
{
|
|
shadow_root.for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&shadow_root](HTML::HTMLSlotElement& slot) {
|
|
if (!slot_or_ancestor_needs_style_update(slot, shadow_root))
|
|
return TraversalDecision::Continue;
|
|
for (auto const& assigned_element : slot.assigned_elements({ .flatten = true }))
|
|
assigned_element->set_needs_style_update(true);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
}
|
|
|
|
void invalidate_root_for_style_sheet_change(DOM::Node& root, StyleSheetInvalidationSet const& result, DOM::StyleInvalidationReason reason, bool force_broad_invalidation)
|
|
{
|
|
auto const& invalidation_set = result.invalidation_set;
|
|
if (force_broad_invalidation || invalidation_set.needs_invalidate_whole_subtree()) {
|
|
root.invalidate_style(reason);
|
|
if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
|
|
shadow_root->for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([](HTML::HTMLSlotElement& slot) {
|
|
slot.set_needs_style_update(true);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
invalidate_assigned_elements_for_dirty_slots(*shadow_root);
|
|
|
|
if (auto* host = shadow_root->host()) {
|
|
// Broad shadow-root mutations are only allowed to escape the shadow tree when the stylesheet can
|
|
// actually reach host-side nodes. Keep host-contained escapes bounded to the host subtree; a
|
|
// layer-order-only change, for example, still needs a full restyle inside the shadow tree, but it
|
|
// should not turn into a document-wide invalidation for unrelated light-DOM. Selectors with
|
|
// host-side sibling reach still need a root-side invalidation.
|
|
if (result.may_match_light_dom_outside_shadow_host) {
|
|
host->root().invalidate_style(reason);
|
|
} else if (result.may_match_light_dom_under_shadow_host) {
|
|
host->invalidate_style(reason);
|
|
} else if (result.may_match_shadow_host) {
|
|
host->invalidate_style(reason);
|
|
shadow_root->set_needs_style_update(true);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto* rule_shadow_root = as_if<DOM::ShadowRoot>(root);
|
|
invalidate_elements_matching_invalidation_set_and_anchor_rules(
|
|
root, result, shadow_host_for_targeted_shadow_root_invalidation(root), rule_shadow_root);
|
|
|
|
if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
|
|
invalidate_assigned_elements_for_dirty_slots(*shadow_root);
|
|
|
|
if (auto* host = shadow_root->host()) {
|
|
// Slotted and host-contained :host selectors only need to walk the current host's light-DOM subtree.
|
|
// :host selectors with sibling combinators can escape to siblings or other nodes rooted alongside the host,
|
|
// so they still need the broader host-root walk below.
|
|
if (result.may_match_light_dom_outside_shadow_host) {
|
|
invalidate_elements_matching_invalidation_set_and_anchor_rules(host->root(), result, host, shadow_root);
|
|
} else if (result.may_match_light_dom_under_shadow_host) {
|
|
invalidate_elements_matching_invalidation_set_and_anchor_rules(*host, result, host, shadow_root);
|
|
} else if (result.may_match_shadow_host) {
|
|
bool host_or_shadow_tree_needs_style_update = false;
|
|
if (Invalidation::element_matches_any_invalidation_set_property(*host, invalidation_set))
|
|
host_or_shadow_tree_needs_style_update = true;
|
|
for (auto const& rule : result.pseudo_element_rules) {
|
|
if (element_matches_anchor_rule(*host, rule, host, shadow_root)) {
|
|
host_or_shadow_tree_needs_style_update = true;
|
|
break;
|
|
}
|
|
}
|
|
if (host_or_shadow_tree_needs_style_update) {
|
|
host->set_needs_style_update(true);
|
|
host->invalidate_style(reason);
|
|
// A targeted :host rule can change inherited values seen by both shadow descendants and the
|
|
// host's light-DOM subtree even when no selector directly matches there. Mark both subtrees dirty
|
|
// so recursive style update descends into those inherited-value dependents too.
|
|
shadow_root->set_needs_style_update(true);
|
|
}
|
|
for (auto const& rule : result.trailing_universal_rules) {
|
|
// Host-anchored trailing-universal selectors such as :host > * and :host * target the host's
|
|
// light-DOM tree, not descendants inside the shadow root. Keep the host as the selector anchor
|
|
// and dirty host-side nodes from there.
|
|
if (element_matches_anchor_rule(*host, rule, host, shadow_root))
|
|
apply_trailing_universal_combinator(rule.combinator, *host, *host);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void add_shadow_root_stylesheet_effects_for_broad_invalidation(DOM::Node& root, StyleSheetInvalidationSet& result, bool requires_broad_invalidation)
|
|
{
|
|
if (!requires_broad_invalidation)
|
|
return;
|
|
|
|
auto* shadow_root = as_if<DOM::ShadowRoot>(root);
|
|
if (!shadow_root)
|
|
return;
|
|
|
|
auto effects = determine_shadow_root_stylesheet_effects(*shadow_root);
|
|
result.may_match_shadow_host |= effects.may_match_shadow_host;
|
|
result.may_match_light_dom_under_shadow_host |= effects.may_match_light_dom_under_shadow_host;
|
|
result.may_match_light_dom_outside_shadow_host |= effects.may_match_light_dom_outside_shadow_host;
|
|
}
|
|
|
|
static bool rule_requires_broad_add_or_remove_invalidation(CSSRule const& rule)
|
|
{
|
|
if (auto const* import_rule = as_if<CSSImportRule>(rule))
|
|
return import_rule->layer_name().has_value();
|
|
|
|
switch (rule.type()) {
|
|
case CSSRule::Type::Property:
|
|
case CSSRule::Type::CounterStyle:
|
|
case CSSRule::Type::LayerBlock:
|
|
case CSSRule::Type::LayerStatement:
|
|
// @font-feature-values changes how font-variant-alternates resolves at computed-style time, so
|
|
// a declaration-time mutation still requires a whole-subtree restyle.
|
|
case CSSRule::Type::FontFeatureValues:
|
|
return true;
|
|
// OPTIMIZATION: @font-face declares a resource whose effect on computed style is deferred until
|
|
// the font actually loads (handled by CSSFontLoaded). Adding or removing the
|
|
// at-rule itself doesn't change any element's computed style.
|
|
case CSSRule::Type::FontFace:
|
|
// OPTIMIZATION: @keyframes rules only matter for elements that already reference the named
|
|
// animation. Sheet add/remove handles each contained @keyframes rule with a
|
|
// targeted invalidation (see invalidate_root_for_keyframes_rules_in_sheet)
|
|
// instead of forcing a whole-subtree restyle here.
|
|
case CSSRule::Type::Keyframes:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void invalidate_style_for_stylesheet_change(DOM::Node& document_or_shadow_root, CSSStyleSheet const& sheet, DOM::StyleInvalidationReason reason)
|
|
{
|
|
if (sheet.rules().length() == 0) {
|
|
// NOTE: If the added sheet has no rules, we don't have to invalidate anything.
|
|
return;
|
|
}
|
|
|
|
if (auto* shadow_root = as_if<DOM::ShadowRoot>(document_or_shadow_root)) {
|
|
shadow_root->style_scope().invalidate_rule_cache();
|
|
} else {
|
|
document_or_shadow_root.document().style_scope().invalidate_rule_cache();
|
|
}
|
|
|
|
if (!document_or_shadow_root.is_shadow_root() && document_or_shadow_root.entire_subtree_needs_style_update()) {
|
|
// NOTE: If the entire subtree is already marked for style update,
|
|
// there's no point spending time building invalidation sets.
|
|
// Shadow roots are special: :host(...) and ::slotted(...) can
|
|
// still require follow-up invalidation outside that subtree.
|
|
return;
|
|
}
|
|
|
|
// OPTIMIZATION: Build the targeted invalidation set and check for broad-invalidation-triggering rule kinds in
|
|
// a single walk over the sheet's effective rules, so the sheet's @media gates evaluate once and
|
|
// every rule is visited once instead of twice.
|
|
StyleSheetInvalidationSet invalidation_set_result;
|
|
bool sheet_contains_broad_invalidation_rule = false;
|
|
sheet.for_each_effective_rule(TraversalOrder::Preorder, [&](CSSRule const& rule) {
|
|
// Add/remove invalidation still has to look at effective non-style rules such as @keyframes or @layer, but
|
|
// inactive top-level media sheets should contribute nothing at all here.
|
|
if (!sheet_contains_broad_invalidation_rule && rule_requires_broad_add_or_remove_invalidation(rule))
|
|
sheet_contains_broad_invalidation_rule = true;
|
|
if (invalidation_set_result.invalidation_set.needs_invalidate_whole_subtree())
|
|
return;
|
|
if (auto const* style_rule = as_if<CSSStyleRule>(rule))
|
|
extend_style_sheet_invalidation_set_with_style_rule(invalidation_set_result, *style_rule);
|
|
});
|
|
|
|
bool requires_broad_invalidation = invalidation_set_result.invalidation_set.needs_invalidate_whole_subtree()
|
|
|| sheet_contains_broad_invalidation_rule;
|
|
add_shadow_root_stylesheet_effects_for_broad_invalidation(document_or_shadow_root, invalidation_set_result, requires_broad_invalidation);
|
|
|
|
invalidate_root_for_style_sheet_change(document_or_shadow_root, invalidation_set_result, reason, requires_broad_invalidation);
|
|
|
|
// OPTIMIZATION: Walk @keyframes rules in the new sheet and dirty only the elements that already
|
|
// reference each animation-name, so a sheet add carrying @keyframes does not have
|
|
// to escalate to a whole-subtree restyle.
|
|
invalidate_root_for_keyframes_rules_in_sheet(document_or_shadow_root, sheet);
|
|
}
|
|
|
|
void invalidate_owners_for_inserted_style_rule(CSSStyleSheet const& style_sheet, CSSStyleRule const& style_rule, DOM::StyleInvalidationReason reason)
|
|
{
|
|
StyleSheetInvalidationSet invalidation_set;
|
|
extend_style_sheet_invalidation_set_with_style_rule(invalidation_set, style_rule);
|
|
|
|
for (auto& document_or_shadow_root : style_sheet.owning_documents_or_shadow_roots()) {
|
|
auto& style_scope = document_or_shadow_root->is_shadow_root()
|
|
? as<DOM::ShadowRoot>(*document_or_shadow_root).style_scope()
|
|
: document_or_shadow_root->document().style_scope();
|
|
style_scope.invalidate_rule_cache();
|
|
|
|
// A dirty shadow subtree can still need follow-up invalidation on the host side for :host(...) and
|
|
// ::slotted(...) matches, so we don't skip shadow roots even when their entire subtree is already marked.
|
|
invalidate_root_for_style_sheet_change(*document_or_shadow_root, invalidation_set, reason);
|
|
}
|
|
}
|
|
|
|
static void for_each_tree_affected_by_shadow_root_stylesheet_change(
|
|
DOM::Node& root,
|
|
bool include_host,
|
|
bool include_light_dom_under_shadow_host,
|
|
Function<void(DOM::Node&)> const& callback)
|
|
{
|
|
callback(root);
|
|
|
|
if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
|
|
auto* host = shadow_root->host();
|
|
if (!host)
|
|
return;
|
|
|
|
// Slotted selectors only reach the current host subtree, but :host combinators can also reach siblings rooted
|
|
// alongside the host. A bare :host selector, however, only needs the host itself.
|
|
if (include_host && include_light_dom_under_shadow_host)
|
|
callback(host->root());
|
|
else if (include_host || include_light_dom_under_shadow_host)
|
|
callback(*host);
|
|
}
|
|
}
|
|
|
|
static bool style_value_references_animation_name(StyleValue const& value, FlyString const& animation_name)
|
|
{
|
|
if (value.is_custom_ident())
|
|
return value.as_custom_ident().custom_ident() == animation_name;
|
|
if (value.is_string())
|
|
return value.as_string().string_value() == animation_name;
|
|
|
|
if (!value.is_value_list())
|
|
return false;
|
|
|
|
for (auto const& item : value.as_value_list().values()) {
|
|
if (item->is_custom_ident() && item->as_custom_ident().custom_ident() == animation_name)
|
|
return true;
|
|
if (item->is_string() && item->as_string().string_value() == animation_name)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool element_or_pseudo_references_animation_name(DOM::Element const& element, FlyString const& animation_name)
|
|
{
|
|
auto references_animation_name_in_properties = [&](CSS::ComputedProperties const& computed_properties) {
|
|
return style_value_references_animation_name(computed_properties.property(PropertyID::AnimationName), animation_name);
|
|
};
|
|
|
|
if (auto computed_properties = element.computed_properties(); computed_properties && references_animation_name_in_properties(*computed_properties))
|
|
return true;
|
|
|
|
bool synthetic_pseudo_element_references_animation_name = false;
|
|
element.for_each_synthetic_pseudo_element([&](Web::CSS::PseudoElement, Web::DOM::SyntheticPseudoElement const& pseudo_element) {
|
|
if (auto computed_properties = pseudo_element.computed_properties(); computed_properties && references_animation_name_in_properties(*computed_properties)) {
|
|
synthetic_pseudo_element_references_animation_name = true;
|
|
return IterationDecision::Break;
|
|
}
|
|
return IterationDecision::Continue;
|
|
});
|
|
|
|
return synthetic_pseudo_element_references_animation_name;
|
|
}
|
|
|
|
static void invalidate_elements_affected_by_inserted_keyframes_rule(DOM::Node& root, FlyString const& animation_name)
|
|
{
|
|
auto invalidate_matching_element = [&](DOM::Element& element) {
|
|
// A new @keyframes rule only matters for elements or pseudo-elements that were already referencing the
|
|
// inserted animation-name.
|
|
if (element_or_pseudo_references_animation_name(element, animation_name))
|
|
element.set_needs_style_update(true);
|
|
return TraversalDecision::Continue;
|
|
};
|
|
|
|
if (root.is_document()) {
|
|
// Document styles are inherited by existing shadow trees too, so document-scoped @keyframes insertions must
|
|
// walk the shadow-including tree instead of stopping at shadow hosts.
|
|
root.for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) {
|
|
if (auto* element = as_if<DOM::Element>(node))
|
|
return invalidate_matching_element(*element);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
return;
|
|
}
|
|
|
|
root.for_each_in_inclusive_subtree_of_type<DOM::Element>(invalidate_matching_element);
|
|
}
|
|
|
|
static ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects_for_sheet(CSSStyleSheet const& style_sheet, DOM::ShadowRoot const& shadow_root)
|
|
{
|
|
ShadowRootStylesheetEffects effects;
|
|
|
|
Vector<GC::Ptr<HTML::HTMLSlotElement const>> slots;
|
|
shadow_root.for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](HTML::HTMLSlotElement const& slot) {
|
|
slots.append(slot);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
|
|
auto selector_may_affect_assigned_nodes_via_slot_inheritance = [&](Selector const& selector) {
|
|
for (auto const& slot : slots) {
|
|
for (auto const* node = static_cast<DOM::Node const*>(slot.ptr()); node && node != &shadow_root; node = node->parent_node()) {
|
|
auto const* element = as_if<DOM::Element>(node);
|
|
if (!element)
|
|
continue;
|
|
SelectorEngine::MatchContext context {
|
|
.style_sheet_for_rule = style_sheet,
|
|
.subject = *element,
|
|
.rule_shadow_root = &shadow_root,
|
|
};
|
|
if (SelectorEngine::matches(selector, *element, shadow_root.host(), context))
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
style_sheet.for_each_effective_style_producing_rule([&](CSSRule const& rule) {
|
|
if (effects.all_set())
|
|
return;
|
|
|
|
if (!is<CSSStyleRule>(rule))
|
|
return;
|
|
|
|
auto const& style_rule = as<CSSStyleRule>(rule);
|
|
for (auto const& selector : style_rule.absolutized_selectors()) {
|
|
effects.may_match_shadow_host |= selector->contains_pseudo_class(PseudoClass::Host);
|
|
effects.may_match_light_dom_under_shadow_host |= selector_may_match_light_dom_under_shadow_host(*selector);
|
|
effects.may_match_light_dom_outside_shadow_host |= selector_may_match_light_dom_outside_shadow_host(*selector);
|
|
|
|
if (!effects.may_affect_assigned_nodes_via_slots && !slots.is_empty())
|
|
effects.may_affect_assigned_nodes_via_slots = selector_may_affect_assigned_nodes_via_slot_inheritance(*selector);
|
|
|
|
if (effects.all_set())
|
|
return;
|
|
}
|
|
});
|
|
|
|
return effects;
|
|
}
|
|
|
|
ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(DOM::ShadowRoot const& shadow_root)
|
|
{
|
|
ShadowRootStylesheetEffects effects;
|
|
|
|
shadow_root.for_each_active_css_style_sheet([&](CSSStyleSheet& style_sheet) {
|
|
auto sheet_effects = determine_shadow_root_stylesheet_effects_for_sheet(style_sheet, shadow_root);
|
|
effects.may_match_shadow_host |= sheet_effects.may_match_shadow_host;
|
|
effects.may_match_light_dom_under_shadow_host |= sheet_effects.may_match_light_dom_under_shadow_host;
|
|
effects.may_match_light_dom_outside_shadow_host |= sheet_effects.may_match_light_dom_outside_shadow_host;
|
|
effects.may_affect_assigned_nodes_via_slots |= sheet_effects.may_affect_assigned_nodes_via_slots;
|
|
});
|
|
|
|
return effects;
|
|
}
|
|
|
|
ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(CSSStyleSheet const& style_sheet)
|
|
{
|
|
ShadowRootStylesheetEffects effects;
|
|
|
|
for (auto& document_or_shadow_root : style_sheet.owning_documents_or_shadow_roots()) {
|
|
auto* shadow_root = as_if<DOM::ShadowRoot>(*document_or_shadow_root);
|
|
if (!shadow_root)
|
|
continue;
|
|
|
|
auto sheet_effects = determine_shadow_root_stylesheet_effects_for_sheet(style_sheet, *shadow_root);
|
|
effects.may_match_shadow_host |= sheet_effects.may_match_shadow_host;
|
|
effects.may_match_light_dom_under_shadow_host |= sheet_effects.may_match_light_dom_under_shadow_host;
|
|
effects.may_match_light_dom_outside_shadow_host |= sheet_effects.may_match_light_dom_outside_shadow_host;
|
|
effects.may_affect_assigned_nodes_via_slots |= sheet_effects.may_affect_assigned_nodes_via_slots;
|
|
|
|
if (effects.all_set())
|
|
break;
|
|
}
|
|
|
|
return effects;
|
|
}
|
|
|
|
static bool invalidate_shadow_host_side_for_style_sheet_change(DOM::ShadowRoot& shadow_root, DOM::StyleInvalidationReason reason, ShadowRootStylesheetEffects const& effects)
|
|
{
|
|
auto* host = shadow_root.host();
|
|
if (!host)
|
|
return false;
|
|
|
|
if (effects.may_match_light_dom_outside_shadow_host) {
|
|
host->root().invalidate_style(reason);
|
|
} else if (effects.may_match_light_dom_under_shadow_host) {
|
|
host->invalidate_style(reason);
|
|
} else if (effects.may_affect_assigned_nodes_via_slots) {
|
|
host->invalidate_style(reason);
|
|
} else if (effects.may_match_shadow_host) {
|
|
host->invalidate_style(reason);
|
|
shadow_root.set_needs_style_update(true);
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void invalidate_style_for_style_sheet_owners(CSSStyleSheet const& style_sheet, DOM::StyleInvalidationReason reason, ShouldInvalidateRuleCache rule_cache_invalidation, ShadowRootStylesheetEffects const* previous_sheet_effects)
|
|
{
|
|
style_sheet.for_each_owning_style_scope([&](StyleScope& style_scope) {
|
|
if (rule_cache_invalidation == ShouldInvalidateRuleCache::Yes)
|
|
style_scope.invalidate_rule_cache();
|
|
|
|
style_scope.node().invalidate_style(reason);
|
|
|
|
auto* shadow_root = as_if<DOM::ShadowRoot>(style_scope.node());
|
|
if (!shadow_root)
|
|
return;
|
|
|
|
invalidate_assigned_elements_for_dirty_slots(*shadow_root);
|
|
bool invalidated_host_side = invalidate_shadow_host_side_for_style_sheet_change(*shadow_root, reason, determine_shadow_root_stylesheet_effects(*shadow_root));
|
|
|
|
if (!invalidated_host_side && previous_sheet_effects)
|
|
invalidate_shadow_host_side_for_style_sheet_change(*shadow_root, reason, *previous_sheet_effects);
|
|
});
|
|
}
|
|
|
|
void invalidate_root_for_keyframes_rule(DOM::Node& root, FlyString const& animation_name)
|
|
{
|
|
// Shadow-scoped keyframes can still affect elements outside the shadow subtree when an active rule in the same
|
|
// scope sets `animation-name` via :host or ::slotted(...). Mirror the fan-out used by the per-rule helper so the
|
|
// walk reaches the host (and host-side light DOM) when the shadow scope can match those elements.
|
|
bool include_host = false;
|
|
bool include_light_dom_under_shadow_host = false;
|
|
if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
|
|
auto effects = determine_shadow_root_stylesheet_effects(*shadow_root);
|
|
include_host = effects.may_match_shadow_host;
|
|
include_light_dom_under_shadow_host = effects.may_match_light_dom_under_shadow_host;
|
|
}
|
|
|
|
for_each_tree_affected_by_shadow_root_stylesheet_change(
|
|
root,
|
|
include_host,
|
|
include_light_dom_under_shadow_host,
|
|
[&](DOM::Node& affected_root) {
|
|
invalidate_elements_affected_by_inserted_keyframes_rule(affected_root, animation_name);
|
|
});
|
|
}
|
|
|
|
void invalidate_root_for_keyframes_rules_in_sheet(DOM::Node& root, CSSStyleSheet const& sheet)
|
|
{
|
|
sheet.for_each_effective_rule(TraversalOrder::Preorder, [&](CSSRule const& rule) {
|
|
auto const* keyframes_rule = as_if<CSSKeyframesRule>(rule);
|
|
if (!keyframes_rule)
|
|
return;
|
|
invalidate_root_for_keyframes_rule(root, keyframes_rule->name());
|
|
});
|
|
}
|
|
|
|
void invalidate_owners_for_inserted_keyframes_rule(CSSStyleSheet const& style_sheet, CSSKeyframesRule const& keyframes_rule)
|
|
{
|
|
for (auto& document_or_shadow_root : style_sheet.owning_documents_or_shadow_roots()) {
|
|
auto& style_scope = document_or_shadow_root->is_shadow_root()
|
|
? as<DOM::ShadowRoot>(*document_or_shadow_root).style_scope()
|
|
: document_or_shadow_root->document().style_scope();
|
|
style_scope.invalidate_rule_cache();
|
|
|
|
if (!document_or_shadow_root->is_shadow_root() && document_or_shadow_root->entire_subtree_needs_style_update())
|
|
continue;
|
|
|
|
invalidate_root_for_keyframes_rule(*document_or_shadow_root, keyframes_rule.name());
|
|
}
|
|
}
|
|
|
|
}
|