From c01d810e5a86d8332ce15e876514e8fcc47994a0 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 13 Jan 2025 12:23:40 +0100 Subject: [PATCH] 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. --- Libraries/LibWeb/DOM/Document.cpp | 5 +- Libraries/LibWeb/DOM/Document.h | 4 + Libraries/LibWeb/DOM/Node.cpp | 23 ++++ Libraries/LibWeb/DOM/Node.h | 9 ++ Libraries/LibWeb/Layout/TreeBuilder.cpp | 162 +++++++++++++++--------- Libraries/LibWeb/Layout/TreeBuilder.h | 8 +- 6 files changed, 152 insertions(+), 59 deletions(-) diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 9855d73a7ba..0454e073bad 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -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(*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 diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 9d08415c4f8..d1c17cc43da 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -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 }; diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index 24c6dbeb6ba..d69eba7c5e2 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -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(*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) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index c3b6e39d6cf..587e0405f0f 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -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 m_layout_node; GC::Ptr 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 }; diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index 83436ca3926..e3901fa9451 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -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_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_node)); }; + GC::Ptr old_layout_node = dom_node.layout_node(); GC::Ptr layout_node; Optional> 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(node)) @@ -349,46 +357,63 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& GC::Ptr style; CSS::Display display; - if (is(dom_node)) { - auto& element = static_cast(dom_node); - element.clear_pseudo_element_nodes({}); - VERIFY(!element.needs_style_update()); - style = element.computed_properties(); - element.resolve_counters(*style); - display = style->display(); - if (display.is_none()) - return; - // TODO: Implement changing element contents with the `content` property. - if (context.layout_svg_mask_or_clip_path) { - if (is(dom_node)) - layout_node = document.heap().allocate(document, static_cast(dom_node), *style); - else if (is(dom_node)) - layout_node = document.heap().allocate(document, static_cast(dom_node), *style); - else - VERIFY_NOT_REACHED(); - // Only layout direct uses of SVG masks/clipPaths. - context.layout_svg_mask_or_clip_path = false; - } else { - layout_node = element.create_layout_node(*style); + if (!should_create_layout_node) { + if (is(dom_node)) { + auto& element = static_cast(dom_node); + style = element.computed_properties(); + display = style->display(); + } + layout_node = dom_node.layout_node(); + } else { + if (is(dom_node)) { + auto& element = static_cast(dom_node); + element.clear_pseudo_element_nodes({}); + VERIFY(!element.needs_style_update()); + style = element.computed_properties(); + element.resolve_counters(*style); + display = style->display(); + if (display.is_none()) + return; + // TODO: Implement changing element contents with the `content` property. + if (context.layout_svg_mask_or_clip_path) { + if (is(dom_node)) + layout_node = document.heap().allocate(document, static_cast(dom_node), *style); + else if (is(dom_node)) + layout_node = document.heap().allocate(document, static_cast(dom_node), *style); + else + VERIFY_NOT_REACHED(); + // Only layout direct uses of SVG masks/clipPaths. + context.layout_svg_mask_or_clip_path = false; + } else { + layout_node = element.create_layout_node(*style); + } + } else if (is(dom_node)) { + style = style_computer.create_document_style(); + display = style->display(); + layout_node = document.heap().allocate(static_cast(dom_node), *style); + } else if (is(dom_node)) { + layout_node = document.heap().allocate(document, static_cast(dom_node)); + display = CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow); } - } else if (is(dom_node)) { - style = style_computer.create_document_style(); - display = style->display(); - layout_node = document.heap().allocate(static_cast(dom_node), *style); - } else if (is(dom_node)) { - layout_node = document.heap().allocate(document, static_cast(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 { - insert_node_into_inline_or_block_ancestor(*layout_node, display, AppendOrPrepend::Append); + } 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_node) ? verify_cast(dom_node).shadow_root() : nullptr; @@ -401,6 +426,44 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& return false; }(); + 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(*layout_node)); + if (shadow_root) { + for (auto* node = shadow_root->first_child(); node; node = node->next_sibling()) { + 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_node).for_each_child + for (auto* node = verify_cast(dom_node).first_child(); node; node = node->next_sibling()) + update_layout_tree(*node, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); + } + + if (dom_node.is_document()) { + // Elements in the top layer do not lay out normally based on their position in the document; instead they + // generate boxes as if they were siblings of the root element. + TemporaryChange layout_mask(context.layout_top_layer, true); + for (auto const& top_layer_element : document.top_layer_elements()) + 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, TreeBuilder::Context&, bool element_has_content_visibility_hidden) +{ // Add node for the ::before pseudo-element. if (is(dom_node) && layout_node->can_have_children() && !element_has_content_visibility_hidden) { auto& element = static_cast(dom_node); @@ -408,28 +471,13 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::Before, AppendOrPrepend::Prepend); pop_parent(); } +} - if ((dom_node.has_children() || shadow_root) && layout_node->can_have_children() && !element_has_content_visibility_hidden) { - push_parent(verify_cast(*layout_node)); - if (shadow_root) { - for (auto* node = shadow_root->first_child(); node; node = node->next_sibling()) { - create_layout_tree(*node, context); - } - } else { - // This is the same as verify_cast(dom_node).for_each_child - for (auto* node = verify_cast(dom_node).first_child(); node; node = node->next_sibling()) - create_layout_tree(*node, context); - } - - if (dom_node.is_document()) { - // Elements in the top layer do not lay out normally based on their position in the document; instead they - // generate boxes as if they were siblings of the root element. - TemporaryChange layout_mask(context.layout_top_layer, true); - for (auto const& top_layer_element : document.top_layer_elements()) - create_layout_tree(top_layer_element, context); - } - pop_parent(); - } +void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref 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(*layout_node)) { auto& element = static_cast(dom_node); @@ -450,7 +498,7 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& push_parent(verify_cast(*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 mask_or_clip_path) { TemporaryChange layout_mask(context.layout_svg_mask_or_clip_path, true); push_parent(verify_cast(*layout_node)); - create_layout_tree(const_cast(*mask_or_clip_path), context); + update_layout_tree(const_cast(*mask_or_clip_path), context, MustCreateSubtree::Yes); pop_parent(); }; if (auto mask = graphics_element.mask()) @@ -550,12 +598,12 @@ GC::Ptr 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 diff --git a/Libraries/LibWeb/Layout/TreeBuilder.h b/Libraries/LibWeb/Layout/TreeBuilder.h index b103500a1d4..14f36e40d12 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.h +++ b/Libraries/LibWeb/Layout/TreeBuilder.h @@ -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, Context&, bool element_has_content_visibility_hidden); + void update_layout_tree_after_children(DOM::Node&, GC::Ref, 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(); }