From 336684bc5cb64c3d3a486f934d0a2d636d557b8f Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Wed, 15 Jan 2025 16:37:30 +0100 Subject: [PATCH] LibWeb: Support inserting non-inline elements into inline elements Our layout tree requires that all containers either have inline or non-inline children. In order to support the layout of non-inline elements inside inline elements, we need to do a bit of tree restructuring. It effectively simulates temporarily closing all inline nodes, appending the block element, and resumes appending to the last open inline node. The acid1.txt expectation needed to be updated to reflect the fact that we now hoist its

elements out of the inline

they were in. Visually, the before and after situations for acid1.html are identical. --- Libraries/LibWeb/Dump.cpp | 9 +- Libraries/LibWeb/HTML/HTMLElement.cpp | 37 ++- Libraries/LibWeb/Layout/Box.h | 1 - Libraries/LibWeb/Layout/InlineNode.cpp | 1 - Libraries/LibWeb/Layout/LayoutState.cpp | 7 +- Libraries/LibWeb/Layout/Node.cpp | 22 +- Libraries/LibWeb/Layout/Node.h | 10 +- Libraries/LibWeb/Layout/TreeBuilder.cpp | 268 ++++++++++++++---- Libraries/LibWeb/Layout/TreeBuilder.h | 1 + Libraries/LibWeb/Painting/PaintableBox.cpp | 100 ++++++- Libraries/LibWeb/Painting/PaintableBox.h | 38 +-- Tests/LibWeb/Layout/expected/acid1.txt | 50 ++-- ...ock-element-inside-inline-element-ref.html | 22 ++ .../block-element-inside-inline-element.html | 44 +++ ...ent-offsetFoo-for-split-inline-element.txt | 4 + .../block-element-inside-inline-element.txt | 16 ++ ...nt-offsetFoo-for-split-inline-element.html | 12 + .../block-element-inside-inline-element.html | 23 ++ 18 files changed, 520 insertions(+), 145 deletions(-) create mode 100644 Tests/LibWeb/Ref/expected/block-element-inside-inline-element-ref.html create mode 100644 Tests/LibWeb/Ref/input/block-element-inside-inline-element.html create mode 100644 Tests/LibWeb/Text/expected/HTML/HTMLElement-offsetFoo-for-split-inline-element.txt create mode 100644 Tests/LibWeb/Text/expected/hit_testing/block-element-inside-inline-element.txt create mode 100644 Tests/LibWeb/Text/input/HTML/HTMLElement-offsetFoo-for-split-inline-element.html create mode 100644 Tests/LibWeb/Text/input/hit_testing/block-element-inside-inline-element.html diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index ead3512da24..4be61533db5 100644 --- a/Libraries/LibWeb/Dump.cpp +++ b/Libraries/LibWeb/Dump.cpp @@ -210,7 +210,6 @@ void dump_tree(StringBuilder& builder, Layout::Node const& layout_node, bool sho nonbox_color_on, identifier, color_off); - builder.append("\n"sv); } else { auto& box = as(layout_node); StringView color_on = is(box) ? svg_box_color_on : box_color_on; @@ -334,10 +333,14 @@ void dump_tree(StringBuilder& builder, Layout::Node const& layout_node, bool sho } } } - - builder.append("\n"sv); } + if (is(layout_node) + && static_cast(layout_node).continuation_of_node()) + builder.append(" continuation"sv); + + builder.append("\n"sv); + if (layout_node.dom_node() && is(*layout_node.dom_node())) { if (auto image_data = static_cast(*layout_node.dom_node()).current_request().image_data()) { if (is(*image_data)) { diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index 23e791548a3..d819ee27309 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2018-2024, Andreas Kling + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -463,7 +464,7 @@ int HTMLElement::offset_top() const if (!paintable_box()) return 0; - CSSPixels top_border_edge_of_element = paintable_box()->absolute_border_box_rect().y(); + CSSPixels top_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().y(); // 2. If the offsetParent of the element is null // return the y-coordinate of the top border edge of the first CSS layout box associated with the element, @@ -487,7 +488,7 @@ int HTMLElement::offset_top() const if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) { top_padding_edge_of_offset_parent = 0; } else { - top_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_padding_box_rect().y(); + top_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().y(); } return (top_border_edge_of_element - top_padding_edge_of_offset_parent).to_int(); } @@ -505,7 +506,7 @@ int HTMLElement::offset_left() const if (!paintable_box()) return 0; - CSSPixels left_border_edge_of_element = paintable_box()->absolute_border_box_rect().x(); + CSSPixels left_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().x(); // 2. If the offsetParent of the element is null // return the x-coordinate of the left border edge of the first CSS layout box associated with the element, @@ -529,7 +530,7 @@ int HTMLElement::offset_left() const if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) { left_padding_edge_of_offset_parent = 0; } else { - left_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_padding_box_rect().x(); + left_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().x(); } return (left_border_edge_of_element - left_padding_edge_of_offset_parent).to_int(); } @@ -540,13 +541,17 @@ int HTMLElement::offset_width() const // NOTE: Ensure that layout is up-to-date before looking at metrics. const_cast(document()).update_layout(); - // 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm. - if (!paintable_box()) + // 1. If the element does not have any associated box return zero and terminate this algorithm. + auto const* box = paintable_box(); + if (!box) return 0; - // 2. Return the width of the axis-aligned bounding box of the border boxes of all fragments generated by the element’s principal box, - // ignoring any transforms that apply to the element and its ancestors. - return paintable_box()->border_box_width().to_int(); + // 2. Return the unscaled width of the axis-aligned bounding box of the border boxes of all fragments generated by + // the element’s principal box, ignoring any transforms that apply to the element and its ancestors. + // + // If the element’s principal box is an inline-level box which was "split" by a block-level descendant, also + // include fragments generated by the block-level descendants, unless they are zero width or height. + return box->absolute_united_border_box_rect().width().to_int(); } // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight @@ -555,13 +560,17 @@ int HTMLElement::offset_height() const // NOTE: Ensure that layout is up-to-date before looking at metrics. const_cast(document()).update_layout(); - // 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm. - if (!paintable_box()) + // 1. If the element does not have any associated box return zero and terminate this algorithm. + auto const* box = paintable_box(); + if (!box) return 0; - // 2. Return the height of the axis-aligned bounding box of the border boxes of all fragments generated by the element’s principal box, - // ignoring any transforms that apply to the element and its ancestors. - return paintable_box()->border_box_height().to_int(); + // 2. Return the unscaled height of the axis-aligned bounding box of the border boxes of all fragments generated by + // the element’s principal box, ignoring any transforms that apply to the element and its ancestors. + // + // If the element’s principal box is an inline-level box which was "split" by a block-level descendant, also + // include fragments generated by the block-level descendants, unless they are zero width or height. + return box->absolute_united_border_box_rect().height().to_int(); } // https://html.spec.whatwg.org/multipage/links.html#cannot-navigate diff --git a/Libraries/LibWeb/Layout/Box.h b/Libraries/LibWeb/Layout/Box.h index a10fb48c42c..549b291fdd7 100644 --- a/Libraries/LibWeb/Layout/Box.h +++ b/Libraries/LibWeb/Layout/Box.h @@ -7,7 +7,6 @@ #pragma once #include -#include #include #include diff --git a/Libraries/LibWeb/Layout/InlineNode.cpp b/Libraries/LibWeb/Layout/InlineNode.cpp index 5eef94e691b..135be2b887f 100644 --- a/Libraries/LibWeb/Layout/InlineNode.cpp +++ b/Libraries/LibWeb/Layout/InlineNode.cpp @@ -8,7 +8,6 @@ #include #include -#include #include #include diff --git a/Libraries/LibWeb/Layout/LayoutState.cpp b/Libraries/LibWeb/Layout/LayoutState.cpp index 38fac64ef28..8e1f3d1431b 100644 --- a/Libraries/LibWeb/Layout/LayoutState.cpp +++ b/Libraries/LibWeb/Layout/LayoutState.cpp @@ -222,7 +222,12 @@ void LayoutState::commit(Box& root) root.document().for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) { node.clear_paintable(); if (node.layout_node() && is(node.layout_node())) { - inline_nodes.set(static_cast(node.layout_node())); + // Inline nodes might have a continuation chain; add all inline nodes that are part of it. + for (GC::Ptr inline_node = static_cast(node.layout_node()); + inline_node; inline_node = inline_node->continuation_of_node()) { + if (is(*inline_node)) + inline_nodes.set(static_cast(inline_node.ptr())); + } } return TraversalDecision::Continue; }); diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index a9c272fd201..0d6fee21978 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2018-2023, Andreas Kling * Copyright (c) 2021-2023, Sam Atkins + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -13,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -30,7 +30,6 @@ #include #include #include -#include namespace Web::Layout { @@ -328,7 +327,7 @@ static CSSPixels snap_a_length_as_a_border_width(double device_pixels_per_css_pi return length; } -void NodeWithStyle::apply_style(const CSS::ComputedProperties& computed_style) +void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style) { auto& computed_values = mutable_computed_values(); @@ -1015,6 +1014,9 @@ void NodeWithStyle::apply_style(const CSS::ComputedProperties& computed_style) computed_values.set_isolation(isolation.value()); propagate_style_to_anonymous_wrappers(); + + if (is(this)) + static_cast(*this).propagate_style_along_continuation(computed_style); } void NodeWithStyle::propagate_style_to_anonymous_wrappers() @@ -1278,4 +1280,18 @@ CSS::UserSelect Node::user_select_used_value() const return computed_value; } +void NodeWithStyleAndBoxModelMetrics::propagate_style_along_continuation(CSS::ComputedProperties const& computed_style) const +{ + for (auto continuation = continuation_of_node(); continuation; continuation = continuation->continuation_of_node()) { + if (!continuation->is_anonymous()) + continuation->apply_style(computed_style); + } +} + +void NodeWithStyleAndBoxModelMetrics::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_continuation_of_node); +} + } diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index fcb87cd09a9..579662fd4b7 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -230,7 +230,7 @@ public: CSS::ImmutableComputedValues const& computed_values() const { return static_cast(*m_computed_values); } CSS::MutableComputedValues& mutable_computed_values() { return static_cast(*m_computed_values); } - void apply_style(const CSS::ComputedProperties&); + void apply_style(CSS::ComputedProperties const&); Gfx::Font const& first_available_font() const; Vector const& background_layers() const { return computed_values().background_layers(); } @@ -266,6 +266,13 @@ public: BoxModelMetrics& box_model() { return m_box_model; } BoxModelMetrics const& box_model() const { return m_box_model; } + GC::Ptr continuation_of_node() const { return m_continuation_of_node; } + void set_continuation_of_node(Badge, GC::Ptr node) { m_continuation_of_node = node; } + + void propagate_style_along_continuation(CSS::ComputedProperties const&) const; + + virtual void visit_edges(Cell::Visitor& visitor) override; + protected: NodeWithStyleAndBoxModelMetrics(DOM::Document& document, DOM::Node* node, GC::Ref style) : NodeWithStyle(document, node, style) @@ -281,6 +288,7 @@ private: virtual bool is_node_with_style_and_box_model_metrics() const final { return true; } BoxModelMetrics m_box_model; + GC::Ptr m_continuation_of_node; }; template<> diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index b4a8d7d7c05..a7d10f7d908 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -2,15 +2,14 @@ * Copyright (c) 2018-2022, Andreas Kling * Copyright (c) 2022-2023, Sam Atkins * Copyright (c) 2022, MacDue + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ -#include #include #include #include -#include #include #include #include @@ -18,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -95,6 +93,10 @@ static Layout::Node& insertion_parent_for_inline_node(Layout::NodeWithStyle& lay static Layout::Node& insertion_parent_for_block_node(Layout::NodeWithStyle& layout_parent, Layout::Node& layout_node) { + // Inline is fine for in-flow block children; we'll maintain the (non-)inline invariant after insertion. + if (layout_parent.is_inline() && layout_parent.display().is_flow_inside() && !layout_node.is_out_of_flow()) + return layout_parent; + if (!has_inline_or_in_flow_block_children(layout_parent)) { // Parent block has no children, insert this block into parent. return layout_parent; @@ -121,26 +123,25 @@ static Layout::Node& insertion_parent_for_block_node(Layout::NodeWithStyle& layo return layout_parent; } - // Parent block has inline-level children (our siblings). - // First move these siblings into an anonymous wrapper block. - Vector> children; - { - GC::Ptr next; - for (GC::Ptr child = layout_parent.first_child(); child; child = next) { - next = child->next_sibling(); - // NOTE: We let out-of-flow children stay in the parent, to preserve tree structure. - if (child->is_out_of_flow()) - continue; - layout_parent.remove_child(*child); - children.append(*child); - } + // Parent block has inline-level children (our siblings); wrap these siblings into an anonymous wrapper block. + Vector> children; + for (GC::Ptr child = layout_parent.first_child(); child; child = child->next_sibling()) { + // NOTE: We let out-of-flow children stay in the parent, to preserve tree structure. + if (child->is_out_of_flow()) + continue; + children.append(*child); } - layout_parent.append_child(layout_parent.create_anonymous_wrapper()); + + auto wrapper = layout_parent.create_anonymous_wrapper(); + wrapper->set_children_are_inline(true); + for (auto child : children) { + layout_parent.remove_child(child); + wrapper->append_child(child); + } + layout_parent.set_children_are_inline(false); - for (auto& child : children) { - layout_parent.last_child()->append_child(*child); - } - layout_parent.last_child()->set_children_are_inline(true); + layout_parent.append_child(wrapper); + // Then it's safe to insert this block into parent. return layout_parent; } @@ -150,45 +151,35 @@ void TreeBuilder::insert_node_into_inline_or_block_ancestor(Layout::Node& node, if (node.display().is_contents()) return; - if (display.is_inline_outside()) { - // Inlines can be inserted into the nearest ancestor without "display: contents". - auto& nearest_ancestor_without_display_contents = [&]() -> Layout::NodeWithStyle& { - for (auto& ancestor : m_ancestor_stack.in_reverse()) { - if (!ancestor->display().is_contents()) - return ancestor; - } - VERIFY_NOT_REACHED(); - }(); - auto& insertion_point = insertion_parent_for_inline_node(nearest_ancestor_without_display_contents); - if (mode == AppendOrPrepend::Prepend) - insertion_point.prepend_child(node); - else - insertion_point.append_child(node); - insertion_point.set_children_are_inline(true); - } else { - // Non-inlines can't be inserted into an inline parent, so find the nearest non-inline ancestor. - auto& nearest_non_inline_ancestor = [&]() -> Layout::NodeWithStyle& { - for (auto& ancestor : m_ancestor_stack.in_reverse()) { - if (ancestor->display().is_contents()) - continue; - if (!ancestor->display().is_inline_outside()) - return ancestor; - if (!ancestor->display().is_flow_inside()) - return ancestor; - if (ancestor->dom_node() && is(*ancestor->dom_node())) - return ancestor; - } - VERIFY_NOT_REACHED(); - }(); - auto& insertion_point = insertion_parent_for_block_node(nearest_non_inline_ancestor, node); - if (mode == AppendOrPrepend::Prepend) - insertion_point.prepend_child(node); - else - insertion_point.append_child(node); + // Find the nearest ancestor that can host the node. + auto& nearest_insertion_ancestor = [&]() -> NodeWithStyle& { + for (auto& ancestor : m_ancestor_stack.in_reverse()) { + auto const& ancestor_display = ancestor->display(); + // Out-of-flow nodes cannot be hosted in inline flow nodes. + if (node.is_out_of_flow() && ancestor_display.is_inline_outside() && ancestor_display.is_flow_inside()) + continue; + + if (!ancestor_display.is_contents()) + return ancestor; + } + VERIFY_NOT_REACHED(); + }(); + + auto& insertion_point = display.is_inline_outside() ? insertion_parent_for_inline_node(nearest_insertion_ancestor) + : insertion_parent_for_block_node(nearest_insertion_ancestor, node); + + if (mode == AppendOrPrepend::Prepend) + insertion_point.prepend_child(node); + else + insertion_point.append_child(node); + + if (display.is_inline_outside()) { + // After inserting an inline-level box into a parent, mark the parent as having inline children. + insertion_point.set_children_are_inline(true); + } else if (node.is_in_flow()) { // After inserting an in-flow block-level box into a parent, mark the parent as having non-inline children. - if (!node.is_floating() && !node.is_absolutely_positioned()) - insertion_point.set_children_are_inline(false); + insertion_point.set_children_are_inline(false); } } @@ -261,6 +252,159 @@ void TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element, CSS::Se pseudo_element_node->mutable_computed_values().set_content(pseudo_element_content); } +// Block nodes inside inline nodes are allowed, but to maintain the invariant that either all layout children are +// inline or non-inline, we need to rearrange the tree a bit. All inline ancestors up to the node we've inserted are +// wrapped in an anonymous block, which is inserted into the nearest non-inline ancestor. We then recreate the inline +// ancestors in another anonymous block inserted after the node so we can continue adding children. +// +// Effectively, we try to turn this: +// +// InlineNode 1 +// TextNode 1 +// InlineNode N +// TextNode N +// BlockContainer (node) +// +// Into this: +// +// BlockContainer (anonymous "before") +// InlineNode 1 +// TextNode 1 +// InlineNode N +// TextNode N +// BlockContainer (anonymous "middle") continuation +// BlockContainer (node) +// BlockContainer (anonymous "after") +// InlineNode 1 continuation +// InlineNode N +// +// To be able to reconstruct their relation after restructuring, layout nodes keep track of their continuation. The +// top-most inline node of the "after" wrapper points to the "middle" wrapper, which points to the top-most inline node +// of the "before" wrapper. All other inline nodes in the "after" wrapper point to their counterparts in the "before" +// wrapper, to make it easier to create the right paintables since a DOM::Node only has a single Layout::Node. +// +// Appending then continues in the "after" tree. If a new block node is then inserted, we can reuse the "middle" wrapper +// if no inline siblings exist for node or its ancestors, and leave the existing "after" wrapper alone. Otherwise, we +// create new wrappers and extend the continuation chain. +// +// Inspired by: https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/ +void TreeBuilder::restructure_block_node_in_inline_parent(NodeWithStyleAndBoxModelMetrics& node) +{ + // Mark parent as inline again + auto& parent = *node.parent(); + VERIFY(!parent.children_are_inline()); + parent.set_children_are_inline(true); + + // Find nearest non-inline, content supporting ancestor that is not an anonymous block. + auto& nearest_block_ancestor = [&] -> NodeWithStyle& { + for (auto* ancestor = parent.parent(); ancestor; ancestor = ancestor->parent()) { + if (!ancestor->is_inline() && !ancestor->display().is_contents() && !ancestor->is_anonymous()) + return *ancestor; + } + VERIFY_NOT_REACHED(); + }(); + nearest_block_ancestor.set_children_are_inline(false); + + // Unwind the ancestor stack to find the topmost inline ancestor. + GC::Ptr topmost_inline_ancestor; + for (auto* ancestor = &parent; ancestor; ancestor = ancestor->parent()) { + if (ancestor == &nearest_block_ancestor) + break; + if (ancestor == m_ancestor_stack.last()) + m_ancestor_stack.take_last(); + if (ancestor->is_inline()) + topmost_inline_ancestor = static_cast(ancestor); + } + VERIFY(topmost_inline_ancestor); + + // We need to host the topmost inline ancestor and its previous siblings in an anonymous "before" wrapper. If an + // inline wrapper does not already exist, we create a new one and add it to the nearest block ancestor. + GC::Ptr before_wrapper; + if (auto last_child = nearest_block_ancestor.last_child(); last_child->is_anonymous() && last_child->children_are_inline()) { + before_wrapper = last_child; + } else { + before_wrapper = nearest_block_ancestor.create_anonymous_wrapper(); + before_wrapper->set_children_are_inline(true); + nearest_block_ancestor.append_child(*before_wrapper); + } + if (topmost_inline_ancestor->parent() != before_wrapper.ptr()) { + GC::Ptr inline_to_move = topmost_inline_ancestor; + while (inline_to_move) { + auto* next = inline_to_move->previous_sibling(); + inline_to_move->remove(); + before_wrapper->insert_before(*inline_to_move, before_wrapper->first_child()); + inline_to_move = next; + } + } + + // If we are part of an existing continuation and all inclusive ancestors have no previous siblings, we can reuse + // the existing middle wrapper. Otherwiser, we create a new middle wrapper to contain the block node and add it to + // the nearest block ancestor. + bool needs_new_continuation = true; + GC::Ptr middle_wrapper; + if (topmost_inline_ancestor->continuation_of_node()) { + needs_new_continuation = false; + for (GC::Ptr ancestor = node; ancestor != topmost_inline_ancestor; ancestor = ancestor->parent()) { + if (ancestor->previous_sibling()) { + needs_new_continuation = true; + break; + } + } + if (!needs_new_continuation) + middle_wrapper = topmost_inline_ancestor->continuation_of_node(); + } + if (!middle_wrapper) { + middle_wrapper = static_cast(*nearest_block_ancestor.create_anonymous_wrapper()); + nearest_block_ancestor.append_child(*middle_wrapper); + middle_wrapper->set_continuation_of_node({}, topmost_inline_ancestor); + } + + // Move the block node to the middle wrapper. + node.remove(); + middle_wrapper->append_child(node); + + // If we need a new continuation, recreate inline ancestors in another anonymous block so we can continue adding new + // nodes. We don't need to do this if we are within an existing continuation and there were no previous siblings in + // any inclusive ancestor of node in the after wrapper. + if (needs_new_continuation) { + auto after_wrapper = nearest_block_ancestor.create_anonymous_wrapper(); + GC::Ptr current_parent = after_wrapper; + for (GC::Ptr inline_node = topmost_inline_ancestor; + inline_node && is(inline_node->dom_node()); inline_node = inline_node->last_child()) { + auto& element = static_cast(*inline_node->dom_node()); + + auto style = element.computed_properties(); + auto& new_inline_node = static_cast(*element.create_layout_node(*style)); + if (inline_node == topmost_inline_ancestor) { + // The topmost inline ancestor points to the middle wrapper, which in turns points to the original node. + new_inline_node.set_continuation_of_node({}, middle_wrapper); + topmost_inline_ancestor = new_inline_node; + } else { + // We need all other inline nodes to point to their original node so we can walk the continuation chain + // in LayoutState and create the right paintables. + new_inline_node.set_continuation_of_node({}, static_cast(*inline_node)); + } + + current_parent->append_child(new_inline_node); + current_parent = new_inline_node; + + // Stop recreating nodes when we've reached node's parent + if (inline_node == &parent) + break; + } + + after_wrapper->set_children_are_inline(true); + nearest_block_ancestor.append_child(after_wrapper); + } + + // Rewind the ancestor stack + for (GC::Ptr inline_node = topmost_inline_ancestor; inline_node; inline_node = inline_node->last_child()) { + if (!is(*inline_node)) + break; + m_ancestor_stack.append(static_cast(*inline_node)); + } +} + static bool is_ignorable_whitespace(Layout::Node const& node) { if (node.is_text_node() && static_cast(node).text_for_rendering().bytes_as_string_view().is_whitespace()) @@ -591,6 +735,14 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::After, AppendOrPrepend::Append); pop_parent(); } + + // If we completely finished inserting a block level element into an inline parent, we need to fix up the tree so + // that we can maintain the invariant that all children are either inline or non-inline. We can't do this earlier, + // because the restructuring adds new children after this node that become part of the ancestor stack. + auto* layout_parent = layout_node->parent(); + if (layout_parent && layout_parent->display().is_inline_outside() && !display.is_contents() + && !display.is_inline_outside() && layout_parent->display().is_flow_inside() && !layout_node->is_out_of_flow()) + restructure_block_node_in_inline_parent(static_cast(*layout_node)); } GC::Ptr TreeBuilder::build(DOM::Node& dom_node) diff --git a/Libraries/LibWeb/Layout/TreeBuilder.h b/Libraries/LibWeb/Layout/TreeBuilder.h index 14f36e40d12..0b2b9c2ff9a 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.h +++ b/Libraries/LibWeb/Layout/TreeBuilder.h @@ -58,6 +58,7 @@ private: }; void insert_node_into_inline_or_block_ancestor(Layout::Node&, CSS::Display, AppendOrPrepend); void create_pseudo_element_if_needed(DOM::Element&, CSS::Selector::PseudoElement::Type, AppendOrPrepend); + void restructure_block_node_in_inline_parent(NodeWithStyleAndBoxModelMetrics&); GC::Ptr m_layout_root; Vector> m_ancestor_stack; diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index f8b902a0506..aeefb7ac350 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2022-2023, Andreas Kling * Copyright (c) 2022-2023, Sam Atkins * Copyright (c) 2024, Aliaksandr Kalenik + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -17,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -214,6 +214,34 @@ CSSPixelRect PaintableBox::compute_absolute_paint_rect() const return rect; } +CSSPixelRect PaintableBox::absolute_padding_box_rect() const +{ + auto absolute_rect = this->absolute_rect(); + CSSPixelRect rect; + rect.set_x(absolute_rect.x() - box_model().padding.left); + rect.set_width(content_width() + box_model().padding.left + box_model().padding.right); + rect.set_y(absolute_rect.y() - box_model().padding.top); + rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom); + return rect; +} + +CSSPixelRect PaintableBox::absolute_border_box_rect() const +{ + auto padded_rect = this->absolute_padding_box_rect(); + CSSPixelRect rect; + auto use_collapsing_borders_model = override_borders_data().has_value(); + // Implement the collapsing border model https://www.w3.org/TR/CSS22/tables.html#collapsing-borders. + auto border_top = use_collapsing_borders_model ? round(box_model().border.top / 2) : box_model().border.top; + auto border_bottom = use_collapsing_borders_model ? round(box_model().border.bottom / 2) : box_model().border.bottom; + auto border_left = use_collapsing_borders_model ? round(box_model().border.left / 2) : box_model().border.left; + auto border_right = use_collapsing_borders_model ? round(box_model().border.right / 2) : box_model().border.right; + rect.set_x(padded_rect.x() - border_left); + rect.set_width(padded_rect.width() + border_left + border_right); + rect.set_y(padded_rect.y() - border_top); + rect.set_height(padded_rect.height() + border_top + border_bottom); + return rect; +} + CSSPixelRect PaintableBox::absolute_paint_rect() const { if (!m_absolute_paint_rect.has_value()) @@ -221,6 +249,51 @@ CSSPixelRect PaintableBox::absolute_paint_rect() const return *m_absolute_paint_rect; } +template +static CSSPixelRect united_rect_for_continuation_chain(PaintableBox const& start, Callable get_rect) +{ + // Combine the absolute rects of all paintable boxes of all nodes in the continuation chain. Without this, we + // calculate the wrong rect for inline nodes that were split because of block elements. + Optional result; + + // FIXME: instead of walking the continuation chain in the layout tree, also keep track of this chain in the + // painting tree so we can skip visiting the layout nodes altogether. + for (auto const* node = &start.layout_node_with_style_and_box_metrics(); node; node = node->continuation_of_node()) { + for (auto const& paintable : node->paintables()) { + if (!is(paintable)) + continue; + auto const& paintable_box = static_cast(paintable); + auto paintable_border_box_rect = get_rect(paintable_box); + if (!result.has_value()) + result = paintable_border_box_rect; + else if (!paintable_border_box_rect.is_empty()) + result->unite(paintable_border_box_rect); + } + } + return result.value_or({}); +} + +CSSPixelRect PaintableBox::absolute_united_border_box_rect() const +{ + return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) { + return paintable_box.absolute_border_box_rect(); + }); +} + +CSSPixelRect PaintableBox::absolute_united_content_rect() const +{ + return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) { + return paintable_box.absolute_rect(); + }); +} + +CSSPixelRect PaintableBox::absolute_united_padding_box_rect() const +{ + return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) { + return paintable_box.absolute_padding_box_rect(); + }); +} + Optional PaintableBox::get_clip_rect() const { auto clip = computed_values().clip(); @@ -396,17 +469,18 @@ void PaintableBox::paint(PaintContext& context, PaintPhase phase) const } if (phase == PaintPhase::Overlay && layout_node().document().inspected_layout_node() == &layout_node_with_style_and_box_metrics()) { - auto content_rect = absolute_rect(); - - auto margin_box = box_model().margin_box(); - CSSPixelRect margin_rect; - margin_rect.set_x(absolute_x() - margin_box.left); - margin_rect.set_width(content_width() + margin_box.left + margin_box.right); - margin_rect.set_y(absolute_y() - margin_box.top); - margin_rect.set_height(content_height() + margin_box.top + margin_box.bottom); - - auto border_rect = absolute_border_box_rect(); - auto padding_rect = absolute_padding_box_rect(); + auto content_rect = absolute_united_content_rect(); + auto margin_rect = united_rect_for_continuation_chain(*this, [](PaintableBox const& box) { + auto margin_box = box.box_model().margin_box(); + return CSSPixelRect { + box.absolute_x() - margin_box.left, + box.absolute_y() - margin_box.top, + box.content_width() + margin_box.left + margin_box.right, + box.content_height() + margin_box.top + margin_box.bottom, + }; + }); + auto border_rect = absolute_united_border_box_rect(); + auto padding_rect = absolute_united_padding_box_rect(); auto paint_inspector_rect = [&](CSSPixelRect const& rect, Color color) { auto device_rect = context.enclosing_device_rect(rect).to_type(); @@ -895,7 +969,7 @@ TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType typ if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y())) return TraversalDecision::Continue; - if (!visible_for_hit_testing()) + if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset)) return TraversalDecision::Continue; return callback(HitTestResult { const_cast(*this) }); diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index 40e35e64990..6f57db8530a 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -57,8 +57,6 @@ public: CSSPixelPoint scroll_offset {}; }; - CSSPixelRect absolute_rect() const; - // Offset from the top left of the containing block's content edge. [[nodiscard]] CSSPixelPoint offset() const; @@ -78,36 +76,16 @@ public: CSSPixels content_width() const { return m_content_size.width(); } CSSPixels content_height() const { return m_content_size.height(); } - CSSPixelRect absolute_padding_box_rect() const - { - auto absolute_rect = this->absolute_rect(); - CSSPixelRect rect; - rect.set_x(absolute_rect.x() - box_model().padding.left); - rect.set_width(content_width() + box_model().padding.left + box_model().padding.right); - rect.set_y(absolute_rect.y() - box_model().padding.top); - rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom); - return rect; - } - - CSSPixelRect absolute_border_box_rect() const - { - auto padded_rect = this->absolute_padding_box_rect(); - CSSPixelRect rect; - auto use_collapsing_borders_model = override_borders_data().has_value(); - // Implement the collapsing border model https://www.w3.org/TR/CSS22/tables.html#collapsing-borders. - auto border_top = use_collapsing_borders_model ? round(box_model().border.top / 2) : box_model().border.top; - auto border_bottom = use_collapsing_borders_model ? round(box_model().border.bottom / 2) : box_model().border.bottom; - auto border_left = use_collapsing_borders_model ? round(box_model().border.left / 2) : box_model().border.left; - auto border_right = use_collapsing_borders_model ? round(box_model().border.right / 2) : box_model().border.right; - rect.set_x(padded_rect.x() - border_left); - rect.set_width(padded_rect.width() + border_left + border_right); - rect.set_y(padded_rect.y() - border_top); - rect.set_height(padded_rect.height() + border_top + border_bottom); - return rect; - } - + CSSPixelRect absolute_rect() const; + CSSPixelRect absolute_padding_box_rect() const; + CSSPixelRect absolute_border_box_rect() const; CSSPixelRect absolute_paint_rect() const; + // These united versions of the above rects take continuation into account. + CSSPixelRect absolute_united_border_box_rect() const; + CSSPixelRect absolute_united_content_rect() const; + CSSPixelRect absolute_united_padding_box_rect() const; + CSSPixels border_box_width() const { auto border_box = box_model().border_box(); diff --git a/Tests/LibWeb/Layout/expected/acid1.txt b/Tests/LibWeb/Layout/expected/acid1.txt index 7ea812158fc..3e8ca81c3d3 100644 --- a/Tests/LibWeb/Layout/expected/acid1.txt +++ b/Tests/LibWeb/Layout/expected/acid1.txt @@ -33,23 +33,28 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline TextNode <#text> InlineNode TextNode <#text> + BlockContainer <(anonymous)> at (235,65) content-size 139.96875x19 children: not-inline continuation + BlockContainer

at (235,65) content-size 139.96875x19 children: inline + frag 0 from TextNode start: 1, length: 5, rect: [235,65 27.5x19] baseline: 12.5 + "bang " + frag 1 from RadioButton start: 0, length: 0, rect: [262.5,65 12x12] baseline: 12 TextNode <#text> + RadioButton at (262.5,65) content-size 12x12 inline-block children: not-inline + TextNode <#text> + BlockContainer <(anonymous)> at (235,84) content-size 139.96875x0 children: inline + InlineNode continuation + TextNode <#text> + BlockContainer <(anonymous)> at (235,84) content-size 139.96875x19 children: not-inline continuation + BlockContainer

at (235,84) content-size 139.96875x19 children: inline + frag 0 from TextNode start: 1, length: 8, rect: [235,84 45.171875x19] baseline: 12.5 + "whimper " + frag 1 from RadioButton start: 0, length: 0, rect: [280.171875,84 12x12] baseline: 12 + TextNode <#text> + RadioButton at (280.171875,84) content-size 12x12 inline-block children: not-inline TextNode <#text> - BlockContainer

at (235,65) content-size 139.96875x19 children: inline - frag 0 from TextNode start: 1, length: 5, rect: [235,65 27.5x19] baseline: 12.5 - "bang " - frag 1 from RadioButton start: 0, length: 0, rect: [262.5,65 12x12] baseline: 12 - TextNode <#text> - RadioButton at (262.5,65) content-size 12x12 inline-block children: not-inline - TextNode <#text> - BlockContainer

at (235,84) content-size 139.96875x19 children: inline - frag 0 from TextNode start: 1, length: 8, rect: [235,84 45.171875x19] baseline: 12.5 - "whimper " - frag 1 from RadioButton start: 0, length: 0, rect: [280.171875,84 12x12] baseline: 12 - TextNode <#text> - RadioButton at (280.171875,84) content-size 12x12 inline-block children: not-inline - TextNode <#text> BlockContainer <(anonymous)> at (235,103) content-size 139.96875x0 children: inline + InlineNode continuation + TextNode <#text> TextNode <#text> TextNode <#text> BlockContainer

  • at (409.96875,60) content-size 50x90 floating [BFC] children: inline @@ -136,13 +141,18 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600] TextPaintable (TextNode<#text>) PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x0] PaintableWithLines (InlineNode) - PaintableWithLines (BlockContainer

    ) [235,65 139.96875x19] - TextPaintable (TextNode<#text>) - RadioButtonPaintable (RadioButton) [262.5,65 12x12] - PaintableWithLines (BlockContainer

    ) [235,84 139.96875x19] - TextPaintable (TextNode<#text>) - RadioButtonPaintable (RadioButton) [280.171875,84 12x12] + PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x19] + PaintableWithLines (BlockContainer

    ) [235,65 139.96875x19] + TextPaintable (TextNode<#text>) + RadioButtonPaintable (RadioButton) [262.5,65 12x12] + PaintableWithLines (BlockContainer(anonymous)) [235,84 139.96875x0] + PaintableWithLines (InlineNode) + PaintableWithLines (BlockContainer(anonymous)) [235,84 139.96875x19] + PaintableWithLines (BlockContainer

    ) [235,84 139.96875x19] + TextPaintable (TextNode<#text>) + RadioButtonPaintable (RadioButton) [280.171875,84 12x12] PaintableWithLines (BlockContainer(anonymous)) [235,103 139.96875x0] + PaintableWithLines (InlineNode) PaintableWithLines (BlockContainer

  • ) [394.96875,45 80x120] TextPaintable (TextNode<#text>) PaintableWithLines (BlockContainer
  • #baz) [135,175 120x120] diff --git a/Tests/LibWeb/Ref/expected/block-element-inside-inline-element-ref.html b/Tests/LibWeb/Ref/expected/block-element-inside-inline-element-ref.html new file mode 100644 index 00000000000..21f3101cd99 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/block-element-inside-inline-element-ref.html @@ -0,0 +1,22 @@ + + + + foo
    bar
    +
    +
    foo
    bar +
    + foo
    bar
    baz +
    + foobar
    baz
    loremipsum +
    + foo
    bar
    baz
    lorem +
    + foo
    bar
    baz
    lorem
    ipsum +
    +
    foo
    bar
    +
    + foo
    bar
    baz +
    + foo
    bar
    + + diff --git a/Tests/LibWeb/Ref/input/block-element-inside-inline-element.html b/Tests/LibWeb/Ref/input/block-element-inside-inline-element.html new file mode 100644 index 00000000000..694ab595d9e --- /dev/null +++ b/Tests/LibWeb/Ref/input/block-element-inside-inline-element.html @@ -0,0 +1,44 @@ + + + + + + + + foo
    bar
    + +
    +
    foo
    bar
    + +
    + foo
    bar
    baz
    + +
    + foobar
    baz
    lorem
    ipsum
    + +
    + foo
    bar
    baz
    lorem
    + +
    + foo
    bar
    baz
    lorem
    ipsum
    + +
    +
    foo
    bar
    + +
    +
    + + +
    + foo
    bar
    + + + diff --git a/Tests/LibWeb/Text/expected/HTML/HTMLElement-offsetFoo-for-split-inline-element.txt b/Tests/LibWeb/Text/expected/HTML/HTMLElement-offsetFoo-for-split-inline-element.txt new file mode 100644 index 00000000000..8ead0692973 --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/HTMLElement-offsetFoo-for-split-inline-element.txt @@ -0,0 +1,4 @@ +b.offsetTop: 8 +b.offsetLeft: 8 +b.offsetWidth: 784 +b.offsetHeight: 51 diff --git a/Tests/LibWeb/Text/expected/hit_testing/block-element-inside-inline-element.txt b/Tests/LibWeb/Text/expected/hit_testing/block-element-inside-inline-element.txt new file mode 100644 index 00000000000..7768d621d03 --- /dev/null +++ b/Tests/LibWeb/Text/expected/hit_testing/block-element-inside-inline-element.txt @@ -0,0 +1,16 @@ +<#text > +index: 1 + +--- +<#text > +index: 1 + +--- +<#text > +index: 1 +
    +--- +<#text > +index: 3 + +--- diff --git a/Tests/LibWeb/Text/input/HTML/HTMLElement-offsetFoo-for-split-inline-element.html b/Tests/LibWeb/Text/input/HTML/HTMLElement-offsetFoo-for-split-inline-element.html new file mode 100644 index 00000000000..8612950e432 --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/HTMLElement-offsetFoo-for-split-inline-element.html @@ -0,0 +1,12 @@ +foo
    bar
    baz
    + + diff --git a/Tests/LibWeb/Text/input/hit_testing/block-element-inside-inline-element.html b/Tests/LibWeb/Text/input/hit_testing/block-element-inside-inline-element.html new file mode 100644 index 00000000000..8ea1c293d21 --- /dev/null +++ b/Tests/LibWeb/Text/input/hit_testing/block-element-inside-inline-element.html @@ -0,0 +1,23 @@ + + +foobar
    baz
    lorem
    + +