diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index ead3512da2..4be61533db 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 23e791548a..d819ee2730 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 a10fb48c42..549b291fdd 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 5eef94e691..135be2b887 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 38fac64ef2..8e1f3d1431 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 a9c272fd20..0d6fee2197 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 fcb87cd09a..579662fd4b 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 b4a8d7d7c0..a7d10f7d90 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 14f36e40d1..0b2b9c2ff9 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 f8b902a050..aeefb7ac35 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 40e35e6499..6f57db8530 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 7ea812158f..3e8ca81c3d 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 0000000000..21f3101cd9 --- /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 0000000000..694ab595d9 --- /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 0000000000..8ead069297 --- /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 0000000000..7768d621d0 --- /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 0000000000..8612950e43 --- /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 0000000000..8ea1c293d2 --- /dev/null +++ b/Tests/LibWeb/Text/input/hit_testing/block-element-inside-inline-element.html @@ -0,0 +1,23 @@ + + +foobar
    baz
    lorem
    + +