InlineContentBreaker.cpp [plain text]
#include "config.h"
#include "InlineContentBreaker.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 isTextContent(const InlineContentBreaker::ContinuousContent& continuousContent)
{
for (auto& run : continuousContent.runs()) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd())
continue;
return inlineItem.isText();
}
return false;
}
static inline bool isVisuallyEmptyWhitespaceContent(const InlineContentBreaker::ContinuousContent& continuousContent)
{
for (auto& run : continuousContent.runs()) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd())
continue;
return inlineItem.isText() && downcast<InlineTextItem>(inlineItem).isWhitespace();
}
return false;
}
static inline bool isNonContentRunsOnly(const InlineContentBreaker::ContinuousContent& continuousContent)
{
for (auto& run : continuousContent.runs()) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd())
continue;
return false;
}
return true;
}
static inline Optional<size_t> firstTextRunIndex(const InlineContentBreaker::ContinuousContent& continuousContent)
{
auto& runs = continuousContent.runs();
for (size_t index = 0; index < runs.size(); ++index) {
if (runs[index].inlineItem.isText())
return index;
}
return { };
}
static inline bool isWrappingAllowed(const InlineItem& inlineItem)
{
auto& styleToUse = inlineItem.isBox() ? inlineItem.layoutBox().parent().style() : inlineItem.layoutBox().style();
return styleToUse.whiteSpace() != WhiteSpace::Pre && styleToUse.whiteSpace() != WhiteSpace::NoWrap;
}
static inline Optional<size_t> lastWrapOpportunityIndex(const InlineContentBreaker::ContinuousContent::RunList& runList)
{
ASSERT(!runList.isEmpty());
auto lastItemIndex = runList.size() - 1;
return isWrappingAllowed(runList[lastItemIndex].inlineItem) ? makeOptional(lastItemIndex) : WTF::nullopt;
}
bool InlineContentBreaker::shouldKeepEndOfLineWhitespace(const ContinuousContent& continuousContent) const
{
auto whitespace = continuousContent.runs()[*firstTextRunIndex(continuousContent)].inlineItem.style().whiteSpace();
return whitespace == WhiteSpace::Normal || whitespace == WhiteSpace::NoWrap || whitespace == WhiteSpace::PreWrap || whitespace == WhiteSpace::PreLine;
}
InlineContentBreaker::Result InlineContentBreaker::processInlineContent(const ContinuousContent& candidateContent, const LineStatus& lineStatus)
{
auto processCandidateContent = [&] {
if (candidateContent.logicalWidth() <= lineStatus.availableWidth)
return Result { Result::Action::Keep };
#if USE_FLOAT_AS_INLINE_LAYOUT_UNIT
if (WTF::areEssentiallyEqual(candidateContent.logicalWidth(), lineStatus.availableWidth))
return Result { Result::Action::Keep };
#endif
return processOverflowingContent(candidateContent, lineStatus);
};
auto result = processCandidateContent();
if (result.action == Result::Action::Keep) {
if (auto lastLineWrapOpportunityIndex = lastWrapOpportunityIndex(candidateContent.runs())) {
auto isEligibleLineWrapOpportunity = [&] (auto& candidateItem) {
if (!lineStatus.isEmpty || !is<InlineTextItem>(candidateItem))
return true;
auto inlineTextItem = downcast<InlineTextItem>(candidateItem);
return !inlineTextItem.isWhitespace() || InlineTextItem::shouldPreserveSpacesAndTabs(inlineTextItem);
};
auto& lastWrapOpportunityCandidateItem = candidateContent.runs()[*lastLineWrapOpportunityIndex].inlineItem;
if (isEligibleLineWrapOpportunity(lastWrapOpportunityCandidateItem)) {
result.lastWrapOpportunityItem = &lastWrapOpportunityCandidateItem;
m_hasWrapOpportunityAtPreviousPosition = true;
}
}
} else if (result.action == Result::Action::Wrap) {
if (lineStatus.trailingSoftHyphenWidth && isTextContent(candidateContent)) {
auto hyphenOverflows = *lineStatus.trailingSoftHyphenWidth > lineStatus.availableWidth;
auto action = hyphenOverflows ? Result::Action::RevertToLastNonOverflowingWrapOpportunity : Result::Action::WrapWithHyphen;
result = { action, IsEndOfLine::Yes };
}
}
return result;
}
struct TrailingTextContent {
size_t runIndex { 0 };
bool hasOverflow { false }; Optional<InlineContentBreaker::PartialRun> partialRun;
};
InlineContentBreaker::Result InlineContentBreaker::processOverflowingContent(const ContinuousContent& overflowContent, const LineStatus& lineStatus) const
{
auto continuousContent = ContinuousContent { overflowContent };
ASSERT(!continuousContent.runs().isEmpty());
ASSERT(continuousContent.logicalWidth() > lineStatus.availableWidth);
if (continuousContent.hasTrailingCollapsibleContent()) {
ASSERT(isTextContent(continuousContent));
if (continuousContent.nonCollapsibleLogicalWidth() <= lineStatus.availableWidth)
return { Result::Action::Keep, IsEndOfLine::No };
if (lineStatus.hasFullyCollapsibleTrailingRun && continuousContent.isFullyCollapsible()) {
return { Result::Action::Keep, IsEndOfLine::No };
}
} else if (lineStatus.collapsibleWidth && isNonContentRunsOnly(continuousContent)) {
if (continuousContent.logicalWidth() <= lineStatus.availableWidth + lineStatus.collapsibleWidth)
return { Result::Action::Keep };
}
if (isVisuallyEmptyWhitespaceContent(continuousContent) && shouldKeepEndOfLineWhitespace(continuousContent)) {
return { Result::Action::Keep };
}
if (isTextContent(continuousContent)) {
if (auto trailingContent = processOverflowingTextContent(continuousContent, lineStatus)) {
if (!trailingContent->runIndex && trailingContent->hasOverflow) {
if (!lineStatus.isEmpty)
return { Result::Action::Wrap, IsEndOfLine::Yes };
auto leadingTextRunIndex = *firstTextRunIndex(continuousContent);
auto& inlineTextItem = downcast<InlineTextItem>(continuousContent.runs()[leadingTextRunIndex].inlineItem);
if (inlineTextItem.length() <= 1)
return Result { Result::Action::Keep, IsEndOfLine::Yes };
auto firstCharacterWidth = TextUtil::width(inlineTextItem, inlineTextItem.start(), inlineTextItem.start() + 1, lineStatus.contentLogicalRight);
auto firstCharacterRun = PartialRun { 1, firstCharacterWidth };
return { Result::Action::Break, IsEndOfLine::Yes, Result::PartialTrailingContent { leadingTextRunIndex, firstCharacterRun } };
}
auto trailingPartialContent = Result::PartialTrailingContent { trailingContent->runIndex, trailingContent->partialRun };
return { Result::Action::Break, IsEndOfLine::Yes, trailingPartialContent };
}
}
if (lineStatus.isEmpty) {
ASSERT(!m_hasWrapOpportunityAtPreviousPosition);
return { Result::Action::Keep, IsEndOfLine::No };
}
auto shouldWrapThisContentToNextLine = [&] {
auto& runs = continuousContent.runs();
auto& lastInlineItem = runs.last().inlineItem;
if (lastInlineItem.isBox() || lastInlineItem.isInlineBoxStart() || lastInlineItem.isInlineBoxEnd())
return isWrappingAllowed(lastInlineItem);
if (lastInlineItem.isText()) {
if (runs.size() == 1) {
return isWrappingAllowed(lastInlineItem);
}
for (auto& run : WTF::makeReversedRange(runs)) {
auto& inlineItem = run.inlineItem;
if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxStart())
return isWrappingAllowed(inlineItem);
ASSERT(!inlineItem.isBox());
}
return isWrappingAllowed(lastInlineItem);
}
ASSERT_NOT_REACHED();
return true;
};
if (shouldWrapThisContentToNextLine())
return { Result::Action::Wrap, IsEndOfLine::Yes };
if (m_hasWrapOpportunityAtPreviousPosition)
return { Result::Action::RevertToLastWrapOpportunity, IsEndOfLine::Yes };
return { Result::Action::Keep, IsEndOfLine::No };
}
Optional<TrailingTextContent> InlineContentBreaker::processOverflowingTextContent(const ContinuousContent& continuousContent, const LineStatus& lineStatus) const
{
auto isBreakableRun = [] (auto& run) {
ASSERT(run.inlineItem.isText() || run.inlineItem.isInlineBoxStart() || run.inlineItem.isInlineBoxEnd());
if (!run.inlineItem.isText()) {
return false;
}
return isWrappingAllowed(run.inlineItem);
};
auto& runs = continuousContent.runs();
auto accumulatedRunWidth = InlineLayoutUnit { };
size_t index = 0;
while (index < runs.size()) {
auto& run = runs[index];
ASSERT(run.inlineItem.isText() || run.inlineItem.isInlineBoxStart() || run.inlineItem.isInlineBoxEnd());
if (accumulatedRunWidth + run.logicalWidth > lineStatus.availableWidth && isBreakableRun(run)) {
auto adjustedAvailableWidth = std::max<InlineLayoutUnit>(0, lineStatus.availableWidth - accumulatedRunWidth);
if (auto partialRun = tryBreakingTextRun(run, lineStatus.contentLogicalRight + accumulatedRunWidth, adjustedAvailableWidth)) {
if (partialRun->length)
return TrailingTextContent { index, false, partialRun };
if (index) {
auto trailingCandidateIndex = index - 1;
auto isAtInlineBox = runs[trailingCandidateIndex].inlineItem.isInlineBoxStart();
if (isAtInlineBox) {
if (!trailingCandidateIndex) {
return TrailingTextContent { 0, true, { } };
}
--trailingCandidateIndex;
}
return TrailingTextContent { trailingCandidateIndex, false, { } };
}
return TrailingTextContent { 0, true, { } };
}
break;
}
accumulatedRunWidth += run.logicalWidth;
++index;
}
while (index--) {
auto& run = runs[index];
accumulatedRunWidth -= run.logicalWidth;
if (isBreakableRun(run)) {
ASSERT(run.inlineItem.isText());
if (auto partialRun = tryBreakingTextRun(run, lineStatus.contentLogicalRight + accumulatedRunWidth, maxInlineLayoutUnit())) {
ASSERT(partialRun->length);
return TrailingTextContent { index, false, partialRun };
}
}
}
return { };
}
InlineContentBreaker::WordBreakRule InlineContentBreaker::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_hasWrapOpportunityAtPreviousPosition)
return WordBreakRule::AtArbitraryPosition;
if (style.overflowWrap() == OverflowWrap::Break && !m_hasWrapOpportunityAtPreviousPosition)
return WordBreakRule::AtArbitraryPosition;
if (!n_hyphenationIsDisabled && style.hyphens() == Hyphens::Auto && canHyphenate(style.computedLocale()))
return WordBreakRule::OnlyHyphenationAllowed;
return WordBreakRule::NoBreak;
}
Optional<InlineContentBreaker::PartialRun> InlineContentBreaker::tryBreakingTextRun(const ContinuousContent::Run& overflowingRun, InlineLayoutUnit logicalLeft, InlineLayoutUnit availableWidth) const
{
ASSERT(overflowingRun.inlineItem.isText());
auto& inlineTextItem = downcast<InlineTextItem>(overflowingRun.inlineItem);
auto& style = inlineTextItem.style();
auto findLastBreakablePosition = availableWidth == maxInlineLayoutUnit();
auto breakRule = wordBreakBehavior(style);
if (breakRule == WordBreakRule::AtArbitraryPosition) {
if (!inlineTextItem.length()) {
return PartialRun { };
}
if (findLastBreakablePosition) {
ASSERT(inlineTextItem.length());
auto trailingPartialRunWidth = TextUtil::width(inlineTextItem, logicalLeft);
return PartialRun { inlineTextItem.length() - 1, trailingPartialRunWidth };
}
auto splitData = TextUtil::split(inlineTextItem, overflowingRun.logicalWidth, availableWidth, logicalLeft);
return PartialRun { splitData.length, splitData.logicalWidth };
}
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, overflowingRun.logicalWidth, availableWidthExcludingHyphen, logicalLeft).length;
}
if (leftSideLength < limitBefore)
return { };
auto hyphenBefore = std::min(leftSideLength, runLength - limitAfter) + 1;
unsigned hyphenLocation = lastHyphenLocation(StringView(inlineTextItem.inlineTextBox().content()).substring(inlineTextItem.start(), inlineTextItem.length()), hyphenBefore, style.computedLocale());
if (!hyphenLocation || hyphenLocation < limitBefore)
return { };
ASSERT(inlineTextItem.start() + hyphenLocation < inlineTextItem.end());
auto trailingPartialRunWidthWithHyphen = TextUtil::width(inlineTextItem, inlineTextItem.start(), inlineTextItem.start() + hyphenLocation, logicalLeft);
return PartialRun { hyphenLocation, trailingPartialRunWidthWithHyphen, hyphenWidth };
}
ASSERT(breakRule == WordBreakRule::NoBreak);
return { };
}
void InlineContentBreaker::ContinuousContent::append(const InlineItem& inlineItem, InlineLayoutUnit logicalWidth, Optional<InlineLayoutUnit> collapsibleWidth)
{
m_runs.append({ inlineItem, logicalWidth });
m_logicalWidth += logicalWidth;
if (!collapsibleWidth) {
m_collapsibleLogicalWidth = { };
return;
}
if (*collapsibleWidth == logicalWidth) {
m_collapsibleLogicalWidth += logicalWidth;
ASSERT(m_collapsibleLogicalWidth <= m_logicalWidth);
return;
}
m_collapsibleLogicalWidth = *collapsibleWidth;
ASSERT(m_collapsibleLogicalWidth <= m_logicalWidth);
}
void InlineContentBreaker::ContinuousContent::reset()
{
m_logicalWidth = { };
m_collapsibleLogicalWidth = { };
m_runs.clear();
}
}
}
#endif