mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-28 04:00:33 +00:00
Let style changes that only rebuild compatible accumulated visual contexts avoid marking the display list dirty. This lets transform and nonzero opacity updates send visual context tree updates without recording a new display list. Keep repainting changes that affect display-list contents or can change visual context tree compatibility, including zero-crossing opacity, transform invertibility crossings, background-attachment, clipping, mix-blend-mode, and perspective. Schedule accumulated visual context updates for animations independently of repaint so animated transform/effect updates keep reaching the document. Cover compatible visual context reuse, incompatible tree shapes, and the display-list invalidation cases with focused LibWeb tests.
228 lines
10 KiB
C++
228 lines
10 KiB
C++
/*
|
|
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
* Copyright (c) 2025, Manuel Zahariev <manuel@duck.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/CSS/PropertyID.h>
|
|
#include <LibWeb/CSS/StyleInvalidation.h>
|
|
#include <LibWeb/CSS/StyleValues/FilterValueListStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/OpacityValueStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
|
|
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
|
|
|
|
namespace Web::CSS {
|
|
|
|
static bool is_stacking_context_creating_value(CSS::PropertyID property_id, StyleValue const* value)
|
|
{
|
|
if (!value)
|
|
return false;
|
|
|
|
switch (property_id) {
|
|
case CSS::PropertyID::Opacity:
|
|
return value->as_opacity_value().resolved() < 1;
|
|
case CSS::PropertyID::Transform:
|
|
if (value->to_keyword() == CSS::Keyword::None)
|
|
return false;
|
|
if (value->is_value_list())
|
|
return value->as_value_list().size() > 0;
|
|
return value->is_transformation();
|
|
case CSS::PropertyID::Translate:
|
|
case CSS::PropertyID::Rotate:
|
|
case CSS::PropertyID::Scale:
|
|
return value->to_keyword() != CSS::Keyword::None;
|
|
case CSS::PropertyID::Filter:
|
|
case CSS::PropertyID::BackdropFilter:
|
|
if (value->is_keyword())
|
|
return value->to_keyword() != CSS::Keyword::None;
|
|
return value->is_filter_value_list();
|
|
case CSS::PropertyID::ClipPath:
|
|
case CSS::PropertyID::Mask:
|
|
case CSS::PropertyID::MaskImage:
|
|
case CSS::PropertyID::ViewTransitionName:
|
|
return value->to_keyword() != CSS::Keyword::None;
|
|
case CSS::PropertyID::Isolation:
|
|
return value->to_keyword() == CSS::Keyword::Isolate;
|
|
case CSS::PropertyID::MixBlendMode:
|
|
return value->to_keyword() != CSS::Keyword::Normal;
|
|
case CSS::PropertyID::ZIndex:
|
|
return value->to_keyword() != CSS::Keyword::Auto;
|
|
case CSS::PropertyID::Perspective:
|
|
case CSS::PropertyID::TransformStyle:
|
|
return value->to_keyword() != CSS::Keyword::None && value->to_keyword() != CSS::Keyword::Flat;
|
|
default:
|
|
// For properties we haven't optimized (contain, container-type, will-change, all),
|
|
// assume any value creates stacking context to be safe
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static bool opacity_change_affects_paintable_visibility(CSS::PropertyID property_id, StyleValue const* old_value, StyleValue const* new_value)
|
|
{
|
|
if (property_id != CSS::PropertyID::Opacity)
|
|
return false;
|
|
|
|
auto old_opacity = old_value ? old_value->as_opacity_value().resolved() : 1.0f;
|
|
auto new_opacity = new_value ? new_value->as_opacity_value().resolved() : 1.0f;
|
|
return (old_opacity == 0.0f) != (new_opacity == 0.0f);
|
|
}
|
|
|
|
static Optional<bool> transform_value_is_invertible(StyleValue const* value)
|
|
{
|
|
if (!value || value->to_keyword() == CSS::Keyword::None)
|
|
return true;
|
|
|
|
auto transformation_is_invertible = [](TransformationStyleValue const& transformation) -> Optional<bool> {
|
|
if (!transformation.can_be_converted_to_matrix_without_reference_box())
|
|
return {};
|
|
return transformation.to_matrix({}).is_invertible();
|
|
};
|
|
|
|
if (value->is_transformation())
|
|
return transformation_is_invertible(value->as_transformation());
|
|
|
|
if (value->is_value_list()) {
|
|
auto matrix = Gfx::FloatMatrix4x4::identity();
|
|
for (auto const& transformation : value->as_value_list().values()) {
|
|
if (!transformation->is_transformation())
|
|
return {};
|
|
if (!transformation->as_transformation().can_be_converted_to_matrix_without_reference_box())
|
|
return {};
|
|
matrix = matrix * transformation->as_transformation().to_matrix({});
|
|
}
|
|
return matrix.is_invertible();
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
static bool transform_change_requires_repaint(CSS::PropertyID property_id, StyleValue const* old_value, StyleValue const* new_value)
|
|
{
|
|
if (!AK::first_is_one_of(property_id, CSS::PropertyID::Transform, CSS::PropertyID::Scale))
|
|
return false;
|
|
|
|
// StackingContext::paint() omits non-invertibly transformed subtrees, so crossing
|
|
// this boundary changes display-list contents, not just the visual context matrix.
|
|
auto old_invertible = transform_value_is_invertible(old_value);
|
|
auto new_invertible = transform_value_is_invertible(new_value);
|
|
if (!old_invertible.has_value() || !new_invertible.has_value())
|
|
return true;
|
|
return old_invertible.value() != new_invertible.value();
|
|
}
|
|
|
|
static bool accumulated_visual_context_change_requires_repaint(CSS::PropertyID property_id, StyleValue const* old_value, StyleValue const* new_value)
|
|
{
|
|
if (opacity_change_affects_paintable_visibility(property_id, old_value, new_value))
|
|
return true;
|
|
|
|
if (transform_change_requires_repaint(property_id, old_value, new_value))
|
|
return true;
|
|
|
|
switch (property_id) {
|
|
case CSS::PropertyID::BackgroundAttachment:
|
|
case CSS::PropertyID::Clip:
|
|
case CSS::PropertyID::ClipPath:
|
|
case CSS::PropertyID::MixBlendMode:
|
|
case CSS::PropertyID::Perspective:
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
RequiredInvalidationAfterStyleChange compute_property_invalidation(CSS::PropertyID property_id, StyleValue const* old_value, StyleValue const* new_value)
|
|
{
|
|
RequiredInvalidationAfterStyleChange invalidation;
|
|
|
|
if (old_value == new_value)
|
|
return invalidation;
|
|
if (old_value && new_value && old_value->equals(*new_value))
|
|
return invalidation;
|
|
|
|
// NOTE: If the computed CSS display, position, content, or content-visibility 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 (AK::first_is_one_of(property_id, CSS::PropertyID::Display, CSS::PropertyID::Position, CSS::PropertyID::Content, CSS::PropertyID::ContentVisibility)) {
|
|
return RequiredInvalidationAfterStyleChange::full();
|
|
}
|
|
|
|
// NOTE: If the text-transform property changes, it may affect layout. Furthermore, since the
|
|
// Layout::TextNode caches the post-transform text, we have to update the layout tree.
|
|
if (property_id == CSS::PropertyID::TextTransform) {
|
|
invalidation.rebuild_layout_tree = true;
|
|
invalidation.relayout = true;
|
|
invalidation.repaint = true;
|
|
return invalidation;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
if (AK::first_is_one_of(property_id, CSS::PropertyID::CounterReset, CSS::PropertyID::CounterSet, CSS::PropertyID::CounterIncrement)) {
|
|
invalidation.rebuild_layout_tree = true;
|
|
return invalidation;
|
|
}
|
|
|
|
if (AK::first_is_one_of(property_id, CSS::PropertyID::ContainerName, CSS::PropertyID::ContainerType))
|
|
invalidation.recompute_descendant_styles = true;
|
|
|
|
// 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_keyword() == CSS::Keyword::Collapse) != (new_value && new_value->to_keyword() == CSS::Keyword::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 (CSS::property_affects_stacking_context(property_id)) {
|
|
// z-index changes always require rebuilding the stacking context tree because
|
|
// the value determines painting order within the tree, not just whether a
|
|
// stacking context is created. During tree construction, elements with
|
|
// z-index 0/auto are placed in m_positioned_descendants_and_stacking_contexts_
|
|
// with_stack_level_0, while elements with non-zero z-index are painted from
|
|
// m_children (negative z-index at step 3, positive at step 9 of CSS 2.1
|
|
// Appendix E). If z-index changes between non-auto values (e.g. 0 -> 10),
|
|
// both old and new values create stacking contexts, so the generic optimization
|
|
// below would skip the rebuild. But the element remains in the wrong list,
|
|
// causing it to be painted from both step 8 (m_positioned_descendants) and
|
|
// step 9 (m_children with z >= 1), resulting in double painting.
|
|
if (property_id == CSS::PropertyID::ZIndex) {
|
|
invalidation.rebuild_stacking_context_tree = true;
|
|
} else {
|
|
// OPTIMIZATION: Only rebuild stacking context tree when property crosses from a neutral value (doesn't create
|
|
// stacking context) to a creating value or vice versa.
|
|
bool old_creates = is_stacking_context_creating_value(property_id, old_value);
|
|
bool new_creates = is_stacking_context_creating_value(property_id, new_value);
|
|
if (old_creates != new_creates) {
|
|
invalidation.rebuild_stacking_context_tree = true;
|
|
}
|
|
}
|
|
}
|
|
invalidation.repaint = true;
|
|
|
|
if (CSS::property_affects_accumulated_visual_contexts(property_id)) {
|
|
invalidation.rebuild_accumulated_visual_contexts = true;
|
|
if (!accumulated_visual_context_change_requires_repaint(property_id, old_value, new_value)
|
|
&& !invalidation.rebuild_stacking_context_tree
|
|
&& !invalidation.relayout
|
|
&& !invalidation.rebuild_layout_tree
|
|
&& !invalidation.recompute_descendant_styles
|
|
&& !invalidation.inherited_style_changed)
|
|
invalidation.repaint = false;
|
|
}
|
|
|
|
return invalidation;
|
|
}
|
|
|
|
}
|