Skip to content

Commit

Permalink
[IFC] TextOnlyLineBuilder should handle content split between boxes
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=260282

Reviewed by Antti Koivisto.

When continuous inline text content is split between boxes (and therefore between InlineTextItems) (e.g. createTextNode()) we need to be able to figure out
if there's a soft wrap opportunity between such inline items. This patch adds this ability to TextOnlyLineBuilder by reusing InlineLineBuilder's endsWithSoftWrapOpportunity logic.

* Source/WebCore/layout/formattingContexts/inline/InlineFormattingGeometry.cpp:
(WebCore::Layout::endsWithSoftWrapOpportunity):
* Source/WebCore/layout/formattingContexts/inline/InlineItemsBuilder.cpp:
(WebCore::Layout::InlineItemsBuilder::handleTextContent):
* Source/WebCore/layout/formattingContexts/inline/TextOnlySimpleLineBuilder.cpp:
(WebCore::Layout::TextOnlySimpleLineBuilder::placeInlineTextContent):
(WebCore::Layout::TextOnlySimpleLineBuilder::handleInlineTextContent):
(WebCore::Layout::TextOnlySimpleLineBuilder::revertToTrailingItem):
(WebCore::Layout::TextOnlySimpleLineBuilder::revertToLastNonOverflowingItem):
(WebCore::Layout::TextOnlySimpleLineBuilder::rebuildLine): Deleted.
* Source/WebCore/layout/formattingContexts/inline/TextOnlySimpleLineBuilder.h:
* Source/WebCore/layout/formattingContexts/inline/text/TextUtil.cpp:
(WebCore::Layout::TextUtil::mayBreakInBetween):
* Source/WebCore/layout/formattingContexts/inline/text/TextUtil.h:

Canonical link: https://commits.webkit.org/267011@main
  • Loading branch information
alanbaradlay committed Aug 17, 2023
1 parent 997e074 commit bc0673f
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -538,47 +538,25 @@ InlineLayoutUnit InlineFormattingGeometry::inlineItemWidth(const InlineItem& inl
return boxGeometry.marginBoxWidth();
}

