Position::upstream/downstream should not need to call ensureLineBoxes
authorantti@apple.com <antti@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 11 Oct 2019 19:03:37 +0000 (19:03 +0000)
committerantti@apple.com <antti@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 11 Oct 2019 19:03:37 +0000 (19:03 +0000)
https://bugs.webkit.org/show_bug.cgi?id=202203

Reviewed by Zalan Bujtas.

Source/WebCore:

This avoids forced switch to complex text layout path by Position constructor and will allow future cleanups.

Currently simple line path strips end of line whitespace when white-space:pre-wrap is set.
These are don't affect rendering but they are needed for editing positions.
This patch makes simple line path match the complex path by generating runs for these whitespaces.

* dom/Position.cpp:
(WebCore::Position::upstream const):
(WebCore::Position::downstream const):
(WebCore::ensureLineBoxesIfNeeded): Deleted.
* rendering/SimpleLineLayout.cpp:
(WebCore::SimpleLineLayout::LineState::appendFragmentAndCreateRunIfNeeded):

Create a new run if isLineBreak bit is set.

(WebCore::SimpleLineLayout::LineState::removeTrailingWhitespace):
(WebCore::SimpleLineLayout::LineState::trailingWhitespaceWidth const):
(WebCore::SimpleLineLayout::computeLineLeft):

Also compute width of the hanging whitespace when aligning the line. This matches the code
in updateLogicalWidthForLeft/Right/CenterAlignedBlock in the complex path.

(WebCore::SimpleLineLayout::preWrap):

breakSpaces implies preWrap is off.

(WebCore::SimpleLineLayout::firstFragment):
(WebCore::SimpleLineLayout::createLineRuns):

Crete runs also for soft linebreaks in pre-wrap.
Add whitespace runs to the end of the line in pre-wrap.

(WebCore::SimpleLineLayout::closeLineEndingAndAdjustRuns):

Hang the whitespace run when wrapping.

(WebCore::SimpleLineLayout::removeTrailingWhitespace): Deleted.

Remainging logic moved to the callsite.

LayoutTests:

Some additional end of line whitespaces.

* TestExpectations:

Skip imported/w3c/web-platform-tests/css/css-text/white-space/pre-wrap-013.html.

This test starts failing because soft linebreak clears the trailing whitespace run.
The failing behavior aligns simple path with the complex path. The existing textarea-pre-wrap-013.html
test (which takes the complex path) is already skipped because of this.

* platform/mac/fast/forms/targeted-frame-submission-expected.txt:
* platform/mac/fast/forms/textarea-scroll-height-expected.txt:
* platform/mac/fast/loader/text-document-wrapping-expected.txt:
* platform/mac/fast/parser/open-comment-in-textarea-expected.txt:
* platform/mac/http/tests/misc/acid3-expected.txt:
* platform/mac/http/tests/navigation/javascriptlink-frames-expected.txt:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@251015 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
LayoutTests/ChangeLog
LayoutTests/TestExpectations
LayoutTests/platform/mac-highsierra/fast/forms/targeted-frame-submission-expected.txt
LayoutTests/platform/mac-highsierra/http/tests/navigation/javascriptlink-frames-expected.txt
LayoutTests/platform/mac/fast/forms/targeted-frame-submission-expected.txt
LayoutTests/platform/mac/fast/forms/textarea-scroll-height-expected.txt
LayoutTests/platform/mac/fast/loader/text-document-wrapping-expected.txt
LayoutTests/platform/mac/fast/parser/open-comment-in-textarea-expected.txt
LayoutTests/platform/mac/http/tests/misc/acid3-expected.txt
LayoutTests/platform/mac/http/tests/navigation/javascriptlink-frames-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/dom/Position.cpp
Source/WebCore/rendering/SimpleLineLayout.cpp

