blob: 931f5f3307a07f3eb485bc936501189dbff71777 [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 "LineLayoutContext.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "InlineLineBreaker.h"
#include "LayoutBox.h"
#include "TextUtil.h"
namespace WebCore {
namespace Layout {
static LayoutUnit inlineItemWidth(const FormattingContext& formattingContext, const InlineItem& inlineItem, LayoutUnit contentLogicalLeft)
{
if (inlineItem.isLineBreak())
return 0_lu;
if (is<InlineTextItem>(inlineItem)) {
auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
auto contentWidth = inlineTextItem.width();
if (!contentWidth) {
auto end = inlineTextItem.isCollapsible() ? inlineTextItem.start() + 1 : inlineTextItem.end();
contentWidth = TextUtil::width(inlineTextItem.layoutBox(), inlineTextItem.start(), end, contentLogicalLeft);
}
auto wordSpacing = inlineTextItem.isWhitespace() ? LayoutUnit(inlineTextItem.style().fontCascade().wordSpacing()) : 0_lu;
return *contentWidth + wordSpacing;
}
auto& layoutBox = inlineItem.layoutBox();
auto& boxGeometry = formattingContext.geometryForBox(layoutBox);
if (layoutBox.isFloatingPositioned())
return boxGeometry.marginBoxWidth();
if (layoutBox.replaced())
return boxGeometry.width();
if (inlineItem.isContainerStart())
return boxGeometry.marginStart() + boxGeometry.borderLeft() + boxGeometry.paddingLeft().valueOr(0);
if (inlineItem.isContainerEnd())
return boxGeometry.marginEnd() + boxGeometry.borderRight() + boxGeometry.paddingRight().valueOr(0);
// Non-replaced inline box (e.g. inline-block)
return boxGeometry.width();
}
LineLayoutContext::LineLayoutContext(const InlineFormattingContext& inlineFormattingContext, const Container& formattingContextRoot, const InlineItems& inlineItems)
: m_inlineFormattingContext(inlineFormattingContext)
, m_formattingContextRoot(formattingContextRoot)
, m_inlineItems(inlineItems)
{
}
LineLayoutContext::LineContent LineLayoutContext::layoutLine(LineBuilder& line, unsigned leadingInlineItemIndex, Optional<unsigned> partialLeadingContentLength)
{
auto initialize = [&] {
m_committedInlineItemCount = 0;
m_uncommittedContent.reset();
m_partialLeadingTextItem = { };
m_partialTrailingTextItem = { };
m_partialContent = { };
};
initialize();
// Iterate through the inline content and place the inline boxes on the current line.
// Start with the partial leading text from the previous line.
auto firstNonPartialInlineItemIndex = leadingInlineItemIndex;
if (partialLeadingContentLength) {
// Handle partial inline item (split text from the previous line).
auto& leadingTextItem = m_inlineItems[leadingInlineItemIndex];
RELEASE_ASSERT(leadingTextItem->isText());
// Construct a partial leading inline item.
ASSERT(!m_partialLeadingTextItem);
m_partialLeadingTextItem = downcast<InlineTextItem>(*leadingTextItem).right(*partialLeadingContentLength);
if (placeInlineItem(line, *m_partialLeadingTextItem) == IsEndOfLine::Yes)
return close(line, leadingInlineItemIndex);
++firstNonPartialInlineItemIndex;
}
for (auto inlineItemIndex = firstNonPartialInlineItemIndex; inlineItemIndex < m_inlineItems.size(); ++inlineItemIndex) {
// FIXME: We should not need to re-measure the dropped, uncommitted content when re-using them on the next line.
if (placeInlineItem(line, *m_inlineItems[inlineItemIndex]) == IsEndOfLine::Yes)
return close(line, leadingInlineItemIndex);
}
// Check the uncommitted content whether they fit now that we know we are at a commit boundary.
if (!m_uncommittedContent.isEmpty())
processUncommittedContent(line);
return close(line, leadingInlineItemIndex);
}
void LineLayoutContext::commitPendingContent(LineBuilder& line)
{
if (m_uncommittedContent.isEmpty())
return;
m_committedInlineItemCount += m_uncommittedContent.size();
for (auto& uncommittedRun : m_uncommittedContent.runs())
line.append(uncommittedRun.inlineItem, uncommittedRun.logicalWidth);
m_uncommittedContent.reset();
}
LineLayoutContext::LineContent LineLayoutContext::close(LineBuilder& line, unsigned leadingInlineItemIndex)
{
ASSERT(m_committedInlineItemCount || line.hasIntrusiveFloat());
m_uncommittedContent.reset();
if (!m_committedInlineItemCount)
return LineContent { { }, { }, WTFMove(m_floats), line.close(), line.lineBox() };
auto trailingInlineItemIndex = leadingInlineItemIndex + m_committedInlineItemCount - 1;
auto isLastLineWithInlineContent = [&] {
if (m_partialContent)
return LineBuilder::IsLastLineWithInlineContent::No;
// Skip floats backwards to see if this is going to be the last line with inline content.
for (auto i = m_inlineItems.size(); i--;) {
if (!m_inlineItems[i]->isFloat())
return i == trailingInlineItemIndex ? LineBuilder::IsLastLineWithInlineContent::Yes : LineBuilder::IsLastLineWithInlineContent::No;
}
// There has to be at least one non-float item.
ASSERT_NOT_REACHED();
return LineBuilder::IsLastLineWithInlineContent::No;
};
return LineContent { trailingInlineItemIndex, m_partialContent, WTFMove(m_floats), line.close(isLastLineWithInlineContent()), line.lineBox() };
}
LineLayoutContext::IsEndOfLine LineLayoutContext::placeInlineItem(LineBuilder& line, const InlineItem& inlineItem)
{
auto currentLogicalRight = line.lineBox().logicalRight();
auto itemLogicalWidth = inlineItemWidth(formattingContext(), inlineItem, currentLogicalRight);
// Floats are special, they are intrusive but they don't really participate in the line layout context.
if (inlineItem.isFloat()) {
// FIXME: It gets a bit more complicated when there's some uncommitted content whether they should be added to the current line
// e.g. text_content<div style="float: left"></div>continuous_text_content
// Not sure what to do when the float takes up the available space and we've got continuous content. Browser engines don't agree.
// Let's just commit the pending content and try placing the float for now.
if (!m_uncommittedContent.isEmpty()) {
if (processUncommittedContent(line) == IsEndOfLine::Yes)
return IsEndOfLine::Yes;
}
auto lineIsConsideredEmpty = line.isVisuallyEmpty() && !line.hasIntrusiveFloat();
if (LineBreaker().shouldWrapFloatBox(itemLogicalWidth, line.availableWidth() + line.trailingTrimmableWidth(), lineIsConsideredEmpty))
return IsEndOfLine::Yes;
// This float can sit on the current line.
auto& floatBox = inlineItem.layoutBox();
// Shrink available space for current line and move existing inline runs.
floatBox.isLeftFloatingPositioned() ? line.moveLogicalLeft(itemLogicalWidth) : line.moveLogicalRight(itemLogicalWidth);
m_floats.append(makeWeakPtr(inlineItem));
++m_committedInlineItemCount;
line.setHasIntrusiveFloat();
return IsEndOfLine::No;
}
// Line breaks are also special.
if (inlineItem.isLineBreak()) {
auto isEndOfLine = !m_uncommittedContent.isEmpty() ? processUncommittedContent(line) : IsEndOfLine::No;
// When the uncommitted content fits(or the line is empty), add the line break to this line as well.
if (isEndOfLine == IsEndOfLine::No) {
m_uncommittedContent.append(inlineItem, itemLogicalWidth);
commitPendingContent(line);
}
return IsEndOfLine::Yes;
}
//
auto isEndOfLine = IsEndOfLine::No;
// Can we commit the pending content already?
if (LineBreaker::Content::isAtContentBoundary(inlineItem, m_uncommittedContent))
isEndOfLine = processUncommittedContent(line);
// The current item might fit as well.
if (isEndOfLine == IsEndOfLine::No)
m_uncommittedContent.append(inlineItem, itemLogicalWidth);
return isEndOfLine;
}
LineLayoutContext::IsEndOfLine LineLayoutContext::processUncommittedContent(LineBuilder& line)
{
auto shouldDisableHyphenation = [&] {
auto& style = m_formattingContextRoot.style();
unsigned limitLines = style.hyphenationLimitLines() == RenderStyle::initialHyphenationLimitLines() ? std::numeric_limits<unsigned>::max() : style.hyphenationLimitLines();
return m_successiveHyphenatedLineCount >= limitLines;
};
// Check if the pending content fits.
auto lineIsConsideredEmpty = line.isVisuallyEmpty() && !line.hasIntrusiveFloat();
auto lineBreaker = LineBreaker { };
if (shouldDisableHyphenation())
lineBreaker.setHyphenationDisabled();
auto lineStatus = LineBreaker::LineStatus { line.availableWidth(), line.trailingTrimmableWidth(), line.isTrailingRunFullyTrimmable(), lineIsConsideredEmpty };
auto breakingContext = lineBreaker.breakingContextForInlineContent(m_uncommittedContent, lineStatus);
// The uncommitted content can fully, partially fit the current line (commit/partial commit) or not at all (reset).
if (breakingContext.contentBreak == LineBreaker::BreakingContext::ContentBreak::Keep)
commitPendingContent(line);
else if (breakingContext.contentBreak == LineBreaker::BreakingContext::ContentBreak::Split) {
ASSERT(breakingContext.partialTrailingContent);
ASSERT(m_uncommittedContent.runs()[breakingContext.partialTrailingContent->runIndex].inlineItem.isText());
// Turn the uncommitted trailing run into a partial trailing run.
auto overflowInlineTextItemIndex = breakingContext.partialTrailingContent->runIndex;
auto& overflowInlineTextItem = downcast<InlineTextItem>(m_uncommittedContent.runs()[overflowInlineTextItemIndex].inlineItem);
// Construct a partial trailing inline run.
ASSERT(!m_partialTrailingTextItem);
auto trailingContentLength = breakingContext.partialTrailingContent->length;
m_partialTrailingTextItem = overflowInlineTextItem.left(trailingContentLength);
m_partialContent = LineContent::PartialContent { breakingContext.partialTrailingContent->needsHyphen, overflowInlineTextItem.length() - trailingContentLength };
// Keep the non-overflow part of the uncommitted runs and add the trailing partial content.
m_uncommittedContent.trim(overflowInlineTextItemIndex);
m_uncommittedContent.append(*m_partialTrailingTextItem, breakingContext.partialTrailingContent->logicalWidth);
commitPendingContent(line);
} else if (breakingContext.contentBreak == LineBreaker::BreakingContext::ContentBreak::Wrap)
m_uncommittedContent.reset();
else
ASSERT_NOT_REACHED();
// Adjust hyphenated line count
m_successiveHyphenatedLineCount = breakingContext.partialTrailingContent && breakingContext.partialTrailingContent->needsHyphen ? m_successiveHyphenatedLineCount + 1 : 0;
return breakingContext.contentBreak == LineBreaker::BreakingContext::ContentBreak::Keep ? IsEndOfLine::No :IsEndOfLine::Yes;
}
}
}
#endif