diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 282ff7cc55b6f..175461412d28e 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 0e8fd51151e00..697883c3d1f78 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1587,7 +1587,20 @@ class WEB_API ComputedValues final : public RefCounted { RefPtr 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 { diff --git a/Libraries/LibWeb/CSS/ContainerQuery.cpp b/Libraries/LibWeb/CSS/ContainerQuery.cpp index 692d5d26716cb..8bfc291c677f0 100644 --- a/Libraries/LibWeb/CSS/ContainerQuery.cpp +++ b/Libraries/LibWeb/CSS/ContainerQuery.cpp @@ -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(*container).set_is_size_query_container(); + + if (m_feature_requirements.contains_style_feature()) { + const_cast(*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 // is evaluated against that query container. return m_condition->evaluate({ diff --git a/Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.cpp b/Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.cpp new file mode 100644 index 0000000000000..4b1a9db036b49 --- /dev/null +++ b/Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +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& out) +{ + if (auto* element = as_if(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(*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(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 stack; + append_flat_tree_children(query_container, stack); + while (!stack.is_empty()) { + auto* node = stack.take_last(); + if (auto* element = as_if(*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); + } +} + +} diff --git a/Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.h b/Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.h new file mode 100644 index 0000000000000..3903f0a448425 --- /dev/null +++ b/Libraries/LibWeb/CSS/Invalidation/ContainerQueryInvalidator.h @@ -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); + +} diff --git a/Libraries/LibWeb/CSS/Length.cpp b/Libraries/LibWeb/CSS/Length.cpp index 733a40ddc307b..2056e69777114 100644 --- a/Libraries/LibWeb/CSS/Length.cpp +++ b/Libraries/LibWeb/CSS/Length.cpp @@ -148,6 +148,10 @@ double Length::container_relative_length_to_px_without_rounding(ResolutionContex const_cast(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(*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(); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index b57011c641c90..6acdfd3122a74 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -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(); @@ -1626,7 +1626,12 @@ Vector> 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(); diff --git a/Libraries/LibWeb/CSS/StylePropertyMapReadOnly.cpp b/Libraries/LibWeb/CSS/StylePropertyMapReadOnly.cpp index be490647fb3e6..79f1b42466b84 100644 --- a/Libraries/LibWeb/CSS/StylePropertyMapReadOnly.cpp +++ b/Libraries/LibWeb/CSS/StylePropertyMapReadOnly.cpp @@ -41,6 +41,14 @@ void StylePropertyMapReadOnly::visit_edges(GC::Cell::Visitor& visitor) [&visitor](GC::Ref& 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, Empty>> StylePropertyMapReadOnly::get(Utf16String property_name) { @@ -113,6 +121,8 @@ WebIDL::ExceptionOr 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 @@ -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 @@ -166,6 +178,8 @@ RefPtr StylePropertyMapReadOnly::get_style_value(Source& sourc { return source.visit( [&property](DOM::AbstractElement& element) -> RefPtr { + 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 diff --git a/Libraries/LibWeb/CSS/StyleScope.cpp b/Libraries/LibWeb/CSS/StyleScope.cpp index 98a73bb9b04e3..e35383896ce26 100644 --- a/Libraries/LibWeb/CSS/StyleScope.cpp +++ b/Libraries/LibWeb/CSS/StyleScope.cpp @@ -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(); diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index ea82570f55273..1af67abcc48f3 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -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(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); } } diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index e50c39afb8318..98819fd65d9c1 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -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 }; }; diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 0bce17b38069c..f0c41fd32f668 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -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(node); if (element && element->style_depends_on_style_container_query()) diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 594ba020a2454..86376f4260064 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -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; } @@ -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 }; diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index d41dfbd5b7d0b..7bed9b1797880 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -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) @@ -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(*this)); + return *m_unique_id; } void Node::visit_edges(Cell::Visitor& visitor) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index ab8dbf12bbc94..62ae921e01b85 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -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 serialize_fragment(HTML::RequireWellFormed, FragmentSerializationMode = FragmentSerializationMode::Inner) const; @@ -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 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 diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index e2aa4e6ee6cbb..f2f9ba87efa8b 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -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; diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index 86d61836de697..322b35623e628 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -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()) { + // 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(element.document(), *string); } else { auto& image = *item.get>(); diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index ba2d454c326b9..e33bc6152153d 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -551,13 +552,8 @@ static void invalidate_descendant_styles_for_container_query_size_change(Paintab if (!content_size_change_affects_container_queries(paintable_box, old_size, new_size)) return; - if (auto* element = as_if(paintable_box.dom_node().ptr())) { - element->for_each_shadow_including_descendant([](DOM::Node& node) { - if (auto* descendant_element = as_if(node); descendant_element && descendant_element->style_depends_on_size_container_query()) - descendant_element->set_needs_style_update(true); - return TraversalDecision::Continue; - }); - } + if (auto* element = as_if(paintable_box.dom_node().ptr())) + CSS::Invalidation::invalidate_descendant_styles_depending_on_size_container_query(*element); } void set_paint_viewport_scrollbars(bool const enabled) diff --git a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp index 343b3b4f96748..c247f24fe1a41 100644 --- a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp +++ b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp @@ -81,6 +81,10 @@ void SVGSVGPaintable::paint_descendants(DisplayListRecordingContext& context, Pa return; paintable.for_each_child_of_type([&](Paintable& child) { + // A child that establishes a stacking context is painted by that context, in the order the + // painting algorithm gives it, and painting it here as well would draw it twice. + if (child.has_stacking_context()) + return IterationDecision::Continue; paint_svg_box(context, child, phase); return IterationDecision::Continue; }); diff --git a/Libraries/LibWeb/Painting/StackingContext.cpp b/Libraries/LibWeb/Painting/StackingContext.cpp index 5012406c79688..9a4c4a50c24ad 100644 --- a/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Libraries/LibWeb/Painting/StackingContext.cpp @@ -399,6 +399,12 @@ void StackingContext::paint_internal(DisplayListRecordingContext& context) const SVGSVGPaintable::paint_svg_box(context, svg_svg_paintable, PaintPhase::Foreground); + // An `` that establishes a stacking context still has descendants that establish one + // of their own - a `` always does - and those are painted by their own + // context rather than by the SVG walk, so this has to paint them like any other root. + for (auto& child : m_children) + paint_child(context, *child); + paint_node(svg_svg_paintable, context, PaintPhase::Outline); if (context.should_paint_overlay()) { paint_node(svg_svg_paintable, context, PaintPhase::Overlay); diff --git a/Libraries/LibWeb/Rust/src/layout/tree_builder.rs b/Libraries/LibWeb/Rust/src/layout/tree_builder.rs index 3aa3179054aeb..93d3d4ce91ed6 100644 --- a/Libraries/LibWeb/Rust/src/layout/tree_builder.rs +++ b/Libraries/LibWeb/Rust/src/layout/tree_builder.rs @@ -1869,9 +1869,11 @@ fn create_pseudo_element_with_frame( if resolved_content.content_is_list && decision != FfiPseudoElementDecision::ContentReplacement { state.ancestor_stack.push(layout_node); for index in 0..resolved_content.content_item_count { - // SAFETY: `index` is below the resolved content item count and the frame retains the returned node. + // SAFETY: `index` is below the resolved content item count and the frame retains any returned node. let content_item = unsafe { (callbacks.create_content_item)(frame, element, pseudo_element, index) }; - assert!(!content_item.is_invalid()); + if content_item.is_invalid() { + continue; + } let layout_host = host.layout(); let current_parent = state.current_parent(); let is_inline_outside = node_is_inline_outside(&layout_host, content_item); diff --git a/Tests/LibWeb/Layout/expected/abspos-pseudo-element-with-inline-as-abspos-containing-block.txt b/Tests/LibWeb/Layout/expected/abspos-pseudo-element-with-inline-as-abspos-containing-block.txt index 5f1813ae00e25..89d3e879c5ef2 100644 --- a/Tests/LibWeb/Layout/expected/abspos-pseudo-element-with-inline-as-abspos-containing-block.txt +++ b/Tests/LibWeb/Layout/expected/abspos-pseudo-element-with-inline-as-abspos-containing-block.txt @@ -5,8 +5,7 @@ Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children frag 0 from TextNode start: 0, length: 8, rect: [8,8 74.125x16] baseline: 12.796875 "Features" TextNode <#text> (not painted) - BlockContainer <(anonymous)> at [85.625,15.1875] positioned [0+0+0 8 0+0+0] [0+0+0 5 0+0+0] [BFC] children: inline - GeneratedTextNode <(anonymous)> (not painted) + BlockContainer <(anonymous)> at [85.625,15.1875] positioned [0+0+0 8 0+0+0] [0+0+0 5 0+0+0] [BFC] children: not-inline TextNode <#text> (not painted) ViewportPaintable (Viewport<#document>) [0,0 800x600] diff --git a/Tests/LibWeb/Layout/expected/block-and-inline/button-with-abspos-pseudo-element.txt b/Tests/LibWeb/Layout/expected/block-and-inline/button-with-abspos-pseudo-element.txt index ed050f6b16627..3411f542c1f03 100644 --- a/Tests/LibWeb/Layout/expected/block-and-inline/button-with-abspos-pseudo-element.txt +++ b/Tests/LibWeb/Layout/expected/block-and-inline/button-with-abspos-pseudo-element.txt @@ -5,8 +5,7 @@ Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children BlockContainer