diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp index 9e7168b6dc1..29ee3b1765d 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -165,7 +166,9 @@ WebIDL::ExceptionOr CSSStyleSheet::insert_rule(StringView rule, unsign // NOTE: The spec doesn't say where to set the parent style sheet, so we'll do it here. parsed_rule->set_parent_style_sheet(this); - if (!constructed() && owner_node() && owner_node()->is_html_style_element() && parsed_rule->type() == CSSRule::Type::Style) + if (!constructed() && owner_node() && owner_node()->is_html_style_element() && parsed_rule->type() == CSSRule::Type::Keyframes) + invalidate_owners_for_inserted_keyframes_rule(*this, as(*parsed_rule)); + else if (!constructed() && owner_node() && owner_node()->is_html_style_element() && parsed_rule->type() == CSSRule::Type::Style) invalidate_owners_for_inserted_style_rule(*this, as(*parsed_rule), DOM::StyleInvalidationReason::StyleSheetInsertRule); else invalidate_owners(DOM::StyleInvalidationReason::StyleSheetInsertRule); diff --git a/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp b/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp index 38aa2e78c10..8e129600414 100644 --- a/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp +++ b/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp @@ -4,12 +4,17 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include @@ -365,4 +370,175 @@ void invalidate_owners_for_inserted_style_rule(CSSStyleSheet const& style_sheet, } } +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 const& callback) +{ + callback(root); + + if (auto* shadow_root = as_if(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; + + for (u8 i = 0; i < to_underlying(CSS::PseudoElement::KnownPseudoElementCount); ++i) { + auto pseudo_element = static_cast(i); + if (auto computed_properties = element.computed_properties(pseudo_element); computed_properties && references_animation_name_in_properties(*computed_properties)) + return true; + } + + return false; +} + +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(node)) + return invalidate_matching_element(*element); + return TraversalDecision::Continue; + }); + return; + } + + root.for_each_in_inclusive_subtree_of_type(invalidate_matching_element); +} + +static ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects_for_sheet(CSSStyleSheet const& style_sheet, DOM::ShadowRoot const& shadow_root) +{ + ShadowRootStylesheetEffects effects; + + Vector> slots; + shadow_root.for_each_in_inclusive_subtree_of_type([&](HTML::HTMLSlotElement const& slot) { + slots.append(slot); + return TraversalDecision::Continue; + }); + + style_sheet.for_each_effective_style_producing_rule([&](CSSRule const& rule) { + if (effects.may_match_shadow_host && effects.may_match_light_dom_under_shadow_host && effects.may_affect_assigned_nodes_via_slots) + return; + + if (!is(rule)) + return; + + auto const& style_rule = as(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); + + if (!effects.may_affect_assigned_nodes_via_slots && !slots.is_empty()) { + for (auto const& slot : slots) { + SelectorEngine::MatchContext context { + .style_sheet_for_rule = style_sheet, + .subject = *slot, + .rule_shadow_root = &shadow_root, + }; + if (SelectorEngine::matches(*selector, *slot, shadow_root.host(), context)) { + effects.may_affect_assigned_nodes_via_slots = true; + break; + } + } + } + + if (effects.may_match_shadow_host && effects.may_match_light_dom_under_shadow_host && effects.may_affect_assigned_nodes_via_slots) + 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_affect_assigned_nodes_via_slots |= sheet_effects.may_affect_assigned_nodes_via_slots; + }); + + return effects; +} + +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(*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; + + bool include_host = false; + bool include_light_dom_under_shadow_host = false; + if (auto* shadow_root = as_if(*document_or_shadow_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( + *document_or_shadow_root, + include_host, + include_light_dom_under_shadow_host, + [&](DOM::Node& affected_root) { + invalidate_elements_affected_by_inserted_keyframes_rule(affected_root, keyframes_rule.name()); + }); + } +} + } diff --git a/Libraries/LibWeb/CSS/StyleSheetInvalidation.h b/Libraries/LibWeb/CSS/StyleSheetInvalidation.h index 28efa1beeee..4af5c5a0947 100644 --- a/Libraries/LibWeb/CSS/StyleSheetInvalidation.h +++ b/Libraries/LibWeb/CSS/StyleSheetInvalidation.h @@ -39,6 +39,12 @@ struct StyleSheetInvalidationSet { Vector trailing_universal_rules; }; +struct ShadowRootStylesheetEffects { + bool may_match_shadow_host { false }; + bool may_match_light_dom_under_shadow_host { false }; + bool may_affect_assigned_nodes_via_slots { false }; +}; + // 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); @@ -54,8 +60,16 @@ WEB_API bool selector_may_match_light_dom_under_shadow_host(StringView selector_ // 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); +// 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&); + // 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); + } diff --git a/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-host-invalidation-counters.txt b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-host-invalidation-counters.txt new file mode 100644 index 00000000000..3d7ad311153 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-host-invalidation-counters.txt @@ -0,0 +1 @@ +PASS: bare :host keyframes insertRule only invalidates the host (0 invalidations) diff --git a/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-slotted-cross-sheet-invalidation.txt b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-slotted-cross-sheet-invalidation.txt new file mode 100644 index 00000000000..2a792e72a2d --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-slotted-cross-sheet-invalidation.txt @@ -0,0 +1,2 @@ +opacity before keyframes insertRule: 1 +opacity after keyframes insertRule: 0.25 diff --git a/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-tree-invalidation.txt b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-tree-invalidation.txt new file mode 100644 index 00000000000..0f9a798c241 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-shadow-tree-invalidation.txt @@ -0,0 +1,2 @@ +opacity before keyframes: 1 +opacity after keyframes: 0.25 diff --git a/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-starts-animation.txt b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-starts-animation.txt new file mode 100644 index 00000000000..6e067f74da8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-starts-animation.txt @@ -0,0 +1,3 @@ +before: 1 +after: 1 +keyframes: 0 diff --git a/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-style-invalidation-counters.txt b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-style-invalidation-counters.txt new file mode 100644 index 00000000000..00c0bac3063 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/insert-rule-keyframes-style-invalidation-counters.txt @@ -0,0 +1 @@ +PASS: keyframes insertRule only invalidates animation users (1 invalidations) diff --git a/Tests/LibWeb/Text/expected/css/insert-rule-shadow-root-slotted-invalidation.txt b/Tests/LibWeb/Text/expected/css/insert-rule-shadow-root-slotted-invalidation.txt new file mode 100644 index 00000000000..ebaf2d1e785 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/insert-rule-shadow-root-slotted-invalidation.txt @@ -0,0 +1,6 @@ +slotted before rule: rgb(0, 0, 0) +slotted after rule: rgb(0, 0, 255) +inherited before slot rule: rgb(0, 0, 0) +inherited after slot rule: rgb(255, 0, 0) +opacity before keyframes: 1 +opacity after keyframes: 0.25 diff --git a/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-host-invalidation-counters.html b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-host-invalidation-counters.html new file mode 100644 index 00000000000..612fe3804d9 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-host-invalidation-counters.html @@ -0,0 +1,42 @@ + + +
+ diff --git a/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-slotted-cross-sheet-invalidation.html b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-slotted-cross-sheet-invalidation.html new file mode 100644 index 00000000000..385b32419c2 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-slotted-cross-sheet-invalidation.html @@ -0,0 +1,29 @@ + + +
slotted
+ diff --git a/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-tree-invalidation.html b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-tree-invalidation.html new file mode 100644 index 00000000000..9df41b33cee --- /dev/null +++ b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-shadow-tree-invalidation.html @@ -0,0 +1,31 @@ + + + +
+ diff --git a/Tests/LibWeb/Text/input/css/insert-rule-keyframes-starts-animation.html b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-starts-animation.html new file mode 100644 index 00000000000..3b8904db721 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-starts-animation.html @@ -0,0 +1,27 @@ + + +
+ + diff --git a/Tests/LibWeb/Text/input/css/insert-rule-keyframes-style-invalidation-counters.html b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-style-invalidation-counters.html new file mode 100644 index 00000000000..2d3eae03209 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/insert-rule-keyframes-style-invalidation-counters.html @@ -0,0 +1,43 @@ + + + +
target
+
bystander 1
+
bystander 2
+
bystander 3
+ diff --git a/Tests/LibWeb/Text/input/css/insert-rule-shadow-root-slotted-invalidation.html b/Tests/LibWeb/Text/input/css/insert-rule-shadow-root-slotted-invalidation.html new file mode 100644 index 00000000000..35077266aca --- /dev/null +++ b/Tests/LibWeb/Text/input/css/insert-rule-shadow-root-slotted-invalidation.html @@ -0,0 +1,40 @@ + + +
slottedinherited
+