Skip to content

Commit

Permalink
Move and expose various helper functions into InlineFormattingGeometry
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=259908
rdar://113533527

Reviewed by Alan Baradlay.

The following functions need to be moved and be accessible via
InlineFormattingGeometry for upcoming work on text-wrap: balance.
 - leadingInlineItemPositionForNextLine (static function in InlineFormattingContext.cpp)
 - LineBuilder::inlineItemWidth
 - LineBuilder::nextWrapOpportunity

* Source/WebCore/layout/formattingContexts/inline/InlineFormattingContext.cpp:
(WebCore::Layout::InlineFormattingContext::lineLayout):
(WebCore::Layout::InlineFormattingContext::computedIntrinsicWidthForConstraint const):
(WebCore::Layout::leadingInlineItemPositionForNextLine): Deleted.
* Source/WebCore/layout/formattingContexts/inline/InlineFormattingGeometry.cpp:
(WebCore::Layout::InlineFormattingGeometry::leadingInlineItemPositionForNextLine):
(WebCore::Layout::InlineFormattingGeometry::inlineItemWidth const):
(WebCore::Layout::endsWithSoftWrapOpportunity):
(WebCore::Layout::isAtSoftWrapOpportunity):
(WebCore::Layout::InlineFormattingGeometry::nextWrapOpportunity):
* Source/WebCore/layout/formattingContexts/inline/InlineFormattingGeometry.h:
* Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.cpp:
(WebCore::Layout::LineBuilder::candidateContentForLine):
(WebCore::Layout::LineBuilder::rebuildLineWithInlineContent):
(WebCore::Layout::endsWithSoftWrapOpportunity): Deleted.
(WebCore::Layout::isAtSoftWrapOpportunity): Deleted.
(WebCore::Layout::LineBuilder::inlineItemWidth const): Deleted.
(WebCore::Layout::LineBuilder::nextWrapOpportunity const): Deleted.
* Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.h:

Canonical link: https://commits.webkit.org/266922@main
  • Loading branch information
RWDavid authored and Brent Fulgham committed Aug 15, 2023
1 parent b603ea0 commit fed29de
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 250 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -162,24 +162,6 @@ LayoutUnit InlineFormattingContext::maximumContentSize()
return ceiledLayoutUnit(computedIntrinsicWidthForConstraint(IntrinsicWidthMode::Maximum, lineBuilder));
}

static InlineItemPosition leadingInlineItemPositionForNextLine(InlineItemPosition lineContentEnd, std::optional<InlineItemPosition> previousLineTrailingInlineItemPosition, InlineItemPosition layoutRangeEnd)
{
if (!previousLineTrailingInlineItemPosition)
return lineContentEnd;
if (previousLineTrailingInlineItemPosition->index < lineContentEnd.index || (previousLineTrailingInlineItemPosition->index == lineContentEnd.index && previousLineTrailingInlineItemPosition->offset < lineContentEnd.offset)) {
// Either full or partial advancing.
return lineContentEnd;
}
if (previousLineTrailingInlineItemPosition->index == lineContentEnd.index && !previousLineTrailingInlineItemPosition->offset && !lineContentEnd.offset) {
// Can't mangage to put any content on line (most likely due to floats). Note that this only applies to full content.
return lineContentEnd;
}
// This looks like a partial content and we are stuck. Let's force-move over to the next inline item.
// We certainly lose some content, but we would be busy looping otherwise.
ASSERT_NOT_REACHED();
return { std::min(lineContentEnd.index + 1, layoutRangeEnd.index), { } };
}

