
The previous implementation used relative X offsets for both left and right-side floats. This made right-side floats super awkward, since we could only determine their X position once the width of the BFC root was known, and for BFC roots with automatic width, this was not even working at all most of the time. This patch changes the way we deal with floats so that BFC keeps track of the offset-from-edge for each float. The offset is the distance from the BFC root edge (left or right, depending on float direction) to the "innermost" margin edge of the floating box. Floating box are now laid out in two passes: while going through the normal flow layout, we put floats in their *static* position (i.e the position they would have occupied if they weren't floating) and then update the Y position value to the final one. The second pass occurs later on, when the BFC root has had its width assigned by the parent context. Once we know the root width, we can set the X position value of floating boxes. (Because the X position of right-side floats is relative to the right edge of the BFC root.)
209 lines
8.1 KiB
C++
209 lines
8.1 KiB
C++
/*
|
|
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/Layout/LineBuilder.h>
|
|
#include <LibWeb/Layout/TextNode.h>
|
|
|
|
namespace Web::Layout {
|
|
|
|
LineBuilder::LineBuilder(InlineFormattingContext& context, FormattingState& formatting_state, LayoutMode layout_mode)
|
|
: m_context(context)
|
|
, m_formatting_state(formatting_state)
|
|
, m_containing_block_state(formatting_state.get_mutable(context.containing_block()))
|
|
, m_layout_mode(layout_mode)
|
|
{
|
|
begin_new_line(false);
|
|
}
|
|
|
|
LineBuilder::~LineBuilder()
|
|
{
|
|
if (m_last_line_needs_update)
|
|
update_last_line();
|
|
}
|
|
|
|
void LineBuilder::break_line()
|
|
{
|
|
update_last_line();
|
|
m_containing_block_state.line_boxes.append(LineBox());
|
|
begin_new_line(true);
|
|
}
|
|
|
|
void LineBuilder::begin_new_line(bool increment_y)
|
|
{
|
|
if (increment_y)
|
|
m_current_y += max(m_max_height_on_current_line, m_context.containing_block().line_height());
|
|
|
|
switch (m_layout_mode) {
|
|
case LayoutMode::Default:
|
|
m_available_width_for_current_line = m_context.available_space_for_line(m_current_y);
|
|
break;
|
|
case LayoutMode::AllPossibleLineBreaks:
|
|
m_available_width_for_current_line = 0;
|
|
break;
|
|
case LayoutMode::OnlyRequiredLineBreaks:
|
|
m_available_width_for_current_line = INFINITY;
|
|
break;
|
|
}
|
|
m_max_height_on_current_line = 0;
|
|
|
|
m_last_line_needs_update = true;
|
|
}
|
|
|
|
LineBox& LineBuilder::ensure_last_line_box()
|
|
{
|
|
auto& line_boxes = m_containing_block_state.line_boxes;
|
|
if (line_boxes.is_empty())
|
|
line_boxes.append(LineBox {});
|
|
return line_boxes.last();
|
|
}
|
|
|
|
void LineBuilder::append_box(Box const& box, float leading_size, float trailing_size, float leading_margin, float trailing_margin)
|
|
{
|
|
auto& box_state = m_formatting_state.get_mutable(box);
|
|
auto& line_box = ensure_last_line_box();
|
|
line_box.add_fragment(box, 0, 0, leading_size, trailing_size, leading_margin, trailing_margin, box_state.content_width, box_state.content_height, box_state.border_box_top(), box_state.border_box_bottom());
|
|
m_max_height_on_current_line = max(m_max_height_on_current_line, box_state.border_box_height());
|
|
|
|
box_state.containing_line_box_fragment = LineBoxFragmentCoordinate {
|
|
.line_box_index = m_containing_block_state.line_boxes.size() - 1,
|
|
.fragment_index = line_box.fragments().size() - 1,
|
|
};
|
|
}
|
|
|
|
void LineBuilder::append_text_chunk(TextNode const& text_node, size_t offset_in_node, size_t length_in_node, float leading_size, float trailing_size, float leading_margin, float trailing_margin, float content_width, float content_height)
|
|
{
|
|
ensure_last_line_box().add_fragment(text_node, offset_in_node, length_in_node, leading_size, trailing_size, leading_margin, trailing_margin, content_width, content_height, 0, 0);
|
|
m_max_height_on_current_line = max(m_max_height_on_current_line, content_height);
|
|
}
|
|
|
|
bool LineBuilder::should_break(LayoutMode layout_mode, float next_item_width, bool should_force_break)
|
|
{
|
|
if (layout_mode == LayoutMode::AllPossibleLineBreaks)
|
|
return true;
|
|
if (should_force_break)
|
|
return true;
|
|
if (layout_mode == LayoutMode::OnlyRequiredLineBreaks)
|
|
return false;
|
|
auto const& line_boxes = m_containing_block_state.line_boxes;
|
|
if (line_boxes.is_empty() || line_boxes.last().is_empty())
|
|
return false;
|
|
auto current_line_width = line_boxes.last().width();
|
|
return (current_line_width + next_item_width) > m_available_width_for_current_line;
|
|
}
|
|
|
|
static float box_baseline(FormattingState const& state, Box const& box)
|
|
{
|
|
auto const& box_state = state.get(box);
|
|
if (!box_state.line_boxes.is_empty())
|
|
return box_state.offset.y() + box_state.line_boxes.last().baseline();
|
|
if (box.has_children() && !box.children_are_inline()) {
|
|
auto const* child_box = box.last_child_of_type<Box>();
|
|
VERIFY(child_box);
|
|
return box_baseline(state, *child_box);
|
|
}
|
|
return box_state.border_box_height();
|
|
}
|
|
|
|
void LineBuilder::update_last_line()
|
|
{
|
|
m_last_line_needs_update = false;
|
|
auto& line_boxes = m_containing_block_state.line_boxes;
|
|
|
|
if (line_boxes.is_empty())
|
|
return;
|
|
|
|
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);
|
|
float bottom = m_current_y + m_context.containing_block().line_height();
|
|
float excess_horizontal_space = m_containing_block_state.content_width - line_box.width();
|
|
|
|
switch (text_align) {
|
|
case CSS::TextAlign::Center:
|
|
case CSS::TextAlign::LibwebCenter:
|
|
x_offset += excess_horizontal_space / 2;
|
|
break;
|
|
case CSS::TextAlign::Right:
|
|
x_offset += excess_horizontal_space;
|
|
break;
|
|
case CSS::TextAlign::Left:
|
|
case CSS::TextAlign::Justify:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
auto fragment_baseline = [&](auto const& fragment) -> float {
|
|
if (fragment.layout_node().is_text_node())
|
|
return fragment.layout_node().font().baseline();
|
|
auto const& box = verify_cast<Layout::Box>(fragment.layout_node());
|
|
return box_baseline(m_formatting_state, box);
|
|
};
|
|
|
|
auto line_box_baseline = [&] {
|
|
float line_box_baseline = 0;
|
|
for (auto const& fragment : line_box.fragments()) {
|
|
auto baseline = fragment_baseline(fragment);
|
|
// NOTE: For fragments with a <length> vertical-align, shift the fragment baseline down by the length.
|
|
// This ensures that we make enough vertical space on the line for any manually-aligned fragments.
|
|
if (auto length_percentage = fragment.layout_node().computed_values().vertical_align().get_pointer<CSS::LengthPercentage>(); length_percentage && length_percentage->is_length())
|
|
baseline += length_percentage->length().to_px(fragment.layout_node());
|
|
line_box_baseline = max(line_box_baseline, baseline);
|
|
}
|
|
return line_box_baseline;
|
|
}();
|
|
|
|
for (size_t i = 0; i < line_box.fragments().size(); ++i) {
|
|
auto& fragment = line_box.fragments()[i];
|
|
|
|
float new_fragment_x = roundf(x_offset + fragment.offset().x());
|
|
float new_fragment_y = 0;
|
|
|
|
auto y_value_for_alignment = [&](CSS::VerticalAlign vertical_align) {
|
|
switch (vertical_align) {
|
|
case CSS::VerticalAlign::Baseline:
|
|
case CSS::VerticalAlign::Bottom:
|
|
case CSS::VerticalAlign::Middle:
|
|
case CSS::VerticalAlign::Sub:
|
|
case CSS::VerticalAlign::Super:
|
|
case CSS::VerticalAlign::TextBottom:
|
|
case CSS::VerticalAlign::TextTop:
|
|
case CSS::VerticalAlign::Top:
|
|
// FIXME: These are all 'baseline'
|
|
return m_current_y + line_box_baseline - fragment_baseline(fragment) + fragment.border_box_top();
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
};
|
|
|
|
auto const& vertical_align = fragment.layout_node().computed_values().vertical_align();
|
|
if (vertical_align.has<CSS::VerticalAlign>()) {
|
|
new_fragment_y = y_value_for_alignment(vertical_align.get<CSS::VerticalAlign>());
|
|
} else {
|
|
if (auto length_percentage = vertical_align.get_pointer<CSS::LengthPercentage>(); length_percentage && length_percentage->is_length()) {
|
|
auto vertical_align_amount = length_percentage->length().to_px(fragment.layout_node());
|
|
new_fragment_y = y_value_for_alignment(CSS::VerticalAlign::Baseline) - vertical_align_amount;
|
|
}
|
|
}
|
|
|
|
fragment.set_offset({ new_fragment_x, new_fragment_y });
|
|
|
|
bottom = max(bottom, new_fragment_y + fragment.height() + fragment.border_box_bottom());
|
|
}
|
|
|
|
line_box.m_bottom = bottom;
|
|
line_box.m_baseline = line_box_baseline;
|
|
}
|
|
|
|
void LineBuilder::remove_last_line_if_empty()
|
|
{
|
|
// If there's an empty line box at the bottom, just remove it instead of giving it height.
|
|
auto& line_boxes = m_containing_block_state.line_boxes;
|
|
if (!line_boxes.is_empty() && line_boxes.last().fragments().is_empty()) {
|
|
line_boxes.take_last();
|
|
m_last_line_needs_update = false;
|
|
}
|
|
}
|
|
}
|