Respect grapheme clusters when wrapping text

Bug: skia:10075
Change-Id: I52fad5db0944e74c780c1dbfa0c8e6eb7fa42cf1
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/278468
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
diff --git a/modules/skparagraph/src/ParagraphImpl.cpp b/modules/skparagraph/src/ParagraphImpl.cpp
index 0598557..61f283e 100644
--- a/modules/skparagraph/src/ParagraphImpl.cpp
+++ b/modules/skparagraph/src/ParagraphImpl.cpp
@@ -271,6 +271,9 @@
                 auto& cluster = fClusters.emplace_back(this, runIndex, glyphStart, glyphEnd, text,
                                                        width, height);
                 cluster.setIsWhiteSpaces();
+                if (fGraphemes.find(cluster.fTextRange.end) != nullptr) {
+                    cluster.setBreakType(Cluster::BreakType::GraphemeBreak);
+                }
             });
         }
 
@@ -288,6 +291,8 @@
         return;
     }
 
+    // Mark all soft line breaks
+    // Remove soft line breaks that are not on grapheme cluster edge
     Cluster* current = fClusters.begin();
     while (!breaker.eof() && current < fClusters.end()) {
         size_t currentPos = breaker.next();
@@ -295,9 +300,15 @@
             if (current->textRange().end > currentPos) {
                 break;
             } else if (current->textRange().end == currentPos) {
-                current->setBreakType(breaker.status() == UBRK_LINE_HARD
-                                      ? Cluster::BreakType::HardLineBreak
-                                      : Cluster::BreakType::SoftLineBreak);
+                if (breaker.status() == UBRK_LINE_HARD) {
+                    // Hard line break stronger than anything
+                    current->setBreakType(Cluster::BreakType::HardLineBreak);
+                } else if (current->isGraphemeBreak()) {
+                    // Only allow soft line break if it's grapheme break
+                    current->setBreakType(Cluster::BreakType::SoftLineBreak);
+                } else {
+                    // Leave it as is (either it's no break or a placeholder)
+                }
                 ++current;
                 break;
             }
@@ -305,7 +316,6 @@
         }
     }
 
-
     // Walk through all the clusters in the direction of shaped text
     // (we have to walk through the styles in the same order, too)
     SkScalar shift = 0;
diff --git a/modules/skparagraph/src/ParagraphImpl.h b/modules/skparagraph/src/ParagraphImpl.h
index 51007cf..b692173 100644
--- a/modules/skparagraph/src/ParagraphImpl.h
+++ b/modules/skparagraph/src/ParagraphImpl.h
@@ -142,6 +142,7 @@
     const ParagraphStyle& paragraphStyle() const { return fParagraphStyle; }
     SkSpan<Cluster> clusters() { return SkSpan<Cluster>(fClusters.begin(), fClusters.size()); }
     sk_sp<FontCollection> fontCollection() const { return fFontCollection; }
+    const SkTHashSet<size_t>& graphemes() const { return fGraphemes; }
     void formatLines(SkScalar maxWidth);
 
     bool strutEnabled() const { return paragraphStyle().getStrutStyle().getStrutEnabled(); }
diff --git a/modules/skparagraph/src/Run.h b/modules/skparagraph/src/Run.h
index ce91c19d..341aac2 100644
--- a/modules/skparagraph/src/Run.h
+++ b/modules/skparagraph/src/Run.h
@@ -233,6 +233,7 @@
         WordBreakWithHyphen,
         SoftLineBreak,  // calculated for all clusters (UBRK_LINE)
         HardLineBreak,  // calculated for all clusters (UBRK_LINE)
+        GraphemeBreak,
     };
 
     Cluster()
@@ -279,6 +280,7 @@
     }
     bool isHardBreak() const { return fBreakType == HardLineBreak; }
     bool isSoftBreak() const { return fBreakType == SoftLineBreak; }
+    bool isGraphemeBreak() const { return fBreakType == GraphemeBreak; }
     size_t startPos() const { return fStart; }
     size_t endPos() const { return fEnd; }
     SkScalar width() const { return fWidth; }
diff --git a/samplecode/SampleParagraph.cpp b/samplecode/SampleParagraph.cpp
index fc04bb2..60d3494 100644
--- a/samplecode/SampleParagraph.cpp
+++ b/samplecode/SampleParagraph.cpp
@@ -2491,8 +2491,8 @@
     SkString name() override { return SkString("Paragraph37"); }
 
     void onDrawContent(SkCanvas* canvas) override {
-
-        const char* text = "ছোৈূোঌ";
+        const char* text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaয়ৠঝোণ৺ঢ়মৈবৗৗঘথফড়৭২খসঢ়ৃঢ়ঁ৷থডঈঽলবনদ২ৢৃঀজঝ৩ঠ৪৫৯০ঌয়্মওৗ৲গখদ৹ঈ৴৹ঢ়ৄএৡফণহলঈ৲থজোৱে ঀকৰঀষজঝঃাখশঽএমংি";
+                //"ৎৣ়ৎঽতঃ৳্ৱব৴ৣঈ৷ূঁঢঢ়শটডৎ৵৵ৰৃ্দংঊাথৗদঊউদ৯ঐৃধা৬হওধি়৭ঽম৯স০ঢফৈঢ়কষঁছফীআে৶ৰ৶ঌৌঊ্ঊঝএঀঃদঞ৮তব৬ৄঊঙঢ়ৡগ৶৹৹ঌড়ঘৄ৷লপ১ভড়৶েঢ়৯ৎকনংট২ংএঢৌৌঐনো০টঽুৠগআ৷৭৩৬তো৻ঈ০ূসষঅঝআমণঔা১ণৈো৵চঽ৩বমৎঙঘ২ঠৠৈী৫তঌণচ৲ঔী৮ঘৰঔ";
          canvas->drawColor(SK_ColorWHITE);
 
         auto fontCollection = sk_make_sp<FontCollection>();
@@ -2510,6 +2510,27 @@
         auto paragraph = builder.Build();
         auto w = width() / 2;
         paragraph->layout(w);
+        auto impl = static_cast<ParagraphImpl*>(paragraph.get());
+
+        auto clusters = impl->clusters();
+        size_t c = 0;
+        SkDebugf("clusters\n");
+        for (auto& cluster: clusters) {
+          SkDebugf(""
+                   "%d: [%d:%d) %s\n", c++,
+              cluster.textRange().start, cluster.textRange().end,
+              cluster.isSoftBreak() ? "soft" :
+                cluster.isHardBreak() ? "hard" :
+                  cluster.isWhitespaces() ? "spaces" : ""
+              );
+        }
+        auto lines = impl->lines();
+        size_t i = 0;
+        SkDebugf("lines\n");
+        for (auto& line : lines) {
+          SkDebugf("%d: [%d:%d)\n", i++, line.trimmedText().start, line.trimmedText().end);
+        }
+
         paragraph->paint(canvas, 0, 0);
     }
 
@@ -2517,7 +2538,6 @@
     typedef Sample INHERITED;
 };
 
-//"\U0001f469\u200D\U0001f469\u200D\U0001f466\U0001f469\u200D\U0001f469\u200D\U0001f467\u200D\U0001f467\U0001f1fa\U0001f1f8"
 //////////////////////////////////////////////////////////////////////////////
 DEF_SAMPLE(return new ParagraphView1();)
 DEF_SAMPLE(return new ParagraphView2();)