LibWeb: Implement partial layout tree updates

DOM nodes now have two additional flags:

- Needs layout tree update
- Child needs layout tree update

These work similarly to the needs-style-update flags, but instead signal
the need to rebuild the corresponding part of the layout tree.

When a specific DOM node needs a layout tree update, we try to create
a new subtree starting at that node, and then replace the subtree in the
old layout tree with the newly created subtree.

This required some refactoring in TreeBuilder so that we can skip over
entire subtrees during a tree update.

Note that no partial updates happen yet (as of this commit) since we
always invalidate the full layout tree still. That will change in the
next commit.
This commit is contained in:
Andreas Kling 2025-01-13 12:23:40 +01:00 committed by Andreas Kling
commit c01d810e5a
Notes: github-actions[bot] 2025-01-18 20:02:07 +00:00
6 changed files with 152 additions and 59 deletions

View file

@ -1009,6 +1009,7 @@ void Document::tear_down_layout_tree()
{
m_layout_root = nullptr;
m_paintable = nullptr;
m_needs_full_layout_tree_update = true;
}
Color Document::background_color() const
@ -1218,7 +1219,7 @@ void Document::update_layout()
auto* document_element = this->document_element();
auto viewport_rect = navigable->viewport_rect();
if (!m_layout_root) {
if (!m_layout_root || needs_layout_tree_update() || child_needs_layout_tree_update() || needs_full_layout_tree_update()) {
Layout::TreeBuilder tree_builder;
m_layout_root = verify_cast<Layout::Viewport>(*tree_builder.build(*this));
@ -1226,6 +1227,8 @@ void Document::update_layout()
propagate_overflow_to_viewport(*document_element, *m_layout_root);
propagate_scrollbar_width_to_viewport(*document_element, *m_layout_root);
}
set_needs_full_layout_tree_update(false);
}
// Assign each box that establishes a formatting context a list of absolutely positioned children it should take care of during layout

View file

@ -502,6 +502,9 @@ public:
bool needs_full_style_update() const { return m_needs_full_style_update; }
void set_needs_full_style_update(bool b) { m_needs_full_style_update = b; }
[[nodiscard]] bool needs_full_layout_tree_update() const { return m_needs_full_layout_tree_update; }
void set_needs_full_layout_tree_update(bool b) { m_needs_full_layout_tree_update = b; }
void set_needs_to_refresh_scroll_state(bool b);
bool has_active_favicon() const { return m_active_favicon; }
@ -936,6 +939,7 @@ private:
bool m_needs_layout { false };
bool m_needs_full_style_update { false };
bool m_needs_full_layout_tree_update { false };
bool m_needs_animated_style_update { false };

View file

