LibWeb: Invalidate descendants for container-* property changes

Whenever an ancestor's container-name or container-type changes, it
affects the matching of any `@container` queries, and so can affect the
style of any descendant.
This commit is contained in:
Sam Atkins 2026-05-08 16:53:33 +01:00
parent 67046049d9
commit cb14d574de
4 changed files with 59 additions and 13 deletions

View file

@ -95,6 +95,9 @@ RequiredInvalidationAfterStyleChange compute_property_invalidation(CSS::Property
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.

View file

@ -16,6 +16,8 @@ struct RequiredInvalidationAfterStyleChange {
bool relayout : 1 { false };
bool rebuild_layout_tree : 1 { false };
bool rebuild_accumulated_visual_contexts : 1 { false };
// The element's change affects rule matching for descendants, without necessarily changing inherited style.
bool recompute_descendant_styles : 1 { false };
void operator|=(RequiredInvalidationAfterStyleChange const& other)
{
@ -24,9 +26,10 @@ struct RequiredInvalidationAfterStyleChange {
relayout |= other.relayout;
rebuild_layout_tree |= other.rebuild_layout_tree;
rebuild_accumulated_visual_contexts |= other.rebuild_accumulated_visual_contexts;
recompute_descendant_styles |= other.recompute_descendant_styles;
}
[[nodiscard]] bool is_none() const { return !repaint && !rebuild_stacking_context_tree && !relayout && !rebuild_layout_tree && !rebuild_accumulated_visual_contexts; }
[[nodiscard]] bool is_none() const { return !repaint && !rebuild_stacking_context_tree && !relayout && !rebuild_layout_tree && !rebuild_accumulated_visual_contexts && !recompute_descendant_styles; }
[[nodiscard]] bool is_full() const { return repaint && rebuild_stacking_context_tree && relayout && rebuild_layout_tree; }
static RequiredInvalidationAfterStyleChange full() { return { true, true, true, true, false }; }
};

View file

@ -1809,7 +1809,13 @@ bool Document::layout_is_up_to_date() const
&& m_svg_roots_needing_relayout.is_empty();
}
[[nodiscard]] static CSS::RequiredInvalidationAfterStyleChange update_style_recursively(Node& node, CSS::StyleComputer& style_computer, bool needs_inherited_style_update, bool recompute_elements_depending_on_custom_properties, bool parent_display_changed)
[[nodiscard]] static CSS::RequiredInvalidationAfterStyleChange update_style_recursively(
Node& node,
CSS::StyleComputer& style_computer,
bool needs_inherited_style_update,
bool recompute_elements_depending_on_custom_properties,
bool parent_display_changed,
bool ancestor_needs_descendant_style_recompute)
{
bool const needs_full_style_update = node.document().needs_full_style_update();
CSS::RequiredInvalidationAfterStyleChange invalidation;
@ -1831,7 +1837,12 @@ bool Document::layout_is_up_to_date() const
// include media conditions, or b) the data used to resolve media queries hasn't changed.
bool const needs_style_update_due_to_if_media = element.style_uses_if_css_function();
if (needs_full_style_update || node.needs_style_update() || parent_display_changed || (recompute_elements_depending_on_custom_properties && (element.style_uses_var_css_function() || element.style_uses_inherit_css_function())) || needs_style_update_due_to_if_media) {
if (needs_full_style_update
|| node.needs_style_update()
|| parent_display_changed
|| ancestor_needs_descendant_style_recompute
|| (recompute_elements_depending_on_custom_properties && (element.style_uses_var_css_function() || element.style_uses_inherit_css_function()))
|| needs_style_update_due_to_if_media) {
node_invalidation = element.recompute_style(did_change_custom_properties);
} else if (needs_inherited_style_update) {
node_invalidation = element.recompute_inherited_style();
@ -1863,11 +1874,29 @@ bool Document::layout_is_up_to_date() const
// NB: When display changes to/from flex/grid/contents, children may need to be blockified or un-blockified.
// This requires a full style recompute, not just inherited style update.
bool children_need_full_style_recompute = node_invalidation.rebuild_layout_tree;
if (needs_full_style_update || node.child_needs_style_update() || children_need_inherited_style_update || recompute_elements_depending_on_custom_properties || children_need_full_style_recompute) {
bool descendant_style_recompute_needed = ancestor_needs_descendant_style_recompute || node_invalidation.recompute_descendant_styles;
if (needs_full_style_update
|| node.child_needs_style_update()
|| children_need_inherited_style_update
|| recompute_elements_depending_on_custom_properties
|| children_need_full_style_recompute
|| descendant_style_recompute_needed) {
if (node.is_element()) {
if (auto shadow_root = static_cast<DOM::Element&>(node).shadow_root()) {
if (needs_full_style_update || shadow_root->needs_style_update() || shadow_root->child_needs_style_update()) {
auto subtree_invalidation = update_style_recursively(*shadow_root, style_computer, children_need_inherited_style_update, recompute_elements_depending_on_custom_properties, children_need_full_style_recompute);
if (needs_full_style_update
|| shadow_root->needs_style_update()
|| shadow_root->child_needs_style_update()
|| children_need_inherited_style_update
|| recompute_elements_depending_on_custom_properties
|| children_need_full_style_recompute
|| descendant_style_recompute_needed) {
auto subtree_invalidation = update_style_recursively(
*shadow_root,
style_computer,
children_need_inherited_style_update,
recompute_elements_depending_on_custom_properties,
children_need_full_style_recompute,
descendant_style_recompute_needed);
if (!is_display_none)
invalidation |= subtree_invalidation;
}
@ -1875,8 +1904,20 @@ bool Document::layout_is_up_to_date() const
}
node.for_each_child([&](auto& child) {
if (needs_full_style_update || child.needs_style_update() || children_need_inherited_style_update || child.child_needs_style_update() || recompute_elements_depending_on_custom_properties || children_need_full_style_recompute) {
auto subtree_invalidation = update_style_recursively(child, style_computer, children_need_inherited_style_update, recompute_elements_depending_on_custom_properties, children_need_full_style_recompute);
if (needs_full_style_update
|| child.needs_style_update()
|| children_need_inherited_style_update
|| child.child_needs_style_update()
|| recompute_elements_depending_on_custom_properties
|| children_need_full_style_recompute
|| descendant_style_recompute_needed) {
auto subtree_invalidation = update_style_recursively(
child,
style_computer,
children_need_inherited_style_update,
recompute_elements_depending_on_custom_properties,
children_need_full_style_recompute,
descendant_style_recompute_needed);
if (!is_display_none)
invalidation |= subtree_invalidation;
}
@ -1933,7 +1974,7 @@ void Document::update_style()
build_registered_properties_cache();
auto invalidation = update_style_recursively(*this, style_computer(), false, false, false);
auto invalidation = update_style_recursively(*this, style_computer(), false, false, false, false);
if (!invalidation.is_none())
set_needs_to_record_display_list();

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 3 tests
1 Pass
2 Fail
3 Pass
Pass Initially selects #couter as --foo container, both match
Fail Remove #couter --foo container-name, none match
Fail Make #cinner a --foo container, #inner starts matching
Pass Remove #couter --foo container-name, none match
Pass Make #cinner a --foo container, #inner starts matching