LibWeb: First slightly naive implementation of CSS floats :^)

Boxes can now be floated left or right, which makes text within the
same block formatting context flow around them.

We were creating way too many block formatting contexts. As it turns
out, we don't need one for every new block, but rather there's a set
of rules that determines whether a given block creates a new block
formatting context.

Each BFC keeps track of the floating boxes within it, and IFC's can
then query it to find the available space for line boxes.

There's a huge hack in here where we assume all lines are the exact
line-height. Making this work with vertically non-uniform lines will
require some architectural changes.
This commit is contained in:
Andreas Kling 2020-12-05 20:10:39 +01:00
parent 11256de366
commit 615a4d4f71
Notes: sideshowbarker 2024-07-19 01:02:59 +09:00
18 changed files with 209 additions and 41 deletions

View file

@ -30,6 +30,7 @@
#include <LibWeb/Dump.h>
#include <LibWeb/Layout/BlockBox.h>
#include <LibWeb/Layout/InitialContainingBlockBox.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/ReplacedBox.h>
#include <LibWeb/Layout/TextNode.h>
@ -107,10 +108,14 @@ HitTestResult BlockBox::hit_test(const Gfx::IntPoint& position, HitTestType type
return { absolute_rect().contains(position.x(), position.y()) ? this : nullptr };
}
void BlockBox::split_into_lines(BlockBox& container, LayoutMode layout_mode)
void BlockBox::split_into_lines(InlineFormattingContext& context, LayoutMode layout_mode)
{
auto& container = context.context_box();
auto* line_box = &container.ensure_last_line_box();
if (layout_mode != LayoutMode::OnlyRequiredLineBreaks && line_box->width() > 0 && line_box->width() + width() > container.width()) {
float available_width = context.available_width_at_line(container.line_boxes().size());
if (layout_mode != LayoutMode::OnlyRequiredLineBreaks && line_box->width() > 0 && line_box->width() + width() > available_width) {
line_box = &container.add_line_box();
}
line_box->add_fragment(*this, 0, 0, width(), height());

View file

@ -52,7 +52,7 @@ public:
template<typename Callback>
void for_each_fragment(Callback) const;
virtual void split_into_lines(BlockBox& container, LayoutMode) override;
virtual void split_into_lines(InlineFormattingContext&, LayoutMode) override;
private:
virtual bool is_block() const override { return true; }

View file

@ -65,6 +65,8 @@ void BlockFormattingContext::run(LayoutMode layout_mode)
if (layout_mode == LayoutMode::Default)
compute_width(context_box());
layout_floating_descendants();
if (context_box().children_are_inline()) {
layout_inline_children(layout_mode);
} else {
@ -403,7 +405,7 @@ void BlockFormattingContext::layout_block_level_children(LayoutMode layout_mode)
float content_width = 0;
context_box().for_each_in_subtree_of_type<Box>([&](auto& box) {
if (box.is_absolutely_positioned() || box.containing_block() != &context_box())
if (box.is_absolutely_positioned() || box.is_floating() || box.containing_block() != &context_box())
return IterationDecision::Continue;
compute_width(box);
@ -530,6 +532,8 @@ void BlockFormattingContext::layout_initial_containing_block(LayoutMode layout_m
icb.set_width(viewport_rect.width());
layout_floating_descendants();
layout_block_level_children(layout_mode);
ASSERT(!icb.children_are_inline());
@ -564,6 +568,46 @@ void BlockFormattingContext::layout_absolutely_positioned_descendants()
});
}
void BlockFormattingContext::layout_floating_descendants()
{
context_box().for_each_in_subtree_of_type<Box>([&](auto& box) {
if (box.is_floating() && box.containing_block() == &context_box()) {
layout_floating_descendant(box);
}
return IterationDecision::Continue;
});
}
void BlockFormattingContext::layout_floating_descendant(Box& box)
{
ASSERT(box.is_floating());
auto& containing_block = context_box();
compute_width(box);
layout_inside(box, LayoutMode::Default);
compute_height(box);
if (box.style().float_() == CSS::Float::Left) {
float x = 0;
if (!m_left_floating_boxes.is_empty()) {
auto& previous_floating_box = *m_left_floating_boxes.last();
x = previous_floating_box.effective_offset().x() + previous_floating_box.width();
}
box.set_offset(x, 0);
m_left_floating_boxes.append(&box);
} else if (box.style().float_() == CSS::Float::Right) {
float x = 0;
if (!m_right_floating_boxes.is_empty()) {
auto& previous_floating_box = *m_right_floating_boxes.last();
x = previous_floating_box.effective_offset().x() - box.width();
} else {
x = containing_block.width() - box.width();
}
box.set_offset(x, 0);
m_right_floating_boxes.append(&box);
}
}
void BlockFormattingContext::layout_absolutely_positioned_descendant(Box& box)
{
auto& containing_block = context_box();

View file

@ -26,6 +26,7 @@
#pragma once
#include <AK/Vector.h>
#include <LibWeb/Forward.h>
#include <LibWeb/Layout/FormattingContext.h>
@ -40,22 +41,32 @@ public:
bool is_initial() const;
const Vector<Box*>& left_floating_boxes() const { return m_left_floating_boxes; }
const Vector<Box*>& right_floating_boxes() const { return m_right_floating_boxes; }
protected:
void compute_width(Box&);
void compute_height(Box&);
private:
virtual bool is_block_formatting_context() const final { return true; }
void compute_width_for_absolutely_positioned_block(Box&);
void layout_initial_containing_block(LayoutMode);
void layout_block_level_children(LayoutMode);
void layout_inline_children(LayoutMode);
void layout_absolutely_positioned_descendants();
void layout_floating_descendants();
void place_block_level_replaced_element_in_normal_flow(Box&);
void place_block_level_non_replaced_element_in_normal_flow(Box&);
void layout_absolutely_positioned_descendant(Box&);
void layout_floating_descendant(Box&);
Vector<Box*> m_left_floating_boxes;
Vector<Box*> m_right_floating_boxes;
};
}

View file

@ -26,6 +26,7 @@
#include <LibWeb/Layout/BlockBox.h>
#include <LibWeb/Layout/BreakNode.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
namespace Web::Layout {
@ -39,9 +40,9 @@ BreakNode::~BreakNode()
{
}
void BreakNode::split_into_lines(BlockBox& block, LayoutMode)
void BreakNode::split_into_lines(InlineFormattingContext& block, LayoutMode)
{
block.add_line_box();
block.context_box().add_line_box();
}
}

View file

@ -41,7 +41,7 @@ public:
private:
virtual bool is_break() const override { return true; }
virtual const char* class_name() const override { return "BreakNode"; }
virtual void split_into_lines(BlockBox&, LayoutMode) override;
virtual void split_into_lines(InlineFormattingContext&, LayoutMode) override;
};
}

