blob: 2032caf676ef336244b322697f0090524f562a02 [file] [log] [blame]
/*
* Copyright (C) 2019 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 "InlineLine.h"
#include "InlineLineBreaker.h"
#include "LayoutBox.h"
#include "LayoutContainer.h"
#include "LayoutState.h"
#include "TextUtil.h"
namespace WebCore {
namespace Layout {
struct UncommittedContent {
struct Run {
const InlineItem& inlineItem;
LayoutUnit logicalWidth;
// FIXME: add optional breaking context (start and end position) for split text content.
};
void add(const InlineItem&, LayoutUnit logicalWidth);
void reset();
Vector<Run> runs() { return m_uncommittedRuns; }
bool isEmpty() const { return m_uncommittedRuns.isEmpty(); }
unsigned size() const { return m_uncommittedRuns.size(); }
LayoutUnit width() const { return m_width; }
private:
Vector<Run> m_uncommittedRuns;
LayoutUnit m_width;
};
void UncommittedContent::add(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
m_uncommittedRuns.append({ inlineItem, logicalWidth });
m_width += logicalWidth;
}
void UncommittedContent::reset()
{
m_uncommittedRuns.clear();
m_width = 0;
}
InlineFormattingContext::LineLayout::LineInput::HorizontalConstraint::HorizontalConstraint(LayoutPoint logicalTopLeft, LayoutUnit availableLogicalWidth)
: logicalTopLeft(logicalTopLeft)
, availableLogicalWidth(availableLogicalWidth)
{
}
InlineFormattingContext::LineLayout::LineInput::LineInput(LayoutPoint logicalTopLeft, LayoutUnit availableLogicalWidth, SkipVerticalAligment skipVerticalAligment, unsigned firstInlineItemIndex, const InlineItems& inlineItems)
: horizontalConstraint(logicalTopLeft, availableLogicalWidth)
, skipVerticalAligment(skipVerticalAligment)
, firstInlineItemIndex(firstInlineItemIndex)
, inlineItems(inlineItems)
{
}
InlineFormattingContext::LineLayout::LineLayout(const InlineFormattingContext& inlineFormattingContext)
: m_formattingContext(inlineFormattingContext)
, m_formattingState(m_formattingContext.formattingState())
, m_floatingState(m_formattingState.floatingState())
, m_formattingRoot(downcast<Container>(m_formattingContext.root()))
{
}
static LayoutUnit inlineItemWidth(const LayoutState& layoutState, const InlineItem& inlineItem, LayoutUnit contentLogicalLeft)
{
if (inlineItem.isLineBreak())
return 0;
if (is<InlineTextItem>(inlineItem)) {
auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
auto end = inlineTextItem.isCollapsed() ? inlineTextItem.start() + 1 : inlineTextItem.end();
return TextUtil::width(downcast<InlineBox>(inlineTextItem.layoutBox()), inlineTextItem.start(), end, contentLogicalLeft);
}
auto& layoutBox = inlineItem.layoutBox();
ASSERT(layoutState.hasDisplayBox(layoutBox));
auto& displayBox = layoutState.displayBoxForLayoutBox(layoutBox);
if (layoutBox.isFloatingPositioned())
return displayBox.marginBoxWidth();
if (layoutBox.isReplaced())
return displayBox.width();
if (inlineItem.isContainerStart())
return displayBox.marginStart() + displayBox.borderLeft() + displayBox.paddingLeft().valueOr(0);
if (inlineItem.isContainerEnd())
return displayBox.marginEnd() + displayBox.borderRight() + displayBox.paddingRight().valueOr(0);
// Non-replaced inline box (e.g. inline-block)
return displayBox.width();
}
InlineFormattingContext::LineLayout::LineContent InlineFormattingContext::LineLayout::placeInlineItems(const LineInput& lineInput) const
{
std::unique_ptr<Line> line;
if (lineInput.skipVerticalAligment == LineInput::SkipVerticalAligment::No) {
auto mimimumLineHeight = m_formattingRoot.style().computedLineHeight();
auto initialBaselineOffset = Line::halfLeadingMetrics(m_formattingRoot.style().fontMetrics(), mimimumLineHeight).offset;
line = std::make_unique<Line>(layoutState(), lineInput.horizontalConstraint.logicalTopLeft, lineInput.horizontalConstraint.availableLogicalWidth, mimimumLineHeight, initialBaselineOffset);
} else
line = std::make_unique<Line>(layoutState(), lineInput.horizontalConstraint.logicalTopLeft.x(), lineInput.horizontalConstraint.availableLogicalWidth);
Vector<WeakPtr<InlineItem>> floats;
unsigned committedInlineItemCount = 0;
UncommittedContent uncommittedContent;
auto commitPendingContent = [&] {
if (uncommittedContent.isEmpty())
return;
committedInlineItemCount += uncommittedContent.size();
for (auto& uncommittedRun : uncommittedContent.runs()) {
auto& inlineItem = uncommittedRun.inlineItem;
if (inlineItem.isHardLineBreak())
line->appendHardLineBreak(inlineItem);
else if (is<InlineTextItem>(inlineItem))
line->appendTextContent(downcast<InlineTextItem>(inlineItem), uncommittedRun.logicalWidth);
else if (inlineItem.isContainerStart())
line->appendInlineContainerStart(inlineItem, uncommittedRun.logicalWidth);
else if (inlineItem.isContainerEnd())
line->appendInlineContainerEnd(inlineItem, uncommittedRun.logicalWidth);
else if (inlineItem.layoutBox().isReplaced())
line->appendReplacedInlineBox(inlineItem, uncommittedRun.logicalWidth);
else
line->appendNonReplacedInlineBox(inlineItem, uncommittedRun.logicalWidth);
}
uncommittedContent.reset();
};
auto closeLine = [&] {
// This might change at some point.
ASSERT(committedInlineItemCount);
return LineContent { lineInput.firstInlineItemIndex + (committedInlineItemCount - 1), WTFMove(floats), line->close() };
};
LineBreaker lineBreaker;
// Iterate through the inline content and place the inline boxes on the current line.
for (auto inlineItemIndex = lineInput.firstInlineItemIndex; inlineItemIndex < lineInput.inlineItems.size(); ++inlineItemIndex) {
auto availableWidth = line->availableWidth() - uncommittedContent.width();
auto currentLogicalRight = line->contentLogicalRight() + uncommittedContent.width();
auto& inlineItem = lineInput.inlineItems[inlineItemIndex];
auto itemLogicalWidth = inlineItemWidth(layoutState(), *inlineItem, currentLogicalRight);
// FIXME: Ensure LineContext::trimmableWidth includes uncommitted content if needed.
auto breakingContext = lineBreaker.breakingContext(*inlineItem, itemLogicalWidth, { availableWidth, currentLogicalRight, line->trailingTrimmableWidth(), !line->hasContent() });
if (breakingContext.isAtBreakingOpportunity)
commitPendingContent();
// Content does not fit the current line.
if (breakingContext.breakingBehavior == LineBreaker::BreakingBehavior::Wrap)
return closeLine();
// Partial content stays on the current line.
if (breakingContext.breakingBehavior == LineBreaker::BreakingBehavior::Split) {
ASSERT(inlineItem->isText());
ASSERT_NOT_IMPLEMENTED_YET();
return closeLine();
}
ASSERT(breakingContext.breakingBehavior == LineBreaker::BreakingBehavior::Keep);
if (inlineItem->isFloat()) {
auto& floatBox = inlineItem->layoutBox();
ASSERT(layoutState().hasDisplayBox(floatBox));
// Shrink availble space for current line and move existing inline runs.
auto floatBoxWidth = layoutState().displayBoxForLayoutBox(floatBox).marginBoxWidth();
floatBox.isLeftFloatingPositioned() ? line->moveLogicalLeft(floatBoxWidth) : line->moveLogicalRight(floatBoxWidth);
floats.append(makeWeakPtr(*inlineItem));
++committedInlineItemCount;
continue;
}
uncommittedContent.add(*inlineItem, itemLogicalWidth);
if (breakingContext.isAtBreakingOpportunity)
commitPendingContent();
if (inlineItem->isHardLineBreak())
return closeLine();
}
commitPendingContent();
return closeLine();
}
void InlineFormattingContext::LineLayout::layout(LayoutUnit widthConstraint) const
{
ASSERT(!m_formattingState.inlineItems().isEmpty());
auto& formattingRootDisplayBox = layoutState().displayBoxForLayoutBox(m_formattingRoot);
auto lineLogicalTop = formattingRootDisplayBox.contentBoxTop();
auto lineLogicalLeft = formattingRootDisplayBox.contentBoxLeft();
auto applyFloatConstraint = [&](auto& lineHorizontalConstraint) {
// Check for intruding floats and adjust logical left/available width for this line accordingly.
if (m_floatingState.isEmpty())
return;
auto availableWidth = lineHorizontalConstraint.availableLogicalWidth;
auto lineLogicalLeft = lineHorizontalConstraint.logicalTopLeft.x();
auto floatConstraints = m_floatingState.constraints({ lineLogicalTop }, m_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;
}
lineHorizontalConstraint.availableLogicalWidth = availableWidth;
lineHorizontalConstraint.logicalTopLeft.setX(lineLogicalLeft);
};
auto& inlineItems = m_formattingState.inlineItems();
unsigned currentInlineItemIndex = 0;
while (currentInlineItemIndex < inlineItems.size()) {
auto lineInput = LineInput { { lineLogicalLeft, lineLogicalTop }, widthConstraint, LineInput::SkipVerticalAligment::No, currentInlineItemIndex, inlineItems };
applyFloatConstraint(lineInput.horizontalConstraint);
auto lineContent = placeInlineItems(lineInput);
createDisplayRuns(*lineContent.runs, lineContent.floats, widthConstraint);
// We should always put at least one run on the line atm. This might change later on though.
ASSERT(lineContent.lastInlineItemIndex);
currentInlineItemIndex = *lineContent.lastInlineItemIndex + 1;
lineLogicalTop = lineContent.runs->logicalBottom();
}
}
LayoutUnit InlineFormattingContext::LineLayout::computedIntrinsicWidth(LayoutUnit widthConstraint) const
{
LayoutUnit maximumLineWidth;
auto& inlineItems = m_formattingState.inlineItems();
unsigned currentInlineItemIndex = 0;
while (currentInlineItemIndex < inlineItems.size()) {
auto lineContent = placeInlineItems({ { }, widthConstraint, LineInput::SkipVerticalAligment::Yes, currentInlineItemIndex, inlineItems });
currentInlineItemIndex = *lineContent.lastInlineItemIndex + 1;
LayoutUnit floatsWidth;
for (auto& floatItem : lineContent.floats)
floatsWidth += layoutState().displayBoxForLayoutBox(floatItem->layoutBox()).marginBoxWidth();
maximumLineWidth = std::max(maximumLineWidth, floatsWidth + lineContent.runs->logicalWidth());
}
return maximumLineWidth;
}
void InlineFormattingContext::LineLayout::createDisplayRuns(const Line::Content& lineContent, const Vector<WeakPtr<InlineItem>>& floats, LayoutUnit widthConstraint) const
{
auto floatingContext = FloatingContext { m_floatingState };
// Move floats to their final position.
for (auto floatItem : floats) {
auto& floatBox = floatItem->layoutBox();
ASSERT(layoutState().hasDisplayBox(floatBox));
auto& displayBox = layoutState().displayBoxForLayoutBox(floatBox);
// Set static position first.
displayBox.setTopLeft({ lineContent.logicalLeft(), lineContent.logicalTop() });
// Float it.
displayBox.setTopLeft(floatingContext.positionForFloat(floatBox));
m_floatingState.append(floatBox);
}
if (lineContent.isEmpty()) {
// Spec tells us to create a zero height, empty line box.
auto lineBox = Display::Rect { lineContent.logicalTop(), lineContent.logicalLeft(), 0 , 0 };
m_formattingState.addLineBox({ lineBox, lineContent.baseline() });
return;
}
auto& inlineDisplayRuns = m_formattingState.inlineRuns();
Optional<unsigned> previousLineLastRunIndex = inlineDisplayRuns.isEmpty() ? Optional<unsigned>() : inlineDisplayRuns.size() - 1;
// 9.4.2 Inline formatting contexts
// A line box is always tall enough for all of the boxes it contains.
// Ignore the initial strut.
auto lineBox = Display::Rect { lineContent.logicalTop(), lineContent.logicalLeft(), 0, !lineContent.isVisuallyEmpty() ? lineContent.logicalHeight() : LayoutUnit { } };
// Create final display runs.
auto& lineRuns = lineContent.runs();
for (unsigned index = 0; index < lineRuns.size(); ++index) {
auto& lineRun = lineRuns.at(index);
auto& inlineItem = lineRun->inlineItem;
auto& logicalRect = lineRun->logicalRect;
auto& layoutBox = inlineItem.layoutBox();
auto& displayBox = layoutState().displayBoxForLayoutBox(layoutBox);
if (inlineItem.isHardLineBreak()) {
displayBox.setTopLeft(logicalRect.topLeft());
displayBox.setContentBoxWidth(logicalRect.width());
displayBox.setContentBoxHeight(logicalRect.height());
m_formattingState.addInlineRun(std::make_unique<Display::Run>(logicalRect));
continue;
}
// Inline level box (replaced or inline-block)
if (inlineItem.isBox()) {
auto topLeft = logicalRect.topLeft();
if (layoutBox.isInFlowPositioned())
topLeft += Geometry::inFlowPositionedPositionOffset(layoutState(), layoutBox);
displayBox.setTopLeft(topLeft);
lineBox.expandHorizontally(logicalRect.width());
m_formattingState.addInlineRun(std::make_unique<Display::Run>(logicalRect));
continue;
}
// Inline level container start (<span>)
if (inlineItem.isContainerStart()) {
displayBox.setTopLeft(logicalRect.topLeft());
lineBox.expandHorizontally(logicalRect.width());
continue;
}
// Inline level container end (</span>)
if (inlineItem.isContainerEnd()) {
if (layoutBox.isInFlowPositioned()) {
auto inflowOffset = Geometry::inFlowPositionedPositionOffset(layoutState(), layoutBox);
displayBox.moveHorizontally(inflowOffset.width());
displayBox.moveVertically(inflowOffset.height());
}
auto marginBoxWidth = logicalRect.left() - displayBox.left();
auto contentBoxWidth = marginBoxWidth - (displayBox.marginStart() + displayBox.borderLeft() + displayBox.paddingLeft().valueOr(0));
// FIXME fix it for multiline.
displayBox.setContentBoxWidth(contentBoxWidth);
displayBox.setContentBoxHeight(logicalRect.height());
lineBox.expandHorizontally(logicalRect.width());
continue;
}
// Text content. Try to join multiple text runs when possible.
ASSERT(lineRun->textContext);
const Line::Content::Run* previousLineRun = !index ? nullptr : lineRuns[index - 1].get();
if (!lineRun->isCollapsed) {
auto previousRunCanBeExtended = previousLineRun ? previousLineRun->canBeExtended : false;
auto requiresNewRun = !index || !previousRunCanBeExtended || &layoutBox != &previousLineRun->inlineItem.layoutBox();
if (requiresNewRun)
m_formattingState.addInlineRun(std::make_unique<Display::Run>(logicalRect, Display::Run::TextContext { lineRun->textContext->start, lineRun->textContext->length }));
else {
auto& lastDisplayRun = m_formattingState.inlineRuns().last();
lastDisplayRun->expandHorizontally(logicalRect.width());
lastDisplayRun->textContext()->expand(lineRun->textContext->length);
}
lineBox.expandHorizontally(logicalRect.width());
}
// FIXME take content breaking into account when part of the layout box is on the previous line.
auto firstInlineRunForLayoutBox = !previousLineRun || &previousLineRun->inlineItem.layoutBox() != &layoutBox;
if (firstInlineRunForLayoutBox) {
// Setup display box for the associated layout box.
displayBox.setTopLeft(logicalRect.topLeft());
displayBox.setContentBoxWidth(lineRun->isCollapsed ? LayoutUnit() : logicalRect.width());
displayBox.setContentBoxHeight(logicalRect.height());
} else if (!lineRun->isCollapsed) {
// FIXME fix it for multirun/multiline.
displayBox.setContentBoxWidth(displayBox.contentBoxWidth() + logicalRect.width());
}
}
// FIXME linebox needs to be ajusted after content alignment.
m_formattingState.addLineBox({ lineBox, lineContent.baseline() });
if (!lineContent.isVisuallyEmpty())
alignRuns(m_formattingRoot.style().textAlign(), previousLineLastRunIndex.valueOr(-1) + 1, widthConstraint - lineContent.logicalWidth());
}
static Optional<LayoutUnit> horizontalAdjustmentForAlignment(TextAlignMode align, LayoutUnit remainingWidth)
{
switch (align) {
case TextAlignMode::Left:
case TextAlignMode::WebKitLeft:
case TextAlignMode::Start:
return { };
case TextAlignMode::Right:
case TextAlignMode::WebKitRight:
case TextAlignMode::End:
return std::max(remainingWidth, 0_lu);
case TextAlignMode::Center:
case TextAlignMode::WebKitCenter:
return std::max(remainingWidth / 2, 0_lu);
case TextAlignMode::Justify:
ASSERT_NOT_REACHED();
break;
}
ASSERT_NOT_REACHED();
return { };
}
void InlineFormattingContext::LineLayout::alignRuns(TextAlignMode textAlign, unsigned firstRunIndex, LayoutUnit availableWidth) const
{
auto adjustment = horizontalAdjustmentForAlignment(textAlign, availableWidth);
if (!adjustment)
return;
auto& inlineDisplayRuns = m_formattingState.inlineRuns();
for (unsigned index = firstRunIndex; index < inlineDisplayRuns.size(); ++index)
inlineDisplayRuns[index]->moveHorizontally(*adjustment);
}
}
}
#endif