blob: e21b874072756ade164bb8f70605759805069743 [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 "FloatingContext.h"
#include "FloatingState.h"
#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, FormattingState& formattingState)
: FormattingContext(formattingContextRoot, formattingState)
{
}
void InlineFormattingContext::layout() const
{
if (!is<Container>(root()))
return;
LOG_WITH_STREAM(FormattingContextLayout, stream << "[Start] -> inline formatting context -> formatting root(" << &root() << ")");
InlineRunProvider inlineRunProvider;
collectInlineContent(inlineRunProvider);
// Compute width/height for non-text content.
for (auto& inlineRun : inlineRunProvider.runs()) {
if (inlineRun.isText())
continue;
auto& layoutBox = inlineRun.inlineItem().layoutBox();
if (layoutBox.establishesFormattingContext()) {
layoutFormattingContextRoot(layoutBox);
continue;
}
computeWidthAndHeightForReplacedInlineBox(layoutBox);
}
layoutInlineContent(inlineRunProvider);
LOG_WITH_STREAM(FormattingContextLayout, stream << "[End] -> inline formatting context -> formatting root(" << &root() << ")");
}
static bool isTrimmableContent(const InlineLineBreaker::Run& run)
{
return run.content.isWhitespace() && run.content.style().collapseWhiteSpace();
}
void InlineFormattingContext::initializeNewLine(Line& line) const
{
auto& formattingRoot = downcast<Container>(root());
auto& formattingRootDisplayBox = layoutState().displayBoxForLayoutBox(formattingRoot);
auto lineLogicalLeft = formattingRootDisplayBox.contentBoxLeft();
auto lineLogicalTop = line.isFirstLine() ? formattingRootDisplayBox.contentBoxTop() : line.logicalBottom();
auto availableWidth = formattingRootDisplayBox.contentBoxWidth();
// Check for intruding floats and adjust logical left/available width for this line accordingly.
auto& floatingState = formattingState().floatingState();
if (!floatingState.isEmpty()) {
auto floatConstraints = floatingState.constraints({ lineLogicalTop }, formattingRoot);
// Check if these constraints actually put limitation on the line.
if (floatConstraints.left && *floatConstraints.left <= formattingRootDisplayBox.contentBoxLeft())
floatConstraints.left = { };
if (floatConstraints.right && *floatConstraints.right >= formattingRootDisplayBox.contentBoxRight())
floatConstraints.right = { };
if (floatConstraints.left && floatConstraints.right) {
ASSERT(*floatConstraints.left < *floatConstraints.right);
availableWidth = *floatConstraints.right - *floatConstraints.left;
lineLogicalLeft = *floatConstraints.left;
} else if (floatConstraints.left) {
ASSERT(*floatConstraints.left > lineLogicalLeft);
availableWidth -= (*floatConstraints.left - lineLogicalLeft);
lineLogicalLeft = *floatConstraints.left;
} else if (floatConstraints.right) {
ASSERT(*floatConstraints.right > lineLogicalLeft);
availableWidth = *floatConstraints.right - lineLogicalLeft;
}
}
line.init({ lineLogicalLeft, lineLogicalTop }, availableWidth, formattingRoot.style().computedLineHeight());
}
void InlineFormattingContext::splitInlineRunIfNeeded(const InlineRun& inlineRun, InlineRuns& splitRuns) const
{
ASSERT(inlineRun.textContext());
ASSERT(inlineRun.overlapsMultipleInlineItems());
// In certain cases, a run can overlap multiple inline elements like this:
// <span>normal text content</span><span style="position: relative; left: 10px;">but this one needs a dedicated run</span><span>end of text</span>
// The content above generates one long run <normal text contentbut this one needs dedicated runend of text>
// However, since the middle run is positioned, it needs to be moved independently from the rest of the content, hence it needs a dedicated inline run.
// 1. Start with the first inline item (element) and travers the list until
// 2. either find an inline item that needs a dedicated run or we reach the end of the run
// 3. Create dedicate inline runs.
auto& inlineContent = inlineFormattingState().inlineContent();
auto contentStart = inlineRun.logicalLeft();
auto startPosition = inlineRun.textContext()->start();
auto remaningLength = inlineRun.textContext()->length();
struct Uncommitted {
const InlineItem* firstInlineItem { nullptr };
const InlineItem* lastInlineItem { nullptr };
unsigned length { 0 };
};
Optional<Uncommitted> uncommitted;
auto commit = [&] {
if (!uncommitted)
return;
contentStart += uncommitted->firstInlineItem->nonBreakableStart();
auto runWidth = Geometry::runWidth(inlineContent, *uncommitted->firstInlineItem, startPosition, uncommitted->length, contentStart);
auto run = InlineRun { { inlineRun.logicalTop(), contentStart, runWidth, inlineRun.logicalHeight() }, *uncommitted->firstInlineItem };
run.setTextContext({ startPosition, uncommitted->length });
splitRuns.append(run);
contentStart += runWidth + uncommitted->lastInlineItem->nonBreakableEnd();
remaningLength -= uncommitted->length;
startPosition = 0;
uncommitted = { };
};
for (auto iterator = inlineContent.find(const_cast<InlineItem*>(&inlineRun.inlineItem())); iterator != inlineContent.end() && remaningLength > 0; ++iterator) {
auto& inlineItem = **iterator;
// Skip all non-inflow boxes (floats, out-of-flow positioned elements). They don't participate in the inline run context.
if (!inlineItem.layoutBox().isInFlow())
continue;
auto currentLength = [&] {
return std::min(remaningLength, inlineItem.textContent().length() - startPosition);
};
// 1. Break before/after -> requires dedicated run -> commit what we've got so far and also commit the current inline element as a separate inline run.
// 2. Break at the beginning of the inline element -> commit what we've got so far. Current element becomes the first uncommitted.
// 3. Break at the end of the inline element -> commit what we've got so far including the current element.
// 4. Inline element does not require run breaking -> add current inline element to uncommitted. Jump to the next element.
auto detachingRules = inlineItem.detachingRules();
// #1
if (detachingRules.containsAll({ InlineItem::DetachingRule::BreakAtStart, InlineItem::DetachingRule::BreakAtEnd })) {
commit();
uncommitted = Uncommitted { &inlineItem, &inlineItem, currentLength() };
commit();
continue;
}
// #2
if (detachingRules.contains(InlineItem::DetachingRule::BreakAtStart))
commit();
// Add current inline item to uncommitted.
// #3 and #4
if (!uncommitted)
uncommitted = Uncommitted { &inlineItem, &inlineItem, 0 };
uncommitted->length += currentLength();
uncommitted->lastInlineItem = &inlineItem;
// #3
if (detachingRules.contains(InlineItem::DetachingRule::BreakAtEnd))
commit();
}
// Either all inline elements needed dedicated runs or neither of them.
if (!remaningLength || remaningLength == inlineRun.textContext()->length())
return;
commit();
}
void InlineFormattingContext::createFinalRuns(Line& line) const
{
auto& inlineFormattingState = this->inlineFormattingState();
for (auto& inlineRun : line.runs()) {
if (inlineRun.overlapsMultipleInlineItems()) {
InlineRuns splitRuns;
splitInlineRunIfNeeded(inlineRun, splitRuns);
for (auto& splitRun : splitRuns)
inlineFormattingState.appendInlineRun(splitRun);
if (!splitRuns.isEmpty())
continue;
}
auto finalRun = [&] {
auto& inlineItem = inlineRun.inlineItem();
if (inlineItem.detachingRules().isEmpty())
return inlineRun;
InlineRun adjustedRun = inlineRun;
auto width = inlineRun.logicalWidth() - inlineItem.nonBreakableStart() - inlineItem.nonBreakableEnd();
adjustedRun.setLogicalLeft(inlineRun.logicalLeft() + inlineItem.nonBreakableStart());
adjustedRun.setLogicalWidth(width);
return adjustedRun;
};
inlineFormattingState.appendInlineRun(finalRun());
}
}
void InlineFormattingContext::postProcessInlineRuns(Line& line, IsLastLine isLastLine) const
{
Geometry::alignRuns(root().style().textAlign(), line, isLastLine);
auto firstRunIndex = inlineFormattingState().inlineRuns().size();
createFinalRuns(line);
placeInFlowPositionedChildren(firstRunIndex);
}
void InlineFormattingContext::closeLine(Line& line, IsLastLine isLastLine) const
{
line.close();
if (!line.hasContent())
return;
postProcessInlineRuns(line, isLastLine);
}
void InlineFormattingContext::appendContentToLine(Line& line, const InlineRunProvider::Run& run, const LayoutSize& runSize) const
{
auto lastRunType = line.lastRunType();
line.appendContent(run, runSize);
if (root().style().textAlign() == TextAlignMode::Justify)
Geometry::computeExpansionOpportunities(line, run, lastRunType.valueOr(InlineRunProvider::Run::Type::NonWhitespace));
}
void InlineFormattingContext::layoutInlineContent(const InlineRunProvider& inlineRunProvider) const
{
auto& layoutState = this->layoutState();
auto& inlineFormattingState = this->inlineFormattingState();
auto floatingContext = FloatingContext { inlineFormattingState.floatingState() };
Line line;
initializeNewLine(line);
InlineLineBreaker lineBreaker(layoutState, inlineFormattingState.inlineContent(), inlineRunProvider.runs());
while (auto run = lineBreaker.nextRun(line.contentLogicalRight(), line.availableWidth(), !line.hasContent())) {
auto isFirstRun = run->position == InlineLineBreaker::Run::Position::LineBegin;
auto isLastRun = run->position == InlineLineBreaker::Run::Position::LineEnd;
auto generatesInlineRun = true;
// Position float and adjust the runs on line.
if (run->content.isFloat()) {
auto& floatBox = run->content.inlineItem().layoutBox();
computeFloatPosition(floatingContext, line, floatBox);
inlineFormattingState.floatingState().append(floatBox);
auto floatBoxWidth = layoutState.displayBoxForLayoutBox(floatBox).marginBox().width();
// Shrink availble space for current line and move existing inline runs.
floatBox.isLeftFloatingPositioned() ? line.adjustLogicalLeft(floatBoxWidth) : line.adjustLogicalRight(floatBoxWidth);
generatesInlineRun = false;
}
// 1. Initialize new line if needed.
// 2. Append inline run unless it is skipped.
// 3. Close current line if needed.
if (isFirstRun) {
// When the first run does not generate an actual inline run, the next run comes in first-run as well.
// No need to spend time on closing/initializing.
// Skip leading whitespace.
if (!generatesInlineRun || isTrimmableContent(*run))
continue;
if (line.hasContent()) {
// Previous run ended up being at the line end. Adjust the line accordingly.
if (!line.isClosed())
closeLine(line, IsLastLine::No);
initializeNewLine(line);
}
}
if (generatesInlineRun) {
auto width = run->width;
auto height = run->content.isText() ? LayoutUnit(root().style().computedLineHeight()) : layoutState.displayBoxForLayoutBox(run->content.inlineItem().layoutBox()).height();
appendContentToLine(line, run->content, { width, height });
}
if (isLastRun)
closeLine(line, IsLastLine::No);
}
closeLine(line, IsLastLine::Yes);
}
void InlineFormattingContext::computeWidthAndMargin(const Box& layoutBox) const
{
auto& layoutState = this->layoutState();
WidthAndMargin widthAndMargin;
if (layoutBox.isFloatingPositioned())
widthAndMargin = Geometry::floatingWidthAndMargin(layoutState, layoutBox);
else if (layoutBox.isInlineBlockBox())
widthAndMargin = Geometry::inlineBlockWidthAndMargin(layoutState, layoutBox);
else if (layoutBox.replaced())
widthAndMargin = Geometry::inlineReplacedWidthAndMargin(layoutState, layoutBox);
else
ASSERT_NOT_REACHED();
auto& displayBox = layoutState.displayBoxForLayoutBox(layoutBox);
displayBox.setContentBoxWidth(widthAndMargin.width);
displayBox.setHorizontalMargin(widthAndMargin.margin);
displayBox.setHorizontalNonComputedMargin(widthAndMargin.nonComputedMargin);
}
void InlineFormattingContext::computeHeightAndMargin(const Box& layoutBox) const
{
auto& layoutState = this->layoutState();
HeightAndMargin heightAndMargin;
if (layoutBox.isFloatingPositioned())
heightAndMargin = Geometry::floatingHeightAndMargin(layoutState, layoutBox);
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.margin);
}
void InlineFormattingContext::layoutFormattingContextRoot(const Box& root) const
{
ASSERT(root.isFloatingPositioned() || root.isInlineBlockBox());
computeBorderAndPadding(root);
computeWidthAndMargin(root);
// Swich over to the new formatting context (the one that the root creates).
auto formattingContext = layoutState().createFormattingStateForFormattingRootIfNeeded(root).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) const
{
ASSERT(!layoutBox.isContainer());
ASSERT(!layoutBox.establishesFormattingContext());
ASSERT(layoutBox.replaced());
computeBorderAndPadding(layoutBox);
computeWidthAndMargin(layoutBox);
computeHeightAndMargin(layoutBox);
}
void InlineFormattingContext::computeFloatPosition(const FloatingContext& floatingContext, Line& line, const Box& floatBox) const
{
auto& layoutState = this->layoutState();
ASSERT(layoutState.hasDisplayBox(floatBox));
auto& displayBox = layoutState.displayBoxForLayoutBox(floatBox);
// Set static position first.
displayBox.setTopLeft({ line.contentLogicalRight(), line.logicalTop() });
// Float it.
displayBox.setTopLeft(floatingContext.positionForFloat(floatBox));
}
void InlineFormattingContext::placeInFlowPositionedChildren(unsigned fistRunIndex) const
{
auto& inlineRuns = inlineFormattingState().inlineRuns();
for (auto runIndex = fistRunIndex; runIndex < inlineRuns.size(); ++runIndex) {
auto& inlineRun = inlineRuns[runIndex];
auto positionOffset = [&](auto& layoutBox) {
// FIXME: Need to figure out whether in-flow offset should stick. This might very well be temporary.
Optional<LayoutSize> offset;
for (auto* box = &layoutBox; box != &root(); box = box->parent()) {
if (!box->isInFlowPositioned())
continue;
offset = offset.valueOr(LayoutSize()) + Geometry::inFlowPositionedPositionOffset(layoutState(), *box);
}
return offset;
};
if (auto offset = positionOffset(inlineRun.inlineItem().layoutBox())) {
inlineRun.moveVertically(offset->height());
inlineRun.moveHorizontally(offset->width());
}
}
}
void InlineFormattingContext::collectInlineContentForSubtree(const Box& root, InlineRunProvider& inlineRunProvider) const
{
// Collect inline content recursively and set breaking rules for the inline elements (for paddings, margins, positioned element etc).
auto& inlineFormattingState = this->inlineFormattingState();
auto createAndAppendInlineItem = [&] {
auto inlineItem = std::make_unique<InlineItem>(root);
inlineRunProvider.append(*inlineItem);
inlineFormattingState.inlineContent().add(WTFMove(inlineItem));
};
if (root.establishesFormattingContext() && &root != &(this->root())) {
createAndAppendInlineItem();
auto& inlineRun = *inlineFormattingState.inlineContent().last();
auto horizontalMargin = Geometry::computedNonCollapsedHorizontalMarginValue(layoutState(), root);
inlineRun.addDetachingRule({ InlineItem::DetachingRule::BreakAtStart, InlineItem::DetachingRule::BreakAtEnd });
inlineRun.addNonBreakableStart(horizontalMargin.start);
inlineRun.addNonBreakableEnd(horizontalMargin.end);
// Skip formatting root subtree. They are not part of this inline formatting context.
return;
}
if (!is<Container>(root)) {
createAndAppendInlineItem();
return;
}
auto* lastInlineBoxBeforeContainer = inlineFormattingState.lastInlineItem();
auto* child = downcast<Container>(root).firstInFlowOrFloatingChild();
while (child) {
collectInlineContentForSubtree(*child, inlineRunProvider);
child = child->nextInFlowOrFloatingSibling();
}
// FIXME: Revisit this when we figured out how inline boxes fit the display tree.
auto padding = Geometry::computedPadding(layoutState(), root);
auto border = Geometry::computedBorder(layoutState(), root);
auto horizontalMargin = Geometry::computedNonCollapsedHorizontalMarginValue(layoutState(), root);
// Setup breaking boundaries for this subtree.
auto* lastDescendantInlineBox = inlineFormattingState.lastInlineItem();
// Empty container?
if (lastInlineBoxBeforeContainer == lastDescendantInlineBox)
return;
auto rootBreaksAtStart = [&] {
if (&root == &(this->root()))
return false;
return (padding && padding->horizontal.left) || border.horizontal.left || horizontalMargin.start || root.isPositioned();
};
auto rootBreaksAtEnd = [&] {
if (&root == &(this->root()))
return false;
return (padding && padding->horizontal.right) || border.horizontal.right || horizontalMargin.end || root.isPositioned();
};
if (rootBreaksAtStart()) {
InlineItem* firstDescendantInlineBox = nullptr;
auto& inlineContent = inlineFormattingState.inlineContent();
if (lastInlineBoxBeforeContainer) {
auto iterator = inlineContent.find(lastInlineBoxBeforeContainer);
firstDescendantInlineBox = (*++iterator).get();
} else
firstDescendantInlineBox = inlineContent.first().get();
ASSERT(firstDescendantInlineBox);
firstDescendantInlineBox->addDetachingRule(InlineItem::DetachingRule::BreakAtStart);
auto startOffset = border.horizontal.left + horizontalMargin.start;
if (padding)
startOffset += padding->horizontal.left;
firstDescendantInlineBox->addNonBreakableStart(startOffset);
}
if (rootBreaksAtEnd()) {
lastDescendantInlineBox->addDetachingRule(InlineItem::DetachingRule::BreakAtEnd);
auto endOffset = border.horizontal.right + horizontalMargin.end;
if (padding)
endOffset += padding->horizontal.right;
lastDescendantInlineBox->addNonBreakableEnd(endOffset);
}
}
void InlineFormattingContext::collectInlineContent(InlineRunProvider& inlineRunProvider) const
{
collectInlineContentForSubtree(root(), inlineRunProvider);
}
FormattingContext::InstrinsicWidthConstraints InlineFormattingContext::instrinsicWidthConstraints() const
{
auto& formattingStateForRoot = layoutState().formattingStateForBox(root());
if (auto instrinsicWidthConstraints = formattingStateForRoot.instrinsicWidthConstraints(root()))
return *instrinsicWidthConstraints;
auto& inlineFormattingState = this->inlineFormattingState();
InlineRunProvider inlineRunProvider;
collectInlineContent(inlineRunProvider);
// Compute width for non-text content.
for (auto& inlineRun : inlineRunProvider.runs()) {
if (inlineRun.isText())
continue;
computeWidthAndMargin(inlineRun.inlineItem().layoutBox());
}
auto maximumLineWidth = [&](auto availableWidth) {
LayoutUnit maxContentLogicalRight;
InlineLineBreaker lineBreaker(layoutState(), inlineFormattingState.inlineContent(), inlineRunProvider.runs());
LayoutUnit lineLogicalRight;
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;
};
return FormattingContext::InstrinsicWidthConstraints { maximumLineWidth(0), maximumLineWidth(LayoutUnit::max()) };
}
}
}
#endif