LibHTML: Introduce LayoutBox and LayoutNodeWithStyleAndBoxModelMetrics

To streamline the layout tree and remove irrelevant data from classes
that don't need it, this patch adds two new LayoutNode subclasses.

LayoutNodeWithStyleAndBoxModelMetrics should be inherited by any layout
node that cares about box model metrics (margin, border, and padding.)
LayoutBox should be inherited by any layout node that can have a rect.

This makes LayoutText significantly smaller (from 140 to 40 bytes) and
clarifies a lot of things about the layout tree.

I'm also adding next_sibling() and previous_sibling() overloads to
LayoutBlock that return a LayoutBlock*. This is okay since blocks only
ever have block siblings.

Do also note that the semantics of is<T> slightly change in this patch:
is<T>(nullptr) now returns true, to facilitate allowing to<T>(nullptr).
This commit is contained in:
Andreas Kling 2019-10-15 16:48:38 +02:00
parent f4f5ede10a
commit 4814253589
Notes: sideshowbarker 2024-07-19 11:41:13 +09:00
18 changed files with 219 additions and 141 deletions

View file

@ -89,7 +89,7 @@ inline bool is(const Node&)
template<typename T>
inline bool is(const Node* node)
{
return node && is<T>(*node);
return !node || is<T>(*node);
}
template<>

View file

@ -58,35 +58,40 @@ void dump_tree(const LayoutNode& layout_node)
else
tag_name = "???";
dbgprintf("%s {%s} at (%d,%d) size %dx%d",
layout_node.class_name(),
tag_name.characters(),
layout_node.x(),
layout_node.y(),
layout_node.width(),
layout_node.height());
if (!layout_node.is_box()) {
dbgprintf("%s {%s}\n", layout_node.class_name(), tag_name.characters());
} else {
auto& layout_box = to<LayoutBox>(layout_node);
dbgprintf("%s {%s} at (%d,%d) size %dx%d",
layout_box.class_name(),
tag_name.characters(),
layout_box.x(),
layout_box.y(),
layout_box.width(),
layout_box.height());
// Dump the horizontal box properties
dbgprintf(" [%d+%d+%d %d %d+%d+%d]",
layout_node.box_model().margin().left.to_px(),
layout_node.box_model().border().left.to_px(),
layout_node.box_model().padding().left.to_px(),
layout_node.width(),
layout_node.box_model().padding().right.to_px(),
layout_node.box_model().border().right.to_px(),
layout_node.box_model().margin().right.to_px());
// Dump the horizontal box properties
dbgprintf(" [%d+%d+%d %d %d+%d+%d]",
layout_box.box_model().margin().left.to_px(),
layout_box.box_model().border().left.to_px(),
layout_box.box_model().padding().left.to_px(),
layout_box.width(),
layout_box.box_model().padding().right.to_px(),
layout_box.box_model().border().right.to_px(),
layout_box.box_model().margin().right.to_px());
// And the vertical box properties
dbgprintf(" [%d+%d+%d %d %d+%d+%d]",
layout_node.box_model().margin().top.to_px(),
layout_node.box_model().border().top.to_px(),
layout_node.box_model().padding().top.to_px(),
layout_node.height(),
layout_node.box_model().padding().bottom.to_px(),
layout_node.box_model().border().bottom.to_px(),
layout_node.box_model().margin().bottom.to_px());
// And the vertical box properties
dbgprintf(" [%d+%d+%d %d %d+%d+%d]",
layout_box.box_model().margin().top.to_px(),
layout_box.box_model().border().top.to_px(),
layout_box.box_model().padding().top.to_px(),
layout_box.height(),
layout_box.box_model().padding().bottom.to_px(),
layout_box.box_model().border().bottom.to_px(),
layout_box.box_model().margin().bottom.to_px());
dbgprintf("\n");
dbgprintf("\n");
}
if (layout_node.is_block() && static_cast<const LayoutBlock&>(layout_node).children_are_inline()) {
auto& block = static_cast<const LayoutBlock&>(layout_node);

View file

@ -3,9 +3,10 @@
#include <LibHTML/DOM/Element.h>
#include <LibHTML/Layout/LayoutBlock.h>
#include <LibHTML/Layout/LayoutInline.h>
#include <LibHTML/Layout/LayoutReplaced.h>
LayoutBlock::LayoutBlock(const Node* node, NonnullRefPtr<StyleProperties> style)
: LayoutNodeWithStyle(node, move(style))
: LayoutBox(node, move(style))
{
}
@ -39,8 +40,10 @@ void LayoutBlock::layout_block_children()
ASSERT(!children_are_inline());
int content_height = 0;
for_each_child([&](auto& child) {
child.layout();
content_height = child.rect().bottom() + child.box_model().full_margin().bottom - rect().top();
ASSERT(is<LayoutBlock>(child));
auto& child_block = static_cast<LayoutBlock&>(child);
child_block.layout();
content_height = child_block.rect().bottom() + child_block.box_model().full_margin().bottom - rect().top();
});
rect().set_height(content_height);
}
@ -69,8 +72,8 @@ void LayoutBlock::layout_inline_children()
fragment.rect().set_x(x() + fragment.rect().x());
fragment.rect().set_y(y() + content_height + (max_height - fragment.rect().height()));
if (fragment.layout_node().is_replaced())
const_cast<LayoutNode&>(fragment.layout_node()).set_rect(fragment.rect());
if (is<LayoutReplaced>(fragment.layout_node()))
const_cast<LayoutReplaced&>(to<LayoutReplaced>(fragment.layout_node())).set_rect(fragment.rect());
}
content_height += max_height;

