Title: [283614] trunk
Revision
283614
Author
[email protected]
Date
2021-10-06 06:40:59 -0700 (Wed, 06 Oct 2021)

Log Message

[LFC][IFC] word-break: break-all only allows mid-word arbitrary breaking positions
https://bugs.webkit.org/show_bug.cgi?id=231247

Reviewed by Antti Koivisto.

Source/WebCore:

Introduce a new type of breaking type where the arbitrary position is only applied when breaking mid-word within a continuous content.
While normally when we search for a breaking position inside an overflowing continuous set of runs, we find ourselves inside a "word" and any breaking
position would match the definition of the mid-work break.

However in other cases when these runs consist more than just a "word", for example

  'XX ' (where the "white-space: break-spaces" sets the line breaking opportunity after the trailing whitespace)
  'word-break: break-all' does not affect the breaking behavior between the trailing X and the ' ' and therefore it is not a breaking position.

It may very well be a valid breaking position if some other CSS properties allow it, but the notion of being able to break at any position (break-all) does not apply here.

see https://drafts.csswg.org/css-text-3/#word-break-property

WPT progressions.

* layout/formattingContexts/inline/InlineContentBreaker.cpp:
(WebCore::Layout::nextTextRunIndex):
(WebCore::Layout::previousTextRunIndex):
(WebCore::Layout::findTrailingRunIndex):
(WebCore::Layout::InlineContentBreaker::tryBreakingOverflowingRun const):
(WebCore::Layout::InlineContentBreaker::tryBreakingPreviousNonOverflowingRuns const):
(WebCore::Layout::InlineContentBreaker::tryBreakingNextOverflowingRuns const):
(WebCore::Layout::InlineContentBreaker::processOverflowingContentWithText const):
(WebCore::Layout::InlineContentBreaker::wordBreakBehavior const):
(WebCore::Layout::InlineContentBreaker::tryBreakingTextRun const):
* layout/formattingContexts/inline/InlineContentBreaker.h:

LayoutTests:

* TestExpectations:

Modified Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (283613 => 283614)


--- trunk/LayoutTests/ChangeLog	2021-10-06 11:26:32 UTC (rev 283613)
+++ trunk/LayoutTests/ChangeLog	2021-10-06 13:40:59 UTC (rev 283614)
@@ -1,3 +1,12 @@
+2021-10-06  Alan Bujtas  <[email protected]>
+
+        [LFC][IFC] word-break: break-all only allows mid-word arbitrary breaking positions
+        https://bugs.webkit.org/show_bug.cgi?id=231247
+
+        Reviewed by Antti Koivisto.
+
+        * TestExpectations:
+
 2021-10-06  Alicia Boya García  <[email protected]>
 
         [MSE][GStreamer] Honor MP4 edit lists

Modified: trunk/LayoutTests/TestExpectations (283613 => 283614)


--- trunk/LayoutTests/TestExpectations	2021-10-06 11:26:32 UTC (rev 283613)
+++ trunk/LayoutTests/TestExpectations	2021-10-06 13:40:59 UTC (rev 283614)
@@ -2630,13 +2630,7 @@
 # overflow-wrap:anywhere feature is not implemented in legacy line layout.
 webkit.org/b/195345 imported/w3c/web-platform-tests/css/css-text/overflow-wrap/overflow-wrap-anywhere-003.html [ ImageOnlyFailure ]
 webkit.org/b/195345 imported/w3c/web-platform-tests/css/css-text/overflow-wrap/overflow-wrap-min-content-size-002.html [ ImageOnlyFailure ]
-webkit.org/b/195345 imported/w3c/web-platform-tests/css/css-text/white-space/break-spaces-before-first-char-006.html [ ImageOnlyFailure ]
 
