LineLayoutContext.cpp [plain text]
#include "config.h"
#include "LineLayoutContext.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "InlineFormattingContext.h"
#include "LayoutBox.h"
#include "TextUtil.h"
namespace WebCore {
namespace Layout {
static inline bool endsWithSoftWrapOpportunity(const InlineTextItem& currentTextItem, const InlineTextItem& nextInlineTextItem)
{
ASSERT(!nextInlineTextItem.isWhitespace());
if (currentTextItem.isWhitespace())
return true;
if (¤tTextItem.inlineTextBox() == &nextInlineTextItem.inlineTextBox())
return true;
auto previousContent = currentTextItem.inlineTextBox().content();
auto lineBreakIterator = LazyLineBreakIterator { nextInlineTextItem.inlineTextBox().content() };
auto previousContentLength = previousContent.length();
UChar lastCharacter = previousContentLength ? previousContent[previousContentLength - 1] : 0;
UChar secondToLastCharacter = previousContentLength > 1 ? previousContent[previousContentLength - 2] : 0;
lineBreakIterator.setPriorContext(lastCharacter, secondToLastCharacter);
return !TextUtil::findNextBreakablePosition(lineBreakIterator, 0, nextInlineTextItem.style());
}
static inline bool isAtSoftWrapOpportunity(const InlineItem& current, const InlineItem& next)
{
ASSERT(current.isText() || current.isBox());
ASSERT(next.isText() || next.isBox());
if (current.isText() && next.isText()) {
auto& currentInlineTextItem = downcast<InlineTextItem>(current);
auto& nextInlineTextItem = downcast<InlineTextItem>(next);
if (currentInlineTextItem.isWhitespace()) {
return true;
}
if (nextInlineTextItem.isWhitespace()) {
return nextInlineTextItem.style().whiteSpace() != WhiteSpace::BreakSpaces;
}
if (current.style().lineBreak() == LineBreak::Anywhere || next.style().lineBreak() == LineBreak::Anywhere) {
return true;
}
return endsWithSoftWrapOpportunity(currentInlineTextItem, nextInlineTextItem);
}
if (current.isBox() || next.isBox()) {
return true;
}
ASSERT_NOT_REACHED();
return true;
}
static inline size_t nextWrapOpportunity(const InlineItems& inlineContent, size_t startIndex, const LineLayoutContext::InlineItemRange layoutRange)
{
auto isAtLineBreak = false;
auto inlineItemIndexWithContent = [&] (auto index) {
for (; index < layoutRange.end; ++index) {
auto& inlineItem = inlineContent[index];
if (inlineItem.isText() || inlineItem.isBox())
return index;
if (inlineItem.isLineBreak()) {
isAtLineBreak = true;
return index;
}
}
return layoutRange.end;
};
auto startContentIndex = inlineItemIndexWithContent(startIndex);
if (isAtLineBreak) {
return startContentIndex + 1;
}
while (startContentIndex < layoutRange.end) {
auto nextContentIndex = inlineItemIndexWithContent(startContentIndex + 1);
if (nextContentIndex == layoutRange.end)
return nextContentIndex;
if (isAtLineBreak) {
return nextContentIndex + 1;
}
if (isAtSoftWrapOpportunity(inlineContent[startContentIndex], inlineContent[nextContentIndex])) {
for (auto candidateIndex = startContentIndex + 1; candidateIndex < nextContentIndex; ++candidateIndex) {
if (inlineContent[candidateIndex].isContainerStart()) {
return candidateIndex;
}
}
return nextContentIndex;
}
startContentIndex = nextContentIndex;
}
return layoutRange.end;
}
struct LineCandidate {
void reset();
struct InlineContent {
const LineBreaker::RunList& runs() const { return m_inlineRuns; }
InlineLayoutUnit logicalWidth() const { return m_LogicalWidth; }
const InlineItem* trailingLineBreak() const { return m_trailingLineBreak; }
void appendInlineItem(const InlineItem&, InlineLayoutUnit logicalWidth);
void appendLineBreak(const InlineItem& inlineItem) { setTrailingLineBreak(inlineItem); }
void reset();
private:
void setTrailingLineBreak(const InlineItem& lineBreakItem) { m_trailingLineBreak = &lineBreakItem; }
InlineLayoutUnit m_LogicalWidth { 0 };
LineBreaker::RunList m_inlineRuns;
const InlineItem* m_trailingLineBreak { nullptr };
};
struct FloatContent {
void append(const InlineItem& floatItem, InlineLayoutUnit logicalWidth, bool isIntrusive);
struct Float {
const InlineItem* item { nullptr };
InlineLayoutUnit logicalWidth { 0 };
bool isIntrusive { true };
};
using FloatList = Vector<Float>;
const FloatList& list() const { return m_floatList; }
InlineLayoutUnit intrusiveWidth() const { return m_intrusiveWidth; }
void reset();
private:
FloatList m_floatList;
InlineLayoutUnit m_intrusiveWidth { 0 };
};
InlineContent inlineContent;
FloatContent floatContent;
};
inline void LineCandidate::InlineContent::appendInlineItem(const InlineItem& inlineItem, InlineLayoutUnit logicalWidth)
{
m_LogicalWidth += logicalWidth;
m_inlineRuns.append({ inlineItem, logicalWidth });
}
inline void LineCandidate::InlineContent::reset()
{
m_LogicalWidth = { };
m_inlineRuns.clear();
m_trailingLineBreak = { };
}
inline void LineCandidate::FloatContent::append(const InlineItem& floatItem, InlineLayoutUnit logicalWidth, bool isIntrusive)
{
if (isIntrusive)
m_intrusiveWidth += logicalWidth;
m_floatList.append({ &floatItem, logicalWidth, isIntrusive });
}
inline void LineCandidate::FloatContent::reset()
{
m_floatList.clear();
m_intrusiveWidth = { };
}
inline void LineCandidate::reset()
{
floatContent.reset();
inlineContent.reset();
}
InlineLayoutUnit LineLayoutContext::inlineItemWidth(const InlineItem& inlineItem, InlineLayoutUnit contentLogicalLeft) const
{
if (is<InlineTextItem>(inlineItem)) {
auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
if (auto contentWidth = inlineTextItem.width())
return *contentWidth;
auto end = inlineTextItem.isCollapsible() ? inlineTextItem.start() + 1 : inlineTextItem.end();
return TextUtil::width(inlineTextItem, inlineTextItem.start(), end, contentLogicalLeft);
}
if (inlineItem.isLineBreak())
return 0;
auto& layoutBox = inlineItem.layoutBox();
auto& boxGeometry = m_inlineFormattingContext.geometryForBox(layoutBox);
if (layoutBox.isFloatingPositioned())
return boxGeometry.marginBoxWidth();
if (layoutBox.isReplacedBox())
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);
return boxGeometry.width();
}
LineLayoutContext::LineLayoutContext(const InlineFormattingContext& inlineFormattingContext, const ContainerBox& formattingContextRoot, const InlineItems& inlineItems)
: m_inlineFormattingContext(inlineFormattingContext)
, m_formattingContextRoot(formattingContextRoot)
, m_inlineItems(inlineItems)
{
}
LineLayoutContext::LineContent LineLayoutContext::layoutLine(LineBuilder& line, const InlineItemRange layoutRange, Optional<unsigned> partialLeadingContentLength)
{
ASSERT(m_floats.isEmpty());
m_partialLeadingTextItem = { };
m_lastWrapOpportunityItem = { };
auto lineBreaker = LineBreaker { };
auto currentItemIndex = layoutRange.start;
unsigned committedInlineItemCount = 0;
auto lineCandidate = LineCandidate { };
while (currentItemIndex < layoutRange.end) {
nextContentForLine(lineCandidate, currentItemIndex, layoutRange, partialLeadingContentLength, line.availableWidth() + line.trimmableTrailingWidth(), line.lineBox().logicalWidth());
auto result = handleFloatsAndInlineContent(lineBreaker, line, layoutRange, lineCandidate);
committedInlineItemCount = result.committedCount.isRevert ? result.committedCount.value : committedInlineItemCount + result.committedCount.value;
auto& inlineContent = lineCandidate.inlineContent;
auto inlineContentIsFullyCommitted = inlineContent.runs().size() == result.committedCount.value && !result.partialContent;
auto isEndOfLine = result.isEndOfLine == LineBreaker::IsEndOfLine::Yes;
if (inlineContentIsFullyCommitted && inlineContent.trailingLineBreak()) {
line.append(*inlineContent.trailingLineBreak(), { });
++committedInlineItemCount;
isEndOfLine = true;
}
if (isEndOfLine) {
return close(line, layoutRange, committedInlineItemCount, result.partialContent);
}
currentItemIndex = layoutRange.start + committedInlineItemCount + m_floats.size();
partialLeadingContentLength = { };
}
return close(line, layoutRange, committedInlineItemCount, { });
}
LineLayoutContext::LineContent LineLayoutContext::close(LineBuilder& line, const InlineItemRange layoutRange, unsigned committedInlineItemCount, Optional<LineContent::PartialContent> partialContent)
{
ASSERT(committedInlineItemCount || !m_floats.isEmpty() || line.hasIntrusiveFloat());
if (!committedInlineItemCount) {
if (m_floats.isEmpty()) {
return LineContent { { }, { }, WTFMove(m_floats), line.close(), line.lineBox() };
}
unsigned trailingInlineItemIndex = layoutRange.start + m_floats.size() - 1;
return LineContent { trailingInlineItemIndex, { }, WTFMove(m_floats), line.close(), line.lineBox() };
}
if (partialContent && partialContent->trailingContentHasHyphen)
++m_successiveHyphenatedLineCount;
else
m_successiveHyphenatedLineCount = 0;
ASSERT(committedInlineItemCount);
unsigned trailingInlineItemIndex = layoutRange.start + committedInlineItemCount + m_floats.size() - 1;
ASSERT(trailingInlineItemIndex < layoutRange.end);
auto isLastLineWithInlineContent = [&] {
if (trailingInlineItemIndex == layoutRange.end - 1)
return LineBuilder::IsLastLineWithInlineContent::Yes;
if (partialContent)
return LineBuilder::IsLastLineWithInlineContent::No;
for (auto i = layoutRange.end; i--;) {
if (!m_inlineItems[i].isFloat())
return i == trailingInlineItemIndex ? LineBuilder::IsLastLineWithInlineContent::Yes : LineBuilder::IsLastLineWithInlineContent::No;
}
ASSERT_NOT_REACHED();
return LineBuilder::IsLastLineWithInlineContent::No;
}();
return LineContent { trailingInlineItemIndex, partialContent, WTFMove(m_floats), line.close(isLastLineWithInlineContent), line.lineBox() };
}
void LineLayoutContext::nextContentForLine(LineCandidate& lineCandidate, unsigned currentInlineItemIndex, const InlineItemRange layoutRange, Optional<unsigned> partialLeadingContentLength, InlineLayoutUnit availableLineWidth, InlineLayoutUnit currentLogicalRight)
{
ASSERT(currentInlineItemIndex < layoutRange.end);
lineCandidate.reset();
auto softWrapOpportunityIndex = nextWrapOpportunity(m_inlineItems, currentInlineItemIndex, layoutRange);
ASSERT(softWrapOpportunityIndex <= layoutRange.end);
if (partialLeadingContentLength) {
m_partialLeadingTextItem = downcast<InlineTextItem>(m_inlineItems[currentInlineItemIndex]).right(*partialLeadingContentLength);
auto itemWidth = inlineItemWidth(*m_partialLeadingTextItem, currentLogicalRight);
lineCandidate.inlineContent.appendInlineItem(*m_partialLeadingTextItem, itemWidth);
currentLogicalRight += itemWidth;
++currentInlineItemIndex;
}
auto accumulatedWidth = InlineLayoutUnit { };
for (auto index = currentInlineItemIndex; index < softWrapOpportunityIndex; ++index) {
auto& inlineItem = m_inlineItems[index];
if (inlineItem.isFloat()) {
auto floatWidth = inlineItemWidth(inlineItem, { });
lineCandidate.floatContent.append(inlineItem, floatWidth, floatWidth <= (availableLineWidth - accumulatedWidth));
accumulatedWidth += floatWidth;
continue;
}
if (inlineItem.isText() || inlineItem.isContainerStart() || inlineItem.isContainerEnd() || inlineItem.isBox()) {
auto inlineItenmWidth = inlineItemWidth(inlineItem, currentLogicalRight);
lineCandidate.inlineContent.appendInlineItem(inlineItem, inlineItenmWidth);
currentLogicalRight += inlineItenmWidth;
accumulatedWidth += inlineItenmWidth;
continue;
}
if (inlineItem.isLineBreak()) {
lineCandidate.inlineContent.appendLineBreak(inlineItem);
continue;
}
ASSERT_NOT_REACHED();
}
}
void LineLayoutContext::commitFloats(LineBuilder& line, const LineCandidate& lineCandidate, CommitIntrusiveFloatsOnly commitIntrusiveOnly)
{
auto& floatContent = lineCandidate.floatContent;
auto leftIntrusiveFloatsWidth = InlineLayoutUnit { };
auto rightIntrusiveFloatsWidth = InlineLayoutUnit { };
auto hasIntrusiveFloat = false;
for (auto& floatCandidate : floatContent.list()) {
if (!floatCandidate.isIntrusive && commitIntrusiveOnly == CommitIntrusiveFloatsOnly::Yes)
continue;
m_floats.append({ floatCandidate.isIntrusive? LineContent::Float::Intrusive::Yes : LineContent::Float::Intrusive::No, floatCandidate.item });
if (floatCandidate.isIntrusive) {
hasIntrusiveFloat = true;
if (floatCandidate.item->layoutBox().isLeftFloatingPositioned())
leftIntrusiveFloatsWidth += floatCandidate.logicalWidth;
else
rightIntrusiveFloatsWidth += floatCandidate.logicalWidth;
}
}
if (hasIntrusiveFloat)
line.setHasIntrusiveFloat();
if (leftIntrusiveFloatsWidth || rightIntrusiveFloatsWidth) {
if (leftIntrusiveFloatsWidth)
line.moveLogicalLeft(leftIntrusiveFloatsWidth);
if (rightIntrusiveFloatsWidth)
line.moveLogicalRight(rightIntrusiveFloatsWidth);
}
}
LineLayoutContext::Result LineLayoutContext::handleFloatsAndInlineContent(LineBreaker& lineBreaker, LineBuilder& line, const InlineItemRange& layoutRange, const LineCandidate& lineCandidate)
{
auto& inlineContent = lineCandidate.inlineContent;
auto& candidateRuns = inlineContent.runs();
if (candidateRuns.isEmpty()) {
commitFloats(line, lineCandidate);
return { LineBreaker::IsEndOfLine::No };
}
auto shouldDisableHyphenation = [&] {
auto& style = root().style();
unsigned limitLines = style.hyphenationLimitLines() == RenderStyle::initialHyphenationLimitLines() ? std::numeric_limits<unsigned>::max() : style.hyphenationLimitLines();
return m_successiveHyphenatedLineCount >= limitLines;
};
if (shouldDisableHyphenation())
lineBreaker.setHyphenationDisabled();
auto& floatContent = lineCandidate.floatContent;
auto availableWidth = line.availableWidth() - floatContent.intrusiveWidth();
auto isLineConsideredEmpty = line.isVisuallyEmpty() && !line.hasIntrusiveFloat();
auto lineStatus = LineBreaker::LineStatus { availableWidth, line.trimmableTrailingWidth(), line.isTrailingRunFullyTrimmable(), isLineConsideredEmpty };
auto result = lineBreaker.shouldWrapInlineContent(candidateRuns, inlineContent.logicalWidth(), lineStatus);
if (result.lastWrapOpportunityItem)
m_lastWrapOpportunityItem = result.lastWrapOpportunityItem;
if (result.action == LineBreaker::Result::Action::Keep) {
for (auto& run : candidateRuns)
line.append(run.inlineItem, run.logicalWidth);
commitFloats(line, lineCandidate);
return { result.isEndOfLine, { candidateRuns.size(), false } };
}
if (result.action == LineBreaker::Result::Action::Push) {
ASSERT(result.isEndOfLine == LineBreaker::IsEndOfLine::Yes);
return { LineBreaker::IsEndOfLine::Yes };
}
if (result.action == LineBreaker::Result::Action::RevertToLastWrapOpportunity) {
ASSERT(result.isEndOfLine == LineBreaker::IsEndOfLine::Yes);
ASSERT(m_lastWrapOpportunityItem);
return { LineBreaker::IsEndOfLine::Yes, { rebuildLine(line, layoutRange), true } };
}
if (result.action == LineBreaker::Result::Action::Split) {
ASSERT(result.isEndOfLine == LineBreaker::IsEndOfLine::Yes);
commitFloats(line, lineCandidate, CommitIntrusiveFloatsOnly::Yes);
ASSERT(result.partialTrailingContent);
commitPartialContent(line, candidateRuns, *result.partialTrailingContent);
auto trailingRunIndex = result.partialTrailingContent->trailingRunIndex;
auto committedInlineItemCount = trailingRunIndex + 1;
if (!result.partialTrailingContent->partialRun)
return { LineBreaker::IsEndOfLine::Yes, { committedInlineItemCount, false } };
auto partialRun = *result.partialTrailingContent->partialRun;
auto& trailingInlineTextItem = downcast<InlineTextItem>(candidateRuns[trailingRunIndex].inlineItem);
ASSERT(partialRun.length < trailingInlineTextItem.length());
auto overflowLength = trailingInlineTextItem.length() - partialRun.length;
return { LineBreaker::IsEndOfLine::Yes, { committedInlineItemCount, false }, LineContent::PartialContent { partialRun.needsHyphen, overflowLength } };
}
ASSERT_NOT_REACHED();
return { LineBreaker::IsEndOfLine::No };
}
void LineLayoutContext::commitPartialContent(LineBuilder& line, const LineBreaker::RunList& runs, const LineBreaker::Result::PartialTrailingContent& partialTrailingContent)
{
for (size_t index = 0; index < runs.size(); ++index) {
auto& run = runs[index];
if (partialTrailingContent.trailingRunIndex == index) {
ASSERT(run.inlineItem.isText());
if (auto partialRun = partialTrailingContent.partialRun) {
auto& trailingInlineTextItem = downcast<InlineTextItem>(runs[partialTrailingContent.trailingRunIndex].inlineItem);
auto partialTrailingTextItem = trailingInlineTextItem.left(partialRun->length);
line.appendPartialTrailingTextItem(partialTrailingTextItem, partialRun->logicalWidth, partialRun->needsHyphen);
return;
}
line.append(run.inlineItem, run.logicalWidth);
return;
}
line.append(run.inlineItem, run.logicalWidth);
}
}
size_t LineLayoutContext::rebuildLine(LineBuilder& line, const InlineItemRange& layoutRange)
{
line.resetContent();
auto currentItemIndex = layoutRange.start;
auto logicalRight = InlineLayoutUnit { };
if (m_partialLeadingTextItem) {
auto logicalWidth = inlineItemWidth(*m_partialLeadingTextItem, logicalRight);
line.append(*m_partialLeadingTextItem, logicalWidth);
logicalRight += logicalWidth;
if (&m_partialLeadingTextItem.value() == m_lastWrapOpportunityItem)
return 1;
++currentItemIndex;
}
for (; currentItemIndex < layoutRange.end; ++currentItemIndex) {
auto& inlineItem = m_inlineItems[currentItemIndex];
auto logicalWidth = inlineItemWidth(inlineItem, logicalRight);
line.append(inlineItem, logicalWidth);
logicalRight += logicalWidth;
if (&inlineItem == m_lastWrapOpportunityItem)
return currentItemIndex - layoutRange.start + 1;
}
return layoutRange.size();
}
}
}
#endif