View file

@ -1,11 +1,11 @@
#pragma once
#include <LibHTML/Layout/LayoutNode.h>
#include <LibHTML/Layout/LayoutBox.h>
#include <LibHTML/Layout/LineBox.h>
class Element;
class LayoutBlock : public LayoutNodeWithStyle {
class LayoutBlock : public LayoutBox {
public:
LayoutBlock(const Node*, NonnullRefPtr<StyleProperties>);
virtual ~LayoutBlock() override;
@ -27,6 +27,11 @@ public:
virtual HitTestResult hit_test(const Point&) const override;
LayoutBlock* previous_sibling() { return to<LayoutBlock>(LayoutNode::previous_sibling()); }
const LayoutBlock* previous_sibling() const { return to<LayoutBlock>(LayoutNode::previous_sibling()); }
LayoutBlock* next_sibling() { return to<LayoutBlock>(LayoutNode::next_sibling()); }
const LayoutBlock* next_sibling() const { return to<LayoutBlock>(LayoutNode::next_sibling()); }
private:
virtual bool is_block() const override { return true; }

View file

@ -0,0 +1,95 @@
#include <LibGUI/GPainter.h>
#include <LibHTML/DOM/Document.h>
#include <LibHTML/Frame.h>
#include <LibHTML/Layout/LayoutBlock.h>
#include <LibHTML/Layout/LayoutBox.h>
//#define DRAW_BOXES_AROUND_LAYOUT_NODES
//#define DRAW_BOXES_AROUND_HOVERED_NODES
void LayoutBox::render(RenderingContext& context)
{
#ifdef DRAW_BOXES_AROUND_LAYOUT_NODES
context.painter().draw_rect(m_rect, Color::Blue);
#endif
#ifdef DRAW_BOXES_AROUND_HOVERED_NODES
if (!is_anonymous() && node() == document().hovered_node())
context.painter().draw_rect(m_rect, Color::Red);
#endif
Rect padded_rect;
padded_rect.set_x(x() - box_model().padding().left.to_px());
padded_rect.set_width(width() + box_model().padding().left.to_px() + box_model().padding().right.to_px());
padded_rect.set_y(y() - box_model().padding().top.to_px());
padded_rect.set_height(height() + box_model().padding().top.to_px() + box_model().padding().bottom.to_px());
auto bgcolor = style().property(CSS::PropertyID::BackgroundColor);
if (bgcolor.has_value() && bgcolor.value()->is_color()) {
context.painter().fill_rect(padded_rect, bgcolor.value()->to_color(document()));
}
// FIXME: Respect all individual border sides
auto border_width_value = style().property(CSS::PropertyID::BorderTopWidth);
auto border_color_value = style().property(CSS::PropertyID::BorderTopColor);
auto border_style_value = style().property(CSS::PropertyID::BorderTopStyle);
if (border_width_value.has_value() && border_color_value.has_value()) {
int border_width = border_width_value.value()->to_length().to_px();
Color border_color = border_color_value.value()->to_color(document());
if (border_style_value.has_value() && border_style_value.value()->to_string() == "inset") {
// border-style: inset
auto shadow_color = Color::from_rgb(0x888888);
auto highlight_color = Color::from_rgb(0x5a5a5a);
context.painter().draw_line(padded_rect.top_left(), padded_rect.top_right(), highlight_color, border_width);
context.painter().draw_line(padded_rect.top_right(), padded_rect.bottom_right(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_right(), padded_rect.bottom_left(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_left(), padded_rect.top_left(), highlight_color, border_width);
} else if (border_style_value.has_value() && border_style_value.value()->to_string() == "outset") {
// border-style: outset
auto highlight_color = Color::from_rgb(0x888888);
auto shadow_color = Color::from_rgb(0x5a5a5a);
context.painter().draw_line(padded_rect.top_left(), padded_rect.top_right(), highlight_color, border_width);
context.painter().draw_line(padded_rect.top_right(), padded_rect.bottom_right(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_right(), padded_rect.bottom_left(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_left(), padded_rect.top_left(), highlight_color, border_width);
} else {
// border-style: solid
context.painter().draw_line(padded_rect.top_left(), padded_rect.top_right(), border_color, border_width);
context.painter().draw_line(padded_rect.top_right(), padded_rect.bottom_right(), border_color, border_width);
context.painter().draw_line(padded_rect.bottom_right(), padded_rect.bottom_left(), border_color, border_width);
context.painter().draw_line(padded_rect.bottom_left(), padded_rect.top_left(), border_color, border_width);
}
}
}
HitTestResult LayoutBox::hit_test(const Point& position) const
{
// FIXME: It would be nice if we could confidently skip over hit testing
// parts of the layout tree, but currently we can't just check
// m_rect.contains() since inline text rects can't be trusted..
HitTestResult result { m_rect.contains(position) ? this : nullptr };
for_each_child([&](auto& child) {
auto child_result = child.hit_test(position);
if (child_result.layout_node)
result = child_result;
});
return result;
}
void LayoutBox::set_needs_display()
{
auto* frame = document().frame();
ASSERT(frame);
if (!is_inline()) {
const_cast<Frame*>(frame)->set_needs_display(rect());
return;
}
for_each_fragment_of_this([&](auto& fragment) {
if (&fragment.layout_node() == this || is_ancestor_of(fragment.layout_node())) {
const_cast<Frame*>(frame)->set_needs_display(fragment.rect());
}
return IterationDecision::Continue;
});
}

View file

@ -0,0 +1,39 @@
#pragma once
#include <LibHTML/Layout/LayoutNode.h>
class LayoutBox : public LayoutNodeWithStyleAndBoxModelMetrics {
public:
const Rect& rect() const { return m_rect; }
Rect& rect() { return m_rect; }
void set_rect(const Rect& rect) { m_rect = rect; }
int x() const { return rect().x(); }
int y() const { return rect().y(); }
int width() const { return rect().width(); }
int height() const { return rect().height(); }
Size size() const { return rect().size(); }
Point position() const { return rect().location(); }
virtual HitTestResult hit_test(const Point& position) const override;
virtual void set_needs_display() override;
protected:
LayoutBox(const Node* node, NonnullRefPtr<StyleProperties> style)
: LayoutNodeWithStyleAndBoxModelMetrics(node, move(style))
{
}
virtual void render(RenderingContext&) override;
private:
virtual bool is_box() const override { return true; }
Rect m_rect;
};
template<>
inline bool is<LayoutBox>(const LayoutNode& node)
{
return node.is_box();
}

View file

@ -2,7 +2,7 @@
#include <LibHTML/Layout/LayoutBreak.h>
LayoutBreak::LayoutBreak(const HTMLBRElement& element)
: LayoutNode(&element)
: LayoutNodeWithStyleAndBoxModelMetrics(&element, StyleProperties::create())
{
set_inline(true);
}

View file

@ -3,7 +3,7 @@
#include <LibHTML/DOM/HTMLBRElement.h>
#include <LibHTML/Layout/LayoutNode.h>
class LayoutBreak final : public LayoutNode {
class LayoutBreak final : public LayoutNodeWithStyleAndBoxModelMetrics {
public:
explicit LayoutBreak(const HTMLBRElement&);
virtual ~LayoutBreak() override;

View file

@ -1,4 +1,5 @@
#include <LibHTML/Frame.h>
#include <LibHTML/Dump.h>
#include <LibHTML/Layout/LayoutDocument.h>
LayoutDocument::LayoutDocument(const Document& document, NonnullRefPtr<StyleProperties> style)
@ -17,10 +18,14 @@ void LayoutDocument::layout()
LayoutNode::layout();
ASSERT(!children_are_inline());
int lowest_bottom = 0;
for_each_child([&](auto& child) {
if (child.rect().bottom() > lowest_bottom)
lowest_bottom = child.rect().bottom();
ASSERT(is<LayoutBlock>(child));
auto& child_block = to<LayoutBlock>(child);
if (child_block.rect().bottom() > lowest_bottom)
lowest_bottom = child_block.rect().bottom();
});
rect().set_bottom(lowest_bottom);
}

View file

@ -3,7 +3,7 @@
#include <LibHTML/Layout/LayoutInline.h>
LayoutInline::LayoutInline(const Element& element, NonnullRefPtr<StyleProperties> style)
: LayoutNodeWithStyle(&element, move(style))
: LayoutNodeWithStyleAndBoxModelMetrics(&element, move(style))
{
set_inline(true);
}

View file

@ -1,15 +1,12 @@
#pragma once
#include <LibHTML/Layout/LayoutNode.h>
#include <LibHTML/Layout/LayoutBox.h>
class LayoutBlock;
class LayoutInline : public LayoutNodeWithStyle {
class LayoutInline : public LayoutNodeWithStyleAndBoxModelMetrics {
public:
LayoutInline(const Element&, NonnullRefPtr<StyleProperties>);
virtual ~LayoutInline() override;
virtual const char* class_name() const override { return "LayoutInline"; }
private:
};

View file

@ -2,7 +2,7 @@
#include <LibHTML/Layout/LayoutListItemMarker.h>
LayoutListItemMarker::LayoutListItemMarker()
: LayoutNode(nullptr)
: LayoutBox(nullptr, StyleProperties::create())
{
}

View file

@ -1,8 +1,8 @@
#pragma once
#include <LibHTML/Layout/LayoutNode.h>
#include <LibHTML/Layout/LayoutBox.h>
class LayoutListItemMarker final : public LayoutNode {
class LayoutListItemMarker final : public LayoutBox {
public:
LayoutListItemMarker();
virtual ~LayoutListItemMarker() override;

View file

@ -5,9 +5,6 @@
#include <LibHTML/Layout/LayoutBlock.h>
#include <LibHTML/Layout/LayoutNode.h>
//#define DRAW_BOXES_AROUND_LAYOUT_NODES
//#define DRAW_BOXES_AROUND_HOVERED_NODES
LayoutNode::LayoutNode(const Node* node)
: m_node(node)
{
@ -42,58 +39,6 @@ void LayoutNode::render(RenderingContext& context)
if (!is_visible())
return;
#ifdef DRAW_BOXES_AROUND_LAYOUT_NODES
context.painter().draw_rect(m_rect, Color::Blue);
#endif
#ifdef DRAW_BOXES_AROUND_HOVERED_NODES
if (!is_anonymous() && node() == document().hovered_node())
context.painter().draw_rect(m_rect, Color::Red);
#endif
Rect padded_rect;
padded_rect.set_x(x() - box_model().padding().left.to_px());
padded_rect.set_width(width() + box_model().padding().left.to_px() + box_model().padding().right.to_px());
padded_rect.set_y(y() - box_model().padding().top.to_px());
padded_rect.set_height(height() + box_model().padding().top.to_px() + box_model().padding().bottom.to_px());
auto bgcolor = style().property(CSS::PropertyID::BackgroundColor);
if (bgcolor.has_value() && bgcolor.value()->is_color()) {
context.painter().fill_rect(padded_rect, bgcolor.value()->to_color(document()));
}
// FIXME: Respect all individual border sides
auto border_width_value = style().property(CSS::PropertyID::BorderTopWidth);
auto border_color_value = style().property(CSS::PropertyID::BorderTopColor);
auto border_style_value = style().property(CSS::PropertyID::BorderTopStyle);
if (border_width_value.has_value() && border_color_value.has_value()) {
int border_width = border_width_value.value()->to_length().to_px();
Color border_color = border_color_value.value()->to_color(document());
if (border_style_value.has_value() && border_style_value.value()->to_string() == "inset") {
// border-style: inset
auto shadow_color = Color::from_rgb(0x888888);
auto highlight_color = Color::from_rgb(0x5a5a5a);
context.painter().draw_line(padded_rect.top_left(), padded_rect.top_right(), highlight_color, border_width);
context.painter().draw_line(padded_rect.top_right(), padded_rect.bottom_right(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_right(), padded_rect.bottom_left(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_left(), padded_rect.top_left(), highlight_color, border_width);
} else if (border_style_value.has_value() && border_style_value.value()->to_string() == "outset") {
// border-style: outset
auto highlight_color = Color::from_rgb(0x888888);
auto shadow_color = Color::from_rgb(0x5a5a5a);
context.painter().draw_line(padded_rect.top_left(), padded_rect.top_right(), highlight_color, border_width);
context.painter().draw_line(padded_rect.top_right(), padded_rect.bottom_right(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_right(), padded_rect.bottom_left(), shadow_color, border_width);
context.painter().draw_line(padded_rect.bottom_left(), padded_rect.top_left(), highlight_color, border_width);
} else {
// border-style: solid
context.painter().draw_line(padded_rect.top_left(), padded_rect.top_right(), border_color, border_width);
context.painter().draw_line(padded_rect.top_right(), padded_rect.bottom_right(), border_color, border_width);
context.painter().draw_line(padded_rect.bottom_right(), padded_rect.bottom_left(), border_color, border_width);
context.painter().draw_line(padded_rect.bottom_left(), padded_rect.top_left(), border_color, border_width);
}
}
// TODO: render our border
for_each_child([&](auto& child) {
child.render(context);
@ -102,10 +47,7 @@ void LayoutNode::render(RenderingContext& context)
HitTestResult LayoutNode::hit_test(const Point& position) const
{
// FIXME: It would be nice if we could confidently skip over hit testing
// parts of the layout tree, but currently we can't just check
// m_rect.contains() since inline text rects can't be trusted..
HitTestResult result { m_rect.contains(position) ? this : nullptr };
HitTestResult result;
for_each_child([&](auto& child) {
auto child_result = child.hit_test(position);
if (child_result.layout_node)
@ -134,18 +76,4 @@ void LayoutNode::split_into_lines(LayoutBlock& container)
void LayoutNode::set_needs_display()
{
auto* frame = document().frame();
ASSERT(frame);
if (!is_inline()) {
const_cast<Frame*>(frame)->set_needs_display(rect());
return;
}
for_each_fragment_of_this([&](auto& fragment) {
if (&fragment.layout_node() == this || is_ancestor_of(fragment.layout_node())) {
const_cast<Frame*>(frame)->set_needs_display(fragment.rect());
}
return IterationDecision::Continue;
});
}

View file

@ -24,20 +24,6 @@ class LayoutNode : public TreeNode<LayoutNode> {
public:
virtual ~LayoutNode();
const Rect& rect() const { return m_rect; }
Rect& rect() { return m_rect; }
void set_rect(const Rect& rect) { m_rect = rect; }
int x() const { return rect().x(); }
int y() const { return rect().y(); }
int width() const { return rect().width(); }
int height() const { return rect().height(); }
Size size() const { return rect().size(); }
Point position() const { return rect().location(); }
BoxModelMetrics& box_model() { return m_box_metrics; }
const BoxModelMetrics& box_model() const { return m_box_metrics; }
virtual HitTestResult hit_test(const Point&) const;
bool is_anonymous() const { return !m_node; }
@ -63,6 +49,7 @@ public:
virtual bool is_text() const { return false; }
virtual bool is_block() const { return false; }
virtual bool is_replaced() const { return false; }
virtual bool is_box() const { return false; }
bool has_style() const { return m_has_style; }
bool is_inline() const { return m_inline; }
@ -87,7 +74,7 @@ public:
bool is_visible() const { return m_visible; }
void set_visible(bool visible) { m_visible = visible; }
void set_needs_display();
virtual void set_needs_display();
template<typename Callback>
void for_each_fragment_of_this(Callback);
@ -100,8 +87,6 @@ private:
const Node* m_node { nullptr };
BoxModelMetrics m_box_metrics;
Rect m_rect;
bool m_inline { false };
bool m_has_style { false };
bool m_visible { true };
@ -126,6 +111,21 @@ private:
NonnullRefPtr<StyleProperties> m_style;
};
class LayoutNodeWithStyleAndBoxModelMetrics : public LayoutNodeWithStyle {
public:
BoxModelMetrics& box_model() { return m_box_model; }
const BoxModelMetrics& box_model() const { return m_box_model; }
protected:
LayoutNodeWithStyleAndBoxModelMetrics(const Node* node, NonnullRefPtr<StyleProperties> style)
: LayoutNodeWithStyle(node, move(style))
{
}
private:
BoxModelMetrics m_box_model;
};
inline const StyleProperties& LayoutNode::style() const
{
if (m_has_style)
@ -147,7 +147,7 @@ inline bool is(const LayoutNode&)
template<typename T>
inline bool is(const LayoutNode* node)
{
return node && is<T>(*node);
return !node || is<T>(*node);
}
template<>

View file

@ -3,7 +3,7 @@
#include <LibHTML/Layout/LayoutReplaced.h>
LayoutReplaced::LayoutReplaced(const Element& element, NonnullRefPtr<StyleProperties> style)
: LayoutNodeWithStyle(&element, move(style))
: LayoutBox(&element, move(style))
{
// FIXME: Allow non-inline replaced elements.
set_inline(true);

View file

@ -1,7 +1,7 @@
#include <LibHTML/DOM/Element.h>
#include <LibHTML/Layout/LayoutNode.h>
#include <LibHTML/Layout/LayoutBox.h>
class LayoutReplaced : public LayoutNodeWithStyle {
class LayoutReplaced : public LayoutBox {
public:
LayoutReplaced(const Element&, NonnullRefPtr<StyleProperties>);
virtual ~LayoutReplaced() override;

View file

@ -34,6 +34,7 @@ LIBHTML_OBJS = \
Parser/HTMLParser.o \
Parser/CSSParser.o \
Layout/LayoutNode.o \
Layout/LayoutBox.o \
Layout/LayoutText.o \
Layout/LayoutBlock.o \
Layout/LayoutInline.o \