InlineLineBreaker.cpp [plain text]
#include "config.h"
#include "InlineLineBreaker.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "FontCascade.h"
#include "Hyphenation.h"
#include "InlineItem.h"
#include "InlineTextItem.h"
#include "TextUtil.h"
namespace WebCore {
namespace Layout {
static inline bool isWrappingAllowed(const RenderStyle& style)
{
return style.whiteSpace() != WhiteSpace::Pre && style.whiteSpace() != WhiteSpace::NoWrap;
}
static inline bool shouldKeepBeginningOfLineWhitespace(const RenderStyle& style)
{
auto whitespace = style.whiteSpace();
return whitespace == WhiteSpace::Pre || whitespace == WhiteSpace::PreWrap || whitespace == WhiteSpace::BreakSpaces;
}
struct ContinousContent {
ContinousContent(const LineBreaker::RunList&);
const LineBreaker::RunList& runs() const { return m_runs; }
bool isEmpty() const { return m_runs.isEmpty(); }
bool hasTextContentOnly() const;
bool isVisuallyEmptyWhitespaceContentOnly() const;
bool hasNonContentRunsOnly() const;
size_t size() const { return m_runs.size(); }
InlineLayoutUnit width() const { return m_width; }
InlineLayoutUnit nonCollapsibleWidth() const { return m_width - m_trailingCollapsibleContent.width; }
bool hasTrailingCollapsibleContent() const { return !!m_trailingCollapsibleContent.width; }
bool isTrailingContentFullyCollapsible() const { return m_trailingCollapsibleContent.isFullyCollapsible; }
Optional<size_t> lastWrapOpportunityIndex() const;
Optional<size_t> firstTextRunIndex() const;
Optional<size_t> lastContentRunIndex() const;
private:
LineBreaker::RunList m_runs;
struct TrailingCollapsibleContent {
void reset();
bool isFullyCollapsible { false };
InlineLayoutUnit width { 0 };
};
TrailingCollapsibleContent m_trailingCollapsibleContent;
InlineLayoutUnit m_width { 0 };
};
struct WrappedTextContent {
unsigned trailingRunIndex { 0 };
bool contentOverflows { false };
Optional<LineBreaker::PartialRun> partialTrailingRun;
};
bool LineBreaker::isContentWrappingAllowed(const ContinousContent& candidateRuns) const
{
auto runIndex = candidateRuns.lastContentRunIndex().valueOr(candidateRuns.size() - 1);
return isWrappingAllowed(candidateRuns.runs()[runIndex].inlineItem.style());
}
bool LineBreaker::shouldKeepEndOfLineWhitespace(const ContinousContent& candidateRuns) const
{
auto whitespace = candidateRuns.runs()[*candidateRuns.firstTextRunIndex()].inlineItem.style().whiteSpace();
return whitespace == WhiteSpace::Normal || whitespace == WhiteSpace::NoWrap || whitespace == WhiteSpace::PreWrap || whitespace == WhiteSpace::PreLine;
}
LineBreaker::Result LineBreaker::shouldWrapInlineContent(const RunList& candidateRuns, const LineStatus& lineStatus)
{
auto candidateContent = ContinousContent { candidateRuns };
ASSERT(!candidateContent.isEmpty());
auto result = tryWrappingInlineContent(candidateContent, lineStatus);
if (result.isEndOfLine == IsEndOfLine::Yes)
return result;
if (auto lastLineWrapOpportunityIndex = candidateContent.lastWrapOpportunityIndex()) {
auto isEligibleLineWrapOpportunity = [&] (auto& candidateItem) {
if (!lineStatus.lineIsEmpty || !candidateItem.isText() || !downcast<InlineTextItem>(candidateItem).isWhitespace())
return true;
return shouldKeepBeginningOfLineWhitespace(candidateItem.style());
};
auto& inlineItem = candidateContent.runs()[*lastLineWrapOpportunityIndex].inlineItem;
if (isEligibleLineWrapOpportunity(inlineItem))
m_lastWrapOpportunity = &inlineItem;
}
return result;
}
LineBreaker::Result LineBreaker::tryWrappingInlineContent(const ContinousContent& candidateContent, const LineStatus& lineStatus) const
{
if (candidateContent.width() <= lineStatus.availableWidth)
return { Result::Action::Keep };
if (candidateContent.hasTrailingCollapsibleContent()) {
ASSERT(candidateContent.hasTextContentOnly());
auto IsEndOfLine = isContentWrappingAllowed(candidateContent) ? IsEndOfLine::Yes : IsEndOfLine::No;
if (candidateContent.nonCollapsibleWidth() <= lineStatus.availableWidth)
return { Result::Action::Keep, IsEndOfLine };
if (lineStatus.lineHasFullyCollapsibleTrailingRun && candidateContent.isTrailingContentFullyCollapsible()) {
return { Result::Action::Keep, IsEndOfLine };
}
} else if (lineStatus.collapsibleWidth && candidateContent.hasNonContentRunsOnly()) {
if (candidateContent.width() <= lineStatus.availableWidth + lineStatus.collapsibleWidth)
return { Result::Action::Keep };
}
if (candidateContent.isVisuallyEmptyWhitespaceContentOnly() && shouldKeepEndOfLineWhitespace(candidateContent)) {
return { Result::Action::Keep };
}
if (candidateContent.hasTextContentOnly()) {
auto& runs = candidateContent.runs();
if (auto wrappedTextContent = wrapTextContent(runs, lineStatus)) {
if (!wrappedTextContent->trailingRunIndex && wrappedTextContent->contentOverflows) {
if (!lineStatus.lineIsEmpty)
return { Result::Action::Push, IsEndOfLine::Yes, { } };
auto firstTextRunIndex = *candidateContent.firstTextRunIndex();
auto& inlineTextItem = downcast<InlineTextItem>(runs[firstTextRunIndex].inlineItem);
ASSERT(inlineTextItem.length());
if (inlineTextItem.length() == 1)
return Result { Result::Action::Keep, IsEndOfLine::Yes };
auto firstCharacterWidth = TextUtil::width(inlineTextItem, inlineTextItem.start(), inlineTextItem.start() + 1);
auto firstCharacterRun = PartialRun { 1, firstCharacterWidth, false };
return { Result::Action::Split, IsEndOfLine::Yes, Result::PartialTrailingContent { firstTextRunIndex, firstCharacterRun } };
}
auto splitContent = Result::PartialTrailingContent { wrappedTextContent->trailingRunIndex, wrappedTextContent->partialTrailingRun };
return { Result::Action::Split, IsEndOfLine::Yes, splitContent };
}
}
if (lineStatus.lineIsEmpty) {
ASSERT(!m_lastWrapOpportunity);
return { Result::Action::Keep, IsEndOfLine::No };
}
if (isContentWrappingAllowed(candidateContent))
return { Result::Action::Push, IsEndOfLine::Yes };
if (m_lastWrapOpportunity)
return { Result::Action::Revert, IsEndOfLine::Yes, { }, m_lastWrapOpportunity };
return { Result::Action::Keep, IsEndOfLine::No };
}
bool LineBreaker::shouldWrapFloatBox(InlineLayoutUnit floatLogicalWidth, InlineLayoutUnit availableWidth, bool lineIsEmpty)
{
return !lineIsEmpty && floatLogicalWidth > availableWidth;
}
Optional<WrappedTextContent> LineBreaker::wrapTextContent(const RunList& runs, const LineStatus& lineStatus) const
{
auto isContentSplitAllowed = [] (auto& run) {
ASSERT(run.inlineItem.isText() || run.inlineItem.isContainerStart() || run.inlineItem.isContainerEnd());
if (!run.inlineItem.isText()) {
return false;
}
return isWrappingAllowed(run.inlineItem.style());
};
InlineLayoutUnit accumulatedRunWidth = 0;
unsigned index = 0;
while (index < runs.size()) {
auto& run = runs[index];
ASSERT(run.inlineItem.isText() || run.inlineItem.isContainerStart() || run.inlineItem.isContainerEnd());
if (accumulatedRunWidth + run.logicalWidth > lineStatus.availableWidth && isContentSplitAllowed(run)) {
auto adjustedAvailableWidth = std::max<InlineLayoutUnit>(0, lineStatus.availableWidth - accumulatedRunWidth);
if (auto partialRun = tryBreakingTextRun(run, adjustedAvailableWidth)) {
if (partialRun->length)
return WrappedTextContent { index, false, partialRun };
if (index)
return WrappedTextContent { index - 1, false, { } };
return WrappedTextContent { 0, true, { } };
}
break;
}
accumulatedRunWidth += run.logicalWidth;
++index;
}
while (index--) {
auto& run = runs[index];
if (isContentSplitAllowed(run)) {
ASSERT(run.inlineItem.isText());
if (auto partialRun = tryBreakingTextRun(run, maxInlineLayoutUnit())) {
ASSERT(partialRun->length);
return WrappedTextContent { index, false, partialRun };
}
}
}
return { };
}
LineBreaker::WordBreakRule LineBreaker::wordBreakBehavior(const RenderStyle& style) const
{
if (style.lineBreak() == LineBreak::Anywhere)
return WordBreakRule::AtArbitraryPosition;
if (style.wordBreak() == WordBreak::BreakAll)
return WordBreakRule::AtArbitraryPosition;
if (style.wordBreak() == WordBreak::KeepAll)
return WordBreakRule::NoBreak;
if (style.wordBreak() == WordBreak::BreakWord && !m_lastWrapOpportunity)
return WordBreakRule::AtArbitraryPosition;
if (style.overflowWrap() == OverflowWrap::Break && !m_lastWrapOpportunity)
return WordBreakRule::AtArbitraryPosition;
if (!n_hyphenationIsDisabled && style.hyphens() == Hyphens::Auto && canHyphenate(style.locale()))
return WordBreakRule::OnlyHyphenationAllowed;
return WordBreakRule::NoBreak;
}
Optional<LineBreaker::PartialRun> LineBreaker::tryBreakingTextRun(const Run& overflowRun, InlineLayoutUnit availableWidth) const
{
ASSERT(overflowRun.inlineItem.isText());
auto& inlineTextItem = downcast<InlineTextItem>(overflowRun.inlineItem);
auto& style = inlineTextItem.style();
auto findLastBreakablePosition = availableWidth == maxInlineLayoutUnit();
auto breakRule = wordBreakBehavior(style);
if (breakRule == WordBreakRule::AtArbitraryPosition) {
if (findLastBreakablePosition) {
return PartialRun { inlineTextItem.length(), overflowRun.logicalWidth, false };
}
auto splitData = TextUtil::split(inlineTextItem.layoutBox(), inlineTextItem.start(), inlineTextItem.length(), overflowRun.logicalWidth, availableWidth, { });
return PartialRun { splitData.length, splitData.logicalWidth, false };
}
if (breakRule == WordBreakRule::OnlyHyphenationAllowed) {
auto runLength = inlineTextItem.length();
unsigned limitBefore = style.hyphenationLimitBefore() == RenderStyle::initialHyphenationLimitBefore() ? 0 : style.hyphenationLimitBefore();
unsigned limitAfter = style.hyphenationLimitAfter() == RenderStyle::initialHyphenationLimitAfter() ? 0 : style.hyphenationLimitAfter();
if (limitBefore >= runLength || limitAfter >= runLength || limitBefore + limitAfter > runLength)
return { };
unsigned leftSideLength = runLength;
auto& fontCascade = style.fontCascade();
auto hyphenWidth = InlineLayoutUnit { fontCascade.width(TextRun { StringView { style.hyphenString() } }) };
if (!findLastBreakablePosition) {
auto availableWidthExcludingHyphen = availableWidth - hyphenWidth;
if (availableWidthExcludingHyphen <= 0 || !enoughWidthForHyphenation(availableWidthExcludingHyphen, fontCascade.pixelSize()))
return { };
leftSideLength = TextUtil::split(inlineTextItem.layoutBox(), inlineTextItem.start(), runLength, overflowRun.logicalWidth, availableWidthExcludingHyphen, { }).length;
}
if (leftSideLength < limitBefore)
return { };
auto textContent = inlineTextItem.layoutBox().textContext()->content;
auto hyphenBefore = std::min(leftSideLength, runLength - limitAfter) + 1;
unsigned hyphenLocation = lastHyphenLocation(StringView(textContent).substring(inlineTextItem.start(), inlineTextItem.length()), hyphenBefore, style.locale());
if (!hyphenLocation || hyphenLocation < limitBefore)
return { };
auto trailingPartialRunWidthWithHyphen = TextUtil::width(inlineTextItem, inlineTextItem.start(), inlineTextItem.start() + hyphenLocation) + hyphenWidth;
return PartialRun { hyphenLocation, trailingPartialRunWidthWithHyphen, true };
}
ASSERT(breakRule == WordBreakRule::NoBreak);
return { };
}
ContinousContent::ContinousContent(const LineBreaker::RunList& runs)
: m_runs(runs)
{
for (auto& run : WTF::makeReversedRange(m_runs)) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isBox()) {
break;
}
if (inlineItem.isText()) {
auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
auto isFullyCollapsible = [&] {
return inlineTextItem.isWhitespace() && !TextUtil::shouldPreserveTrailingWhitespace(inlineTextItem.style());
};
if (isFullyCollapsible()) {
m_trailingCollapsibleContent.width += run.logicalWidth;
m_trailingCollapsibleContent.isFullyCollapsible = true;
continue;
}
if (auto collapsibleWidth = inlineTextItem.style().letterSpacing()) {
m_trailingCollapsibleContent.width += collapsibleWidth;
m_trailingCollapsibleContent.isFullyCollapsible = false;
}
break;
}
}
for (auto& run : m_runs) {
ASSERT(!run.inlineItem.isLineBreak());
m_width += run.logicalWidth;
}
}
bool ContinousContent::hasTextContentOnly() const
{
for (auto& run : m_runs) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isContainerStart() || inlineItem.isContainerEnd())
continue;
return inlineItem.isText();
}
return false;
}
bool ContinousContent::isVisuallyEmptyWhitespaceContentOnly() const
{
for (auto& run : m_runs) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isContainerStart() || inlineItem.isContainerEnd())
continue;
return inlineItem.isText() && downcast<InlineTextItem>(inlineItem).isWhitespace();
}
return false;
}
Optional<size_t> ContinousContent::firstTextRunIndex() const
{
for (size_t index = 0; index < m_runs.size(); ++index) {
if (m_runs[index].inlineItem.isText())
return index;
}
return { };
}
Optional<size_t> ContinousContent::lastContentRunIndex() const
{
for (size_t index = m_runs.size(); index--;) {
if (m_runs[index].inlineItem.isText() || m_runs[index].inlineItem.isBox())
return index;
}
return { };
}
bool ContinousContent::hasNonContentRunsOnly() const
{
for (auto& run : m_runs) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isContainerStart() || inlineItem.isContainerEnd())
continue;
return false;
}
return true;
}
Optional<size_t> ContinousContent::lastWrapOpportunityIndex() const
{
auto lastItemIndex = m_runs.size() - 1;
return isWrappingAllowed(m_runs[lastItemIndex].inlineItem.style()) ? makeOptional(lastItemIndex) : WTF::nullopt;
}
void ContinousContent::TrailingCollapsibleContent::reset()
{
isFullyCollapsible = false;
width = 0_lu;
}
}
}
#endif