From c5f2a88f69f89bbfd18bb456f55ad63ea5622d20 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Sun, 12 Jan 2025 18:38:05 +0300 Subject: [PATCH] LibWeb: Use invalidation sets to reduce style recalculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Libraries/LibWeb/CMakeLists.txt | 2 + Libraries/LibWeb/CSS/InvalidationSet.cpp | 90 ++++++++ Libraries/LibWeb/CSS/InvalidationSet.h | 77 +++++++ Libraries/LibWeb/CSS/StyleComputer.cpp | 48 +++++ Libraries/LibWeb/CSS/StyleComputer.h | 5 + .../LibWeb/CSS/StyleInvalidationData.cpp | 192 ++++++++++++++++++ Libraries/LibWeb/CSS/StyleInvalidationData.h | 25 +++ Libraries/LibWeb/DOM/Element.cpp | 50 ++++- Libraries/LibWeb/DOM/Element.h | 3 +- Libraries/LibWeb/DOM/Node.cpp | 58 ++++++ Libraries/LibWeb/DOM/Node.h | 2 + 11 files changed, 549 insertions(+), 3 deletions(-) create mode 100644 Libraries/LibWeb/CSS/InvalidationSet.cpp create mode 100644 Libraries/LibWeb/CSS/InvalidationSet.h create mode 100644 Libraries/LibWeb/CSS/StyleInvalidationData.cpp create mode 100644 Libraries/LibWeb/CSS/StyleInvalidationData.h diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 247581966f..97bcb85204 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/CSS/InvalidationSet.cpp b/Libraries/LibWeb/CSS/InvalidationSet.cpp new file mode 100644 index 0000000000..8314feedee --- /dev/null +++ b/Libraries/LibWeb/CSS/InvalidationSet.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +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 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::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 Formatter::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 Formatter::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 {}; +} + +} diff --git a/Libraries/LibWeb/CSS/InvalidationSet.h b/Libraries/LibWeb/CSS/InvalidationSet.h new file mode 100644 index 0000000000..3799b713b0 --- /dev/null +++ b/Libraries/LibWeb/CSS/InvalidationSet.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +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 const& callback) const; + +private: + bool m_needs_invalidate_self { false }; + bool m_needs_invalidate_whole_subtree { false }; + HashTable m_properties; +}; + +} + +namespace AK { + +template<> +struct Traits : DefaultTraits { + 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 : Formatter { + ErrorOr format(FormatBuilder&, Web::CSS::InvalidationSet::Property const&); +}; + +template<> +struct Formatter : Formatter { + ErrorOr format(FormatBuilder&, Web::CSS::InvalidationSet const&); +}; + +} diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index f06e1ab782..1bd57a09a9 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -427,6 +428,46 @@ Vector const& StyleComputer::get_hover_rules() const return m_hover_rules; } +InvalidationSet StyleComputer::invalidation_set_for_properties(Vector 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 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(); @@ -2576,6 +2617,11 @@ NonnullOwnPtr StyleComputer::make_rule_cache_for_casca return static_cast(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(); + m_style_invalidation_data = make(); 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&) diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index 2c60e60c1c..54492fdbf9 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -149,6 +150,9 @@ public: Vector const& get_hover_rules() const; Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const; + InvalidationSet invalidation_set_for_properties(Vector 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 m_selector_insights; Vector m_hover_rules; + OwnPtr m_style_invalidation_data; OwnPtr m_author_rule_cache; OwnPtr m_user_rule_cache; OwnPtr m_user_agent_rule_cache; diff --git a/Libraries/LibWeb/CSS/StyleInvalidationData.cpp b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp new file mode 100644 index 0000000000..14b9ec26d1 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +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 +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 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 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; +} + +} diff --git a/Libraries/LibWeb/CSS/StyleInvalidationData.h b/Libraries/LibWeb/CSS/StyleInvalidationData.h new file mode 100644 index 0000000000..fb33bada9b --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleInvalidationData.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::CSS { + +struct StyleInvalidationData { + HashMap descendant_invalidation_sets; + HashTable ids_used_in_has_selectors; + HashTable class_names_used_in_has_selectors; + HashTable attribute_names_used_in_has_selectors; + HashTable tag_names_used_in_has_selectors; + + InvalidationSet build_invalidation_sets_for_selector(Selector const& selector); +}; + +} diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 0d8f8f4a54..cbc837a988 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -462,7 +462,7 @@ void Element::run_attribute_change_steps(FlyString const& local_name, Optional const& old_value, Optional 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 old_classes; + Vector 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 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); diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 8535e8ca36..cf1e31aba2 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -263,6 +263,7 @@ public: static GC::Ptr create_layout_node_for_display_type(DOM::Document&, CSS::Display const&, GC::Ref, Element*); bool affected_by_hover() const; + bool affected_by_invalidation_property(CSS::InvalidationSet::Property const&) const; void set_pseudo_element_node(Badge, CSS::Selector::PseudoElement::Type, GC::Ptr); GC::Ptr 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 const& old_value, Optional const& new_value); WebIDL::ExceptionOr> insert_adjacent(StringView where, GC::Ref node); diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index b87db9fd43..d247024256 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -470,6 +470,64 @@ void Node::invalidate_style(StyleInvalidationReason reason) document().schedule_style_update(); } +void Node::invalidate_style(StyleInvalidationReason reason, Vector 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(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(*this)) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index 587e0405f0..bd21c40e61 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -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 const&); void set_document(Badge, Document&);