ladybird/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
Andreas Kling 39b7fbfeb9 LibWeb: Rewrite CSS float implementation to use offset-from-edge
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.)
2022-03-18 15:18:48 +01:00

280 lines
12 KiB
C++

/*
* Copyright (c) 2020-2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Length.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/Dump.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/InlineLevelIterator.h>
#include <LibWeb/Layout/LineBuilder.h>
#include <LibWeb/Layout/ReplacedBox.h>
namespace Web::Layout {
constexpr float text_justification_threshold = 0.1;
InlineFormattingContext::InlineFormattingContext(FormattingState& state, BlockContainer const& containing_block, BlockFormattingContext& parent)
: FormattingContext(Type::Inline, state, containing_block, &parent)
{
}
InlineFormattingContext::~InlineFormattingContext() = default;
BlockFormattingContext& InlineFormattingContext::parent()
{
return static_cast<BlockFormattingContext&>(*FormattingContext::parent());
}
BlockFormattingContext const& InlineFormattingContext::parent() const
{
return static_cast<BlockFormattingContext const&>(*FormattingContext::parent());
}
float InlineFormattingContext::leftmost_x_offset_at(float y) const
{
// NOTE: Floats are relative to the BFC root box, not necessarily the containing block of this IFC.
auto box_in_root_rect = margin_box_rect_in_ancestor_coordinate_space(containing_block(), parent().root(), m_state);
float y_in_root = box_in_root_rect.y() + y;
auto space = parent().space_used_by_floats(y_in_root);
float containing_block_x = m_state.get(containing_block()).offset.x();
return max(space.left, containing_block_x) - containing_block_x;
}
float InlineFormattingContext::available_space_for_line(float y) const
{
// NOTE: Floats are relative to the BFC root box, not necessarily the containing block of this IFC.
auto box_in_root_rect = margin_box_rect_in_ancestor_coordinate_space(containing_block(), parent().root(), m_state);
float y_in_root = box_in_root_rect.y() + y;
auto space = parent().space_used_by_floats(y_in_root);
auto const& containing_block_state = m_state.get(containing_block());
auto const& root_block_state = m_state.get(parent().root());
space.left = max(space.left, containing_block_state.offset.x());
space.right = min(root_block_state.content_width - space.right, containing_block_state.offset.x() + containing_block_state.content_width);
return space.right - space.left;
}
void InlineFormattingContext::run(Box const&, LayoutMode layout_mode)
{
VERIFY(containing_block().children_are_inline());
generate_line_boxes(layout_mode);
float min_line_height = containing_block().line_height();
float max_line_width = 0;
float content_height = 0;
for (auto& line_box : m_state.get(containing_block()).line_boxes) {
float max_height = min_line_height;
for (auto& fragment : line_box.fragments()) {
max_height = max(max_height, fragment.border_box_height());
}
max_line_width = max(max_line_width, line_box.width());
content_height += max_height;
}
auto& containing_block_state = m_state.get_mutable(containing_block());
if (layout_mode != LayoutMode::Default) {
containing_block_state.content_width = max_line_width;
}
containing_block_state.content_height = content_height;
}
void InlineFormattingContext::dimension_box_on_line(Box const& box, LayoutMode layout_mode)
{
auto width_of_containing_block = CSS::Length::make_px(m_state.get(containing_block()).content_width);
auto& box_state = m_state.get_mutable(box);
auto const& computed_values = box.computed_values();
box_state.margin_left = computed_values.margin().left.resolved(box, width_of_containing_block).to_px(box);
box_state.border_left = computed_values.border_left().width;
box_state.padding_left = computed_values.padding().left.resolved(box, width_of_containing_block).to_px(box);
box_state.margin_right = computed_values.margin().right.resolved(box, width_of_containing_block).to_px(box);
box_state.border_right = computed_values.border_right().width;
box_state.padding_right = computed_values.padding().right.resolved(box, width_of_containing_block).to_px(box);
box_state.padding_top = computed_values.padding().top.resolved(box, width_of_containing_block).to_px(box);
box_state.padding_bottom = computed_values.padding().bottom.resolved(box, width_of_containing_block).to_px(box);
box_state.margin_top = computed_values.margin().top.resolved(box, width_of_containing_block).to_px(box);
box_state.margin_bottom = computed_values.margin().bottom.resolved(box, width_of_containing_block).to_px(box);
if (is<ReplacedBox>(box)) {
auto& replaced = verify_cast<ReplacedBox>(box);
box_state.content_width = compute_width_for_replaced_element(m_state, replaced);
box_state.content_height = compute_height_for_replaced_element(m_state, replaced);
return;
}
if (box.is_inline_block()) {
auto const& inline_block = verify_cast<BlockContainer>(box);
auto const& containing_block_state = m_state.get(containing_block());
auto& width_value = inline_block.computed_values().width();
if (!width_value.has_value() || (width_value->is_length() && width_value->length().is_auto())) {
auto result = calculate_shrink_to_fit_widths(inline_block);
auto available_width = containing_block_state.content_width
- box_state.margin_left
- box_state.border_left
- box_state.padding_left
- box_state.padding_right
- box_state.border_right
- box_state.margin_right;
auto width = min(max(result.preferred_minimum_width, available_width), result.preferred_width);
box_state.content_width = width;
} else {
auto container_width = CSS::Length::make_px(containing_block_state.content_width);
box_state.content_width = width_value->resolved(box, container_width).to_px(inline_block);
}
auto independent_formatting_context = layout_inside(inline_block, layout_mode);
auto& height_value = inline_block.computed_values().height();
if (!height_value.has_value() || (height_value->is_length() && height_value->length().is_auto())) {
// FIXME: (10.6.6) If 'height' is 'auto', the height depends on the element's descendants per 10.6.7.
BlockFormattingContext::compute_height(inline_block, m_state);
} else {
auto container_height = CSS::Length::make_px(containing_block_state.content_height);
box_state.content_height = height_value->resolved(box, container_height).to_px(inline_block);
}
independent_formatting_context->parent_context_did_dimension_child_root_box();
return;
}
// Non-replaced, non-inline-block, box on a line!?
// I don't think we should be here. Dump the box tree so we can take a look at it.
dbgln("FIXME: I've been asked to dimension a non-replaced, non-inline-block box on a line:");
dump_tree(box);
}
void InlineFormattingContext::apply_justification_to_fragments(FormattingState::NodeState const& containing_block_state, CSS::TextJustify text_justify, LineBox& line_box, bool is_last_line)
{
switch (text_justify) {
case CSS::TextJustify::None:
return;
// FIXME: These two cases currently fall back to auto, handle them as well.
case CSS::TextJustify::InterCharacter:
case CSS::TextJustify::InterWord:
case CSS::TextJustify::Auto:
break;
}
float excess_horizontal_space = containing_block_state.content_width - line_box.width();
// Only justify the text if the excess horizontal space is less than or
// equal to 10%, or if we are not looking at the last line box.
if (is_last_line && excess_horizontal_space / containing_block_state.content_width > text_justification_threshold)
return;
float excess_horizontal_space_including_whitespace = excess_horizontal_space;
size_t whitespace_count = 0;
for (auto& fragment : line_box.fragments()) {
if (fragment.is_justifiable_whitespace()) {
++whitespace_count;
excess_horizontal_space_including_whitespace += fragment.width();
}
}
float justified_space_width = whitespace_count > 0 ? (excess_horizontal_space_including_whitespace / static_cast<float>(whitespace_count)) : 0;
// This is the amount that each fragment will be offset by. If a whitespace
// fragment is shorter than the justified space width, it increases to push
// subsequent fragments, and decreases to pull them back otherwise.
float running_diff = 0;
for (size_t i = 0; i < line_box.fragments().size(); ++i) {
auto& fragment = line_box.fragments()[i];
auto offset = fragment.offset();
offset.translate_by(running_diff, 0);
fragment.set_offset(offset);
if (fragment.is_justifiable_whitespace()
&& fragment.width() != justified_space_width) {
running_diff += justified_space_width - fragment.width();
fragment.set_width(justified_space_width);
}
}
}
void InlineFormattingContext::generate_line_boxes(LayoutMode layout_mode)
{
auto& containing_block_state = m_state.get_mutable(containing_block());
auto& line_boxes = containing_block_state.line_boxes;
line_boxes.clear_with_capacity();
InlineLevelIterator iterator(*this, m_state, containing_block(), layout_mode);
LineBuilder line_builder(*this, m_state, layout_mode);
for (;;) {
auto item_opt = iterator.next(line_builder.available_width_for_current_line());
if (!item_opt.has_value())
break;
auto& item = item_opt.value();
// Ignore collapsible whitespace chunks at the start of line, and if the last fragment already ends in whitespace.
if (item.is_collapsible_whitespace && line_boxes.last().is_empty_or_ends_in_whitespace())
continue;
switch (item.type) {
case InlineLevelIterator::Item::Type::ForcedBreak:
line_builder.break_line();
break;
case InlineLevelIterator::Item::Type::Element: {
auto& box = verify_cast<Layout::Box>(*item.node);
line_builder.break_if_needed(layout_mode, item.border_box_width(), item.should_force_break);
line_builder.append_box(box, item.border_start + item.padding_start, item.padding_end + item.border_end, item.margin_start, item.margin_end);
break;
}
case InlineLevelIterator::Item::Type::AbsolutelyPositionedElement:
if (is<Box>(*item.node))
parent().add_absolutely_positioned_box(static_cast<Layout::Box const&>(*item.node));
break;
case InlineLevelIterator::Item::Type::Text: {
auto& text_node = verify_cast<Layout::TextNode>(*item.node);
line_builder.break_if_needed(layout_mode, item.border_box_width(), item.should_force_break);
line_builder.append_text_chunk(
text_node,
item.offset_in_node,
item.length_in_node,
item.border_start + item.padding_start,
item.padding_end + item.border_end,
item.margin_start,
item.margin_end,
item.width,
text_node.computed_values().font_size());
break;
}
}
}
for (auto& line_box : line_boxes) {
line_box.trim_trailing_whitespace();
}
line_builder.remove_last_line_if_empty();
auto const& containing_block = this->containing_block();
auto text_align = containing_block.computed_values().text_align();
auto text_justify = containing_block.computed_values().text_justify();
if (text_align == CSS::TextAlign::Justify) {
auto const& containing_block_state = m_state.get(containing_block);
for (size_t i = 0; i < line_boxes.size(); i++) {
auto& line_box = line_boxes[i];
auto is_last_line = i == line_boxes.size() - 1;
apply_justification_to_fragments(containing_block_state, text_justify, line_box, is_last_line);
}
}
}
}