Merge "Treat ReplacementSpan transition point as a break candidate"
diff --git a/libs/minikin/MeasuredText.cpp b/libs/minikin/MeasuredText.cpp
index e5c8dd5..af0d0ee 100644
--- a/libs/minikin/MeasuredText.cpp
+++ b/libs/minikin/MeasuredText.cpp
@@ -142,7 +142,10 @@
 
         proc.updateLocaleIfNecessary(*run);
         for (uint32_t i = range.getStart(); i < range.getEnd(); ++i) {
-            proc.feedChar(i, textBuf[i], widths[i], run->canBreak());
+            // Even if the run is not a candidate of line break, treat the end of run as the line
+            // break candidate.
+            const bool canBreak = run->canBreak() || (i + 1) == range.getEnd();
+            proc.feedChar(i, textBuf[i], widths[i], canBreak);
 
             const uint32_t nextCharOffset = i + 1;
             if (nextCharOffset != proc.nextWordBreak) {
diff --git a/libs/minikin/OptimalLineBreaker.cpp b/libs/minikin/OptimalLineBreaker.cpp
index 3e6319b..b05a94d 100644
--- a/libs/minikin/OptimalLineBreaker.cpp
+++ b/libs/minikin/OptimalLineBreaker.cpp
@@ -231,7 +231,10 @@
 
         for (uint32_t i = range.getStart(); i < range.getEnd(); ++i) {
             MINIKIN_ASSERT(textBuf[i] != CHAR_TAB, "TAB is not supported in optimal line breaker");
-            proc.feedChar(i, textBuf[i], measured.widths[i], run->canBreak());
+            // Even if the run is not a candidate of line break, treat the end of run as the line
+            // break candidate.
+            const bool canBreak = run->canBreak() || (i + 1) == range.getEnd();
+            proc.feedChar(i, textBuf[i], measured.widths[i], canBreak);
 
             const uint32_t nextCharOffset = i + 1;
             if (nextCharOffset != proc.nextWordBreak) {
diff --git a/tests/unittest/OptimalLineBreakerTest.cpp b/tests/unittest/OptimalLineBreakerTest.cpp
index ef1f6a9..51aab60 100644
--- a/tests/unittest/OptimalLineBreakerTest.cpp
+++ b/tests/unittest/OptimalLineBreakerTest.cpp
@@ -1496,6 +1496,81 @@
     }
 }
 
+TEST_F(OptimalLineBreakerTest, testReplacementSpanNotBreakTest_continuedReplacementSpan) {
+    constexpr float CHAR_WIDTH = 10.0;
+
+    constexpr StartHyphenEdit NO_START_HYPHEN = StartHyphenEdit::NO_EDIT;
+    constexpr EndHyphenEdit NO_END_HYPHEN = EndHyphenEdit::NO_EDIT;
+
+    const auto textBuf = utf8ToUtf16("This is an example text.");
+
+    // In this test case, assign a replacement run for "is an " with 5 times of CHAR_WIDTH.
+    auto doLineBreak = [=](float width) {
+        MeasuredTextBuilder builder;
+        builder.addReplacementRun(0, 5, 5 * CHAR_WIDTH, LocaleListCache::getId("en-US"));
+        builder.addReplacementRun(5, 8, 3 * CHAR_WIDTH, LocaleListCache::getId("en-US"));
+        builder.addReplacementRun(8, 11, 3 * CHAR_WIDTH, LocaleListCache::getId("en-US"));
+        builder.addReplacementRun(11, 19, 8 * CHAR_WIDTH, LocaleListCache::getId("en-US"));
+        builder.addReplacementRun(19, 24, 5 * CHAR_WIDTH, LocaleListCache::getId("en-US"));
+        std::unique_ptr<MeasuredText> measuredText =
+                builder.build(textBuf, false /* compute hyphenation */,
+                              false /* compute full layout */, nullptr /* no hint */);
+        RectangleLineWidth rectangleLineWidth(width);
+        TabStops tabStops(nullptr, 0, 0);
+        return breakLineOptimal(textBuf, *measuredText, rectangleLineWidth,
+                                BreakStrategy::HighQuality, HyphenationFrequency::None,
+                                false /* justified */);
+    };
+
+    {
+        constexpr float LINE_WIDTH = 100;
+        // clang-format off
+        std::vector<LineBreakExpectation> expect = {
+                {"This ",    50, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"is an ",   60, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"example ", 80, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"text.",    50, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+        };
+        // clang-format on
+        const auto actual = doLineBreak(LINE_WIDTH);
+        EXPECT_TRUE(sameLineBreak(expect, actual)) << toString(expect) << std::endl
+                                                   << " vs " << std::endl
+                                                   << toString(textBuf, actual);
+    }
+    {
+        constexpr float LINE_WIDTH = 40;
+        // clang-format off
+        std::vector<LineBreakExpectation> expect = {
+                {"This ",    50, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"is ",      30, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"an ",      30, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"example ", 80, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"text.",    50, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+        };
+        // clang-format on
+        const auto actual = doLineBreak(LINE_WIDTH);
+        EXPECT_TRUE(sameLineBreak(expect, actual)) << toString(expect) << std::endl
+                                                   << " vs " << std::endl
+                                                   << toString(textBuf, actual);
+    }
+    {
+        constexpr float LINE_WIDTH = 10;
+        // clang-format off
+        std::vector<LineBreakExpectation> expect = {
+                {"This ",    50, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"is ",      30, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"an ",      30, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"example ", 80, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+                {"text.",    50, NO_START_HYPHEN, NO_END_HYPHEN, 0, 0},
+        };
+        // clang-format on
+        const auto actual = doLineBreak(LINE_WIDTH);
+        EXPECT_TRUE(sameLineBreak(expect, actual)) << toString(expect) << std::endl
+                                                   << " vs " << std::endl
+                                                   << toString(textBuf, actual);
+    }
+}
+
 TEST_F(OptimalLineBreakerTest, testReplacementSpanNotBreakTest_CJK) {
     constexpr float CHAR_WIDTH = 10.0;