Bugs

Mainly, simplified iteration over visual run for performance reasons.
Check for locale when comparing fonts.
Try to resolve ALL unresolved codepoints.

Change-Id: Ic126ca9bcb3970e2cbd6da9c384c493f9fd81b0d
Bug: skia:9956, skia:9970, skia:9951
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/273463
Commit-Queue: Julia Lavrova <jlavrova@google.com>
Reviewed-by: Ben Wagner <bungeman@google.com>
diff --git a/modules/skparagraph/include/TextStyle.h b/modules/skparagraph/include/TextStyle.h
index 08509d7..d24b644 100644
--- a/modules/skparagraph/include/TextStyle.h
+++ b/modules/skparagraph/include/TextStyle.h
@@ -51,6 +51,7 @@
 enum TextDecorationStyle { kSolid, kDouble, kDotted, kDashed, kWavy };
 
 enum StyleType {
+    kNone,
     kAllAttributes,
     kFont,
     kForeground,
diff --git a/modules/skparagraph/src/OneLineShaper.cpp b/modules/skparagraph/src/OneLineShaper.cpp
index 5ba6e8a..48c6e91 100644
--- a/modules/skparagraph/src/OneLineShaper.cpp
+++ b/modules/skparagraph/src/OneLineShaper.cpp
@@ -4,6 +4,7 @@
 #include "modules/skparagraph/src/OneLineShaper.h"
 #include <unicode/uchar.h>
 #include <algorithm>
+#include <unordered_set>
 #include "src/utils/SkUTF.h"
 
 namespace skia {
@@ -262,7 +263,7 @@
     RunBlock unresolved(fCurrentRun, clusteredText(glyphRange), glyphRange, 0);
     if (unresolved.fGlyphs.width() == fCurrentRun->size()) {
         SkASSERT(unresolved.fText.width() == fCurrentRun->fTextRange.width());
-    } else if (!fUnresolvedBlocks.empty()) {
+    } else if (fUnresolvedBlocks.size() > 1) {
         auto& lastUnresolved = fUnresolvedBlocks.back();
         if (lastUnresolved.fRun != nullptr &&
             lastUnresolved.fRun->fIndex == fCurrentRun->fIndex) {
@@ -407,25 +408,44 @@
             auto unresolvedRange = fUnresolvedBlocks.front().fText;
             auto unresolvedText = fParagraph->text(unresolvedRange);
             const char* ch = unresolvedText.begin();
+            // TODO: Make in a global cache for all fallback fonts
+            std::unordered_set<SkUnichar> tried;
             SkUnichar unicode = utf8_next(&ch, unresolvedText.end());
+            while (true) {
+                auto typeface = fParagraph->fFontCollection->defaultFallback(
+                        unicode, textStyle.getFontStyle(), textStyle.getLocale());
 
-            auto typeface = fParagraph->fFontCollection->defaultFallback(
-                    unicode, textStyle.getFontStyle(), textStyle.getLocale());
-
-            if (typeface == nullptr) {
-                return;
-            }
-
-            if (!visitor(typeface)) {
-                // Resolved everything
-                return;
-            } else {
-                // Check if anything was resolved and stop it it was not
-                auto last = fUnresolvedBlocks.back();
-                if (unresolvedRange == last.fText) {
+                if (typeface == nullptr) {
                     return;
                 }
+
+                if (!visitor(typeface)) {
+                    // Resolved everything
+                    return;
+                }
+
+                // Check if anything was resolved and stop it it was not
+                auto last = fUnresolvedBlocks.back();
+                if (!(unresolvedRange == last.fText)) {
+                    // Resolved something, no need to repeat
+                    break;
+                }
+
+                if (ch == unresolvedText.end()) {
+                    // Not a single codepoint could be resolved but we can switch to another block
+                    break;
+                }
+
+                // We can stop here or we can switch to another DIFFERENT codepoint
+                while (ch != unresolvedText.end()) {
+                    unicode = utf8_next(&ch, unresolvedText.end());
+                    if (tried.find(unicode) == tried.end()) {
+                        tried.emplace(unicode);
+                        break;
+                    }
+                }
             }
+
         }
     }
 }
diff --git a/modules/skparagraph/src/ParagraphImpl.cpp b/modules/skparagraph/src/ParagraphImpl.cpp
index b1328a4..066fd35 100644
--- a/modules/skparagraph/src/ParagraphImpl.cpp
+++ b/modules/skparagraph/src/ParagraphImpl.cpp
@@ -9,8 +9,9 @@
 #include "modules/skparagraph/src/TextWrapper.h"
 #include "src/core/SkSpan.h"
 #include "src/utils/SkUTF.h"
-#include <algorithm>
 #include <unicode/ustring.h>
+#include <algorithm>
+#include <chrono>
 #include <queue>
 
 namespace skia {
@@ -138,12 +139,10 @@
     }
 
     if (fState < kShaped) {
-
         fGraphemes.reset();
         this->markGraphemes();
 
         if (!this->shapeTextIntoEndlessLine()) {
-
             this->resetContext();
             // TODO: merge the two next calls - they always come together
             this->resolveStrut();
@@ -180,7 +179,7 @@
         fState = kMarked;
     }
 
-    if (fState >= kLineBroken)  {
+    if (fState >= kLineBroken) {
         if (fOldWidth != floorWidth || fOldHeight != fHeight) {
             fState = kMarked;
         }
@@ -193,7 +192,6 @@
         this->fLines.reset();
         this->breakShapedTextIntoLines(floorWidth);
         fState = kLineBroken;
-
     }
 
     if (fState < kFormatted) {
@@ -212,7 +210,8 @@
     fMaxIntrinsicWidth = littleRound(fMaxIntrinsicWidth);
 
     // TODO: This is strictly Flutter thing. Must be factored out into some flutter code
-    if (fParagraphStyle.getMaxLines() == 1 || (fParagraphStyle.unlimited_lines() && fParagraphStyle.ellipsized())) {
+    if (fParagraphStyle.getMaxLines() == 1 ||
+        (fParagraphStyle.unlimited_lines() && fParagraphStyle.ellipsized())) {
         fMinIntrinsicWidth = fMaxIntrinsicWidth;
     }
 }
@@ -605,6 +604,7 @@
     if (fText.isEmpty()) {
         if (start == 0 && end > 0) {
             // On account of implied "\n" that is always at the end of the text
+            //SkDebugf("getRectsForRange(%d, %d): %f\n", start, end, fHeight);
             results.emplace_back(SkRect::MakeXYWH(0, 0, 0, fHeight), fParagraphStyle.getTextDirection());
         }
         return results;
@@ -649,6 +649,9 @@
         }
     }
 
+    auto firstBoxOnTheLine = results.size();
+    const Run* lastRun = nullptr;
+    auto paragraphTextDirection = paragraphStyle().getTextDirection();
     for (auto& line : fLines) {
         auto lineText = line.textWithSpaces();
         auto intersect = lineText * text;
@@ -656,42 +659,23 @@
             continue;
         }
 
-        // Found a line that intersects with the text
-        auto firstBoxOnTheLine = results.size();
-        auto paragraphTextDirection = paragraphStyle().getTextDirection();
-        const Run* lastRun = nullptr;
         line.iterateThroughVisualRuns(true,
-            [&](const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) {
+            [&]
+            (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
+            *runWidthInLine = line.iterateThroughSingleRunByStyles(
+            run, runOffsetInLine, textRange, StyleType::kNone,
+            [&]
+            (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context0) {
 
                 auto intersect = textRange * text;
-                if (intersect.empty() || textRange.empty()) {
-                    auto context = line.measureTextInsideOneRun(textRange, run, runOffset, 0, true, false);
-                    *width = context.clip.width();
-                    if (textRange.width() > 0) {
-                        return true;
-                    } else {
-                        intersect = textRange;
-                    }
-                } else {
-                    TextRange head;
-                    if (run->leftToRight() && textRange.start != intersect.start) {
-                        head = TextRange(textRange.start, intersect.start);
-                        *width = line.measureTextInsideOneRun(head, run, runOffset, 0, true, false).clip.width();
-                    } else if (!run->leftToRight() && textRange.end != intersect.end) {
-                        head = TextRange(intersect.end, textRange.end);
-                        *width = line.measureTextInsideOneRun(head, run, runOffset, 0, true, false).clip.width();
-                    } else {
-                        *width = 0;
-                    }
+                if (intersect.empty()) {
+                    return true;
                 }
 
-                auto runInLineWidth = line.measureTextInsideOneRun(textRange, run, runOffset, 0, true, false).clip.width();
-                runOffset += *width;
-                *width = runInLineWidth;
-
                 // Found a run that intersects with the text
-                auto context = line.measureTextInsideOneRun(intersect, run, runOffset, 0, true, true);
+                auto context = line.measureTextInsideOneRun(intersect, run, runOffsetInLine, 0, true, true);
                 SkRect clip = context.clip;
+                clip.offset(context0.fTextShift - context.fTextShift, 0);
 
                 if (rectHeightStyle == RectHeightStyle::kMax) {
                     // TODO: Change it once flutter rolls into google3
@@ -798,9 +782,10 @@
                 if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) {
                     results.emplace_back(trailingSpaces, paragraphTextDirection);
                 }
-
                 return true;
             });
+            return true;
+        });
 
         if (rectWidthStyle == RectWidthStyle::kMax) {
             // Align the very left/right box horizontally
@@ -822,14 +807,12 @@
         }
 
         for (auto& r : results) {
-
-          r.rect.fLeft = littleRound(r.rect.fLeft);
-          r.rect.fRight = littleRound(r.rect.fRight);
-          r.rect.fTop = littleRound(r.rect.fTop);
-          r.rect.fBottom = littleRound(r.rect.fBottom);
+            r.rect.fLeft = littleRound(r.rect.fLeft);
+            r.rect.fRight = littleRound(r.rect.fRight);
+            r.rect.fTop = littleRound(r.rect.fTop);
+            r.rect.fBottom = littleRound(r.rect.fBottom);
         }
     }
-
     return results;
 }
 
@@ -889,97 +872,99 @@
         // This is so far the the line vertically closest to our coordinates
         // (or the first one, or the only one - all the same)
         line.iterateThroughVisualRuns(true,
-            [this, &line, dx, &result]
-            (const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) {
+                [this, &line, dx, &result]
+                (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
+                *runWidthInLine = line.iterateThroughSingleRunByStyles(
+                run, runOffsetInLine, textRange, StyleType::kNone,
+                [this, line, dx, &result]
+                (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context) {
 
-                auto findCodepointByTextIndex = [this](ClusterIndex clusterIndex8) {
-                    auto codepoint = std::lower_bound(
-                        fCodePoints.begin(), fCodePoints.end(),
-                        clusterIndex8,
-                        [](const Codepoint& lhs,size_t rhs) -> bool { return lhs.fTextIndex < rhs; });
+                    auto findCodepointByTextIndex = [this](ClusterIndex clusterIndex8) {
+                        auto codepoint = std::lower_bound(
+                            fCodePoints.begin(), fCodePoints.end(),
+                            clusterIndex8,
+                            [](const Codepoint& lhs,size_t rhs) -> bool { return lhs.fTextIndex < rhs; });
 
-                    return codepoint - fCodePoints.begin();
-                };
+                        return codepoint - fCodePoints.begin();
+                    };
 
-                auto offsetX = line.offset().fX;
-                auto context = line.measureTextInsideOneRun(textRange, run, runOffset, 0, true, false);
-                *width = context.clip.width();
-                if (dx < context.clip.fLeft + offsetX) {
-                    // All the other runs are placed right of this one
-                    auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos));
-                    result = { SkToS32(codepointIndex), kDownstream };
+                    auto offsetX = line.offset().fX;
+                    if (dx < context.clip.fLeft + offsetX) {
+                        // All the other runs are placed right of this one
+                        auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos));
+                        result = { SkToS32(codepointIndex), kDownstream };
+                        return false;
+                    }
+
+                    if (dx >= context.clip.fRight + offsetX) {
+                        // We have to keep looking but just in case keep the last one as the closes
+                        // so far
+                        auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos + context.size));
+                        result = { SkToS32(codepointIndex), kUpstream };
+                        return true;
+                    }
+
+                    // So we found the run that contains our coordinates
+                    // Find the glyph position in the run that is the closest left of our point
+                    // TODO: binary search
+                    size_t found = context.pos;
+                    for (size_t i = context.pos; i < context.pos + context.size; ++i) {
+                        // TODO: this rounding is done to match Flutter tests. Must be removed..
+                        auto index = context.run->leftToRight() ? i : context.size - i;
+                        auto end = littleRound(context.run->positionX(index) + context.fTextShift + offsetX);
+                        if ((context.run->leftToRight() ? end > dx : dx > end)) {
+                            break;
+                        }
+                        found = index;
+                    }
+
+                    if (!context.run->leftToRight()) {
+                        --found;
+                    }
+
+                    auto glyphStart = context.run->positionX(found) + context.fTextShift + offsetX;
+                    auto glyphWidth = context.run->positionX(found + 1) - context.run->positionX(found);
+                    auto clusterIndex8 = context.run->globalClusterIndex(found);
+                    auto clusterEnd8 = context.run->globalClusterIndex(found + 1);
+                    TextRange clusterText (clusterIndex8, clusterEnd8);
+
+                    // Find the grapheme positions in codepoints that contains the point
+                    auto codepointIndex = findCodepointByTextIndex(clusterIndex8);
+                    CodepointRange codepoints(codepointIndex, codepointIndex);
+                    for (codepoints.end = codepointIndex + 1; codepoints.end < fCodePoints.size(); ++codepoints.end) {
+                        auto& cp = fCodePoints[codepoints.end];
+                        if (cp.fTextIndex >= clusterText.end) {
+                            break;
+                        }
+                    }
+                    auto graphemeSize = codepoints.width();
+
+                    // We only need to inspect one glyph (maybe not even the entire glyph)
+                    SkScalar center;
+                    bool insideGlyph = false;
+                    if (graphemeSize > 1) {
+                        auto averageCodepointWidth = glyphWidth / graphemeSize;
+                        auto delta = dx - glyphStart;
+                        auto insideIndex = SkScalarFloorToInt(delta / averageCodepointWidth);
+                        insideGlyph = delta > averageCodepointWidth;
+                        center = glyphStart + averageCodepointWidth * insideIndex + averageCodepointWidth / 2;
+                        codepointIndex += insideIndex;
+                    } else {
+                        center = glyphStart + glyphWidth / 2;
+                    }
+                    if ((dx < center) == context.run->leftToRight() || insideGlyph) {
+                        result = { SkToS32(codepointIndex), kDownstream };
+                    } else {
+                        result = { SkToS32(codepointIndex + 1), kUpstream };
+                    }
+                    // No need to continue
                     return false;
-                }
 
-                if (dx >= context.clip.fRight + offsetX) {
-                    // We have to keep looking but just in case keep the last one as the closes
-                    // so far
-                    auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos + context.size));
-                    result = { SkToS32(codepointIndex), kUpstream };
-                    return true;
-                }
-
-                // So we found the run that contains our coordinates
-                // Find the glyph position in the run that is the closest left of our point
-                // TODO: binary search
-                size_t found = context.pos;
-                for (size_t i = context.pos; i < context.pos + context.size; ++i) {
-                    // TODO: this rounding is done to match Flutter tests. Must be removed..
-                    auto index = context.run->leftToRight() ? i : context.size - i;
-                    auto end = littleRound(context.run->positionX(index) + context.fTextShift + offsetX);
-                    if ((context.run->leftToRight() ? end > dx : dx > end)) {
-                        break;
-                    }
-                    found = index;
-                }
-
-                if (!context.run->leftToRight()) {
-                    --found;
-                }
-
-                auto glyphStart = context.run->positionX(found) + context.fTextShift + offsetX;
-                auto glyphWidth = context.run->positionX(found + 1) - context.run->positionX(found);
-                auto clusterIndex8 = context.run->globalClusterIndex(found);
-                auto clusterEnd8 = context.run->globalClusterIndex(found + 1);
-                TextRange clusterText (clusterIndex8, clusterEnd8);
-
-                // Find the grapheme positions in codepoints that contains the point
-                auto codepointIndex = findCodepointByTextIndex(clusterIndex8);
-                CodepointRange codepoints(codepointIndex, codepointIndex);
-                for (codepoints.end = codepointIndex + 1; codepoints.end < fCodePoints.size(); ++codepoints.end) {
-                    auto& cp = fCodePoints[codepoints.end];
-                    if (cp.fTextIndex >= clusterText.end) {
-                        break;
-                    }
-                }
-                auto graphemeSize = codepoints.width();
-
-                // We only need to inspect one glyph (maybe not even the entire glyph)
-                SkScalar center;
-                bool insideGlyph = false;
-                if (graphemeSize > 1) {
-                    auto averageCodepointWidth = glyphWidth / graphemeSize;
-                    auto delta = dx - glyphStart;
-                    auto insideIndex = SkScalarFloorToInt(delta / averageCodepointWidth);
-                    insideGlyph = delta > averageCodepointWidth;
-                    center = glyphStart + averageCodepointWidth * insideIndex + averageCodepointWidth / 2;
-                    codepointIndex += insideIndex;
-                } else {
-                    center = glyphStart + glyphWidth / 2;
-                }
-                if ((dx < center) == context.run->leftToRight() || insideGlyph) {
-                    result = { SkToS32(codepointIndex), kDownstream };
-                } else {
-                    result = { SkToS32(codepointIndex + 1), kUpstream };
-                }
-                // No need to continue
-                return false;
+                });
+                return true;
             });
