blob: e3c1536dcd32a1efed8128d332b118e28bdd9e72 [file] [log] [blame]
/*
* Copyright (C) 2018 Apple Inc. 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 APPLE INC. AND ITS 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 APPLE INC. OR ITS 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 "config.h"
#include "InlineFormattingContext.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "InlineFormattingState.h"
#include "InlineLineBreaker.h"
#include "InlineRunProvider.h"
#include "LayoutBox.h"
#include "LayoutContainer.h"
#include "LayoutInlineBox.h"
#include "LayoutInlineContainer.h"
#include "LayoutState.h"
#include "Logging.h"
#include "Textutil.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/text/TextStream.h>
namespace WebCore {
namespace Layout {
WTF_MAKE_ISO_ALLOCATED_IMPL(InlineFormattingContext);
InlineFormattingContext::InlineFormattingContext(const Box& formattingContextRoot, InlineFormattingState& formattingState)
: FormattingContext(formattingContextRoot, formattingState)
{
}
static inline const Box* nextInPreOrder(const Box& layoutBox, const Container& root)
{
const Box* nextInPreOrder = nullptr;
if (!layoutBox.establishesFormattingContext() && is<Container>(layoutBox) && downcast<Container>(layoutBox).hasInFlowOrFloatingChild())
return downcast<Container>(layoutBox).firstInFlowOrFloatingChild();
for (nextInPreOrder = &layoutBox; nextInPreOrder && nextInPreOrder != &root; nextInPreOrder = nextInPreOrder->parent()) {
if (auto* nextSibling = nextInPreOrder->nextInFlowOrFloatingSibling())
return nextSibling;
}
return nullptr;
}
void InlineFormattingContext::layout() const
{
if (!is<Container>(root()))
return;
LOG_WITH_STREAM(FormattingContextLayout, stream << "[Start] -> inline formatting context -> formatting root(" << &root() << ")");
auto& root = downcast<Container>(this->root());
auto usedValues = UsedHorizontalValues { layoutState().displayBoxForLayoutBox(root).contentBoxWidth() };
auto* layoutBox = root.firstInFlowOrFloatingChild();
// Compute width/height for non-text content and margin/border/padding for inline containers.
while (layoutBox) {
if (layoutBox->establishesFormattingContext())
layoutFormattingContextRoot(*layoutBox, usedValues);
else if (is<Container>(*layoutBox)) {
auto& inlineContainer = downcast<InlineContainer>(*layoutBox);
computeMargin(inlineContainer, usedValues);
computeBorderAndPadding(inlineContainer, usedValues);
} else if (layoutBox->isReplaced())
computeWidthAndHeightForReplacedInlineBox(*layoutBox, usedValues);
layoutBox = nextInPreOrder(*layoutBox, root);
}
InlineRunProvider inlineRunProvider;
collectInlineContent(inlineRunProvider);
LineLayout(*this).layout(inlineRunProvider);
LOG_WITH_STREAM(FormattingContextLayout, stream << "[End] -> inline formatting context -> formatting root(" << &root << ")");
}
void InlineFormattingContext::computeIntrinsicWidthConstraints() const
{
ASSERT(is<Container>(root()));
auto& layoutState = this->layoutState();
auto& root = downcast<Container>(this->root());
ASSERT(!layoutState.formattingStateForBox(root).intrinsicWidthConstraints(root));
Vector<const Box*> formattingContextRootList;
auto usedValues = UsedHorizontalValues { };
auto* layoutBox = root.firstInFlowOrFloatingChild();
while (layoutBox) {
if (layoutBox->establishesFormattingContext()) {
formattingContextRootList.append(layoutBox);
if (layoutBox->isFloatingPositioned())
computeIntrinsicWidthForFloatBox(*layoutBox);
else if (layoutBox->isInlineBlockBox())
computeIntrinsicWidthForInlineBlock(*layoutBox);
else
ASSERT_NOT_REACHED();
} else if (layoutBox->isReplaced() || is<Container>(*layoutBox)) {
computeBorderAndPadding(*layoutBox, usedValues);
// inline-block and replaced.
auto needsWidthComputation = layoutBox->isReplaced() || layoutBox->establishesFormattingContext();
if (needsWidthComputation)
computeWidthAndMargin(*layoutBox, usedValues);
else {
// Simple inline container with no intrinsic width <span>.
computeMargin(*layoutBox, usedValues);
}
}
layoutBox = nextInPreOrder(*layoutBox, root);
}
InlineRunProvider inlineRunProvider;
collectInlineContent(inlineRunProvider);
auto maximumLineWidth = [&](auto availableWidth) {
LayoutUnit maxContentLogicalRight;
auto lineBreaker = InlineLineBreaker { layoutState, formattingState().inlineContent(), inlineRunProvider.runs() };
LayoutUnit lineLogicalRight;
// Switch to the min/max formatting root width values before formatting the lines.
for (auto* formattingRoot : formattingContextRootList) {
auto intrinsicWidths = layoutState.formattingStateForBox(*formattingRoot).intrinsicWidthConstraints(*formattingRoot);
layoutState.displayBoxForLayoutBox(*formattingRoot).setContentBoxWidth(availableWidth ? intrinsicWidths->maximum : intrinsicWidths->minimum);
}
while (auto run = lineBreaker.nextRun(lineLogicalRight, availableWidth, !lineLogicalRight)) {
if (run->position == InlineLineBreaker::Run::Position::LineBegin)
lineLogicalRight = 0;
lineLogicalRight += run->width;
maxContentLogicalRight = std::max(maxContentLogicalRight, lineLogicalRight);
}
return maxContentLogicalRight;
};
auto intrinsicWidthConstraints = Geometry::constrainByMinMaxWidth(root, { maximumLineWidth(0), maximumLineWidth(LayoutUnit::max()) });
layoutState.formattingStateForBox(root).setIntrinsicWidthConstraints(root, intrinsicWidthConstraints);
}
void InlineFormattingContext::computeIntrinsicWidthForFloatBox(const Box& layoutBox) const
{
ASSERT(layoutBox.isFloatingPositioned());
auto& layoutState = this->layoutState();
auto usedHorizontalValues = UsedHorizontalValues { };
computeBorderAndPadding(layoutBox, usedHorizontalValues);
computeMargin(layoutBox, usedHorizontalValues);
layoutState.createFormattingContext(layoutBox)->computeIntrinsicWidthConstraints();
auto usedVerticalValues = UsedVerticalValues { };
auto heightAndMargin = Geometry::floatingHeightAndMargin(layoutState, layoutBox, usedVerticalValues, usedHorizontalValues);
auto& displayBox = layoutState.displayBoxForLayoutBox(layoutBox);
displayBox.setContentBoxHeight(heightAndMargin.height);
displayBox.setVerticalMargin({ heightAndMargin.nonCollapsedMargin, { } });
}
void InlineFormattingContext::computeIntrinsicWidthForInlineBlock(const Box& layoutBox) const
{
ASSERT(layoutBox.isInlineBlockBox());
auto usedValues = UsedHorizontalValues { };
computeBorderAndPadding(layoutBox, usedValues);
computeMargin(layoutBox, usedValues);
layoutState().createFormattingContext(layoutBox)->computeIntrinsicWidthConstraints();
}
void InlineFormattingContext::computeMargin(const Box& layoutBox, UsedHorizontalValues usedValues) const
{
auto computedHorizontalMargin = Geometry::computedHorizontalMargin(layoutBox, usedValues);
auto& displayBox = layoutState().displayBoxForLayoutBox(layoutBox);
displayBox.setHorizontalComputedMargin(computedHorizontalMargin);
displayBox.setHorizontalMargin({ computedHorizontalMargin.start.valueOr(0), computedHorizontalMargin.end.valueOr(0) });
}
void InlineFormattingContext::computeWidthAndMargin(const Box& layoutBox, UsedHorizontalValues usedValues) const
{
auto& layoutState = this->layoutState();
WidthAndMargin widthAndMargin;
if (layoutBox.isFloatingPositioned())
widthAndMargin = Geometry::floatingWidthAndMargin(layoutState, layoutBox, usedValues);
else if (layoutBox.isInlineBlockBox())
widthAndMargin = Geometry::inlineBlockWidthAndMargin(layoutState, layoutBox, usedValues);
else if (layoutBox.replaced())
widthAndMargin = Geometry::inlineReplacedWidthAndMargin(layoutState, layoutBox, usedValues);
else
ASSERT_NOT_REACHED();
auto& displayBox = layoutState.displayBoxForLayoutBox(layoutBox);
displayBox.setContentBoxWidth(widthAndMargin.width);
displayBox.setHorizontalMargin(widthAndMargin.usedMargin);
displayBox.setHorizontalComputedMargin(widthAndMargin.computedMargin);
}
void InlineFormattingContext::computeHeightAndMargin(const Box& layoutBox) const
{
auto& layoutState = this->layoutState();
HeightAndMargin heightAndMargin;
if (layoutBox.isFloatingPositioned())
heightAndMargin = Geometry::floatingHeightAndMargin(layoutState, layoutBox, { }, UsedHorizontalValues { layoutState.displayBoxForLayoutBox(*layoutBox.containingBlock()).contentBoxWidth() });
else if (layoutBox.isInlineBlockBox())
heightAndMargin = Geometry::inlineBlockHeightAndMargin(layoutState, layoutBox);
else if (layoutBox.replaced())
heightAndMargin = Geometry::inlineReplacedHeightAndMargin(layoutState, layoutBox, { });
else
ASSERT_NOT_REACHED();
auto& displayBox = layoutState.displayBoxForLayoutBox(layoutBox);
displayBox.setContentBoxHeight(heightAndMargin.height);
displayBox.setVerticalMargin({ heightAndMargin.nonCollapsedMargin, { } });
}
void InlineFormattingContext::layoutFormattingContextRoot(const Box& root, UsedHorizontalValues usedValues) const
{
ASSERT(root.isFloatingPositioned() || root.isInlineBlockBox());
ASSERT(usedValues.containingBlockWidth);
computeBorderAndPadding(root, usedValues);
computeWidthAndMargin(root, usedValues);
// Swich over to the new formatting context (the one that the root creates).
auto formattingContext = layoutState().createFormattingContext(root);
formattingContext->layout();
// Come back and finalize the root's height and margin.
computeHeightAndMargin(root);
// Now that we computed the root's height, we can go back and layout the out-of-flow descedants (if any).
formattingContext->layoutOutOfFlowDescendants(root);
}
void InlineFormattingContext::computeWidthAndHeightForReplacedInlineBox(const Box& layoutBox, UsedHorizontalValues usedValues) const
{
ASSERT(!layoutBox.isContainer());
ASSERT(!layoutBox.establishesFormattingContext());
ASSERT(layoutBox.replaced());
ASSERT(usedValues.containingBlockWidth);
computeBorderAndPadding(layoutBox, usedValues);
computeWidthAndMargin(layoutBox, usedValues);
computeHeightAndMargin(layoutBox);
}
static void addDetachingRules(InlineItem& inlineItem, Optional<LayoutUnit> nonBreakableStartWidth, Optional<LayoutUnit> nonBreakableEndWidth)
{
OptionSet<InlineItem::DetachingRule> detachingRules;
if (nonBreakableStartWidth) {
detachingRules.add(InlineItem::DetachingRule::BreakAtStart);
inlineItem.addNonBreakableStart(*nonBreakableStartWidth);
}
if (nonBreakableEndWidth) {
detachingRules.add(InlineItem::DetachingRule::BreakAtEnd);
inlineItem.addNonBreakableEnd(*nonBreakableEndWidth);
}
inlineItem.addDetachingRule(detachingRules);
}
static InlineItem& createAndAppendInlineItem(InlineRunProvider& inlineRunProvider, InlineContent& inlineContent, const Box& layoutBox)
{
ASSERT(layoutBox.isInlineLevelBox() || layoutBox.isFloatingPositioned());
auto inlineItem = std::make_unique<InlineItem>(layoutBox);
auto* inlineItemPtr = inlineItem.get();
inlineContent.add(WTFMove(inlineItem));
inlineRunProvider.append(*inlineItemPtr);
return *inlineItemPtr;
}
void InlineFormattingContext::collectInlineContent(InlineRunProvider& inlineRunProvider) const
{
if (!is<Container>(root()))
return;
auto& root = downcast<Container>(this->root());
if (!root.hasInFlowOrFloatingChild())
return;
// The logic here is very similar to BFC layout.
// 1. Travers down the layout tree and collect "start" unbreakable widths (margin-left, border-left, padding-left)
// 2. Create InlineItem per leaf inline box (text nodes, inline-blocks, floats) and set "start" unbreakable width on them.
// 3. Climb back and collect "end" unbreakable width and set it on the last InlineItem.
auto& layoutState = this->layoutState();
auto& inlineContent = formattingState().inlineContent();
enum class NonBreakableWidthType { Start, End };
auto nonBreakableWidth = [&](auto& container, auto type) {
auto& displayBox = layoutState.displayBoxForLayoutBox(container);
if (type == NonBreakableWidthType::Start)
return displayBox.marginStart() + displayBox.borderLeft() + displayBox.paddingLeft().valueOr(0);
return displayBox.marginEnd() + displayBox.borderRight() + displayBox.paddingRight().valueOr(0);
};
LayoutQueue layoutQueue;
layoutQueue.append(root.firstInFlowOrFloatingChild());
Optional<LayoutUnit> nonBreakableStartWidth;
Optional<LayoutUnit> nonBreakableEndWidth;
InlineItem* lastInlineItem = nullptr;
while (!layoutQueue.isEmpty()) {
while (true) {
auto& layoutBox = *layoutQueue.last();
if (!is<Container>(layoutBox))
break;
auto& container = downcast<Container>(layoutBox);
if (container.establishesFormattingContext()) {
// Formatting contexts are treated as leaf nodes.
auto& inlineItem = createAndAppendInlineItem(inlineRunProvider, inlineContent, container);
auto& displayBox = layoutState.displayBoxForLayoutBox(container);
auto currentNonBreakableStartWidth = nonBreakableStartWidth.valueOr(0) + displayBox.marginStart() + nonBreakableEndWidth.valueOr(0);
addDetachingRules(inlineItem, currentNonBreakableStartWidth, displayBox.marginEnd());
nonBreakableStartWidth = { };
nonBreakableEndWidth = { };
// Formatting context roots take care of their subtrees. Continue with next sibling if exists.
layoutQueue.removeLast();
if (!container.nextInFlowOrFloatingSibling())
break;
layoutQueue.append(container.nextInFlowOrFloatingSibling());
continue;
}
// Check if this non-formatting context container has any non-breakable start properties (margin-left, border-left, padding-left)
// <span style="padding-left: 5px"><span style="padding-left: 5px">foobar</span></span> -> 5px + 5px
auto currentNonBreakableStartWidth = nonBreakableWidth(layoutBox, NonBreakableWidthType::Start);
if (currentNonBreakableStartWidth || layoutBox.isPositioned())
nonBreakableStartWidth = nonBreakableStartWidth.valueOr(0) + currentNonBreakableStartWidth;
if (!container.hasInFlowOrFloatingChild())
break;
layoutQueue.append(container.firstInFlowOrFloatingChild());
}
while (!layoutQueue.isEmpty()) {
auto& layoutBox = *layoutQueue.takeLast();
if (is<Container>(layoutBox)) {
// This is the end of an inline container. Compute the non-breakable end width and add it to the last inline box.
// <span style="padding-right: 5px">foobar</span> -> 5px; last inline item -> "foobar"
auto currentNonBreakableEndWidth = nonBreakableWidth(layoutBox, NonBreakableWidthType::End);
if (currentNonBreakableEndWidth || layoutBox.isPositioned())
nonBreakableEndWidth = nonBreakableEndWidth.valueOr(0) + currentNonBreakableEndWidth;
// Add it to the last inline box
if (lastInlineItem) {
addDetachingRules(*lastInlineItem, { }, nonBreakableEndWidth);
nonBreakableEndWidth = { };
}
} else {
// Leaf inline box
auto& inlineItem = createAndAppendInlineItem(inlineRunProvider, inlineContent, layoutBox);
// Add start and the (through empty containers) accumulated end width.
// <span style="padding-left: 1px">foobar</span> -> nonBreakableStartWidth: 1px;
// <span style="padding: 5px"></span>foobar -> nonBreakableStartWidth: 5px; nonBreakableEndWidth: 5px
if (nonBreakableStartWidth || nonBreakableEndWidth) {
addDetachingRules(inlineItem, nonBreakableStartWidth.valueOr(0) + nonBreakableEndWidth.valueOr(0), { });
nonBreakableStartWidth = { };
nonBreakableEndWidth = { };
}
lastInlineItem = &inlineItem;
}
if (auto* nextSibling = layoutBox.nextInFlowOrFloatingSibling()) {
layoutQueue.append(nextSibling);
break;
}
}
}
}
}
}
#endif