mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 04:09:13 +00:00
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 <p> elements out of the inline <form> they were in. Visually, the before and after situations for acid1.html are identical.
This commit is contained in:
parent
7eb4f3da37
commit
336684bc5c
Notes:
github-actions[bot]
2025-01-23 08:34:24 +00:00
Author: https://github.com/gmta
Commit: 336684bc5c
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3276
Reviewed-by: https://github.com/kalenikaliaksandr ✅
18 changed files with 520 additions and 145 deletions
|
@ -210,7 +210,6 @@ void dump_tree(StringBuilder& builder, Layout::Node const& layout_node, bool sho
|
||||||
nonbox_color_on,
|
nonbox_color_on,
|
||||||
identifier,
|
identifier,
|
||||||
color_off);
|
color_off);
|
||||||
builder.append("\n"sv);
|
|
||||||
} else {
|
} else {
|
||||||
auto& box = as<Layout::Box>(layout_node);
|
auto& box = as<Layout::Box>(layout_node);
|
||||||
StringView color_on = is<Layout::SVGBox>(box) ? svg_box_color_on : box_color_on;
|
StringView color_on = is<Layout::SVGBox>(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::NodeWithStyleAndBoxModelMetrics>(layout_node)
|
||||||
|
&& static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(layout_node).continuation_of_node())
|
||||||
|
builder.append(" continuation"sv);
|
||||||
|
|
||||||
|
builder.append("\n"sv);
|
||||||
|
|
||||||
if (layout_node.dom_node() && is<HTML::HTMLImageElement>(*layout_node.dom_node())) {
|
if (layout_node.dom_node() && is<HTML::HTMLImageElement>(*layout_node.dom_node())) {
|
||||||
if (auto image_data = static_cast<HTML::HTMLImageElement const&>(*layout_node.dom_node()).current_request().image_data()) {
|
if (auto image_data = static_cast<HTML::HTMLImageElement const&>(*layout_node.dom_node()).current_request().image_data()) {
|
||||||
if (is<SVG::SVGDecodedImageData>(*image_data)) {
|
if (is<SVG::SVGDecodedImageData>(*image_data)) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
|
* Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
|
||||||
|
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -463,7 +464,7 @@ int HTMLElement::offset_top() const
|
||||||
if (!paintable_box())
|
if (!paintable_box())
|
||||||
return 0;
|
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
|
// 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,
|
// 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()) {
|
if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) {
|
||||||
top_padding_edge_of_offset_parent = 0;
|
top_padding_edge_of_offset_parent = 0;
|
||||||
} else {
|
} 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();
|
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())
|
if (!paintable_box())
|
||||||
return 0;
|
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
|
// 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,
|
// 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()) {
|
if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) {
|
||||||
left_padding_edge_of_offset_parent = 0;
|
left_padding_edge_of_offset_parent = 0;
|
||||||
} else {
|
} 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();
|
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.
|
// NOTE: Ensure that layout is up-to-date before looking at metrics.
|
||||||
const_cast<DOM::Document&>(document()).update_layout();
|
const_cast<DOM::Document&>(document()).update_layout();
|
||||||
|
|
||||||
// 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm.
|
// 1. If the element does not have any associated box return zero and terminate this algorithm.
|
||||||
if (!paintable_box())
|
auto const* box = paintable_box();
|
||||||
|
if (!box)
|
||||||
return 0;
|
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,
|
// 2. Return the unscaled width of the axis-aligned bounding box of the border boxes of all fragments generated by
|
||||||
// ignoring any transforms that apply to the element and its ancestors.
|
// the element’s principal box, ignoring any transforms that apply to the element and its ancestors.
|
||||||
return paintable_box()->border_box_width().to_int();
|
//
|
||||||
|
// 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
|
// 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.
|
// NOTE: Ensure that layout is up-to-date before looking at metrics.
|
||||||
const_cast<DOM::Document&>(document()).update_layout();
|
const_cast<DOM::Document&>(document()).update_layout();
|
||||||
|
|
||||||
// 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm.
|
// 1. If the element does not have any associated box return zero and terminate this algorithm.
|
||||||
if (!paintable_box())
|
auto const* box = paintable_box();
|
||||||
|
if (!box)
|
||||||
return 0;
|
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,
|
// 2. Return the unscaled height of the axis-aligned bounding box of the border boxes of all fragments generated by
|
||||||
// ignoring any transforms that apply to the element and its ancestors.
|
// the element’s principal box, ignoring any transforms that apply to the element and its ancestors.
|
||||||
return paintable_box()->border_box_height().to_int();
|
//
|
||||||
|
// 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
|
// https://html.spec.whatwg.org/multipage/links.html#cannot-navigate
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <AK/OwnPtr.h>
|
#include <AK/OwnPtr.h>
|
||||||
#include <LibGfx/Rect.h>
|
|
||||||
#include <LibJS/Heap/Cell.h>
|
#include <LibJS/Heap/Cell.h>
|
||||||
#include <LibWeb/Layout/Node.h>
|
#include <LibWeb/Layout/Node.h>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
#include <LibWeb/DOM/Document.h>
|
#include <LibWeb/DOM/Document.h>
|
||||||
#include <LibWeb/DOM/Element.h>
|
#include <LibWeb/DOM/Element.h>
|
||||||
#include <LibWeb/Layout/BlockContainer.h>
|
|
||||||
#include <LibWeb/Layout/InlineFormattingContext.h>
|
#include <LibWeb/Layout/InlineFormattingContext.h>
|
||||||
#include <LibWeb/Layout/InlineNode.h>
|
#include <LibWeb/Layout/InlineNode.h>
|
||||||
|
|
||||||
|
|
|
@ -222,7 +222,12 @@ void LayoutState::commit(Box& root)
|
||||||
root.document().for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) {
|
root.document().for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) {
|
||||||
node.clear_paintable();
|
node.clear_paintable();
|
||||||
if (node.layout_node() && is<InlineNode>(node.layout_node())) {
|
if (node.layout_node() && is<InlineNode>(node.layout_node())) {
|
||||||
inline_nodes.set(static_cast<InlineNode*>(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<NodeWithStyleAndBoxModelMetrics*>(node.layout_node());
|
||||||
|
inline_node; inline_node = inline_node->continuation_of_node()) {
|
||||||
|
if (is<InlineNode>(*inline_node))
|
||||||
|
inline_nodes.set(static_cast<InlineNode*>(inline_node.ptr()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return TraversalDecision::Continue;
|
return TraversalDecision::Continue;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
|
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
|
||||||
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
|
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
|
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -13,7 +14,6 @@
|
||||||
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
|
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/MathDepthStyleValue.h>
|
|
||||||
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
|
||||||
|
@ -30,7 +30,6 @@
|
||||||
#include <LibWeb/Layout/TableWrapper.h>
|
#include <LibWeb/Layout/TableWrapper.h>
|
||||||
#include <LibWeb/Layout/TextNode.h>
|
#include <LibWeb/Layout/TextNode.h>
|
||||||
#include <LibWeb/Layout/Viewport.h>
|
#include <LibWeb/Layout/Viewport.h>
|
||||||
#include <LibWeb/Platform/FontPlugin.h>
|
|
||||||
|
|
||||||
namespace Web::Layout {
|
namespace Web::Layout {
|
||||||
|
|
||||||
|
@ -328,7 +327,7 @@ static CSSPixels snap_a_length_as_a_border_width(double device_pixels_per_css_pi
|
||||||
return length;
|
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();
|
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());
|
computed_values.set_isolation(isolation.value());
|
||||||
|
|
||||||
propagate_style_to_anonymous_wrappers();
|
propagate_style_to_anonymous_wrappers();
|
||||||
|
|
||||||
|
if (is<NodeWithStyleAndBoxModelMetrics>(this))
|
||||||
|
static_cast<NodeWithStyleAndBoxModelMetrics&>(*this).propagate_style_along_continuation(computed_style);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NodeWithStyle::propagate_style_to_anonymous_wrappers()
|
void NodeWithStyle::propagate_style_to_anonymous_wrappers()
|
||||||
|
@ -1278,4 +1280,18 @@ CSS::UserSelect Node::user_select_used_value() const
|
||||||
return computed_value;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,7 +230,7 @@ public:
|
||||||
CSS::ImmutableComputedValues const& computed_values() const { return static_cast<CSS::ImmutableComputedValues const&>(*m_computed_values); }
|
CSS::ImmutableComputedValues const& computed_values() const { return static_cast<CSS::ImmutableComputedValues const&>(*m_computed_values); }
|
||||||
CSS::MutableComputedValues& mutable_computed_values() { return static_cast<CSS::MutableComputedValues&>(*m_computed_values); }
|
CSS::MutableComputedValues& mutable_computed_values() { return static_cast<CSS::MutableComputedValues&>(*m_computed_values); }
|
||||||
|
|
||||||
void apply_style(const CSS::ComputedProperties&);
|
void apply_style(CSS::ComputedProperties const&);
|
||||||
|
|
||||||
Gfx::Font const& first_available_font() const;
|
Gfx::Font const& first_available_font() const;
|
||||||
Vector<CSS::BackgroundLayerData> const& background_layers() const { return computed_values().background_layers(); }
|
Vector<CSS::BackgroundLayerData> const& background_layers() const { return computed_values().background_layers(); }
|
||||||
|
@ -266,6 +266,13 @@ public:
|
||||||
BoxModelMetrics& box_model() { return m_box_model; }
|
BoxModelMetrics& box_model() { return m_box_model; }
|
||||||
BoxModelMetrics const& box_model() const { return m_box_model; }
|
BoxModelMetrics const& box_model() const { return m_box_model; }
|
||||||
|
|
||||||
|
GC::Ptr<NodeWithStyleAndBoxModelMetrics> continuation_of_node() const { return m_continuation_of_node; }
|
||||||
|
void set_continuation_of_node(Badge<TreeBuilder>, GC::Ptr<NodeWithStyleAndBoxModelMetrics> node) { m_continuation_of_node = node; }
|
||||||
|
|
||||||
|
void propagate_style_along_continuation(CSS::ComputedProperties const&) const;
|
||||||
|
|
||||||
|
virtual void visit_edges(Cell::Visitor& visitor) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
NodeWithStyleAndBoxModelMetrics(DOM::Document& document, DOM::Node* node, GC::Ref<CSS::ComputedProperties> style)
|
NodeWithStyleAndBoxModelMetrics(DOM::Document& document, DOM::Node* node, GC::Ref<CSS::ComputedProperties> style)
|
||||||
: NodeWithStyle(document, node, style)
|
: NodeWithStyle(document, node, style)
|
||||||
|
@ -281,6 +288,7 @@ private:
|
||||||
virtual bool is_node_with_style_and_box_model_metrics() const final { return true; }
|
virtual bool is_node_with_style_and_box_model_metrics() const final { return true; }
|
||||||
|
|
||||||
BoxModelMetrics m_box_model;
|
BoxModelMetrics m_box_model;
|
||||||
|
GC::Ptr<NodeWithStyleAndBoxModelMetrics> m_continuation_of_node;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
|
|
|
@ -2,15 +2,14 @@
|
||||||
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
|
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
|
||||||
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
|
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
||||||
|
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <AK/Error.h>
|
|
||||||
#include <AK/Optional.h>
|
#include <AK/Optional.h>
|
||||||
#include <AK/TemporaryChange.h>
|
#include <AK/TemporaryChange.h>
|
||||||
#include <LibWeb/CSS/StyleComputer.h>
|
#include <LibWeb/CSS/StyleComputer.h>
|
||||||
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
|
|
||||||
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
|
||||||
#include <LibWeb/DOM/Document.h>
|
#include <LibWeb/DOM/Document.h>
|
||||||
|
@ -18,7 +17,6 @@
|
||||||
#include <LibWeb/DOM/ParentNode.h>
|
#include <LibWeb/DOM/ParentNode.h>
|
||||||
#include <LibWeb/DOM/ShadowRoot.h>
|
#include <LibWeb/DOM/ShadowRoot.h>
|
||||||
#include <LibWeb/Dump.h>
|
#include <LibWeb/Dump.h>
|
||||||
#include <LibWeb/HTML/HTMLButtonElement.h>
|
|
||||||
#include <LibWeb/HTML/HTMLInputElement.h>
|
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||||
#include <LibWeb/HTML/HTMLLIElement.h>
|
#include <LibWeb/HTML/HTMLLIElement.h>
|
||||||
#include <LibWeb/HTML/HTMLOListElement.h>
|
#include <LibWeb/HTML/HTMLOListElement.h>
|
||||||
|
@ -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)
|
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)) {
|
if (!has_inline_or_in_flow_block_children(layout_parent)) {
|
||||||
// Parent block has no children, insert this block into parent.
|
// Parent block has no children, insert this block into parent.
|
||||||
return layout_parent;
|
return layout_parent;
|
||||||
|
@ -121,26 +123,25 @@ static Layout::Node& insertion_parent_for_block_node(Layout::NodeWithStyle& layo
|
||||||
return layout_parent;
|
return layout_parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent block has inline-level children (our siblings).
|
// Parent block has inline-level children (our siblings); wrap these siblings into an anonymous wrapper block.
|
||||||
// First move these siblings into an anonymous wrapper block.
|
Vector<GC::Ref<Node>> children;
|
||||||
Vector<GC::Root<Layout::Node>> children;
|
for (GC::Ptr<Node> 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.
|
||||||
GC::Ptr<Layout::Node> next;
|
if (child->is_out_of_flow())
|
||||||
for (GC::Ptr<Layout::Node> child = layout_parent.first_child(); child; child = next) {
|
continue;
|
||||||
next = child->next_sibling();
|
children.append(*child);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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);
|
layout_parent.set_children_are_inline(false);
|
||||||
for (auto& child : children) {
|
layout_parent.append_child(wrapper);
|
||||||
layout_parent.last_child()->append_child(*child);
|
|
||||||
}
|
|
||||||
layout_parent.last_child()->set_children_are_inline(true);
|
|
||||||
// Then it's safe to insert this block into parent.
|
// Then it's safe to insert this block into parent.
|
||||||
return layout_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())
|
if (node.display().is_contents())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (display.is_inline_outside()) {
|
// Find the nearest ancestor that can host the node.
|
||||||
// Inlines can be inserted into the nearest ancestor without "display: contents".
|
auto& nearest_insertion_ancestor = [&]() -> NodeWithStyle& {
|
||||||
auto& nearest_ancestor_without_display_contents = [&]() -> Layout::NodeWithStyle& {
|
for (auto& ancestor : m_ancestor_stack.in_reverse()) {
|
||||||
for (auto& ancestor : m_ancestor_stack.in_reverse()) {
|
auto const& ancestor_display = ancestor->display();
|
||||||
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<SVG::SVGForeignObjectElement>(*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);
|
|
||||||
|
|
||||||
|
// 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.
|
// 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);
|
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<NodeWithStyleAndBoxModelMetrics> 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<NodeWithStyleAndBoxModelMetrics*>(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<Node> 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<Node> 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<NodeWithStyleAndBoxModelMetrics> middle_wrapper;
|
||||||
|
if (topmost_inline_ancestor->continuation_of_node()) {
|
||||||
|
needs_new_continuation = false;
|
||||||
|
for (GC::Ptr<Node> 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<NodeWithStyleAndBoxModelMetrics&>(*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<Node> current_parent = after_wrapper;
|
||||||
|
for (GC::Ptr<Node> inline_node = topmost_inline_ancestor;
|
||||||
|
inline_node && is<DOM::Element>(inline_node->dom_node()); inline_node = inline_node->last_child()) {
|
||||||
|
auto& element = static_cast<DOM::Element&>(*inline_node->dom_node());
|
||||||
|
|
||||||
|
auto style = element.computed_properties();
|
||||||
|
auto& new_inline_node = static_cast<NodeWithStyleAndBoxModelMetrics&>(*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<NodeWithStyleAndBoxModelMetrics&>(*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<Node> inline_node = topmost_inline_ancestor; inline_node; inline_node = inline_node->last_child()) {
|
||||||
|
if (!is<NodeWithStyle>(*inline_node))
|
||||||
|
break;
|
||||||
|
m_ancestor_stack.append(static_cast<NodeWithStyle&>(*inline_node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static bool is_ignorable_whitespace(Layout::Node const& node)
|
static bool is_ignorable_whitespace(Layout::Node const& node)
|
||||||
{
|
{
|
||||||
if (node.is_text_node() && static_cast<TextNode const&>(node).text_for_rendering().bytes_as_string_view().is_whitespace())
|
if (node.is_text_node() && static_cast<TextNode const&>(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);
|
create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::After, AppendOrPrepend::Append);
|
||||||
pop_parent();
|
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<NodeWithStyleAndBoxModelMetrics&>(*layout_node));
|
||||||
}
|
}
|
||||||
|
|
||||||
GC::Ptr<Layout::Node> TreeBuilder::build(DOM::Node& dom_node)
|
GC::Ptr<Layout::Node> TreeBuilder::build(DOM::Node& dom_node)
|
||||||
|
|
|
@ -58,6 +58,7 @@ private:
|
||||||
};
|
};
|
||||||
void insert_node_into_inline_or_block_ancestor(Layout::Node&, CSS::Display, AppendOrPrepend);
|
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 create_pseudo_element_if_needed(DOM::Element&, CSS::Selector::PseudoElement::Type, AppendOrPrepend);
|
||||||
|
void restructure_block_node_in_inline_parent(NodeWithStyleAndBoxModelMetrics&);
|
||||||
|
|
||||||
GC::Ptr<Layout::Node> m_layout_root;
|
GC::Ptr<Layout::Node> m_layout_root;
|
||||||
Vector<GC::Ref<Layout::NodeWithStyle>> m_ancestor_stack;
|
Vector<GC::Ref<Layout::NodeWithStyle>> m_ancestor_stack;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
|
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
|
||||||
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
|
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||||||
|
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -17,7 +18,6 @@
|
||||||
#include <LibWeb/HTML/Window.h>
|
#include <LibWeb/HTML/Window.h>
|
||||||
#include <LibWeb/Layout/BlockContainer.h>
|
#include <LibWeb/Layout/BlockContainer.h>
|
||||||
#include <LibWeb/Layout/InlineNode.h>
|
#include <LibWeb/Layout/InlineNode.h>
|
||||||
#include <LibWeb/Layout/Viewport.h>
|
|
||||||
#include <LibWeb/Painting/BackgroundPainting.h>
|
#include <LibWeb/Painting/BackgroundPainting.h>
|
||||||
#include <LibWeb/Painting/PaintableBox.h>
|
#include <LibWeb/Painting/PaintableBox.h>
|
||||||
#include <LibWeb/Painting/SVGPaintable.h>
|
#include <LibWeb/Painting/SVGPaintable.h>
|
||||||
|
@ -214,6 +214,34 @@ CSSPixelRect PaintableBox::compute_absolute_paint_rect() const
|
||||||
return rect;
|
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
|
CSSPixelRect PaintableBox::absolute_paint_rect() const
|
||||||
{
|
{
|
||||||
if (!m_absolute_paint_rect.has_value())
|
if (!m_absolute_paint_rect.has_value())
|
||||||
|
@ -221,6 +249,51 @@ CSSPixelRect PaintableBox::absolute_paint_rect() const
|
||||||
return *m_absolute_paint_rect;
|
return *m_absolute_paint_rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename Callable>
|
||||||
|
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<CSSPixelRect> 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<PaintableBox>(paintable))
|
||||||
|
continue;
|
||||||
|
auto const& paintable_box = static_cast<PaintableBox const&>(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<CSSPixelRect> PaintableBox::get_clip_rect() const
|
Optional<CSSPixelRect> PaintableBox::get_clip_rect() const
|
||||||
{
|
{
|
||||||
auto clip = computed_values().clip();
|
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()) {
|
if (phase == PaintPhase::Overlay && layout_node().document().inspected_layout_node() == &layout_node_with_style_and_box_metrics()) {
|
||||||
auto content_rect = absolute_rect();
|
auto content_rect = absolute_united_content_rect();
|
||||||
|
auto margin_rect = united_rect_for_continuation_chain(*this, [](PaintableBox const& box) {
|
||||||
auto margin_box = box_model().margin_box();
|
auto margin_box = box.box_model().margin_box();
|
||||||
CSSPixelRect margin_rect;
|
return CSSPixelRect {
|
||||||
margin_rect.set_x(absolute_x() - margin_box.left);
|
box.absolute_x() - margin_box.left,
|
||||||
margin_rect.set_width(content_width() + margin_box.left + margin_box.right);
|
box.absolute_y() - margin_box.top,
|
||||||
margin_rect.set_y(absolute_y() - margin_box.top);
|
box.content_width() + margin_box.left + margin_box.right,
|
||||||
margin_rect.set_height(content_height() + margin_box.top + margin_box.bottom);
|
box.content_height() + margin_box.top + margin_box.bottom,
|
||||||
|
};
|
||||||
auto border_rect = absolute_border_box_rect();
|
});
|
||||||
auto padding_rect = absolute_padding_box_rect();
|
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 paint_inspector_rect = [&](CSSPixelRect const& rect, Color color) {
|
||||||
auto device_rect = context.enclosing_device_rect(rect).to_type<int>();
|
auto device_rect = context.enclosing_device_rect(rect).to_type<int>();
|
||||||
|
@ -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()))
|
if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y()))
|
||||||
return TraversalDecision::Continue;
|
return TraversalDecision::Continue;
|
||||||
|
|
||||||
if (!visible_for_hit_testing())
|
if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset))
|
||||||
return TraversalDecision::Continue;
|
return TraversalDecision::Continue;
|
||||||
|
|
||||||
return callback(HitTestResult { const_cast<PaintableBox&>(*this) });
|
return callback(HitTestResult { const_cast<PaintableBox&>(*this) });
|
||||||
|
|
|
@ -57,8 +57,6 @@ public:
|
||||||
CSSPixelPoint scroll_offset {};
|
CSSPixelPoint scroll_offset {};
|
||||||
};
|
};
|
||||||
|
|
||||||
CSSPixelRect absolute_rect() const;
|
|
||||||
|
|
||||||
// Offset from the top left of the containing block's content edge.
|
// Offset from the top left of the containing block's content edge.
|
||||||
[[nodiscard]] CSSPixelPoint offset() const;
|
[[nodiscard]] CSSPixelPoint offset() const;
|
||||||
|
|
||||||
|
@ -78,36 +76,16 @@ public:
|
||||||
CSSPixels content_width() const { return m_content_size.width(); }
|
CSSPixels content_width() const { return m_content_size.width(); }
|
||||||
CSSPixels content_height() const { return m_content_size.height(); }
|
CSSPixels content_height() const { return m_content_size.height(); }
|
||||||
|
|
||||||
CSSPixelRect absolute_padding_box_rect() const
|
CSSPixelRect absolute_rect() const;
|
||||||
{
|
CSSPixelRect absolute_padding_box_rect() const;
|
||||||
auto absolute_rect = this->absolute_rect();
|
CSSPixelRect absolute_border_box_rect() const;
|
||||||
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_paint_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
|
CSSPixels border_box_width() const
|
||||||
{
|
{
|
||||||
auto border_box = box_model().border_box();
|
auto border_box = box_model().border_box();
|
||||||
|
|
|
@ -33,23 +33,28 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
|
||||||
TextNode <#text>
|
TextNode <#text>
|
||||||
InlineNode <form>
|
InlineNode <form>
|
||||||
TextNode <#text>
|
TextNode <#text>
|
||||||
|
BlockContainer <(anonymous)> at (235,65) content-size 139.96875x19 children: not-inline continuation
|
||||||
|
BlockContainer <p> 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>
|
TextNode <#text>
|
||||||
|
RadioButton <input> 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 <form> continuation
|
||||||
|
TextNode <#text>
|
||||||
|
BlockContainer <(anonymous)> at (235,84) content-size 139.96875x19 children: not-inline continuation
|
||||||
|
BlockContainer <p> 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 <input> at (280.171875,84) content-size 12x12 inline-block children: not-inline
|
||||||
TextNode <#text>
|
TextNode <#text>
|
||||||
BlockContainer <p> 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 <input> at (262.5,65) content-size 12x12 inline-block children: not-inline
|
|
||||||
TextNode <#text>
|
|
||||||
BlockContainer <p> 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 <input> 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
|
BlockContainer <(anonymous)> at (235,103) content-size 139.96875x0 children: inline
|
||||||
|
InlineNode <form> continuation
|
||||||
|
TextNode <#text>
|
||||||
TextNode <#text>
|
TextNode <#text>
|
||||||
TextNode <#text>
|
TextNode <#text>
|
||||||
BlockContainer <li> at (409.96875,60) content-size 50x90 floating [BFC] children: inline
|
BlockContainer <li> at (409.96875,60) content-size 50x90 floating [BFC] children: inline
|
||||||
|
@ -136,13 +141,18 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600]
|
||||||
TextPaintable (TextNode<#text>)
|
TextPaintable (TextNode<#text>)
|
||||||
PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x0]
|
PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x0]
|
||||||
PaintableWithLines (InlineNode<FORM>)
|
PaintableWithLines (InlineNode<FORM>)
|
||||||
PaintableWithLines (BlockContainer<P>) [235,65 139.96875x19]
|
PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x19]
|
||||||
TextPaintable (TextNode<#text>)
|
PaintableWithLines (BlockContainer<P>) [235,65 139.96875x19]
|
||||||
RadioButtonPaintable (RadioButton<INPUT>) [262.5,65 12x12]
|
TextPaintable (TextNode<#text>)
|
||||||
PaintableWithLines (BlockContainer<P>) [235,84 139.96875x19]
|
RadioButtonPaintable (RadioButton<INPUT>) [262.5,65 12x12]
|
||||||
TextPaintable (TextNode<#text>)
|
PaintableWithLines (BlockContainer(anonymous)) [235,84 139.96875x0]
|
||||||
RadioButtonPaintable (RadioButton<INPUT>) [280.171875,84 12x12]
|
PaintableWithLines (InlineNode<FORM>)
|
||||||
|
PaintableWithLines (BlockContainer(anonymous)) [235,84 139.96875x19]
|
||||||
|
PaintableWithLines (BlockContainer<P>) [235,84 139.96875x19]
|
||||||
|
TextPaintable (TextNode<#text>)
|
||||||
|
RadioButtonPaintable (RadioButton<INPUT>) [280.171875,84 12x12]
|
||||||
PaintableWithLines (BlockContainer(anonymous)) [235,103 139.96875x0]
|
PaintableWithLines (BlockContainer(anonymous)) [235,103 139.96875x0]
|
||||||
|
PaintableWithLines (InlineNode<FORM>)
|
||||||
PaintableWithLines (BlockContainer<LI>) [394.96875,45 80x120]
|
PaintableWithLines (BlockContainer<LI>) [394.96875,45 80x120]
|
||||||
TextPaintable (TextNode<#text>)
|
TextPaintable (TextNode<#text>)
|
||||||
PaintableWithLines (BlockContainer<LI>#baz) [135,175 120x120]
|
PaintableWithLines (BlockContainer<LI>#baz) [135,175 120x120]
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<b>foo</b><div><b>bar</b></div>
|
||||||
|
<hr>
|
||||||
|
<div><b>foo</b></div><b>bar</b>
|
||||||
|
<hr>
|
||||||
|
<b>foo</b><div><b>bar</b></div><b>baz</b>
|
||||||
|
<hr>
|
||||||
|
<b>foo<i>bar</i></b><div><b><i>baz</i></b></div><b><i>lorem</i>ipsum</b>
|
||||||
|
<hr>
|
||||||
|
<b>foo</b><div><b>bar</b></div><div><b>baz</b></div><b>lorem</b>
|
||||||
|
<hr>
|
||||||
|
<b>foo</b><div><b>bar</b></div><b><u>baz</u></b><div><b>lorem</b></div><b>ipsum</b>
|
||||||
|
<hr>
|
||||||
|
<div>foo<div><b>bar</b></div></div>
|
||||||
|
<hr>
|
||||||
|
<b>foo</b><div><b>bar</b></div><b>baz</b>
|
||||||
|
<hr>
|
||||||
|
<span>foo</span><div>bar</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="match" href="../expected/block-element-inside-inline-element-ref.html" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Block at end of inline element -->
|
||||||
|
<b>foo<div>bar</div></b>
|
||||||
|
<!-- Block at beginning of inline element -->
|
||||||
|
<hr>
|
||||||
|
<b><div>foo</div>bar</b>
|
||||||
|
<!-- Block in middle of inline element -->
|
||||||
|
<hr>
|
||||||
|
<b>foo<div>bar</div>baz</b>
|
||||||
|
<!-- Block in middle of two inline elements -->
|
||||||
|
<hr>
|
||||||
|
<b>foo<i>bar<div>baz</div>lorem</i>ipsum</b>
|
||||||
|
<!-- Multiple subsequent blocks -->
|
||||||
|
<hr>
|
||||||
|
<b>foo<div>bar</div><div>baz</div>lorem</b>
|
||||||
|
<!-- Multiple subsequent blocks with inline element between them -->
|
||||||
|
<hr>
|
||||||
|
<b>foo<div>bar</div><u>baz</u><div>lorem</div>ipsum</b>
|
||||||
|
<!-- Block in inline element following inline element -->
|
||||||
|
<hr>
|
||||||
|
<div>foo<b><div>bar</div></b></div>
|
||||||
|
<!-- Dynamic tree mutation test -->
|
||||||
|
<hr>
|
||||||
|
<div id="target1"></div>
|
||||||
|
<script>
|
||||||
|
document.querySelector('#target1').innerHTML = '<b>foo<div>bar</div>baz</b>';
|
||||||
|
</script>
|
||||||
|
<!-- Dynamic style update -->
|
||||||
|
<hr>
|
||||||
|
<span id="target2" style="font-weight: bold">foo <div>bar</div></span>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.body.offsetWidth; // force layout
|
||||||
|
const target2 = document.querySelector('#target2');
|
||||||
|
target2.setAttribute('style', null);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,4 @@
|
||||||
|
b.offsetTop: 8
|
||||||
|
b.offsetLeft: 8
|
||||||
|
b.offsetWidth: 784
|
||||||
|
b.offsetHeight: 51
|
|
@ -0,0 +1,16 @@
|
||||||
|
<#text >
|
||||||
|
index: 1
|
||||||
|
<BODY >
|
||||||
|
---
|
||||||
|
<#text >
|
||||||
|
index: 1
|
||||||
|
<B id="b1" >
|
||||||
|
---
|
||||||
|
<#text >
|
||||||
|
index: 1
|
||||||
|
<DIV id="d1" >
|
||||||
|
---
|
||||||
|
<#text >
|
||||||
|
index: 3
|
||||||
|
<B id="b1" >
|
||||||
|
---
|
|
@ -0,0 +1,12 @@
|
||||||
|
<b>foo<div>bar</div>baz</b>
|
||||||
|
<script src="../include.js"></script>
|
||||||
|
<script>
|
||||||
|
test(() => {
|
||||||
|
const b = document.querySelector('b');
|
||||||
|
|
||||||
|
println(`b.offsetTop: ${b.offsetTop}`);
|
||||||
|
println(`b.offsetLeft: ${b.offsetLeft}`);
|
||||||
|
println(`b.offsetWidth: ${b.offsetWidth}`);
|
||||||
|
println(`b.offsetHeight: ${b.offsetHeight}`);
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="../include.js"></script>
|
||||||
|
<body>foo<b id="b1">bar<div id="d1">baz</div>lorem</b>
|
||||||
|
<script>
|
||||||
|
test(() => {
|
||||||
|
logHitTest = (x, y) => {
|
||||||
|
const hit = internals.hitTest(x, y);
|
||||||
|
printElement(hit.node);
|
||||||
|
println(`index: ${hit.indexInNode}`);
|
||||||
|
printElement(hit.node.parentNode);
|
||||||
|
println('---');
|
||||||
|
}
|
||||||
|
logHitTest(20, 15);
|
||||||
|
logHitTest(48, 15);
|
||||||
|
logHitTest(23, 30);
|
||||||
|
logHitTest(33, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
Loading…
Add table
Add a link
Reference in a new issue