index 83944c2..f507fd3 100644 (file)
@@ -1,3 +1,27 @@
+2019-10-11  Antti Koivisto  <antti@apple.com>
+
+        Position::upstream/downstream should not need to call ensureLineBoxes
+        https://bugs.webkit.org/show_bug.cgi?id=202203
+
+        Reviewed by Zalan Bujtas.
+
+        Some additional end of line whitespaces.
+
+        * TestExpectations:
+
+        Skip imported/w3c/web-platform-tests/css/css-text/white-space/pre-wrap-013.html.
+
+        This test starts failing because soft linebreak clears the trailing whitespace run.
+        The failing behavior aligns simple path with the complex path. The existing textarea-pre-wrap-013.html
+        test (which takes the complex path) is already skipped because of this.
+
+        * platform/mac/fast/forms/targeted-frame-submission-expected.txt:
+        * platform/mac/fast/forms/textarea-scroll-height-expected.txt:
+        * platform/mac/fast/loader/text-document-wrapping-expected.txt:
+        * platform/mac/fast/parser/open-comment-in-textarea-expected.txt:
+        * platform/mac/http/tests/misc/acid3-expected.txt:
+        * platform/mac/http/tests/navigation/javascriptlink-frames-expected.txt:
+
 2019-10-11  Dean Jackson  <dino@apple.com>
 
         Layout test fast/events/touch/ios/passive-by-default-on-document-and-window.html is a flaky failure on Internal iOS Testers
index ef1216e..6117526 100644 (file)
@@ -2463,6 +2463,7 @@ webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/text-transform/
 webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/text-transform/text-transform-upperlower-039.html [ ImageOnlyFailure ]
 webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/text-transform/text-transform-upperlower-103.html [ ImageOnlyFailure Pass ]
 webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/text-transform/text-transform-upperlower-104.html [ ImageOnlyFailure Pass ]
+webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/white-space/pre-wrap-013.html [ ImageOnlyFailure ]
 webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/white-space/textarea-pre-wrap-012.html [ ImageOnlyFailure ]
 webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/white-space/textarea-pre-wrap-013.html [ ImageOnlyFailure ]
 webkit.org/b/183258 imported/w3c/web-platform-tests/css/css-text/word-break/word-break-break-all-004.html [ ImageOnlyFailure ]