-webkit.org/b/197277 imported/w3c/web-platform-tests/css/css-text/white-space/break-spaces-008.html [ ImageOnlyFailure ]
-webkit.org/b/197277 imported/w3c/web-platform-tests/css/css-text/white-space/break-spaces-before-first-char-004.html [ ImageOnlyFailure ]
-webkit.org/b/197277 imported/w3c/web-platform-tests/css/css-text/white-space/break-spaces-before-first-char-005.html [ ImageOnlyFailure ]
-webkit.org/b/197277 imported/w3c/web-platform-tests/css/css-text/word-break/word-break-break-all-017.html [ ImageOnlyFailure ]
-
 webkit.org/b/197409 imported/w3c/web-platform-tests/css/css-text/word-break/word-break-break-all-016.html [ ImageOnlyFailure ]
 webkit.org/b/197409 imported/w3c/web-platform-tests/css/css-text/word-break/word-break-break-all-019.html [ ImageOnlyFailure ]
 webkit.org/b/197411 imported/w3c/web-platform-tests/css/css-text/word-break/word-break-break-all-018.html [ ImageOnlyFailure ]
@@ -4435,7 +4429,6 @@
 webkit.org/b/214290 imported/w3c/web-platform-tests/css/css-text/text-transform/text-transform-multiple-001.html [ ImageOnlyFailure ]
 webkit.org/b/214290 imported/w3c/web-platform-tests/css/css-text/text-transform/text-transform-upperlower-016.html [ ImageOnlyFailure ]
 webkit.org/b/214290 imported/w3c/web-platform-tests/css/css-text/text-transform/text-transform-upperlower-044.html [ ImageOnlyFailure ]
-webkit.org/b/214290 imported/w3c/web-platform-tests/css/css-text/white-space/break-spaces-before-first-char-015.html [ ImageOnlyFailure ]
 webkit.org/b/214290 imported/w3c/web-platform-tests/css/css-text/white-space/eol-spaces-bidi-001.html [ ImageOnlyFailure ]
 webkit.org/b/214290 imported/w3c/web-platform-tests/css/css-text/white-space/seg-break-transformation-018.html [ ImageOnlyFailure ]
 webkit.org/b/214290 imported/w3c/web-platform-tests/css/css-text/white-space/seg-break-transformation-019.html [ ImageOnlyFailure ]

Modified: trunk/Source/WebCore/ChangeLog (283613 => 283614)


--- trunk/Source/WebCore/ChangeLog	2021-10-06 11:26:32 UTC (rev 283613)
+++ trunk/Source/WebCore/ChangeLog	2021-10-06 13:40:59 UTC (rev 283614)
@@ -1,3 +1,37 @@
+2021-10-06  Alan Bujtas  <[email protected]>
+
+        [LFC][IFC] word-break: break-all only allows mid-word arbitrary breaking positions
+        https://bugs.webkit.org/show_bug.cgi?id=231247
+
+        Reviewed by Antti Koivisto.
+
+        Introduce a new type of breaking type where the arbitrary position is only applied when breaking mid-word within a continuous content.
+        While normally when we search for a breaking position inside an overflowing continuous set of runs, we find ourselves inside a "word" and any breaking
+        position would match the definition of the mid-work break.
+
+        However in other cases when these runs consist more than just a "word", for example
+
+          'XX ' (where the "white-space: break-spaces" sets the line breaking opportunity after the trailing whitespace)
+          'word-break: break-all' does not affect the breaking behavior between the trailing X and the ' ' and therefore it is not a breaking position. 
+
+        It may very well be a valid breaking position if some other CSS properties allow it, but the notion of being able to break at any position (break-all) does not apply here.
+
+        see https://drafts.csswg.org/css-text-3/#word-break-property
+
+        WPT progressions.
+
+        * layout/formattingContexts/inline/InlineContentBreaker.cpp:
+        (WebCore::Layout::nextTextRunIndex):
+        (WebCore::Layout::previousTextRunIndex):
+        (WebCore::Layout::findTrailingRunIndex):
+        (WebCore::Layout::InlineContentBreaker::tryBreakingOverflowingRun const):
+        (WebCore::Layout::InlineContentBreaker::tryBreakingPreviousNonOverflowingRuns const):
+        (WebCore::Layout::InlineContentBreaker::tryBreakingNextOverflowingRuns const):
+        (WebCore::Layout::InlineContentBreaker::processOverflowingContentWithText const):
+        (WebCore::Layout::InlineContentBreaker::wordBreakBehavior const):
+        (WebCore::Layout::InlineContentBreaker::tryBreakingTextRun const):
+        * layout/formattingContexts/inline/InlineContentBreaker.h:
+
 2021-10-06  Youenn Fablet  <[email protected]>
 
         Implement https://w3c.github.io/push-api/#receiving-a-push-message