-
         break;
     }
-
-    //SkDebugf("getGlyphPositionAtCoordinate(%f,%f) = %d\n", dx, dy, result.position);
     return result;
 }
 
diff --git a/modules/skparagraph/src/TextLine.cpp b/modules/skparagraph/src/TextLine.cpp
index 5924c92..0708b59 100644
--- a/modules/skparagraph/src/TextLine.cpp
+++ b/modules/skparagraph/src/TextLine.cpp
@@ -793,6 +793,16 @@
         SkASSERT(false);
     }
 
+    if (styleType == StyleType::kNone) {
+        ClipContext clipContext = this->measureTextInsideOneRun(textRange, run, runOffset, 0, false, false);
+        if (clipContext.clip.height() > 0) {
+            visitor(textRange, TextStyle(), clipContext);
+            return clipContext.clip.width();
+        } else {
+            return 0;
+        }
+    }
+
     TextIndex start = EMPTY_INDEX;
     size_t size = 0;
     const TextStyle* prevStyle = nullptr;
diff --git a/modules/skparagraph/src/TextStyle.cpp b/modules/skparagraph/src/TextStyle.cpp
index d4ce181..c211ab9 100644
--- a/modules/skparagraph/src/TextStyle.cpp
+++ b/modules/skparagraph/src/TextStyle.cpp
@@ -153,8 +153,11 @@
 
         case kFont:
             // TODO: should not we take typefaces in account?
