Files
ladybird/Libraries/LibWeb/CSS/StyleInvalidation.cpp
Tim Ledbetter f29bc0c996 LibWeb: Use scroll compensation for background-attachment: fixed
Instead of baking the current scroll offset into the background
positioning area at record time, use the `ScrollCompensation` visual
context node to negate ancestor scroll frames dynamically at replay
time. This keeps the background fixed relative to the viewport even
when the display list is cached and replayed at different scroll
positions, and works correctly with arbitrarily nested scroll
containers.
2026-05-08 13:09:40 +02:00

157 lines
7.2 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>
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;
}
}
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;
}
// 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;
// Transform, perspective, clip, clip-path, effects, and background-attachment properties require rebuilding AccumulatedVisualContext tree.
if (AK::first_is_one_of(property_id,
CSS::PropertyID::Transform,
CSS::PropertyID::Rotate,
CSS::PropertyID::Scale,
CSS::PropertyID::Translate,
CSS::PropertyID::Perspective,
CSS::PropertyID::TransformOrigin,
CSS::PropertyID::PerspectiveOrigin,
CSS::PropertyID::Clip,
CSS::PropertyID::ClipPath,
CSS::PropertyID::Opacity,
CSS::PropertyID::MixBlendMode,
CSS::PropertyID::Filter,
CSS::PropertyID::BackgroundAttachment)) {
invalidation.rebuild_accumulated_visual_contexts = true;
}
return invalidation;
}
}