diff options
author | Andreas Kling <kling@serenityos.org> | 2020-11-22 13:38:18 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-11-22 14:36:56 +0100 |
commit | e1a24edfa9e85213ea44b19f16f848841d1f4405 (patch) | |
tree | aa4b02976c59748540f84e77f6d48730064cea93 | |
parent | 00aac65af54a67ec6f67f53e49aa1514496b395c (diff) | |
download | serenity-e1a24edfa9e85213ea44b19f16f848841d1f4405.zip |
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.
46 files changed, 1359 insertions, 881 deletions
diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index b97707bdfb..a164f3046c 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index d93ed285b0..0a1fe0e506 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -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()) { diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 4ad2f6ba16..ed56031c91 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -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; diff --git a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp new file mode 100644 index 0000000000..3c22fc1d52 --- /dev/null +++ b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp @@ -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); +} + +} diff --git a/Libraries/LibWeb/Layout/BlockFormattingContext.h b/Libraries/LibWeb/Layout/BlockFormattingContext.h new file mode 100644 index 0000000000..88b2773ecd --- /dev/null +++ b/Libraries/LibWeb/Layout/BlockFormattingContext.h @@ -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&); +}; + +} diff --git a/Libraries/LibWeb/Layout/FormattingContext.cpp b/Libraries/LibWeb/Layout/FormattingContext.cpp new file mode 100644 index 0000000000..1b52502813 --- /dev/null +++ b/Libraries/LibWeb/Layout/FormattingContext.cpp @@ -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 }; +} + +} diff --git a/Libraries/LibWeb/Layout/FormattingContext.h b/Libraries/LibWeb/Layout/FormattingContext.h new file mode 100644 index 0000000000..0b681b88f0 --- /dev/null +++ b/Libraries/LibWeb/Layout/FormattingContext.h @@ -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; +}; + +} diff --git a/Libraries/LibWeb/Layout/InlineFormattingContext.cpp b/Libraries/LibWeb/Layout/InlineFormattingContext.cpp new file mode 100644 index 0000000000..5edcce8192 --- /dev/null +++ b/Libraries/LibWeb/Layout/InlineFormattingContext.cpp @@ -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); +} + +} diff --git a/Libraries/LibWeb/Layout/InlineFormattingContext.h b/Libraries/LibWeb/Layout/InlineFormattingContext.h new file mode 100644 index 0000000000..6534b6d6be --- /dev/null +++ b/Libraries/LibWeb/Layout/InlineFormattingContext.h @@ -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); +}; + +} diff --git a/Libraries/LibWeb/Layout/LayoutBlock.cpp b/Libraries/LibWeb/Layout/LayoutBlock.cpp index d0acaa4e89..e673c31a58 100644 --- a/Libraries/LibWeb/Layout/LayoutBlock.cpp +++ b/Libraries/LibWeb/Layout/LayoutBlock.cpp @@ -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(); diff --git a/Libraries/LibWeb/Layout/LayoutBlock.h b/Libraries/LibWeb/Layout/LayoutBlock.h index aa663628b7..7e5c4341c2 100644 --- a/Libraries/LibWeb/Layout/LayoutBlock.h +++ b/Libraries/LibWeb/Layout/LayoutBlock.h @@ -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> diff --git a/Libraries/LibWeb/Layout/LayoutBox.cpp b/Libraries/LibWeb/Layout/LayoutBox.cpp index 9cfed06ff4..80ee295d02 100644 --- a/Libraries/LibWeb/Layout/LayoutBox.cpp +++ b/Libraries/LibWeb/Layout/LayoutBox.cpp @@ -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(); +} + } diff --git a/Libraries/LibWeb/Layout/LayoutBox.h b/Libraries/LibWeb/Layout/LayoutBox.h index 004175ee99..f5e971e6b0 100644 --- a/Libraries/LibWeb/Layout/LayoutBox.h +++ b/Libraries/LibWeb/Layout/LayoutBox.h @@ -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; } diff --git a/Libraries/LibWeb/Layout/LayoutButton.cpp b/Libraries/LibWeb/Layout/LayoutButton.cpp index 653de61a8e..3322894f64 100644 --- a/Libraries/LibWeb/Layout/LayoutButton.cpp +++ b/Libraries/LibWeb/Layout/LayoutButton.cpp @@ -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) diff --git a/Libraries/LibWeb/Layout/LayoutButton.h b/Libraries/LibWeb/Layout/LayoutButton.h index 29153eb054..685e4d6722 100644 --- a/Libraries/LibWeb/Layout/LayoutButton.h +++ b/Libraries/LibWeb/Layout/LayoutButton.h @@ -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()); } diff --git a/Libraries/LibWeb/Layout/LayoutCanvas.cpp b/Libraries/LibWeb/Layout/LayoutCanvas.cpp index cca04e6ac0..e78b17c456 100644 --- a/Libraries/LibWeb/Layout/LayoutCanvas.cpp +++ b/Libraries/LibWeb/Layout/LayoutCanvas.cpp @@ -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) diff --git a/Libraries/LibWeb/Layout/LayoutCanvas.h b/Libraries/LibWeb/Layout/LayoutCanvas.h index b42f0f463b..09a4d634ba 100644 --- a/Libraries/LibWeb/Layout/LayoutCanvas.h +++ b/Libraries/LibWeb/Layout/LayoutCanvas.h @@ -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()); } diff --git a/Libraries/LibWeb/Layout/LayoutCheckBox.cpp b/Libraries/LibWeb/Layout/LayoutCheckBox.cpp index b63137b2b0..3195044057 100644 --- a/Libraries/LibWeb/Layout/LayoutCheckBox.cpp +++ b/Libraries/LibWeb/Layout/LayoutCheckBox.cpp @@ -36,19 +36,14 @@ 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) diff --git a/Libraries/LibWeb/Layout/LayoutCheckBox.h b/Libraries/LibWeb/Layout/LayoutCheckBox.h index 8b9644a0d6..ed495e35f7 100644 --- a/Libraries/LibWeb/Layout/LayoutCheckBox.h +++ b/Libraries/LibWeb/Layout/LayoutCheckBox.h @@ -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()); } diff --git a/Libraries/LibWeb/Layout/LayoutDocument.cpp b/Libraries/LibWeb/Layout/LayoutDocument.cpp index 772ca83df9..9a5b1b0632 100644 --- a/Libraries/LibWeb/Layout/LayoutDocument.cpp +++ b/Libraries/LibWeb/Layout/LayoutDocument.cpp @@ -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()); diff --git a/Libraries/LibWeb/Layout/LayoutDocument.h b/Libraries/LibWeb/Layout/LayoutDocument.h index 0e5d39aebe..96d6a4c425 100644 --- a/Libraries/LibWeb/Layout/LayoutDocument.h +++ b/Libraries/LibWeb/Layout/LayoutDocument.h @@ -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; diff --git a/Libraries/LibWeb/Layout/LayoutFrame.cpp b/Libraries/LibWeb/Layout/LayoutFrame.cpp index 59dec1ea02..5f9b3b0236 100644 --- a/Libraries/LibWeb/Layout/LayoutFrame.cpp +++ b/Libraries/LibWeb/Layout/LayoutFrame.cpp @@ -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) diff --git a/Libraries/LibWeb/Layout/LayoutFrame.h b/Libraries/LibWeb/Layout/LayoutFrame.h index 09a1e0e332..4dbc134fe8 100644 --- a/Libraries/LibWeb/Layout/LayoutFrame.h +++ b/Libraries/LibWeb/Layout/LayoutFrame.h @@ -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()); } diff --git a/Libraries/LibWeb/Layout/LayoutImage.cpp b/Libraries/LibWeb/Layout/LayoutImage.cpp index dc76527722..b05d528f69 100644 --- a/Libraries/LibWeb/Layout/LayoutImage.cpp +++ b/Libraries/LibWeb/Layout/LayoutImage.cpp @@ -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) diff --git a/Libraries/LibWeb/Layout/LayoutImage.h b/Libraries/LibWeb/Layout/LayoutImage.h index 6eefa962a9..740be79e8c 100644 --- a/Libraries/LibWeb/Layout/LayoutImage.h +++ b/Libraries/LibWeb/Layout/LayoutImage.h @@ -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()); } diff --git a/Libraries/LibWeb/Layout/LayoutListItem.cpp b/Libraries/LibWeb/Layout/LayoutListItem.cpp index 67e1b506f8..5b8cc5165e 100644 --- a/Libraries/LibWeb/Layout/LayoutListItem.cpp +++ b/Libraries/LibWeb/Layout/LayoutListItem.cpp @@ -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())); diff --git a/Libraries/LibWeb/Layout/LayoutListItem.h b/Libraries/LibWeb/Layout/LayoutListItem.h index bb5cff174e..2bb8dcb80f 100644 --- a/Libraries/LibWeb/Layout/LayoutListItem.h +++ b/Libraries/LibWeb/Layout/LayoutListItem.h @@ -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() diff --git a/Libraries/LibWeb/Layout/LayoutNode.cpp b/Libraries/LibWeb/Layout/LayoutNode.cpp index 3bd10abeea..2686904767 100644 --- a/Libraries/LibWeb/Layout/LayoutNode.cpp +++ b/Libraries/LibWeb/Layout/LayoutNode.cpp @@ -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(); diff --git a/Libraries/LibWeb/Layout/LayoutNode.h b/Libraries/LibWeb/Layout/LayoutNode.h index 8b955288af..12e8702c83 100644 --- a/Libraries/LibWeb/Layout/LayoutNode.h +++ b/Libraries/LibWeb/Layout/LayoutNode.h @@ -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, diff --git a/Libraries/LibWeb/Layout/LayoutReplaced.cpp b/Libraries/LibWeb/Layout/LayoutReplaced.cpp index a29d07a731..2e7e22bea5 100644 --- a/Libraries/LibWeb/Layout/LayoutReplaced.cpp +++ b/Libraries/LibWeb/Layout/LayoutReplaced.cpp @@ -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); } } diff --git a/Libraries/LibWeb/Layout/LayoutReplaced.h b/Libraries/LibWeb/Layout/LayoutReplaced.h index e77c3b866a..a474534cec 100644 --- a/Libraries/LibWeb/Layout/LayoutReplaced.h +++ b/Libraries/LibWeb/Layout/LayoutReplaced.h @@ -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; } + float calculate_width() const; + float calculate_height() const; + + virtual void prepare_for_replaced_layout() { } + 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 const char* class_name() const override { return "LayoutReplaced"; } bool m_has_intrinsic_width { false }; diff --git a/Libraries/LibWeb/Layout/LayoutSVGGraphics.cpp b/Libraries/LibWeb/Layout/LayoutSVGGraphics.cpp index 91d3c9e9b7..787b7e16fd 100644 --- a/Libraries/LibWeb/Layout/LayoutSVGGraphics.cpp +++ b/Libraries/LibWeb/Layout/LayoutSVGGraphics.cpp @@ -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); }); -} - } diff --git a/Libraries/LibWeb/Layout/LayoutSVGGraphics.h b/Libraries/LibWeb/Layout/LayoutSVGGraphics.h index 36cdd8e025..a6e3352752 100644 --- a/Libraries/LibWeb/Layout/LayoutSVGGraphics.h +++ b/Libraries/LibWeb/Layout/LayoutSVGGraphics.h @@ -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: diff --git a/Libraries/LibWeb/Layout/LayoutSVGPath.cpp b/Libraries/LibWeb/Layout/LayoutSVGPath.cpp index 2d6e554f8a..b33190c73b 100644 --- a/Libraries/LibWeb/Layout/LayoutSVGPath.cpp +++ b/Libraries/LibWeb/Layout/LayoutSVGPath.cpp @@ -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) diff --git a/Libraries/LibWeb/Layout/LayoutSVGPath.h b/Libraries/LibWeb/Layout/LayoutSVGPath.h index a8ed8c725c..0ed38f34d1 100644 --- a/Libraries/LibWeb/Layout/LayoutSVGPath.h +++ b/Libraries/LibWeb/Layout/LayoutSVGPath.h @@ -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"; } diff --git a/Libraries/LibWeb/Layout/LayoutSVGSVG.cpp b/Libraries/LibWeb/Layout/LayoutSVGSVG.cpp index c67fac9320..5b114c4b75 100644 --- a/Libraries/LibWeb/Layout/LayoutSVGSVG.cpp +++ b/Libraries/LibWeb/Layout/LayoutSVGSVG.cpp @@ -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) diff --git a/Libraries/LibWeb/Layout/LayoutSVGSVG.h b/Libraries/LibWeb/Layout/LayoutSVGSVG.h index ed0aae5328..ea321d5a36 100644 --- a/Libraries/LibWeb/Layout/LayoutSVGSVG.h +++ b/Libraries/LibWeb/Layout/LayoutSVGSVG.h @@ -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"; } diff --git a/Libraries/LibWeb/Layout/LayoutTable.cpp b/Libraries/LibWeb/Layout/LayoutTable.cpp index 19b46a1461..721aa7daa9 100644 --- a/Libraries/LibWeb/Layout/LayoutTable.cpp +++ b/Libraries/LibWeb/Layout/LayoutTable.cpp @@ -39,9 +39,4 @@ LayoutTable::~LayoutTable() { } -void LayoutTable::layout(LayoutMode layout_mode) -{ - LayoutBlock::layout(layout_mode); -} - } diff --git a/Libraries/LibWeb/Layout/LayoutTable.h b/Libraries/LibWeb/Layout/LayoutTable.h index 8134bcf792..b94874a0b1 100644 --- a/Libraries/LibWeb/Layout/LayoutTable.h +++ b/Libraries/LibWeb/Layout/LayoutTable.h @@ -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"; } diff --git a/Libraries/LibWeb/Layout/LayoutTableRow.cpp b/Libraries/LibWeb/Layout/LayoutTableRow.cpp index c2110db37e..a94e522c47 100644 --- a/Libraries/LibWeb/Layout/LayoutTableRow.cpp +++ b/Libraries/LibWeb/Layout/LayoutTableRow.cpp @@ -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); -} - } diff --git a/Libraries/LibWeb/Layout/LayoutTableRow.h b/Libraries/LibWeb/Layout/LayoutTableRow.h index 41dbe6c734..a1855abe32 100644 --- a/Libraries/LibWeb/Layout/LayoutTableRow.h +++ b/Libraries/LibWeb/Layout/LayoutTableRow.h @@ -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"; } }; diff --git a/Libraries/LibWeb/Layout/LayoutTableRowGroup.cpp b/Libraries/LibWeb/Layout/LayoutTableRowGroup.cpp index 2b12e8e523..c35b775634 100644 --- a/Libraries/LibWeb/Layout/LayoutTableRowGroup.cpp +++ b/Libraries/LibWeb/Layout/LayoutTableRowGroup.cpp @@ -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); -} - } diff --git a/Libraries/LibWeb/Layout/LayoutTableRowGroup.h b/Libraries/LibWeb/Layout/LayoutTableRowGroup.h index 5cb15a33d3..a07ced75cb 100644 --- a/Libraries/LibWeb/Layout/LayoutTableRowGroup.h +++ b/Libraries/LibWeb/Layout/LayoutTableRowGroup.h @@ -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"; } }; diff --git a/Libraries/LibWeb/Layout/LineBox.h b/Libraries/LibWeb/Layout/LineBox.h index cb28084e75..eeb177d638 100644 --- a/Libraries/LibWeb/Layout/LineBox.h +++ b/Libraries/LibWeb/Layout/LineBox.h @@ -49,6 +49,7 @@ public: private: friend class LayoutBlock; + friend class Layout::InlineFormattingContext; NonnullOwnPtrVector<LineBoxFragment> m_fragments; float m_width { 0 }; }; diff --git a/Libraries/LibWeb/Layout/TableFormattingContext.cpp b/Libraries/LibWeb/Layout/TableFormattingContext.cpp new file mode 100644 index 0000000000..a23d301d19 --- /dev/null +++ b/Libraries/LibWeb/Layout/TableFormattingContext.cpp @@ -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); +} + +} diff --git a/Libraries/LibWeb/Layout/TableFormattingContext.h b/Libraries/LibWeb/Layout/TableFormattingContext.h new file mode 100644 index 0000000000..5ab4db75eb --- /dev/null +++ b/Libraries/LibWeb/Layout/TableFormattingContext.h @@ -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); +}; + +} |