Modified: trunk/Source/WebCore/layout/formattingContexts/inline/InlineContentBreaker.cpp (283613 => 283614)


--- trunk/Source/WebCore/layout/formattingContexts/inline/InlineContentBreaker.cpp	2021-10-06 11:26:32 UTC (rev 283613)
+++ trunk/Source/WebCore/layout/formattingContexts/inline/InlineContentBreaker.cpp	2021-10-06 13:40:59 UTC (rev 283614)
@@ -75,6 +75,24 @@
     return false;
 }
 
+static inline std::optional<size_t> nextTextRunIndex(const InlineContentBreaker::ContinuousContent::RunList& runs, size_t startIndex)
+{
+    for (auto index = startIndex + 1; index < runs.size(); ++index) {
+        if (runs[index].inlineItem.isText())
+            return index;
+    }
+    return { };
+}
+
+static inline std::optional<size_t> previousTextRunIndex(const InlineContentBreaker::ContinuousContent::RunList& runs, size_t startIndex)
+{
+    for (auto index = startIndex; index--;) {
+        if (runs[index].inlineItem.isText())
+            return index;
+    }
+    return { };
+}
+
 static inline bool isVisuallyEmptyWhitespaceContent(const InlineContentBreaker::ContinuousContent& continuousContent)
 {
     // [<span></span> ] [<span> </span>] [ <span style="padding: 0px;"></span>] are all considered visually empty whitespace content.
@@ -268,7 +286,6 @@
     // Try not break content at inline box boundary
     // e.g. <span>fits</span><span>overflows</span>
     // when the text "overflows" completely overflows, let's break the content right before the '<span>'.
-    // FIXME: Add support for subsequent empty inline boxes e.g.
     auto trailingCandidateIndex = breakableRunIndex - 1;
     auto isAtInlineBox = runs[trailingCandidateIndex].inlineItem.isInlineBoxStart();
     return !isAtInlineBox ? trailingCandidateIndex : trailingCandidateIndex ? std::make_optional(trailingCandidateIndex - 1) : std::nullopt;
@@ -291,7 +308,7 @@
     auto overflowingRun = runs[overflowingRunIndex];
     if (!isWrappableRun(overflowingRun))
         return { };
-    auto partialOverflowingRun = tryBreakingTextRun(overflowingRun, lineStatus.contentLogicalRight + nonOverflowingContentWidth, std::max(0.0f, lineStatus.availableWidth - nonOverflowingContentWidth), lineStatus.hasWrapOpportunityAtPreviousPosition);
+    auto partialOverflowingRun = tryBreakingTextRun(runs, overflowingRunIndex, lineStatus.contentLogicalRight + nonOverflowingContentWidth, std::max(0.0f, lineStatus.availableWidth - nonOverflowingContentWidth), lineStatus.hasWrapOpportunityAtPreviousPosition);
     if (!partialOverflowingRun)
         return { };
     if (partialOverflowingRun->length)
@@ -313,7 +330,7 @@
         if (!isWrappableRun(run))
             continue;
         ASSERT(run.inlineItem.isText());
-        if (auto partialRun = tryBreakingTextRun(run, lineStatus.contentLogicalRight + previousContentWidth, { }, lineStatus.hasWrapOpportunityAtPreviousPosition)) {
+        if (auto partialRun = tryBreakingTextRun(runs, index, lineStatus.contentLogicalRight + previousContentWidth, { }, lineStatus.hasWrapOpportunityAtPreviousPosition)) {
             // We know this run fits, so if breaking is allowed on the run, it should return a non-empty left-side
             // since it's either at hyphen position or the entire run is returned.
             ASSERT(partialRun->length);
@@ -334,26 +351,19 @@
             continue;
         }
         ASSERT(run.inlineItem.isText());
-        // We know that this run does not fit the available space. If we can break it at any position, let's just use the start of the run.
-        if (wordBreakBehavior(run.style, lineStatus.hasWrapOpportunityAtPreviousPosition) == WordBreakRule::AtArbitraryPosition) {
-            // We must be on an inline box boundary. Let's go back to the run in front of the inline box start run.
-            // e.g. <span>unbreakable_and_overflow<span style="word-break: break-all">breakable</span>
-            // We are at "breakable", <span> is at index - 1 and the trailing run is at index - 2.
-            ASSERT(runs[index - 1].inlineItem.isInlineBoxStart());
-            auto trailingRunIndex = findTrailingRunIndex(runs, index);
-            if (!trailingRunIndex) {
-                // This continuous content did not fit from the get-go. No trailing run.
-                return OverflowingTextContent::BreakingPosition { };
+        // At this point the available space is zero. Let's try the break these overflowing set of runs at the earliest possible.
+        if (auto partialRun = tryBreakingTextRun(runs, index, lineStatus.contentLogicalRight + nextContentWidth, 0, lineStatus.hasWrapOpportunityAtPreviousPosition)) {
+            // <span>unbreakable_and_overflows<span style="word-break: break-all">breakable</span>
+            // The partial run length could very well be 0 meaning the trailing run is actually the overflowing run (see above in the example).
+            if (partialRun->length) {
+                // We managed to break this text run mid content. It has to be either an arbitrary mid-word or a hyphen break.
+                return OverflowingTextContent::BreakingPosition { index, OverflowingTextContent::BreakingPosition::TrailingContent { true, partialRun } };
             }
+            auto trailingRunIndex = *findTrailingRunIndex(runs, index);
             // At worst we are back to the overflowing run, like in the example above.
-            ASSERT(*trailingRunIndex >= overflowingRunIndex);
-            return OverflowingTextContent::BreakingPosition { *trailingRunIndex, OverflowingTextContent::BreakingPosition::TrailingContent { true } };
+            ASSERT(trailingRunIndex >= overflowingRunIndex);
+            return OverflowingTextContent::BreakingPosition { trailingRunIndex, OverflowingTextContent::BreakingPosition::TrailingContent { true } };
         }
-        if (auto partialRun = tryBreakingTextRun(run, lineStatus.contentLogicalRight + nextContentWidth, { }, lineStatus.hasWrapOpportunityAtPreviousPosition)) {
-            ASSERT(partialRun->length);
-            // We managed to break this text run mid content. It has to be a hyphen break.
-            return OverflowingTextContent::BreakingPosition { index, OverflowingTextContent::BreakingPosition::TrailingContent { true, partialRun } };
-        }
         nextContentWidth += run.logicalWidth;
     }
     return { };
@@ -381,11 +391,11 @@
     // We have to have an overflowing run.
     RELEASE_ASSERT(overflowingRunIndex < runs.size());
 
-    // Check if we actually break this run.
+    // Check first if we can actually break the overflowing run.
     if (auto breakingPosition = tryBreakingOverflowingRun(lineStatus, runs, overflowingRunIndex, nonOverflowingContentWidth))
         return { overflowingRunIndex, breakingPosition };
 
-    // We did not manage to break the run that actually overflows the line.
+    // We did not manage to break the run that overflows the line.
     // Let's try to find a previous breaking position starting from the overflowing run. It surely fits.
     if (auto breakingPosition = tryBreakingPreviousNonOverflowingRuns(lineStatus, runs, overflowingRunIndex, nonOverflowingContentWidth))
         return { overflowingRunIndex, breakingPosition };
@@ -410,7 +420,7 @@
 
     // Breaking is allowed within “words”.
     if (style.wordBreak() == WordBreak::BreakAll)
-        return { WordBreakRule::AtArbitraryPosition };
+        return { WordBreakRule::AtArbitraryPositionWithinWords };
 
     auto includeHyphenationIfAllowed = [&](std::optional<InlineContentBreaker::WordBreakRule> wordBreakRule) -> OptionSet<InlineContentBreaker::WordBreakRule> {
         auto hyphenationIsAllowed = !n_hyphenationIsDisabled && style.hyphens() == Hyphens::Auto && canHyphenate(style.computedLocale());
@@ -437,11 +447,12 @@
     return includeHyphenationIfAllowed({ });
 }
 
-std::optional<InlineContentBreaker::PartialRun> InlineContentBreaker::tryBreakingTextRun(const ContinuousContent::Run& overflowingRun, InlineLayoutUnit logicalLeft, std::optional<InlineLayoutUnit> availableWidth, bool hasWrapOpportunityAtPreviousPosition) const
+std::optional<InlineContentBreaker::PartialRun> InlineContentBreaker::tryBreakingTextRun(const ContinuousContent::RunList& runs, size_t candidateRunIndex, InlineLayoutUnit logicalLeft, std::optional<InlineLayoutUnit> availableWidth, bool hasWrapOpportunityAtPreviousPosition) const
 {
-    ASSERT(overflowingRun.inlineItem.isText());
-    auto& inlineTextItem = downcast<InlineTextItem>(overflowingRun.inlineItem);
-    auto& style = overflowingRun.style;
+    auto& candidateRun = runs[candidateRunIndex];
+    ASSERT(candidateRun.inlineItem.isText());
+    auto& inlineTextItem = downcast<InlineTextItem>(candidateRun.inlineItem);
+    auto& style = candidateRun.style;
     auto availableSpaceIsInfinite = !availableWidth.has_value();
 
     auto breakRules = wordBreakBehavior(style, hasWrapOpportunityAtPreviousPosition);
@@ -449,6 +460,54 @@
         return { };
 
     auto& fontCascade = style.fontCascade();
+    if (breakRules.contains(WordBreakRule::AtArbitraryPositionWithinWords)) {
+        // Breaking is allowed within “words”: specifically, in addition to soft wrap opportunities allowed for normal, any typographic letter units
+        // It does not affect rules governing the soft wrap opportunities created by white space. Hyphenation is not applied.
+        ASSERT(!breakRules.contains(WordBreakRule::AtHyphenationOpportunities));
+        if (inlineTextItem.isWhitespace()) {
+            // AtArbitraryPositionWithinWords does not affect the breaking opportunities around whitespace.
+            return { };
+        }
+
+        if (!inlineTextItem.length()) {
+            // Empty text runs may be breakable based on style, but in practice we can't really split them any further.
+            return PartialRun { };
+        }
+
+        if (availableSpaceIsInfinite) {
+            // When the run can be split at arbitrary position we would normally just return the entire run when it is intended to fit on the line
+            // but here we have to check for mid-word break.
+            auto nextIndex = nextTextRunIndex(runs, candidateRunIndex);
+            if (!nextIndex || !downcast<InlineTextItem>(runs[*nextIndex].inlineItem).isWhitespace())
+                return PartialRun { inlineTextItem.length(), TextUtil::width(inlineTextItem, fontCascade, logicalLeft) };
+            // We've got room for the run but we have to try to break it mid-word.
+            if (inlineTextItem.length() == 1) {
+                // We can't mid-word break a single character.
+                return { };
+            }
+            auto startPosition = inlineTextItem.start();
+            auto endPosition = inlineTextItem.end() - 1;
+            return PartialRun { inlineTextItem.length() - 1, TextUtil::width(inlineTextItem, fontCascade, startPosition, endPosition, logicalLeft) };
+        }
+
+        if (!*availableWidth) {
+            auto previousIndex = previousTextRunIndex(runs, candidateRunIndex);
+            if (!previousIndex || !downcast<InlineTextItem>(runs[*previousIndex].inlineItem).isWhitespace()) {
+                // Fast path for cases when there's no room at all. We can break this content at the start.
+                return PartialRun { };
+            }
+            if (inlineTextItem.length() == 1) {
+                // We can't mid-word break a single character.
+                return { };
+            }
+            auto startPosition = inlineTextItem.start() + 1;
+            auto endPosition = inlineTextItem.end();
+            return PartialRun { inlineTextItem.length() - 1, TextUtil::width(inlineTextItem, fontCascade, startPosition, endPosition, logicalLeft) };
+        }
+        auto midWordBreak = TextUtil::midWordBreak(inlineTextItem, fontCascade, candidateRun.logicalWidth, *availableWidth, logicalLeft);
+        return PartialRun { midWordBreak.length, midWordBreak.logicalWidth };
+    }
+
     if (breakRules.contains(WordBreakRule::AtHyphenationOpportunities)) {
         auto tryBreakingAtHyphenationOpportunity = [&]() -> std::optional<PartialRun> {
             // Find the hyphen position as follows:
@@ -471,7 +530,7 @@
                 auto availableWidthExcludingHyphen = *availableWidth - hyphenWidth;
                 if (availableWidthExcludingHyphen <= 0 || !enoughWidthForHyphenation(availableWidthExcludingHyphen, fontCascade.pixelSize()))
                     return { };
-                leftSideLength = TextUtil::midWordBreak(inlineTextItem, fontCascade, overflowingRun.logicalWidth, availableWidthExcludingHyphen, logicalLeft).length;
+                leftSideLength = TextUtil::midWordBreak(inlineTextItem, fontCascade, candidateRun.logicalWidth, availableWidthExcludingHyphen, logicalLeft).length;
             }
             if (leftSideLength < limitBefore)
                 return { };
@@ -505,12 +564,13 @@
                 // Fast path for cases when there's no room at all. The content is breakable but we don't have space for it.
                 return { };
             }
-            auto midWordBreak = TextUtil::midWordBreak(inlineTextItem, fontCascade, overflowingRun.logicalWidth, *availableWidth, logicalLeft);
+            auto midWordBreak = TextUtil::midWordBreak(inlineTextItem, fontCascade, candidateRun.logicalWidth, *availableWidth, logicalLeft);
             return { midWordBreak.length, midWordBreak.logicalWidth };
         };
         // With arbitrary breaking there's always a valid breaking position (even if it is before the first position).
         return tryBreakingAtArbitraryPosition();
     }
+
     return { };
 }
 

Modified: trunk/Source/WebCore/layout/formattingContexts/inline/InlineContentBreaker.h (283613 => 283614)


--- trunk/Source/WebCore/layout/formattingContexts/inline/InlineContentBreaker.h	2021-10-06 11:26:32 UTC (rev 283613)
+++ trunk/Source/WebCore/layout/formattingContexts/inline/InlineContentBreaker.h	2021-10-06 13:40:59 UTC (rev 283614)
@@ -134,14 +134,15 @@
         std::optional<BreakingPosition> breakingPosition { }; // Where we actually break this overflowing content.
     };
     OverflowingTextContent processOverflowingContentWithText(const ContinuousContent&, const LineStatus&) const;
