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;