diff --git a/Libraries/LibWeb/CSS/ComputedProperties.h b/Libraries/LibWeb/CSS/ComputedProperties.h index ab8baf0b3d6..f5cebc869be 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.h +++ b/Libraries/LibWeb/CSS/ComputedProperties.h @@ -213,6 +213,9 @@ public: static float resolve_opacity_value(CSSStyleValue const& value); + bool did_match_any_hover_rules() const { return m_did_match_any_hover_rules; } + void set_did_match_any_hover_rules() { m_did_match_any_hover_rules = true; } + private: friend class StyleComputer; @@ -236,6 +239,8 @@ private: mutable RefPtr m_font_list; Optional m_line_height; + + bool m_did_match_any_hover_rules { false }; }; } diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index 5055f4d6b0d..030f571d30d 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -35,7 +35,7 @@ namespace Web::SelectorEngine { -static inline bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, int component_list_index, DOM::Element const& element, GC::Ptr shadow_host, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor = nullptr); +static inline bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor = nullptr); // Upward traversal for descendant (' ') and immediate child combinator ('>') // If we're starting inside a shadow tree, traversal stops at the nearest shadow host. @@ -80,10 +80,10 @@ static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector } // https://drafts.csswg.org/selectors-4/#relational -static inline bool matches_relative_selector(CSS::Selector const& selector, size_t compound_index, Optional style_sheet_for_rule, DOM::Element const& element, GC::Ptr shadow_host, GC::Ref anchor) +static inline bool matches_relative_selector(CSS::Selector const& selector, size_t compound_index, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ref anchor) { if (compound_index >= selector.compound_selectors().size()) - return matches(selector, style_sheet_for_rule, element, shadow_host, {}, {}, SelectorKind::Relative, anchor); + return matches(selector, element, shadow_host, context, {}, {}, SelectorKind::Relative, anchor); switch (selector.compound_selectors()[compound_index].combinator) { // Shouldn't be possible because we've parsed relative selectors, which always have a combinator, implicitly or explicitly. @@ -95,7 +95,7 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size if (!descendant.is_element()) return TraversalDecision::Continue; auto const& descendant_element = static_cast(descendant); - if (matches(selector, style_sheet_for_rule, descendant_element, shadow_host, {}, {}, SelectorKind::Relative, anchor)) { + if (matches(selector, descendant_element, shadow_host, context, {}, {}, SelectorKind::Relative, anchor)) { has = true; return TraversalDecision::Break; } @@ -109,9 +109,9 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size if (!child.is_element()) return IterationDecision::Continue; auto const& child_element = static_cast(child); - if (!matches(selector, style_sheet_for_rule, compound_index, child_element, shadow_host, {}, SelectorKind::Relative, anchor)) + if (!matches(selector, compound_index, child_element, shadow_host, context, {}, SelectorKind::Relative, anchor)) return IterationDecision::Continue; - if (matches_relative_selector(selector, compound_index + 1, style_sheet_for_rule, child_element, shadow_host, anchor)) { + if (matches_relative_selector(selector, compound_index + 1, child_element, shadow_host, context, anchor)) { has = true; return IterationDecision::Break; } @@ -123,15 +123,15 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size auto* sibling = element.next_element_sibling(); if (!sibling) return false; - if (!matches(selector, style_sheet_for_rule, compound_index, *sibling, shadow_host, {}, SelectorKind::Relative, anchor)) + if (!matches(selector, compound_index, *sibling, shadow_host, context, {}, SelectorKind::Relative, anchor)) return false; - return matches_relative_selector(selector, compound_index + 1, style_sheet_for_rule, *sibling, shadow_host, anchor); + return matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor); } case CSS::Selector::Combinator::SubsequentSibling: { for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { - if (!matches(selector, style_sheet_for_rule, compound_index, *sibling, shadow_host, {}, SelectorKind::Relative, anchor)) + if (!matches(selector, compound_index, *sibling, shadow_host, context, {}, SelectorKind::Relative, anchor)) continue; - if (matches_relative_selector(selector, compound_index + 1, style_sheet_for_rule, *sibling, shadow_host, anchor)) + if (matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor)) return true; } return false; @@ -143,9 +143,9 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size } // https://drafts.csswg.org/selectors-4/#relational -static inline bool matches_has_pseudo_class(CSS::Selector const& selector, Optional style_sheet_for_rule, DOM::Element const& anchor, GC::Ptr shadow_host) +static inline bool matches_has_pseudo_class(CSS::Selector const& selector, DOM::Element const& anchor, GC::Ptr shadow_host, MatchContext& context) { - return matches_relative_selector(selector, 0, style_sheet_for_rule, anchor, shadow_host, anchor); + return matches_relative_selector(selector, 0, anchor, shadow_host, context, anchor); } // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-link @@ -215,7 +215,7 @@ static inline bool matches_indeterminate_pseudo_class(DOM::Element const& elemen return false; } -static inline Web::DOM::Attr const* get_optionally_namespaced_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, Optional style_sheet_for_rule, DOM::Element const& element) +static inline Web::DOM::Attr const* get_optionally_namespaced_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, GC::Ptr style_sheet_for_rule, DOM::Element const& element) { auto const& qualified_name = attribute.qualified_name; auto const& attribute_name = qualified_name.name.name; @@ -237,7 +237,7 @@ static inline Web::DOM::Attr const* get_optionally_namespaced_attribute(CSS::Sel case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: return element.attributes()->get_attribute_namespace_agnostic(attribute_name); case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: - if (!style_sheet_for_rule.has_value()) + if (!style_sheet_for_rule) return nullptr; auto const& selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); if (!selector_namespace.has_value()) @@ -247,7 +247,7 @@ static inline Web::DOM::Attr const* get_optionally_namespaced_attribute(CSS::Sel VERIFY_NOT_REACHED(); } -static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] Optional style_sheet_for_rule, DOM::Element const& element) +static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] GC::Ptr style_sheet_for_rule, DOM::Element const& element) { auto const& attribute_name = attribute.qualified_name.name.name; @@ -435,7 +435,7 @@ static bool matches_open_state_pseudo_class(DOM::Element const& element, bool op } // https://drafts.csswg.org/css-scoping/#host-selector -static inline bool matches_host_pseudo_class(GC::Ref element, GC::Ptr shadow_host, CSS::SelectorList const& argument_selector_list, Optional style_sheet_for_rule) +static inline bool matches_host_pseudo_class(GC::Ref element, GC::Ptr shadow_host, MatchContext& context, CSS::SelectorList const& argument_selector_list) { // When evaluated in the context of a shadow tree, it matches the shadow tree’s shadow host if the shadow host, // in its normal context, matches the selector argument. In any other context, it matches nothing. @@ -444,12 +444,12 @@ static inline bool matches_host_pseudo_class(GC::Ref element // NOTE: There's either 0 or 1 argument selector, since the syntax is :host or :host() if (!argument_selector_list.is_empty()) - return matches(argument_selector_list.first(), style_sheet_for_rule, element, nullptr); + return matches(argument_selector_list.first(), element, nullptr, context); return true; } -static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, Optional style_sheet_for_rule, DOM::Element const& element, GC::Ptr shadow_host, GC::Ptr scope, SelectorKind selector_kind) +static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind) { switch (pseudo_class.type) { case CSS::PseudoClass::Link: @@ -477,6 +477,7 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla case CSS::PseudoClass::Active: return element.is_active(); case CSS::PseudoClass::Hover: + context.did_match_any_hover_rules = true; return matches_hover_pseudo_class(element); case CSS::PseudoClass::Focus: return element.is_focused(); @@ -513,7 +514,7 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla case CSS::PseudoClass::Root: return is(element); case CSS::PseudoClass::Host: - return matches_host_pseudo_class(element, shadow_host, pseudo_class.argument_selector_list, style_sheet_for_rule); + return matches_host_pseudo_class(element, shadow_host, context, pseudo_class.argument_selector_list); case CSS::PseudoClass::Scope: return scope ? &element == scope : is(element); case CSS::PseudoClass::FirstOfType: @@ -545,20 +546,20 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla return false; // 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, style_sheet_for_rule, element, shadow_host)) + if (matches_has_pseudo_class(selector, element, shadow_host, context)) return true; } return false; case CSS::PseudoClass::Is: case CSS::PseudoClass::Where: for (auto& selector : pseudo_class.argument_selector_list) { - if (matches(selector, style_sheet_for_rule, element, shadow_host)) + if (matches(selector, element, shadow_host, context)) return true; } return false; case CSS::PseudoClass::Not: for (auto& selector : pseudo_class.argument_selector_list) { - if (matches(selector, style_sheet_for_rule, element, shadow_host)) + if (matches(selector, element, shadow_host, context)) return false; } return true; @@ -575,11 +576,11 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla if (!parent) return false; - auto matches_selector_list = [&style_sheet_for_rule, shadow_host](CSS::SelectorList const& list, DOM::Element const& element) { + auto matches_selector_list = [&context, shadow_host](CSS::SelectorList const& list, DOM::Element const& element) { if (list.is_empty()) return true; for (auto const& child_selector : list) { - if (matches(child_selector, style_sheet_for_rule, element, shadow_host)) { + if (matches(child_selector, element, shadow_host, context)) { return true; } } @@ -758,12 +759,12 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla static ALWAYS_INLINE bool matches_namespace( CSS::Selector::SimpleSelector::QualifiedName const& qualified_name, DOM::Element const& element, - Optional style_sheet_for_rule) + GC::Ptr style_sheet_for_rule) { switch (qualified_name.namespace_type) { case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: // "if no default namespace has been declared for selectors, this is equivalent to *|E." - if (!style_sheet_for_rule.has_value() || !style_sheet_for_rule->default_namespace_rule()) + if (!style_sheet_for_rule || !style_sheet_for_rule->default_namespace_rule()) return true; // "Otherwise it is equivalent to ns|E where ns is the default namespace." return element.namespace_uri() == style_sheet_for_rule->default_namespace_rule()->namespace_uri(); @@ -778,7 +779,7 @@ static ALWAYS_INLINE bool matches_namespace( // Unrecognized namespace prefixes are invalid, so don't match. // (We can't detect this at parse time, since a namespace rule may be inserted later.) // So, if we don't have a context to look up namespaces from, we fail to match. - if (!style_sheet_for_rule.has_value()) + if (!style_sheet_for_rule) return false; auto selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); @@ -787,7 +788,7 @@ static ALWAYS_INLINE bool matches_namespace( VERIFY_NOT_REACHED(); } -static inline bool matches(CSS::Selector::SimpleSelector const& component, Optional style_sheet_for_rule, DOM::Element const& element, GC::Ptr shadow_host, GC::Ptr scope, SelectorKind selector_kind, [[maybe_unused]] GC::Ptr anchor) +static inline bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind, [[maybe_unused]] GC::Ptr anchor) { if (should_block_shadow_host_matching(component, shadow_host, element)) return false; @@ -807,7 +808,7 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, Optio } } - return matches_namespace(qualified_name, element, style_sheet_for_rule); + return matches_namespace(qualified_name, element, context.style_sheet_for_rule); } case CSS::Selector::SimpleSelector::Type::Id: return component.name() == element.id(); @@ -818,9 +819,9 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, Optio return element.has_class(component.name(), case_sensitivity); } case CSS::Selector::SimpleSelector::Type::Attribute: - return matches_attribute(component.attribute(), style_sheet_for_rule, element); + return matches_attribute(component.attribute(), context.style_sheet_for_rule, element); case CSS::Selector::SimpleSelector::Type::PseudoClass: - return matches_pseudo_class(component.pseudo_class(), style_sheet_for_rule, element, shadow_host, scope, selector_kind); + return matches_pseudo_class(component.pseudo_class(), element, shadow_host, context, scope, selector_kind); case CSS::Selector::SimpleSelector::Type::PseudoElement: // Pseudo-element matching/not-matching is handled in the top level matches(). return true; @@ -828,7 +829,7 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, Optio // Nesting either behaves like :is(), or like :scope. // :is() is handled already, by us replacing it with :is() directly, so if we // got here, it's :scope. - return matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector { .type = CSS::PseudoClass::Scope }, style_sheet_for_rule, element, shadow_host, scope, selector_kind); + return matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector { .type = CSS::PseudoClass::Scope }, element, shadow_host, context, scope, selector_kind); case CSS::Selector::SimpleSelector::Type::Invalid: // Invalid selectors never match return false; @@ -836,11 +837,11 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, Optio VERIFY_NOT_REACHED(); } -bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, int component_list_index, DOM::Element const& element, GC::Ptr shadow_host, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor) +bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor) { auto& compound_selector = selector.compound_selectors()[component_list_index]; for (auto& simple_selector : compound_selector.simple_selectors) { - if (!matches(simple_selector, style_sheet_for_rule, element, shadow_host, scope, selector_kind, anchor)) { + if (!matches(simple_selector, element, shadow_host, context, scope, selector_kind, anchor)) { return false; } } @@ -861,7 +862,7 @@ bool matches(CSS::Selector const& selector, Optional continue; if (ancestor == anchor) return false; - if (matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast(*ancestor), shadow_host, scope, selector_kind, anchor)) + if (matches(selector, component_list_index - 1, static_cast(*ancestor), shadow_host, context, scope, selector_kind, anchor)) return true; } return false; @@ -870,17 +871,17 @@ bool matches(CSS::Selector const& selector, Optional auto parent = traverse_up(element, shadow_host); if (!parent || !parent->is_element()) return false; - return matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast(*parent), shadow_host, scope, selector_kind, anchor); + return matches(selector, component_list_index - 1, static_cast(*parent), shadow_host, context, scope, selector_kind, anchor); } case CSS::Selector::Combinator::NextSibling: VERIFY(component_list_index != 0); if (auto* sibling = element.previous_element_sibling()) - return matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, shadow_host, scope, selector_kind, anchor); + return matches(selector, component_list_index - 1, *sibling, shadow_host, context, scope, selector_kind, anchor); return false; case CSS::Selector::Combinator::SubsequentSibling: VERIFY(component_list_index != 0); for (auto* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) { - if (matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, shadow_host, scope, selector_kind, anchor)) + if (matches(selector, component_list_index - 1, *sibling, shadow_host, context, scope, selector_kind, anchor)) return true; } return false; @@ -890,24 +891,24 @@ bool matches(CSS::Selector const& selector, Optional VERIFY_NOT_REACHED(); } -bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, DOM::Element const& element, GC::Ptr shadow_host, Optional pseudo_element, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor) +bool matches(CSS::Selector const& selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, Optional pseudo_element, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor) { VERIFY(!selector.compound_selectors().is_empty()); if (pseudo_element.has_value() && selector.pseudo_element().has_value() && selector.pseudo_element().value().type() != pseudo_element) return false; if (!pseudo_element.has_value() && selector.pseudo_element().has_value()) return false; - return matches(selector, style_sheet_for_rule, selector.compound_selectors().size() - 1, element, shadow_host, scope, selector_kind, anchor); + return matches(selector, selector.compound_selectors().size() - 1, element, shadow_host, context, scope, selector_kind, anchor); } -static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, Optional style_sheet_for_rule, DOM::Element const& element, GC::Ptr shadow_host) +static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context) { if (should_block_shadow_host_matching(simple_selector, shadow_host, element)) return false; switch (simple_selector.type) { case CSS::Selector::SimpleSelector::Type::Universal: - return matches_namespace(simple_selector.qualified_name(), element, style_sheet_for_rule); + return matches_namespace(simple_selector.qualified_name(), element, context.style_sheet_for_rule); case CSS::Selector::SimpleSelector::Type::TagName: if (element.document().document_type() == DOM::Document::Type::HTML) { if (simple_selector.qualified_name().name.lowercase_name != element.local_name()) @@ -915,7 +916,7 @@ static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& si } else if (!Infra::is_ascii_case_insensitive_match(simple_selector.qualified_name().name.name, element.local_name())) { return false; } - return matches_namespace(simple_selector.qualified_name(), element, style_sheet_for_rule); + return matches_namespace(simple_selector.qualified_name(), element, context.style_sheet_for_rule); case CSS::Selector::SimpleSelector::Type::Class: { // Class selectors are matched case insensitively in quirks mode. // See: https://drafts.csswg.org/selectors-4/#class-html @@ -925,30 +926,30 @@ static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& si case CSS::Selector::SimpleSelector::Type::Id: return simple_selector.name() == element.id(); case CSS::Selector::SimpleSelector::Type::Attribute: - return matches_attribute(simple_selector.attribute(), style_sheet_for_rule, element); + return matches_attribute(simple_selector.attribute(), context.style_sheet_for_rule, element); case CSS::Selector::SimpleSelector::Type::PseudoClass: - return matches_pseudo_class(simple_selector.pseudo_class(), style_sheet_for_rule, element, shadow_host, nullptr, SelectorKind::Normal); + return matches_pseudo_class(simple_selector.pseudo_class(), element, shadow_host, context, nullptr, SelectorKind::Normal); default: VERIFY_NOT_REACHED(); } } -static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, Optional style_sheet_for_rule, DOM::Element const& element, GC::Ptr shadow_host) +static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context) { for (auto const& simple_selector : compound_selector.simple_selectors) { - if (!fast_matches_simple_selector(simple_selector, style_sheet_for_rule, element, shadow_host)) + if (!fast_matches_simple_selector(simple_selector, element, shadow_host, context)) return false; } return true; } -bool fast_matches(CSS::Selector const& selector, Optional style_sheet_for_rule, DOM::Element const& element_to_match, GC::Ptr shadow_host) +bool fast_matches(CSS::Selector const& selector, DOM::Element const& element_to_match, GC::Ptr shadow_host, MatchContext& context) { DOM::Element const* current = &element_to_match; ssize_t compound_selector_index = selector.compound_selectors().size() - 1; - if (!fast_matches_compound_selector(selector.compound_selectors().last(), style_sheet_for_rule, *current, shadow_host)) + if (!fast_matches_compound_selector(selector.compound_selectors().last(), *current, shadow_host, context)) return false; // NOTE: If we fail after following a child combinator, we may need to backtrack @@ -971,7 +972,7 @@ bool fast_matches(CSS::Selector const& selector, Optionalparent_element(), compound_selector_index }; compound_selector = &selector.compound_selectors()[--compound_selector_index]; for (current = current->parent_element(); current; current = current->parent_element()) { - if (fast_matches_compound_selector(*compound_selector, style_sheet_for_rule, *current, shadow_host)) + if (fast_matches_compound_selector(*compound_selector, *current, shadow_host, context)) break; } if (!current) @@ -982,7 +983,7 @@ bool fast_matches(CSS::Selector const& selector, Optionalparent_element(); if (!current) return false; - if (!fast_matches_compound_selector(*compound_selector, style_sheet_for_rule, *current, shadow_host)) { + if (!fast_matches_compound_selector(*compound_selector, *current, shadow_host, context)) { if (backtrack_state.element) { current = backtrack_state.element; compound_selector_index = backtrack_state.compound_selector_index; diff --git a/Libraries/LibWeb/CSS/SelectorEngine.h b/Libraries/LibWeb/CSS/SelectorEngine.h index 8af55792689..b4395ad6390 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.h +++ b/Libraries/LibWeb/CSS/SelectorEngine.h @@ -16,9 +16,14 @@ enum class SelectorKind { Relative, }; -bool matches(CSS::Selector const&, Optional style_sheet_for_rule, DOM::Element const&, GC::Ptr shadow_host, Optional = {}, GC::Ptr scope = {}, SelectorKind selector_kind = SelectorKind::Normal, GC::Ptr anchor = nullptr); +struct MatchContext { + GC::Ptr style_sheet_for_rule {}; + bool did_match_any_hover_rules { false }; +}; -[[nodiscard]] bool fast_matches(CSS::Selector const&, Optional style_sheet_for_rule, DOM::Element const&, GC::Ptr shadow_host); +bool matches(CSS::Selector const&, DOM::Element const&, GC::Ptr shadow_host, MatchContext& context, Optional = {}, GC::Ptr scope = {}, SelectorKind selector_kind = SelectorKind::Normal, GC::Ptr anchor = nullptr); + +[[nodiscard]] bool fast_matches(CSS::Selector const&, DOM::Element const&, GC::Ptr shadow_host, MatchContext& context); [[nodiscard]] bool can_use_fast_matches(CSS::Selector const&); [[nodiscard]] bool matches_hover_pseudo_class(DOM::Element const&); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 1b13a4e0684..1e5ccf96129 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -422,7 +422,7 @@ bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector) return false; } -Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element, FlyString const& qualified_layer_name) const +Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name) const { auto const& root_node = element.root(); auto shadow_root = is(root_node) ? static_cast(&root_node) : nullptr; @@ -435,22 +435,16 @@ Vector StyleComputer::collect_matching_rules(DOM::Element const& e auto const& rule_cache = rule_cache_for_cascade_origin(cascade_origin); - bool is_hovered = SelectorEngine::matches_hover_pseudo_class(element); - Vector rules_to_run; auto add_rules_to_run = [&](Vector const& rules) { rules_to_run.grow_capacity(rules_to_run.size() + rules.size()); if (pseudo_element.has_value()) { for (auto const& rule : rules) { - if (rule.must_be_hovered && !is_hovered) - continue; if (rule.contains_pseudo_element && filter_namespace_rule(element, rule) && filter_layer(qualified_layer_name, rule)) rules_to_run.unchecked_append(rule); } } else { for (auto const& rule : rules) { - if (rule.must_be_hovered && !is_hovered) - continue; if (!rule.contains_pseudo_element && filter_namespace_rule(element, rule) && filter_layer(qualified_layer_name, rule)) rules_to_run.unchecked_append(rule); } @@ -537,11 +531,16 @@ Vector StyleComputer::collect_matching_rules(DOM::Element const& e auto const& selector = rule_to_run.absolutized_selectors()[rule_to_run.selector_index]; + SelectorEngine::MatchContext context { .style_sheet_for_rule = *rule_to_run.sheet }; + ScopeGuard guard = [&] { + if (context.did_match_any_hover_rules) + did_match_any_hover_rules = true; + }; if (rule_to_run.can_use_fast_matches) { - if (!SelectorEngine::fast_matches(selector, *rule_to_run.sheet, element, shadow_host_to_use)) + if (!SelectorEngine::fast_matches(selector, element, shadow_host_to_use, context)) continue; } else { - if (!SelectorEngine::matches(selector, *rule_to_run.sheet, element, shadow_host_to_use, pseudo_element)) + if (!SelectorEngine::matches(selector, element, shadow_host_to_use, context, pseudo_element)) continue; } matching_rules.append(rule_to_run); @@ -1488,24 +1487,24 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_ // https://www.w3.org/TR/css-cascade/#cascading // https://drafts.csswg.org/css-cascade-5/#layering -GC::Ref StyleComputer::compute_cascaded_values(DOM::Element& element, Optional pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const +GC::Ref StyleComputer::compute_cascaded_values(DOM::Element& element, Optional pseudo_element, bool& did_match_any_pseudo_element_rules, bool& did_match_any_hover_rules, ComputeStyleMode mode) const { auto cascaded_properties = m_document->heap().allocate(); // First, we collect all the CSS rules whose selectors match `element`: MatchingRuleSet matching_rule_set; - matching_rule_set.user_agent_rules = collect_matching_rules(element, CascadeOrigin::UserAgent, pseudo_element); + matching_rule_set.user_agent_rules = collect_matching_rules(element, CascadeOrigin::UserAgent, pseudo_element, did_match_any_hover_rules); sort_matching_rules(matching_rule_set.user_agent_rules); - matching_rule_set.user_rules = collect_matching_rules(element, CascadeOrigin::User, pseudo_element); + matching_rule_set.user_rules = collect_matching_rules(element, CascadeOrigin::User, pseudo_element, did_match_any_hover_rules); sort_matching_rules(matching_rule_set.user_rules); // @layer-ed author rules for (auto const& layer_name : m_qualified_layer_names_in_order) { - auto layer_rules = collect_matching_rules(element, CascadeOrigin::Author, pseudo_element, layer_name); + auto layer_rules = collect_matching_rules(element, CascadeOrigin::Author, pseudo_element, did_match_any_hover_rules, layer_name); sort_matching_rules(layer_rules); matching_rule_set.author_rules.append({ layer_name, layer_rules }); } // Un-@layer-ed author rules - auto unlayered_author_rules = collect_matching_rules(element, CascadeOrigin::Author, pseudo_element); + auto unlayered_author_rules = collect_matching_rules(element, CascadeOrigin::Author, pseudo_element, did_match_any_hover_rules); sort_matching_rules(unlayered_author_rules); matching_rule_set.author_rules.append({ {}, unlayered_author_rules }); @@ -2299,7 +2298,8 @@ GC::Ptr StyleComputer::compute_style_impl(DOM::Element& elem // 1. Perform the cascade. This produces the "specified style" bool did_match_any_pseudo_element_rules = false; - auto cascaded_properties = compute_cascaded_values(element, pseudo_element, did_match_any_pseudo_element_rules, mode); + bool did_match_any_hover_rules = false; + auto cascaded_properties = compute_cascaded_values(element, pseudo_element, did_match_any_pseudo_element_rules, did_match_any_hover_rules, mode); element.set_cascaded_properties(pseudo_element, cascaded_properties); @@ -2332,7 +2332,10 @@ GC::Ptr StyleComputer::compute_style_impl(DOM::Element& elem } } - return compute_properties(element, pseudo_element, cascaded_properties); + auto computed_properties = compute_properties(element, pseudo_element, cascaded_properties); + if (did_match_any_hover_rules) + computed_properties->set_did_match_any_hover_rules(); + return computed_properties; } GC::Ref StyleComputer::compute_properties(DOM::Element& element, Optional pseudo_element, CascadedProperties& cascaded_properties) const diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index 66c789ed46e..0d213c12064 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -146,7 +146,7 @@ public: [[nodiscard]] GC::Ref compute_style(DOM::Element&, Optional = {}) const; [[nodiscard]] GC::Ptr compute_pseudo_element_style_if_needed(DOM::Element&, Optional) const; - Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional, FlyString const& qualified_layer_name = {}) const; + Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const; void invalidate_rule_cache(); @@ -191,7 +191,7 @@ private: [[nodiscard]] bool should_reject_with_ancestor_filter(Selector const&) const; [[nodiscard]] GC::Ptr compute_style_impl(DOM::Element&, Optional, ComputeStyleMode) const; - [[nodiscard]] GC::Ref compute_cascaded_values(DOM::Element&, Optional, bool& did_match_any_pseudo_element_rules, ComputeStyleMode) const; + [[nodiscard]] GC::Ref compute_cascaded_values(DOM::Element&, Optional, bool& did_match_any_pseudo_element_rules, bool& did_match_any_hover_rules, ComputeStyleMode) const; static RefPtr find_matching_font_weight_ascending(Vector const& candidates, int target_weight, float font_size_in_pt, bool inclusive); static RefPtr find_matching_font_weight_descending(Vector const& candidates, int target_weight, float font_size_in_pt, bool inclusive); RefPtr font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const; diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index ac6f4f534d5..3024797bb59 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -1494,10 +1494,22 @@ void Document::set_hovered_node(Node* node) m_hovered_node = node; auto* common_ancestor = find_common_ancestor(old_hovered_node, m_hovered_node); - if (common_ancestor) - common_ancestor->invalidate_style(StyleInvalidationReason::Hover); - else + if (!style_computer().has_has_selectors()) { + Node& invalidation_root = common_ancestor ? *common_ancestor : document(); + invalidation_root.for_each_in_inclusive_subtree([&](Node& node) { + if (!node.is_element()) + return TraversalDecision::Continue; + auto& element = static_cast(node); + if (element.affected_by_hover()) { + element.set_needs_style_update(true); + } else { + element.set_needs_inherited_style_update(true); + } + return TraversalDecision::Continue; + }); + } else { invalidate_style(StyleInvalidationReason::Hover); + } // https://w3c.github.io/uievents/#mouseout if (old_hovered_node && old_hovered_node != m_hovered_node) { diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 697f396df4e..aaabbbae035 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -742,7 +742,8 @@ WebIDL::ExceptionOr Element::matches(StringView selectors) const // 3. If the result of match a selector against an element, using s, this, and scoping root this, returns success, then return true; otherwise, return false. auto sel = maybe_selectors.value(); for (auto& s : sel) { - if (SelectorEngine::matches(s, {}, *this, nullptr, {}, static_cast(this))) + SelectorEngine::MatchContext context; + if (SelectorEngine::matches(s, *this, nullptr, context, {}, static_cast(this))) return true; } return false; @@ -761,7 +762,8 @@ WebIDL::ExceptionOr Element::closest(StringView selectors) auto matches_selectors = [this](CSS::SelectorList const& selector_list, Element const* element) { // 4. For each element in elements, if match a selector against an element, using s, element, and scoping root this, returns success, return element. for (auto const& selector : selector_list) { - if (SelectorEngine::matches(selector, {}, *element, nullptr, {}, this)) + SelectorEngine::MatchContext context; + if (SelectorEngine::matches(selector, *element, nullptr, context, {}, this)) return true; } return false; @@ -1123,6 +1125,22 @@ GC::Ptr Element::get_pseudo_element_node(CSS::Selector::P return nullptr; } +bool Element::affected_by_hover() const +{ + if (m_computed_properties && m_computed_properties->did_match_any_hover_rules()) { + return true; + } + if (m_pseudo_element_data) { + for (auto& pseudo_element : *m_pseudo_element_data) { + if (!pseudo_element.computed_properties) + continue; + if (pseudo_element.computed_properties->did_match_any_hover_rules()) + return true; + } + } + return false; +} + bool Element::has_pseudo_elements() const { if (m_pseudo_element_data) { diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 070f7046691..7a469063a1a 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -261,6 +261,8 @@ public: static GC::Ptr create_layout_node_for_display_type(DOM::Document&, CSS::Display const&, GC::Ref, Element*); + bool affected_by_hover() const; + void set_pseudo_element_node(Badge, CSS::Selector::PseudoElement::Type, GC::Ptr); GC::Ptr get_pseudo_element_node(CSS::Selector::PseudoElement::Type) const; bool has_pseudo_elements() const; diff --git a/Libraries/LibWeb/DOM/ParentNode.cpp b/Libraries/LibWeb/DOM/ParentNode.cpp index 4f1a23d4f12..ef5392d9b2e 100644 --- a/Libraries/LibWeb/DOM/ParentNode.cpp +++ b/Libraries/LibWeb/DOM/ParentNode.cpp @@ -70,7 +70,8 @@ static WebIDL::ExceptionOr, GC::Ref>> scope_m // FIXME: This should be shadow-including. https://drafts.csswg.org/selectors-4/#match-a-selector-against-a-tree node.for_each_in_subtree_of_type([&](auto& element) { for (auto& selector : selectors) { - if (SelectorEngine::matches(selector, {}, element, nullptr, {}, node)) { + SelectorEngine::MatchContext context; + if (SelectorEngine::matches(selector, element, nullptr, context, {}, node)) { if (return_matches == ReturnMatches::First) { single_result = &element; return TraversalDecision::Break;