LibWeb: Optimize :hover style invalidation

Instead of recalculating styles for all nodes in the common ancestor of
the new and old hovered nodes' subtrees, this change introduces the
following approach:
- While calculating ComputedProperties, a flag is saved if any rule
  applied to an element is affected by the hover state during the
  execution of SelectorEngine::matches().
- When the hovered element changes, styles are marked for recalculation
  only if the flag saved in ComputedProperties indicates that the
  element could be affected by the hover state.
This commit is contained in:
Aliaksandr Kalenik 2025-01-03 20:39:25 +03:00 committed by Andreas Kling
commit e465e922bd
Notes: github-actions[bot] 2025-01-04 19:33:47 +00:00
9 changed files with 124 additions and 77 deletions

View file

@ -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<Gfx::FontCascadeList> m_font_list;
Optional<CSSPixels> m_line_height;
bool m_did_match_any_hover_rules { false };
};
}

View file

@ -35,7 +35,7 @@
namespace Web::SelectorEngine {
static inline bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, int component_list_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> anchor = nullptr);
static inline bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, GC::Ref<DOM::Element const> anchor)
static inline bool matches_relative_selector(CSS::Selector const& selector, size_t compound_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ref<DOM::Element const> 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<DOM::Element const&>(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<DOM::Element const&>(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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& anchor, GC::Ptr<DOM::Element const> shadow_host)
static inline bool matches_has_pseudo_class(CSS::Selector const& selector, DOM::Element const& anchor, GC::Ptr<DOM::Element const> 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<CSS::CSSStyleSheet const&> 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<CSS::CSSStyleSheet const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element)
static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] GC::Ptr<CSS::CSSStyleSheet const> 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<DOM::Element const> element, GC::Ptr<DOM::Element const> shadow_host, CSS::SelectorList const& argument_selector_list, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule)
static inline bool matches_host_pseudo_class(GC::Ref<DOM::Element const> element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, CSS::SelectorList const& argument_selector_list)
{
// When evaluated in the context of a shadow tree, it matches the shadow trees 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<DOM::Element const> element
// NOTE: There's either 0 or 1 argument selector, since the syntax is :host or :host(<compound-selector>)
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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind)
static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> 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<HTML::HTMLHtmlElement>(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<HTML::HTMLHtmlElement>(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<CSS::CSSStyleSheet const&> style_sheet_for_rule)
GC::Ptr<CSS::CSSStyleSheet const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, [[maybe_unused]] GC::Ptr<DOM::Element const> anchor)
static inline bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, [[maybe_unused]] GC::Ptr<DOM::Element const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, int component_list_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> anchor)
bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> 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<CSS::CSSStyleSheet const&>
continue;
if (ancestor == anchor)
return false;
if (matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast<DOM::Element const&>(*ancestor), shadow_host, scope, selector_kind, anchor))
if (matches(selector, component_list_index - 1, static_cast<DOM::Element const&>(*ancestor), shadow_host, context, scope, selector_kind, anchor))
return true;
}
return false;
@ -870,17 +871,17 @@ bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&>
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<DOM::Element const&>(*parent), shadow_host, scope, selector_kind, anchor);
return matches(selector, component_list_index - 1, static_cast<DOM::Element const&>(*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<CSS::CSSStyleSheet const&>
VERIFY_NOT_REACHED();
}
bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> anchor)
bool matches(CSS::Selector const& selector, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host)
static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, DOM::Element const& element, GC::Ptr<DOM::Element const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host)
static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, DOM::Element const& element, GC::Ptr<DOM::Element const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element_to_match, GC::Ptr<DOM::Element const> shadow_host)
bool fast_matches(CSS::Selector const& selector, DOM::Element const& element_to_match, GC::Ptr<DOM::Element const> 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, Optional<CSS::CSSStyleSheet con
backtrack_state = { current->parent_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, Optional<CSS::CSSStyleSheet con
current = current->parent_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;

View file

@ -16,9 +16,14 @@ enum class SelectorKind {
Relative,
};
bool matches(CSS::Selector const&, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const&, GC::Ptr<DOM::Element const> shadow_host, Optional<CSS::Selector::PseudoElement::Type> = {}, GC::Ptr<DOM::ParentNode const> scope = {}, SelectorKind selector_kind = SelectorKind::Normal, GC::Ptr<DOM::Element const> anchor = nullptr);
struct MatchContext {
GC::Ptr<CSS::CSSStyleSheet const> style_sheet_for_rule {};
bool did_match_any_hover_rules { false };
};
[[nodiscard]] bool fast_matches(CSS::Selector const&, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const&, GC::Ptr<DOM::Element const> shadow_host);
bool matches(CSS::Selector const&, DOM::Element const&, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, Optional<CSS::Selector::PseudoElement::Type> = {}, GC::Ptr<DOM::ParentNode const> scope = {}, SelectorKind selector_kind = SelectorKind::Normal, GC::Ptr<DOM::Element const> anchor = nullptr);
[[nodiscard]] bool fast_matches(CSS::Selector const&, DOM::Element const&, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context);
[[nodiscard]] bool can_use_fast_matches(CSS::Selector const&);
[[nodiscard]] bool matches_hover_pseudo_class(DOM::Element const&);

View file

@ -422,7 +422,7 @@ bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector)
return false;
}
Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, FlyString const& qualified_layer_name) const
Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name) const
{
auto const& root_node = element.root();
auto shadow_root = is<DOM::ShadowRoot>(root_node) ? static_cast<DOM::ShadowRoot const*>(&root_node) : nullptr;
@ -435,22 +435,16 @@ Vector<MatchingRule> 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<MatchingRule, 512> rules_to_run;
auto add_rules_to_run = [&](Vector<MatchingRule> 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<MatchingRule> 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<CascadedProperties> StyleComputer::compute_cascaded_values(DOM::Element& element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const
GC::Ref<CascadedProperties> StyleComputer::compute_cascaded_values(DOM::Element& element, Optional<CSS::Selector::PseudoElement::Type> 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<CascadedProperties>();
// 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<ComputedProperties> 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<ComputedProperties> 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<ComputedProperties> StyleComputer::compute_properties(DOM::Element& element, Optional<Selector::PseudoElement::Type> pseudo_element, CascadedProperties& cascaded_properties) const

View file

@ -146,7 +146,7 @@ public:
[[nodiscard]] GC::Ref<ComputedProperties> compute_style(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type> = {}) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_pseudo_element_style_if_needed(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>) const;
Vector<MatchingRule> collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional<CSS::Selector::PseudoElement::Type>, FlyString const& qualified_layer_name = {}) const;
Vector<MatchingRule> collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional<CSS::Selector::PseudoElement::Type>, 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<ComputedProperties> compute_style_impl(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>, ComputeStyleMode) const;
[[nodiscard]] GC::Ref<CascadedProperties> compute_cascaded_values(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>, bool& did_match_any_pseudo_element_rules, ComputeStyleMode) const;
[[nodiscard]] GC::Ref<CascadedProperties> compute_cascaded_values(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>, bool& did_match_any_pseudo_element_rules, bool& did_match_any_hover_rules, ComputeStyleMode) const;
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive);
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive);
RefPtr<Gfx::FontCascadeList const> font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const;

View file

@ -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<Element&>(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) {

View file

@ -742,7 +742,8 @@ WebIDL::ExceptionOr<bool> 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<ParentNode const*>(this)))
SelectorEngine::MatchContext context;
if (SelectorEngine::matches(s, *this, nullptr, context, {}, static_cast<ParentNode const*>(this)))
return true;
}
return false;
@ -761,7 +762,8 @@ WebIDL::ExceptionOr<DOM::Element const*> 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<Layout::NodeWithStyle> 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) {

View file

@ -261,6 +261,8 @@ public:
static GC::Ptr<Layout::NodeWithStyle> create_layout_node_for_display_type(DOM::Document&, CSS::Display const&, GC::Ref<CSS::ComputedProperties>, Element*);
bool affected_by_hover() const;
void set_pseudo_element_node(Badge<Layout::TreeBuilder>, CSS::Selector::PseudoElement::Type, GC::Ptr<Layout::NodeWithStyle>);
GC::Ptr<Layout::NodeWithStyle> get_pseudo_element_node(CSS::Selector::PseudoElement::Type) const;
bool has_pseudo_elements() const;

View file

@ -70,7 +70,8 @@ static WebIDL::ExceptionOr<Variant<GC::Ptr<Element>, GC::Ref<NodeList>>> 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<Element>([&](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;