[LFC][IFC] Trim trailing letter-spacing at inline container boundary
https://bugs.webkit.org/show_bug.cgi?id=204895
<rdar://problem/57666898>

Reviewed by Antti Koivisto.

According to https://www.w3.org/TR/css-text-3/#letter-spacing-property, "An inline box only
includes letter spacing between characters completely contained within that element".
This patch enables this behavior by trimming the trailing letter spacing at [container end].

<div>1<span style="letter-spacing: 100px;">2</span>3</div> ->
[1][container start][2][container end][3]
vs.
[1][container start][2<-----100px----->][container end][3]

* layout/inlineformatting/InlineLineBuilder.cpp:
(WebCore::Layout::LineBuilder::removeTrailingTrimmableContent):
(WebCore::Layout::LineBuilder::appendInlineContainerEnd):
(WebCore::Layout::LineBuilder::TrimmableContent::trim):
(WebCore::Layout::LineBuilder::TrimmableContent::trimTrailingRun):
* layout/inlineformatting/InlineLineBuilder.h:
(WebCore::Layout::LineBuilder::TrimmableContent::isTrailingRunPartiallyTrimmable const):

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@253154 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 2638d77..08d0f00 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,28 @@
+2019-12-05  Zalan Bujtas  <zalan@apple.com>
+
+        [LFC][IFC] Trim trailing letter-spacing at inline container boundary
+        https://bugs.webkit.org/show_bug.cgi?id=204895
+        <rdar://problem/57666898>
+
+        Reviewed by Antti Koivisto.
+
+        According to https://www.w3.org/TR/css-text-3/#letter-spacing-property, "An inline box only
+        includes letter spacing between characters completely contained within that element".
+        This patch enables this behavior by trimming the trailing letter spacing at [container end].
+
+        <div>1<span style="letter-spacing: 100px;">2</span>3</div> ->
+        [1][container start][2][container end][3]
+        vs.
+        [1][container start][2<-----100px----->][container end][3]
+
+        * layout/inlineformatting/InlineLineBuilder.cpp:
+        (WebCore::Layout::LineBuilder::removeTrailingTrimmableContent):
+        (WebCore::Layout::LineBuilder::appendInlineContainerEnd):
+        (WebCore::Layout::LineBuilder::TrimmableContent::trim):
+        (WebCore::Layout::LineBuilder::TrimmableContent::trimTrailingRun):
+        * layout/inlineformatting/InlineLineBuilder.h:
+        (WebCore::Layout::LineBuilder::TrimmableContent::isTrailingRunPartiallyTrimmable const):
+
 2019-12-05  youenn fablet  <youenn@apple.com>
 
         maplike should define a set method
diff --git a/Source/WebCore/layout/inlineformatting/InlineLineBuilder.cpp b/Source/WebCore/layout/inlineformatting/InlineLineBuilder.cpp
index a08424a..6374d4f 100644
--- a/Source/WebCore/layout/inlineformatting/InlineLineBuilder.cpp
+++ b/Source/WebCore/layout/inlineformatting/InlineLineBuilder.cpp
@@ -367,9 +367,7 @@
     if (m_trimmableContent.isEmpty() || m_inlineItemRuns.isEmpty())
         return;
 
-    m_lineBox.shrinkHorizontally(m_trimmableContent.width());
-
-    m_trimmableContent.trim();
+    m_lineBox.shrinkHorizontally(m_trimmableContent.trim());
     // If we trimmed the first visible run on the line, we need to re-check the visibility status.
     if (!m_lineIsVisuallyEmptyBeforeTrimmableContent)
         return;
@@ -441,6 +439,14 @@
 void LineBuilder::appendInlineContainerEnd(const InlineItem& inlineItem, LayoutUnit logicalWidth)
 {
     // This is really just a placeholder to mark the end of the inline level container </span>.
+    auto trimTrailingLetterSpacing = [&] {
+        if (!m_trimmableContent.isTrailingRunPartiallyTrimmable())
+            return;
+        m_lineBox.shrinkHorizontally(m_trimmableContent.trimTrailingRun());
+    };
+    // Prevent trailing letter-spacing from spilling out of the inline container.
+    // https://drafts.csswg.org/css-text-3/#letter-spacing-property See example 21.
+    trimTrailingLetterSpacing();
     appendNonBreakableSpace(inlineItem, contentLogicalRight(), logicalWidth);
 }
 
@@ -712,10 +718,9 @@
     m_firstRunIndex = m_firstRunIndex.valueOr(runIndex);
 }
 
-void LineBuilder::TrimmableContent::trim()
+LayoutUnit LineBuilder::TrimmableContent::trim()
 {
-    if (!m_firstRunIndex)
-        return;
+    ASSERT(!isEmpty());
 #ifndef NDEBUG
     auto hasSeenNonWhitespaceTextContent = false;
 #endif
@@ -747,6 +752,38 @@
     }
     ASSERT(accumulatedTrimmedWidth == width());
     reset();
+    return accumulatedTrimmedWidth;
+}
+
+LayoutUnit LineBuilder::TrimmableContent::trimTrailingRun()
+{
+    ASSERT(!isEmpty());
+    // Find the last trimmable run (it is not necessarily the last run e.g [container start][whitespace][container end])
+    for (auto index = m_inlineitemRunList.size(); index-- && *m_firstRunIndex >= index;) {
+        auto& run = m_inlineitemRunList[index];
+        if (!run.isText()) {
+            ASSERT(run.isContainerStart() || run.isContainerEnd());
+            continue;
+        }
+        LayoutUnit trimmedWidth;
+        if (run.isWhitespace()) {
+            trimmedWidth = run.logicalWidth();
+            run.setCollapsesToZeroAdvanceWidth();
+        } else {
+            ASSERT(run.hasTrailingLetterSpacing());
+            trimmedWidth = run.trailingLetterSpacing();
+            run.removeTrailingLetterSpacing();
+        }
+        m_width -= trimmedWidth;
+        // We managed to trim the last trimmable run.
+        if (index == *m_firstRunIndex) {
+            ASSERT(!m_width);
+            m_firstRunIndex = { };
+        }
+        return trimmedWidth;
+    }
+    ASSERT_NOT_REACHED();
+    return { };
 }
 
 LineBuilder::InlineItemRun::InlineItemRun(const InlineItem& inlineItem, LayoutUnit logicalLeft, LayoutUnit logicalWidth, WTF::Optional<Display::Run::TextContext> textContext)
diff --git a/Source/WebCore/layout/inlineformatting/InlineLineBuilder.h b/Source/WebCore/layout/inlineformatting/InlineLineBuilder.h
index d9f77c5..1c01479 100644
--- a/Source/WebCore/layout/inlineformatting/InlineLineBuilder.h
+++ b/Source/WebCore/layout/inlineformatting/InlineLineBuilder.h
@@ -205,13 +205,15 @@
         TrimmableContent(InlineItemRunList&);
 
         void append(size_t runIndex);
-        void trim();
+        LayoutUnit trim();
+        LayoutUnit trimTrailingRun();
         void reset();
 
         LayoutUnit width() const { return m_width; }
         Optional<size_t> firstRunIndex() { return m_firstRunIndex; }
         bool isEmpty() const { return !m_firstRunIndex.hasValue(); }
         bool isTrailingRunFullyTrimmable() const { return m_lastRunIsFullyTrimmable; }
+        bool isTrailingRunPartiallyTrimmable() const { return !isEmpty() && !isTrailingRunFullyTrimmable(); }
 
     private:
         InlineItemRunList& m_inlineitemRunList;