diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index 4d891894a82..1baff8bc2f6 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -549,6 +549,9 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla // :has() cannot be nested in a :has() if (selector_kind == SelectorKind::Relative) return false; + if (context.collect_per_element_selector_involvement_metadata && &element == context.subject) { + const_cast(element).set_affected_by_has_pseudo_class_in_subject_position(true); + } // These selectors should be relative selectors (https://drafts.csswg.org/selectors-4/#relative-selector) for (auto& selector : pseudo_class.argument_selector_list) { if (matches_has_pseudo_class(selector, element, shadow_host, context)) diff --git a/Libraries/LibWeb/CSS/SelectorEngine.h b/Libraries/LibWeb/CSS/SelectorEngine.h index b4395ad6390..37430aa414f 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.h +++ b/Libraries/LibWeb/CSS/SelectorEngine.h @@ -18,6 +18,8 @@ enum class SelectorKind { struct MatchContext { GC::Ptr style_sheet_for_rule {}; + GC::Ptr subject {}; + bool collect_per_element_selector_involvement_metadata { false }; bool did_match_any_hover_rules { false }; }; diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 53820b8577d..17bed09d3df 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -586,7 +586,11 @@ Vector StyleComputer::collect_matching_rules(DOM::Element c auto const& selector = rule_to_run.selector; - SelectorEngine::MatchContext context { .style_sheet_for_rule = *rule_to_run.sheet }; + SelectorEngine::MatchContext context { + .style_sheet_for_rule = *rule_to_run.sheet, + .subject = element, + .collect_per_element_selector_involvement_metadata = true, + }; ScopeGuard guard = [&] { if (context.did_match_any_hover_rules) did_match_any_hover_rules = true; diff --git a/Libraries/LibWeb/CSS/StyleInvalidationData.cpp b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp index cf473a7c4f7..8acc5a5ec8a 100644 --- a/Libraries/LibWeb/CSS/StyleInvalidationData.cpp +++ b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp @@ -125,6 +125,7 @@ static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector case PseudoClass::Disabled: case PseudoClass::PlaceholderShown: case PseudoClass::Checked: + case PseudoClass::Has: invalidation_set.set_needs_invalidate_pseudo_class(pseudo_class.type); break; default: diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index b37b295a03b..3d1d2a07c8a 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -1166,6 +1166,8 @@ bool Element::includes_properties_from_invalidation_set(CSS::InvalidationSet con } case CSS::InvalidationSet::Property::Type::PseudoClass: { switch (property.value.get()) { + case CSS::PseudoClass::Has: + return true; case CSS::PseudoClass::Enabled: { return (is(*this) || is(*this) || is(*this) || is(*this) || is(*this) || is(*this) || is(*this)) && !is_actually_disabled(); diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 130fec9aa73..1679b4fee57 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -411,6 +411,9 @@ public: bool has_style_containment() const; bool has_paint_containment() const; + 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; } + protected: Element(Document&, DOM::QualifiedName); virtual void initialize(JS::Realm&) override; @@ -500,6 +503,7 @@ private: bool m_in_top_layer { false }; bool m_style_uses_css_custom_properties { false }; + bool m_affected_by_has_pseudo_class_in_subject_position { false }; OwnPtr m_counters_set; diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index be1919c630a..7df5b736fca 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -401,16 +401,47 @@ GC::Ptr Node::navigable() const } } +void Node::invalidate_elements_affected_by_has() +{ + 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); + 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()) { + needs_style_recalculation = true; + } else if (element.includes_properties_from_invalidation_set(invalidation_set)) { + needs_style_recalculation = true; + } + + if (needs_style_recalculation) { + element.set_needs_style_update(true); + } else { + element.set_needs_inherited_style_update(true); + } + return TraversalDecision::Continue; + }); +} + void Node::invalidate_style(StyleInvalidationReason reason) { if (is_character_data()) return; - // FIXME: This is very not optimal! We should figure out a smaller set of elements to invalidate, - // but right now the :has() selector means we have to invalidate everything. - if (!is_document() && document().style_computer().may_have_has_selectors()) { - document().invalidate_style(reason); - return; + if (document().style_computer().may_have_has_selectors()) { + document().invalidate_elements_affected_by_has(); } if (!needs_style_update() && !document().needs_full_style_update()) { @@ -462,7 +493,7 @@ void Node::invalidate_style(StyleInvalidationReason reason) document().schedule_style_update(); } -void Node::invalidate_style(StyleInvalidationReason reason, Vector const& properties, StyleInvalidationOptions options) +void Node::invalidate_style(StyleInvalidationReason, Vector const& properties, StyleInvalidationOptions options) { if (is_character_data()) return; @@ -472,8 +503,7 @@ void Node::invalidate_style(StyleInvalidationReason reason, Vector