-    std::optional<PartialRun> tryBreakingTextRun(const ContinuousContent::Run& overflowRun, InlineLayoutUnit logicalLeft, std::optional<InlineLayoutUnit> availableWidth, bool hasWrapOpportunityAtPreviousPosition) const;
+    std::optional<PartialRun> tryBreakingTextRun(const ContinuousContent::RunList& runs, size_t candidateRunIndex, InlineLayoutUnit logicalLeft, std::optional<InlineLayoutUnit> availableWidth, bool hasWrapOpportunityAtPreviousPosition) const;
     std::optional<OverflowingTextContent::BreakingPosition> tryBreakingOverflowingRun(const LineStatus&, const ContinuousContent::RunList&, size_t overflowingRunIndex, InlineLayoutUnit nonOverflowingContentWidth) const;
     std::optional<OverflowingTextContent::BreakingPosition> tryBreakingPreviousNonOverflowingRuns(const LineStatus&, const ContinuousContent::RunList&, size_t overflowingRunIndex, InlineLayoutUnit nonOverflowingContentWidth) const;
     std::optional<OverflowingTextContent::BreakingPosition> tryBreakingNextOverflowingRuns(const LineStatus&, const ContinuousContent::RunList&, size_t overflowingRunIndex, InlineLayoutUnit nonOverflowingContentWidth) const;
 
     enum class WordBreakRule {
-        AtArbitraryPosition        = 1 << 0,
-        AtHyphenationOpportunities = 1 << 1
+        AtArbitraryPositionWithinWords = 1 << 0,
+        AtArbitraryPosition            = 1 << 1,
+        AtHyphenationOpportunities     = 1 << 2
     };
     OptionSet<WordBreakRule> wordBreakBehavior(const RenderStyle&, bool hasWrapOpportunityAtPreviousPosition) const;
     bool shouldKeepEndOfLineWhitespace(const ContinuousContent&) const;
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to