mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-31 13:19:05 +00:00
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:
parent
b41a490e40
commit
c01d810e5a
Notes:
github-actions[bot]
2025-01-18 20:02:07 +00:00
Author: https://github.com/awesomekling
Commit: c01d810e5a
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3274
6 changed files with 152 additions and 59 deletions
|
@ -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,46 +357,63 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
|
|||
GC::Ptr<CSS::ComputedProperties> style;
|
||||
CSS::Display display;
|
||||
|
||||
if (is<DOM::Element>(dom_node)) {
|
||||
auto& element = static_cast<DOM::Element&>(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<SVG::SVGMaskElement>(dom_node))
|
||||
layout_node = document.heap().allocate<Layout::SVGMaskBox>(document, static_cast<SVG::SVGMaskElement&>(dom_node), *style);
|
||||
else if (is<SVG::SVGClipPathElement>(dom_node))
|
||||
layout_node = document.heap().allocate<Layout::SVGClipBox>(document, static_cast<SVG::SVGClipPathElement&>(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::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({});
|
||||
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<SVG::SVGMaskElement>(dom_node))
|
||||
layout_node = document.heap().allocate<Layout::SVGMaskBox>(document, static_cast<SVG::SVGMaskElement&>(dom_node), *style);
|
||||
else if (is<SVG::SVGClipPathElement>(dom_node))
|
||||
layout_node = document.heap().allocate<Layout::SVGClipBox>(document, static_cast<SVG::SVGClipPathElement&>(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::Document>(dom_node)) {
|
||||
style = style_computer.create_document_style();
|
||||
display = style->display();
|
||||
layout_node = document.heap().allocate<Layout::Viewport>(static_cast<DOM::Document&>(dom_node), *style);
|
||||
} else if (is<DOM::Text>(dom_node)) {
|
||||
layout_node = document.heap().allocate<Layout::TextNode>(document, static_cast<DOM::Text&>(dom_node));
|
||||
display = CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow);
|
||||
}
|
||||
} else if (is<DOM::Document>(dom_node)) {
|
||||
style = style_computer.create_document_style();
|
||||
display = style->display();
|
||||
layout_node = document.heap().allocate<Layout::Viewport>(static_cast<DOM::Document&>(dom_node), *style);
|
||||
} else if (is<DOM::Text>(dom_node)) {
|
||||
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 {
|
||||
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::Element>(dom_node) ? verify_cast<DOM::Element>(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<NodeWithStyle>(*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::ParentNode>(dom_node).for_each_child
|
||||
for (auto* node = verify_cast<DOM::ParentNode>(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<bool> 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> 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);
|
||||
|
@ -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<NodeWithStyle>(*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::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);
|
||||
}
|
||||
|
||||
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<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);
|
||||
}
|
||||
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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue