- Revision
- 253934
- Author
- [email protected]
- Date
- 2019-12-29 07:59:01 -0800 (Sun, 29 Dec 2019)
Log Message
[LFC][IFC] Move soft wrap opportunity code out of LineBreaker
https://bugs.webkit.org/show_bug.cgi?id=205621
<rdar://problem/58227670>
Reviewed by Antti Koivisto.
Let's not mix soft and line wrap opportunity logic.
* layout/inlineformatting/InlineLineBreaker.cpp:
(WebCore::Layout::ContinousContent::runs const):
(WebCore::Layout::ContinousContent::isEmpty const):
(WebCore::Layout::ContinousContent::size const):
(WebCore::Layout::ContinousContent::width const):
(WebCore::Layout::ContinousContent::nonCollapsibleWidth const):
(WebCore::Layout::ContinousContent::hasTrailingCollapsibleContent const):
(WebCore::Layout::ContinousContent::isTrailingContentFullyCollapsible const):
(WebCore::Layout::LineBreaker::wrapTextContent const):
(WebCore::Layout::ContinousContent::ContinousContent):
(WebCore::Layout::ContinousContent::hasTextContentOnly const):
(WebCore::Layout::ContinousContent::isVisuallyEmptyWhitespaceContentOnly const):
(WebCore::Layout::ContinousContent::firstTextRunIndex const):
(WebCore::Layout::ContinousContent::lastContentRunIndex const):
(WebCore::Layout::ContinousContent::hasNonContentRunsOnly const):
(WebCore::Layout::ContinousContent::lastWrapOpportunityIndex const):
(WebCore::Layout::ContinousContent::TrailingCollapsibleContent::reset):
(WebCore::Layout::endsWithSoftWrapOpportunity): Deleted.
(WebCore::Layout::isAtSoftWrapOpportunity): Deleted.
(WebCore::Layout::LineBreaker::nextWrapOpportunity): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::ContinousContent): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::hasTextContentOnly const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::isVisuallyEmptyWhitespaceContentOnly const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::firstTextRunIndex const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::lastContentRunIndex const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::hasNonContentRunsOnly const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::lastWrapOpportunityIndex const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::TrailingCollapsibleContent::reset): Deleted.
* layout/inlineformatting/InlineLineBreaker.h:
(WebCore::Layout::LineBreaker::ContinousContent::runs const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::isEmpty const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::size const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::width const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::nonCollapsibleWidth const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::hasTrailingCollapsibleContent const): Deleted.
(WebCore::Layout::LineBreaker::ContinousContent::isTrailingContentFullyCollapsible const): Deleted.
* layout/inlineformatting/LineLayoutContext.cpp:
(WebCore::Layout::endsWithSoftWrapOpportunity):
(WebCore::Layout::isAtSoftWrapOpportunity):
(WebCore::Layout::nextWrapOpportunity):
(WebCore::Layout::LineLayoutContext::nextContentForLine):
Modified Paths
Diff
Modified: trunk/Source/WebCore/ChangeLog (253933 => 253934)
--- trunk/Source/WebCore/ChangeLog 2019-12-29 07:01:18 UTC (rev 253933)
+++ trunk/Source/WebCore/ChangeLog 2019-12-29 15:59:01 UTC (rev 253934)
@@ -1,3 +1,55 @@
+2019-12-29 Zalan Bujtas <[email protected]>
+
+ [LFC][IFC] Move soft wrap opportunity code out of LineBreaker
+ https://bugs.webkit.org/show_bug.cgi?id=205621
+ <rdar://problem/58227670>
+
+ Reviewed by Antti Koivisto.
+
+ Let's not mix soft and line wrap opportunity logic.
+
+ * layout/inlineformatting/InlineLineBreaker.cpp:
+ (WebCore::Layout::ContinousContent::runs const):
+ (WebCore::Layout::ContinousContent::isEmpty const):
+ (WebCore::Layout::ContinousContent::size const):
+ (WebCore::Layout::ContinousContent::width const):
+ (WebCore::Layout::ContinousContent::nonCollapsibleWidth const):
+ (WebCore::Layout::ContinousContent::hasTrailingCollapsibleContent const):
+ (WebCore::Layout::ContinousContent::isTrailingContentFullyCollapsible const):
+ (WebCore::Layout::LineBreaker::wrapTextContent const):
+ (WebCore::Layout::ContinousContent::ContinousContent):
+ (WebCore::Layout::ContinousContent::hasTextContentOnly const):
+ (WebCore::Layout::ContinousContent::isVisuallyEmptyWhitespaceContentOnly const):
+ (WebCore::Layout::ContinousContent::firstTextRunIndex const):
+ (WebCore::Layout::ContinousContent::lastContentRunIndex const):
+ (WebCore::Layout::ContinousContent::hasNonContentRunsOnly const):
+ (WebCore::Layout::ContinousContent::lastWrapOpportunityIndex const):
+ (WebCore::Layout::ContinousContent::TrailingCollapsibleContent::reset):
+ (WebCore::Layout::endsWithSoftWrapOpportunity): Deleted.
+ (WebCore::Layout::isAtSoftWrapOpportunity): Deleted.
+ (WebCore::Layout::LineBreaker::nextWrapOpportunity): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::ContinousContent): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::hasTextContentOnly const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::isVisuallyEmptyWhitespaceContentOnly const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::firstTextRunIndex const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::lastContentRunIndex const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::hasNonContentRunsOnly const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::lastWrapOpportunityIndex const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::TrailingCollapsibleContent::reset): Deleted.
+ * layout/inlineformatting/InlineLineBreaker.h:
+ (WebCore::Layout::LineBreaker::ContinousContent::runs const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::isEmpty const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::size const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::width const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::nonCollapsibleWidth const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::hasTrailingCollapsibleContent const): Deleted.
+ (WebCore::Layout::LineBreaker::ContinousContent::isTrailingContentFullyCollapsible const): Deleted.
+ * layout/inlineformatting/LineLayoutContext.cpp:
+ (WebCore::Layout::endsWithSoftWrapOpportunity):
+ (WebCore::Layout::isAtSoftWrapOpportunity):
+ (WebCore::Layout::nextWrapOpportunity):
+ (WebCore::Layout::LineLayoutContext::nextContentForLine):
+
2019-12-28 Zalan Bujtas <[email protected]>
[LFC][IFC] Keep track of last line wrap opportunity
Modified: trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.cpp (253933 => 253934)
--- trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.cpp 2019-12-29 07:01:18 UTC (rev 253933)
+++ trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.cpp 2019-12-29 15:59:01 UTC (rev 253934)
@@ -49,6 +49,43 @@
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
{
// Use the last inline item with content (where we would be wrapping) to decide if content wrapping is allowed.
@@ -151,7 +188,7 @@
return !lineIsEmpty && floatLogicalWidth > availableWidth;
}
-Optional<LineBreaker::WrappedTextContent> LineBreaker::wrapTextContent(const RunList& runs, const LineStatus& lineStatus) const
+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());
@@ -290,145 +327,7 @@
return { };
}
-static 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, it's guaranteed that
- // they are split at a soft breaking opportunity. See InlineTextItem::moveToNextBreakablePosition.
- if (¤tTextItem.layoutBox() == &nextInlineTextItem.layoutBox())
- return true;
- // Now we need to collect at least 3 adjacent characters to be able to make a descision 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.layoutBox().textContext()->content;
- auto lineBreakIterator = LazyLineBreakIterator { nextInlineTextItem.layoutBox().textContext()->content };
- auto previousContentLength = previousContent.length();
- // FIXME: We should look into the entire uncommitted content for more text context.
- UChar lastCharacter = previousContentLength ? previousContent[previousContentLength - 1] : 0;
- UChar secondToLastCharacter = previousContentLength > 1 ? previousContent[previousContentLength - 2] : 0;
- lineBreakIterator.setPriorContext(lastCharacter, secondToLastCharacter);
- // 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(lineBreakIterator, 0, nextInlineTextItem.style());
-}
-
-static bool isAtSoftWrapOpportunity(const InlineItem& current, const InlineItem& next)
-{
- // "is at" simple means that there's a soft wrap opportunity right after the [current].
- // [text][ ][text][container start]... (<div>text content<span>..</div>)
- // soft wrap indexes: 0 and 1 definitely, 2 depends on the content after the [container 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. [container start][prior_continuous_content][container 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.isBox() || next.isBox()) {
- // [text][container start][container 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;
- }
- 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;
- }
- auto& currentInlineTextItem = downcast<InlineTextItem>(current);
- auto& nextInlineTextItem = downcast<InlineTextItem>(next);
- if (currentInlineTextItem.isWhitespace()) {
- // [ ][text] : after [whitespace] position is a soft wrap opportunity.
- return true;
- }
- if (nextInlineTextItem.isWhitespace()) {
- // [text][ ] (<span>text</span> )
- // white-space: break-spaces: line breaking opportunity exists after every preserved white space character, but not before.
- return nextInlineTextItem.style().whiteSpace() != WhiteSpace::BreakSpaces;
- }
- // 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);
-}
-
-size_t LineBreaker::nextWrapOpportunity(const InlineItems& inlineContent, unsigned startIndex)
-{
- // 1. Find the start candidate by skipping leading non-content items e.g <span><span>start : skip "<span><span>"
- // 2. Find the end candidate by skipping non-content items inbetween e.g. <span><span>start</span>end: skip "</span>"
- // 3. Check if there's a soft wrap opportunity between the 2 candidate inline items and repeat.
- // 4. Any force line break inbetween is considered as a wrap opportunity.
-
- // [ex-][container start][container end][float][ample] (ex-<span></span><div style="float:left"></div>ample) : wrap index is at [ex-].
- // [ex][container start][amp-][container start][le] (ex<span>amp-<span>ample) : wrap index is at [amp-].
- // [ex-][container start][line break][ample] (ex-<span><br>ample) : wrap index is after [br].
- auto end = inlineContent.size();
-
- struct WrapContent {
- WrapContent(size_t index, bool isAtLineBreak)
- : m_index(index)
- , m_isAtLineBreak(isAtLineBreak)
- {
- }
- size_t operator*() const { return m_index; }
- bool isAtLineBreak() const { return m_isAtLineBreak; }
-
- private:
- size_t m_index { 0 };
- bool m_isAtLineBreak { false };
- };
- auto nextInlineItemWithContent = [&] (auto index) {
- // Break at the first text/box/line break inline item.
- for (; index < end; ++index) {
- auto& inlineItem = *inlineContent[index];
- if (inlineItem.isText() || inlineItem.isBox() || inlineItem.isLineBreak())
- return WrapContent { index, inlineItem.isLineBreak() };
- }
- return WrapContent { end, false };
- };
-
- // Start at the first inline item with content.
- // [container start][ex-] : start at [ex-]
- auto startContent = nextInlineItemWithContent(startIndex);
- if (startContent.isAtLineBreak()) {
- // Content starts with a line break. The wrap position is after the line break.
- return *startContent + 1;
- }
-
- while (*startContent != end) {
- // 1. Find the next inline item with content.
- // 2. Check if there's a soft wrap opportunity between the start and the next inline item.
- auto nextContent = nextInlineItemWithContent(*startContent + 1);
- if (*nextContent == end || nextContent.isAtLineBreak())
- return *nextContent;
- if (isAtSoftWrapOpportunity(*inlineContent[*startContent], *inlineContent[*nextContent])) {
- // There's a soft wrap opportunity between the start and the nextContent.
- // Now forward-find from the start position to see where we can actually wrap.
- // [ex-][ample] vs. [ex-][container start][container end][ample]
- // where [ex-] is startContent and [ample] is the nextContent.
- auto candidateIndex = *startContent + 1;
- for (; candidateIndex < *nextContent; ++candidateIndex) {
- if (inlineContent[candidateIndex]->isContainerStart()) {
- // inline content and [container start] and [container end] form unbreakable content.
- // ex-<span></span>ample : wrap opportunity is after "ex-".
- // ex-</span></span>ample : wrap opportunity is after "ex-</span></span>".
- // ex-</span><span>ample</span> : wrap opportunity is after "ex-</span>".
- // ex-<span><span>ample</span></span> : wrap opportunity is after "ex-".
- return candidateIndex;
- }
- }
- return candidateIndex;
- }
- startContent = nextContent;
- }
- return end;
-}
-
-LineBreaker::ContinousContent::ContinousContent(const RunList& runs)
+ContinousContent::ContinousContent(const LineBreaker::RunList& runs)
: m_runs(runs)
{
// Figure out the trailing collapsible state.
@@ -466,7 +365,7 @@
}
}
-bool LineBreaker::ContinousContent::hasTextContentOnly() const
+bool ContinousContent::hasTextContentOnly() const
{
// <span>text</span> is considered a text run even with the [container start][container end] inline items.
// Due to commit boundary rules, we just need to check the first non-typeless inline item (can't have both [img] and [text])
@@ -479,7 +378,7 @@
return false;
}
-bool LineBreaker::ContinousContent::isVisuallyEmptyWhitespaceContentOnly() const
+bool ContinousContent::isVisuallyEmptyWhitespaceContentOnly() const
{
// [<span></span> ] [<span> </span>] [ <span style="padding: 0px;"></span>] are all considered visually empty whitespace content.
// [<span style="border: 1px solid red"></span> ] while this is whitespace content only, it is not considered visually empty.
@@ -494,7 +393,7 @@
return false;
}
-Optional<unsigned> LineBreaker::ContinousContent::firstTextRunIndex() const
+Optional<size_t> ContinousContent::firstTextRunIndex() const
{
for (size_t index = 0; index < m_runs.size(); ++index) {
if (m_runs[index].inlineItem.isText())
@@ -503,7 +402,7 @@
return { };
}
-Optional<unsigned> LineBreaker::ContinousContent::lastContentRunIndex() const
+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())
@@ -512,7 +411,7 @@
return { };
}
-bool LineBreaker::ContinousContent::hasNonContentRunsOnly() const
+bool ContinousContent::hasNonContentRunsOnly() const
{
// <span></span> <- non content runs.
for (auto& run : m_runs) {
@@ -524,7 +423,7 @@
return true;
}
-Optional<size_t> LineBreaker::ContinousContent::lastWrapOpportunityIndex() const
+Optional<size_t> ContinousContent::lastWrapOpportunityIndex() const
{
// <span style="white-space: pre">no_wrap</span><span>yes_wrap</span><span style="white-space: pre">no_wrap</span>.
// [container start][no_wrap][container end][container start][yes_wrap][container end][container start][no_wrap][container end]
@@ -536,7 +435,7 @@
return { };
}
-void LineBreaker::ContinousContent::TrailingCollapsibleContent::reset()
+void ContinousContent::TrailingCollapsibleContent::reset()
{
isFullyCollapsible = false;
width = 0_lu;
Modified: trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.h (253933 => 253934)
--- trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.h 2019-12-29 07:01:18 UTC (rev 253933)
+++ trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.h 2019-12-29 15:59:01 UTC (rev 253934)
@@ -34,6 +34,8 @@
class InlineItem;
class InlineTextItem;
+struct ContinousContent;
+struct WrappedTextContent;
class LineBreaker {
public:
@@ -49,10 +51,10 @@
Split, // Partial content is on the current line.
Push // Content is pushed to the next line.
};
- Action action;
+ Action action { Action::Keep };
IsEndOfLine isEndOfLine { IsEndOfLine::No };
struct PartialTrailingContent {
- unsigned trailingRunIndex { 0 };
+ size_t trailingRunIndex { 0 };
Optional<PartialRun> partialRun; // nullopt partial run means the trailing run is a complete run.
};
Optional<PartialTrailingContent> partialTrailingContent;
@@ -67,7 +69,6 @@
InlineLayoutUnit logicalWidth { 0 };
};
using RunList = Vector<Run, 30>;
- static size_t nextWrapOpportunity(const InlineItems&, unsigned startIndex);
struct LineStatus {
InlineLayoutUnit availableWidth { 0 };
@@ -90,42 +91,6 @@
// [content]
// [container start][span1][container end][between][container start][span2][container end]
// see https://drafts.csswg.org/css-text-3/#line-break-details
- struct ContinousContent {
- ContinousContent(const RunList&);
-
- const 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<unsigned> firstTextRunIndex() const;
- Optional<unsigned> lastContentRunIndex() const;
-
- private:
- 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<PartialRun> partialTrailingRun;
- };
Optional<WrappedTextContent> wrapTextContent(const RunList&, const LineStatus&) const;
Result tryWrappingInlineContent(const ContinousContent&, const LineStatus&) const;
Optional<PartialRun> tryBreakingTextRun(const Run& overflowRun, InlineLayoutUnit availableWidth) const;
Modified: trunk/Source/WebCore/layout/inlineformatting/LineLayoutContext.cpp (253933 => 253934)
--- trunk/Source/WebCore/layout/inlineformatting/LineLayoutContext.cpp 2019-12-29 07:01:18 UTC (rev 253933)
+++ trunk/Source/WebCore/layout/inlineformatting/LineLayoutContext.cpp 2019-12-29 15:59:01 UTC (rev 253934)
@@ -34,6 +34,144 @@
namespace WebCore {
namespace Layout {
+static 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, it's guaranteed that
+ // they are split at a soft breaking opportunity. See InlineTextItem::moveToNextBreakablePosition.
+ if (¤tTextItem.layoutBox() == &nextInlineTextItem.layoutBox())
+ return true;
+ // Now we need to collect at least 3 adjacent characters to be able to make a descision 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.layoutBox().textContext()->content;
+ auto lineBreakIterator = LazyLineBreakIterator { nextInlineTextItem.layoutBox().textContext()->content };
+ auto previousContentLength = previousContent.length();
+ // FIXME: We should look into the entire uncommitted content for more text context.
+ UChar lastCharacter = previousContentLength ? previousContent[previousContentLength - 1] : 0;
+ UChar secondToLastCharacter = previousContentLength > 1 ? previousContent[previousContentLength - 2] : 0;
+ lineBreakIterator.setPriorContext(lastCharacter, secondToLastCharacter);
+ // 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(lineBreakIterator, 0, nextInlineTextItem.style());
+}
+
+static bool isAtSoftWrapOpportunity(const InlineItem& current, const InlineItem& next)
+{
+ // "is at" simple means that there's a soft wrap opportunity right after the [current].
+ // [text][ ][text][container start]... (<div>text content<span>..</div>)
+ // soft wrap indexes: 0 and 1 definitely, 2 depends on the content after the [container 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. [container start][prior_continuous_content][container 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.isBox() || next.isBox()) {
+ // [text][container start][container 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;
+ }
+ 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;
+ }
+ auto& currentInlineTextItem = downcast<InlineTextItem>(current);
+ auto& nextInlineTextItem = downcast<InlineTextItem>(next);
+ if (currentInlineTextItem.isWhitespace()) {
+ // [ ][text] : after [whitespace] position is a soft wrap opportunity.
+ return true;
+ }
+ if (nextInlineTextItem.isWhitespace()) {
+ // [text][ ] (<span>text</span> )
+ // white-space: break-spaces: line breaking opportunity exists after every preserved white space character, but not before.
+ return nextInlineTextItem.style().whiteSpace() != WhiteSpace::BreakSpaces;
+ }
+ // 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);
+}
+
+static size_t nextWrapOpportunity(const InlineItems& inlineContent, unsigned startIndex)
+{
+ // 1. Find the start candidate by skipping leading non-content items e.g <span><span>start : skip "<span><span>"
+ // 2. Find the end candidate by skipping non-content items inbetween e.g. <span><span>start</span>end: skip "</span>"
+ // 3. Check if there's a soft wrap opportunity between the 2 candidate inline items and repeat.
+ // 4. Any force line break inbetween is considered as a wrap opportunity.
+
+ // [ex-][container start][container end][float][ample] (ex-<span></span><div style="float:left"></div>ample) : wrap index is at [ex-].
+ // [ex][container start][amp-][container start][le] (ex<span>amp-<span>ample) : wrap index is at [amp-].
+ // [ex-][container start][line break][ample] (ex-<span><br>ample) : wrap index is after [br].
+ auto end = inlineContent.size();
+
+ struct WrapContent {
+ WrapContent(size_t index, bool isAtLineBreak)
+ : m_index(index)
+ , m_isAtLineBreak(isAtLineBreak)
+ {
+ }
+ size_t operator*() const { return m_index; }
+ bool isAtLineBreak() const { return m_isAtLineBreak; }
+
+ private:
+ size_t m_index { 0 };
+ bool m_isAtLineBreak { false };
+ };
+ auto nextInlineItemWithContent = [&] (auto index) {
+ // Break at the first text/box/line break inline item.
+ for (; index < end; ++index) {
+ auto& inlineItem = *inlineContent[index];
+ if (inlineItem.isText() || inlineItem.isBox() || inlineItem.isLineBreak())
+ return WrapContent { index, inlineItem.isLineBreak() };
+ }
+ return WrapContent { end, false };
+ };
+
+ // Start at the first inline item with content.
+ // [container start][ex-] : start at [ex-]
+ auto startContent = nextInlineItemWithContent(startIndex);
+ if (startContent.isAtLineBreak()) {
+ // Content starts with a line break. The wrap position is after the line break.
+ return *startContent + 1;
+ }
+
+ while (*startContent != end) {
+ // 1. Find the next inline item with content.
+ // 2. Check if there's a soft wrap opportunity between the start and the next inline item.
+ auto nextContent = nextInlineItemWithContent(*startContent + 1);
+ if (*nextContent == end || nextContent.isAtLineBreak())
+ return *nextContent;
+ if (isAtSoftWrapOpportunity(*inlineContent[*startContent], *inlineContent[*nextContent])) {
+ // There's a soft wrap opportunity between the start and the nextContent.
+ // Now forward-find from the start position to see where we can actually wrap.
+ // [ex-][ample] vs. [ex-][container start][container end][ample]
+ // where [ex-] is startContent and [ample] is the nextContent.
+ auto candidateIndex = *startContent + 1;
+ for (; candidateIndex < *nextContent; ++candidateIndex) {
+ if (inlineContent[candidateIndex]->isContainerStart()) {
+ // inline content and [container start] and [container end] form unbreakable content.
+ // ex-<span></span>ample : wrap opportunity is after "ex-".
+ // ex-</span></span>ample : wrap opportunity is after "ex-</span></span>".
+ // ex-</span><span>ample</span> : wrap opportunity is after "ex-</span>".
+ // ex-<span><span>ample</span></span> : wrap opportunity is after "ex-".
+ return candidateIndex;
+ }
+ }
+ return candidateIndex;
+ }
+ startContent = nextContent;
+ }
+ return end;
+}
+
struct LineCandidateContent {
void append(const InlineItem&, Optional<InlineLayoutUnit> logicalWidth);
@@ -190,7 +328,7 @@
// 1. Simply add any overflow content from the previous line to the candidate content. It's always a text content.
// 2. Find the next soft wrap position or explicit line break.
// 3. Collect floats between the inline content.
- auto softWrapOpportunityIndex = LineBreaker::nextWrapOpportunity(m_inlineItems, inlineItemIndex);
+ auto softWrapOpportunityIndex = nextWrapOpportunity(m_inlineItems, inlineItemIndex);
// softWrapOpportunityIndex == m_inlineItems.size() means we don't have any wrap opportunity in this content.
ASSERT(softWrapOpportunityIndex <= m_inlineItems.size());