Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Libraries/LibWeb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ set(SOURCES
CSS/HypotheticalElement.cpp
CSS/Invalidation/AdoptedStyleSheetInvalidator.cpp
CSS/Invalidation/AttributeInvalidator.cpp
CSS/Invalidation/ContainerQueryInvalidator.cpp
CSS/Invalidation/CustomElementInvalidator.cpp
CSS/Invalidation/EmbeddedContentInvalidator.cpp
CSS/Invalidation/ElementStateInvalidator.cpp
Expand Down
15 changes: 14 additions & 1 deletion Libraries/LibWeb/CSS/ComputedValues.h
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,20 @@ class WEB_API ComputedValues final : public RefCounted<ComputedValues> {
RefPtr<AbstractImageStyleValue const> list_style_image;
QuotesData quotes { InitialValues::quotes() };

bool operator==(InheritedListValues const&) const = default;
bool operator==(InheritedListValues const& other) const
{
// The image is compared by value: recomputation builds a fresh one for the same
// declaration, and comparing the addresses would call the group different for it.
auto images_equal = [](auto const& first, auto const& second) {
if (!first || !second)
return !first && !second;
return *first == *second;
};
return list_style_type == other.list_style_type
&& list_style_position == other.list_style_position
&& images_equal(list_style_image, other.list_style_image)
&& quotes == other.quotes;
}
};

struct InheritedUIValues {
Expand Down
17 changes: 17 additions & 0 deletions Libraries/LibWeb/CSS/ContainerQuery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,23 @@ MatchResult ContainerQuery::evaluate(DOM::AbstractElement const& element, Option
if (!container_satisfies_requirements(*container, m_feature_requirements))
continue;

// A style feature asks about the container's own computed style, so the container has to know
// that a style change on it is a change for something under it. Nothing else says so: the
// dependency is recorded on the element that asked, which is not the element that moves.
// The value the query compares against is resolved too, and that resolution can read the
// root - `style(--length: calc(1rem * 10))` moves when the root font size does - so the root
// is named as well.
// A size feature asks about the container's own box, and the scan a resize does for the
// dependents under it starts from the same fact: whether anything ever asked.
if (m_feature_requirements.contains_size_feature())
const_cast<DOM::Element&>(*container).set_is_size_query_container();

if (m_feature_requirements.contains_style_feature()) {
const_cast<DOM::Element&>(*container).set_is_style_query_container();
if (auto* root = element.document().document_element())
root->set_is_style_query_container();
}

// Once an eligible query container has been selected for an element, each container feature in the
// <container-query> is evaluated against that query container.
return m_condition->evaluate({
Expand Down
62 changes: 62 additions & 0 deletions Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/Vector.h>
#include <LibWeb/CSS/Invalidation/ContainerQueryInvalidator.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/HTMLSlotElement.h>

namespace Web::CSS::Invalidation {

// The children of a node in the flat tree: a host's are its shadow tree's, a slot's are the nodes
// assigned to it, and everything else's are its DOM children. This is the inverse of the walk that
// selects a query container, which is what makes it the right one for finding that container's
// dependents.
static void append_flat_tree_children(DOM::Node& node, Vector<DOM::Node*>& out)
{
if (auto* element = as_if<DOM::Element>(node)) {
if (auto shadow_root = element->shadow_root()) {
for (auto* child = shadow_root->first_child(); child; child = child->next_sibling())
out.append(child);
return;
}
if (auto* slot = as_if<HTML::HTMLSlotElement>(*element); slot && !slot->assigned_nodes_internal().is_empty()) {
for (auto const& slottable : slot->assigned_nodes_internal())
slottable.visit([&](auto const& assigned) { out.append(static_cast<DOM::Node*>(assigned.ptr())); });
return;
}
}

for (auto* child = node.first_child(); child; child = child->next_sibling())
out.append(child);
}

void invalidate_descendant_styles_depending_on_size_container_query(DOM::Element& query_container)
{
// Only an element some size query or container-relative unit resolved against can have a
// dependent under it, and `container-type` is set far more widely than it is asked about.
if (!query_container.is_size_query_container())
return;

auto& counters = query_container.document().style_invalidation_counters();

Vector<DOM::Node*> stack;
append_flat_tree_children(query_container, stack);
while (!stack.is_empty()) {
auto* node = stack.take_last();
if (auto* element = as_if<DOM::Element>(*node)) {
++counters.size_query_container_scan_visits;
if (element->style_depends_on_size_container_query())
element->set_needs_style_update(true);
}
append_flat_tree_children(*node, stack);
}
}

}
23 changes: 23 additions & 0 deletions Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#pragma once

namespace Web::DOM {

class Element;

}

namespace Web::CSS::Invalidation {

// A query container's size moved, so every element whose size query or container-relative unit
// resolved against it has to be recomputed. Which elements those are is not a selector question:
// the container is chosen by walking the flat tree upwards, so the dependents are its flat-tree
// descendants, and a container no query ever selected has none at all.
void invalidate_descendant_styles_depending_on_size_container_query(DOM::Element& query_container);

}
4 changes: 4 additions & 0 deletions Libraries/LibWeb/CSS/Length.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ double Length::container_relative_length_to_px_without_rounding(ResolutionContex
const_cast<DOM::Element&>(subject).set_style_depends_on_size_container_query();

auto query_container = get_or_compute_query_container_for_axis(context, physical_axis);
// A container unit asks about the container's box exactly as a size query does, so the
// container has to know that a resize of it is a change for something under it.
if (query_container)
const_cast<DOM::Element&>(*query_container).set_is_size_query_container();
if (!query_container) {
context.record_viewport_relative_length_resolution();
auto viewport_length = physical_axis == ContainerRelativeAxis::Width ? context.viewport_rect.width() : context.viewport_rect.height();
Expand Down
9 changes: 7 additions & 2 deletions Libraries/LibWeb/CSS/StyleComputer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1336,7 +1336,7 @@ void StyleComputer::collect_animation_effects_into(DOM::AbstractElement abstract
.transform_reference_box_width = 0,
.transform_reference_box_height = 0,
};
if (auto paintable = prepared_values.first().effect->target()->unsafe_paintable(); paintable) {
if (auto paintable = prepared_values.first().effect->target()->unsafe_paintable(); paintable && paintable->has_layout_node()) {
auto reference_box = paintable->transform_reference_box();
animation_context.has_transform_reference_box = true;
animation_context.transform_reference_box_width = reference_box.width().to_double();
Expand Down Expand Up @@ -1626,7 +1626,12 @@ Vector<GC::Ref<Animations::KeyframeEffect>> StyleComputer::start_needed_transiti
.transform_reference_box_width = 0,
.transform_reference_box_height = 0,
};
if (auto paintable = abstract_element.element().unsafe_paintable(); paintable) {
// A paintable outlives the layout node it was made for, and a style recompute can reach an
// element whose layout node is already gone: the layout tree builder updates the style of an
// element a bypass path reached, and `display: none` leaves the flag set until then. A reference
// box is a fact about a layout box, so without one there is none - exactly as when the element
// was never painted at all.
if (auto paintable = abstract_element.element().unsafe_paintable(); paintable && paintable->has_layout_node()) {
auto reference_box = paintable->transform_reference_box();
transition_animation_context.has_transform_reference_box = true;
transition_animation_context.transform_reference_box_width = reference_box.width().to_double();
Expand Down
14 changes: 14 additions & 0 deletions Libraries/LibWeb/CSS/StylePropertyMapReadOnly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ void StylePropertyMapReadOnly::visit_edges(GC::Cell::Visitor& visitor)
[&visitor](GC::Ref<CSSStyleDeclaration>& declaration) { visitor.visit(declaration); });
}

// A computed style map describes an element's computed style, and a disconnected element has none.
// `getComputedStyle` refuses one outright rather than reporting a style nothing decided, and the map
// answers the same way: empty, with every property absent.
static bool has_computed_style(DOM::AbstractElement const& abstract_element)
{
return abstract_element.element().is_connected();
}

// https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymapreadonly-get
WebIDL::ExceptionOr<Variant<GC::Ref<CSSStyleValue>, Empty>> StylePropertyMapReadOnly::get(Utf16String property_name)
{
Expand Down Expand Up @@ -113,6 +121,8 @@ WebIDL::ExceptionOr<bool> StylePropertyMapReadOnly::has(Utf16String property_nam
// 4. If props[property] exists, return true. Otherwise, return false.
return props.visit(
[&property](DOM::AbstractElement& element) {
if (!has_computed_style(element))
return false;
// From https://drafts.css-houdini.org/css-typed-om-1/#dom-element-computedstylemap we need to include:
// "the name and computed value of every longhand CSS property supported by the User Agent, every
// registered custom property, and every non-registered custom property which is not set to its initial
Expand All @@ -139,6 +149,8 @@ WebIDL::UnsignedLong StylePropertyMapReadOnly::size() const
// 1. Return the size of the value of this’s [[declarations]] internal slot.
return m_declarations.visit(
[](DOM::AbstractElement const& element) {
if (!has_computed_style(element))
return size_t { 0 };
// From https://drafts.css-houdini.org/css-typed-om-1/#dom-element-computedstylemap we need to include:
// "the name and computed value of every longhand CSS property supported by the User Agent, every
// registered custom property, and every non-registered custom property which is not set to its initial
Expand Down Expand Up @@ -166,6 +178,8 @@ RefPtr<StyleValue const> StylePropertyMapReadOnly::get_style_value(Source& sourc
{
return source.visit(
[&property](DOM::AbstractElement& element) -> RefPtr<StyleValue const> {
if (!has_computed_style(element))
return nullptr;
// From https://drafts.css-houdini.org/css-typed-om-1/#dom-element-computedstylemap we need to include:
// "the name and computed value of every longhand CSS property supported by the User Agent, every
// registered custom property, and every non-registered custom property which is not set to its initial
Expand Down
11 changes: 11 additions & 0 deletions Libraries/LibWeb/CSS/StyleScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,17 @@ void StyleScope::populate_rule_cache(StyleRuleCache& rule_cache)
{
build_user_style_sheet_if_needed();

// A user-agent sheet is a process-wide singleton with no owning document, so nothing that walks a
// document's own sheets ever evaluates its media rules. Its `@media` answers are still per
// document - `(scripting)` is - so they are evaluated here, where the rule cache that consumes
// them is built. Without this the cache is built against whatever state some other document
// happened to leave behind, and `noscript` keeps the UA sheet's `display: none` only by accident.
for (auto origin : { CascadeOrigin::UserAgent, CascadeOrigin::User }) {
for_each_stylesheet(origin, [&](CSSStyleSheet& sheet) {
sheet.evaluate_media_queries(document());
});
}

build_qualified_layer_names_cache(rule_cache);

rule_cache.pseudo_class_rule_cache[to_underlying(PseudoClass::Hover)] = make<RuleCache>();
Expand Down
8 changes: 2 additions & 6 deletions Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
#include <LibWeb/CSS/CustomPropertyRegistration.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/FontFaceSet.h>
#include <LibWeb/CSS/Invalidation/ContainerQueryInvalidator.h>
#include <LibWeb/CSS/Invalidation/MediaQueryInvalidator.h>
#include <LibWeb/CSS/Invalidation/PseudoClassInvalidator.h>
#include <LibWeb/CSS/Invalidation/StyleInvalidator.h>
Expand Down Expand Up @@ -2189,12 +2190,7 @@ void Document::update_layout(UpdateLayoutReason reason)
if (!query_container->is_connected())
continue;

query_container->for_each_shadow_including_descendant([](Node& node) {
if (auto* element = as_if<Element>(node); element && element->style_depends_on_size_container_query())
element->set_needs_style_update(true);

return TraversalDecision::Continue;
});
CSS::Invalidation::invalidate_descendant_styles_depending_on_size_container_query(query_container);
}
}

Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/DOM/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,8 @@ class WEB_API Document
u64 registered_properties_cache_rebuilds { 0 };
u64 style_sheet_invalidation_set_builds { 0 };
u64 scope_rule_cache_builds { 0 };
u64 style_query_container_scans { 0 };
u64 size_query_container_scan_visits { 0 };
u64 relayouts_performed { 0 };
u64 scrollable_overflow_recalculations { 0 };
};
Expand Down
7 changes: 7 additions & 0 deletions Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,13 @@ void Element::mark_descendants_with_stale_styles_for_style_update()

void Element::invalidate_descendant_styles_depending_on_style_container_query()
{
// Only an element some style container query resolved against can be what a dependent under it
// was asking about, and most documents have no style container queries at all.
if (!m_is_style_query_container)
return;

++document().style_invalidation_counters().style_query_container_scans;

for_each_shadow_including_descendant([](auto& node) {
auto* element = as_if<Element>(node);
if (element && element->style_depends_on_style_container_query())
Expand Down
9 changes: 9 additions & 0 deletions Libraries/LibWeb/DOM/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,13 @@ class WEB_API Element
void set_style_depends_on_size_container_query() { m_style_depends_on_size_container_query = true; }
bool style_depends_on_style_container_query() const { return m_style_depends_on_style_container_query; }
void set_style_depends_on_style_container_query() { m_style_depends_on_style_container_query = true; }
// Set on the element a container query selected as its query container, so a change on it knows
// whether anything under it was ever asking. Neither is ever cleared: a dependent that stops
// asking republishes nothing, and answering "maybe" costs the scan the element used to pay
// unconditionally.
void set_is_style_query_container() { m_is_style_query_container = true; }
bool is_size_query_container() const { return m_is_size_query_container; }
void set_is_size_query_container() { m_is_size_query_container = true; }
void invalidate_descendant_styles_depending_on_style_container_query();

bool child_style_uses_tree_counting_function() const { return m_child_style_uses_tree_counting_function; }
Expand Down Expand Up @@ -861,6 +868,8 @@ class WEB_API Element
bool m_style_uses_inherit_css_function : 1 { false };
bool m_style_depends_on_size_container_query : 1 { false };
bool m_style_depends_on_style_container_query : 1 { false };
bool m_is_style_query_container : 1 { false };
bool m_is_size_query_container : 1 { false };
bool m_child_style_uses_tree_counting_function : 1 { false };
bool m_affected_by_has_pseudo_class_in_subject_position : 1 { false };
bool m_affected_by_has_pseudo_class_in_non_subject_position : 1 { false };
Expand Down
11 changes: 9 additions & 2 deletions Libraries/LibWeb/DOM/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ Node::Node(Document& document, NodeType type)
: EventTarget()
, m_document(&document)
, m_type(type)
, m_unique_id(allocate_unique_id(*this))
{
// A Document is its own shadow-including root, so it is always connected.
if (type == NodeType::DOCUMENT_NODE)
Expand Down Expand Up @@ -162,7 +161,15 @@ CSS::UserSelect Node::user_select_used_value() const
void Node::finalize()
{
Base::finalize();
deallocate_unique_id(m_unique_id);
if (m_unique_id.has_value())
deallocate_unique_id(*m_unique_id);
}

UniqueNodeID Node::unique_id() const
{
if (!m_unique_id.has_value())
m_unique_id = allocate_unique_id(const_cast<Node&>(*this));
return *m_unique_id;
}

void Node::visit_edges(Cell::Visitor& visitor)
Expand Down
4 changes: 2 additions & 2 deletions Libraries/LibWeb/DOM/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ class WEB_API Node : public EventTarget
bool is_shadow_including_ancestor_of(Node const&) const;
bool is_shadow_including_inclusive_ancestor_of(Node const&) const;

[[nodiscard]] UniqueNodeID unique_id() const { return m_unique_id; }
[[nodiscard]] UniqueNodeID unique_id() const;
static Node* from_unique_id(UniqueNodeID);

WebIDL::ExceptionOr<Utf16String> serialize_fragment(HTML::RequireWellFormed, FragmentSerializationMode = FragmentSerializationMode::Inner) const;
Expand Down Expand Up @@ -559,7 +559,7 @@ class WEB_API Node : public EventTarget
bool m_is_connected { false };
bool m_inside_blocking_wheel_event_handler { false };

UniqueNodeID m_unique_id;
mutable Optional<UniqueNodeID> m_unique_id;

// https://dom.spec.whatwg.org/#registered-observer-list
// "Nodes have a strong reference to registered observers in their registered observer list." https://dom.spec.whatwg.org/#garbage-collection
Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/Internals/Internals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,8 @@ JS::Object* Internals::style_invalidation_counters_object() const
object->define_direct_property("registeredPropertiesCacheRebuilds"_utf16_fly_string, JS::Value(counters.registered_properties_cache_rebuilds), JS::default_attributes);
object->define_direct_property("styleSheetInvalidationSetBuilds"_utf16_fly_string, JS::Value(counters.style_sheet_invalidation_set_builds), JS::default_attributes);
object->define_direct_property("scopeRuleCacheBuilds"_utf16_fly_string, JS::Value(counters.scope_rule_cache_builds), JS::default_attributes);
object->define_direct_property("styleQueryContainerScans"_utf16_fly_string, JS::Value(counters.style_query_container_scans), JS::default_attributes);
object->define_direct_property("sizeQueryContainerScanVisits"_utf16_fly_string, JS::Value(counters.size_query_container_scan_visits), JS::default_attributes);
object->define_direct_property("relayoutsPerformed"_utf16_fly_string, JS::Value(counters.relayouts_performed), JS::default_attributes);
object->define_direct_property("scrollableOverflowRecalculations"_utf16_fly_string, JS::Value(counters.scrollable_overflow_recalculations), JS::default_attributes);
return object;
Expand Down
5 changes: 5 additions & 0 deletions Libraries/LibWeb/Layout/TreeBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,11 @@ RustFFI::FfiPseudoTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_pseudo_tr
VERIFY(index < frame.resolved_content.data.size());
auto& item = frame.resolved_content.data[index];
if (auto const* string = item.get_pointer<Utf16String>()) {
// An empty generated text node carries the inline fragment of an ordinary inline pseudo-element.
// Other pseudo-element boxes exist independently of their contents, so avoid giving them a
// zero-length child that would force layout to measure an otherwise empty box.
if (string->is_empty() && !(frame.display.is_inline_outside() && frame.display.is_flow_inside()))
return Node::slot_id(nullptr);
frame.content_item = make_ref_counted<GeneratedTextNode>(element.document(), *string);
} else {
auto& image = *item.get<NonnullRefPtr<CSS::AbstractImageStyleValue>>();
Expand Down
Loading
Loading