LibWeb: Use invalidation sets to reduce style recalculation

Implements idea described in
https://docs.google.com/document/d/1vEW86DaeVs4uQzNFI5R-_xS9TcS1Cs_EUsHRSgCHGu8

Invalidation sets are used to reduce the number of elements marked for
style recalculation by collecting metadata from style rules about the
dependencies between properties that could affect an element’s style.

Currently, this optimization is only applied to style invalidation
triggered by class list mutations on an element.
This commit is contained in:
Aliaksandr Kalenik 2025-01-12 18:38:05 +03:00 committed by Andreas Kling
parent 58c78cb003
commit c5f2a88f69
Notes: github-actions[bot] 2025-01-19 18:55:55 +00:00
11 changed files with 549 additions and 3 deletions

View file

@ -82,6 +82,7 @@ set(SOURCES
CSS/GridTrackPlacement.cpp
CSS/GridTrackSize.cpp
CSS/Interpolation.cpp
CSS/InvalidationSet.cpp
CSS/Length.cpp
CSS/LengthBox.cpp
CSS/MediaList.cpp
@ -116,6 +117,7 @@ set(SOURCES
CSS/Sizing.cpp
CSS/StyleComputer.cpp
CSS/StyleInvalidation.cpp
CSS/StyleInvalidationData.cpp
CSS/StyleProperty.cpp
CSS/StyleSheet.cpp
CSS/StyleSheetIdentifier.cpp

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/InvalidationSet.h>
namespace Web::CSS {
void InvalidationSet::include_all_from(InvalidationSet const& other)
{
m_needs_invalidate_self |= other.m_needs_invalidate_self;
m_needs_invalidate_whole_subtree |= other.m_needs_invalidate_whole_subtree;
for (auto const& property : other.m_properties)
m_properties.set(property);
}
bool InvalidationSet::is_empty() const
{
return !m_needs_invalidate_self && !m_needs_invalidate_whole_subtree && m_properties.is_empty();
}
void InvalidationSet::for_each_property(Function<void(Property const&)> const& callback) const
{
if (m_needs_invalidate_self)
callback({ Property::Type::InvalidateSelf });
if (m_needs_invalidate_whole_subtree)
callback({ Property::Type::InvalidateWholeSubtree });
for (auto const& property : m_properties)
callback(property);
}
}
namespace AK {
unsigned Traits<Web::CSS::InvalidationSet::Property>::hash(Web::CSS::InvalidationSet::Property const& invalidation_set_property)
{
return pair_int_hash(to_underlying(invalidation_set_property.type), invalidation_set_property.name.hash());
}
ErrorOr<void> Formatter<Web::CSS::InvalidationSet::Property>::format(FormatBuilder& builder, Web::CSS::InvalidationSet::Property const& invalidation_set_property)
{
switch (invalidation_set_property.type) {
case Web::CSS::InvalidationSet::Property::Type::InvalidateSelf: {
TRY(builder.put_string("$"sv));
return {};
}
case Web::CSS::InvalidationSet::Property::Type::Class: {
TRY(builder.put_string("."sv));
TRY(builder.put_string(invalidation_set_property.name));
return {};
}
case Web::CSS::InvalidationSet::Property::Type::Id: {
TRY(builder.put_string("#"sv));
TRY(builder.put_string(invalidation_set_property.name));
return {};
}
case Web::CSS::InvalidationSet::Property::Type::TagName: {
TRY(builder.put_string(invalidation_set_property.name));
return {};
}
case Web::CSS::InvalidationSet::Property::Type::Attribute: {
TRY(builder.put_string("["sv));
TRY(builder.put_string(invalidation_set_property.name));
TRY(builder.put_string("]"sv));
return {};
}
case Web::CSS::InvalidationSet::Property::Type::InvalidateWholeSubtree: {
TRY(builder.put_string("*"sv));
return {};
}
default:
VERIFY_NOT_REACHED();
}
}
ErrorOr<void> Formatter<Web::CSS::InvalidationSet>::format(FormatBuilder& builder, Web::CSS::InvalidationSet const& invalidation_set)
{
bool first = true;
invalidation_set.for_each_property([&](auto const& property) {
if (!first)
builder.builder().append(", "sv);
builder.builder().appendff("{}", property);
});
return {};
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <AK/Format.h>
#include <AK/Forward.h>
#include <AK/HashTable.h>
#include <AK/Traits.h>
namespace Web::CSS {
class InvalidationSet {
public:
struct Property {
enum class Type : u8 {
InvalidateSelf,
InvalidateWholeSubtree,
Class,
Id,
TagName,
Attribute,
};
Type type;
FlyString name {};
bool operator==(Property const& other) const = default;
};
void include_all_from(InvalidationSet const& other);
bool needs_invalidate_self() const { return m_needs_invalidate_self; }
void set_needs_invalidate_self() { m_needs_invalidate_self = true; }
bool needs_invalidate_whole_subtree() const { return m_needs_invalidate_whole_subtree; }
void set_needs_invalidate_whole_subtree() { m_needs_invalidate_whole_subtree = true; }
void set_needs_invalidate_class(FlyString const& name) { m_properties.set({ Property::Type::Class, name }); }
void set_needs_invalidate_id(FlyString const& name) { m_properties.set({ Property::Type::Id, name }); }
void set_needs_invalidate_tag_name(FlyString const& name) { m_properties.set({ Property::Type::TagName, name }); }
void set_needs_invalidate_attribute(FlyString const& name) { m_properties.set({ Property::Type::Attribute, name }); }
bool is_empty() const;
void for_each_property(Function<void(Property const&)> const& callback) const;
private:
bool m_needs_invalidate_self { false };
bool m_needs_invalidate_whole_subtree { false };
HashTable<Property> m_properties;
};
}
namespace AK {
template<>
struct Traits<Web::CSS::InvalidationSet::Property> : DefaultTraits<String> {
static unsigned hash(Web::CSS::InvalidationSet::Property const&);
static bool equals(Web::CSS::InvalidationSet::Property const& a, Web::CSS::InvalidationSet::Property const& b) { return a == b; }
};
template<>
struct Formatter<Web::CSS::InvalidationSet::Property> : Formatter<StringView> {
ErrorOr<void> format(FormatBuilder&, Web::CSS::InvalidationSet::Property const&);
};
template<>
struct Formatter<Web::CSS::InvalidationSet> : Formatter<StringView> {
ErrorOr<void> format(FormatBuilder&, Web::CSS::InvalidationSet const&);
};
}

View file

@ -37,6 +37,7 @@
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/CSSTransition.h>
#include <LibWeb/CSS/Interpolation.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/CSS/StyleComputer.h>
@ -427,6 +428,46 @@ Vector<MatchingRule> const& StyleComputer::get_hover_rules() const
return m_hover_rules;
}
InvalidationSet StyleComputer::invalidation_set_for_properties(Vector<InvalidationSet::Property> const& properties) const
{
if (!m_style_invalidation_data)
return {};
auto const& descendant_invalidation_sets = m_style_invalidation_data->descendant_invalidation_sets;
InvalidationSet result;
for (auto const& property : properties) {
if (auto it = descendant_invalidation_sets.find(property); it != descendant_invalidation_sets.end())
result.include_all_from(it->value);
}
return result;
}
bool StyleComputer::invalidation_property_used_in_has_selector(InvalidationSet::Property const& property) const
{
if (!m_style_invalidation_data)
return true;
switch (property.type) {
case InvalidationSet::Property::Type::Id:
if (m_style_invalidation_data->ids_used_in_has_selectors.contains(property.name))
return true;
break;
case InvalidationSet::Property::Type::Class:
if (m_style_invalidation_data->class_names_used_in_has_selectors.contains(property.name))
return true;
break;
case InvalidationSet::Property::Type::Attribute:
if (m_style_invalidation_data->attribute_names_used_in_has_selectors.contains(property.name))
return true;
break;
case InvalidationSet::Property::Type::TagName:
if (m_style_invalidation_data->tag_names_used_in_has_selectors.contains(property.name))
return true;
break;
default:
break;
}
return false;
}
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();
@ -2576,6 +2617,11 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca
return static_cast<CSSNestedDeclarations const&>(rule).parent_style_rule().absolutized_selectors();
VERIFY_NOT_REACHED();
}();
for (auto const& selector : absolutized_selectors) {
m_style_invalidation_data->build_invalidation_sets_for_selector(selector);
}
for (CSS::Selector const& selector : absolutized_selectors) {
MatchingRule matching_rule {
shadow_root,
@ -2842,6 +2888,7 @@ void StyleComputer::build_qualified_layer_names_cache()
void StyleComputer::build_rule_cache()
{
m_selector_insights = make<SelectorInsights>();
m_style_invalidation_data = make<StyleInvalidationData>();
if (auto user_style_source = document().page().user_style(); user_style_source.has_value()) {
m_user_style_sheet = GC::make_root(parse_css_stylesheet(CSS::Parser::ParsingContext(document()), user_style_source.value()));
@ -2869,6 +2916,7 @@ void StyleComputer::invalidate_rule_cache()
m_user_agent_rule_cache = nullptr;
m_hover_rules.clear_with_capacity();
m_style_invalidation_data = nullptr;
}
void StyleComputer::did_load_font(FlyString const&)

View file

@ -19,6 +19,7 @@
#include <LibWeb/CSS/CascadedProperties.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleInvalidationData.h>
#include <LibWeb/Forward.h>
#include <LibWeb/Loader/ResourceLoader.h>
@ -149,6 +150,9 @@ public:
Vector<MatchingRule> const& get_hover_rules() 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;
InvalidationSet invalidation_set_for_properties(Vector<InvalidationSet::Property> const&) const;
bool invalidation_property_used_in_has_selector(InvalidationSet::Property const&) const;
void invalidate_rule_cache();
Gfx::Font const& initial_font() const;
@ -278,6 +282,7 @@ private:
OwnPtr<SelectorInsights> m_selector_insights;
Vector<MatchingRule> m_hover_rules;
OwnPtr<StyleInvalidationData> m_style_invalidation_data;
OwnPtr<RuleCache> m_author_rule_cache;
OwnPtr<RuleCache> m_user_rule_cache;
OwnPtr<RuleCache> m_user_agent_rule_cache;

View file

@ -0,0 +1,192 @@
/*
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/GenericShorthands.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleInvalidationData.h>
namespace Web::CSS {
// Iterates over the given selector, grouping consecutive simple selectors that have no combinator (Combinator::None).
// For example, given "div:not(.a) + .b[foo]", the callback is invoked twice:
// once for "div:not(.a)" and once for ".b[foo]".
template<typename Callback>
static void for_each_consecutive_simple_selector_group(Selector const& selector, Callback callback)
{
auto const& compound_selectors = selector.compound_selectors();
int compound_selector_index = compound_selectors.size() - 1;
Vector<Selector::SimpleSelector const&> simple_selectors;
Selector::Combinator combinator = Selector::Combinator::None;
bool is_rightmost = true;
while (compound_selector_index >= 0) {
if (!simple_selectors.is_empty()) {
callback(simple_selectors, combinator, is_rightmost);
simple_selectors.clear();
is_rightmost = false;
}
auto const& compound_selector = compound_selectors[compound_selector_index];
for (auto const& simple_selector : compound_selector.simple_selectors) {
simple_selectors.append(simple_selector);
}
combinator = compound_selector.combinator;
--compound_selector_index;
}
if (!simple_selectors.is_empty()) {
callback(simple_selectors, combinator, is_rightmost);
}
}
static void collect_properties_used_in_has(Selector::SimpleSelector const& selector, StyleInvalidationData& style_invalidation_data, bool in_has)
{
switch (selector.type) {
case Selector::SimpleSelector::Type::Id: {
if (in_has)
style_invalidation_data.ids_used_in_has_selectors.set(selector.name());
break;
}
case Selector::SimpleSelector::Type::Class: {
if (in_has)
style_invalidation_data.class_names_used_in_has_selectors.set(selector.name());
break;
}
case Selector::SimpleSelector::Type::Attribute: {
if (in_has)
style_invalidation_data.attribute_names_used_in_has_selectors.set(selector.attribute().qualified_name.name.lowercase_name);
break;
}
case Selector::SimpleSelector::Type::TagName: {
if (in_has)
style_invalidation_data.tag_names_used_in_has_selectors.set(selector.qualified_name().name.lowercase_name);
break;
}
case Selector::SimpleSelector::Type::PseudoClass: {
auto const& pseudo_class = selector.pseudo_class();
for (auto const& child_selector : pseudo_class.argument_selector_list) {
for (auto const& compound_selector : child_selector->compound_selectors()) {
for (auto const& simple_selector : compound_selector.simple_selectors) {
collect_properties_used_in_has(simple_selector, style_invalidation_data, in_has || pseudo_class.type == PseudoClass::Has);
}
}
}
break;
}
default:
break;
}
}
enum class ExcludePropertiesNestedInNotPseudoClass : bool {
No,
Yes,
};
static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& rule_invalidation_data)
{
switch (selector.type) {
case Selector::SimpleSelector::Type::Class:
invalidation_set.set_needs_invalidate_class(selector.name());
break;
case Selector::SimpleSelector::Type::Id:
invalidation_set.set_needs_invalidate_id(selector.name());
break;
case Selector::SimpleSelector::Type::TagName:
invalidation_set.set_needs_invalidate_tag_name(selector.qualified_name().name.lowercase_name);
break;
case Selector::SimpleSelector::Type::Attribute:
invalidation_set.set_needs_invalidate_attribute(selector.attribute().qualified_name.name.lowercase_name);
break;
case Selector::SimpleSelector::Type::PseudoClass: {
auto const& pseudo_class = selector.pseudo_class();
if (pseudo_class.type == PseudoClass::Has)
break;
if (exclude_properties_nested_in_not_pseudo_class == ExcludePropertiesNestedInNotPseudoClass::Yes && pseudo_class.type == PseudoClass::Not)
break;
for (auto const& nested_selector : pseudo_class.argument_selector_list) {
auto rightmost_invalidation_set_for_selector = rule_invalidation_data.build_invalidation_sets_for_selector(*nested_selector);
invalidation_set.include_all_from(rightmost_invalidation_set_for_selector);
}
break;
}
default:
break;
}
}
InvalidationSet StyleInvalidationData::build_invalidation_sets_for_selector(Selector const& selector)
{
auto const& compound_selectors = selector.compound_selectors();
int compound_selector_index = compound_selectors.size() - 1;
VERIFY(compound_selector_index >= 0);
InvalidationSet invalidation_set_for_rightmost_selector;
Selector::Combinator previous_compound_combinator = Selector::Combinator::None;
for_each_consecutive_simple_selector_group(selector, [&](Vector<Selector::SimpleSelector const&> const& simple_selectors, Selector::Combinator combinator, bool is_rightmost) {
// Collect properties used in :has() so we can decide if only specific properties
// trigger descendant invalidation or if the entire document must be invalidated.
for (auto const& simple_selector : simple_selectors) {
bool in_has = false;
if (simple_selector.type == Selector::SimpleSelector::Type::PseudoClass) {
auto const& pseudo_class = simple_selector.pseudo_class();
if (pseudo_class.type == PseudoClass::Has)
in_has = true;
}
collect_properties_used_in_has(simple_selector, *this, in_has);
}
if (is_rightmost) {
// The rightmost selector is handled twice:
// 1) Include properties nested in :not()
// 2) Exclude properties nested in :not()
//
// This ensures we handle cases like:
// :not(.foo) => produce invalidation set .foo { $ } ($ = invalidate self)
// .bar :not(.foo) => produce invalidation sets .foo { $ } and .bar { * } (* = invalidate subtree)
// which means invalidation_set_for_rightmost_selector should be empty
for (auto const& simple_selector : simple_selectors) {
InvalidationSet s;
build_invalidation_sets_for_simple_selector(simple_selector, s, ExcludePropertiesNestedInNotPseudoClass::No, *this);
s.for_each_property([&](auto const& invalidation_property) {
auto& descendant_invalidation_set = descendant_invalidation_sets.ensure(invalidation_property, [] { return InvalidationSet {}; });
descendant_invalidation_set.set_needs_invalidate_self();
});
}
for (auto const& simple_selector : simple_selectors) {
build_invalidation_sets_for_simple_selector(simple_selector, invalidation_set_for_rightmost_selector, ExcludePropertiesNestedInNotPseudoClass::Yes, *this);
}
} else {
VERIFY(previous_compound_combinator != Selector::Combinator::None);
for (auto const& simple_selector : simple_selectors) {
InvalidationSet s;
build_invalidation_sets_for_simple_selector(simple_selector, s, ExcludePropertiesNestedInNotPseudoClass::No, *this);
s.for_each_property([&](auto const& invalidation_property) {
auto& descendant_invalidation_set = descendant_invalidation_sets.ensure(invalidation_property, [] {
return InvalidationSet {};
});
// If the rightmost selector's invalidation set is empty, it means there's no
// specific property-based invalidation, so we fall back to invalidating the whole subtree.
// If combinator to the right of current compound selector is NextSibling or SubsequentSibling,
// we also need to invalidate the whole subtree, because we don't support sibling invalidation sets.
if (AK::first_is_one_of(previous_compound_combinator, Selector::Combinator::NextSibling, Selector::Combinator::SubsequentSibling)) {
descendant_invalidation_set.set_needs_invalidate_whole_subtree();
} else if (invalidation_set_for_rightmost_selector.is_empty()) {
descendant_invalidation_set.set_needs_invalidate_whole_subtree();
} else {
descendant_invalidation_set.include_all_from(invalidation_set_for_rightmost_selector);
}
});
}
}
previous_compound_combinator = combinator;
});
return invalidation_set_for_rightmost_selector;
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/Forward.h>
namespace Web::CSS {
struct StyleInvalidationData {
HashMap<InvalidationSet::Property, InvalidationSet> descendant_invalidation_sets;
HashTable<FlyString> ids_used_in_has_selectors;
HashTable<FlyString> class_names_used_in_has_selectors;
HashTable<FlyString> attribute_names_used_in_has_selectors;
HashTable<FlyString> tag_names_used_in_has_selectors;
InvalidationSet build_invalidation_sets_for_selector(Selector const& selector);
};
}

View file

@ -462,7 +462,7 @@ void Element::run_attribute_change_steps(FlyString const& local_name, Optional<S
attribute_changed(local_name, old_value, value, namespace_);
if (old_value != value) {
invalidate_style_after_attribute_change(local_name);
invalidate_style_after_attribute_change(local_name, old_value, value);
document().bump_dom_tree_version();
}
}
@ -1142,6 +1142,29 @@ bool Element::affected_by_hover() const
return false;
}
bool Element::affected_by_invalidation_property(CSS::InvalidationSet::Property const& property) const
{
switch (property.type) {
case CSS::InvalidationSet::Property::Type::Class:
return m_classes.contains_slow(property.name);
case CSS::InvalidationSet::Property::Type::Id:
return m_id == property.name;
case CSS::InvalidationSet::Property::Type::TagName:
return local_name() == property.name;
case CSS::InvalidationSet::Property::Type::Attribute: {
if (property.name == HTML::AttributeNames::id || property.name == HTML::AttributeNames::class_)
return true;
return has_attribute(property.name);
}
case CSS::InvalidationSet::Property::Type::InvalidateSelf:
return false;
case CSS::InvalidationSet::Property::Type::InvalidateWholeSubtree:
return true;
default:
VERIFY_NOT_REACHED();
}
}
bool Element::has_pseudo_elements() const
{
if (m_pseudo_element_data) {
@ -1948,7 +1971,7 @@ static bool attribute_name_may_affect_selectors(Element const& element, FlyStrin
return element.document().style_computer().has_attribute_selector(attribute_name);
}
void Element::invalidate_style_after_attribute_change(FlyString const& attribute_name)
void Element::invalidate_style_after_attribute_change(FlyString const& attribute_name, Optional<String> const& old_value, Optional<String> const& new_value)
{
// FIXME: Only invalidate if the attribute can actually affect style.
@ -1970,6 +1993,29 @@ void Element::invalidate_style_after_attribute_change(FlyString const& attribute
return;
}
if (attribute_name == HTML::AttributeNames::class_) {
Vector<StringView> old_classes;
Vector<StringView> new_classes;
if (old_value.has_value())
old_classes = old_value->bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);
if (new_value.has_value())
new_classes = new_value->bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);
Vector<CSS::InvalidationSet::Property> changed_properties;
for (auto& old_class : old_classes) {
if (!new_classes.contains_slow(old_class)) {
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Class, .name = FlyString::from_utf8_without_validation(old_class.bytes()) });
}
}
for (auto& new_class : new_classes) {
if (!old_classes.contains_slow(new_class)) {
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Class, .name = FlyString::from_utf8_without_validation(new_class.bytes()) });
}
}
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Attribute, .name = HTML::AttributeNames::class_ });
invalidate_style(StyleInvalidationReason::ElementAttributeChange, changed_properties);
return;
}
if (is_presentational_hint(attribute_name)
|| attribute_name_may_affect_selectors(*this, attribute_name)) {
invalidate_style(StyleInvalidationReason::ElementAttributeChange);

View file

@ -263,6 +263,7 @@ 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;
bool affected_by_invalidation_property(CSS::InvalidationSet::Property const&) 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;
@ -420,7 +421,7 @@ protected:
private:
void make_html_uppercased_qualified_name();
void invalidate_style_after_attribute_change(FlyString const& attribute_name);
void invalidate_style_after_attribute_change(FlyString const& attribute_name, Optional<String> const& old_value, Optional<String> const& new_value);
WebIDL::ExceptionOr<GC::Ptr<Node>> insert_adjacent(StringView where, GC::Ref<Node> node);

View file

@ -470,6 +470,64 @@ void Node::invalidate_style(StyleInvalidationReason reason)
document().schedule_style_update();
}
void Node::invalidate_style(StyleInvalidationReason reason, Vector<CSS::InvalidationSet::Property> const& properties)
{
if (is_character_data())
return;
bool properties_used_in_has_selectors = false;
for (auto const& property : properties) {
properties_used_in_has_selectors |= document().style_computer().invalidation_property_used_in_has_selector(property);
}
if (properties_used_in_has_selectors) {
document().invalidate_style(reason);
return;
}
auto invalidation_set = document().style_computer().invalidation_set_for_properties(properties);
if (invalidation_set.is_empty())
return;
if (invalidation_set.needs_invalidate_self()) {
set_needs_style_update(true);
}
auto element_has_properties_from_invalidation_set = [&](Element& element) {
bool result = false;
invalidation_set.for_each_property([&](auto const& property) {
if (element.affected_by_invalidation_property(property))
result = true;
});
return result;
};
auto invalidate_entire_subtree = [&](Node& subtree_root) {
subtree_root.for_each_shadow_including_inclusive_descendant([&](Node& node) {
if (!node.is_element())
return TraversalDecision::Continue;
auto& element = static_cast<Element&>(node);
bool needs_style_recalculation = invalidation_set.needs_invalidate_whole_subtree() || element_has_properties_from_invalidation_set(element);
if (needs_style_recalculation) {
element.set_needs_style_update(true);
} else {
element.set_needs_inherited_style_update(true);
}
return TraversalDecision::Continue;
});
};
invalidate_entire_subtree(*this);
if (invalidation_set.needs_invalidate_whole_subtree()) {
for (auto* sibling = next_sibling(); sibling; sibling = sibling->next_sibling()) {
if (sibling->is_element())
invalidate_entire_subtree(*sibling);
}
}
document().schedule_style_update();
}
String Node::child_text_content() const
{
if (!is<ParentNode>(*this))

View file

@ -14,6 +14,7 @@
#include <AK/RefPtr.h>
#include <AK/TypeCasts.h>
#include <AK/Vector.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/DOM/AccessibilityTreeNode.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/DOM/Slottable.h>
@ -297,6 +298,7 @@ public:
void set_child_needs_style_update(bool b) { m_child_needs_style_update = b; }
void invalidate_style(StyleInvalidationReason);
void invalidate_style(StyleInvalidationReason, Vector<CSS::InvalidationSet::Property> const&);
void set_document(Badge<Document>, Document&);