static bool mayExitFromPartialLayout(const InlineDamage& lineDamage, size_t lineIndex, const InlineDisplay::Boxes& newContent)
{
if (lineDamage.start()->lineIndex == lineIndex) {
Expand Down Expand Up @@ -228,7 +210,7 @@ InlineLayoutResult InlineFormattingContext::lineLayout(AbstractLineBuilder& line
inlineLayoutState.setClearGapAfterLastLine(formattingGeometry().logicalTopForNextLine(lineLayoutResult, lineLogicalRect, floatingContext) - lineLogicalRect.bottom());

auto lineContentEnd = lineLayoutResult.inlineItemRange.end;
leadingInlineItemPosition = leadingInlineItemPositionForNextLine(lineContentEnd, previousLineEnd, needsLayoutRange.end);
leadingInlineItemPosition = InlineFormattingGeometry::leadingInlineItemPositionForNextLine(lineContentEnd, previousLineEnd, needsLayoutRange.end);
auto isLastLine = leadingInlineItemPosition == needsLayoutRange.end && lineLayoutResult.floatContent.suspendedFloats.isEmpty();
if (isLastLine) {
layoutResult.range = !isPartialLayout ? InlineLayoutResult::Range::Full : InlineLayoutResult::Range::FullFromDamage;
Expand Down Expand Up @@ -321,7 +303,7 @@ InlineLayoutUnit InlineFormattingContext::computedIntrinsicWidthForConstraint(In
};
maximumContentWidth = std::max(maximumContentWidth, lineLayoutResult.lineGeometry.logicalTopLeft.x() + lineLayoutResult.contentGeometry.logicalWidth + floatContentWidth());

layoutRange.start = leadingInlineItemPositionForNextLine(lineLayoutResult.inlineItemRange.end, previousLineEnd, layoutRange.end);
layoutRange.start = InlineFormattingGeometry::leadingInlineItemPositionForNextLine(lineLayoutResult.inlineItemRange.end, previousLineEnd, layoutRange.end);
if (layoutRange.isEmpty()) {
auto shouldCacheLineBreakingResultForSubsequentLayout = !lineIndex && mayCacheLayoutResult == MayCacheLayoutResult::Yes;
if (shouldCacheLineBreakingResultForSubsequentLayout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,246 @@ InlineLayoutUnit InlineFormattingGeometry::horizontalAlignmentOffset(const Rende
return { };
}

InlineItemPosition InlineFormattingGeometry::leadingInlineItemPositionForNextLine(InlineItemPosition lineContentEnd, std::optional<InlineItemPosition> previousLineTrailingInlineItemPosition, InlineItemPosition layoutRangeEnd)
{
if (!previousLineTrailingInlineItemPosition)
return lineContentEnd;
if (previousLineTrailingInlineItemPosition->index < lineContentEnd.index || (previousLineTrailingInlineItemPosition->index == lineContentEnd.index && previousLineTrailingInlineItemPosition->offset < lineContentEnd.offset)) {
// Either full or partial advancing.
return lineContentEnd;
}
if (previousLineTrailingInlineItemPosition->index == lineContentEnd.index && !previousLineTrailingInlineItemPosition->offset && !lineContentEnd.offset) {
// Can't mangage to put any content on line (most likely due to floats). Note that this only applies to full content.
return lineContentEnd;
}
// This looks like a partial content and we are stuck. Let's force-move over to the next inline item.
// We certainly lose some content, but we would be busy looping otherwise.
ASSERT_NOT_REACHED();
return { std::min(lineContentEnd.index + 1, layoutRangeEnd.index), { } };
}

InlineLayoutUnit InlineFormattingGeometry::inlineItemWidth(const InlineItem& inlineItem, InlineLayoutUnit contentLogicalLeft, bool useFirstLineStyle) const
{
ASSERT(inlineItem.layoutBox().isInlineLevelBox());
if (is<InlineTextItem>(inlineItem)) {
auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
if (auto contentWidth = inlineTextItem.width())
return *contentWidth;
auto& fontCascade = useFirstLineStyle ? inlineTextItem.firstLineStyle().fontCascade() : inlineTextItem.style().fontCascade();
if (!inlineTextItem.isWhitespace() || InlineTextItem::shouldPreserveSpacesAndTabs(inlineTextItem))
return TextUtil::width(inlineTextItem, fontCascade, contentLogicalLeft);
return TextUtil::width(inlineTextItem, fontCascade, inlineTextItem.start(), inlineTextItem.start() + 1, contentLogicalLeft);
}

if (inlineItem.isLineBreak() || inlineItem.isWordBreakOpportunity())
return { };

auto& layoutBox = inlineItem.layoutBox();
auto& boxGeometry = formattingContext().geometryForBox(layoutBox);

if (layoutBox.isReplacedBox())
return boxGeometry.marginBoxWidth();

if (inlineItem.isInlineBoxStart()) {
auto logicalWidth = boxGeometry.marginStart() + boxGeometry.borderStart() + boxGeometry.paddingStart().value_or(0);
#if ENABLE(CSS_BOX_DECORATION_BREAK)
auto& style = useFirstLineStyle ? inlineItem.firstLineStyle() : inlineItem.style();
if (style.boxDecorationBreak() == BoxDecorationBreak::Clone)
logicalWidth += boxGeometry.borderEnd() + boxGeometry.paddingEnd().value_or(0_lu);
#endif
return logicalWidth;
}

if (inlineItem.isInlineBoxEnd())
return boxGeometry.marginEnd() + boxGeometry.borderEnd() + boxGeometry.paddingEnd().value_or(0);

// FIXME: The overhang should be computed to not overlap the neighboring runs or overflow the line.
if (auto* rubyAdjustments = layoutBox.rubyAdjustments()) {
auto& overhang = useFirstLineStyle ? rubyAdjustments->firstLineOverhang : rubyAdjustments->overhang;
return boxGeometry.marginBoxWidth() - (overhang.start + overhang.end);
}

// Non-replaced inline box (e.g. inline-block)
return boxGeometry.marginBoxWidth();
}

static inline bool endsWithSoftWrapOpportunity(const InlineTextItem& currentTextItem, const InlineTextItem& nextInlineTextItem)
{
ASSERT(!nextInlineTextItem.isWhitespace());
// We are at the position after a whitespace.
if (currentTextItem.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())
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 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);
}

static inline bool isAtSoftWrapOpportunity(const InlineItem& current, const InlineItem& next)
{
// FIXME: Transition no-wrapping logic from InlineContentBreaker to here where we compute the soft wrap opportunity indexes.
// "is at" simple means that there's a soft wrap opportunity right after the [current].
// [text][ ][text][inline box start]... (<div>text content<span>..</div>)
// soft wrap indexes: 0 and 1 definitely, 2 depends on the content after the [inline box start].

// https://drafts.csswg.org/css-text-3/#line-break-details
// Figure out if the new incoming content puts the uncommitted content on a soft wrap opportunity.
// e.g. [inline box start][prior_continuous_content][inline box end] (<span>prior_continuous_content</span>)
// An incoming <img> box would enable us to commit the "<span>prior_continuous_content</span>" content
// but an incoming text content would not necessarily.
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() && nextInlineTextItem.isWhitespace()) {
// <span> </span><span> </span>. Depending on the styles, there may or may not be a soft wrap opportunity between these 2 whitespace content.
return TextUtil::isWrappingAllowed(currentInlineTextItem.style()) || TextUtil::isWrappingAllowed(nextInlineTextItem.style());
}
if (currentInlineTextItem.isWhitespace()) {
// " <span>text</span>" : after [whitespace] position is a soft wrap opportunity.
return TextUtil::isWrappingAllowed(currentInlineTextItem.style());
}
if (nextInlineTextItem.isWhitespace()) {
// "<span>text</span> "
// 'white-space: break-spaces' and '-webkit-line-break: after-white-space': line breaking opportunity exists after every preserved white space character, but not before.
auto& style = nextInlineTextItem.style();
return TextUtil::isWrappingAllowed(style) && style.whiteSpaceCollapse() != WhiteSpaceCollapse::BreakSpaces && style.lineBreak() != LineBreak::AfterWhiteSpace;
}
if (current.style().lineBreak() == LineBreak::Anywhere || next.style().lineBreak() == LineBreak::Anywhere) {
// There is a soft wrap opportunity around every typographic character unit, including around any punctuation character
// or preserved white spaces, or in the middle of words.
return true;
}
// Both current and next items are non-whitespace text.
// [text][text] : is a continuous content.
// [text-][text] : after [hyphen] position is a soft wrap opportunity.
return endsWithSoftWrapOpportunity(currentInlineTextItem, nextInlineTextItem);
}
if (current.layoutBox().isListMarkerBox() || next.layoutBox().isListMarkerBox())
return true;
if (current.isBox() || next.isBox()) {
// [text][inline box start][inline box end][inline box] (text<span></span><img>) : there's a soft wrap opportunity between the [text] and [img].
// The line breaking behavior of a replaced element or other atomic inline is equivalent to an ideographic character.
return true;
}
ASSERT_NOT_REACHED();
return true;
}

size_t InlineFormattingGeometry::nextWrapOpportunity(size_t startIndex, const InlineItemRange& layoutRange, const InlineItems& inlineItems)
{
// 1. Find the start candidate by skipping leading non-content items e.g "<span><span>start". Opportunity is after "<span><span>".
// 2. Find the end candidate by skipping non-content items inbetween e.g. "<span><span>start</span>end". Opportunity is after "</span>".
// 3. Check if there's a soft wrap opportunity between the 2 candidate inline items and repeat.
// 4. Any force line break/explicit wrap content inbetween is considered as wrap opportunity.

// [ex-][inline box start][inline box end][float][ample] (ex-<span></span><div style="float:left"></div>ample). Wrap index is at [ex-].
// [ex][inline box start][amp-][inline box start][le] (ex<span>amp-<span>ample). Wrap index is at [amp-].
// [ex-][inline box start][line break][ample] (ex-<span><br>ample). Wrap index is after [br].
auto previousInlineItemIndex = std::optional<size_t> { };
for (auto index = startIndex; index < layoutRange.endIndex(); ++index) {
auto& inlineItem = inlineItems[index];
if (inlineItem.isLineBreak() || inlineItem.isWordBreakOpportunity()) {
// We always stop at explicit wrapping opportunities e.g. <br>. However the wrap position may be at later position.
// e.g. <span><span><br></span></span> <- wrap position is after the second </span>
// but in case of <span><br><span></span></span> <- wrap position is right after <br>.
for (++index; index < layoutRange.endIndex() && inlineItems[index].isInlineBoxEnd(); ++index) { }
return index;
}
if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd()) {
// Need to see what comes next to decide.
continue;
}
ASSERT(inlineItem.isText() || inlineItem.isBox() || inlineItem.isFloat());
if (inlineItem.isFloat()) {
// While floats are not part of the inline content and they are not supposed to introduce soft wrap opportunities,
// e.g. [text][float box][float box][text][float box][text] is essentially just [text][text][text]
// figuring out whether a float (or set of floats) should stay on the line or not (and handle potentially out of order inline items)
// brings in unnecessary complexity.
// For now let's always treat a float as a soft wrap opportunity.
auto wrappingPosition = index == startIndex ? std::min(index + 1, layoutRange.endIndex()) : index;
return wrappingPosition;
}
if (!previousInlineItemIndex) {
previousInlineItemIndex = index;
continue;
}
// At this point previous and current items are not necessarily adjacent items e.g "previous<span>current</span>"
auto& previousItem = inlineItems[*previousInlineItemIndex];
auto& currentItem = inlineItems[index];
if (isAtSoftWrapOpportunity(previousItem, currentItem)) {
if (*previousInlineItemIndex + 1 == index && (!previousItem.isText() || !currentItem.isText())) {
// We only know the exact soft wrap opportunity index when the previous and current items are next to each other.
return index;
}
// There's a soft wrap opportunity between 'previousInlineItemIndex' and 'index'.
// Now forward-find from the start position to see where we can actually wrap.
// [ex-][ample] vs. [ex-][inline box start][inline box end][ample]
// where [ex-] is previousInlineItemIndex and [ample] is index.

// inline content and their inline boxes form unbreakable content.
// ex-<span></span>ample : wrap opportunity is after "ex-<span></span>".
// ex-<span>ample : wrap opportunity is after "ex-".
// ex-<span><span></span></span>ample : wrap opportunity is after "ex-<span><span></span></span>".
// ex-</span></span>ample : wrap opportunity is after "ex-</span></span>".
// ex-</span><span>ample : wrap opportunity is after "ex-</span>".
// ex-<span><span>ample : wrap opportunity is after "ex-".
struct InlineBoxPosition {
const Box* inlineBox { nullptr };
size_t index { 0 };
};
Vector<InlineBoxPosition> inlineBoxStack;
auto start = *previousInlineItemIndex;
auto end = index;
// Soft wrap opportunity is at the first inline box that encloses the trailing content.
for (auto candidateIndex = start + 1; candidateIndex < end; ++candidateIndex) {
auto& inlineItem = inlineItems[candidateIndex];
ASSERT(inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd());
if (inlineItem.isInlineBoxStart())
inlineBoxStack.append({ &inlineItem.layoutBox(), candidateIndex });
else if (inlineItem.isInlineBoxEnd() && !inlineBoxStack.isEmpty())
inlineBoxStack.removeLast();
}
return inlineBoxStack.isEmpty() ? index : inlineBoxStack.first().index;
}
previousInlineItemIndex = index;
}
return layoutRange.endIndex();
}


}
}

Loading

0 comments on commit fed29de

Please sign in to comment.