LibWeb: Reorganize layout system in terms of formatting contexts

This is a first (huge) step towards modernizing the layout architecture
and bringing it closer to spec language.

Layout is now performed by a stack of formatting contexts, operating on
the box tree (or layout tree, if you will.)

There are currently three types of formatting context:

- BlockFormattingContext (BFC)
- InlineFormattingContext (IFC)
- TableFormattingContext (TFC)

Document::layout() creates the initial BlockFormattingContext (BFC)
which lays out the initial containing block (ICB), and then we recurse
through the tree, creating BFC, IFC or TFC as appropriate and handing
over control at the context boundaries.

The majority of this patch is just refactoring the old logic spread out
in LayoutBlock and LayoutTableRowGroup, and turning into these context
classes instead. A lot more cleanup will be needed.

There are many architectural wins here, the main one being that layout
is no longer performed by boxes themselves, which gives us much greater
flexibility in the outer/inner layout of a given box.
This commit is contained in:
Andreas Kling 2020-11-22 13:38:18 +01:00
parent 00aac65af5
commit e1a24edfa9
Notes: sideshowbarker 2024-07-19 01:19:42 +09:00
46 changed files with 1360 additions and 882 deletions

View file

@ -133,7 +133,10 @@ set(SOURCES
HTML/Parser/StackOfOpenElements.cpp
HighResolutionTime/Performance.cpp
InProcessWebView.cpp
Layout/BlockFormattingContext.cpp
Layout/BoxModelMetrics.cpp
Layout/FormattingContext.cpp
Layout/InlineFormattingContext.cpp
Layout/LayoutBlock.cpp
Layout/LayoutBox.cpp
Layout/LayoutBreak.cpp
@ -162,6 +165,7 @@ set(SOURCES
Layout/LayoutWidget.cpp
Layout/LineBox.cpp
Layout/LineBoxFragment.cpp
Layout/TableFormattingContext.cpp
LayoutTreeModel.cpp
Loader/FrameLoader.cpp
Loader/ImageLoader.cpp

View file

@ -44,6 +44,7 @@
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/DOM/Window.h>
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <LibWeb/HTML/HTMLFrameSetElement.h>
@ -52,6 +53,7 @@
#include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/HTMLTitleElement.h>
#include <LibWeb/InProcessWebView.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/LayoutDocument.h>
#include <LibWeb/Layout/LayoutTreeBuilder.h>
#include <LibWeb/Namespace.h>
@ -348,7 +350,10 @@ void Document::layout()
LayoutTreeBuilder tree_builder;
m_layout_root = static_ptr_cast<LayoutDocument>(tree_builder.build(*this));
}
m_layout_root->layout();
Layout::BlockFormattingContext root_formatting_context(*m_layout_root);
root_formatting_context.run(LayoutMode::Default);
m_layout_root->set_needs_display();
if (frame()->is_main_frame()) {

View file

@ -26,6 +26,10 @@
#pragma once
namespace Web {
enum class LayoutMode;
}
namespace Web::CSS {
class Selector;
class StyleProperties;
@ -146,12 +150,19 @@ class SVGPathElement;
class SVGSVGElement;
}
namespace Web::Layout {
class BlockFormattingContext;
class FormattingContext;
class InlineFormattingContext;
}
namespace Web {
class EventHandler;
class Frame;
class FrameLoader;
class InProcessWebView;
class LayoutBlock;
class LayoutBox;
class LayoutButton;
class LayoutCheckBox;
class LayoutDocument;

View file

@ -0,0 +1,632 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <LibWeb/CSS/Length.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/LayoutBlock.h>
#include <LibWeb/Layout/LayoutBox.h>
#include <LibWeb/Layout/LayoutDocument.h>
#include <LibWeb/Layout/LayoutListItem.h>
#include <LibWeb/Layout/LayoutWidget.h>
#include <LibWeb/Page/Frame.h>
namespace Web::Layout {
BlockFormattingContext::BlockFormattingContext(LayoutBox& context_box)
: FormattingContext(context_box)
{
}
BlockFormattingContext::~BlockFormattingContext()
{
}
bool BlockFormattingContext::is_initial() const
{
return context_box().is_root();
}
void BlockFormattingContext::run(LayoutMode layout_mode)
{
if (is_initial()) {
layout_initial_containing_block(layout_mode);
return;
}
// FIXME: BFC currently computes the width+height of the context box.
// This is necessary to be able to place absolutely positioned descendants.
// The same work is also done by the parent BFC for each of its blocks..
if (layout_mode == LayoutMode::Default)
compute_width(context_box());
if (context_box().children_are_inline()) {
layout_inline_children(layout_mode);
} else {
layout_block_level_children(layout_mode);
}
if (layout_mode == LayoutMode::Default)
compute_height(context_box());
// No need to layout absolute positioned boxes during shrink-to-fit layouts.
if (layout_mode == LayoutMode::Default)
layout_absolutely_positioned_descendants();
}
void BlockFormattingContext::compute_width(LayoutBox& box)
{
if (box.is_replaced()) {
// FIXME: This should not be done *by* LayoutReplaced
auto& replaced = downcast<LayoutReplaced>(box);
replaced.prepare_for_replaced_layout();
auto width = replaced.calculate_width();
replaced.set_width(width);
return;
}
if (box.is_absolutely_positioned()) {
compute_width_for_absolutely_positioned_block(box);
return;
}
auto& style = box.style();
float width_of_containing_block = box.width_of_logical_containing_block();
auto zero_value = CSS::Length::make_px(0);
auto margin_left = CSS::Length::make_auto();
auto margin_right = CSS::Length::make_auto();
const auto padding_left = style.padding().left.resolved_or_zero(box, width_of_containing_block);
const auto padding_right = style.padding().right.resolved_or_zero(box, width_of_containing_block);
auto try_compute_width = [&](const auto& a_width) {
CSS::Length width = a_width;
margin_left = style.margin().left.resolved_or_zero(box, width_of_containing_block);
margin_right = style.margin().right.resolved_or_zero(box, width_of_containing_block);
float total_px = style.border_left().width + style.border_right().width;
for (auto& value : { margin_left, padding_left, width, padding_right, margin_right }) {
total_px += value.to_px(box);
}
if (!box.is_replaced() && !box.is_inline()) {
// 10.3.3 Block-level, non-replaced elements in normal flow
// If 'width' is not 'auto' and 'border-left-width' + 'padding-left' + 'width' + 'padding-right' + 'border-right-width' (plus any of 'margin-left' or 'margin-right' that are not 'auto') is larger than the width of the containing block, then any 'auto' values for 'margin-left' or 'margin-right' are, for the following rules, treated as zero.
if (width.is_auto() && total_px > width_of_containing_block) {
if (margin_left.is_auto())
margin_left = zero_value;
if (margin_right.is_auto())
margin_right = zero_value;
}
// 10.3.3 cont'd.
auto underflow_px = width_of_containing_block - total_px;
if (width.is_auto()) {
if (margin_left.is_auto())
margin_left = zero_value;
if (margin_right.is_auto())
margin_right = zero_value;
if (underflow_px >= 0) {
width = CSS::Length(underflow_px, CSS::Length::Type::Px);
} else {
width = zero_value;
margin_right = CSS::Length(margin_right.to_px(box) + underflow_px, CSS::Length::Type::Px);
}
} else {
if (!margin_left.is_auto() && !margin_right.is_auto()) {
margin_right = CSS::Length(margin_right.to_px(box) + underflow_px, CSS::Length::Type::Px);
} else if (!margin_left.is_auto() && margin_right.is_auto()) {
margin_right = CSS::Length(underflow_px, CSS::Length::Type::Px);
} else if (margin_left.is_auto() && !margin_right.is_auto()) {
margin_left = CSS::Length(underflow_px, CSS::Length::Type::Px);
} else { // margin_left.is_auto() && margin_right.is_auto()
auto half_of_the_underflow = CSS::Length(underflow_px / 2, CSS::Length::Type::Px);
margin_left = half_of_the_underflow;
margin_right = half_of_the_underflow;
}
}
} else if (!box.is_replaced() && box.is_inline_block()) {
// 10.3.9 'Inline-block', non-replaced elements in normal flow
// A computed value of 'auto' for 'margin-left' or 'margin-right' becomes a used value of '0'.
if (margin_left.is_auto())
margin_left = zero_value;
if (margin_right.is_auto())
margin_right = zero_value;
// If 'width' is 'auto', the used value is the shrink-to-fit width as for floating elements.
if (width.is_auto()) {
// Find the available width: in this case, this is the width of the containing
// block minus the used values of 'margin-left', 'border-left-width', 'padding-left',
// 'padding-right', 'border-right-width', 'margin-right', and the widths of any relevant scroll bars.
float available_width = width_of_containing_block
- margin_left.to_px(box) - style.border_left().width - padding_left.to_px(box)
- padding_right.to_px(box) - style.border_right().width - margin_right.to_px(box);
auto result = calculate_shrink_to_fit_widths(box);
// Then the shrink-to-fit width is: min(max(preferred minimum width, available width), preferred width).
width = CSS::Length(min(max(result.preferred_minimum_width, available_width), result.preferred_width), CSS::Length::Type::Px);
}
}
return width;
};
auto specified_width = style.width().resolved_or_auto(box, width_of_containing_block);
// 1. The tentative used width is calculated (without 'min-width' and 'max-width')
auto used_width = try_compute_width(specified_width);
// 2. The tentative used width is greater than 'max-width', the rules above are applied again,
// but this time using the computed value of 'max-width' as the computed value for 'width'.
auto specified_max_width = style.max_width().resolved_or_auto(box, width_of_containing_block);
if (!specified_max_width.is_auto()) {
if (used_width.to_px(box) > specified_max_width.to_px(box)) {
used_width = try_compute_width(specified_max_width);
}
}
// 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
// but this time using the value of 'min-width' as the computed value for 'width'.
auto specified_min_width = style.min_width().resolved_or_auto(box, width_of_containing_block);
if (!specified_min_width.is_auto()) {
if (used_width.to_px(box) < specified_min_width.to_px(box)) {
used_width = try_compute_width(specified_min_width);
}
}
box.set_width(used_width.to_px(box));
box.box_model().margin.left = margin_left;
box.box_model().margin.right = margin_right;
box.box_model().border.left = CSS::Length::make_px(style.border_left().width);
box.box_model().border.right = CSS::Length::make_px(style.border_right().width);
box.box_model().padding.left = padding_left;
box.box_model().padding.right = padding_right;
}
void BlockFormattingContext::compute_width_for_absolutely_positioned_block(LayoutBox& box)
{
auto& containing_block = context_box();
auto& style = box.style();
auto zero_value = CSS::Length::make_px(0);
auto margin_left = CSS::Length::make_auto();
auto margin_right = CSS::Length::make_auto();
const auto border_left = style.border_left().width;
const auto border_right = style.border_right().width;
const auto padding_left = style.padding().left.resolved(zero_value, box, containing_block.width());
const auto padding_right = style.padding().right.resolved(zero_value, box, containing_block.width());
auto try_compute_width = [&](const auto& a_width) {
margin_left = style.margin().left.resolved(zero_value, box, containing_block.width());
margin_right = style.margin().right.resolved(zero_value, box, containing_block.width());
auto left = style.offset().left.resolved_or_auto(box, containing_block.width());
auto right = style.offset().right.resolved_or_auto(box, containing_block.width());
auto width = a_width;
auto solve_for_left = [&] {
return CSS::Length(containing_block.width() - margin_left.to_px(box) - border_left - padding_left.to_px(box) - width.to_px(box) - padding_right.to_px(box) - border_right - margin_right.to_px(box) - right.to_px(box), CSS::Length::Type::Px);
};
auto solve_for_width = [&] {
return CSS::Length(containing_block.width() - left.to_px(box) - margin_left.to_px(box) - border_left - padding_left.to_px(box) - padding_right.to_px(box) - border_right - margin_right.to_px(box) - right.to_px(box), CSS::Length::Type::Px);
};
auto solve_for_right = [&] {
return CSS::Length(containing_block.width() - left.to_px(box) - margin_left.to_px(box) - border_left - padding_left.to_px(box) - width.to_px(box) - padding_right.to_px(box) - border_right - margin_right.to_px(box), CSS::Length::Type::Px);
};
// If all three of 'left', 'width', and 'right' are 'auto':
if (left.is_auto() && width.is_auto() && right.is_auto()) {
// First set any 'auto' values for 'margin-left' and 'margin-right' to 0.
if (margin_left.is_auto())
margin_left = CSS::Length::make_px(0);
if (margin_right.is_auto())
margin_right = CSS::Length::make_px(0);
// Then, if the 'direction' property of the element establishing the static-position containing block
// is 'ltr' set 'left' to the static position and apply rule number three below;
// otherwise, set 'right' to the static position and apply rule number one below.
// FIXME: This is very hackish.
left = CSS::Length::make_px(0);
goto Rule3;
}
if (!left.is_auto() && !width.is_auto() && !right.is_auto()) {
// FIXME: This should be solved in a more complicated way.
return width;
}
if (margin_left.is_auto())
margin_left = CSS::Length::make_px(0);
if (margin_right.is_auto())
margin_right = CSS::Length::make_px(0);
// 1. 'left' and 'width' are 'auto' and 'right' is not 'auto',
// then the width is shrink-to-fit. Then solve for 'left'
if (left.is_auto() && width.is_auto() && !right.is_auto()) {
auto result = calculate_shrink_to_fit_widths(box);
solve_for_left();
auto available_width = solve_for_width();
width = CSS::Length(min(max(result.preferred_minimum_width, available_width.to_px(box)), result.preferred_width), CSS::Length::Type::Px);
}
// 2. 'left' and 'right' are 'auto' and 'width' is not 'auto',
// then if the 'direction' property of the element establishing
// the static-position containing block is 'ltr' set 'left'
// to the static position, otherwise set 'right' to the static position.
// Then solve for 'left' (if 'direction is 'rtl') or 'right' (if 'direction' is 'ltr').
else if (left.is_auto() && right.is_auto() && !width.is_auto()) {
// FIXME: Check direction
// FIXME: Use the static-position containing block
left = zero_value;
right = solve_for_right();
}
// 3. 'width' and 'right' are 'auto' and 'left' is not 'auto',
// then the width is shrink-to-fit. Then solve for 'right'
else if (width.is_auto() && right.is_auto() && !left.is_auto()) {
Rule3:
auto result = calculate_shrink_to_fit_widths(box);
right = solve_for_right();
auto available_width = solve_for_width();
width = CSS::Length(min(max(result.preferred_minimum_width, available_width.to_px(box)), result.preferred_width), CSS::Length::Type::Px);
}
// 4. 'left' is 'auto', 'width' and 'right' are not 'auto', then solve for 'left'
else if (left.is_auto() && !width.is_auto() && !right.is_auto()) {
left = solve_for_left();
}
// 5. 'width' is 'auto', 'left' and 'right' are not 'auto', then solve for 'width'
else if (width.is_auto() && !left.is_auto() && !right.is_auto()) {
width = solve_for_width();
}
// 6. 'right' is 'auto', 'left' and 'width' are not 'auto', then solve for 'right'
else if (right.is_auto() && !left.is_auto() && !width.is_auto()) {
right = solve_for_right();
}
return width;
};
auto specified_width = style.width().resolved_or_auto(box, containing_block.width());
// 1. The tentative used width is calculated (without 'min-width' and 'max-width')
auto used_width = try_compute_width(specified_width);
// 2. The tentative used width is greater than 'max-width', the rules above are applied again,
// but this time using the computed value of 'max-width' as the computed value for 'width'.
auto specified_max_width = style.max_width().resolved_or_auto(box, containing_block.width());
if (!specified_max_width.is_auto()) {
if (used_width.to_px(box) > specified_max_width.to_px(box)) {
used_width = try_compute_width(specified_max_width);
}
}
// 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
// but this time using the value of 'min-width' as the computed value for 'width'.
auto specified_min_width = style.min_width().resolved_or_auto(box, containing_block.width());
if (!specified_min_width.is_auto()) {
if (used_width.to_px(box) < specified_min_width.to_px(box)) {
used_width = try_compute_width(specified_min_width);
}
}
box.set_width(used_width.to_px(box));
box.box_model().margin.left = margin_left;
box.box_model().margin.right = margin_right;
box.box_model().border.left = CSS::Length::make_px(border_left);
box.box_model().border.right = CSS::Length::make_px(border_right);
box.box_model().padding.left = padding_left;
box.box_model().padding.right = padding_right;
}
void BlockFormattingContext::compute_height(LayoutBox& box)
{
if (box.is_replaced()) {
// FIXME: This should not be done *by* LayoutReplaced
auto height = downcast<LayoutReplaced>(box).calculate_height();
box.set_height(height);
return;
}
auto& style = box.style();
auto& containing_block = context_box();
CSS::Length specified_height;
if (style.height().is_percentage() && !containing_block.style().height().is_absolute()) {
specified_height = CSS::Length::make_auto();
} else {
specified_height = style.height().resolved_or_auto(box, containing_block.height());
}
auto specified_max_height = style.max_height().resolved_or_auto(box, containing_block.height());
box.box_model().margin.top = style.margin().top.resolved_or_zero(box, containing_block.width());
box.box_model().margin.bottom = style.margin().bottom.resolved_or_zero(box, containing_block.width());
box.box_model().border.top = CSS::Length::make_px(style.border_top().width);
box.box_model().border.bottom = CSS::Length::make_px(style.border_bottom().width);
box.box_model().padding.top = style.padding().top.resolved_or_zero(box, containing_block.width());
box.box_model().padding.bottom = style.padding().bottom.resolved_or_zero(box, containing_block.width());
if (!specified_height.is_auto()) {
float used_height = specified_height.to_px(box);
if (!specified_max_height.is_auto())
used_height = min(used_height, specified_max_height.to_px(box));
box.set_height(used_height);
}
}
void BlockFormattingContext::layout_inline_children(LayoutMode layout_mode)
{
InlineFormattingContext context(context_box());
context.run(layout_mode);
}
void BlockFormattingContext::layout_block_level_children(LayoutMode layout_mode)
{
float content_height = 0;
float content_width = 0;
context_box().for_each_in_subtree_of_type<LayoutBox>([&](auto& box) {
if (box.is_absolutely_positioned() || box.containing_block() != &context_box())
return IterationDecision::Continue;
compute_width(box);
layout_inside(box, layout_mode);
compute_height(box);
if (box.is_replaced())
place_block_level_replaced_element_in_normal_flow(box);
else if (box.is_block())
place_block_level_non_replaced_element_in_normal_flow(box);
else
dbgln("FIXME: LayoutBlock::layout_contained_boxes doesn't know how to place a {}", box.class_name());
// FIXME: This should be factored differently. It's uncool that we mutate the tree *during* layout!
// Instead, we should generate the marker box during the tree build.
if (is<LayoutListItem>(box))
downcast<LayoutListItem>(box).layout_marker();
content_height = max(content_height, box.effective_offset().y() + box.height() + box.box_model().margin_box(box).bottom);
content_width = max(content_width, downcast<LayoutBox>(box).width());
return IterationDecision::Continue;
});
if (layout_mode != LayoutMode::Default) {
if (context_box().style().width().is_undefined() || context_box().style().width().is_auto())
context_box().set_width(content_width);
}
// FIXME: It's not right to always shrink-wrap the context box to the content here.
context_box().set_height(content_height);
}
void BlockFormattingContext::place_block_level_replaced_element_in_normal_flow(LayoutBox& box)
{
auto& containing_block = context_box();
ASSERT(!containing_block.is_absolutely_positioned());
auto& replaced_element_box_model = box.box_model();
replaced_element_box_model.margin.top = box.style().margin().top.resolved_or_zero(context_box(), containing_block.width());
replaced_element_box_model.margin.bottom = box.style().margin().bottom.resolved_or_zero(context_box(), containing_block.width());
replaced_element_box_model.border.top = CSS::Length::make_px(box.style().border_top().width);
replaced_element_box_model.border.bottom = CSS::Length::make_px(box.style().border_bottom().width);
replaced_element_box_model.padding.top = box.style().padding().top.resolved_or_zero(context_box(), containing_block.width());
replaced_element_box_model.padding.bottom = box.style().padding().bottom.resolved_or_zero(context_box(), containing_block.width());
float x = replaced_element_box_model.margin.left.to_px(context_box())
+ replaced_element_box_model.border.left.to_px(context_box())
+ replaced_element_box_model.padding.left.to_px(context_box())
+ replaced_element_box_model.offset.left.to_px(context_box());
float y = replaced_element_box_model.margin_box(context_box()).top + context_box().box_model().offset.top.to_px(context_box());
box.set_offset(x, y);
}
void BlockFormattingContext::place_block_level_non_replaced_element_in_normal_flow(LayoutBox& box)
{
auto zero_value = CSS::Length::make_px(0);
auto& containing_block = context_box();
auto& box_model = box.box_model();
auto& style = box.style();
box_model.margin.top = style.margin().top.resolved(zero_value, containing_block, containing_block.width());
box_model.margin.bottom = style.margin().bottom.resolved(zero_value, containing_block, containing_block.width());
box_model.border.top = CSS::Length::make_px(style.border_top().width);
box_model.border.bottom = CSS::Length::make_px(style.border_bottom().width);
box_model.padding.top = style.padding().top.resolved(zero_value, containing_block, containing_block.width());
box_model.padding.bottom = style.padding().bottom.resolved(zero_value, containing_block, containing_block.width());
float x = box_model.margin.left.to_px(containing_block)
+ box_model.border.left.to_px(containing_block)
+ box_model.padding.left.to_px(containing_block)
+ box_model.offset.left.to_px(containing_block);
if (containing_block.style().text_align() == CSS::TextAlign::VendorSpecificCenter) {
x = (containing_block.width() / 2) - box.width() / 2;
}
float y = box_model.margin_box(containing_block).top
+ box_model.offset.top.to_px(containing_block);
// NOTE: Empty (0-height) preceding siblings have their margins collapsed with *their* preceding sibling, etc.
float collapsed_bottom_margin_of_preceding_siblings = 0;
auto* relevant_sibling = box.previous_sibling_of_type<LayoutBlock>();
while (relevant_sibling != nullptr) {
if (!relevant_sibling->is_absolutely_positioned() && !relevant_sibling->is_floating()) {
collapsed_bottom_margin_of_preceding_siblings = max(collapsed_bottom_margin_of_preceding_siblings, relevant_sibling->box_model().margin.bottom.to_px(*relevant_sibling));
if (relevant_sibling->height() > 0)
break;
}
relevant_sibling = relevant_sibling->previous_sibling();
}
if (relevant_sibling) {
y += relevant_sibling->effective_offset().y() + relevant_sibling->height() + relevant_sibling->box_model().padding.bottom.to_px(*relevant_sibling);
// Collapse top margin with bottom margin of preceding siblings if needed
float my_margin_top = box_model.margin.top.to_px(containing_block);
if (my_margin_top < 0 || collapsed_bottom_margin_of_preceding_siblings < 0) {
// Negative margins present.
float largest_negative_margin = -min(my_margin_top, collapsed_bottom_margin_of_preceding_siblings);
float largest_positive_margin = (my_margin_top < 0 && collapsed_bottom_margin_of_preceding_siblings < 0) ? 0 : max(my_margin_top, collapsed_bottom_margin_of_preceding_siblings);
float final_margin = largest_positive_margin - largest_negative_margin;
y += final_margin - my_margin_top;
} else if (collapsed_bottom_margin_of_preceding_siblings > my_margin_top) {
// Sibling's margin is larger than mine, adjust so we use sibling's.
y += collapsed_bottom_margin_of_preceding_siblings - my_margin_top;
}
}
box.set_offset(x, y);
}
void BlockFormattingContext::layout_initial_containing_block(LayoutMode layout_mode)
{
auto viewport_rect = context_box().frame().viewport_rect();
auto& icb = downcast<LayoutDocument>(context_box());
icb.build_stacking_context_tree();
icb.set_width(viewport_rect.width());
layout_block_level_children(layout_mode);
ASSERT(!icb.children_are_inline());
// FIXME: The ICB should have the height of the viewport.
// Instead of auto-sizing the ICB, we should spill into overflow.
float lowest_bottom = 0;
icb.for_each_child_of_type<LayoutBox>([&](auto& child) {
lowest_bottom = max(lowest_bottom, child.absolute_rect().bottom());
});
icb.set_height(lowest_bottom);
// No need to layout absolute positioned boxes during shrink-to-fit layouts.
if (layout_mode == LayoutMode::Default)
layout_absolutely_positioned_descendants();
// FIXME: This is a total hack. Make sure any GUI::Widgets are moved into place after layout.
// We should stop embedding GUI::Widgets entirely, since that won't work out-of-process.
icb.for_each_in_subtree_of_type<LayoutWidget>([&](auto& widget) {
widget.update_widget();
return IterationDecision::Continue;
});
}
void BlockFormattingContext::layout_absolutely_positioned_descendants()
{
context_box().for_each_in_subtree_of_type<LayoutBox>([&](auto& box) {
if (box.is_absolutely_positioned() && box.containing_block() == &context_box()) {
layout_absolutely_positioned_descendant(box);
}
return IterationDecision::Continue;
});
}
void BlockFormattingContext::layout_absolutely_positioned_descendant(LayoutBox& box)
{
auto& containing_block = context_box();
auto& box_model = box.box_model();
auto zero_value = CSS::Length::make_px(0);
auto specified_width = box.style().width().resolved_or_auto(box, containing_block.width());
compute_width(box);
layout_inside(box, LayoutMode::Default);
compute_height(box);
box_model.margin.left = box.style().margin().left.resolved_or_auto(box, containing_block.width());
box_model.margin.top = box.style().margin().top.resolved_or_auto(box, containing_block.height());
box_model.margin.right = box.style().margin().right.resolved_or_auto(box, containing_block.width());
box_model.margin.bottom = box.style().margin().bottom.resolved_or_auto(box, containing_block.height());
box_model.border.left = CSS::Length::make_px(box.style().border_left().width);
box_model.border.right = CSS::Length::make_px(box.style().border_right().width);
box_model.border.top = CSS::Length::make_px(box.style().border_top().width);
box_model.border.bottom = CSS::Length::make_px(box.style().border_bottom().width);
box_model.offset.left = box.style().offset().left.resolved_or_auto(box, containing_block.width());
box_model.offset.top = box.style().offset().top.resolved_or_auto(box, containing_block.height());
box_model.offset.right = box.style().offset().right.resolved_or_auto(box, containing_block.width());
box_model.offset.bottom = box.style().offset().bottom.resolved_or_auto(box, containing_block.height());
if (box_model.offset.left.is_auto() && specified_width.is_auto() && box_model.offset.right.is_auto()) {
if (box_model.margin.left.is_auto())
box_model.margin.left = zero_value;
if (box_model.margin.right.is_auto())
box_model.margin.right = zero_value;
}
Gfx::FloatPoint used_offset;
if (!box_model.offset.left.is_auto()) {
float x_offset = box_model.offset.left.to_px(box)
+ box_model.border_box(box).left;
used_offset.set_x(x_offset + box_model.margin.left.to_px(box));
} else if (!box_model.offset.right.is_auto()) {
float x_offset = 0
- box_model.offset.right.to_px(box)
- box_model.border_box(box).right;
used_offset.set_x(containing_block.width() + x_offset - box.width() - box_model.margin.right.to_px(box));
} else {
float x_offset = box_model.margin_box(box).left;
used_offset.set_x(x_offset);
}
if (!box_model.offset.top.is_auto()) {
float y_offset = box_model.offset.top.to_px(box)
+ box_model.border_box(box).top;
used_offset.set_y(y_offset + box_model.margin.top.to_px(box));
} else if (!box_model.offset.bottom.is_auto()) {
float y_offset = 0
- box_model.offset.bottom.to_px(box)
- box_model.border_box(box).bottom;
used_offset.set_y(containing_block.height() + y_offset - box.height() - box_model.margin.bottom.to_px(box));
} else {
float y_offset = box_model.margin_box(box).top;
used_offset.set_y(y_offset);
}
box.set_offset(used_offset);
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibWeb/Forward.h>
#include <LibWeb/Layout/FormattingContext.h>
namespace Web::Layout {
class BlockFormattingContext : public FormattingContext {
public:
explicit BlockFormattingContext(LayoutBox& containing_block);
~BlockFormattingContext();
virtual void run(LayoutMode) override;
bool is_initial() const;
protected:
void compute_width(LayoutBox&);
void compute_height(LayoutBox&);
private:
void compute_width_for_absolutely_positioned_block(LayoutBox&);
void layout_initial_containing_block(LayoutMode);
void layout_block_level_children(LayoutMode);
void layout_inline_children(LayoutMode);
void layout_absolutely_positioned_descendants();
void place_block_level_replaced_element_in_normal_flow(LayoutBox&);
void place_block_level_non_replaced_element_in_normal_flow(LayoutBox&);
void layout_absolutely_positioned_descendant(LayoutBox&);
};
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/FormattingContext.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/LayoutBox.h>
#include <LibWeb/Layout/TableFormattingContext.h>
namespace Web::Layout {
FormattingContext::FormattingContext(LayoutBox& context_box)
: m_context_box(context_box)
{
}
FormattingContext::~FormattingContext()
{
}
void FormattingContext::layout_inside(LayoutBox& box, LayoutMode layout_mode)
{
if (box.is_table()) {
TableFormattingContext context(box);
context.run(layout_mode);
} else if (box.children_are_inline()) {
InlineFormattingContext context(box);
context.run(layout_mode);
} else {
BlockFormattingContext context(box);
context.run(layout_mode);
}
}
static float greatest_child_width(const LayoutBox& box)
{
float max_width = 0;
if (box.children_are_inline()) {
for (auto& child : box.line_boxes()) {
max_width = max(max_width, child.width());
}
} else {
box.for_each_child_of_type<LayoutBox>([&](auto& child) {
max_width = max(max_width, child.width());
});
}
return max_width;
}
FormattingContext::ShrinkToFitResult FormattingContext::calculate_shrink_to_fit_widths(LayoutBox& box)
{
// Calculate the preferred width by formatting the content without breaking lines
// other than where explicit line breaks occur.
layout_inside(box, LayoutMode::OnlyRequiredLineBreaks);
float preferred_width = greatest_child_width(box);
// Also calculate the preferred minimum width, e.g., by trying all possible line breaks.
// CSS 2.2 does not define the exact algorithm.
layout_inside(box, LayoutMode::AllPossibleLineBreaks);
float preferred_minimum_width = greatest_child_width(box);
return { preferred_width, preferred_minimum_width };
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibWeb/Forward.h>
namespace Web::Layout {
class FormattingContext {
public:
virtual void run(LayoutMode) = 0;
LayoutBox& context_box() { return m_context_box; }
const LayoutBox& context_box() const { return m_context_box; }
protected:
FormattingContext(LayoutBox&);
virtual ~FormattingContext();
static void layout_inside(LayoutBox&, LayoutMode);
struct ShrinkToFitResult {
float preferred_width { 0 };
float preferred_minimum_width { 0 };
};
ShrinkToFitResult calculate_shrink_to_fit_widths(LayoutBox&);
LayoutBox& m_context_box;
};
}

View file

@ -0,0 +1,197 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <LibWeb/CSS/Length.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/Dump.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/LayoutBlock.h>
#include <LibWeb/Layout/LayoutBox.h>
#include <LibWeb/Layout/LayoutInline.h>
#include <LibWeb/Layout/LayoutReplaced.h>
namespace Web::Layout {
InlineFormattingContext::InlineFormattingContext(LayoutBox& containing_block)
: FormattingContext(containing_block)
{
}
InlineFormattingContext::~InlineFormattingContext()
{
}
void InlineFormattingContext::run(LayoutMode layout_mode)
{
auto& containing_block = downcast<LayoutBlock>(context_box());
ASSERT(containing_block.children_are_inline());
containing_block.line_boxes().clear();
containing_block.for_each_child([&](auto& child) {
ASSERT(child.is_inline());
if (child.is_absolutely_positioned())
return;
child.split_into_lines(containing_block, layout_mode);
});
for (auto& line_box : containing_block.line_boxes()) {
line_box.trim_trailing_whitespace();
}
// If there's an empty line box at the bottom, just remove it instead of giving it height.
if (!containing_block.line_boxes().is_empty() && containing_block.line_boxes().last().fragments().is_empty())
containing_block.line_boxes().take_last();
auto text_align = containing_block.style().text_align();
float min_line_height = containing_block.specified_style().line_height(containing_block);
float line_spacing = min_line_height - containing_block.specified_style().font().glyph_height();
float content_height = 0;
float max_linebox_width = 0;
for (auto& line_box : containing_block.line_boxes()) {
float max_height = min_line_height;
for (auto& fragment : line_box.fragments()) {
max_height = max(max_height, fragment.height());
}
float x_offset = 0;
float excess_horizontal_space = (float)containing_block.width() - line_box.width();
switch (text_align) {
case CSS::TextAlign::Center:
case CSS::TextAlign::VendorSpecificCenter:
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;
}
float excess_horizontal_space_including_whitespace = excess_horizontal_space;
int whitespace_count = 0;
if (text_align == CSS::TextAlign::Justify) {
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 ? (excess_horizontal_space_including_whitespace / (float)whitespace_count) : 0;
for (size_t i = 0; i < line_box.fragments().size(); ++i) {
auto& fragment = line_box.fragments()[i];
// Vertically align everyone's bottom to the line.
// FIXME: Support other kinds of vertical alignment.
fragment.set_offset({ roundf(x_offset + fragment.offset().x()), content_height + (max_height - fragment.height()) - (line_spacing / 2) });
if (text_align == CSS::TextAlign::Justify
&& fragment.is_justifiable_whitespace()
&& fragment.width() != justified_space_width) {
float diff = justified_space_width - fragment.width();
fragment.set_width(justified_space_width);
// Shift subsequent sibling fragments to the right to adjust for change in width.
for (size_t j = i + 1; j < line_box.fragments().size(); ++j) {
auto offset = line_box.fragments()[j].offset();
offset.move_by(diff, 0);
line_box.fragments()[j].set_offset(offset);
}
}
if (fragment.layout_node().is_box())
dimension_box_on_line(const_cast<LayoutBox&>(downcast<LayoutBox>(fragment.layout_node())), layout_mode);
float final_line_box_width = 0;
for (auto& fragment : line_box.fragments())
final_line_box_width += fragment.width();
line_box.m_width = final_line_box_width;
max_linebox_width = max(max_linebox_width, final_line_box_width);
}
content_height += max_height;
}
if (layout_mode != LayoutMode::Default) {
containing_block.set_width(max_linebox_width);
}
containing_block.set_height(content_height);
}
void InlineFormattingContext::dimension_box_on_line(LayoutBox& box, LayoutMode layout_mode)
{
auto& containing_block = downcast<LayoutBlock>(context_box());
if (box.is_replaced()) {
auto& replaced = const_cast<LayoutReplaced&>(downcast<LayoutReplaced>(box));
replaced.set_width(replaced.calculate_width());
replaced.set_height(replaced.calculate_height());
return;
}
if (box.is_inline_block()) {
auto& inline_block = const_cast<LayoutBlock&>(downcast<LayoutBlock>(box));
if (inline_block.style().width().is_undefined_or_auto()) {
auto result = calculate_shrink_to_fit_widths(inline_block);
// FIXME: (10.3.5) find the available width: in this case, this is the width of the containing
// block minus the used values of 'margin-left', 'border-left-width', 'padding-left',
// 'padding-right', 'border-right-width', 'margin-right', and the widths of any
// relevant scroll bars.
auto available_width = containing_block.width();
auto width = min(max(result.preferred_minimum_width, available_width), result.preferred_width);
inline_block.set_width(width);
} else {
inline_block.set_width(inline_block.style().width().to_px(inline_block));
}
FormattingContext::layout_inside(inline_block, layout_mode);
if (inline_block.style().height().is_undefined_or_auto()) {
// FIXME: (10.6.6) If 'height' is 'auto', the height depends on the element's descendants per 10.6.7.
} else {
inline_block.set_height(inline_block.style().height().to_px(inline_block));
}
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);
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibWeb/Forward.h>
#include <LibWeb/Layout/FormattingContext.h>
namespace Web::Layout {
class InlineFormattingContext final : public FormattingContext {
public:
InlineFormattingContext(LayoutBox& containing_block);
~InlineFormattingContext();
virtual void run(LayoutMode) override;
private:
void dimension_box_on_line(LayoutBox&, LayoutMode);
};
}

View file

@ -56,642 +56,6 @@ LayoutNode& LayoutBlock::inline_wrapper()
return *last_child();
}
void LayoutBlock::layout(LayoutMode layout_mode)
{
compute_width();
layout_inside(layout_mode);
compute_height();
layout_absolutely_positioned_descendants();
}
void LayoutBlock::layout_absolutely_positioned_descendant(LayoutBox& box)
{
box.layout(LayoutMode::Default);
auto& box_model = box.box_model();
auto zero_value = CSS::Length::make_px(0);
auto specified_width = box.style().width().resolved_or_auto(box, width());
box_model.margin.left = box.style().margin().left.resolved_or_auto(box, width());
box_model.margin.top = box.style().margin().top.resolved_or_auto(box, height());
box_model.margin.right = box.style().margin().right.resolved_or_auto(box, width());
box_model.margin.bottom = box.style().margin().bottom.resolved_or_auto(box, height());
box_model.border.left = CSS::Length::make_px(box.style().border_left().width);
box_model.border.right = CSS::Length::make_px(box.style().border_right().width);
box_model.border.top = CSS::Length::make_px(box.style().border_top().width);
box_model.border.bottom = CSS::Length::make_px(box.style().border_bottom().width);
box_model.offset.left = box.style().offset().left.resolved_or_auto(box, width());
box_model.offset.top = box.style().offset().top.resolved_or_auto(box, height());
box_model.offset.right = box.style().offset().right.resolved_or_auto(box, width());
box_model.offset.bottom = box.style().offset().bottom.resolved_or_auto(box, height());
if (box_model.offset.left.is_auto() && specified_width.is_auto() && box_model.offset.right.is_auto()) {
if (box_model.margin.left.is_auto())
box_model.margin.left = zero_value;
if (box_model.margin.right.is_auto())
box_model.margin.right = zero_value;
}
Gfx::FloatPoint used_offset;
if (!box_model.offset.left.is_auto()) {
float x_offset = box_model.offset.left.to_px(box)
+ box_model.border_box(box).left;
used_offset.set_x(x_offset + box_model.margin.left.to_px(box));
} else if (!box_model.offset.right.is_auto()) {
float x_offset = 0
- box_model.offset.right.to_px(box)
- box_model.border_box(box).right;
used_offset.set_x(width() + x_offset - box.width() - box_model.margin.right.to_px(box));
} else {
float x_offset = box_model.margin_box(box).left;
used_offset.set_x(x_offset);
}
if (!box_model.offset.top.is_auto()) {
float y_offset = box_model.offset.top.to_px(box)
+ box_model.border_box(box).top;
used_offset.set_y(y_offset + box_model.margin.top.to_px(box));
} else if (!box_model.offset.bottom.is_auto()) {
float y_offset = 0
- box_model.offset.bottom.to_px(box)
- box_model.border_box(box).bottom;
used_offset.set_y(height() + y_offset - box.height() - box_model.margin.bottom.to_px(box));
} else {
float y_offset = box_model.margin_box(box).top;
used_offset.set_y(y_offset);
}
box.set_offset(used_offset);
}
void LayoutBlock::layout_inside(LayoutMode layout_mode)
{
if (children_are_inline())
layout_inline_children(layout_mode);
else
layout_contained_boxes(layout_mode);
}
void LayoutBlock::layout_absolutely_positioned_descendants()
{
for_each_in_subtree_of_type<LayoutBox>([&](auto& box) {
if (box.is_absolutely_positioned() && box.containing_block() == this) {
layout_absolutely_positioned_descendant(box);
}
return IterationDecision::Continue;
});
}
void LayoutBlock::layout_contained_boxes(LayoutMode layout_mode)
{
float content_height = 0;
float content_width = 0;
for_each_in_subtree_of_type<LayoutBox>([&](auto& box) {
if (box.is_absolutely_positioned() || box.containing_block() != this)
return IterationDecision::Continue;
box.layout(layout_mode);
if (box.is_replaced())
place_block_level_replaced_element_in_normal_flow(downcast<LayoutReplaced>(box));
else if (box.is_block())
place_block_level_non_replaced_element_in_normal_flow(downcast<LayoutBlock>(box));
else
dbg() << "FIXME: LayoutBlock::layout_contained_boxes doesn't know how to place a " << box.class_name();
content_height = max(content_height, box.effective_offset().y() + box.height() + box.box_model().margin_box(*this).bottom);
content_width = max(content_width, downcast<LayoutBox>(box).width());
return IterationDecision::Continue;
});
if (layout_mode != LayoutMode::Default) {
if (style().width().is_undefined() || style().width().is_auto())
set_width(content_width);
}
set_height(content_height);
}
void LayoutBlock::layout_inline_children(LayoutMode layout_mode)
{
ASSERT(children_are_inline());
m_line_boxes.clear();
for_each_child([&](auto& child) {
ASSERT(child.is_inline());
if (child.is_absolutely_positioned())
return;
child.split_into_lines(*this, layout_mode);
});
for (auto& line_box : m_line_boxes) {
line_box.trim_trailing_whitespace();
}
// If there's an empty line box at the bottom, just remove it instead of giving it height.
if (!m_line_boxes.is_empty() && m_line_boxes.last().fragments().is_empty())
m_line_boxes.take_last();
auto text_align = style().text_align();
float min_line_height = specified_style().line_height(*this);
float line_spacing = min_line_height - specified_style().font().glyph_height();
float content_height = 0;
float max_linebox_width = 0;
for (auto& line_box : m_line_boxes) {
float max_height = min_line_height;
for (auto& fragment : line_box.fragments()) {
max_height = max(max_height, fragment.height());
}
float x_offset = 0;
float excess_horizontal_space = (float)width() - line_box.width();
switch (text_align) {
case CSS::TextAlign::Center:
case CSS::TextAlign::VendorSpecificCenter:
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;
}
float excess_horizontal_space_including_whitespace = excess_horizontal_space;
int whitespace_count = 0;
if (text_align == CSS::TextAlign::Justify) {
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 ? (excess_horizontal_space_including_whitespace / (float)whitespace_count) : 0;
for (size_t i = 0; i < line_box.fragments().size(); ++i) {
auto& fragment = line_box.fragments()[i];
// Vertically align everyone's bottom to the line.
// FIXME: Support other kinds of vertical alignment.
fragment.set_offset({ roundf(x_offset + fragment.offset().x()), content_height + (max_height - fragment.height()) - (line_spacing / 2) });
if (text_align == CSS::TextAlign::Justify) {
if (fragment.is_justifiable_whitespace()) {
if (fragment.width() != justified_space_width) {
float diff = justified_space_width - fragment.width();
fragment.set_width(justified_space_width);
// Shift subsequent sibling fragments to the right to adjust for change in width.
for (size_t j = i + 1; j < line_box.fragments().size(); ++j) {
auto offset = line_box.fragments()[j].offset();
offset.move_by(diff, 0);
line_box.fragments()[j].set_offset(offset);
}
}
}
}
if (fragment.layout_node().is_inline_block()) {
auto& inline_block = const_cast<LayoutBlock&>(downcast<LayoutBlock>(fragment.layout_node()));
inline_block.set_size(fragment.size());
inline_block.layout(layout_mode);
}
float final_line_box_width = 0;
for (auto& fragment : line_box.fragments())
final_line_box_width += fragment.width();
line_box.m_width = final_line_box_width;
max_linebox_width = max(max_linebox_width, final_line_box_width);
}
content_height += max_height;
}
if (layout_mode != LayoutMode::Default) {
set_width(max_linebox_width);
}
set_height(content_height);
}
void LayoutBlock::compute_width_for_absolutely_positioned_block()
{
auto& containing_block = *this->containing_block();
auto zero_value = CSS::Length::make_px(0);
auto margin_left = CSS::Length::make_auto();
auto margin_right = CSS::Length::make_auto();
const auto border_left = style().border_left().width;
const auto border_right = style().border_right().width;
const auto padding_left = style().padding().left.resolved(zero_value, *this, containing_block.width());
const auto padding_right = style().padding().right.resolved(zero_value, *this, containing_block.width());
auto try_compute_width = [&](const auto& a_width) {
margin_left = style().margin().left.resolved(zero_value, *this, containing_block.width());
margin_right = style().margin().right.resolved(zero_value, *this, containing_block.width());
auto left = style().offset().left.resolved_or_auto(*this, containing_block.width());
auto right = style().offset().right.resolved_or_auto(*this, containing_block.width());
auto width = a_width;
auto solve_for_left = [&] {
return CSS::Length(containing_block.width() - margin_left.to_px(*this) - border_left - padding_left.to_px(*this) - width.to_px(*this) - padding_right.to_px(*this) - border_right - margin_right.to_px(*this) - right.to_px(*this), CSS::Length::Type::Px);
};
auto solve_for_width = [&] {
return CSS::Length(containing_block.width() - left.to_px(*this) - margin_left.to_px(*this) - border_left - padding_left.to_px(*this) - padding_right.to_px(*this) - border_right - margin_right.to_px(*this) - right.to_px(*this), CSS::Length::Type::Px);
};
auto solve_for_right = [&] {
return CSS::Length(containing_block.width() - left.to_px(*this) - margin_left.to_px(*this) - border_left - padding_left.to_px(*this) - width.to_px(*this) - padding_right.to_px(*this) - border_right - margin_right.to_px(*this), CSS::Length::Type::Px);
};
// If all three of 'left', 'width', and 'right' are 'auto':
if (left.is_auto() && width.is_auto() && right.is_auto()) {
// First set any 'auto' values for 'margin-left' and 'margin-right' to 0.
if (margin_left.is_auto())
margin_left = CSS::Length::make_px(0);
if (margin_right.is_auto())
margin_right = CSS::Length::make_px(0);
// Then, if the 'direction' property of the element establishing the static-position containing block
// is 'ltr' set 'left' to the static position and apply rule number three below;
// otherwise, set 'right' to the static position and apply rule number one below.
// FIXME: This is very hackish.
left = CSS::Length::make_px(0);
goto Rule3;
}
if (!left.is_auto() && !width.is_auto() && !right.is_auto()) {
// FIXME: This should be solved in a more complicated way.
return width;
}
if (margin_left.is_auto())
margin_left = CSS::Length::make_px(0);
if (margin_right.is_auto())
margin_right = CSS::Length::make_px(0);
// 1. 'left' and 'width' are 'auto' and 'right' is not 'auto',
// then the width is shrink-to-fit. Then solve for 'left'
if (left.is_auto() && width.is_auto() && !right.is_auto()) {
auto result = calculate_shrink_to_fit_width();
solve_for_left();
auto available_width = solve_for_width();
width = CSS::Length(min(max(result.preferred_minimum_width, available_width.to_px(*this)), result.preferred_width), CSS::Length::Type::Px);
}
// 2. 'left' and 'right' are 'auto' and 'width' is not 'auto',
// then if the 'direction' property of the element establishing
// the static-position containing block is 'ltr' set 'left'
// to the static position, otherwise set 'right' to the static position.
// Then solve for 'left' (if 'direction is 'rtl') or 'right' (if 'direction' is 'ltr').
else if (left.is_auto() && right.is_auto() && !width.is_auto()) {
// FIXME: Check direction
// FIXME: Use the static-position containing block
left = zero_value;
right = solve_for_right();
}
// 3. 'width' and 'right' are 'auto' and 'left' is not 'auto',
// then the width is shrink-to-fit. Then solve for 'right'
else if (width.is_auto() && right.is_auto() && !left.is_auto()) {
Rule3:
auto result = calculate_shrink_to_fit_width();
right = solve_for_right();
auto available_width = solve_for_width();
width = CSS::Length(min(max(result.preferred_minimum_width, available_width.to_px(*this)), result.preferred_width), CSS::Length::Type::Px);
}
// 4. 'left' is 'auto', 'width' and 'right' are not 'auto', then solve for 'left'
else if (left.is_auto() && !width.is_auto() && !right.is_auto()) {
left = solve_for_left();
}
// 5. 'width' is 'auto', 'left' and 'right' are not 'auto', then solve for 'width'
else if (width.is_auto() && !left.is_auto() && !right.is_auto()) {
width = solve_for_width();
}
// 6. 'right' is 'auto', 'left' and 'width' are not 'auto', then solve for 'right'
else if (right.is_auto() && !left.is_auto() && !width.is_auto()) {
right = solve_for_right();
}
return width;
};
auto specified_width = style().width().resolved_or_auto(*this, containing_block.width());
// 1. The tentative used width is calculated (without 'min-width' and 'max-width')
auto used_width = try_compute_width(specified_width);
// 2. The tentative used width is greater than 'max-width', the rules above are applied again,
// but this time using the computed value of 'max-width' as the computed value for 'width'.
auto specified_max_width = style().max_width().resolved_or_auto(*this, containing_block.width());
if (!specified_max_width.is_auto()) {
if (used_width.to_px(*this) > specified_max_width.to_px(*this)) {
used_width = try_compute_width(specified_max_width);
}
}
// 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
// but this time using the value of 'min-width' as the computed value for 'width'.
auto specified_min_width = style().min_width().resolved_or_auto(*this, containing_block.width());
if (!specified_min_width.is_auto()) {
if (used_width.to_px(*this) < specified_min_width.to_px(*this)) {
used_width = try_compute_width(specified_min_width);
}
}
set_width(used_width.to_px(*this));
box_model().margin.left = margin_left;
box_model().margin.right = margin_right;
box_model().border.left = CSS::Length::make_px(border_left);
box_model().border.right = CSS::Length::make_px(border_right);
box_model().padding.left = padding_left;
box_model().padding.right = padding_right;
}
float LayoutBlock::width_of_logical_containing_block() const
{
auto* containing_block = this->containing_block();
ASSERT(containing_block);
return containing_block->width();
}
void LayoutBlock::compute_width()
{
if (is_absolutely_positioned())
return compute_width_for_absolutely_positioned_block();
float width_of_containing_block = this->width_of_logical_containing_block();
auto zero_value = CSS::Length::make_px(0);
auto margin_left = CSS::Length::make_auto();
auto margin_right = CSS::Length::make_auto();
const auto padding_left = style().padding().left.resolved_or_zero(*this, width_of_containing_block);
const auto padding_right = style().padding().right.resolved_or_zero(*this, width_of_containing_block);
auto try_compute_width = [&](const auto& a_width) {
CSS::Length width = a_width;
margin_left = style().margin().left.resolved_or_zero(*this, width_of_containing_block);
margin_right = style().margin().right.resolved_or_zero(*this, width_of_containing_block);
float total_px = style().border_left().width + style().border_right().width;
for (auto& value : { margin_left, padding_left, width, padding_right, margin_right }) {
total_px += value.to_px(*this);
}
if (!is_replaced() && !is_inline()) {
// 10.3.3 Block-level, non-replaced elements in normal flow
// If 'width' is not 'auto' and 'border-left-width' + 'padding-left' + 'width' + 'padding-right' + 'border-right-width' (plus any of 'margin-left' or 'margin-right' that are not 'auto') is larger than the width of the containing block, then any 'auto' values for 'margin-left' or 'margin-right' are, for the following rules, treated as zero.
if (width.is_auto() && total_px > width_of_containing_block) {
if (margin_left.is_auto())
margin_left = zero_value;
if (margin_right.is_auto())
margin_right = zero_value;
}
// 10.3.3 cont'd.
auto underflow_px = width_of_containing_block - total_px;
if (width.is_auto()) {
if (margin_left.is_auto())
margin_left = zero_value;
if (margin_right.is_auto())
margin_right = zero_value;
if (underflow_px >= 0) {
width = CSS::Length(underflow_px, CSS::Length::Type::Px);
} else {
width = zero_value;
margin_right = CSS::Length(margin_right.to_px(*this) + underflow_px, CSS::Length::Type::Px);
}
} else {
if (!margin_left.is_auto() && !margin_right.is_auto()) {
margin_right = CSS::Length(margin_right.to_px(*this) + underflow_px, CSS::Length::Type::Px);
} else if (!margin_left.is_auto() && margin_right.is_auto()) {
margin_right = CSS::Length(underflow_px, CSS::Length::Type::Px);
} else if (margin_left.is_auto() && !margin_right.is_auto()) {
margin_left = CSS::Length(underflow_px, CSS::Length::Type::Px);
} else { // margin_left.is_auto() && margin_right.is_auto()
auto half_of_the_underflow = CSS::Length(underflow_px / 2, CSS::Length::Type::Px);
margin_left = half_of_the_underflow;
margin_right = half_of_the_underflow;
}
}
} else if (!is_replaced() && is_inline_block()) {
// 10.3.9 'Inline-block', non-replaced elements in normal flow
// A computed value of 'auto' for 'margin-left' or 'margin-right' becomes a used value of '0'.
if (margin_left.is_auto())
margin_left = zero_value;
if (margin_right.is_auto())
margin_right = zero_value;
// If 'width' is 'auto', the used value is the shrink-to-fit width as for floating elements.
if (width.is_auto()) {
// Find the available width: in this case, this is the width of the containing
// block minus the used values of 'margin-left', 'border-left-width', 'padding-left',
// 'padding-right', 'border-right-width', 'margin-right', and the widths of any relevant scroll bars.
float available_width = width_of_containing_block
- margin_left.to_px(*this) - style().border_left().width - padding_left.to_px(*this)
- padding_right.to_px(*this) - style().border_right().width - margin_right.to_px(*this);
auto result = calculate_shrink_to_fit_width();
// Then the shrink-to-fit width is: min(max(preferred minimum width, available width), preferred width).
width = CSS::Length(min(max(result.preferred_minimum_width, available_width), result.preferred_width), CSS::Length::Type::Px);
}
}
return width;
};
auto specified_width = style().width().resolved_or_auto(*this, width_of_containing_block);
// 1. The tentative used width is calculated (without 'min-width' and 'max-width')
auto used_width = try_compute_width(specified_width);
// 2. The tentative used width is greater than 'max-width', the rules above are applied again,
// but this time using the computed value of 'max-width' as the computed value for 'width'.
auto specified_max_width = style().max_width().resolved_or_auto(*this, width_of_containing_block);
if (!specified_max_width.is_auto()) {
if (used_width.to_px(*this) > specified_max_width.to_px(*this)) {
used_width = try_compute_width(specified_max_width);
}
}
// 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
// but this time using the value of 'min-width' as the computed value for 'width'.
auto specified_min_width = style().min_width().resolved_or_auto(*this, width_of_containing_block);
if (!specified_min_width.is_auto()) {
if (used_width.to_px(*this) < specified_min_width.to_px(*this)) {
used_width = try_compute_width(specified_min_width);
}
}
set_width(used_width.to_px(*this));
box_model().margin.left = margin_left;
box_model().margin.right = margin_right;
box_model().border.left = CSS::Length::make_px(style().border_left().width);
box_model().border.right = CSS::Length::make_px(style().border_right().width);
box_model().padding.left = padding_left;
box_model().padding.right = padding_right;
}
void LayoutBlock::place_block_level_replaced_element_in_normal_flow(LayoutReplaced& box)
{
ASSERT(!is_absolutely_positioned());
auto& containing_block = *this;
auto& replaced_element_box_model = box.box_model();
replaced_element_box_model.margin.top = box.style().margin().top.resolved_or_zero(*this, containing_block.width());
replaced_element_box_model.margin.bottom = box.style().margin().bottom.resolved_or_zero(*this, containing_block.width());
replaced_element_box_model.border.top = CSS::Length::make_px(box.style().border_top().width);
replaced_element_box_model.border.bottom = CSS::Length::make_px(box.style().border_bottom().width);
replaced_element_box_model.padding.top = box.style().padding().top.resolved_or_zero(*this, containing_block.width());
replaced_element_box_model.padding.bottom = box.style().padding().bottom.resolved_or_zero(*this, containing_block.width());
float x = replaced_element_box_model.margin.left.to_px(*this)
+ replaced_element_box_model.border.left.to_px(*this)
+ replaced_element_box_model.padding.left.to_px(*this)
+ replaced_element_box_model.offset.left.to_px(*this);
float y = replaced_element_box_model.margin_box(*this).top + box_model().offset.top.to_px(*this);
box.set_offset(x, y);
}
LayoutBlock::ShrinkToFitResult LayoutBlock::calculate_shrink_to_fit_width()
{
auto greatest_child_width = [&] {
float max_width = 0;
if (children_are_inline()) {
for (auto& box : line_boxes()) {
max_width = max(max_width, box.width());
}
} else {
for_each_child([&](auto& child) {
if (child.is_box())
max_width = max(max_width, downcast<LayoutBox>(child).width());
});
}
return max_width;
};
// Calculate the preferred width by formatting the content without breaking lines
// other than where explicit line breaks occur.
layout_inside(LayoutMode::OnlyRequiredLineBreaks);
float preferred_width = greatest_child_width();
// Also calculate the preferred minimum width, e.g., by trying all possible line breaks.
// CSS 2.2 does not define the exact algorithm.
layout_inside(LayoutMode::AllPossibleLineBreaks);
float preferred_minimum_width = greatest_child_width();
return { preferred_width, preferred_minimum_width };
}
void LayoutBlock::place_block_level_non_replaced_element_in_normal_flow(LayoutBlock& block)
{
auto zero_value = CSS::Length::make_px(0);
auto& containing_block = *this;
auto& box = block.box_model();
auto& style = block.style();
box.margin.top = style.margin().top.resolved(zero_value, *this, containing_block.width());
box.margin.bottom = style.margin().bottom.resolved(zero_value, *this, containing_block.width());
box.border.top = CSS::Length::make_px(style.border_top().width);
box.border.bottom = CSS::Length::make_px(style.border_bottom().width);
box.padding.top = style.padding().top.resolved(zero_value, *this, containing_block.width());
box.padding.bottom = style.padding().bottom.resolved(zero_value, *this, containing_block.width());
float x = box.margin.left.to_px(*this)
+ box.border.left.to_px(*this)
+ box.padding.left.to_px(*this)
+ box.offset.left.to_px(*this);
if (this->style().text_align() == CSS::TextAlign::VendorSpecificCenter) {
x = (containing_block.width() / 2) - block.width() / 2;
}
float y = box.margin_box(*this).top
+ box.offset.top.to_px(*this);
// NOTE: Empty (0-height) preceding siblings have their margins collapsed with *their* preceding sibling, etc.
float collapsed_bottom_margin_of_preceding_siblings = 0;
auto* relevant_sibling = block.previous_sibling();
while (relevant_sibling != nullptr) {
if (!relevant_sibling->is_absolutely_positioned() && !relevant_sibling->is_floating()) {
collapsed_bottom_margin_of_preceding_siblings = max(collapsed_bottom_margin_of_preceding_siblings, relevant_sibling->box_model().margin.bottom.to_px(*relevant_sibling));
if (relevant_sibling->height() > 0)
break;
}
relevant_sibling = relevant_sibling->previous_sibling();
}
if (relevant_sibling) {
y += relevant_sibling->effective_offset().y() + relevant_sibling->height() + relevant_sibling->box_model().padding.bottom.to_px(*relevant_sibling);
// Collapse top margin with bottom margin of preceding siblings if needed
float my_margin_top = box.margin.top.to_px(*this);
if (my_margin_top < 0 || collapsed_bottom_margin_of_preceding_siblings < 0) {
// Negative margins present.
float largest_negative_margin = -min(my_margin_top, collapsed_bottom_margin_of_preceding_siblings);
float largest_positive_margin = (my_margin_top < 0 && collapsed_bottom_margin_of_preceding_siblings < 0) ? 0 : max(my_margin_top, collapsed_bottom_margin_of_preceding_siblings);
float final_margin = largest_positive_margin - largest_negative_margin;
y += final_margin - my_margin_top;
} else if (collapsed_bottom_margin_of_preceding_siblings > my_margin_top) {
// Sibling's margin is larger than mine, adjust so we use sibling's.
y += collapsed_bottom_margin_of_preceding_siblings - my_margin_top;
}
}
block.set_offset(x, y);
}
void LayoutBlock::compute_height()
{
auto& containing_block = *this->containing_block();
CSS::Length specified_height;
if (style().height().is_percentage() && !containing_block.style().height().is_absolute()) {
specified_height = CSS::Length::make_auto();
} else {
specified_height = style().height().resolved_or_auto(*this, containing_block.height());
}
auto specified_max_height = style().max_height().resolved_or_auto(*this, containing_block.height());
box_model().margin.top = style().margin().top.resolved_or_zero(*this, containing_block.width());
box_model().margin.bottom = style().margin().bottom.resolved_or_zero(*this, containing_block.width());
box_model().border.top = CSS::Length::make_px(style().border_top().width);
box_model().border.bottom = CSS::Length::make_px(style().border_bottom().width);
box_model().padding.top = style().padding().top.resolved_or_zero(*this, containing_block.width());
box_model().padding.bottom = style().padding().bottom.resolved_or_zero(*this, containing_block.width());
if (!specified_height.is_auto()) {
float used_height = specified_height.to_px(*this);
if (!specified_max_height.is_auto())
used_height = min(used_height, specified_max_height.to_px(*this));
set_height(used_height);
}
}
void LayoutBlock::paint(PaintContext& context, PaintPhase phase)
{
if (!is_visible())
@ -767,23 +131,8 @@ NonnullRefPtr<CSS::StyleProperties> LayoutBlock::style_for_anonymous_block() con
return new_style;
}
LineBox& LayoutBlock::ensure_last_line_box()
{
if (m_line_boxes.is_empty())
return add_line_box();
return m_line_boxes.last();
}
LineBox& LayoutBlock::add_line_box()
{
m_line_boxes.append(LineBox());
return m_line_boxes.last();
}
void LayoutBlock::split_into_lines(LayoutBlock& container, LayoutMode layout_mode)
{
layout(layout_mode);
auto* line_box = &container.ensure_last_line_box();
if (layout_mode != LayoutMode::OnlyRequiredLineBreaks && line_box->width() > 0 && line_box->width() + width() > container.width()) {
line_box = &container.add_line_box();

View file

@ -38,17 +38,10 @@ public:
virtual const char* class_name() const override { return "LayoutBlock"; }
virtual void layout(LayoutMode = LayoutMode::Default) override;
virtual void paint(PaintContext&, PaintPhase) override;
virtual LayoutNode& inline_wrapper() override;
Vector<LineBox>& line_boxes() { return m_line_boxes; }
const Vector<LineBox>& line_boxes() const { return m_line_boxes; }
LineBox& ensure_last_line_box();
LineBox& add_line_box();
virtual HitTestResult hit_test(const Gfx::IntPoint&, HitTestType) const override;
LayoutBlock* previous_sibling() { return downcast<LayoutBlock>(LayoutNode::previous_sibling()); }
@ -63,36 +56,10 @@ public:
virtual void split_into_lines(LayoutBlock& container, LayoutMode) override;
void layout_inside(LayoutMode);
protected:
void compute_width();
void compute_height();
void layout_absolutely_positioned_descendants();
virtual float width_of_logical_containing_block() const;
private:
virtual bool is_block() const override { return true; }
struct ShrinkToFitResult {
float preferred_width { 0 };
float preferred_minimum_width { 0 };
};
ShrinkToFitResult calculate_shrink_to_fit_width();
void compute_width_for_absolutely_positioned_block();
void place_block_level_non_replaced_element_in_normal_flow(LayoutBlock&);
void place_block_level_replaced_element_in_normal_flow(LayoutReplaced&);
void layout_absolutely_positioned_descendant(LayoutBox&);
NonnullRefPtr<CSS::StyleProperties> style_for_anonymous_block() const;
void layout_inline_children(LayoutMode);
void layout_contained_boxes(LayoutMode);
Vector<LineBox> m_line_boxes;
};
template<typename Callback>

View file

@ -328,4 +328,24 @@ bool LayoutBox::establishes_stacking_context() const
return false;
}
LineBox& LayoutBox::ensure_last_line_box()
{
if (m_line_boxes.is_empty())
return add_line_box();
return m_line_boxes.last();
}
LineBox& LayoutBox::add_line_box()
{
m_line_boxes.append(LineBox());
return m_line_boxes.last();
}
float LayoutBox::width_of_logical_containing_block() const
{
auto* containing_block = this->containing_block();
ASSERT(containing_block);
return containing_block->width();
}
}

View file

@ -29,6 +29,7 @@
#include <AK/OwnPtr.h>
#include <LibGfx/Rect.h>
#include <LibWeb/Layout/LayoutNode.h>
#include <LibWeb/Layout/LineBox.h>
#include <LibWeb/Painting/StackingContext.h>
namespace Web {
@ -70,6 +71,14 @@ public:
virtual void paint(PaintContext&, PaintPhase) override;
Vector<LineBox>& line_boxes() { return m_line_boxes; }
const Vector<LineBox>& line_boxes() const { return m_line_boxes; }
LineBox& ensure_last_line_box();
LineBox& add_line_box();
virtual float width_of_logical_containing_block() const;
protected:
LayoutBox(DOM::Document& document, DOM::Node* node, NonnullRefPtr<CSS::StyleProperties> style)
: LayoutNodeWithStyleAndBoxModelMetrics(document, node, move(style))
@ -78,6 +87,8 @@ protected:
virtual void did_set_rect() { }
Vector<LineBox> m_line_boxes;
private:
virtual bool is_box() const final { return true; }

View file

@ -43,7 +43,7 @@ LayoutButton::~LayoutButton()
{
}
void LayoutButton::layout(LayoutMode layout_mode)
void LayoutButton::prepare_for_replaced_layout()
{
auto& font = specified_style().font();
set_intrinsic_width(font.width(node().value()) + 20);
@ -51,7 +51,6 @@ void LayoutButton::layout(LayoutMode layout_mode)
set_intrinsic_height(20);
set_has_intrinsic_height(true);
LayoutReplaced::layout(layout_mode);
}
void LayoutButton::paint(PaintContext& context, PaintPhase phase)

View file

@ -36,7 +36,7 @@ public:
LayoutButton(DOM::Document&, HTML::HTMLInputElement&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutButton() override;
virtual void layout(LayoutMode = LayoutMode::Default) override;
virtual void prepare_for_replaced_layout() override;
virtual void paint(PaintContext&, PaintPhase) override;
const HTML::HTMLInputElement& node() const { return static_cast<const HTML::HTMLInputElement&>(LayoutReplaced::node()); }

View file

@ -40,13 +40,12 @@ LayoutCanvas::~LayoutCanvas()
{
}
void LayoutCanvas::layout(LayoutMode layout_mode)
void LayoutCanvas::prepare_for_replaced_layout()
{
set_has_intrinsic_width(true);
set_has_intrinsic_height(true);
set_intrinsic_width(node().width());
set_intrinsic_height(node().height());
LayoutReplaced::layout(layout_mode);
}
void LayoutCanvas::paint(PaintContext& context, PaintPhase phase)

View file

@ -36,7 +36,7 @@ public:
LayoutCanvas(DOM::Document&, HTML::HTMLCanvasElement&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutCanvas() override;
virtual void layout(LayoutMode = LayoutMode::Default) override;
virtual void prepare_for_replaced_layout() override;
virtual void paint(PaintContext&, PaintPhase) override;
const HTML::HTMLCanvasElement& node() const { return static_cast<const HTML::HTMLCanvasElement&>(LayoutReplaced::node()); }

View file

@ -35,20 +35,15 @@ namespace Web {
LayoutCheckBox::LayoutCheckBox(DOM::Document& document, HTML::HTMLInputElement& element, NonnullRefPtr<CSS::StyleProperties> style)
: LayoutReplaced(document, element, move(style))
{
}
LayoutCheckBox::~LayoutCheckBox()
{
}
void LayoutCheckBox::layout(LayoutMode layout_mode)
{
set_has_intrinsic_width(true);
set_has_intrinsic_height(true);
set_intrinsic_width(13);
set_intrinsic_height(13);
LayoutReplaced::layout(layout_mode);
}
LayoutCheckBox::~LayoutCheckBox()
{
}
void LayoutCheckBox::paint(PaintContext& context, PaintPhase phase)

View file

@ -36,7 +36,6 @@ public:
LayoutCheckBox(DOM::Document&, HTML::HTMLInputElement&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutCheckBox() override;
virtual void layout(LayoutMode = LayoutMode::Default) override;
virtual void paint(PaintContext&, PaintPhase) override;
const HTML::HTMLInputElement& node() const { return static_cast<const HTML::HTMLInputElement&>(LayoutReplaced::node()); }

View file

@ -63,34 +63,6 @@ void LayoutDocument::build_stacking_context_tree()
});
}
void LayoutDocument::layout(LayoutMode layout_mode)
{
build_stacking_context_tree();
set_width(frame().size().width());
LayoutNode::layout(layout_mode);
ASSERT(!children_are_inline());
float lowest_bottom = 0;
for_each_child([&](auto& child) {
ASSERT(is<LayoutBlock>(child));
auto& child_block = downcast<LayoutBlock>(child);
lowest_bottom = max(lowest_bottom, child_block.absolute_rect().bottom());
});
set_height(lowest_bottom);
layout_absolutely_positioned_descendants();
// FIXME: This is a total hack. Make sure any GUI::Widgets are moved into place after layout.
// We should stop embedding GUI::Widgets entirely, since that won't work out-of-process.
for_each_in_subtree_of_type<LayoutWidget>([&](auto& widget) {
widget.update_widget();
return IterationDecision::Continue;
});
}
void LayoutDocument::did_set_viewport_rect(Badge<Frame>, const Gfx::IntRect& a_viewport_rect)
{
Gfx::FloatRect viewport_rect(a_viewport_rect.x(), a_viewport_rect.y(), a_viewport_rect.width(), a_viewport_rect.height());

View file

@ -38,7 +38,6 @@ public:
const DOM::Document& node() const { return static_cast<const DOM::Document&>(*LayoutNode::node()); }
virtual const char* class_name() const override { return "LayoutDocument"; }
virtual void layout(LayoutMode = LayoutMode::Default) override;
void paint_all_phases(PaintContext&);
virtual void paint(PaintContext&, PaintPhase) override;

View file

@ -48,7 +48,7 @@ LayoutFrame::~LayoutFrame()
{
}
void LayoutFrame::layout(LayoutMode layout_mode)
void LayoutFrame::prepare_for_replaced_layout()
{
ASSERT(node().content_frame());
@ -57,8 +57,6 @@ void LayoutFrame::layout(LayoutMode layout_mode)
// FIXME: Do proper error checking, etc.
set_intrinsic_width(node().attribute(HTML::AttributeNames::width).to_int().value_or(300));
set_intrinsic_height(node().attribute(HTML::AttributeNames::height).to_int().value_or(150));
LayoutReplaced::layout(layout_mode);
}
void LayoutFrame::paint(PaintContext& context, PaintPhase phase)

View file

@ -37,7 +37,7 @@ public:
virtual ~LayoutFrame() override;
virtual void paint(PaintContext&, PaintPhase) override;
virtual void layout(LayoutMode) override;
virtual void prepare_for_replaced_layout() override;
const HTML::HTMLIFrameElement& node() const { return downcast<HTML::HTMLIFrameElement>(LayoutReplaced::node()); }
HTML::HTMLIFrameElement& node() { return downcast<HTML::HTMLIFrameElement>(LayoutReplaced::node()); }

View file

@ -52,7 +52,7 @@ int LayoutImage::preferred_height() const
return node().attribute(HTML::AttributeNames::height).to_int().value_or(m_image_loader.height());
}
void LayoutImage::layout(LayoutMode layout_mode)
void LayoutImage::prepare_for_replaced_layout()
{
if (!m_image_loader.has_loaded_or_failed()) {
set_has_intrinsic_width(true);
@ -91,8 +91,6 @@ void LayoutImage::layout(LayoutMode layout_mode)
set_width(16);
set_height(16);
}
LayoutReplaced::layout(layout_mode);
}
void LayoutImage::paint(PaintContext& context, PaintPhase phase)

View file

@ -36,7 +36,7 @@ public:
LayoutImage(DOM::Document&, DOM::Element&, NonnullRefPtr<CSS::StyleProperties>, const ImageLoader&);
virtual ~LayoutImage() override;
virtual void layout(LayoutMode = LayoutMode::Default) override;
virtual void prepare_for_replaced_layout() override;
virtual void paint(PaintContext&, PaintPhase) override;
const DOM::Element& node() const { return static_cast<const DOM::Element&>(LayoutReplaced::node()); }

View file

@ -38,18 +38,15 @@ LayoutListItem::~LayoutListItem()
{
}
void LayoutListItem::layout(LayoutMode layout_mode)
void LayoutListItem::layout_marker()
{
if (m_marker) {
remove_child(*m_marker);
m_marker = nullptr;
}
LayoutBlock::layout(layout_mode);
if (specified_style().string_or_fallback(CSS::PropertyID::ListStyleType, "disc") == "none") {
if (specified_style().string_or_fallback(CSS::PropertyID::ListStyleType, "disc") == "none")
return;
}
if (!m_marker) {
m_marker = adopt(*new LayoutListItemMarker(document()));

View file

@ -38,12 +38,17 @@ public:
LayoutListItem(DOM::Document&, DOM::Element&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutListItem() override;
virtual void layout(LayoutMode = LayoutMode::Default) override;
void layout_marker();
private:
virtual const char* class_name() const override { return "LayoutListItem"; }
virtual bool is_list_item() const override { return true; }
RefPtr<LayoutListItemMarker> m_marker;
};
}
AK_BEGIN_TYPE_TRAITS(Web::LayoutListItem)
static bool is_type(const Web::LayoutNode& layout_node) { return layout_node.is_list_item(); }
AK_END_TYPE_TRAITS()

View file

@ -49,13 +49,6 @@ LayoutNode::~LayoutNode()
m_node->set_layout_node({}, nullptr);
}
void LayoutNode::layout(LayoutMode layout_mode)
{
for_each_child([&](auto& child) {
child.layout(layout_mode);
});
}
bool LayoutNode::can_contain_boxes_with_position_absolute() const
{
return style().position() != CSS::Position::Static || is_root();

View file

@ -40,6 +40,12 @@
namespace Web {
enum class LayoutMode {
Default,
AllPossibleLineBreaks,
OnlyRequiredLineBreaks,
};
struct HitTestResult {
RefPtr<LayoutNode> layout_node;
int index_in_node { 0 };
@ -94,6 +100,7 @@ public:
virtual bool is_break() const { return false; }
virtual bool is_check_box() const { return false; }
virtual bool is_button() const { return false; }
virtual bool is_list_item() const { return false; }
bool has_style() const { return m_has_style; }
bool is_inline() const { return m_inline; }
@ -107,14 +114,6 @@ public:
virtual void handle_mouseup(Badge<EventHandler>, const Gfx::IntPoint&, unsigned button, unsigned modifiers);
virtual void handle_mousemove(Badge<EventHandler>, const Gfx::IntPoint&, unsigned buttons, unsigned modifiers);
enum class LayoutMode {
Default,
AllPossibleLineBreaks,
OnlyRequiredLineBreaks,
};
virtual void layout(LayoutMode);
enum class PaintPhase {
Background,
Border,

View file

@ -117,20 +117,18 @@ float LayoutReplaced::calculate_height() const
return used_height;
}
void LayoutReplaced::layout(LayoutMode)
void LayoutReplaced::split_into_lines(LayoutBlock& container, LayoutMode)
{
set_width(calculate_width());
set_height(calculate_height());
}
void LayoutReplaced::split_into_lines(LayoutBlock& container, LayoutMode layout_mode)
{
layout(layout_mode);
// 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())
if (line_box->width() > 0 && line_box->width() + width > container.width())
line_box = &container.add_line_box();
line_box->add_fragment(*this, 0, 0, width(), height());
line_box->add_fragment(*this, 0, 0, width, height);
}
}

View file

@ -57,14 +57,15 @@ public:
void set_intrinsic_height(float height) { m_intrinsic_height = height; }
void set_intrinsic_ratio(float ratio) { m_intrinsic_ratio = ratio; }
protected:
virtual void layout(LayoutMode) override;
virtual void split_into_lines(LayoutBlock& container, LayoutMode) override;
private:
float calculate_width() const;
float calculate_height() const;
virtual void prepare_for_replaced_layout() { }
protected:
virtual void split_into_lines(LayoutBlock& container, LayoutMode) override;
private:
virtual const char* class_name() const override { return "LayoutReplaced"; }
bool m_has_intrinsic_width { false };

View file

@ -49,11 +49,4 @@ void LayoutSVGGraphics::before_children_paint(PaintContext& context, LayoutNode:
context.svg_context().set_stroke_width(graphics_element.stroke_width().value());
}
void LayoutSVGGraphics::layout(LayoutNode::LayoutMode mode)
{
LayoutReplaced::layout(mode);
for_each_child([&](auto& child) { child.layout(mode); });
}
}

View file

@ -37,7 +37,6 @@ public:
LayoutSVGGraphics(DOM::Document&, SVG::SVGGraphicsElement&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutSVGGraphics() override = default;
virtual void layout(LayoutMode mode) override;
virtual void before_children_paint(PaintContext& context, LayoutNode::PaintPhase phase) override;
private:

View file

@ -35,15 +35,16 @@ LayoutSVGPath::LayoutSVGPath(DOM::Document& document, SVG::SVGPathElement& eleme
{
}
void LayoutSVGPath::layout(LayoutNode::LayoutMode mode)
void LayoutSVGPath::prepare_for_replaced_layout()
{
auto& bounding_box = node().get_path().bounding_box();
set_has_intrinsic_width(true);
set_has_intrinsic_height(true);
set_intrinsic_width(bounding_box.width());
set_intrinsic_height(bounding_box.height());
// FIXME: This does not belong here! Someone at a higher level should place this box.
set_offset(bounding_box.top_left());
LayoutSVGGraphics::layout(mode);
}
void LayoutSVGPath::paint(PaintContext& context, LayoutNode::PaintPhase phase)

View file

@ -37,8 +37,8 @@ public:
SVG::SVGPathElement& node() { return downcast<SVG::SVGPathElement>(LayoutSVGGraphics::node()); }
void layout(LayoutMode mode) override;
void paint(PaintContext& context, PaintPhase phase) override;
virtual void prepare_for_replaced_layout() override;
virtual void paint(PaintContext& context, PaintPhase phase) override;
private:
virtual const char* class_name() const override { return "LayoutSVGPath"; }

View file

@ -33,13 +33,12 @@ LayoutSVGSVG::LayoutSVGSVG(DOM::Document& document, SVG::SVGSVGElement& element,
{
}
void LayoutSVGSVG::layout(LayoutMode layout_mode)
void LayoutSVGSVG::prepare_for_replaced_layout()
{
set_has_intrinsic_width(true);
set_has_intrinsic_height(true);
set_intrinsic_width(node().width());
set_intrinsic_height(node().height());
LayoutSVGGraphics::layout(layout_mode);
}
void LayoutSVGSVG::before_children_paint(PaintContext& context, LayoutNode::PaintPhase phase)

View file

@ -34,14 +34,14 @@ namespace Web {
class LayoutSVGSVG final : public LayoutSVGGraphics {
public:
LayoutSVGSVG(DOM::Document&, SVG::SVGSVGElement&, NonnullRefPtr<CSS::StyleProperties>);
~LayoutSVGSVG() override = default;
virtual ~LayoutSVGSVG() override = default;
SVG::SVGSVGElement& node() { return downcast<SVG::SVGSVGElement>(LayoutSVGGraphics::node()); }
void layout(LayoutMode = LayoutMode::Default) override;
virtual void prepare_for_replaced_layout() override;
void before_children_paint(PaintContext& context, LayoutNode::PaintPhase phase) override;
void after_children_paint(PaintContext& context, PaintPhase phase) override;
virtual void before_children_paint(PaintContext& context, LayoutNode::PaintPhase phase) override;
virtual void after_children_paint(PaintContext& context, PaintPhase phase) override;
private:
const char* class_name() const override { return "LayoutSVGSVG"; }

View file

@ -39,9 +39,4 @@ LayoutTable::~LayoutTable()
{
}
void LayoutTable::layout(LayoutMode layout_mode)
{
LayoutBlock::layout(layout_mode);
}
}

View file

@ -35,8 +35,6 @@ public:
LayoutTable(DOM::Document&, DOM::Element&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutTable() override;
virtual void layout(LayoutMode = LayoutMode::Default) override;
private:
virtual bool is_table() const override { return true; }
virtual const char* class_name() const override { return "LayoutTable"; }

View file

@ -40,57 +40,4 @@ LayoutTableRow::~LayoutTableRow()
{
}
void LayoutTableRow::layout(LayoutMode)
{
}
void LayoutTableRow::calculate_column_widths(Vector<float>& column_widths)
{
size_t column_index = 0;
auto* table = first_ancestor_of_type<LayoutTable>();
bool use_auto_layout = !table || table->style().width().is_undefined_or_auto();
for_each_child_of_type<LayoutTableCell>([&](auto& cell) {
if (use_auto_layout) {
cell.layout(LayoutMode::OnlyRequiredLineBreaks);
} else {
cell.layout(LayoutMode::Default);
}
column_widths[column_index] = max(column_widths[column_index], cell.width());
column_index += cell.colspan();
});
}
void LayoutTableRow::layout_row(const Vector<float>& column_widths)
{
size_t column_index = 0;
float tallest_cell_height = 0;
float content_width = 0;
auto* table = first_ancestor_of_type<LayoutTable>();
bool use_auto_layout = !table || table->style().width().is_undefined_or_auto();
for_each_child_of_type<LayoutTableCell>([&](auto& cell) {
cell.set_offset(effective_offset().translated(content_width, 0));
// Layout the cell contents a second time, now that we know its final width.
if (use_auto_layout) {
cell.layout_inside(LayoutMode::OnlyRequiredLineBreaks);
} else {
cell.layout_inside(LayoutMode::Default);
}
size_t cell_colspan = cell.colspan();
for (size_t i = 0; i < cell_colspan; ++i)
content_width += column_widths[column_index++];
tallest_cell_height = max(tallest_cell_height, cell.height());
});
if (use_auto_layout) {
set_width(content_width);
} else {
set_width(table->width());
}
set_height(tallest_cell_height);
}
}

View file

@ -37,11 +37,7 @@ public:
LayoutTableRow(DOM::Document&, DOM::Element&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutTableRow() override;
void layout_row(const Vector<float>& column_widths);
void calculate_column_widths(Vector<float>& column_widths);
private:
virtual void layout(LayoutMode) override;
virtual bool is_table_row() const override { return true; }
virtual const char* class_name() const override { return "LayoutTableRow"; }
};

View file

@ -53,27 +53,4 @@ size_t LayoutTableRowGroup::column_count() const
return table_column_count;
}
void LayoutTableRowGroup::layout(LayoutMode)
{
compute_width();
auto column_count = this->column_count();
Vector<float> column_widths;
column_widths.resize(column_count);
for_each_child_of_type<LayoutTableRow>([&](auto& row) {
row.calculate_column_widths(column_widths);
});
float content_height = 0;
for_each_child_of_type<LayoutTableRow>([&](auto& row) {
row.set_offset(0, content_height);
row.layout_row(column_widths);
content_height += row.height();
});
set_height(content_height);
}
}

View file

@ -35,11 +35,9 @@ public:
LayoutTableRowGroup(DOM::Document&, DOM::Element&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~LayoutTableRowGroup() override;
virtual void layout(LayoutMode = LayoutMode::Default) override;
private:
size_t column_count() const;
private:
virtual bool is_table_row_group() const override { return true; }
virtual const char* class_name() const override { return "LayoutTableRowGroup"; }
};

View file

@ -49,6 +49,7 @@ public:
private:
friend class LayoutBlock;
friend class Layout::InlineFormattingContext;
NonnullOwnPtrVector<LineBoxFragment> m_fragments;
float m_width { 0 };
};

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <LibWeb/CSS/Length.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/LayoutBlock.h>
#include <LibWeb/Layout/LayoutBox.h>
#include <LibWeb/Layout/LayoutTable.h>
#include <LibWeb/Layout/LayoutTableCell.h>
#include <LibWeb/Layout/LayoutTableRow.h>
#include <LibWeb/Layout/LayoutTableRowGroup.h>
#include <LibWeb/Layout/TableFormattingContext.h>
#include <LibWeb/Page/Frame.h>
namespace Web::Layout {
TableFormattingContext::TableFormattingContext(LayoutBox& context_box)
: BlockFormattingContext(context_box)
{
}
TableFormattingContext::~TableFormattingContext()
{
}
void TableFormattingContext::run(LayoutMode)
{
compute_width(context_box());
context_box().for_each_child_of_type<LayoutTableRowGroup>([&](auto& box) {
compute_width(box);
auto column_count = box.column_count();
Vector<float> column_widths;
column_widths.resize(column_count);
box.template for_each_child_of_type<LayoutTableRow>([&](auto& row) {
calculate_column_widths(row, column_widths);
});
float content_height = 0;
box.template for_each_child_of_type<LayoutTableRow>([&](auto& row) {
row.set_offset(0, content_height);
layout_row(row, column_widths);
content_height += row.height();
});
box.set_height(content_height);
});
compute_height(context_box());
}
void TableFormattingContext::calculate_column_widths(LayoutBox& row, Vector<float>& column_widths)
{
size_t column_index = 0;
auto* table = row.first_ancestor_of_type<LayoutTable>();
bool use_auto_layout = !table || table->style().width().is_undefined_or_auto();
row.for_each_child_of_type<LayoutTableCell>([&](auto& cell) {
compute_width(cell);
if (use_auto_layout) {
layout_inside(cell, LayoutMode::OnlyRequiredLineBreaks);
} else {
layout_inside(cell, LayoutMode::Default);
}
column_widths[column_index] = max(column_widths[column_index], cell.width());
column_index += cell.colspan();
});
}
void TableFormattingContext::layout_row(LayoutBox& row, Vector<float>& column_widths)
{
size_t column_index = 0;
float tallest_cell_height = 0;
float content_width = 0;
auto* table = row.first_ancestor_of_type<LayoutTable>();
bool use_auto_layout = !table || table->style().width().is_undefined_or_auto();
row.for_each_child_of_type<LayoutTableCell>([&](auto& cell) {
cell.set_offset(row.effective_offset().translated(content_width, 0));
// Layout the cell contents a second time, now that we know its final width.
if (use_auto_layout) {
layout_inside(cell, LayoutMode::OnlyRequiredLineBreaks);
} else {
layout_inside(cell, LayoutMode::Default);
}
size_t cell_colspan = cell.colspan();
for (size_t i = 0; i < cell_colspan; ++i)
content_width += column_widths[column_index++];
tallest_cell_height = max(tallest_cell_height, cell.height());
});
if (use_auto_layout) {
row.set_width(content_width);
} else {
row.set_width(table->width());
}
row.set_height(tallest_cell_height);
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Forward.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
namespace Web::Layout {
class TableFormattingContext final : public BlockFormattingContext {
public:
explicit TableFormattingContext(LayoutBox& containing_block);
~TableFormattingContext();
virtual void run(LayoutMode) override;
private:
void calculate_column_widths(LayoutBox& row, Vector<float>& column_widths);
void layout_row(LayoutBox& row, Vector<float>& column_widths);
};
}