Move the layout tree from GC allocation to refcounted ownership so removed layout and paint subtrees are destroyed synchronously instead of waiting for the next GC sweep. This dramatically reduces GC memory usage peaks after layout tree churn and makes it easier for memory use to fall back after large document updates. Update layout factories, tree traversal, SVG layout node creation, paintable back-pointers, and pseudo-element layout links to use RefPtr ownership. Make display: contents follow the same shape as Blink and WebKit: the element itself does not create a layout node, and its children are flattened into the nearest layout parent. Wrap direct non-whitespace text in an anonymous inline node when the boxless element contributes inherited style to that text. Use an internal inline wrapper for display: contents pseudo-elements so generated content can still participate in layout, painting, hit testing, and pseudo-element queries. Keep CSSOM reporting the computed display value from the pseudo style, not the internal wrapper. Remove the retained out-of-tree layout node list and its testing hook, since the flattened model does not need a side owner for boxless elements. Add coverage for inherited text style, dynamic insertion order, pseudo-element hit testing, and computed style queries.
450 lines
17 KiB
C++
450 lines
17 KiB
C++
/*
|
|
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <AK/DoublyLinkedList.h>
|
|
#include <AK/NonnullOwnPtr.h>
|
|
#include <AK/NonnullRefPtr.h>
|
|
#include <AK/RefCounted.h>
|
|
#include <AK/Vector.h>
|
|
#include <AK/WeakPtr.h>
|
|
#include <AK/Weakable.h>
|
|
#include <LibGC/Cell.h>
|
|
#include <LibGC/Weak.h>
|
|
#include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/ImageStyleValue.h>
|
|
#include <LibWeb/Export.h>
|
|
#include <LibWeb/Forward.h>
|
|
#include <LibWeb/Painting/DisplayListRecordingContext.h>
|
|
#include <LibWeb/Painting/Paintable.h>
|
|
#include <LibWeb/RefCountedTreeNode.h>
|
|
|
|
namespace Web::Layout {
|
|
|
|
#define LAYOUT_NODE(class_, base_class) \
|
|
public: \
|
|
using Base = base_class; \
|
|
virtual StringView class_name() const override \
|
|
{ \
|
|
return #class_##sv; \
|
|
}
|
|
|
|
class InlineNode;
|
|
|
|
enum class LayoutMode {
|
|
// Normal layout. No min-content or max-content constraints applied.
|
|
Normal,
|
|
|
|
// Intrinsic size determination.
|
|
// Boxes honor min-content and max-content constraints (set via LayoutState::UsedValues::{width,height}_constraint)
|
|
// by considering their containing block to be 0-sized or infinitely large in the relevant axis.
|
|
// https://drafts.csswg.org/css-sizing-3/#intrinsic-sizing
|
|
IntrinsicSizing,
|
|
};
|
|
|
|
class WEB_API Node
|
|
: public RefCounted<Node>
|
|
, public Weakable<Node>
|
|
, public RefCountedTreeNode<Node> {
|
|
|
|
public:
|
|
using Base = RefCountedTreeNode<Node>;
|
|
|
|
virtual ~Node();
|
|
virtual StringView class_name() const { return "Node"sv; }
|
|
|
|
bool is_anonymous() const;
|
|
DOM::Node const* dom_node() const;
|
|
DOM::Node* dom_node();
|
|
|
|
DOM::Element const* pseudo_element_generator() const;
|
|
DOM::Element* pseudo_element_generator();
|
|
|
|
bool needs_layout_update() const { return m_needs_layout_update; }
|
|
void set_needs_layout_update(DOM::SetNeedsLayoutReason);
|
|
void reset_needs_layout_update() { m_needs_layout_update = false; }
|
|
|
|
bool is_generated_for_pseudo_element() const { return m_generated_for.has_value(); }
|
|
Optional<CSS::PseudoElement> generated_for_pseudo_element() const { return m_generated_for; }
|
|
bool is_generated_for_before_pseudo_element() const { return m_generated_for == CSS::PseudoElement::Before; }
|
|
bool is_generated_for_after_pseudo_element() const { return m_generated_for == CSS::PseudoElement::After; }
|
|
bool is_generated_for_backdrop_pseudo_element() const { return m_generated_for == CSS::PseudoElement::Backdrop; }
|
|
void set_generated_for(CSS::PseudoElement type, DOM::Element&);
|
|
|
|
using PaintableList = DoublyLinkedList<NonnullRefPtr<Painting::Paintable>>;
|
|
|
|
RefPtr<Painting::Paintable> first_paintable()
|
|
{
|
|
if (m_paintable.is_empty())
|
|
return nullptr;
|
|
return m_paintable.first();
|
|
}
|
|
RefPtr<Painting::Paintable const> first_paintable() const
|
|
{
|
|
if (m_paintable.is_empty())
|
|
return nullptr;
|
|
return m_paintable.first();
|
|
}
|
|
PaintableList& paintables() { return m_paintable; }
|
|
PaintableList const& paintables() const { return m_paintable; }
|
|
void add_paintable(RefPtr<Painting::Paintable>);
|
|
void clear_paintables();
|
|
void prepare_for_detach_from_layout_tree();
|
|
void prepare_subtree_for_detach_from_layout_tree();
|
|
|
|
virtual RefPtr<Painting::Paintable> create_paintable() const;
|
|
|
|
DOM::Document& document();
|
|
DOM::Document const& document() const;
|
|
|
|
GC::Ptr<HTML::Navigable> navigable() const;
|
|
|
|
Viewport const& root() const;
|
|
Viewport& root();
|
|
|
|
bool is_root_element() const;
|
|
|
|
String debug_description() const;
|
|
|
|
bool has_style() const { return m_has_style; }
|
|
bool has_style_or_parent_with_style() const;
|
|
|
|
virtual bool can_have_children() const { return true; }
|
|
|
|
CSS::Display display() const;
|
|
CSS::Display display_before_box_type_transformation() const;
|
|
|
|
bool is_inline() const;
|
|
bool is_inline_block() const;
|
|
bool is_inline_table() const;
|
|
|
|
bool is_atomic_inline() const;
|
|
|
|
bool is_out_of_flow(FormattingContext const&) const;
|
|
|
|
// These are used to optimize hot is<T> variants for some classes where dynamic_cast is too slow.
|
|
virtual bool is_box() const { return false; }
|
|
virtual bool is_block_container() const { return false; }
|
|
virtual bool is_inline_node() const { return false; }
|
|
virtual bool is_break_node() const { return false; }
|
|
virtual bool is_text_node() const { return false; }
|
|
virtual bool is_text_slice_node() const { return false; }
|
|
virtual bool is_viewport() const { return false; }
|
|
virtual bool is_svg_box() const { return false; }
|
|
virtual bool is_svg_geometry_box() const { return false; }
|
|
virtual bool is_svg_clip_box() const { return false; }
|
|
virtual bool is_svg_mask_box() const { return false; }
|
|
virtual bool is_svg_pattern_box() const { return false; }
|
|
virtual bool is_svg_svg_box() const { return false; }
|
|
virtual bool is_svg_graphics_box() const { return false; }
|
|
virtual bool is_svg_foreign_object_box() const { return false; }
|
|
virtual bool is_label() const { return false; }
|
|
virtual bool is_replaced_box() const { return false; }
|
|
virtual bool is_textarea_box() const { return false; }
|
|
virtual bool is_list_item_box() const { return false; }
|
|
virtual bool is_list_item_marker_box() const { return false; }
|
|
virtual bool is_fieldset_box() const { return false; }
|
|
virtual bool is_legend_box() const { return false; }
|
|
virtual bool is_table_wrapper() const { return false; }
|
|
virtual bool is_node_with_style() const { return false; }
|
|
virtual bool is_node_with_style_and_box_model_metrics() const { return false; }
|
|
|
|
bool is_replaced_box_with_children() const { return is_replaced_box() && can_have_children(); }
|
|
|
|
template<typename T>
|
|
bool fast_is() const = delete;
|
|
|
|
bool is_floating() const;
|
|
bool is_positioned() const;
|
|
bool is_absolutely_positioned() const;
|
|
bool is_fixed_position() const;
|
|
bool is_sticky_position() const;
|
|
|
|
bool is_flex_item() const { return m_is_flex_item; }
|
|
void set_flex_item(bool b) { m_is_flex_item = b; }
|
|
|
|
bool is_grid_item() const { return m_is_grid_item; }
|
|
void set_grid_item(bool b) { m_is_grid_item = b; }
|
|
|
|
bool vertical_align_applies() const
|
|
{
|
|
// https://drafts.csswg.org/css-flexbox/#flex-containers
|
|
// "vertical-align has no effect on a flex item"
|
|
if (is_flex_item())
|
|
return false;
|
|
// https://drafts.csswg.org/css-grid-1/#grid-container
|
|
// "vertical-align has no effect on a grid item"
|
|
if (is_grid_item())
|
|
return false;
|
|
// FIXME: Per-spec, vertical-align only applies to inline-level boxes and table cells; this should be narrowed
|
|
// to that — rather than only excluding flex and grid items.
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] Box const* containing_block() const { return m_containing_block; }
|
|
[[nodiscard]] Box* containing_block() { return m_containing_block; }
|
|
|
|
// Returns the inline node that actually establishes the containing block for this absolutely
|
|
// positioned element, if applicable. This is needed because m_containing_block can only hold
|
|
// a Box*, but CSS allows inline elements (like a <span> with position:relative) to establish
|
|
// containing blocks for their absolutely positioned descendants.
|
|
// See the large FIXME comment in FormattingContext.cpp for full context.
|
|
[[nodiscard]] InlineNode const* inline_containing_block_if_applicable() const { return m_inline_containing_block_if_applicable; }
|
|
|
|
void recompute_containing_block(Badge<DOM::Document>);
|
|
|
|
[[nodiscard]] Box const* static_position_containing_block() const;
|
|
[[nodiscard]] Box* static_position_containing_block() { return const_cast<Box*>(const_cast<Node const*>(this)->static_position_containing_block()); }
|
|
|
|
// Closest non-anonymous ancestor box, to be used when resolving percentage values.
|
|
// Anonymous block boxes are ignored when resolving percentage values that would refer to it:
|
|
// the closest non-anonymous ancestor box is used instead.
|
|
// https://www.w3.org/TR/CSS22/visuren.html#anonymous-block-level
|
|
Box const* non_anonymous_containing_block() const;
|
|
|
|
bool establishes_stacking_context() const;
|
|
|
|
bool computed_values_establish_absolute_positioning_containing_block() const;
|
|
bool establishes_an_absolute_positioning_containing_block() const;
|
|
bool establishes_a_fixed_positioning_containing_block() const;
|
|
|
|
Gfx::Font const& first_available_font() const;
|
|
Gfx::Font const& font(DisplayListRecordingContext&) const;
|
|
Gfx::Font const& font(float scale_factor) const;
|
|
|
|
CSS::ImmutableComputedValues const& computed_values() const;
|
|
|
|
NodeWithStyle* parent();
|
|
NodeWithStyle const* parent() const;
|
|
|
|
void inserted_into(Node&) { }
|
|
void removed_from(Node&) { }
|
|
void children_changed() { }
|
|
|
|
bool children_are_inline() const { return m_children_are_inline; }
|
|
void set_children_are_inline(bool value) { m_children_are_inline = value; }
|
|
|
|
u32 initial_quote_nesting_level() const { return m_initial_quote_nesting_level; }
|
|
void set_initial_quote_nesting_level(u32 value) { m_initial_quote_nesting_level = value; }
|
|
|
|
// An element is called out of flow if it is floated, absolutely positioned, or is the root element.
|
|
// https://www.w3.org/TR/CSS22/visuren.html#positioning-scheme
|
|
bool is_out_of_flow() const { return is_floating() || is_absolutely_positioned(); }
|
|
|
|
// An element is called in-flow if it is not out-of-flow.
|
|
// https://www.w3.org/TR/CSS22/visuren.html#positioning-scheme
|
|
bool is_in_flow() const { return !is_out_of_flow(); }
|
|
|
|
[[nodiscard]] bool has_css_transform() const
|
|
{
|
|
auto const& computed_values = this->computed_values();
|
|
return !computed_values.transformations().is_empty()
|
|
|| computed_values.rotate()
|
|
|| computed_values.translate()
|
|
|| computed_values.scale();
|
|
}
|
|
|
|
// https://drafts.csswg.org/css-ui/#propdef-user-select
|
|
CSS::UserSelect user_select_used_value() const;
|
|
|
|
// https://drafts.csswg.org/css-contain-2/#containment-types
|
|
bool has_size_containment() const;
|
|
bool has_inline_size_containment() const;
|
|
bool has_layout_containment() const;
|
|
bool has_style_containment() const;
|
|
bool has_paint_containment() const;
|
|
|
|
[[nodiscard]] bool has_been_wrapped_in_table_wrapper() const { return m_has_been_wrapped_in_table_wrapper; }
|
|
void set_has_been_wrapped_in_table_wrapper(bool value) { m_has_been_wrapped_in_table_wrapper = value; }
|
|
|
|
enum class AttachToDOMNode {
|
|
No,
|
|
Yes,
|
|
};
|
|
|
|
protected:
|
|
Node(DOM::Document&, DOM::Node*, AttachToDOMNode = AttachToDOMNode::Yes);
|
|
|
|
private:
|
|
friend class NodeWithStyle;
|
|
|
|
GC::Weak<DOM::Node> m_dom_node;
|
|
PaintableList m_paintable;
|
|
|
|
Box* m_containing_block { nullptr };
|
|
|
|
// For absolutely positioned elements, if there's an inline element (like a <span> with
|
|
// position:relative) that should be the containing block but can't be stored in m_containing_block
|
|
// (because it's not a Box), we store it here. This happens when a block element is inside an
|
|
// inline element - the layout tree restructures so the block becomes a sibling of the inline,
|
|
// but the CSS containing block relationship is based on the DOM structure.
|
|
InlineNode const* m_inline_containing_block_if_applicable { nullptr };
|
|
|
|
GC::Weak<DOM::Element> m_pseudo_element_generator;
|
|
|
|
bool m_anonymous { false };
|
|
bool m_has_style { false };
|
|
bool m_children_are_inline { false };
|
|
|
|
bool m_is_flex_item { false };
|
|
bool m_is_grid_item { false };
|
|
|
|
bool m_has_been_wrapped_in_table_wrapper { false };
|
|
bool m_is_body { false };
|
|
|
|
bool m_needs_layout_update { false };
|
|
|
|
Optional<CSS::PseudoElement> m_generated_for;
|
|
|
|
u32 m_initial_quote_nesting_level { 0 };
|
|
};
|
|
|
|
class WEB_API NodeWithStyle : public Node {
|
|
LAYOUT_NODE(NodeWithStyle, Node);
|
|
|
|
public:
|
|
virtual ~NodeWithStyle() override;
|
|
|
|
class ImageObserver final : public CSS::ImageStyleValue::Client {
|
|
public:
|
|
ImageObserver(NodeWithStyle&, NonnullRefPtr<CSS::ImageStyleValue const> image);
|
|
virtual ~ImageObserver() override;
|
|
|
|
virtual void image_style_value_did_update(CSS::ImageStyleValue&) override;
|
|
|
|
private:
|
|
WeakPtr<NodeWithStyle> m_owner;
|
|
NonnullRefPtr<CSS::ImageStyleValue const> m_image;
|
|
};
|
|
|
|
CSS::ImmutableComputedValues const& computed_values() const { return static_cast<CSS::ImmutableComputedValues const&>(*m_computed_values); }
|
|
CSS::MutableComputedValues& mutable_computed_values() { return static_cast<CSS::MutableComputedValues&>(*m_computed_values); }
|
|
|
|
void clear_image_observers();
|
|
void apply_style(CSS::ComputedProperties const&);
|
|
|
|
Gfx::Font const& first_available_font() const;
|
|
Vector<CSS::BackgroundLayerData> const& background_layers() const { return computed_values().background_layers(); }
|
|
CSS::AbstractImageStyleValue const* list_style_image() const { return m_list_style_image; }
|
|
CSS::StyleScope const& style_scope() const;
|
|
|
|
NonnullRefPtr<NodeWithStyle> create_anonymous_wrapper() const;
|
|
|
|
void transfer_table_box_computed_values_to_wrapper_computed_values(CSS::ComputedValues& wrapper_computed_values);
|
|
|
|
bool is_body() const { return m_is_body; }
|
|
bool is_scroll_container() const;
|
|
|
|
void set_computed_values(NonnullOwnPtr<CSS::ComputedValues>);
|
|
|
|
u32 layout_index() const { return m_layout_index; }
|
|
void set_layout_index(u32 index) { m_layout_index = index; }
|
|
|
|
protected:
|
|
NodeWithStyle(DOM::Document&, DOM::Node*, CSS::ComputedProperties const&);
|
|
NodeWithStyle(DOM::Document&, DOM::Node*, NonnullOwnPtr<CSS::ComputedValues>);
|
|
|
|
private:
|
|
virtual bool is_node_with_style() const final { return true; }
|
|
|
|
void reset_table_box_computed_values_used_by_wrapper_to_init_values();
|
|
void propagate_non_inherit_values(NodeWithStyle& target_node) const;
|
|
void propagate_style_to_anonymous_wrappers();
|
|
|
|
void rebuild_image_observers();
|
|
|
|
NonnullOwnPtr<CSS::ComputedValues> m_computed_values;
|
|
RefPtr<CSS::AbstractImageStyleValue const> m_list_style_image;
|
|
Vector<NonnullOwnPtr<ImageObserver>> m_image_observers;
|
|
u32 m_layout_index { 0 };
|
|
};
|
|
|
|
template<>
|
|
inline bool Node::fast_is<NodeWithStyle>() const { return is_node_with_style(); }
|
|
|
|
class NodeWithStyleAndBoxModelMetrics : public NodeWithStyle {
|
|
LAYOUT_NODE(NodeWithStyleAndBoxModelMetrics, NodeWithStyle);
|
|
|
|
public:
|
|
NodeWithStyleAndBoxModelMetrics* continuation_of_node() const { return m_continuation_of_node.ptr(); }
|
|
void set_continuation_of_node(Badge<TreeBuilder>, NodeWithStyleAndBoxModelMetrics* node) { m_continuation_of_node = node; }
|
|
|
|
bool should_create_inline_continuation() const;
|
|
|
|
void propagate_style_along_continuation(CSS::ComputedProperties const&) const;
|
|
|
|
protected:
|
|
NodeWithStyleAndBoxModelMetrics(DOM::Document&, DOM::Node*, CSS::ComputedProperties const&);
|
|
|
|
NodeWithStyleAndBoxModelMetrics(DOM::Document& document, DOM::Node* node, NonnullOwnPtr<CSS::ComputedValues> computed_values)
|
|
: NodeWithStyle(document, node, move(computed_values))
|
|
{
|
|
}
|
|
|
|
private:
|
|
virtual bool is_node_with_style_and_box_model_metrics() const final { return true; }
|
|
|
|
WeakPtr<NodeWithStyleAndBoxModelMetrics> m_continuation_of_node;
|
|
};
|
|
|
|
template<>
|
|
inline bool Node::fast_is<NodeWithStyleAndBoxModelMetrics>() const { return is_node_with_style_and_box_model_metrics(); }
|
|
|
|
inline bool Node::has_style_or_parent_with_style() const
|
|
{
|
|
return m_has_style || (parent() != nullptr && parent()->has_style_or_parent_with_style());
|
|
}
|
|
|
|
inline Gfx::Font const& Node::first_available_font() const
|
|
{
|
|
VERIFY(has_style_or_parent_with_style());
|
|
if (m_has_style)
|
|
return static_cast<NodeWithStyle const*>(this)->first_available_font();
|
|
return parent()->first_available_font();
|
|
}
|
|
|
|
inline Gfx::Font const& Node::font(DisplayListRecordingContext& context) const
|
|
{
|
|
return font(context.device_pixels_per_css_pixel());
|
|
}
|
|
|
|
inline Gfx::Font const& Node::font(float scale_factor) const
|
|
{
|
|
auto const& font = first_available_font();
|
|
return font.with_size(font.point_size() * scale_factor);
|
|
}
|
|
|
|
inline CSS::ImmutableComputedValues const& Node::computed_values() const
|
|
{
|
|
VERIFY(has_style_or_parent_with_style());
|
|
|
|
if (m_has_style)
|
|
return static_cast<NodeWithStyle const*>(this)->computed_values();
|
|
return parent()->computed_values();
|
|
}
|
|
|
|
inline NodeWithStyle const* Node::parent() const
|
|
{
|
|
return static_cast<NodeWithStyle const*>(Base::parent().ptr());
|
|
}
|
|
|
|
inline NodeWithStyle* Node::parent()
|
|
{
|
|
return static_cast<NodeWithStyle*>(Base::parent().ptr());
|
|
}
|
|
|
|
inline Gfx::Font const& NodeWithStyle::first_available_font() const
|
|
{
|
|
// https://drafts.csswg.org/css-fonts/#first-available-font
|
|
// First font for which the character U+0020 (space) is not excluded by a unicode-range
|
|
return computed_values().font_list().font_for_code_point(' ');
|
|
}
|
|
|
|
bool overflow_value_makes_box_a_scroll_container(CSS::Overflow overflow);
|
|
|
|
}
|