diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index 753d8a20bcc..474ed55608e 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -121,6 +121,9 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size return has; } case CSS::Selector::Combinator::NextSibling: { + if (context.collect_per_element_selector_involvement_metadata) { + const_cast(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true); + } auto* sibling = element.next_element_sibling(); if (!sibling) return false; @@ -129,6 +132,9 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size return matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor); } case CSS::Selector::Combinator::SubsequentSibling: { + if (context.collect_per_element_selector_involvement_metadata) { + const_cast(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true); + } for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { if (!matches(selector, compound_index, *sibling, shadow_host, context, {}, SelectorKind::Relative, anchor)) continue; diff --git a/Libraries/LibWeb/CSS/StyleInvalidationData.cpp b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp index 2991789af2c..27865e2d6bc 100644 --- a/Libraries/LibWeb/CSS/StyleInvalidationData.cpp +++ b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp @@ -107,6 +107,8 @@ enum class InsideNthChildPseudoClass { static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidationData& style_invalidation_data, Selector const& selector, InsideNthChildPseudoClass inside_nth_child_pseudo_class); +static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data); + static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_selector) { switch (selector.type) { @@ -130,7 +132,12 @@ static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector case PseudoClass::Disabled: case PseudoClass::PlaceholderShown: case PseudoClass::Checked: - case PseudoClass::Has: + case PseudoClass::Has: { + for (auto const& nested_selector : pseudo_class.argument_selector_list) { + add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(*nested_selector, style_invalidation_data); + } + [[fallthrough]]; + } case PseudoClass::Link: case PseudoClass::AnyLink: case PseudoClass::LocalLink: @@ -158,6 +165,47 @@ static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector } } +static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data) +{ + // Normally, :has() invalidation scope is limited to ancestors and ancestor siblings, however it could require + // descendants invalidation when :is() with complex selector is used inside :has() relative selector. + // For example ".a:has(:is(.b .c))" requires invalidation whenever "b" class is added or removed. + // To cover this case, we add descendant invalidation set that requires whole subtree invalidation for each + // property used in non-subject part of complex selector. + + auto invalidate_whole_subtree_for_invalidation_properties_in_non_subject_part_of_complex_selector = [&](Selector const& selector) { + for_each_consecutive_simple_selector_group(selector, [&](Vector const& simple_selectors, Selector::Combinator, bool rightmost) { + if (rightmost) { + return; + } + + InvalidationSet invalidation_set; + for (auto const& simple_selector : simple_selectors) { + build_invalidation_sets_for_simple_selector(simple_selector, invalidation_set, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, InsideNthChildPseudoClass::No); + } + + invalidation_set.for_each_property([&](auto const& invalidation_property) { + auto& descendant_invalidation_set = style_invalidation_data.descendant_invalidation_sets.ensure(invalidation_property, [] { return InvalidationSet {}; }); + descendant_invalidation_set.set_needs_invalidate_whole_subtree(); + return IterationDecision::Continue; + }); + }); + }; + + for_each_consecutive_simple_selector_group(selector, [&](Vector const& simple_selectors, Selector::Combinator, bool) { + for (auto const& simple_selector : simple_selectors) { + if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass) + continue; + auto const& pseudo_class = simple_selector.pseudo_class(); + if (pseudo_class.type == PseudoClass::Is || pseudo_class.type == PseudoClass::Where || pseudo_class.type == PseudoClass::Not) { + for (auto const& selector : pseudo_class.argument_selector_list) { + invalidate_whole_subtree_for_invalidation_properties_in_non_subject_part_of_complex_selector(*selector); + } + } + } + }); +} + static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidationData& style_invalidation_data, Selector const& selector, InsideNthChildPseudoClass inside_nth_child_pseudo_class) { auto const& compound_selectors = selector.compound_selectors(); diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 277c02833d0..9ac5a55a2ab 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -1438,7 +1438,7 @@ void Document::update_style() // style change event. [CSS-Transitions-2] m_transition_generation++; - invalidate_elements_affected_by_has(); + invalidate_elements_affected_by_has_in_non_subject_position(); if (!needs_full_style_update() && !needs_style_update() && !child_needs_style_update()) return; @@ -1659,30 +1659,24 @@ static Node* find_common_ancestor(Node* a, Node* b) return nullptr; } -void Document::invalidate_elements_affected_by_has() +void Document::invalidate_elements_affected_by_has_in_non_subject_position() { - if (!m_needs_invalidate_elements_affected_by_has) + if (!m_needs_invalidate_elements_affected_by_has_in_non_subject_position) return; - m_needs_invalidate_elements_affected_by_has = false; + m_needs_invalidate_elements_affected_by_has_in_non_subject_position = false; + // Take care of elements that affected by :has() in non-subject position, i.e., ".a:has(.b) > .c". + // Elements affected by :has() in subject position, i.e., ".a:has(.b)", are handled by + // Node::invalidate_style() and should already be marked for style update by now. Vector changed_properties; changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::Has }); - auto invalidation_set = document().style_computer().invalidation_set_for_properties(changed_properties); + auto invalidation_set = style_computer().invalidation_set_for_properties(changed_properties); for_each_shadow_including_inclusive_descendant([&](Node& node) { if (!node.is_element()) return TraversalDecision::Continue; auto& element = static_cast(node); bool needs_style_recalculation = false; - // There are two cases in which an element must be invalidated, depending on the position of :has() in a selector: - // 1) In the subject position, i.e., ".a:has(.b)". In that case, invalidation sets are not helpful - // for narrowing down the set of elements that need to be invalidated. Instead, we invalidate - // all elements that were tested against selectors with :has() in the subject position during - // selector matching. - // 2) In the non-subject position, i.e., ".a:has(.b) > .c". Here, invalidation sets can be used to - // determine that only elements with the "c" class have to be invalidated. - if (element.affected_by_has_pseudo_class_in_subject_position()) { - needs_style_recalculation = true; - } else if (invalidation_set.needs_invalidate_whole_subtree()) { + if (invalidation_set.needs_invalidate_whole_subtree()) { needs_style_recalculation = true; } else if (element.includes_properties_from_invalidation_set(invalidation_set)) { needs_style_recalculation = true; diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 34fa966c385..e01e00a1d3b 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -510,8 +510,8 @@ public: [[nodiscard]] bool needs_full_layout_tree_update() const { return m_needs_full_layout_tree_update; } void set_needs_full_layout_tree_update(bool b) { m_needs_full_layout_tree_update = b; } - bool needs_invalidate_elements_affected_by_has() const { return m_needs_invalidate_elements_affected_by_has; } - void set_needs_invalidate_elements_affected_by_has(bool b) { m_needs_invalidate_elements_affected_by_has = b; } + bool needs_invalidate_elements_affected_by_has_in_non_subject_position() const { return m_needs_invalidate_elements_affected_by_has_in_non_subject_position; } + void set_needs_invalidate_elements_affected_by_has_in_non_subject_position(bool b) { m_needs_invalidate_elements_affected_by_has_in_non_subject_position = b; } void set_needs_to_refresh_scroll_state(bool b); @@ -817,7 +817,7 @@ private: // ^HTML::GlobalEventHandlers virtual GC::Ptr global_event_handlers_to_event_target(FlyString const&) final { return *this; } - void invalidate_elements_affected_by_has(); + void invalidate_elements_affected_by_has_in_non_subject_position(); void tear_down_layout_tree(); @@ -961,7 +961,7 @@ private: bool m_needs_full_style_update { false }; bool m_needs_full_layout_tree_update { false }; - bool m_needs_invalidate_elements_affected_by_has { false }; + bool m_needs_invalidate_elements_affected_by_has_in_non_subject_position { false }; bool m_needs_animated_style_update { false }; diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 7834914ef68..db9f2442d4c 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -423,6 +423,9 @@ public: bool affected_by_has_pseudo_class_in_subject_position() const { return m_affected_by_has_pseudo_class_in_subject_position; } void set_affected_by_has_pseudo_class_in_subject_position(bool value) { m_affected_by_has_pseudo_class_in_subject_position = value; } + bool affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator() const { return m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator; } + void set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(bool value) { m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator = value; } + bool affected_by_sibling_combinator() const { return m_affected_by_sibling_combinator; } void set_affected_by_sibling_combinator(bool value) { m_affected_by_sibling_combinator = value; } @@ -531,6 +534,7 @@ private: bool m_affected_by_sibling_combinator : 1 { false }; bool m_affected_by_first_or_last_child_pseudo_class : 1 { false }; bool m_affected_by_nth_child_pseudo_class : 1 { false }; + bool m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator : 1 { false }; OwnPtr m_counters_set; diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index 7909c601cc4..677008fc022 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -397,13 +397,41 @@ GC::Ptr Node::navigable() const } } +void Node::invalidate_ancestors_affected_by_has_in_subject_position() +{ + // This call only takes care of invalidating `:has()` in subject position (e.g., ".a:has(.b)"). + // Non-subject position (e.g., ".a:has(.b) + .c") is still handled by another call that uses + // invalidation sets and requires whole document traversal. + // Here we assume that :has() invalidation scope is limited to ancestors and sibling ancestors. + for (auto* ancestor = this; ancestor; ancestor = ancestor->parent_or_shadow_host()) { + if (!ancestor->is_element()) + continue; + auto& element = static_cast(*ancestor); + if (element.affected_by_has_pseudo_class_in_subject_position()) { + element.set_needs_style_update(true); + } + + // If any previous sibling ancestor was tested against selectors like ".a:has(+ .b)" or ".a:has(~ .b)" + // its style might be affected by the change in descendant node. + for (auto* previous_sibling = element.previous_sibling(); previous_sibling; previous_sibling = previous_sibling->previous_sibling()) { + if (!previous_sibling->is_element()) + continue; + auto& element = static_cast(*previous_sibling); + if (element.affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator()) { + element.set_needs_style_update(true); + } + } + } +} + void Node::invalidate_style(StyleInvalidationReason reason) { if (is_character_data()) return; if (document().style_computer().may_have_has_selectors()) { - document().set_needs_invalidate_elements_affected_by_has(true); + invalidate_ancestors_affected_by_has_in_subject_position(); + document().set_needs_invalidate_elements_affected_by_has_in_non_subject_position(true); } if (!needs_style_update() && !document().needs_full_style_update()) { @@ -465,7 +493,8 @@ void Node::invalidate_style(StyleInvalidationReason, Vector