04e038912ea20936ef9ee21eafbce186b835e7ab
[WebKit-https.git] / Source / WebCore / layout / inlineformatting / LineLayoutContext.cpp
1 /*
2  * Copyright (C) 2019 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "LineLayoutContext.h"
28
29 #if ENABLE(LAYOUT_FORMATTING_CONTEXT)
30
31 #include "InlineFormattingContext.h"
32 #include "LayoutBox.h"
33 #include "TextUtil.h"
34
35 namespace WebCore {
36 namespace Layout {
37
38 static inline bool endsWithSoftWrapOpportunity(const InlineTextItem& currentTextItem, const InlineTextItem& nextInlineTextItem)
39 {
40     ASSERT(!nextInlineTextItem.isWhitespace());
41     // We are at the position after a whitespace.
42     if (currentTextItem.isWhitespace())
43         return true;
44     // When both these non-whitespace runs belong to the same layout box, it's guaranteed that
45     // they are split at a soft breaking opportunity. See InlineTextItem::moveToNextBreakablePosition.
46     if (&currentTextItem.inlineTextBox() == &nextInlineTextItem.inlineTextBox())
47         return true;
48     // 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.
49     // [ex-][ample] <- second to last[x] last[-] current[a]
50     // We need at least 1 character in the current inline text item and 2 more from previous inline items.
51     auto previousContent = currentTextItem.inlineTextBox().content();
52     auto lineBreakIterator = LazyLineBreakIterator { nextInlineTextItem.inlineTextBox().content() };
53     auto previousContentLength = previousContent.length();
54     // FIXME: We should look into the entire uncommitted content for more text context.
55     UChar lastCharacter = previousContentLength ? previousContent[previousContentLength - 1] : 0;
56     UChar secondToLastCharacter = previousContentLength > 1 ? previousContent[previousContentLength - 2] : 0;
57     lineBreakIterator.setPriorContext(lastCharacter, secondToLastCharacter);
58     // Now check if we can break right at the inline item boundary.
59     // With the [ex-ample], findNextBreakablePosition should return the startPosition (0).
60     // FIXME: Check if there's a more correct way of finding breaking opportunities.
61     return !TextUtil::findNextBreakablePosition(lineBreakIterator, 0, nextInlineTextItem.style());
62 }
63
64 static inline bool isAtSoftWrapOpportunity(const InlineItem& current, const InlineItem& next)
65 {
66     // "is at" simple means that there's a soft wrap opportunity right after the [current].
67     // [text][ ][text][container start]... (<div>text content<span>..</div>)
68     // soft wrap indexes: 0 and 1 definitely, 2 depends on the content after the [container start].
69
70     // https://drafts.csswg.org/css-text-3/#line-break-details
71     // Figure out if the new incoming content puts the uncommitted content on a soft wrap opportunity.
72     // e.g. [container start][prior_continuous_content][container end] (<span>prior_continuous_content</span>)
73     // An incoming <img> box would enable us to commit the "<span>prior_continuous_content</span>" content
74     // but an incoming text content would not necessarily.
75     ASSERT(current.isText() || current.isBox());
76     ASSERT(next.isText() || next.isBox());
77     if (current.isText() && next.isText()) {
78         auto& currentInlineTextItem = downcast<InlineTextItem>(current);
79         auto& nextInlineTextItem = downcast<InlineTextItem>(next);
80         if (currentInlineTextItem.isWhitespace()) {
81             // [ ][text] : after [whitespace] position is a soft wrap opportunity.
82             return true;
83         }
84         if (nextInlineTextItem.isWhitespace()) {
85             // [text][ ] (<span>text</span> )
86             // white-space: break-spaces: line breaking opportunity exists after every preserved white space character, but not before.
87             return nextInlineTextItem.style().whiteSpace() != WhiteSpace::BreakSpaces;
88         }
89         if (current.style().lineBreak() == LineBreak::Anywhere || next.style().lineBreak() == LineBreak::Anywhere) {
90             // There is a soft wrap opportunity around every typographic character unit, including around any punctuation character
91             // or preserved white spaces, or in the middle of words.
92             return true;
93         }
94         // Both current and next items are non-whitespace text.
95         // [text][text] : is a continuous content.
96         // [text-][text] : after [hyphen] position is a soft wrap opportunity.
97         return endsWithSoftWrapOpportunity(currentInlineTextItem, nextInlineTextItem);
98     }
99     if (current.isBox() || next.isBox()) {
100         // [text][container start][container end][inline box] (text<span></span><img>) : there's a soft wrap opportunity between the [text] and [img].
101         // The line breaking behavior of a replaced element or other atomic inline is equivalent to an ideographic character.
102         return true;
103     }
104     ASSERT_NOT_REACHED();
105     return true;
106 }
107
108 static inline size_t nextWrapOpportunity(const InlineItems& inlineContent, size_t startIndex, const LineLayoutContext::InlineItemRange layoutRange)
109 {
110     // 1. Find the start candidate by skipping leading non-content items e.g <span><span>start : skip "<span><span>"
111     // 2. Find the end candidate by skipping non-content items inbetween e.g. <span><span>start</span>end: skip "</span>"
112     // 3. Check if there's a soft wrap opportunity between the 2 candidate inline items and repeat.
113     // 4. Any force line break inbetween is considered as a wrap opportunity.
114
115     // [ex-][container start][container end][float][ample] (ex-<span></span><div style="float:left"></div>ample) : wrap index is at [ex-].
116     // [ex][container start][amp-][container start][le] (ex<span>amp-<span>ample) : wrap index is at [amp-].
117     // [ex-][container start][line break][ample] (ex-<span><br>ample) : wrap index is after [br].
118     auto isAtLineBreak = false;
119
120     auto inlineItemIndexWithContent = [&] (auto index) {
121         // Note that floats are not part of the inline content. We should treat them as if they were not here as far as wrap opportunities are concerned.
122         // [text][float box][text] is essentially just [text][text]
123         for (; index < layoutRange.end; ++index) {
124             auto& inlineItem = inlineContent[index];
125             if (inlineItem.isText() || inlineItem.isBox())
126                 return index;
127             if (inlineItem.isLineBreak()) {
128                 isAtLineBreak = true;
129                 return index;
130             }
131         }
132         return layoutRange.end;
133     };
134
135     // Start at the first inline item with content.
136     // [container start][ex-] : start at [ex-]
137     auto startContentIndex = inlineItemIndexWithContent(startIndex);
138     if (isAtLineBreak) {
139         // Content starts with a line break. The wrap position is after the line break.
140         return startContentIndex + 1;
141     }
142
143     while (startContentIndex < layoutRange.end) {
144         // 1. Find the next inline item with content.
145         // 2. Check if there's a soft wrap opportunity between the start and the next inline item.
146         auto nextContentIndex = inlineItemIndexWithContent(startContentIndex + 1);
147         if (nextContentIndex == layoutRange.end)
148             return nextContentIndex;
149         if (isAtLineBreak) {
150             // We always stop at line breaks. The wrap position is after the line break.
151             return nextContentIndex + 1;
152         }
153         if (isAtSoftWrapOpportunity(inlineContent[startContentIndex], inlineContent[nextContentIndex])) {
154             // There's a soft wrap opportunity between the start and the nextContent.
155             // Now forward-find from the start position to see where we can actually wrap.
156             // [ex-][ample] vs. [ex-][container start][container end][ample]
157             // where [ex-] is startContent and [ample] is the nextContent.
158             for (auto candidateIndex = startContentIndex + 1; candidateIndex < nextContentIndex; ++candidateIndex) {
159                 if (inlineContent[candidateIndex].isContainerStart()) {
160                     // inline content and [container start] and [container end] form unbreakable content.
161                     // ex-<span></span>ample  : wrap opportunity is after "ex-".
162                     // ex-</span></span>ample : wrap opportunity is after "ex-</span></span>".
163                     // ex-</span><span>ample</span> : wrap opportunity is after "ex-</span>".
164                     // ex-<span><span>ample</span></span> : wrap opportunity is after "ex-".
165                     return candidateIndex;
166                 }
167             }
168             return nextContentIndex;
169         }
170         startContentIndex = nextContentIndex;
171     }
172     return layoutRange.end;
173 }
174
175 struct LineCandidate {
176     void reset();
177
178     struct InlineContent {
179         const LineBreaker::RunList& runs() const { return m_inlineRuns; }
180         InlineLayoutUnit logicalWidth() const { return m_LogicalWidth; }
181         const InlineItem* trailingLineBreak() const { return m_trailingLineBreak; }
182
183         void appendInlineItem(const InlineItem&, InlineLayoutUnit logicalWidth);
184         void appendLineBreak(const InlineItem& inlineItem) { setTrailingLineBreak(inlineItem); }
185         void reset();
186
187     private:
188         void setTrailingLineBreak(const InlineItem& lineBreakItem) { m_trailingLineBreak = &lineBreakItem; }
189
190         InlineLayoutUnit m_LogicalWidth { 0 };
191         LineBreaker::RunList m_inlineRuns;
192         const InlineItem* m_trailingLineBreak { nullptr };
193     };
194
195     struct FloatContent {
196         void append(const InlineItem& floatItem, InlineLayoutUnit logicalWidth, bool isIntrusive);
197
198         struct Float {
199             const InlineItem* item { nullptr };
200             InlineLayoutUnit logicalWidth { 0 };
201             bool isIntrusive { true };
202         };
203         using FloatList = Vector<Float>;
204         const FloatList& list() const { return m_floatList; }
205         InlineLayoutUnit intrusiveWidth() const { return m_intrusiveWidth; }
206
207         void reset();
208
209     private:
210         FloatList m_floatList;
211         InlineLayoutUnit m_intrusiveWidth { 0 };
212     };
213     // Candidate content is a collection of inline items and/or float boxes.
214     InlineContent inlineContent;
215     FloatContent floatContent;
216 };
217
218 inline void LineCandidate::InlineContent::appendInlineItem(const InlineItem& inlineItem, InlineLayoutUnit logicalWidth)
219 {
220     m_LogicalWidth += logicalWidth;
221     m_inlineRuns.append({ inlineItem, logicalWidth });
222 }
223
224 inline void LineCandidate::InlineContent::reset()
225 {
226     m_LogicalWidth = { };
227     m_inlineRuns.clear();
228     m_trailingLineBreak = { };
229 }
230
231 inline void LineCandidate::FloatContent::append(const InlineItem& floatItem, InlineLayoutUnit logicalWidth, bool isIntrusive)
232 {
233     if (isIntrusive)
234         m_intrusiveWidth += logicalWidth;
235     m_floatList.append({ &floatItem, logicalWidth, isIntrusive });
236 }
237
238 inline void LineCandidate::FloatContent::reset()
239 {
240     m_floatList.clear();
241     m_intrusiveWidth = { };
242 }
243
244 inline void LineCandidate::reset()
245 {
246     floatContent.reset();
247     inlineContent.reset();
248 }
249
250 InlineLayoutUnit LineLayoutContext::inlineItemWidth(const InlineItem& inlineItem, InlineLayoutUnit contentLogicalLeft) const
251 {
252     if (is<InlineTextItem>(inlineItem)) {
253         auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
254         if (auto contentWidth = inlineTextItem.width())
255             return *contentWidth;
256         auto end = inlineTextItem.isCollapsible() ? inlineTextItem.start() + 1 : inlineTextItem.end();
257         return TextUtil::width(inlineTextItem, inlineTextItem.start(), end, contentLogicalLeft);
258     }
259
260     if (inlineItem.isLineBreak())
261         return 0;
262
263     auto& layoutBox = inlineItem.layoutBox();
264     auto& boxGeometry = m_inlineFormattingContext.geometryForBox(layoutBox);
265
266     if (layoutBox.isFloatingPositioned())
267         return boxGeometry.marginBoxWidth();
268
269     if (layoutBox.isReplacedBox())
270         return boxGeometry.width();
271
272     if (inlineItem.isContainerStart())
273         return boxGeometry.marginStart() + boxGeometry.borderLeft() + boxGeometry.paddingLeft().valueOr(0);
274
275     if (inlineItem.isContainerEnd())
276         return boxGeometry.marginEnd() + boxGeometry.borderRight() + boxGeometry.paddingRight().valueOr(0);
277
278     // Non-replaced inline box (e.g. inline-block)
279     return boxGeometry.width();
280 }
281
282 LineLayoutContext::LineLayoutContext(const InlineFormattingContext& inlineFormattingContext, const ContainerBox& formattingContextRoot, const InlineItems& inlineItems)
283     : m_inlineFormattingContext(inlineFormattingContext)
284     , m_formattingContextRoot(formattingContextRoot)
285     , m_inlineItems(inlineItems)
286 {
287 }
288
289 LineLayoutContext::LineContent LineLayoutContext::layoutLine(LineBuilder& line, const InlineItemRange layoutRange, Optional<unsigned> partialLeadingContentLength)
290 {
291     ASSERT(m_floats.isEmpty());
292     m_partialLeadingTextItem = { };
293     m_lastWrapOpportunityItem = { };
294     auto lineBreaker = LineBreaker { };
295     auto currentItemIndex = layoutRange.start;
296     unsigned committedInlineItemCount = 0;
297     auto lineCandidate = LineCandidate { };
298     while (currentItemIndex < layoutRange.end) {
299         // 1. Collect the set of runs that we can commit to the line as one entity e.g. <span>text_and_span_start_span_end</span>.
300         // 2. Apply floats and shrink the available horizontal space e.g. <span>intru_<div style="float: left"></div>sive_float</span>.
301         // 3. Check if the content fits the line and commit the content accordingly (full, partial or not commit at all).
302         // 4. Return if we are at the end of the line either by not being able to fit more content or because of an explicit line break.
303         nextContentForLine(lineCandidate, currentItemIndex, layoutRange, partialLeadingContentLength, line.availableWidth() + line.trimmableTrailingWidth(), line.lineBox().logicalWidth());
304         // Now check if we can put this content on the current line.
305         auto result = handleFloatsAndInlineContent(lineBreaker, line, layoutRange, lineCandidate);
306         committedInlineItemCount = result.committedCount.isRevert ? result.committedCount.value : committedInlineItemCount + result.committedCount.value;
307         auto& inlineContent = lineCandidate.inlineContent;
308         auto inlineContentIsFullyCommitted = inlineContent.runs().size() == result.committedCount.value && !result.partialContent;
309         auto isEndOfLine = result.isEndOfLine == LineBreaker::IsEndOfLine::Yes;
310
311         if (inlineContentIsFullyCommitted && inlineContent.trailingLineBreak()) {
312             // Fully commited (or empty) content followed by a line break means "end of line".
313             line.append(*inlineContent.trailingLineBreak(), { });
314             ++committedInlineItemCount;
315             isEndOfLine = true;
316         }
317         if (isEndOfLine) {
318             // We can't place any more items on the current line.
319             return close(line, layoutRange, committedInlineItemCount, result.partialContent);
320         }
321         currentItemIndex = layoutRange.start + committedInlineItemCount + m_floats.size();
322         partialLeadingContentLength = { };
323     }
324     // Looks like we've run out of runs.
325     return close(line, layoutRange, committedInlineItemCount, { });
326 }
327
328 LineLayoutContext::LineContent LineLayoutContext::close(LineBuilder& line, const InlineItemRange layoutRange, unsigned committedInlineItemCount, Optional<LineContent::PartialContent> partialContent)
329 {
330     ASSERT(committedInlineItemCount || line.hasIntrusiveFloat());
331     if (!committedInlineItemCount) {
332         if (m_floats.isEmpty()) {
333             // We didn't manage to add a run or a float at this vertical position.
334             return LineContent { { }, { }, WTFMove(m_floats), line.close(), line.lineBox() };
335         }
336         auto trailingInlineItemIndex = layoutRange.start + m_floats.size();
337         return LineContent { trailingInlineItemIndex, { }, WTFMove(m_floats), line.close(), line.lineBox() };
338     }
339     // Adjust hyphenated line count.
340     if (partialContent && partialContent->trailingContentHasHyphen)
341         ++m_successiveHyphenatedLineCount;
342     else
343         m_successiveHyphenatedLineCount = 0;
344     ASSERT(committedInlineItemCount);
345     auto trailingInlineItemIndex = layoutRange.start + committedInlineItemCount + m_floats.size() - 1;
346     ASSERT(trailingInlineItemIndex < layoutRange.end);
347     auto isLastLineWithInlineContent = [&] {
348         if (trailingInlineItemIndex == layoutRange.end - 1)
349             return LineBuilder::IsLastLineWithInlineContent::Yes;
350         if (partialContent)
351             return LineBuilder::IsLastLineWithInlineContent::No;
352         // Omit floats to see if this is the last line with inline content.
353         for (auto i = layoutRange.end; i--;) {
354             if (!m_inlineItems[i].isFloat())
355                 return i == trailingInlineItemIndex ? LineBuilder::IsLastLineWithInlineContent::Yes : LineBuilder::IsLastLineWithInlineContent::No;
356         }
357         // There has to be at least one non-float item.
358         ASSERT_NOT_REACHED();
359         return LineBuilder::IsLastLineWithInlineContent::No;
360     }();
361     return LineContent { trailingInlineItemIndex, partialContent, WTFMove(m_floats), line.close(isLastLineWithInlineContent), line.lineBox() };
362 }
363
364 void LineLayoutContext::nextContentForLine(LineCandidate& lineCandidate, unsigned currentInlineItemIndex, const InlineItemRange layoutRange, Optional<unsigned> partialLeadingContentLength, InlineLayoutUnit availableLineWidth, InlineLayoutUnit currentLogicalRight)
365 {
366     ASSERT(currentInlineItemIndex < layoutRange.end);
367     lineCandidate.reset();
368     // 1. Simply add any overflow content from the previous line to the candidate content. It's always a text content.
369     // 2. Find the next soft wrap position or explicit line break.
370     // 3. Collect floats between the inline content.
371     auto softWrapOpportunityIndex = nextWrapOpportunity(m_inlineItems, currentInlineItemIndex, layoutRange);
372     // softWrapOpportunityIndex == layoutRange.end means we don't have any wrap opportunity in this content.
373     ASSERT(softWrapOpportunityIndex <= layoutRange.end);
374
375     if (partialLeadingContentLength) {
376         // Handle leading partial content first (split text from the previous line).
377         // Construct a partial leading inline item.
378         m_partialLeadingTextItem = downcast<InlineTextItem>(m_inlineItems[currentInlineItemIndex]).right(*partialLeadingContentLength);
379         auto itemWidth = inlineItemWidth(*m_partialLeadingTextItem, currentLogicalRight);
380         lineCandidate.inlineContent.appendInlineItem(*m_partialLeadingTextItem, itemWidth);
381         currentLogicalRight += itemWidth;
382         ++currentInlineItemIndex;
383     }
384
385     auto accumulatedWidth = InlineLayoutUnit { };
386     for (auto index = currentInlineItemIndex; index < softWrapOpportunityIndex; ++index) {
387         auto& inlineItem = m_inlineItems[index];
388         if (inlineItem.isFloat()) {
389             // Floats are not part of the line context.
390             auto floatWidth = inlineItemWidth(inlineItem, { });
391             lineCandidate.floatContent.append(inlineItem, floatWidth, floatWidth <= (availableLineWidth - accumulatedWidth));
392             accumulatedWidth += floatWidth;
393             continue;
394         }
395         if (inlineItem.isText() || inlineItem.isContainerStart() || inlineItem.isContainerEnd() || inlineItem.isBox()) {
396             auto inlineItenmWidth = inlineItemWidth(inlineItem, currentLogicalRight);
397             lineCandidate.inlineContent.appendInlineItem(inlineItem, inlineItenmWidth);
398             currentLogicalRight += inlineItenmWidth;
399             accumulatedWidth += inlineItenmWidth;
400             continue;
401         }
402         if (inlineItem.isLineBreak()) {
403             lineCandidate.inlineContent.appendLineBreak(inlineItem);
404             continue;
405         }
406         ASSERT_NOT_REACHED();
407     }
408 }
409
410 void LineLayoutContext::commitFloats(LineBuilder& line, const LineCandidate& lineCandidate, CommitIntrusiveFloatsOnly commitIntrusiveOnly)
411 {
412     auto& floatContent = lineCandidate.floatContent;
413     auto leftFloatsWidth = InlineLayoutUnit { };
414     auto rightFloatsWidth = InlineLayoutUnit { };
415
416     for (auto& floatCandidate : floatContent.list()) {
417         if (floatCandidate.isIntrusive && commitIntrusiveOnly == CommitIntrusiveFloatsOnly::Yes)
418             continue;
419         if (!floatCandidate.isIntrusive) {
420             m_floats.append({ LineContent::Float::Intrusive::No, floatCandidate.item });
421             continue;
422         }
423         m_floats.append({ LineContent::Float::Intrusive::Yes, floatCandidate.item });
424         // This float is intrusive and it shrinks the current line.
425         // Shrink available space for current line.
426         if (floatCandidate.item->layoutBox().isLeftFloatingPositioned())
427             leftFloatsWidth += floatCandidate.logicalWidth;
428         else
429             rightFloatsWidth += floatCandidate.logicalWidth;
430     }
431     if (leftFloatsWidth || rightFloatsWidth) {
432         line.setHasIntrusiveFloat();
433         if (leftFloatsWidth)
434             line.moveLogicalLeft(leftFloatsWidth);
435         if (rightFloatsWidth)
436             line.moveLogicalRight(rightFloatsWidth);
437     }
438 }
439
440 LineLayoutContext::Result LineLayoutContext::handleFloatsAndInlineContent(LineBreaker& lineBreaker, LineBuilder& line, const InlineItemRange& layoutRange, const LineCandidate& lineCandidate)
441 {
442     auto& inlineContent = lineCandidate.inlineContent;
443     auto& candidateRuns = inlineContent.runs();
444     if (candidateRuns.isEmpty()) {
445         commitFloats(line, lineCandidate);
446         return { LineBreaker::IsEndOfLine::No };
447     }
448
449     auto shouldDisableHyphenation = [&] {
450         auto& style = root().style();
451         unsigned limitLines = style.hyphenationLimitLines() == RenderStyle::initialHyphenationLimitLines() ? std::numeric_limits<unsigned>::max() : style.hyphenationLimitLines();
452         return m_successiveHyphenatedLineCount >= limitLines;
453     };
454     if (shouldDisableHyphenation())
455         lineBreaker.setHyphenationDisabled();
456
457     auto& floatContent = lineCandidate.floatContent;
458     // Check if this new content fits.
459     auto availableWidth = line.availableWidth() - floatContent.intrusiveWidth();
460     auto isLineConsideredEmpty = line.isVisuallyEmpty() && !line.hasIntrusiveFloat();
461     auto lineStatus = LineBreaker::LineStatus { availableWidth, line.trimmableTrailingWidth(), line.isTrailingRunFullyTrimmable(), isLineConsideredEmpty };
462     auto result = lineBreaker.shouldWrapInlineContent(candidateRuns, inlineContent.logicalWidth(), lineStatus);
463     if (result.lastWrapOpportunityItem)
464         m_lastWrapOpportunityItem = result.lastWrapOpportunityItem;
465     if (result.action == LineBreaker::Result::Action::Keep) {
466         // This continuous content can be fully placed on the current line including non-intrusive floats.
467         for (auto& run : candidateRuns)
468             line.append(run.inlineItem, run.logicalWidth);
469         commitFloats(line, lineCandidate);
470         return { result.isEndOfLine, { candidateRuns.size(), false } };
471     }
472     if (result.action == LineBreaker::Result::Action::Push) {
473         ASSERT(result.isEndOfLine == LineBreaker::IsEndOfLine::Yes);
474         // This continuous content can't be placed on the current line. Nothing to commit at this time.
475         return { LineBreaker::IsEndOfLine::Yes };
476     }
477     if (result.action == LineBreaker::Result::Action::RevertToLastWrapOpportunity) {
478         ASSERT(result.isEndOfLine == LineBreaker::IsEndOfLine::Yes);
479         // Not only this content can't be placed on the current line, but we even need to revert the line back to an earlier position.
480         ASSERT(m_lastWrapOpportunityItem);
481         return { LineBreaker::IsEndOfLine::Yes, { rebuildLine(line, layoutRange), true } };
482     }
483     if (result.action == LineBreaker::Result::Action::Split) {
484         ASSERT(result.isEndOfLine == LineBreaker::IsEndOfLine::Yes);
485         // Commit the combination of full and partial content on the current line.
486         commitFloats(line, lineCandidate, CommitIntrusiveFloatsOnly::Yes);
487         ASSERT(result.partialTrailingContent);
488         commitPartialContent(line, candidateRuns, *result.partialTrailingContent);
489         // When splitting multiple runs <span style="word-break: break-all">text</span><span>content</span>, we might end up splitting them at run boundary.
490         // It simply means we don't really have a partial run. Partial content yes, but not partial run.
491         auto trailingRunIndex = result.partialTrailingContent->trailingRunIndex;
492         auto committedInlineItemCount = trailingRunIndex + 1;
493         if (!result.partialTrailingContent->partialRun)
494             return { LineBreaker::IsEndOfLine::Yes, { committedInlineItemCount, false } };
495
496         auto partialRun = *result.partialTrailingContent->partialRun;
497         auto& trailingInlineTextItem = downcast<InlineTextItem>(candidateRuns[trailingRunIndex].inlineItem);
498         auto overflowLength = trailingInlineTextItem.length() - partialRun.length;
499         return { LineBreaker::IsEndOfLine::Yes, { committedInlineItemCount, false }, LineContent::PartialContent { partialRun.needsHyphen, overflowLength } };
500     }
501     ASSERT_NOT_REACHED();
502     return { LineBreaker::IsEndOfLine::No };
503 }
504
505 void LineLayoutContext::commitPartialContent(LineBuilder& line, const LineBreaker::RunList& runs, const LineBreaker::Result::PartialTrailingContent& partialTrailingContent)
506 {
507     for (size_t index = 0; index < runs.size(); ++index) {
508         auto& run = runs[index];
509         if (partialTrailingContent.trailingRunIndex == index) {
510             ASSERT(run.inlineItem.isText());
511             // Create and commit partial trailing item.
512             if (auto partialRun = partialTrailingContent.partialRun) {
513                 auto& trailingInlineTextItem = downcast<InlineTextItem>(runs[partialTrailingContent.trailingRunIndex].inlineItem);
514                 auto partialTrailingTextItem = trailingInlineTextItem.left(partialRun->length);
515                 line.appendPartialTrailingTextItem(partialTrailingTextItem, partialRun->logicalWidth, partialRun->needsHyphen);
516                 return;
517             }
518             // The partial run is the last content to commit.
519             line.append(run.inlineItem, run.logicalWidth);
520             return;
521         }
522         line.append(run.inlineItem, run.logicalWidth);
523     }
524 }
525
526 size_t LineLayoutContext::rebuildLine(LineBuilder& line, const InlineItemRange& layoutRange)
527 {
528     // Clear the line and start appending the inline items closing with the last wrap opportunity run.
529     line.resetContent();
530     auto currentItemIndex = layoutRange.start;
531     auto logicalRight = InlineLayoutUnit { };
532     if (m_partialLeadingTextItem) {
533         auto logicalWidth = inlineItemWidth(*m_partialLeadingTextItem, logicalRight);
534         line.append(*m_partialLeadingTextItem, logicalWidth);
535         logicalRight += logicalWidth;
536         if (&m_partialLeadingTextItem.value() == m_lastWrapOpportunityItem)
537             return 1;
538         ++currentItemIndex;
539     }
540     for (; currentItemIndex < layoutRange.end; ++currentItemIndex) {
541         auto& inlineItem = m_inlineItems[currentItemIndex];
542         auto logicalWidth = inlineItemWidth(inlineItem, logicalRight);
543         line.append(inlineItem, logicalWidth);
544         logicalRight += logicalWidth;
545         if (&inlineItem == m_lastWrapOpportunityItem)
546             return currentItemIndex - layoutRange.start + 1;
547     }
548     return layoutRange.size();
549 }
550
551 }
552 }
553
554 #endif