static inline bool endsWithSoftWrapOpportunity(const InlineTextItem& currentTextItem, const InlineTextItem& nextInlineTextItem)
static inline bool endsWithSoftWrapOpportunity(const InlineTextItem& previousInlineTextItem, const InlineTextItem& nextInlineTextItem)
{
ASSERT(!nextInlineTextItem.isWhitespace());
// We are at the position after a whitespace.
if (currentTextItem.isWhitespace())
if (previousInlineTextItem.isWhitespace())
return true;
// When both these non-whitespace runs belong to the same layout box with the same bidi level, it's guaranteed that
// they are split at a soft breaking opportunity. See InlineItemsBuilder::moveToNextBreakablePosition.
if (&currentTextItem.inlineTextBox() == &nextInlineTextItem.inlineTextBox()) {
if (currentTextItem.bidiLevel() == nextInlineTextItem.bidiLevel())
if (&previousInlineTextItem.inlineTextBox() == &nextInlineTextItem.inlineTextBox()) {
if (previousInlineTextItem.bidiLevel() == nextInlineTextItem.bidiLevel())
return true;
// The bidi boundary may or may not be the reason for splitting the inline text box content.
// FIXME: We could add a "reason flag" to InlineTextItem to tell why the split happened.
auto& style = currentTextItem.style();
auto lineBreakIteratorFactory = CachedLineBreakIteratorFactory { currentTextItem.inlineTextBox().content(), style.computedLocale(), TextUtil::lineBreakIteratorMode(style.lineBreak()), TextUtil::contentAnalysis(style.wordBreak()) };
auto& style = previousInlineTextItem.style();
auto lineBreakIteratorFactory = CachedLineBreakIteratorFactory { previousInlineTextItem.inlineTextBox().content(), style.computedLocale(), TextUtil::lineBreakIteratorMode(style.lineBreak()), TextUtil::contentAnalysis(style.wordBreak()) };
auto softWrapOpportunityCandidate = nextInlineTextItem.start();
return TextUtil::findNextBreakablePosition(lineBreakIteratorFactory, softWrapOpportunityCandidate, style) == softWrapOpportunityCandidate;
}
// Now we need to collect at least 3 adjacent characters to be able to make a decision whether the previous text item ends with breaking opportunity.
// [ex-][ample] <- second to last[x] last[-] current[a]
// We need at least 1 character in the current inline text item and 2 more from previous inline items.
auto previousContent = currentTextItem.inlineTextBox().content();
auto currentContent = nextInlineTextItem.inlineTextBox().content();
if (!previousContent.is8Bit()) {
// FIXME: Remove this workaround when we move over to a better way of handling prior-context with Unicode.
// See the templated CharacterType in nextBreakablePosition for last and lastlast characters.
currentContent.convertTo16Bit();
}
auto& style = nextInlineTextItem.style();
auto lineBreakIteratorFactory = CachedLineBreakIteratorFactory { currentContent, style.computedLocale(), TextUtil::lineBreakIteratorMode(style.lineBreak()), TextUtil::contentAnalysis(style.wordBreak()) };
auto previousContentLength = previousContent.length();
// FIXME: We should look into the entire uncommitted content for more text context.
UChar lastCharacter = previousContentLength ? previousContent[previousContentLength - 1] : 0;
if (lastCharacter == softHyphen && currentTextItem.style().hyphens() == Hyphens::None)
return false;
UChar secondToLastCharacter = previousContentLength > 1 ? previousContent[previousContentLength - 2] : 0;
lineBreakIteratorFactory.priorContext().set({ secondToLastCharacter, lastCharacter });
// Now check if we can break right at the inline item boundary.
// With the [ex-ample], findNextBreakablePosition should return the startPosition (0).
// FIXME: Check if there's a more correct way of finding breaking opportunities.
return !TextUtil::findNextBreakablePosition(lineBreakIteratorFactory, 0, style);
return TextUtil::mayBreakInBetween(previousInlineTextItem, nextInlineTextItem);
}

static inline bool isAtSoftWrapOpportunity(const InlineItem& current, const InlineItem& next)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,13 +678,6 @@ void InlineItemsBuilder::handleTextContent(const InlineTextBox& inlineTextBox, I
if (handleWhitespace())
continue;

auto checkIfAjdactentInlineTextItemsEligibleForTextOnlyLayout = [&] {
if (!m_isNonBidiTextAndForcedLineBreakOnlyContent || inlineItems.size() <= 1)
return;
auto& adjacentInlineItem = inlineItems[inlineItems.size() - 2];
m_isNonBidiTextAndForcedLineBreakOnlyContent = adjacentInlineItem.isLineBreak() || (adjacentInlineItem.isText() && downcast<InlineTextItem>(adjacentInlineItem).isWhitespace());
};

auto handleNonBreakingSpace = [&] {
if (style.nbspMode() != NBSPMode::Space) {
// Let's just defer to regular non-whitespace inline items when non breaking space needs no special handling.
Expand All @@ -703,10 +696,8 @@ void InlineItemsBuilder::handleTextContent(const InlineTextBox& inlineTextBox, I
currentPosition = endPosition;
return true;
};
if (handleNonBreakingSpace()) {
checkIfAjdactentInlineTextItemsEligibleForTextOnlyLayout();
if (handleNonBreakingSpace())
continue;
}

auto handleNonWhitespace = [&] {
auto startPosition = currentPosition;
Expand All @@ -728,10 +719,8 @@ void InlineItemsBuilder::handleTextContent(const InlineTextBox& inlineTextBox, I
currentPosition = endPosition;
return true;
};
if (handleNonWhitespace()) {
checkIfAjdactentInlineTextItemsEligibleForTextOnlyLayout();
if (handleNonWhitespace())
continue;
}
// Unsupported content?
ASSERT_NOT_REACHED();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,14 @@ InlineItemPosition TextOnlySimpleLineBuilder::placeInlineTextContent(const Inlin

auto nextItemIndex = layoutRange.startIndex();
auto isAtSoftWrapOpportunityOrContentEnd = [&](auto& inlineTextItem) {
return hasWrapOpportunityBeforeWhitespace || inlineTextItem.isWhitespace() || nextItemIndex >= layoutRange.endIndex() || m_inlineItems[nextItemIndex].isLineBreak();
if (inlineTextItem.isWhitespace())
return true;
if (nextItemIndex >= layoutRange.endIndex() || m_inlineItems[nextItemIndex].isLineBreak())
return true;
auto& nextInlineTextItem = downcast<InlineTextItem>(m_inlineItems[nextItemIndex]);
if (nextInlineTextItem.isWhitespace())
return hasWrapOpportunityBeforeWhitespace;
return &inlineTextItem.inlineTextBox() == &nextInlineTextItem.inlineTextBox() || TextUtil::mayBreakInBetween(inlineTextItem, nextInlineTextItem);
};

// Handle leading partial content first (overflowing text from the previous line).
Expand Down Expand Up @@ -238,7 +245,8 @@ InlineItemPosition TextOnlySimpleLineBuilder::placeNonWrappingInlineTextContent(

TextOnlyLineBreakResult TextOnlySimpleLineBuilder::handleInlineTextContent(const InlineContentBreaker::ContinuousContent& candidateContent, const InlineItemRange& layoutRange)
{
auto availableWidth = (m_lineLogicalRect.width() + LayoutUnit::epsilon()) - m_line.contentLogicalRight();
auto contentLogicalRight = m_line.contentLogicalRight();
auto availableWidth = (m_lineLogicalRect.width() + LayoutUnit::epsilon()) - (!std::isnan(contentLogicalRight) ? contentLogicalRight : 0.f);
auto lineStatus = InlineContentBreaker::LineStatus { m_line.contentLogicalRight(), availableWidth, m_line.trimmableTrailingWidth(), m_line.trailingSoftHyphenWidth(), m_line.isTrailingRunFullyTrimmable(), m_line.hasContentOrListMarker(), !m_wrapOpportunityList.isEmpty() };
m_inlineContentBreaker.setIsInIntrinsicWidthMode(isInIntrinsicWidthMode());
auto lineBreakingResult = m_inlineContentBreaker.processInlineContent(candidateContent, lineStatus);
Expand All @@ -249,10 +257,8 @@ TextOnlyLineBreakResult TextOnlySimpleLineBuilder::handleInlineTextContent(const
auto& committedRuns = candidateContent.runs();
for (auto& run : committedRuns)
m_line.appendTextFast(downcast<InlineTextItem>(run.inlineItem), run.style, run.logicalWidth);

auto& trailingInlineItem = committedRuns.last().inlineItem;
if (m_line.hasContentOrListMarker() && downcast<InlineTextItem>(trailingInlineItem).isWhitespace())
m_wrapOpportunityList.append(&downcast<InlineTextItem>(trailingInlineItem));
if (m_line.hasContentOrListMarker())
m_wrapOpportunityList.append(&downcast<InlineTextItem>(committedRuns.last().inlineItem));
return { lineBreakingResult.isEndOfLine, committedRuns.size() };
}

Expand Down Expand Up @@ -284,6 +290,8 @@ TextOnlyLineBreakResult TextOnlySimpleLineBuilder::handleInlineTextContent(const
auto& trailingRun = runs[trailingRunIndex];
if (!lineBreakingResult.partialTrailingContent->partialRun) {
m_line.appendTextFast(downcast<InlineTextItem>(trailingRun.inlineItem), trailingRun.style, trailingRun.logicalWidth);
if (auto hyphenWidth = lineBreakingResult.partialTrailingContent->hyphenWidth)
m_line.addTrailingHyphen(*hyphenWidth);
return { InlineContentBreaker::IsEndOfLine::Yes, committedInlineItemCount, { } };
}

Expand All @@ -298,11 +306,18 @@ TextOnlyLineBreakResult TextOnlySimpleLineBuilder::handleInlineTextContent(const
return processPartialContent();
}

if (lineBreakingResult.action == InlineContentBreaker::Result::Action::RevertToLastWrapOpportunity) {
ASSERT(!m_wrapOpportunityList.isEmpty());
return { InlineContentBreaker::IsEndOfLine::Yes, rebuildLine(layoutRange, *m_wrapOpportunityList.last()), { }, true };
// Revert to a previous position cases.
if (m_wrapOpportunityList.isEmpty()) {
ASSERT_NOT_REACHED();
return { InlineContentBreaker::IsEndOfLine::Yes };
}

if (lineBreakingResult.action == InlineContentBreaker::Result::Action::RevertToLastWrapOpportunity)
return { InlineContentBreaker::IsEndOfLine::Yes, revertToTrailingItem(layoutRange, *m_wrapOpportunityList.last()), { }, true };

if (lineBreakingResult.action == InlineContentBreaker::Result::Action::RevertToLastNonOverflowingWrapOpportunity)
return { InlineContentBreaker::IsEndOfLine::Yes, revertToLastNonOverflowingItem(layoutRange), { }, true };

ASSERT_NOT_REACHED();
return { InlineContentBreaker::IsEndOfLine::Yes };
}
Expand All @@ -321,7 +336,7 @@ void TextOnlySimpleLineBuilder::handleLineEnding(InlineItemPosition placedConten
m_line.handleTrailingHangingContent(intrinsicWidthMode(), horizontalAvailableSpace, isLastLine);
}

size_t TextOnlySimpleLineBuilder::rebuildLine(const InlineItemRange& layoutRange, const InlineTextItem& trailingInlineItem)
size_t TextOnlySimpleLineBuilder::revertToTrailingItem(const InlineItemRange& layoutRange, const InlineTextItem& trailingInlineItem)
{
auto isFirstFormattedLine = this->isFirstFormattedLine();
m_line.initialize({ }, isFirstFormattedLine);
Expand All @@ -344,6 +359,29 @@ size_t TextOnlySimpleLineBuilder::rebuildLine(const InlineItemRange& layoutRange
return { };
}

size_t TextOnlySimpleLineBuilder::revertToLastNonOverflowingItem(const InlineItemRange& layoutRange)
{
// Revert all the way back to a wrap opportunity when either a soft hyphen fits or no hyphen is required.
for (auto i = m_wrapOpportunityList.size(); i--;) {
auto committedCount = revertToTrailingItem(layoutRange, *m_wrapOpportunityList[i]);
auto trailingSoftHyphenWidth = m_line.trailingSoftHyphenWidth();

auto hasRevertedEnough = [&] {
if (!i || !trailingSoftHyphenWidth)
return true;
auto availableWidth = m_lineLogicalRect.width() - m_line.contentLogicalRight();
return *trailingSoftHyphenWidth <= availableWidth;
};
if (hasRevertedEnough()) {
if (trailingSoftHyphenWidth)
m_line.addTrailingHyphen(*trailingSoftHyphenWidth);
return committedCount;
}
}
ASSERT_NOT_REACHED();
return 0;
}

const ElementBox& TextOnlySimpleLineBuilder::root() const
{
return formattingContext().root();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class TextOnlySimpleLineBuilder : public AbstractLineBuilder {
TextOnlyLineBreakResult handleInlineTextContent(const InlineContentBreaker::ContinuousContent&, const InlineItemRange&);
void initialize(const InlineItemRange&, const InlineRect& initialLogicalRect, const std::optional<PreviousLine>&);
void handleLineEnding(InlineItemPosition, size_t layoutRangeEndIndex);
size_t rebuildLine(const InlineItemRange&, const InlineTextItem&);
size_t revertToTrailingItem(const InlineItemRange&, const InlineTextItem&);
size_t revertToLastNonOverflowingItem(const InlineItemRange&);

bool isFirstFormattedLine() const { return !m_previousLine.has_value(); }

Expand Down
31 changes: 31 additions & 0 deletions Source/WebCore/layout/formattingContexts/inline/text/TextUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,37 @@ TextUtil::WordBreakLeft TextUtil::breakWord(const InlineTextBox& inlineTextBox,
return leftSide;
}

bool TextUtil::mayBreakInBetween(const InlineTextItem& previousInlineItem, const InlineTextItem& nextInlineItem)
{
// Check if these 2 adjacent non-whitespace inline items are connected at a breakable position.
ASSERT(!previousInlineItem.isWhitespace() && !nextInlineItem.isWhitespace());

auto previousContent = previousInlineItem.inlineTextBox().content();
auto nextContent = nextInlineItem.inlineTextBox().content();
// Now we need to collect at least 3 adjacent characters to be able to make a decision whether the previous text item ends with breaking opportunity.
// [ex-][ample] <- second to last[x] last[-] current[a]
// We need at least 1 character in the current inline text item and 2 more from previous inline items.
if (!previousContent.is8Bit()) {
// FIXME: Remove this workaround when we move over to a better way of handling prior-context with Unicode.
// See the templated CharacterType in nextBreakablePosition for last and lastlast characters.
nextContent.convertTo16Bit();
}
auto& previousContentStyle = previousInlineItem.style();
auto& nextContentStyle = nextInlineItem.style();
auto lineBreakIteratorFactory = CachedLineBreakIteratorFactory { nextContent, nextContentStyle.computedLocale(), TextUtil::lineBreakIteratorMode(nextContentStyle.lineBreak()), TextUtil::contentAnalysis(nextContentStyle.wordBreak()) };
auto previousContentLength = previousContent.length();
// FIXME: We should look into the entire uncommitted content for more text context.
UChar lastCharacter = previousContentLength ? previousContent[previousContentLength - 1] : 0;
if (lastCharacter == softHyphen && previousContentStyle.hyphens() == Hyphens::None)
return false;
UChar secondToLastCharacter = previousContentLength > 1 ? previousContent[previousContentLength - 2] : 0;
lineBreakIteratorFactory.priorContext().set({ secondToLastCharacter, lastCharacter });
// Now check if we can break right at the inline item boundary.
// With the [ex-ample], findNextBreakablePosition should return the startPosition (0).
// FIXME: Check if there's a more correct way of finding breaking opportunities.
return !findNextBreakablePosition(lineBreakIteratorFactory, 0, nextContentStyle);
}

unsigned TextUtil::findNextBreakablePosition(CachedLineBreakIteratorFactory& lineBreakIteratorFactory, unsigned startPosition, const RenderStyle& style)
{
auto keepAllWordsForCJK = style.wordBreak() == WordBreak::KeepAll;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class TextUtil {
static WordBreakLeft breakWord(const InlineTextBox&, size_t start, size_t length, InlineLayoutUnit width, InlineLayoutUnit availableWidth, InlineLayoutUnit contentLogicalLeft, const FontCascade&);
static WordBreakLeft breakWord(const InlineTextItem&, const FontCascade&, InlineLayoutUnit textWidth, InlineLayoutUnit availableWidth, InlineLayoutUnit contentLogicalLeft);

static bool mayBreakInBetween(const InlineTextItem& previousInlineItem, const InlineTextItem& nextInlineItem);
static unsigned findNextBreakablePosition(CachedLineBreakIteratorFactory&, unsigned startPosition, const RenderStyle&);
static TextBreakIterator::LineMode::Behavior lineBreakIteratorMode(LineBreak);
static TextBreakIterator::ContentAnalysis contentAnalysis(WordBreak);
Expand Down

0 comments on commit bc0673f

Please sign in to comment.