Round unresolved font blocks to grapheme edges

I had to stage the change with SK_PARAGRAPH_GRAPHEME_EDGES because of
some google3 tests.
Bug: skia:11196

Change-Id: Ic45e43e6c69f649c6bc0fedcc6f303891139b85e
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/359566
Commit-Queue: Julia Lavrova <jlavrova@google.com>
Reviewed-by: Ben Wagner <bungeman@google.com>
diff --git a/modules/skparagraph/samples/SampleParagraph.cpp b/modules/skparagraph/samples/SampleParagraph.cpp
index 9e7faeb..1a761a9 100644
--- a/modules/skparagraph/samples/SampleParagraph.cpp
+++ b/modules/skparagraph/samples/SampleParagraph.cpp
@@ -3268,6 +3268,116 @@
     using INHERITED = Sample;
 };
 
+class ParagraphView54 : public ParagraphView_Base {
+protected:
+    SkString name() override { return SkString("Paragraph54"); }
+
+    void onDrawContent(SkCanvas* canvas) override {
+        canvas->drawColor(SK_ColorWHITE);
+        //std::string text("يَهْدِيْكُمُ اللَّهُ وَيُصْلِحُ بَالَكُمُ");
+        //auto text = "ד👨‍👩‍👧‍👦😀";
+        auto text = "👨‍👩‍👧‍👦😀";
+
+        //auto fontCollection = sk_make_sp<FontCollection>();
+        auto fontCollection = getFontCollection();
+        fontCollection->setDefaultFontManager(SkFontMgr::RefDefault());
+        fontCollection->enableFontFallback();
+        //fontCollection->disableFontFallback();
+
+        ParagraphStyle paragraph_style;
+        //paragraph_style.setTextDirection(TextDirection::kRtl);
+
+        ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+        TextStyle text_style;
+        text_style.setFontFamilies({SkString("Noto Naskh Arabic")});
+        text_style.setFontSize(36);
+        text_style.setColor(SK_ColorBLACK);
+        builder.pushStyle(text_style);
+        builder.addText(text);
+
+        auto paragraph = builder.Build();
+        paragraph->layout(/*360*/width());
+        paragraph->paint(canvas, 0, 0);
+    }
+
+private:
+    using INHERITED = Sample;
+};
+
+class ParagraphView55 : public ParagraphView_Base {
+protected:
+    SkString name() override { return SkString("Paragraph55"); }
+
+    void onDrawContent(SkCanvas* canvas) override {
+        canvas->drawColor(SK_ColorWHITE);
+        std::string text("يَهْدِيْكُمُ اللَّهُ وَيُصْلِحُ بَالَكُمُ");
+
+        //auto fontCollection = sk_make_sp<FontCollection>();
+        //fontCollection->setDefaultFontManager(SkFontMgr::RefDefault());
+        //fontCollection->enableFontFallback();
+        auto fontCollection = getFontCollection();
+        fontCollection->disableFontFallback();
+
+        ParagraphStyle paragraph_style;
+        paragraph_style.setTextDirection(TextDirection::kRtl);
+
+        ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+        TextStyle text_style;
+        text_style.setFontFamilies({SkString("Noto Naskh Arabic")});
+        text_style.setFontSize(64);
+        text_style.setColor(SK_ColorBLACK);
+        builder.pushStyle(text_style);
+        builder.addText(text.substr(0, 10).data());
+        text_style.setColor(SK_ColorRED);
+        builder.pushStyle(text_style);
+        builder.addText(text.substr(10, 20).data());
+        text_style.setColor(SK_ColorBLACK);
+        builder.pushStyle(text_style);
+        builder.addText(text.substr(30, 50).data());
+
+        auto paragraph = builder.Build();
+        paragraph->layout(/*360*/width());
+        paragraph->paint(canvas, 0, 0);
+    }
+
+private:
+    using INHERITED = Sample;
+};
+
+class ParagraphView56 : public ParagraphView_Base {
+protected:
+    SkString name() override { return SkString("Paragraph56"); }
+
+    void onDrawContent(SkCanvas* canvas) override {
+        canvas->drawColor(SK_ColorWHITE);
+        auto text = "BAM BAM BAM by Jade Baraldo\n"
+                    "Now on Top 100 Music Videos United States";
+
+        auto fontCollection = sk_make_sp<TestFontCollection>(GetResourcePath("fonts").c_str(), false);
+        fontCollection->addFontFromFile("music/Roboto-Regular.ttf", "roboto");
+        fontCollection->addFontFromFile("music/NotoSansCJK-Regular.ttc", "noto");
+        fontCollection->addFontFromFile("music/NotoColorEmoji.ttf", "emoji");
+
+        ParagraphStyle paragraph_style;
+        ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+        TextStyle text_style;
+        //text_style.setFontFamilies({SkString("Noto Naskh Arabic")});
+        text_style.setFontFamilies({SkString("roboto"),
+                                    SkString("noto"),
+                                    SkString("emoji")});
+        text_style.setFontSize(20);
+        text_style.setColor(SK_ColorBLACK);
+        builder.pushStyle(text_style);
+        builder.addText(text);
+        auto paragraph = builder.Build();
+        paragraph->layout(width());
+        paragraph->paint(canvas, 0, 0);
+    }
+
+private:
+    using INHERITED = Sample;
+};
+
 }  // namespace
 
 //////////////////////////////////////////////////////////////////////////////
