diff --git a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp index 55cb35d794c..950e41d641f 100644 --- a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp +++ b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp @@ -871,6 +871,27 @@ void KeyframeEffect::visit_edges(Cell::Visitor& visitor) visitor.visit(keyframe); } +static CSS::RequiredInvalidationAfterStyleChange compute_required_invalidation(HashMap> const& old_properties, HashMap> const& new_properties) +{ + CSS::RequiredInvalidationAfterStyleChange invalidation; + auto old_and_new_properties = MUST(Bitmap::create(to_underlying(CSS::last_property_id) + 1, 0)); + for (auto const& [property_id, _] : old_properties) + old_and_new_properties.set(to_underlying(property_id), 1); + for (auto const& [property_id, _] : new_properties) + old_and_new_properties.set(to_underlying(property_id), 1); + for (auto i = to_underlying(CSS::first_property_id); i <= to_underlying(CSS::last_property_id); ++i) { + if (!old_and_new_properties.get(i)) + continue; + auto property_id = static_cast(i); + auto old_value = old_properties.get(property_id).value_or({}); + auto new_value = new_properties.get(property_id).value_or({}); + if (!old_value && !new_value) + continue; + invalidation |= compute_property_invalidation(property_id, old_value, new_value); + } + return invalidation; +} + void KeyframeEffect::update_style_properties() { if (!target()) @@ -886,7 +907,7 @@ void KeyframeEffect::update_style_properties() if (!style) return; - auto style_before_animation_update = style->clone(); + auto animated_properties_before_update = style->animated_property_values(); auto& document = target()->document(); document.style_computer().collect_animation_into(*this, *style, CSS::StyleComputer::AnimationRefresh::Yes); @@ -908,7 +929,7 @@ void KeyframeEffect::update_style_properties() return IterationDecision::Continue; }); - auto invalidation = DOM::Element::compute_required_invalidation(style_before_animation_update, *style); + auto invalidation = compute_required_invalidation(animated_properties_before_update, style->animated_property_values()); if (target()->layout_node()) target()->layout_node()->apply_style(*style); diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 01f143fa340..4164993c6f6 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -89,6 +89,7 @@ set(SOURCES CSS/Serialize.cpp CSS/Size.cpp CSS/StyleComputer.cpp + CSS/StyleInvalidation.cpp CSS/StyleProperties.cpp CSS/StyleProperty.cpp CSS/StyleSheet.cpp diff --git a/Userland/Libraries/LibWeb/CSS/StyleInvalidation.cpp b/Userland/Libraries/LibWeb/CSS/StyleInvalidation.cpp new file mode 100644 index 00000000000..fd02111a3bd --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleInvalidation.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::CSS { + +RequiredInvalidationAfterStyleChange compute_property_invalidation(CSS::PropertyID property_id, RefPtr const& old_value, RefPtr const& new_value) +{ + RequiredInvalidationAfterStyleChange invalidation; + + bool const property_value_changed = (!old_value || !new_value) || *old_value != *new_value; + if (!property_value_changed) + return invalidation; + + // NOTE: If the computed CSS display property changes, we have to rebuild the entire layout tree. + // In the future, we should figure out ways to rebuild a smaller part of the tree. + if (property_id == CSS::PropertyID::Display) { + return RequiredInvalidationAfterStyleChange::full(); + } + + // NOTE: If one of the overflow properties change, we rebuild the entire layout tree. + // This ensures that overflow propagation from root/body to viewport happens correctly. + // In the future, we can make this invalidation narrower. + if (property_id == CSS::PropertyID::OverflowX || property_id == CSS::PropertyID::OverflowY) { + return RequiredInvalidationAfterStyleChange::full(); + } + + // OPTIMIZATION: Special handling for CSS `visibility`: + if (property_id == CSS::PropertyID::Visibility) { + // We don't need to relayout if the visibility changes from visible to hidden or vice versa. Only collapse requires relayout. + if ((old_value && old_value->to_identifier() == CSS::ValueID::Collapse) != (new_value && new_value->to_identifier() == CSS::ValueID::Collapse)) + invalidation.relayout = true; + // Of course, we still have to repaint on any visibility change. + invalidation.repaint = true; + } else if (CSS::property_affects_layout(property_id)) { + invalidation.relayout = true; + } + + if (property_id == CSS::PropertyID::Opacity && old_value && new_value) { + // OPTIMIZATION: An element creates a stacking context when its opacity changes from 1 to less than 1 + // and stops to create one when opacity returns to 1. So stacking context tree rebuild is + // not required for opacity changes within the range below 1. + auto old_value_opacity = CSS::StyleProperties::resolve_opacity_value(*old_value); + auto new_value_opacity = CSS::StyleProperties::resolve_opacity_value(*new_value); + if (old_value_opacity != new_value_opacity && (old_value_opacity == 1 || new_value_opacity == 1)) { + invalidation.rebuild_stacking_context_tree = true; + } + } else if (CSS::property_affects_stacking_context(property_id)) { + invalidation.rebuild_stacking_context_tree = true; + } + invalidation.repaint = true; + + return invalidation; +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleInvalidation.h b/Userland/Libraries/LibWeb/CSS/StyleInvalidation.h new file mode 100644 index 00000000000..c116781b875 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleInvalidation.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::CSS { + +struct RequiredInvalidationAfterStyleChange { + bool repaint : 1 { false }; + bool rebuild_stacking_context_tree : 1 { false }; + bool relayout : 1 { false }; + bool rebuild_layout_tree : 1 { false }; + + void operator|=(RequiredInvalidationAfterStyleChange const& other) + { + repaint |= other.repaint; + rebuild_stacking_context_tree |= other.rebuild_stacking_context_tree; + relayout |= other.relayout; + rebuild_layout_tree |= other.rebuild_layout_tree; + } + + [[nodiscard]] bool is_none() const { return !repaint && !rebuild_stacking_context_tree && !relayout && !rebuild_layout_tree; } + static RequiredInvalidationAfterStyleChange full() { return { true, true, true, true }; } +}; + +RequiredInvalidationAfterStyleChange compute_property_invalidation(CSS::PropertyID property_id, RefPtr const& old_value, RefPtr const& new_value); + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp index aef4c67b136..80698604909 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -34,17 +34,6 @@ namespace Web::CSS { -NonnullRefPtr StyleProperties::clone() const -{ - auto clone = adopt_ref(*new StyleProperties); - clone->m_property_values = m_property_values; - clone->m_animated_property_values = m_animated_property_values; - clone->m_font_list = m_font_list; - clone->m_line_height = m_line_height; - clone->m_math_depth = m_math_depth; - return clone; -} - bool StyleProperties::is_property_important(CSS::PropertyID property_id) const { return m_property_values[to_underlying(property_id)].style && m_property_values[to_underlying(property_id)].important == Important::Yes; @@ -289,7 +278,7 @@ Optional StyleProperties::z_index() const return {}; } -static float resolve_opacity_value(CSS::StyleValue const& value) +float StyleProperties::resolve_opacity_value(CSS::StyleValue const& value) { float unclamped_opacity = 1.0f; diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.h b/Userland/Libraries/LibWeb/CSS/StyleProperties.h index 4c5d36c348c..6ec795f5145 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.h @@ -23,8 +23,6 @@ public: static NonnullRefPtr create() { return adopt_ref(*new StyleProperties); } - NonnullRefPtr clone() const; - template inline void for_each_property(Callback callback) const { @@ -55,6 +53,7 @@ public: auto& properties() { return m_property_values; } auto const& properties() const { return m_property_values; } + HashMap> const& animated_property_values() const { return m_animated_property_values; } void reset_animated_properties(); bool is_property_important(CSS::PropertyID property_id) const; @@ -182,6 +181,8 @@ public: static NonnullRefPtr font_fallback(bool monospace, bool bold); + static float resolve_opacity_value(CSS::StyleValue const& value); + private: friend class StyleComputer; diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 594227a5fa3..7d34316b529 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -1116,10 +1116,10 @@ void Document::update_layout() m_layout_update_timer->stop(); } -[[nodiscard]] static Element::RequiredInvalidationAfterStyleChange update_style_recursively(Node& node) +[[nodiscard]] static CSS::RequiredInvalidationAfterStyleChange update_style_recursively(Node& node) { bool const needs_full_style_update = node.document().needs_full_style_update(); - Element::RequiredInvalidationAfterStyleChange invalidation; + CSS::RequiredInvalidationAfterStyleChange invalidation; // NOTE: If the current node has `display:none`, we can disregard all invalidation // caused by its children, as they will not be rendered anyway. diff --git a/Userland/Libraries/LibWeb/DOM/Element.cpp b/Userland/Libraries/LibWeb/DOM/Element.cpp index fdff9890f13..2c19e694bb5 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.cpp +++ b/Userland/Libraries/LibWeb/DOM/Element.cpp @@ -492,9 +492,9 @@ void Element::attribute_changed(FlyString const& name, Optional const& v } } -Element::RequiredInvalidationAfterStyleChange Element::compute_required_invalidation(CSS::StyleProperties const& old_style, CSS::StyleProperties const& new_style) +static CSS::RequiredInvalidationAfterStyleChange compute_required_invalidation(CSS::StyleProperties const& old_style, CSS::StyleProperties const& new_style) { - Element::RequiredInvalidationAfterStyleChange invalidation; + CSS::RequiredInvalidationAfterStyleChange invalidation; if (!old_style.computed_font_list().equals(new_style.computed_font_list())) invalidation.relayout = true; @@ -506,51 +506,12 @@ Element::RequiredInvalidationAfterStyleChange Element::compute_required_invalida if (!old_value && !new_value) continue; - bool const property_value_changed = (!old_value || !new_value) || *old_value != *new_value; - if (!property_value_changed) - continue; - - // NOTE: If the computed CSS display property changes, we have to rebuild the entire layout tree. - // In the future, we should figure out ways to rebuild a smaller part of the tree. - if (property_id == CSS::PropertyID::Display) { - return Element::RequiredInvalidationAfterStyleChange::full(); - } - - // NOTE: If one of the overflow properties change, we rebuild the entire layout tree. - // This ensures that overflow propagation from root/body to viewport happens correctly. - // In the future, we can make this invalidation narrower. - if (property_id == CSS::PropertyID::OverflowX || property_id == CSS::PropertyID::OverflowY) { - return Element::RequiredInvalidationAfterStyleChange::full(); - } - - // OPTIMIZATION: Special handling for CSS `visibility`: - if (property_id == CSS::PropertyID::Visibility) { - // We don't need to relayout if the visibility changes from visible to hidden or vice versa. Only collapse requires relayout. - if ((old_value && old_value->to_identifier() == CSS::ValueID::Collapse) != (new_value && new_value->to_identifier() == CSS::ValueID::Collapse)) - invalidation.relayout = true; - // Of course, we still have to repaint on any visibility change. - invalidation.repaint = true; - } else if (CSS::property_affects_layout(property_id)) { - invalidation.relayout = true; - } - if (property_id == CSS::PropertyID::Opacity) { - // OPTIMIZATION: An element creates a stacking context when its opacity changes from 1 to less than 1 - // and stops to create one when opacity returns to 1. So stacking context tree rebuild is - // not required for opacity changes within the range below 1. - auto old_value_opacity = old_style.opacity(); - auto new_value_opacity = new_style.opacity(); - if (old_value_opacity != new_value_opacity && (old_value_opacity == 1 || new_value_opacity == 1)) { - invalidation.rebuild_stacking_context_tree = true; - } - } else if (CSS::property_affects_stacking_context(property_id)) { - invalidation.rebuild_stacking_context_tree = true; - } - invalidation.repaint = true; + invalidation |= CSS::compute_property_invalidation(property_id, old_value, new_value); } return invalidation; } -Element::RequiredInvalidationAfterStyleChange Element::recompute_style() +CSS::RequiredInvalidationAfterStyleChange Element::recompute_style() { set_needs_style_update(false); VERIFY(parent()); @@ -565,11 +526,11 @@ Element::RequiredInvalidationAfterStyleChange Element::recompute_style() new_computed_css_values->set_property(CSS::PropertyID::TextAlign, CSS::IdentifierStyleValue::create(CSS::ValueID::Start)); } - RequiredInvalidationAfterStyleChange invalidation; + CSS::RequiredInvalidationAfterStyleChange invalidation; if (m_computed_css_values) invalidation = compute_required_invalidation(*m_computed_css_values, *new_computed_css_values); else - invalidation = RequiredInvalidationAfterStyleChange::full(); + invalidation = CSS::RequiredInvalidationAfterStyleChange::full(); if (invalidation.is_none()) return invalidation; diff --git a/Userland/Libraries/LibWeb/DOM/Element.h b/Userland/Libraries/LibWeb/DOM/Element.h index abb8a0df991..1785a1d41f4 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.h +++ b/Userland/Libraries/LibWeb/DOM/Element.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -148,27 +149,7 @@ public: void run_attribute_change_steps(FlyString const& local_name, Optional const& old_value, Optional const& value, Optional const& namespace_); virtual void attribute_changed(FlyString const& name, Optional const& value); - struct [[nodiscard]] RequiredInvalidationAfterStyleChange { - bool repaint { false }; - bool rebuild_stacking_context_tree { false }; - bool relayout { false }; - bool rebuild_layout_tree { false }; - - void operator|=(RequiredInvalidationAfterStyleChange const& other) - { - repaint |= other.repaint; - rebuild_stacking_context_tree |= other.rebuild_stacking_context_tree; - relayout |= other.relayout; - rebuild_layout_tree |= other.rebuild_layout_tree; - } - - [[nodiscard]] bool is_none() const { return !repaint && !rebuild_stacking_context_tree && !relayout && !rebuild_layout_tree; } - static RequiredInvalidationAfterStyleChange full() { return { true, true, true, true }; } - }; - - static Element::RequiredInvalidationAfterStyleChange compute_required_invalidation(CSS::StyleProperties const& old_style, CSS::StyleProperties const& new_style); - - RequiredInvalidationAfterStyleChange recompute_style(); + CSS::RequiredInvalidationAfterStyleChange recompute_style(); Optional use_pseudo_element() const { return m_use_pseudo_element; } void set_use_pseudo_element(Optional use_pseudo_element) { m_use_pseudo_element = move(use_pseudo_element); }