ladybird/Libraries/LibWeb/CSS/StyleInvalidation.cpp
Aliaksandr Kalenik 04c3ad669b LibWeb: Rebuild AccumulatedVisualContext on transform changes
Ensure AccumulatedVisualContext stays synchronized when CSS transform
properties change.

AccumulatedVisualContext copies transform and perspective matrices from
the paintable tree at assignment time. When CSS properties that affect
these matrices change (transform, rotate, scale, translate, perspective,
transform-origin, perspective-origin), we must rebuild the
AccumulatedVisualContext tree to reflect the new values.

This adds a rebuild_accumulated_visual_contexts flag to style
invalidation that triggers a full rebuild during the next paint.

Note: The current invalidation strategy is inefficient - it rebuilds
the entire tree even for single-element transform changes. This could
be improved by patching the AccumulatedVisualContext node in-place with
updated matrices, but only when the transform doesn't transition
from/to none (which would change the tree structure). This optimization
is left for future work.
2026-01-15 19:50:53 +01:00

90 lines
4.3 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/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
namespace Web::CSS {
RequiredInvalidationAfterStyleChange compute_property_invalidation(CSS::PropertyID property_id, RefPtr<StyleValue const> const& old_value, RefPtr<StyleValue const> const& new_value)
{
RequiredInvalidationAfterStyleChange invalidation;
bool const property_value_changed = (old_value || new_value) && ((!old_value || !new_value) || *old_value != *new_value);
if (!property_value_changed)
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 = property_value_changed;
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 (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 = old_value->as_number().number();
auto new_value_opacity = new_value->as_number().number();
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;
// Transform and perspective 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)) {
invalidation.rebuild_accumulated_visual_contexts = true;
}
return invalidation;
}
}