-            return fFontStyle == other.fFontStyle && fFontFamilies == other.fFontFamilies &&
-                   fFontSize == other.fFontSize && fHeight == other.fHeight;
+            return fFontStyle == other.fFontStyle &&
+                   fLocale == other.fLocale &&
+                   fFontFamilies == other.fFontFamilies &&
+                   fFontSize == other.fFontSize &&
+                   fHeight == other.fHeight;
         default:
             SkASSERT(false);
             return false;
diff --git a/samplecode/SampleParagraph.cpp b/samplecode/SampleParagraph.cpp
index a146872..5dee999 100644
--- a/samplecode/SampleParagraph.cpp
+++ b/samplecode/SampleParagraph.cpp
@@ -2189,7 +2189,7 @@
 
     void onDrawContent(SkCanvas* canvas) override {
 
-        const char* text = "去了 qw 其 er 他的 事实 证明自己";
+        auto text = u"\U0001f600\U0001f601\U0001f602\U0001f923\U0001f603\U0001f604\U0001f605\U0001f606\U0001f609\U0001f60a\U0001f60b\U0001f60e\U0001f60d\U0001f618\U0001f970\U0001f617\U0001f619\U0001f61a\u263A\uFE0F\U0001f642\U0001f917";
         canvas->drawColor(SK_ColorWHITE);
 
         auto fontCollection = sk_make_sp<FontCollection>();
@@ -2206,29 +2206,26 @@
         builder.addText(text);
         auto paragraph = builder.Build();
         paragraph->layout(width());
-        paragraph->paint(canvas, 0, 0);
 
-        for (size_t i = 0; i < strlen(text) - 1; ++i) {
 
+        SkColor colors[] = {
+            SK_ColorRED,
+            SK_ColorGREEN,
+            SK_ColorBLUE,
+            SK_ColorMAGENTA,
+            SK_ColorYELLOW
+        };
+        SkPaint paint;
+        for (size_t i = 0; i < 42; ++i) {
             auto result = paragraph->getRectsForRange(i, i + 1, RectHeightStyle::kTight, RectWidthStyle::kTight);
-            SkDebugf("[%d:%d):\n", i, i+1);
-            for (auto& r : result) {
-                SkDebugf("rect: [%f:%f] %s\n",
-                r.rect.fLeft, r.rect.fRight, r.direction == TextDirection::kRtl ? "rtl" : "ltr");
-
-                auto pos = paragraph->getGlyphPositionAtCoordinate(
-                        r.rect.fLeft + r.rect.width() / 2, r.rect.fTop + r.rect.height());
-                SkDebugf("pos: %d(%s)\n",
-                        pos.position, pos.affinity == Affinity::kUpstream ? "up" : "down");
-
-                auto word = paragraph->getWordBoundary(pos.position);
-                SkDebugf("word: [%d:%d)\n", word.start, word.end);
-
-                if (i < word.start || i >= word.end) {
-                    SkDebugf("Wrong!\n");
-                }
+            if (result.empty()) {
+                continue;
             }
+            auto rect = result[0].rect;
+            paint.setColor(colors[i % 5]);
+            canvas->drawRect(rect, paint);
         }
+        paragraph->paint(canvas, 0, 0);
     }
 
 private:
@@ -2241,7 +2238,6 @@
 
     void onDrawContent(SkCanvas* canvas) override {
 
-        auto text = u"A\u05D0";
         canvas->drawColor(SK_ColorWHITE);
 
         auto fontCollection = sk_make_sp<FontCollection>();
@@ -2253,23 +2249,97 @@
         TextStyle text_style;
         text_style.setColor(SK_ColorBLACK);
         text_style.setFontFamilies({SkString("Roboto")});
-        text_style.setFontSize(60);
+        text_style.setFontSize(40);
         builder.pushStyle(text_style);
-        builder.addText(text);
+        auto s = u"েن েূথ";
+        builder.addText(s);
         auto paragraph = builder.Build();
         paragraph->layout(width());
         paragraph->paint(canvas, 0, 0);
-
-        for (auto i = 0; i < 3; ++i) {
-            auto word = paragraph->getWordBoundary(i);
-            SkDebugf("word @%d: [%d:%d)\n", i, word.start, word.end);
-        }
     }
 
 private:
     typedef Sample INHERITED;
 };
 
+class ParagraphView32 : public ParagraphView_Base {
+protected:
+    SkString name() override { return SkString("Paragraph32"); }
+
+    void onDrawContent(SkCanvas* canvas) override {
+
+        canvas->drawColor(SK_ColorWHITE);
+
+        auto fontCollection = sk_make_sp<FontCollection>();
+        fontCollection->setDefaultFontManager(SkFontMgr::RefDefault());
+        fontCollection->enableFontFallback();
+
+        ParagraphStyle paragraph_style;
+        ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+        TextStyle text_style;
+        text_style.setColor(SK_ColorBLACK);
+        text_style.setFontFamilies({SkString("Roboto")});
+        text_style.setFontSize(40);
+        text_style.setLocale(SkString("ko"));
+        builder.pushStyle(text_style);
+        builder.addText(u"\u904d ko ");
+        text_style.setLocale(SkString("zh_Hant"));
+        builder.pushStyle(text_style);
+        builder.addText(u"\u904d zh-Hant ");
+        text_style.setLocale(SkString("zh_Hans"));
+        builder.pushStyle(text_style);
+        builder.addText(u"\u904d zh-Hans ");
+        text_style.setLocale(SkString("zh_HK"));
+        builder.pushStyle(text_style);
+        builder.addText(u"\u904d zh-HK ");
+        auto paragraph = builder.Build();
+        paragraph->layout(width());
+        paragraph->paint(canvas, 0, 0);
+    }
+
+private:
+    typedef Sample INHERITED;
+};
+
+class ParagraphView33 : public ParagraphView_Base {
+protected:
+    SkString name() override { return SkString("Paragraph33"); }
+
+    void onDrawContent(SkCanvas* canvas) override {
+
+        canvas->drawColor(SK_ColorWHITE);
+
+        auto fontCollection = sk_make_sp<FontCollection>();
+        fontCollection->setDefaultFontManager(SkFontMgr::RefDefault());
+        fontCollection->enableFontFallback();
+
+        ParagraphStyle paragraph_style;
+        paragraph_style.setTextAlign(TextAlign::kJustify);
+        ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+        TextStyle text_style;
+        text_style.setColor(SK_ColorBLACK);
+        text_style.setFontFamilies({SkString("Roboto"), SkString("Noto Color Emoji")});
+        text_style.setFontSize(36);
+        builder.pushStyle(text_style);
+        builder.addText(u"AAAAA \U0001f600 BBBBB CCCCC DDDDD EEEEE");
+        auto paragraph = builder.Build();
+        paragraph->layout(109);
+        paragraph->paint(canvas, 0, 0);
+    }
+
+private:
+    typedef Sample INHERITED;
+};
+/*
+void main() {
+  runApp(new Text('AAAAA \u{1f600} BBBBB CCCCC DDDDD EEEEE',
+    style: TextStyle(fontSize: 36.0),
+    textDirection: TextDirection.ltr,
+    textAlign: TextAlign.justify,
+  ));
+}
+}
+ */
 //
 //////////////////////////////////////////////////////////////////////////////
 
@@ -2303,3 +2373,5 @@
 DEF_SAMPLE(return new ParagraphView29();)
 DEF_SAMPLE(return new ParagraphView30();)
 DEF_SAMPLE(return new ParagraphView31();)
+DEF_SAMPLE(return new ParagraphView32();)
+DEF_SAMPLE(return new ParagraphView33();)