index 82750ad..23f90f8 100644 (file)
@@ -24,4 +24,5 @@ layer at (0,0) size 800x600
                 RenderBlock {PRE} at (0,0) size 284x15
                   RenderText {#text} at (0,0) size 55x15
                     text run at (0,0) width 55: "SUCCESS"
+                    text run at (54,0) width 1: " "
         RenderText {#text} at (0,0) size 0x0
index 389952c..c5c3fff 100644 (file)
@@ -108,6 +108,7 @@ layer at (0,0) size 800x600
             RenderBlock {DIV} at (3,3) size 225x26
               RenderText {#text} at (0,0) size 178x13
                 text run at (0,0) width 178: "More initial text before user input."
+                text run at (177,0) width 1: " "
               RenderBR {BR} at (0,13) size 0x13
       RenderFrame {FRAME} at (0,540) size 800x60
         layer at (0,0) size 785x90
index bf859ea..30ebf1d 100644 (file)
@@ -24,4 +24,5 @@ layer at (0,0) size 800x600
                 RenderBlock {PRE} at (0,0) size 284x15
                   RenderText {#text} at (0,0) size 55x15
                     text run at (0,0) width 55: "SUCCESS"
+                    text run at (54,0) width 1: " "
         RenderText {#text} at (0,0) size 0x0
index d92134d..3d16b0b 100644 (file)
@@ -12,26 +12,42 @@ layer at (8,8) size 200x200 clip at (9,9) size 183x198 scrollHeight 316
     RenderBlock {DIV} at (3,3) size 179x312
       RenderText {#text} at (0,0) size 83x299
         text run at (0,0) width 83: "Lots of content."
+        text run at (82,0) width 1: " "
         text run at (0,13) width 83: "Lots of content."
+        text run at (82,13) width 1: " "
         text run at (0,26) width 83: "Lots of content."
+        text run at (82,26) width 1: " "
         text run at (0,39) width 83: "Lots of content."
+        text run at (82,39) width 1: " "
         text run at (0,52) width 0: " "
         text run at (0,65) width 83: "Lots of content."
+        text run at (82,65) width 1: " "
         text run at (0,78) width 83: "Lots of content."
+        text run at (82,78) width 1: " "
         text run at (0,91) width 0: " "
         text run at (0,104) width 83: "Lots of content."
+        text run at (82,104) width 1: " "
         text run at (0,117) width 83: "Lots of content."
+        text run at (82,117) width 1: " "
         text run at (0,130) width 0: " "
         text run at (0,143) width 83: "Lots of content."
+        text run at (82,143) width 1: " "
         text run at (0,156) width 83: "Lots of content."
+        text run at (82,156) width 1: " "
         text run at (0,169) width 0: " "
         text run at (0,182) width 83: "Lots of content."
+        text run at (82,182) width 1: " "
         text run at (0,195) width 83: "Lots of content."
+        text run at (82,195) width 1: " "
         text run at (0,208) width 0: " "
         text run at (0,221) width 83: "Lots of content."
+        text run at (82,221) width 1: " "
         text run at (0,234) width 83: "Lots of content."
+        text run at (82,234) width 1: " "
         text run at (0,247) width 0: " "
         text run at (0,260) width 83: "Lots of content."
+        text run at (82,260) width 1: " "
         text run at (0,273) width 83: "Lots of content."
+        text run at (82,273) width 1: " "
         text run at (0,286) width 0: " "
       RenderBR {BR} at (0,299) size 0x13
index a6c601d..d796c81 100644 (file)
@@ -12,7 +12,9 @@ layer at (0,0) size 800x600
               RenderBlock {PRE} at (0,0) size 784x75
                 RenderText {#text} at (0,0) size 781x75
                   text run at (0,0) width 406: "This line should wrap with no horizontal scroll bar:"
+                  text run at (405,0) width 1: " "
                   text run at (0,15) width 0: " "
                   text run at (0,30) width 781: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv"
                   text run at (0,45) width 781: "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr"
                   text run at (0,60) width 469: "stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
+                  text run at (468,60) width 1: " "
index e89ad7f..23f4892 100644 (file)
@@ -8,8 +8,10 @@ layer at (0,0) size 800x600
 layer at (10,10) size 161x32 clip at (11,11) size 144x30 scrollHeight 56
   RenderTextControl {TEXTAREA} at (2,2) size 161x32 [bgcolor=#FFFFFF] [border: (1px solid #000000)]
     RenderBlock {DIV} at (3,3) size 140x52
-      RenderText {#text} at (0,0) size 136x39
+      RenderText {#text} at (0,0) size 139x39
         text run at (0,0) width 22: "<!--"
-        text run at (0,13) width 136: "This should be part of the"
+        text run at (21,0) width 1: " "
+        text run at (0,13) width 139: "This should be part of the "
         text run at (0,26) width 47: "textarea."
+        text run at (46,26) width 1: " "
       RenderBR {BR} at (0,39) size 0x13
index 98cf28e..8937c19 100644 (file)
@@ -54,7 +54,7 @@ layer at (20,20) size 644x433
                       text run at (0,90) width 8: "Y"
                       text run at (0,105) width 8: "P"
                       text run at (0,120) width 8: "E"
-                      text run at (0,135) width 8: " "
+                      text run at (0,135) width 0: " "
                       text run at (0,150) width 8: "h"
                       text run at (0,165) width 8: "t"
                       text run at (0,180) width 8: "m"
index 680f54b..c65860d 100644 (file)
@@ -108,6 +108,7 @@ layer at (0,0) size 800x600
             RenderBlock {DIV} at (3,3) size 225x26
               RenderText {#text} at (0,0) size 178x13
                 text run at (0,0) width 178: "More initial text before user input."
+                text run at (177,0) width 1: " "
               RenderBR {BR} at (0,13) size 0x13
       RenderFrame {FRAME} at (0,540) size 800x60
         layer at (0,0) size 785x90
index 6f7d1a3..20378d4 100644 (file)
@@ -1,3 +1,50 @@
+2019-10-11  Antti Koivisto  <antti@apple.com>
+
+        Position::upstream/downstream should not need to call ensureLineBoxes
+        https://bugs.webkit.org/show_bug.cgi?id=202203
+
+        Reviewed by Zalan Bujtas.
+
+        This avoids forced switch to complex text layout path by Position constructor and will allow future cleanups.
+
+        Currently simple line path strips end of line whitespace when white-space:pre-wrap is set.
+        These are don't affect rendering but they are needed for editing positions.
+        This patch makes simple line path match the complex path by generating runs for these whitespaces.
+
+        * dom/Position.cpp:
+        (WebCore::Position::upstream const):
+        (WebCore::Position::downstream const):
+        (WebCore::ensureLineBoxesIfNeeded): Deleted.
+        * rendering/SimpleLineLayout.cpp:
+        (WebCore::SimpleLineLayout::LineState::appendFragmentAndCreateRunIfNeeded):
+
+        Create a new run if isLineBreak bit is set.
+
+        (WebCore::SimpleLineLayout::LineState::removeTrailingWhitespace):
+        (WebCore::SimpleLineLayout::LineState::trailingWhitespaceWidth const):
+        (WebCore::SimpleLineLayout::computeLineLeft):
+
+        Also compute width of the hanging whitespace when aligning the line. This matches the code
+        in updateLogicalWidthForLeft/Right/CenterAlignedBlock in the complex path.
+
+        (WebCore::SimpleLineLayout::preWrap):
+
+        breakSpaces implies preWrap is off.
+
+        (WebCore::SimpleLineLayout::firstFragment):
+        (WebCore::SimpleLineLayout::createLineRuns):
+
+        Crete runs also for soft linebreaks in pre-wrap.
+        Add whitespace runs to the end of the line in pre-wrap.
+
+        (WebCore::SimpleLineLayout::closeLineEndingAndAdjustRuns):
+
+        Hang the whitespace run when wrapping.
+
+        (WebCore::SimpleLineLayout::removeTrailingWhitespace): Deleted.
+
+        Remainging logic moved to the callsite.
+
 2019-10-11  Jonathan Bedard  <jbedard@apple.com>
 
         Unreviewed, rolling out r250945.
index dac9f89..848efd1 100644 (file)
@@ -657,13 +657,6 @@ static bool isStreamer(const PositionIterator& pos)
     return pos.atStartOfNode();
 }
 
-static void ensureLineBoxesIfNeeded(RenderObject& renderer)
-{
-    if (!is<RenderText>(renderer) && !is<RenderLineBreak>(renderer))
-        return;
-    is<RenderText>(renderer) ? downcast<RenderText>(renderer).ensureLineBoxes() : downcast<RenderLineBreak>(renderer).ensureLineBoxes();
-}
-
 // This function and downstream() are used for moving back and forth between visually equivalent candidates.
 // For example, for the text node "foo     bar" where whitespace is collapsible, there are two candidates 
 // that map to the VisiblePosition between 'b' and the space.  This function will return the left candidate 
@@ -710,9 +703,6 @@ Position Position::upstream(EditingBoundaryCrossingRule rule) const
         if (!renderer || renderer->style().visibility() != Visibility::Visible)
             continue;
 
-        // FIXME: The code below doesn't need line boxes.
-        ensureLineBoxesIfNeeded(*renderer);
-
         if (rule == CanCrossEditingBoundary && boundaryCrossed) {
             lastVisible = currentPosition;
             break;
@@ -825,9 +815,6 @@ Position Position::downstream(EditingBoundaryCrossingRule rule) const
         if (!renderer || renderer->style().visibility() != Visibility::Visible)
             continue;
 
-        // FIXME: The code below doesn't need line boxes.
-        ensureLineBoxesIfNeeded(*renderer);
-
         if (rule == CanCrossEditingBoundary && boundaryCrossed) {
             lastVisible = currentPosition;
             break;
index 5166aea..a53e35d 100644 (file)
@@ -355,30 +355,6 @@ bool canUseFor(const RenderBlockFlow& flow)
     return canUseForWithReason(flow, IncludeReasons::First) == NoReason;
 }
 
-static float computeLineLeft(TextAlignMode textAlign, float availableWidth, float committedWidth, float logicalLeftOffset)
-{
-    float remainingWidth = availableWidth - committedWidth;
-    float left = logicalLeftOffset;
-    switch (textAlign) {
-    case TextAlignMode::Left:
-    case TextAlignMode::WebKitLeft:
-    case TextAlignMode::Start:
-        return left;
-    case TextAlignMode::Right:
-    case TextAlignMode::WebKitRight:
-    case TextAlignMode::End:
-        return left + std::max<float>(remainingWidth, 0);
-    case TextAlignMode::Center:
-    case TextAlignMode::WebKitCenter:
-        return left + std::max<float>(remainingWidth / 2, 0);
-    case TextAlignMode::Justify:
-        ASSERT_NOT_REACHED();
-        break;
-    }
-    ASSERT_NOT_REACHED();
-    return 0;
-}
-
 static void revertAllRunsOnCurrentLine(Layout::RunVector& runs)
 {
     while (!runs.isEmpty() && !runs.last().isEndOfLine)
@@ -491,10 +467,10 @@ public:
                 // This fragment is collapsed completely. No run is needed.
                 return;
             }
-            if (m_lastFragment.isLastInRenderer() || m_lastFragment.isCollapsed())
+            Run& lastRun = runs.last();
+            if (m_lastFragment.isLastInRenderer() || m_lastFragment.isCollapsed() || fragment.isLineBreak() || lastRun.isLineBreak)
                 runs.append(Run(fragment.start(), endPosition, m_runsWidth, m_runsWidth + fragment.width(), false, fragment.hasHyphen(), fragment.isLineBreak()));
             else {
-                Run& lastRun = runs.last();
                 lastRun.end = endPosition;
                 lastRun.logicalRight += fragment.width();
                 ASSERT(!lastRun.hasHyphen);
@@ -534,7 +510,7 @@ public:
 
     void removeTrailingWhitespace(Layout::RunVector& runs)
     {
-        if (m_lastFragment.type() != TextFragmentIterator::TextFragment::Whitespace)
+        if (!hasTrailingWhitespace())
             return;
         if (m_lastNonWhitespaceFragment) {
             auto needsReverting = m_lastNonWhitespaceFragment->end() != m_lastFragment.end();
@@ -556,6 +532,8 @@ public:
         m_lastFragment = TextFragmentIterator::TextFragment();
     }
 
+    float trailingWhitespaceWidth() const { return m_trailingWhitespaceWidth; }
+
 private:
     bool expansionOpportunity(TextFragmentIterator::TextFragment::Type currentFragmentType, TextFragmentIterator::TextFragment::Type previousFragmentType) const
     {
@@ -581,24 +559,37 @@ private:
     Optional<Vector<TextFragmentIterator::TextFragment, 30>> m_fragments;
 };
 
-static bool preWrap(const TextFragmentIterator::Style& style)
+static float computeLineLeft(const LineState& line, TextAlignMode textAlign, float& hangingWhitespaceWidth)
 {
-    return style.wrapLines && !style.collapseWhitespace;
+    float totalWidth = line.width() - hangingWhitespaceWidth;
+    float remainingWidth = line.availableWidth() - totalWidth;
+    float left = line.logicalLeftOffset();
+    switch (textAlign) {
+    case TextAlignMode::Left:
+    case TextAlignMode::WebKitLeft:
+    case TextAlignMode::Start:
+        hangingWhitespaceWidth = std::max(0.f, std::min(hangingWhitespaceWidth, remainingWidth));
+        return left;
+    case TextAlignMode::Right:
+    case TextAlignMode::WebKitRight:
+    case TextAlignMode::End:
+        hangingWhitespaceWidth = 0;
+        return left + std::max<float>(remainingWidth, 0);
+    case TextAlignMode::Center:
+    case TextAlignMode::WebKitCenter:
+        hangingWhitespaceWidth = std::max(0.f, std::min(hangingWhitespaceWidth, (remainingWidth + 1) / 2));
+        return left + std::max<float>(remainingWidth / 2, 0);
+    case TextAlignMode::Justify:
+        ASSERT_NOT_REACHED();
+        break;
+    }
+    ASSERT_NOT_REACHED();
+    return 0;
 }
-    
-static void removeTrailingWhitespace(LineState& lineState, Layout::RunVector& runs, const TextFragmentIterator& textFragmentIterator)
+
+static bool preWrap(const TextFragmentIterator::Style& style)
 {
-    if (!lineState.hasTrailingWhitespace())
-        return;
-    // Remove collapsed whitespace, or non-collapsed pre-wrap whitespace, unless it's the only content on the line -so removing the whitesapce
-    // would produce an empty line.
-    const auto& style = textFragmentIterator.style();
-    bool collapseWhitespace = style.collapseWhitespace || (!style.breakSpaces && preWrap(style));
-    if (!collapseWhitespace)
-        return;
-    if (preWrap(style) && lineState.isWhitespaceOnly())
-        return;
-    lineState.removeTrailingWhitespace(runs);
+    return style.wrapLines && !style.collapseWhitespace && !style.breakSpaces;
 }
 
 static void updateLineConstrains(const RenderBlockFlow& flow, LineState& line, const LineState& previousLine, unsigned& numberOfPrecedingLinesWithHyphen, const TextFragmentIterator::Style& style, bool isFirstLine)
@@ -758,11 +749,10 @@ static TextFragmentIterator::TextFragment firstFragment(TextFragmentIterator& te
     }
     // Special overflow pre-wrap whitespace handling: skip the overflowed whitespace (even when style says not-collapsible)
     // if we manage to fit at least one character on the previous line.
-    auto preWrapIsOn = preWrap(style);
-    if ((style.collapseWhitespace || preWrapIsOn) && previousLine.firstCharacterFits()) {
+    if ((style.collapseWhitespace || style.wrapLines) && previousLine.firstCharacterFits()) {
         // If skipping the whitespace puts us on a newline, skip the newline too as we already wrapped the line.
         auto firstFragmentCandidate = consumeLineBreakIfNeeded(textFragmentIterator.nextTextFragment(), textFragmentIterator, currentLine, runs,
-            preWrapIsOn ? PreWrapLineBreakRule::Ignore : PreWrapLineBreakRule::Preserve);
+            preWrap(style) ? PreWrapLineBreakRule::Ignore : PreWrapLineBreakRule::Preserve);
         return skipWhitespaceIfNeeded(firstFragmentCandidate, textFragmentIterator);
     }
     return skipWhitespaceIfNeeded(overflowedFragment, textFragmentIterator);
@@ -799,7 +789,7 @@ static bool createLineRuns(LineState& line, const LineState& previousLine, Layou
         // Hard and soft linebreaks.
         if (fragment.isLineBreak()) {
             // Add the new line fragment only if there's nothing on the line. (otherwise the extra new line character would show up at the end of the content.)
-            if (line.isEmpty() || fragment.type() == TextFragmentIterator::TextFragment::HardLineBreak) {
+            if (line.isEmpty() || fragment.type() == TextFragmentIterator::TextFragment::HardLineBreak || preWrap(style)) {
                 if (style.textAlign == TextAlignMode::Right || style.textAlign == TextAlignMode::WebKitRight)
                     line.removeTrailingWhitespace(runs);
                 line.appendFragmentAndCreateRunIfNeeded(fragment, runs);
@@ -826,6 +816,14 @@ static bool createLineRuns(LineState& line, const LineState& previousLine, Layou
                     textFragmentIterator.revertToEndOfFragment(line.revertToLastCompleteFragment(runs));
                     break;
                 }
+                if (preWrap(style)) {
+                    line.appendFragmentAndCreateRunIfNeeded(fragment, runs);
+                    fragment = textFragmentIterator.nextTextFragment(line.width());
+                    if (fragment.isLineBreak())
+                        continue;
+                    line.setOverflowedFragment(fragment);
+                    break;
+                }
                 // Split the whitespace; left part stays on this line, right is pushed to next line.
                 line.setOverflowedFragment(splitFragmentToFitLine(fragment, line, textFragmentIterator));
                 line.appendFragmentAndCreateRunIfNeeded(fragment, runs);
@@ -931,22 +929,38 @@ static void closeLineEndingAndAdjustRuns(LineState& line, Layout::RunVector& run
 {
     if (!runs.size() || (lastRunIndexOfPreviousLine && runs.size() - 1 == lastRunIndexOfPreviousLine.value()))
         return;
-    removeTrailingWhitespace(line, runs, textFragmentIterator);
+
+    const auto& style = textFragmentIterator.style();
+
+    if (style.collapseWhitespace)
+        line.removeTrailingWhitespace(runs);
+
     if (!runs.size())
         return;
+
     // Adjust runs' position by taking line's alignment into account.
-    const auto& style = textFragmentIterator.style();
     auto firstRunIndex = lastRunIndexOfPreviousLine ? lastRunIndexOfPreviousLine.value() + 1 : 0;
     auto lineLogicalLeft = line.logicalLeftOffset();
     auto textAlign = textAlignForLine(style, lastLineInFlow || (line.lastFragment().isValid() && line.lastFragment().type() == TextFragmentIterator::TextFragment::HardLineBreak));
-    if (textAlign == TextAlignMode::Justify)
+
+    // https://www.w3.org/TR/css-text-3/#white-space-phase-2
+    bool shouldHangTrailingWhitespace = style.wrapLines && line.trailingWhitespaceWidth();
+    auto hangingWhitespaceWidth = shouldHangTrailingWhitespace ? line.trailingWhitespaceWidth() : 0;
+
+    if (textAlign == TextAlignMode::Justify) {
         justifyRuns(line, runs, firstRunIndex);
-    else
-        lineLogicalLeft = computeLineLeft(textAlign, line.availableWidth(), line.width(), line.logicalLeftOffset());
+        hangingWhitespaceWidth = 0;
+    } else
+        lineLogicalLeft = computeLineLeft(line, textAlign, hangingWhitespaceWidth);
+
     for (auto i = firstRunIndex; i < runs.size(); ++i) {
         runs[i].logicalLeft += lineLogicalLeft;
         runs[i].logicalRight += lineLogicalLeft;
     }
+
+    if (shouldHangTrailingWhitespace && hangingWhitespaceWidth < line.trailingWhitespaceWidth())
+        runs.last().logicalRight = runs.last().logicalRight - (line.trailingWhitespaceWidth() - hangingWhitespaceWidth);
+
     runs.last().isEndOfLine = true;
     ++lineCount;
 }