View file

@ -24,6 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <LibWeb/Dump.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/FormattingContext.h>
@ -34,7 +35,7 @@ namespace Web::Layout {
FormattingContext::FormattingContext(Box& context_box, FormattingContext* parent)
: m_parent(parent)
, m_context_box(context_box)
, m_context_box(&context_box)
{
}
@ -42,8 +43,37 @@ FormattingContext::~FormattingContext()
{
}
bool FormattingContext::creates_block_formatting_context(const Box& box)
{
if (box.is_root_element())
return true;
if (box.is_floating())
return true;
if (box.is_absolutely_positioned())
return true;
if (box.is_inline_block())
return true;
if (box.is_table_cell())
return true;
// FIXME: table-caption
// FIXME: anonymous table cells
// FIXME: Block elements where overflow has a value other than visible and clip.
// FIXME: display: flow-root
// FIXME: Elements with contain: layout, content, or paint.
// FIXME: flex
// FIXME: grid
// FIXME: multicol
// FIXME: column-span: all
return false;
}
void FormattingContext::layout_inside(Box& box, LayoutMode layout_mode)
{
if (creates_block_formatting_context(box)) {
BlockFormattingContext context(box, this);
context.run(layout_mode);
return;
}
if (box.is_table()) {
TableFormattingContext context(box, this);
context.run(layout_mode);
@ -51,8 +81,12 @@ void FormattingContext::layout_inside(Box& box, LayoutMode layout_mode)
InlineFormattingContext context(box, this);
context.run(layout_mode);
} else {
BlockFormattingContext context(box, this);
context.run(layout_mode);
// FIXME: This needs refactoring!
ASSERT(is_block_formatting_context());
auto& old_box = context_box();
set_context_box(box);
run(layout_mode);
set_context_box(old_box);
}
}

View file

@ -34,16 +34,22 @@ class FormattingContext {
public:
virtual void run(LayoutMode) = 0;
Box& context_box() { return m_context_box; }
const Box& context_box() const { return m_context_box; }
Box& context_box() { return *m_context_box; }
const Box& context_box() const { return *m_context_box; }
FormattingContext* parent() { return m_parent; }
const FormattingContext* parent() const { return m_parent; }
virtual bool is_block_formatting_context() const { return false; }
static bool creates_block_formatting_context(const Box&);
protected:
FormattingContext(Box&, FormattingContext* parent = nullptr);
virtual ~FormattingContext();
void set_context_box(Box& box) { m_context_box = &box; }
void layout_inside(Box&, LayoutMode);
struct ShrinkToFitResult {
@ -54,7 +60,7 @@ protected:
ShrinkToFitResult calculate_shrink_to_fit_widths(Box&);
FormattingContext* m_parent { nullptr };
Box& m_context_box;
Box* m_context_box { nullptr };
};
}

View file

@ -45,6 +45,50 @@ InlineFormattingContext::~InlineFormattingContext()
{
}
struct AvailableSpaceForLineInfo {
float left { 0 };
float right { 0 };
};
static AvailableSpaceForLineInfo available_space_for_line(const InlineFormattingContext& context, size_t line_index)
{
AvailableSpaceForLineInfo info;
// FIXME: This is a total hack guess since we don't actually know the final y position of lines here!
float line_height = context.context_box().specified_style().line_height(context.context_box());
float y = (line_index * line_height) + line_height / 2;
auto& bfc = static_cast<const BlockFormattingContext&>(*context.parent());
for (ssize_t i = bfc.left_floating_boxes().size() - 1; i >= 0; --i) {
auto& floating_box = *bfc.left_floating_boxes().at(i);
Gfx::FloatRect rect { floating_box.effective_offset(), floating_box.size() };
if (rect.contains_vertically(y)) {
info.left = rect.right() + 1;
break;
}
}
info.right = context.context_box().width();
for (ssize_t i = bfc.right_floating_boxes().size() - 1; i >= 0; --i) {
auto& floating_box = *bfc.right_floating_boxes().at(i);
Gfx::FloatRect rect { floating_box.effective_offset(), floating_box.size() };
if (rect.contains_vertically(y)) {
info.right = rect.left() - 1;
break;
}
}
return info;
}
float InlineFormattingContext::available_width_at_line(size_t line_index) const
{
auto info = available_space_for_line(*this, line_index);
return info.right - info.left;
}
void InlineFormattingContext::run(LayoutMode layout_mode)
{
auto& containing_block = downcast<BlockBox>(context_box());
@ -56,7 +100,7 @@ void InlineFormattingContext::run(LayoutMode layout_mode)
if (child.is_absolutely_positioned())
return;
child.split_into_lines(containing_block, layout_mode);
child.split_into_lines(*this, layout_mode);
});
for (auto& line_box : containing_block.line_boxes()) {
@ -73,13 +117,15 @@ void InlineFormattingContext::run(LayoutMode layout_mode)
float content_height = 0;
float max_linebox_width = 0;
for (auto& line_box : containing_block.line_boxes()) {
for (size_t line_index = 0; line_index < containing_block.line_boxes().size(); ++line_index) {
auto& line_box = containing_block.line_boxes()[line_index];
float max_height = min_line_height;
for (auto& fragment : line_box.fragments()) {
max_height = max(max_height, fragment.height());
}
float x_offset = 0;
float x_offset = available_space_for_line(*this, line_index).left;
float excess_horizontal_space = (float)containing_block.width() - line_box.width();
switch (text_align) {

View file

@ -38,6 +38,8 @@ public:
virtual void run(LayoutMode) override;
float available_width_at_line(size_t line_index) const;
private:
void dimension_box_on_line(Box&, LayoutMode);
};

View file

@ -27,6 +27,7 @@
#include <LibGfx/Painter.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/Layout/BlockBox.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/InlineNode.h>
namespace Web::Layout {
@ -41,14 +42,16 @@ InlineNode::~InlineNode()
{
}
void InlineNode::split_into_lines(BlockBox& containing_block, LayoutMode layout_mode)
void InlineNode::split_into_lines(InlineFormattingContext& context, LayoutMode layout_mode)
{
auto& containing_block = context.context_box();
if (!style().padding().left.is_undefined_or_auto()) {
float padding_left = style().padding().left.resolved(CSS::Length::make_px(0), *this, containing_block.width()).to_px(*this);
containing_block.ensure_last_line_box().add_fragment(*this, 0, 0, padding_left, 0, LineBoxFragment::Type::Leading);
}
NodeWithStyleAndBoxModelMetrics::split_into_lines(containing_block, layout_mode);
NodeWithStyleAndBoxModelMetrics::split_into_lines(context, layout_mode);
if (!style().padding().right.is_undefined_or_auto()) {
float padding_right = style().padding().right.resolved(CSS::Length::make_px(0), *this, containing_block.width()).to_px(*this);

View file

@ -38,7 +38,7 @@ public:
virtual void paint_fragment(PaintContext&, const LineBoxFragment&, PaintPhase) const override;
virtual void split_into_lines(BlockBox& containing_block, LayoutMode) override;
virtual void split_into_lines(InlineFormattingContext&, LayoutMode) override;
private:
virtual bool is_inline_node() const final { return true; }

View file

@ -30,6 +30,7 @@
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/Layout/BlockBox.h>
#include <LibWeb/Layout/FormattingContext.h>
#include <LibWeb/Layout/InitialContainingBlockBox.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Layout/ReplacedBox.h>
@ -65,6 +66,13 @@ const BlockBox* Node::containing_block() const
return downcast<BlockBox>(ancestor);
};
auto nearest_block_ancestor_that_creates_a_block_formatting_context = [this] {
auto* ancestor = parent();
while (ancestor && (!is<Box>(*ancestor) || !FormattingContext::creates_block_formatting_context(downcast<Box>(*ancestor))))
ancestor = ancestor->parent();
return downcast<BlockBox>(ancestor);
};
if (is_text())
return nearest_block_ancestor();
@ -82,6 +90,9 @@ const BlockBox* Node::containing_block() const
if (position == CSS::Position::Fixed)
return &root();
if (is_floating())
return nearest_block_ancestor_that_creates_a_block_formatting_context();
return nearest_block_ancestor();
}
@ -140,10 +151,10 @@ InitialContainingBlockBox& Node::root()
return *document().layout_node();
}
void Node::split_into_lines(BlockBox& container, LayoutMode layout_mode)
void Node::split_into_lines(InlineFormattingContext& context, LayoutMode layout_mode)
{
for_each_child([&](auto& child) {
child.split_into_lines(container, layout_mode);
child.split_into_lines(context, layout_mode);
});
}

View file

@ -150,7 +150,7 @@ public:
void removed_from(Node&) { }
void children_changed() { }
virtual void split_into_lines(BlockBox& container, LayoutMode);
virtual void split_into_lines(InlineFormattingContext&, LayoutMode);
bool is_visible() const { return m_visible; }
void set_visible(bool visible) { m_visible = visible; }

View file

@ -25,6 +25,7 @@
*/
#include <LibWeb/DOM/Element.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/BlockBox.h>
#include <LibWeb/Layout/ReplacedBox.h>
@ -117,17 +118,19 @@ float ReplacedBox::calculate_height() const
return used_height;
}
void ReplacedBox::split_into_lines(Layout::BlockBox& container, LayoutMode)
void ReplacedBox::split_into_lines(InlineFormattingContext& context, LayoutMode)
{
auto& containing_block = context.context_box();
// FIXME: This feels out of place. It would be nice if someone at a higher level
// made sure we had usable geometry by the time we start splitting.
prepare_for_replaced_layout();
auto width = calculate_width();
auto height = calculate_height();
auto* line_box = &container.ensure_last_line_box();
if (line_box->width() > 0 && line_box->width() + width > container.width())
line_box = &container.add_line_box();
auto* line_box = &containing_block.ensure_last_line_box();
if (line_box->width() > 0 && line_box->width() + width > context.available_width_at_line(containing_block.line_boxes().size()))
line_box = &containing_block.add_line_box();
line_box->add_fragment(*this, 0, 0, width, height);
}

View file

@ -65,7 +65,7 @@ public:
virtual bool can_have_children() const override { return false; }
protected:
virtual void split_into_lines(Layout::BlockBox& container, LayoutMode) override;
virtual void split_into_lines(InlineFormattingContext&, LayoutMode) override;
private:
virtual const char* class_name() const override { return "ReplacedBox"; }

View file

@ -31,6 +31,7 @@
#include <LibGfx/Font.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Layout/BlockBox.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Page/Frame.h>
#include <ctype.h>
@ -189,14 +190,15 @@ void TextNode::for_each_chunk(Callback callback, LayoutMode layout_mode, bool do
commit_chunk(view.end(), false, true);
}
void TextNode::split_into_lines_by_rules(BlockBox& container, LayoutMode layout_mode, bool do_collapse, bool do_wrap_lines, bool do_wrap_breaks)
void TextNode::split_into_lines_by_rules(InlineFormattingContext& context, LayoutMode layout_mode, bool do_collapse, bool do_wrap_lines, bool do_wrap_breaks)
{
auto& containing_block = context.context_box();
auto& font = specified_style().font();
float space_width = font.glyph_width(' ') + font.glyph_spacing();
auto& line_boxes = container.line_boxes();
container.ensure_last_line_box();
float available_width = container.width() - line_boxes.last().width();
auto& line_boxes = containing_block.line_boxes();
containing_block.ensure_last_line_box();
float available_width = context.available_width_at_line(line_boxes.size()) - line_boxes.last().width();
// Collapse whitespace into single spaces
if (do_collapse) {
@ -261,8 +263,8 @@ void TextNode::split_into_lines_by_rules(BlockBox& container, LayoutMode layout_
chunk_width = font.width(chunk.view) + font.glyph_spacing();
if (line_boxes.last().width() > 0 && chunk_width > available_width) {
container.add_line_box();
available_width = container.width();
containing_block.add_line_box();
available_width = context.available_width_at_line(line_boxes.size());
}
if (need_collapse & line_boxes.last().fragments().is_empty())
continue;
@ -275,21 +277,21 @@ void TextNode::split_into_lines_by_rules(BlockBox& container, LayoutMode layout_
if (do_wrap_lines) {
if (available_width < 0) {
container.add_line_box();
available_width = container.width();
containing_block.add_line_box();
available_width = context.available_width_at_line(line_boxes.size());
}
}
if (do_wrap_breaks) {
if (chunk.is_break) {
container.add_line_box();
available_width = container.width();
containing_block.add_line_box();
available_width = context.available_width_at_line(line_boxes.size());
}
}
}
}
void TextNode::split_into_lines(BlockBox& container, LayoutMode layout_mode)
void TextNode::split_into_lines(InlineFormattingContext& context, LayoutMode layout_mode)
{
bool do_collapse = true;
bool do_wrap_lines = true;
@ -313,7 +315,7 @@ void TextNode::split_into_lines(BlockBox& container, LayoutMode layout_mode)
do_wrap_breaks = true;
}
split_into_lines_by_rules(container, layout_mode, do_collapse, do_wrap_lines, do_wrap_breaks);
split_into_lines_by_rules(context, layout_mode, do_collapse, do_wrap_lines, do_wrap_breaks);
}
}

View file

@ -48,12 +48,12 @@ public:
virtual void paint_fragment(PaintContext&, const LineBoxFragment&, PaintPhase) const override;
virtual void split_into_lines(BlockBox& container, LayoutMode) override;
virtual void split_into_lines(InlineFormattingContext&, LayoutMode) override;
const CSS::StyleProperties& specified_style() const { return parent()->specified_style(); }
private:
void split_into_lines_by_rules(BlockBox& container, LayoutMode, bool do_collapse, bool do_wrap_lines, bool do_wrap_breaks);
void split_into_lines_by_rules(InlineFormattingContext&, LayoutMode, bool do_collapse, bool do_wrap_lines, bool do_wrap_breaks);
void paint_cursor_if_needed(PaintContext&, const LineBoxFragment&) const;
template<typename Callback>