ladybird/Libraries/LibWeb/CSS/StyleSheetInvalidation.h
Andreas Kling 2a35973abd LibWeb: Target media query change invalidation
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.
2026-05-25 19:18:10 +02:00

134 lines
7.6 KiB
C++

/*
* Copyright (c) 2026, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <AK/RefPtr.h>
#include <AK/Vector.h>
#include <LibGC/Ptr.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleInvalidationData.h>
#include <LibWeb/DOM/StyleInvalidationReason.h>
#include <LibWeb/Forward.h>
namespace Web::CSS {
// Targeted invalidation plan derived from a stylesheet (or a single inserted rule) for the add/remove and insertRule
// paths. Carries a primary invalidation set plus anchor-based rules for selectors whose rightmost compound is
// pseudo-element-only or trailing-universal.
struct StyleSheetInvalidationSet {
InvalidationSet invalidation_set;
bool may_match_shadow_host { false };
bool may_match_light_dom_under_shadow_host { false };
bool may_match_light_dom_outside_shadow_host { false };
struct PseudoElementInvalidationRule {
InvalidationSet anchor_set;
RefPtr<Selector> anchor_selector;
GC::Ptr<CSSStyleSheet const> style_sheet_for_rule;
};
struct TrailingUniversalInvalidationRule {
InvalidationSet anchor_set;
RefPtr<Selector> anchor_selector;
Selector::Combinator combinator { Selector::Combinator::None };
GC::Ptr<CSSStyleSheet const> style_sheet_for_rule;
};
Vector<PseudoElementInvalidationRule> pseudo_element_rules;
Vector<TrailingUniversalInvalidationRule> trailing_universal_rules;
};
struct ShadowRootStylesheetEffects {
bool may_match_shadow_host { false };
bool may_match_light_dom_under_shadow_host { false };
bool may_match_light_dom_outside_shadow_host { false };
bool may_affect_assigned_nodes_via_slots { false };
bool all_set() const
{
return may_match_shadow_host
&& may_match_light_dom_under_shadow_host
&& may_match_light_dom_outside_shadow_host
&& may_affect_assigned_nodes_via_slots;
}
};
enum class ShouldInvalidateRuleCache {
No,
Yes,
};
// Extend `result` with the invalidation effects of `style_rule`'s selectors. Falls back to a whole-subtree
// invalidation flag inside `result` when a selector is not amenable to targeted invalidation.
void extend_style_sheet_invalidation_set_with_style_rule(StyleSheetInvalidationSet& result, CSSStyleRule const& style_rule);
// Extend `result` with the invalidation effects of `rule` when the rule becomes effective or ineffective. Style
// rules and nested declaration rules contribute their selector-derived invalidation sets. Rule kinds that affect
// cascade or computed-value resolution globally fall back to whole-subtree invalidation.
void extend_style_sheet_invalidation_set_with_rule(StyleSheetInvalidationSet& result, CSSRule const& rule);
// Shadow-root rules can escape the shadow tree either through ::slotted(...) or through :host with a combinator to
// another compound, such as :host > * or :host + .foo. Those selectors must fan out invalidation to the host side
// instead of treating the change as shadow-local.
bool selector_may_match_light_dom_under_shadow_host(Selector const&);
WEB_API bool selector_may_match_light_dom_under_shadow_host(StringView selector_text);
// Shadow-root :host rules with sibling combinators can target host siblings or other nodes outside the host subtree.
// These need broader host-root invalidation than host-subtree selectors like :host > * or :host *.
bool selector_may_match_light_dom_outside_shadow_host(Selector const&);
WEB_API bool selector_may_match_light_dom_outside_shadow_host(StringView selector_text);
// Apply a built invalidation set to `root` (a Document or a ShadowRoot). When `force_broad_invalidation` is true,
// schedule a tree-wide restyle regardless of the targeted set; this is used when the sheet contains rule kinds (such
// as @property or @keyframes) whose effects are not captured by selector invalidation alone.
void invalidate_root_for_style_sheet_change(DOM::Node& root, StyleSheetInvalidationSet const&, DOM::StyleInvalidationReason, bool force_broad_invalidation = false);
// When a broad shadow-root stylesheet invalidation can change the cascade for rules outside the immediate changed
// rule set, merge in the current shadow-scope selector reach so the broad invalidation fans out to :host and
// ::slotted(...) targets as needed.
void add_shadow_root_stylesheet_effects_for_broad_invalidation(DOM::Node& root, StyleSheetInvalidationSet&, bool requires_broad_invalidation);
// Targeted invalidation used after a stylesheet is added to or removed from a Document or ShadowRoot, either via a
// <style> element or via adoptedStyleSheets. The caller is responsible for updating the sheet's
// owning_documents_or_shadow_roots and any list-membership before invoking this.
void invalidate_style_for_stylesheet_change(DOM::Node& document_or_shadow_root, CSSStyleSheet const& sheet, DOM::StyleInvalidationReason);
// Summarize how any currently-active stylesheet in `shadow_root` can escape the shadow subtree. Used by mutation
// paths that need host-side fallout derived from the whole shadow scope rather than a single sheet.
ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(DOM::ShadowRoot const&);
// Slotted light-DOM nodes inherit from their assigned <slot>, so any shadow invalidation that dirties slot elements
// must also dirty the flattened assignees outside the shadow subtree.
void invalidate_assigned_elements_for_dirty_slots(DOM::ShadowRoot&);
// Summarize how `style_sheet` can escape the shadow subtree across all shadow roots it is owned by. Used to snapshot
// the pre-mutation reach of a sheet whose own rules are about to change.
ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(CSSStyleSheet const&);
// Invalidate style for every document or shadow root that owns `style_sheet`, including any host-side fallout for
// shadow-root selectors. Callers choose whether the rule cache must be invalidated for the mutation they perform.
void invalidate_style_for_style_sheet_owners(CSSStyleSheet const& style_sheet, DOM::StyleInvalidationReason, ShouldInvalidateRuleCache, ShadowRootStylesheetEffects const* previous_sheet_effects = nullptr);
// Apply a targeted invalidation to all documents and shadow roots that own `style_sheet` in response to inserting
// `style_rule` into it.
void invalidate_owners_for_inserted_style_rule(CSSStyleSheet const& style_sheet, CSSStyleRule const& style_rule, DOM::StyleInvalidationReason);
// Apply a targeted invalidation to all documents and shadow roots that own `style_sheet` in response to inserting
// `keyframes_rule` into it. Only elements already referencing the inserted animation-name are dirtied.
void invalidate_owners_for_inserted_keyframes_rule(CSSStyleSheet const& style_sheet, CSSKeyframesRule const& keyframes_rule);
// For every @keyframes rule contained in `sheet`, dirty only the elements (and pseudo-elements) under `root` that
// already reference the keyframes name. When `root` is a shadow root, the walk also fans out to the shadow host (and
// host-side light DOM) if any active rule in the same scope can match those nodes via :host or ::slotted(...). Used
// by the sheet add/remove paths so a sheet that contains @keyframes does not have to fall back to a whole-subtree
// invalidation.
void invalidate_root_for_keyframes_rules_in_sheet(DOM::Node& root, CSSStyleSheet const& sheet);
// Dirty only the elements (and pseudo-elements) under `root` that already reference `animation_name`. When `root` is
// a shadow root, the walk also fans out to the shadow host side if active rules in the same scope can match there.
void invalidate_root_for_keyframes_rule(DOM::Node& root, FlyString const& animation_name);
}