mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-22 09:12:13 -05:00
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:
parent
58c78cb003
commit
c5f2a88f69
Notes:
github-actions[bot]
2025-01-19 18:55:55 +00:00
Author: https://github.com/kalenikaliaksandr Commit: https://github.com/LadybirdBrowser/ladybird/commit/c5f2a88f69f Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3292 Reviewed-by: https://github.com/awesomekling
11 changed files with 549 additions and 3 deletions
|
@ -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
|
||||
|
|
90
Libraries/LibWeb/CSS/InvalidationSet.cpp
Normal file
90
Libraries/LibWeb/CSS/InvalidationSet.cpp
Normal 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 {};
|
||||
}
|
||||
|
||||
}
|
77
Libraries/LibWeb/CSS/InvalidationSet.h
Normal file
77
Libraries/LibWeb/CSS/InvalidationSet.h
Normal 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&);
|
||||
};
|
||||
|
||||
}
|
|
@ -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&)
|
||||
|
|
|
@ -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;
|
||||
|
|
192
Libraries/LibWeb/CSS/StyleInvalidationData.cpp
Normal file
192
Libraries/LibWeb/CSS/StyleInvalidationData.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
25
Libraries/LibWeb/CSS/StyleInvalidationData.h
Normal file
25
Libraries/LibWeb/CSS/StyleInvalidationData.h
Normal 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);
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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&);
|
||||
|
||||
|
|
Loading…
Reference in a new issue