@ -1326,6 +1326,29 @@ void Node::set_needs_inherited_style_update(bool value)
}
}
void Node::set_needs_layout_tree_update(bool value)
{
if (m_needs_layout_tree_update == value)
return;
m_needs_layout_tree_update = value;
// NOTE: If this is a shadow root, we need to propagate the layout tree update to the host.
if (is_shadow_root()) {
auto& shadow_root = static_cast<ShadowRoot&>(*this);
if (auto host = shadow_root.host())
host->set_needs_layout_tree_update(value);
}
if (m_needs_layout_tree_update) {
for (auto* ancestor = parent_or_shadow_host(); ancestor; ancestor = ancestor->parent_or_shadow_host()) {
if (ancestor->m_child_needs_layout_tree_update)
break;
ancestor->m_child_needs_layout_tree_update = true;
}
document().set_needs_layout();
}
}
void Node::set_needs_style_update(bool value)
{
if (m_needs_style_update == value)

View file

@ -281,6 +281,12 @@ public:
virtual bool is_child_allowed(Node const&) const { return true; }
[[nodiscard]] bool needs_layout_tree_update() const { return m_needs_layout_tree_update; }
void set_needs_layout_tree_update(bool);
[[nodiscard]] bool child_needs_layout_tree_update() const { return m_child_needs_layout_tree_update; }
void set_child_needs_layout_tree_update(bool b) { m_child_needs_layout_tree_update = b; }
bool needs_style_update() const { return m_needs_style_update; }
void set_needs_style_update(bool);
@ -789,6 +795,9 @@ protected:
GC::Ptr<Layout::Node> m_layout_node;
GC::Ptr<Painting::Paintable> m_paintable;
NodeType m_type { NodeType::INVALID };
bool m_needs_layout_tree_update { false };
bool m_child_needs_layout_tree_update { false };
bool m_needs_style_update { false };
bool m_needs_inherited_style_update { false };
bool m_child_needs_style_update { false };

View file

@ -306,8 +306,13 @@ i32 TreeBuilder::calculate_list_item_index(DOM::Node& dom_node)
return 1;
}
void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& context)
void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& context, MustCreateSubtree must_create_subtree)
{
bool should_create_layout_node = must_create_subtree == MustCreateSubtree::Yes
|| dom_node.needs_layout_tree_update()
|| dom_node.document().needs_full_layout_tree_update()
|| (dom_node.is_document() && !dom_node.layout_node());
if (dom_node.is_element()) {
auto& element = static_cast<DOM::Element&>(dom_node);
if (element.in_top_layer() && !context.layout_top_layer)
@ -321,6 +326,7 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
dom_node.document().style_computer().pop_ancestor(static_cast<DOM::Element const&>(dom_node));
};
GC::Ptr<Layout::Node> old_layout_node = dom_node.layout_node();
GC::Ptr<Layout::Node> layout_node;
Optional<TemporaryChange<bool>> has_svg_root_change;
@ -329,6 +335,8 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
// go through the DOM tree and remove any old layout & paint nodes since they are now all stale.
if (!layout_node) {
dom_node.for_each_in_inclusive_subtree([&](auto& node) {
node.set_needs_layout_tree_update(false);
node.set_child_needs_layout_tree_update(false);
node.detach_layout_node({});
node.clear_paintable();
if (is<DOM::Element>(node))
@ -349,6 +357,14 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
GC::Ptr<CSS::ComputedProperties> style;
CSS::Display display;
if (!should_create_layout_node) {
if (is<DOM::Element>(dom_node)) {
auto& element = static_cast<DOM::Element&>(dom_node);
style = element.computed_properties();
display = style->display();
}
layout_node = dom_node.layout_node();
} else {
if (is<DOM::Element>(dom_node)) {
auto& element = static_cast<DOM::Element&>(dom_node);
element.clear_pseudo_element_nodes({});
@ -379,17 +395,26 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
layout_node = document.heap().allocate<Layout::TextNode>(document, static_cast<DOM::Text&>(dom_node));
display = CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow);
}
}
if (!layout_node)
return;
if (!dom_node.parent_or_shadow_host()) {
if (dom_node.is_document()) {
m_layout_root = layout_node;
} else if (layout_node->is_svg_box()) {
m_ancestor_stack.last()->append_child(*layout_node);
} else if (should_create_layout_node) {
// Decide whether to replace an existing node (partial tree update) or insert a new one appropriately.
if (must_create_subtree == MustCreateSubtree::No
&& old_layout_node
&& old_layout_node->parent()
&& old_layout_node != layout_node) {
old_layout_node->parent()->replace_child(*layout_node, *old_layout_node);
} else {
insert_node_into_inline_or_block_ancestor(*layout_node, display, AppendOrPrepend::Append);
}
}
auto shadow_root = is<DOM::Element>(dom_node) ? verify_cast<DOM::Element>(dom_node).shadow_root() : nullptr;
@ -401,24 +426,22 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
return false;
}();
// Add node for the ::before pseudo-element.
if (is<DOM::Element>(dom_node) && layout_node->can_have_children() && !element_has_content_visibility_hidden) {
auto& element = static_cast<DOM::Element&>(dom_node);
push_parent(verify_cast<NodeWithStyle>(*layout_node));
create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::Before, AppendOrPrepend::Prepend);
pop_parent();
}
if (should_create_layout_node)
update_layout_tree_before_children(dom_node, *layout_node, context, element_has_content_visibility_hidden);
if (should_create_layout_node || dom_node.child_needs_layout_tree_update()) {
if ((dom_node.has_children() || shadow_root) && layout_node->can_have_children() && !element_has_content_visibility_hidden) {
push_parent(verify_cast<NodeWithStyle>(*layout_node));
if (shadow_root) {
for (auto* node = shadow_root->first_child(); node; node = node->next_sibling()) {
create_layout_tree(*node, context);
update_layout_tree(*node, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No);
}
shadow_root->set_child_needs_layout_tree_update(false);
shadow_root->set_needs_layout_tree_update(false);
} else {
// This is the same as verify_cast<DOM::ParentNode>(dom_node).for_each_child
for (auto* node = verify_cast<DOM::ParentNode>(dom_node).first_child(); node; node = node->next_sibling())
create_layout_tree(*node, context);
update_layout_tree(*node, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No);
}
if (dom_node.is_document()) {
@ -426,10 +449,35 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
// generate boxes as if they were siblings of the root element.
TemporaryChange<bool> layout_mask(context.layout_top_layer, true);
for (auto const& top_layer_element : document.top_layer_elements())
create_layout_tree(top_layer_element, context);
update_layout_tree(top_layer_element, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No);
}
pop_parent();
}
}
if (should_create_layout_node)
update_layout_tree_after_children(dom_node, *layout_node, context, element_has_content_visibility_hidden);
dom_node.set_needs_layout_tree_update(false);
dom_node.set_child_needs_layout_tree_update(false);
}
void TreeBuilder::update_layout_tree_before_children(DOM::Node& dom_node, GC::Ref<Layout::Node> layout_node, TreeBuilder::Context&, bool element_has_content_visibility_hidden)
{
// Add node for the ::before pseudo-element.
if (is<DOM::Element>(dom_node) && layout_node->can_have_children() && !element_has_content_visibility_hidden) {
auto& element = static_cast<DOM::Element&>(dom_node);
push_parent(verify_cast<NodeWithStyle>(*layout_node));
create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::Before, AppendOrPrepend::Prepend);
pop_parent();
}
}
void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref<Layout::Node> layout_node, TreeBuilder::Context& context, bool element_has_content_visibility_hidden)
{
auto& document = dom_node.document();
auto& style_computer = document.style_computer();
auto display = layout_node->display();
if (is<ListItemBox>(*layout_node)) {
auto& element = static_cast<DOM::Element&>(dom_node);
@ -450,7 +498,7 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
push_parent(verify_cast<NodeWithStyle>(*layout_node));
for (auto const& slottable : slottables)
slottable.visit([&](auto& node) { create_layout_tree(node, context); });
slottable.visit([&](auto& node) { update_layout_tree(node, context, MustCreateSubtree::Yes); });
pop_parent();
}
@ -464,7 +512,7 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
auto layout_mask_or_clip_path = [&](GC::Ptr<SVG::SVGElement const> mask_or_clip_path) {
TemporaryChange<bool> layout_mask(context.layout_svg_mask_or_clip_path, true);
push_parent(verify_cast<NodeWithStyle>(*layout_node));
create_layout_tree(const_cast<SVG::SVGElement&>(*mask_or_clip_path), context);
update_layout_tree(const_cast<SVG::SVGElement&>(*mask_or_clip_path), context, MustCreateSubtree::Yes);
pop_parent();
};
if (auto mask = graphics_element.mask())
@ -550,12 +598,12 @@ GC::Ptr<Layout::Node> TreeBuilder::build(DOM::Node& dom_node)
Context context;
m_quote_nesting_level = 0;
create_layout_tree(dom_node, context);
update_layout_tree(dom_node, context, MustCreateSubtree::No);
if (auto* root = dom_node.document().layout_node())
fixup_tables(*root);
return move(m_layout_root);
return m_layout_root;
}
template<CSS::DisplayInternal internal, typename Callback>

View file

@ -29,7 +29,13 @@ private:
i32 calculate_list_item_index(DOM::Node&);
void create_layout_tree(DOM::Node&, Context&);
void update_layout_tree_before_children(DOM::Node&, GC::Ref<Layout::Node>, Context&, bool element_has_content_visibility_hidden);
void update_layout_tree_after_children(DOM::Node&, GC::Ref<Layout::Node>, Context&, bool element_has_content_visibility_hidden);
enum class MustCreateSubtree {
No,
Yes,
};
void update_layout_tree(DOM::Node&, Context&, MustCreateSubtree);
void push_parent(Layout::NodeWithStyle& node) { m_ancestor_stack.append(node); }
void pop_parent() { m_ancestor_stack.take_last(); }