diff --git a/Base/res/html/misc/float-stress-1.html b/Base/res/html/misc/float-stress-1.html
new file mode 100644
index 0000000000..37e03a0ea6
--- /dev/null
+++ b/Base/res/html/misc/float-stress-1.html
@@ -0,0 +1,67 @@
+
+
+
+
+ foo bar baz foo bar baz
+
+
+
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
+
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
+
+ foo bar baz foo bar baz
+
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
+ foo bar baz foo bar baz
diff --git a/Userland/Libraries/LibWeb/Layout/BlockFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/BlockFormattingContext.cpp
index 7efea3e353..31b83db127 100644
--- a/Userland/Libraries/LibWeb/Layout/BlockFormattingContext.cpp
+++ b/Userland/Libraries/LibWeb/Layout/BlockFormattingContext.cpp
@@ -616,17 +616,15 @@ void BlockFormattingContext::layout_floating_box(Box const& box, BlockContainer
// First we place the box normally (to get the right y coordinate.)
// If we have a LineBuilder, we're in the middle of inline layout, otherwise this is block layout.
if (line_builder) {
- float y_offset = box_state.margin_box_top();
- line_builder->break_if_needed(box_state.margin_box_width());
- box_state.offset.set_y(line_builder->current_y() + y_offset);
- line_builder->adjust_last_line_after_inserting_floating_box({}, box.computed_values().float_(), box_state.margin_box_width());
+ auto y = line_builder->y_for_float_to_be_inserted_here(box);
+ box_state.offset.set_y(y + box_state.margin_box_top());
} else {
place_block_level_element_in_normal_flow_vertically(box, containing_block);
place_block_level_element_in_normal_flow_horizontally(box, containing_block);
}
// Then we float it to the left or right.
- auto float_box = [&](FloatSide side, FloatSideData& side_data) {
+ auto float_box = [&](FloatSide side, FloatSideData& side_data, FloatSideData& other_side_data) {
float offset_from_edge = 0;
auto float_to_edge = [&] {
if (side == FloatSide::Left)
@@ -684,7 +682,15 @@ void BlockFormattingContext::layout_floating_box(Box const& box, BlockContainer
side_data.clear();
}
}
- y += side_data.y_offset;
+
+ // NOTE: If we're in inline layout, the LineBuilder has already provided the right Y offset.
+ // In block layout, we adjust by the side's current Y offset here.
+ // FIXME: It's annoying that we have different behavior for inline vs block here.
+ // Find a way to unify the behavior so we don't need to branch here.
+
+ if (!line_builder)
+ y += side_data.y_offset;
+
side_data.all_boxes.append(adopt_own(*new FloatingBox {
.box = box,
.offset_from_edge = offset_from_edge,
@@ -703,16 +709,24 @@ void BlockFormattingContext::layout_floating_box(Box const& box, BlockContainer
// NOTE: We don't set the X position here, that happens later, once we know the root block width.
// See parent_context_did_dimension_child_root_box() for that logic.
box_state.offset.set_y(y);
+
+ // If the new box was inserted below the bottom of the opposite side,
+ // we reset the other side back to its edge.
+ if (y > other_side_data.y_offset)
+ other_side_data.clear();
};
// Next, float to the left and/or right
if (box.computed_values().float_() == CSS::Float::Left) {
- float_box(FloatSide::Left, m_left_floats);
+ float_box(FloatSide::Left, m_left_floats, m_right_floats);
} else if (box.computed_values().float_() == CSS::Float::Right) {
- float_box(FloatSide::Right, m_right_floats);
+ float_box(FloatSide::Right, m_right_floats, m_left_floats);
}
m_state.get_mutable(root()).add_floating_descendant(box);
+
+ if (line_builder)
+ line_builder->recalculate_available_space();
}
void BlockFormattingContext::layout_list_item_marker(ListItemBox const& list_item_box)
@@ -755,7 +769,8 @@ BlockFormattingContext::SpaceUsedByFloats BlockFormattingContext::space_used_by_
{
SpaceUsedByFloats space_used_by_floats;
- for (auto const& floating_box : m_left_floats.current_boxes.in_reverse()) {
+ for (auto const& floating_box_ptr : m_left_floats.all_boxes.in_reverse()) {
+ auto const& floating_box = *floating_box_ptr;
auto const& floating_box_state = m_state.get(floating_box.box);
// NOTE: The floating box is *not* in the final horizontal position yet, but the size and vertical position is valid.
auto rect = border_box_rect_in_ancestor_coordinate_space(floating_box.box, root(), m_state);
@@ -767,7 +782,8 @@ BlockFormattingContext::SpaceUsedByFloats BlockFormattingContext::space_used_by_
}
}
- for (auto const& floating_box : m_right_floats.current_boxes.in_reverse()) {
+ for (auto const& floating_box_ptr : m_right_floats.all_boxes.in_reverse()) {
+ auto const& floating_box = *floating_box_ptr;
auto const& floating_box_state = m_state.get(floating_box.box);
// NOTE: The floating box is *not* in the final horizontal position yet, but the size and vertical position is valid.
auto rect = border_box_rect_in_ancestor_coordinate_space(floating_box.box, root(), m_state);
diff --git a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
index 0ab47b3cba..6640229e4d 100644
--- a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
+++ b/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
@@ -305,4 +305,23 @@ bool InlineFormattingContext::any_floats_intrude_at_y(float y) const
return space.left > 0 || space.right > 0;
}
+bool InlineFormattingContext::can_fit_new_line_at_y(float y) const
+{
+ auto box_in_root_rect = content_box_rect_in_ancestor_coordinate_space(containing_block(), parent().root(), m_state);
+ float y_in_root = box_in_root_rect.y() + y;
+ auto space_top = parent().space_used_by_floats(y_in_root);
+ auto space_bottom = parent().space_used_by_floats(y_in_root + containing_block().line_height() - 1);
+
+ [[maybe_unused]] auto top_left_edge = space_top.left;
+ [[maybe_unused]] auto top_right_edge = m_effective_containing_block_width - space_top.right;
+ [[maybe_unused]] auto bottom_left_edge = space_bottom.left;
+ [[maybe_unused]] auto bottom_right_edge = m_effective_containing_block_width - space_bottom.right;
+
+ if (top_left_edge > bottom_right_edge)
+ return false;
+ if (bottom_left_edge > top_right_edge)
+ return false;
+ return true;
+}
+
}
diff --git a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.h b/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.h
index 525f968144..0eeb452aa7 100644
--- a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.h
+++ b/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.h
@@ -30,6 +30,7 @@ public:
float leftmost_x_offset_at(float y) const;
float available_space_for_line(float y) const;
bool any_floats_intrude_at_y(float y) const;
+ bool can_fit_new_line_at_y(float y) const;
float effective_containing_block_width() const { return m_effective_containing_block_width; }
diff --git a/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp b/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp
index e1176ecc99..9f11e914df 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp
+++ b/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp
@@ -27,19 +27,40 @@ LineBuilder::~LineBuilder()
void LineBuilder::break_line(Optional
next_item_width)
{
update_last_line();
+ size_t break_count = 0;
+ bool floats_intrude_at_current_y = false;
do {
m_containing_block_state.line_boxes.append(LineBox());
- begin_new_line(true);
- } while (next_item_width.has_value()
- && next_item_width.value() > m_available_width_for_current_line
- && m_context.any_floats_intrude_at_y(m_current_y));
+ begin_new_line(true, break_count == 0);
+ break_count++;
+ floats_intrude_at_current_y = m_context.any_floats_intrude_at_y(m_current_y);
+ } while ((floats_intrude_at_current_y && !m_context.can_fit_new_line_at_y(m_current_y))
+ || (next_item_width.has_value()
+ && next_item_width.value() > m_available_width_for_current_line
+ && floats_intrude_at_current_y));
}
-void LineBuilder::begin_new_line(bool increment_y)
+void LineBuilder::begin_new_line(bool increment_y, bool is_first_break_in_sequence)
{
- if (increment_y)
- m_current_y += max(m_max_height_on_current_line, m_context.containing_block().line_height());
- m_available_width_for_current_line = m_context.available_space_for_line(m_current_y);
+ if (increment_y) {
+ if (is_first_break_in_sequence) {
+ // First break is simple, just go to the start of the next line.
+ m_current_y += max(m_max_height_on_current_line, m_context.containing_block().line_height());
+ } else {
+ // We're doing more than one break in a row.
+ // This means we're trying to squeeze past intruding floats.
+ // Scan 1px at a time until we find a Y value where a new line can fit.
+ // FIXME: This is super dumb and inefficient.
+ float candidate_y = m_current_y + 1;
+ while (true) {
+ if (m_context.can_fit_new_line_at_y(candidate_y))
+ break;
+ ++candidate_y;
+ }
+ m_current_y = candidate_y;
+ }
+ }
+ recalculate_available_space();
m_max_height_on_current_line = 0;
m_last_line_needs_update = true;
}
@@ -71,6 +92,32 @@ void LineBuilder::append_text_chunk(TextNode const& text_node, size_t offset_in_
m_max_height_on_current_line = max(m_max_height_on_current_line, content_height);
}
+float LineBuilder::y_for_float_to_be_inserted_here(Box const& box)
+{
+ auto const& box_state = m_layout_state.get(box);
+ auto width = box_state.margin_box_width();
+
+ auto current_line_width = ensure_last_line_box().width();
+ if (roundf(current_line_width + width) > m_available_width_for_current_line) {
+ float candidate_y = m_current_y + m_context.containing_block().line_height();
+ // FIXME: This is super dumb, we move 1px downwards per iteration and stop
+ // when we find an Y value where we don't collide with other floats.
+ while (true) {
+ if (width > m_context.available_space_for_line(candidate_y)) {
+ if (!m_context.any_floats_intrude_at_y(candidate_y)) {
+ return candidate_y;
+ }
+ } else {
+ return candidate_y;
+ }
+ candidate_y += 1;
+ }
+
+ return m_current_y + m_context.containing_block().line_height();
+ }
+ return m_current_y;
+}
+
bool LineBuilder::should_break(float next_item_width)
{
if (!isfinite(m_available_width_for_current_line))
@@ -82,6 +129,8 @@ bool LineBuilder::should_break(float next_item_width)
// at this Y coordinate, we don't need to break before inserting anything.
if (!m_context.any_floats_intrude_at_y(m_current_y))
return false;
+ if (!m_context.any_floats_intrude_at_y(m_current_y + m_context.containing_block().line_height()))
+ return false;
}
auto current_line_width = ensure_last_line_box().width();
return roundf(current_line_width + next_item_width) > m_available_width_for_current_line;
@@ -124,7 +173,12 @@ void LineBuilder::update_last_line()
auto& line_box = line_boxes.last();
auto text_align = m_context.containing_block().computed_values().text_align();
- float x_offset = m_context.leftmost_x_offset_at(m_current_y);
+
+ auto current_line_height = max(m_max_height_on_current_line, m_context.containing_block().line_height());
+ float x_offset_top = m_context.leftmost_x_offset_at(m_current_y);
+ float x_offset_bottom = m_context.leftmost_x_offset_at(m_current_y + current_line_height - 1);
+ float x_offset = max(x_offset_top, x_offset_bottom);
+
float excess_horizontal_space = m_context.effective_containing_block_width() - line_box.width();
switch (text_align) {
@@ -260,18 +314,12 @@ void LineBuilder::remove_last_line_if_empty()
}
}
-void LineBuilder::adjust_last_line_after_inserting_floating_box(Badge, CSS::Float float_, float space_used_by_float)
+void LineBuilder::recalculate_available_space()
{
- // NOTE: LineBuilder generates lines from left-to-right, so if we've just added a left-side float,
- // that means every fragment already on this line has to move towards the right.
- if (float_ == CSS::Float::Left && !m_containing_block_state.line_boxes.is_empty()) {
- for (auto& fragment : m_containing_block_state.line_boxes.last().fragments())
- fragment.set_offset(fragment.offset().translated(space_used_by_float, 0));
- }
-
- m_available_width_for_current_line -= space_used_by_float;
- if (m_available_width_for_current_line < 0)
- m_available_width_for_current_line = 0;
+ auto current_line_height = max(m_max_height_on_current_line, m_context.containing_block().line_height());
+ auto available_at_top_of_line_box = m_context.available_space_for_line(m_current_y);
+ auto available_at_bottom_of_line_box = m_context.available_space_for_line(m_current_y + current_line_height - 1);
+ m_available_width_for_current_line = min(available_at_bottom_of_line_box, available_at_top_of_line_box);
}
}
diff --git a/Userland/Libraries/LibWeb/Layout/LineBuilder.h b/Userland/Libraries/LibWeb/Layout/LineBuilder.h
index b240612d64..42fa6f65df 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBuilder.h
+++ b/Userland/Libraries/LibWeb/Layout/LineBuilder.h
@@ -39,11 +39,13 @@ public:
void remove_last_line_if_empty();
float current_y() const { return m_current_y; }
+ void set_current_y(float y) { m_current_y = y; }
- void adjust_last_line_after_inserting_floating_box(Badge, CSS::Float, float space_used_by_float);
+ void recalculate_available_space();
+ float y_for_float_to_be_inserted_here(Box const&);
private:
- void begin_new_line(bool increment_y);
+ void begin_new_line(bool increment_y, bool is_first_break_in_sequence = true);
bool should_break(float next_item_width);