@@ -3322,3 +3432,6 @@
 DEF_SAMPLE(return new ParagraphView51();)
 DEF_SAMPLE(return new ParagraphView52();)
 DEF_SAMPLE(return new ParagraphView53();)
+DEF_SAMPLE(return new ParagraphView54();)
+DEF_SAMPLE(return new ParagraphView55();)
+DEF_SAMPLE(return new ParagraphView56();)
diff --git a/modules/skparagraph/src/OneLineShaper.cpp b/modules/skparagraph/src/OneLineShaper.cpp
index 3beb5dc..d1707b8 100644
--- a/modules/skparagraph/src/OneLineShaper.cpp
+++ b/modules/skparagraph/src/OneLineShaper.cpp
@@ -19,7 +19,12 @@
     fCurrentRun->commit();
 
     auto oldUnresolvedCount = fUnresolvedBlocks.size();
-
+/*
+    SkDebugf("Run [%d:%d)\n", fCurrentRun->fTextRange.start, fCurrentRun->fTextRange.end);
+    for (size_t i = 0; i < fCurrentRun->size(); ++i) {
+        SkDebugf("[%d] %d %d %f\n", i, fCurrentRun->fGlyphs[i], fCurrentRun->fClusterIndexes[i], fCurrentRun->fPositions[i].fX);
+    }
+*/
     // Find all unresolved blocks
     sortOutGlyphs([&](GlyphRange block){
         if (block.width() == 0) {
@@ -291,6 +296,60 @@
 
 // Glue whitespaces to the next/prev unresolved blocks
 // (so we don't have chinese text with english whitespaces broken into millions of tiny runs)
+#ifndef SK_PARAGRAPH_GRAPHEME_EDGES
+void OneLineShaper::sortOutGlyphs(std::function<void(GlyphRange)>&& sortOutUnresolvedBLock) {
+
+    size_t unresolvedGlyphs = 0;
+
+    GlyphRange block = EMPTY_RANGE;
+    bool graphemeResolved = false;
+    TextIndex graphemeStart = EMPTY_INDEX;
+    for (size_t i = 0; i < fCurrentRun->size(); ++i) {
+
+        ClusterIndex ci = clusterIndex(i);
+        // Removing all pretty optimizations for whitespaces
+        // because they get in a way of grapheme rounding
+        // Inspect the glyph
+        auto glyph = fCurrentRun->fGlyphs[i];
+
+        GraphemeIndex gi = fParagraph->findPreviousGraphemeBoundary(ci);
+        if ((fCurrentRun->leftToRight() ? gi > graphemeStart : gi < graphemeStart) || graphemeStart == EMPTY_INDEX) {
+            // We only count glyph resolved if all the glyphs in its grapheme are resolved
+            graphemeResolved = glyph != 0;
+            graphemeStart = gi;
+        } else if (glyph == 0) {
+            // Found unresolved glyph - the entire grapheme is unresolved now
+            graphemeResolved = false;
+        }
+
+        if (!graphemeResolved) { // Unresolved glyph and not control codepoint
+            ++unresolvedGlyphs;
+            if (block.start == EMPTY_INDEX) {
+                // Start new unresolved block
+                block.start = i;
+                block.end = EMPTY_INDEX;
+            } else {
+                // Keep skipping unresolved block
+            }
+        } else { // Resolved glyph or control codepoint
+            if (block.start == EMPTY_INDEX) {
+                // Keep skipping resolved code points
+            } else {
+                // This is the end of unresolved block
+                block.end = i;
+                sortOutUnresolvedBLock(block);
+                block = EMPTY_RANGE;
+            }
+        }
+    }
+
+    // One last block could have been left
+    if (block.start != EMPTY_INDEX) {
+        block.end = fCurrentRun->size();
+        sortOutUnresolvedBLock(block);
+    }
+}
+#else
 void OneLineShaper::sortOutGlyphs(std::function<void(GlyphRange)>&& sortOutUnresolvedBLock) {
 
     auto text = fCurrentRun->fOwner->text();
@@ -353,6 +412,7 @@
         sortOutUnresolvedBLock(block);
     }
 }
+#endif
 
 void OneLineShaper::iterateThroughFontStyles(TextRange textRange,
                                              SkSpan<Block> styleSpan,
diff --git a/modules/skparagraph/tests/SkParagraphTest.cpp b/modules/skparagraph/tests/SkParagraphTest.cpp
index b6b5a9c..154a22e 100644
--- a/modules/skparagraph/tests/SkParagraphTest.cpp
+++ b/modules/skparagraph/tests/SkParagraphTest.cpp
@@ -45,7 +45,6 @@
 
 struct GrContextOptions;
 
-
 #define VeryLongCanvasWidth 1000000
 #define TestCanvasWidth 1000
 #define TestCanvasHeight 600
@@ -1239,11 +1238,7 @@
     paragraph->layout(550);
 
     auto impl = static_cast<ParagraphImpl*>(paragraph.get());
-#ifdef SK_PARAGRAPH_LIBTXT_SPACES_RESOLUTION
     REPORTER_ASSERT(reporter, impl->runs().size() == 5);
-#else
-    REPORTER_ASSERT(reporter, impl->runs().size() == 4);
-#endif
     REPORTER_ASSERT(reporter, impl->styles().size() == 1);  // paragraph style does not count
     REPORTER_ASSERT(reporter, impl->styles()[0].fStyle.equals(text_style));
 
@@ -3990,13 +3985,8 @@
     paragraph->layout(TestCanvasWidth);
     paragraph->paint(canvas.get(), 10.0, 15.0);
 
-#ifdef SK_PARAGRAPH_LIBTXT_SPACES_RESOLUTION
     size_t spaceRun = 1;
     REPORTER_ASSERT(reporter, paragraph->unresolvedGlyphs() == 2); // From the text1 ("字典" - excluding the last space)
-#else
-    size_t spaceRun = 0;
-    REPORTER_ASSERT(reporter, paragraph->unresolvedGlyphs() == 3); // From the text1 ("字典 " - including the last space)
-#endif
 
     auto impl = static_cast<ParagraphImpl*>(paragraph.get());
 
@@ -4009,9 +3999,7 @@
     // [Apple + Han] 5, 6
     auto robotoAdvance = impl->runs()[0].advance().fX +
                          impl->runs()[1].advance().fX;
-#ifdef SK_PARAGRAPH_LIBTXT_SPACES_RESOLUTION
     robotoAdvance += impl->runs()[2].advance().fX;
-#endif
 
     REPORTER_ASSERT(reporter, SkScalarNearlyEqual(robotoAdvance, 64.199f, EPSILON50));
     REPORTER_ASSERT(reporter, SkScalarNearlyEqual(impl->runs()[2 + spaceRun].advance().fX, 139.125f, EPSILON100));
@@ -5503,11 +5491,7 @@
     paragraph->paint(canvas.get(), 0, 0);
 
     auto impl = static_cast<ParagraphImpl*>(paragraph.get());
-#ifdef SK_PARAGRAPH_LIBTXT_SPACES_RESOLUTION
     REPORTER_ASSERT(reporter, impl->runs().size() == (10 + 11));
-#else
-    REPORTER_ASSERT(reporter, impl->runs().size() == 1);
-#endif
 }
 
 DEF_TEST(SkParagraph_FontResolutionInLTR, reporter) {
@@ -5534,20 +5518,12 @@
     paragraph->paint(canvas.get(), 0, 0);
 
     auto impl = static_cast<ParagraphImpl*>(paragraph.get());
-#ifdef SK_PARAGRAPH_LIBTXT_SPACES_RESOLUTION
     REPORTER_ASSERT(reporter, impl->runs().size() == 5);
     REPORTER_ASSERT(reporter, impl->runs()[0].textRange().width() == 4); // "abc "
     REPORTER_ASSERT(reporter, impl->runs()[1].textRange().width() == 2); // "{unresolved}"
     REPORTER_ASSERT(reporter, impl->runs()[2].textRange().width() == 1); // " "
     REPORTER_ASSERT(reporter, impl->runs()[3].textRange().width() == 2); // "{unresolved}"
     REPORTER_ASSERT(reporter, impl->runs()[4].textRange().width() == 4); // " def"
-#else
-    REPORTER_ASSERT(reporter, impl->runs().size() == 3);
-    REPORTER_ASSERT(reporter, impl->runs()[0].textRange().width() == 4); // "abc "
-    REPORTER_ASSERT(reporter, impl->runs()[1].textRange().width() == 5); // "{unresolved} {unresolved}"
-    REPORTER_ASSERT(reporter, impl->runs()[2].textRange().width() == 4); // " def"
-
-#endif
 }
 
 DEF_TEST(SkParagraph_Intrinsic, reporter) {
@@ -5818,3 +5794,5 @@
     REPORTER_ASSERT(reporter, SkScalarNearlyEqual(len1, 300.0f));
     REPORTER_ASSERT(reporter, SkScalarNearlyEqual(len2, 250.0f));
 }
+
+// "\u05D3\u{1F468}\u200D\u{1F469}\u200D\u{1F467}\u200D\u{1F466}\uD83D\uDE00"
diff --git a/public.bzl b/public.bzl
index d8421a9..d33a9bc 100644
--- a/public.bzl
+++ b/public.bzl
@@ -723,6 +723,7 @@
         # Google3 probably doesn't want this feature yet
         "SK_DISABLE_REDUCE_OPLIST_SPLITTING",
         # Staging flags for API changes
+        "SK_PARAGRAPH_GRAPHEME_EDGES",
         # Should remove after we update golden images
         "SK_WEBP_ENCODER_USE_DEFAULT_METHOD",
         # Experiment to diagnose image diffs in Google3