Reland "SkParagraph"
This is a reland of 10ad0b9b01e4b8a4721ae2ec1adee9ca7d0fe534
Original change's description:
> SkParagraph
>
> Change-Id: I0a4be75fd0c18021c201bcc1edfdfad8556edeff
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/192100
> Reviewed-by: Ben Wagner <bungeman@google.com>
> Reviewed-by: Mike Reed <reed@google.com>
> Commit-Queue: Julia Lavrova <jlavrova@google.com>
Change-Id: I46cf43eae693edf68e45345acd0eb39e04e02bfc
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/219863
Reviewed-by: Herb Derby <herb@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
diff --git a/modules/skparagraph/src/FontCollection.cpp b/modules/skparagraph/src/FontCollection.cpp
new file mode 100644
index 0000000..c085b99
--- /dev/null
+++ b/modules/skparagraph/src/FontCollection.cpp
@@ -0,0 +1,141 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/include/FontCollection.h"
+#include <string>
+
+namespace skia {
+namespace textlayout {
+
+bool FontCollection::FamilyKey::operator==(const FontCollection::FamilyKey& other) const {
+ return fFontFamily == other.fFontFamily && fLocale == other.fLocale &&
+ fFontStyle == other.fFontStyle;
+}
+
+size_t FontCollection::FamilyKey::Hasher::operator()(const FontCollection::FamilyKey& key) const {
+ return std::hash<std::string>()(key.fFontFamily.c_str()) ^
+ std::hash<std::string>()(key.fLocale.c_str()) ^
+ std::hash<uint32_t>()(key.fFontStyle.weight()) ^
+ std::hash<uint32_t>()(key.fFontStyle.slant());
+}
+
+FontCollection::FontCollection()
+ : fEnableFontFallback(true)
+ , fDefaultFontManager(SkFontMgr::RefDefault())
+ , fDefaultFamilyName(DEFAULT_FONT_FAMILY) {}
+
+FontCollection::~FontCollection() = default;
+
+size_t FontCollection::getFontManagersCount() const { return this->getFontManagerOrder().size(); }
+
+void FontCollection::setAssetFontManager(sk_sp<SkFontMgr> font_manager) {
+ fAssetFontManager = font_manager;
+}
+
+void FontCollection::setDynamicFontManager(sk_sp<SkFontMgr> font_manager) {
+ fDynamicFontManager = font_manager;
+}
+
+void FontCollection::setTestFontManager(sk_sp<SkFontMgr> font_manager) {
+ fTestFontManager = font_manager;
+}
+
+void FontCollection::setDefaultFontManager(sk_sp<SkFontMgr> fontManager,
+ const char defaultFamilyName[]) {
+ fDefaultFontManager = fontManager;
+ fDefaultFamilyName = defaultFamilyName;
+}
+
+// Return the available font managers in the order they should be queried.
+std::vector<sk_sp<SkFontMgr>> FontCollection::getFontManagerOrder() const {
+ std::vector<sk_sp<SkFontMgr>> order;
+ if (fDynamicFontManager) {
+ order.push_back(fDynamicFontManager);
+ }
+ if (fAssetFontManager) {
+ order.push_back(fAssetFontManager);
+ }
+ if (fTestFontManager) {
+ order.push_back(fTestFontManager);
+ }
+ if (fDefaultFontManager && fEnableFontFallback) {
+ order.push_back(fDefaultFontManager);
+ }
+ return order;
+}
+
+sk_sp<SkTypeface> FontCollection::matchTypeface(const char familyName[], SkFontStyle fontStyle) {
+ // Look inside the font collections cache first
+ FamilyKey familyKey(familyName, "en", fontStyle);
+ auto found = fTypefaces.find(familyKey);
+ if (found) {
+ return *found;
+ }
+
+ sk_sp<SkTypeface> typeface = nullptr;
+ for (const auto& manager : this->getFontManagerOrder()) {
+ SkFontStyleSet* set = manager->matchFamily(familyName);
+ if (nullptr == set || set->count() == 0) {
+ continue;
+ }
+
+ for (int i = 0; i < set->count(); ++i) {
+ set->createTypeface(i);
+ }
+
+ sk_sp<SkTypeface> match(set->matchStyle(fontStyle));
+ if (match) {
+ typeface = std::move(match);
+ return typeface;
+ }
+ }
+
+ return nullptr;
+}
+
+sk_sp<SkTypeface> FontCollection::matchDefaultTypeface(SkFontStyle fontStyle) {
+ // Look inside the font collections cache first
+ FamilyKey familyKey(fDefaultFamilyName.c_str(), "en", fontStyle);
+ auto found = fTypefaces.find(familyKey);
+ if (found) {
+ return *found;
+ }
+
+ sk_sp<SkTypeface> typeface = nullptr;
+ for (const auto& manager : this->getFontManagerOrder()) {
+ SkFontStyleSet* set = manager->matchFamily(fDefaultFamilyName.c_str());
+ if (nullptr == set || set->count() == 0) {
+ continue;
+ }
+
+ for (int i = 0; i < set->count(); ++i) {
+ set->createTypeface(i);
+ }
+
+ sk_sp<SkTypeface> match(set->matchStyle(fontStyle));
+ if (match) {
+ typeface = std::move(match);
+ return typeface;
+ }
+ }
+
+ return nullptr;
+}
+
+sk_sp<SkTypeface> FontCollection::defaultFallback(SkUnichar unicode, SkFontStyle fontStyle) {
+
+ for (const auto& manager : this->getFontManagerOrder()) {
+ std::vector<const char*> bcp47;
+ sk_sp<SkTypeface> typeface(manager->matchFamilyStyleCharacter(
+ 0, fontStyle, bcp47.data(), bcp47.size(), unicode));
+ if (typeface != nullptr) {
+ return typeface;
+ }
+ }
+
+ auto result = fDefaultFontManager->matchFamilyStyle(fDefaultFamilyName.c_str(), fontStyle);
+ return sk_ref_sp<SkTypeface>(result);
+}
+
+void FontCollection::disableFontFallback() { fEnableFontFallback = false; }
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/FontIterator.cpp b/modules/skparagraph/src/FontIterator.cpp
new file mode 100644
index 0000000..13f5359
--- /dev/null
+++ b/modules/skparagraph/src/FontIterator.cpp
@@ -0,0 +1,265 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/FontIterator.h"
+#include <unicode/brkiter.h>
+#include <unicode/ubidi.h>
+#include "include/core/SkBlurTypes.h"
+#include "include/core/SkCanvas.h"
+#include "include/core/SkFontMgr.h"
+#include "include/core/SkPictureRecorder.h"
+#include "modules/skparagraph/src/ParagraphImpl.h"
+#include "src/core/SkSpan.h"
+#include "src/utils/SkUTF.h"
+
+namespace {
+SkUnichar utf8_next(const char** ptr, const char* end) {
+ SkUnichar val = SkUTF::NextUTF8(ptr, end);
+ return val < 0 ? 0xFFFD : val;
+}
+} // namespace
+
+// TODO: FontCollection and FontIterator have common functionality
+namespace skia {
+namespace textlayout {
+
+FontIterator::FontIterator(SkSpan<const char> utf8,
+ SkSpan<TextBlock>
+ styles,
+ sk_sp<FontCollection>
+ fonts,
+ bool hintingOn)
+ : fText(utf8)
+ , fStyles(styles)
+ , fCurrentChar(utf8.begin())
+ , fFontCollection(std::move(fonts))
+ , fHintingOn(hintingOn) {
+ findAllFontsForAllStyledBlocks();
+}
+
+void FontIterator::consume() {
+ SkASSERT(fCurrentChar < fText.end());
+ auto found = fFontMapping.find(fCurrentChar);
+ SkASSERT(found != nullptr);
+ fFont = found->first;
+ fLineHeight = found->second;
+
+ // Move until we find the first character that cannot be resolved with the current font
+ while (++fCurrentChar != fText.end()) {
+ found = fFontMapping.find(fCurrentChar);
+ if (found != nullptr) {
+ if (fFont == found->first && fLineHeight == found->second) {
+ continue;
+ }
+ break;
+ }
+ }
+}
+
+void FontIterator::findAllFontsForAllStyledBlocks() {
+ TextBlock combined;
+ for (auto& block : fStyles) {
+ SkASSERT(combined.text().begin() == nullptr ||
+ combined.text().end() == block.text().begin());
+
+ if (combined.text().begin() != nullptr &&
+ block.style().matchOneAttribute(StyleType::kFont, combined.style())) {
+ combined.add(block.text());
+ continue;
+ }
+
+ if (!combined.text().empty()) {
+ findAllFontsForStyledBlock(combined.style(), combined.text());
+ }
+
+ combined = block;
+ }
+ findAllFontsForStyledBlock(combined.style(), combined.text());
+
+ if (!fText.empty() && fFontMapping.find(fText.begin()) == nullptr) {
+ // Resolve the first character with the first found font
+ fFontMapping.set(fText.begin(), fFirstResolvedFont);
+ }
+}
+
+void FontIterator::findAllFontsForStyledBlock(const TextStyle& style, SkSpan<const char> text) {
+ fCodepoints.reset();
+ fCharacters.reset();
+ fUnresolvedIndexes.reset();
+ fUnresolvedCodepoints.reset();
+
+ // Extract all unicode codepoints
+ const char* current = text.begin();
+ while (current != text.end()) {
+ fCharacters.emplace_back(current);
+ fCodepoints.emplace_back(utf8_next(¤t, text.end()));
+ fUnresolvedIndexes.emplace_back(fUnresolvedIndexes.size());
+ }
+ fUnresolved = fCodepoints.size();
+
+ // Walk through all available fonts to resolve the block
+ for (auto& fontFamily : style.getFontFamilies()) {
+ auto typeface = fFontCollection->matchTypeface(fontFamily.c_str(), style.getFontStyle());
+ if (typeface.get() == nullptr) {
+ continue;
+ }
+
+ // Resolve all unresolved characters
+ auto font = makeFont(typeface, style.getFontSize(), style.getHeight());
+ resolveAllCharactersByFont(font);
+ if (fUnresolved == 0) {
+ break;
+ }
+ }
+
+ if (fUnresolved > 0) {
+ auto typeface = fFontCollection->matchDefaultTypeface(style.getFontStyle());
+ if (typeface.get() != nullptr) {
+ // Resolve all unresolved characters
+ auto font = makeFont(typeface, style.getFontSize(), style.getHeight());
+ resolveAllCharactersByFont(font);
+ }
+ }
+
+ addResolvedWhitespacesToMapping();
+
+ if (fUnresolved > 0 && fFontCollection->fontFallbackEnabled()) {
+ while (fUnresolved > 0) {
+ auto unicode = firstUnresolved();
+ auto typeface = fFontCollection->defaultFallback(unicode, style.getFontStyle());
+ if (typeface == nullptr) {
+ break;
+ }
+ auto font = makeFont(typeface, style.getFontSize(), style.getHeight());
+ if (!resolveAllCharactersByFont(font)) {
+ // Not a single unicode character was resolved
+ break;
+ }
+ SkString name;
+ typeface->getFamilyName(&name);
+ SkDebugf("Default font fallback resolution: %s\n", name.c_str());
+ }
+ }
+
+ // In case something still unresolved
+ if (fResolvedFonts.count() == 0) {
+ makeFont(fFontCollection->defaultFallback(firstUnresolved(), style.getFontStyle()),
+ style.getFontSize(), style.getHeight());
+ if (fFirstResolvedFont.first.getTypeface() != nullptr) {
+ SkString name;
+ fFirstResolvedFont.first.getTypeface()->getFamilyName(&name);
+ SkDebugf("Urgent font resolution: %s\n", name.c_str());
+ } else {
+ SkDebugf("No font!!!\n");
+ }
+ }
+}
+
+size_t FontIterator::resolveAllCharactersByFont(std::pair<SkFont, SkScalar> font) {
+ // Consolidate all unresolved unicodes in one array to make a batch call
+ SkTArray<SkGlyphID> glyphs(fUnresolved);
+ glyphs.push_back_n(fUnresolved, SkGlyphID(0));
+ font.first.getTypeface()->unicharsToGlyphs(
+ fUnresolved == fCodepoints.size() ? fCodepoints.data() : fUnresolvedCodepoints.data(),
+ fUnresolved, glyphs.data());
+
+ SkRange<size_t> resolved(0, 0);
+ SkRange<size_t> whitespaces(0, 0);
+ size_t stillUnresolved = 0;
+
+ auto processRuns = [&]() {
+ if (resolved.width() == 0) {
+ return;
+ }
+
+ if (resolved.width() == whitespaces.width()) {
+ // The entire run is just whitespaces;
+ // Remember the font and mark whitespaces back unresolved
+ // to calculate its mapping for the other fonts
+ for (auto w = whitespaces.start; w != whitespaces.end; ++w) {
+ if (fWhitespaces.find(w) == nullptr) {
+ fWhitespaces.set(w, font);
+ }
+ fUnresolvedIndexes[stillUnresolved++] = w;
+ fUnresolvedCodepoints.emplace_back(fCodepoints[w]);
+ }
+ } else {
+ fFontMapping.set(fCharacters[resolved.start], font);
+ }
+ };
+
+ // Try to resolve all the unresolved unicode points
+ for (size_t i = 0; i < glyphs.size(); ++i) {
+ auto glyph = glyphs[i];
+ auto index = fUnresolvedIndexes[i];
+
+ if (glyph == 0) {
+ processRuns();
+
+ resolved = SkRange<size_t>(0, 0);
+ whitespaces = SkRange<size_t>(0, 0);
+
+ fUnresolvedIndexes[stillUnresolved++] = index;
+ fUnresolvedCodepoints.emplace_back(fCodepoints[index]);
+ continue;
+ }
+
+ if (index == resolved.end) {
+ ++resolved.end;
+ } else {
+ processRuns();
+ resolved = SkRange<size_t>(index, index + 1);
+ }
+ if (u_isUWhiteSpace(fCodepoints[index])) {
+ if (index == whitespaces.end) {
+ ++whitespaces.end;
+ } else {
+ whitespaces = SkRange<size_t>(index, index + 1);
+ }
+ } else {
+ whitespaces = SkRange<size_t>(0, 0);
+ }
+ }
+
+ // One last time to take care of the tail run
+ processRuns();
+
+ size_t wasUnresolved = fUnresolved;
+ fUnresolved = stillUnresolved;
+ return fUnresolved < wasUnresolved;
+}
+
+void FontIterator::addResolvedWhitespacesToMapping() {
+ size_t resolvedWhitespaces = 0;
+ for (size_t i = 0; i < fUnresolved; ++i) {
+ auto index = fUnresolvedIndexes[i];
+ auto found = fWhitespaces.find(index);
+ if (found != nullptr) {
+ fFontMapping.set(fCharacters[index], *found);
+ ++resolvedWhitespaces;
+ }
+ }
+ fUnresolved -= resolvedWhitespaces;
+}
+
+std::pair<SkFont, SkScalar> FontIterator::makeFont(sk_sp<SkTypeface> typeface,
+ SkScalar size,
+ SkScalar height) {
+ SkFont font(typeface, size);
+ font.setEdging(SkFont::Edging::kAntiAlias);
+ if (!fHintingOn) {
+ font.setHinting(SkFontHinting::kSlight);
+ font.setSubpixel(true);
+ }
+ auto pair = std::make_pair(font, height);
+
+ auto foundFont = fResolvedFonts.find(pair);
+ if (foundFont == nullptr) {
+ if (fResolvedFonts.count() == 0) {
+ fFirstResolvedFont = pair;
+ }
+ fResolvedFonts.add(pair);
+ }
+
+ return pair;
+}
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/FontIterator.h b/modules/skparagraph/src/FontIterator.h
new file mode 100644
index 0000000..68858ec
--- /dev/null
+++ b/modules/skparagraph/src/FontIterator.h
@@ -0,0 +1,79 @@
+// Copyright 2019 Google LLC.
+#ifndef FontIterator_DEFINED
+#define FontIterator_DEFINED
+
+#include <unicode/brkiter.h>
+#include <unicode/ubidi.h>
+#include "modules/skparagraph/src/ParagraphImpl.h"
+#include "include/core/SkBlurTypes.h"
+#include "include/core/SkCanvas.h"
+#include "include/core/SkFontMgr.h"
+#include "include/core/SkPictureRecorder.h"
+#include "src/core/SkSpan.h"
+#include "src/utils/SkUTF.h"
+
+namespace skia {
+namespace textlayout {
+
+class FontIterator final : public SkShaper::FontRunIterator {
+public:
+ FontIterator(SkSpan<const char> utf8,
+ SkSpan<TextBlock> styles,
+ sk_sp<FontCollection> fonts,
+ bool hintingOn);
+
+ void consume() override;
+
+ size_t endOfCurrentRun() const override { return fCurrentChar - fText.begin(); }
+ bool atEnd() const override { return fCurrentChar == fText.end(); }
+ const SkFont& currentFont() const override { return fFont; }
+ SkScalar lineHeight() const { return fLineHeight; }
+
+private:
+ struct Hash {
+ uint32_t operator()(const std::pair<SkFont, SkScalar>& key) const {
+ return SkTypeface::UniqueID(key.first.getTypeface()) +
+ SkScalarCeilToInt(key.first.getSize()) + SkScalarCeilToInt(key.second);
+ }
+ };
+
+ void findAllFontsForAllStyledBlocks();
+
+ void findAllFontsForStyledBlock(const TextStyle& style, SkSpan<const char> text);
+
+ std::pair<SkFont, SkScalar> makeFont(sk_sp<SkTypeface> typeface, SkScalar size,
+ SkScalar height);
+
+ size_t resolveAllCharactersByFont(std::pair<SkFont, SkScalar> font);
+ void addResolvedWhitespacesToMapping();
+
+ SkUnichar firstUnresolved() {
+ if (fUnresolved == 0) return 0;
+
+ bool firstTry = fUnresolved == fCodepoints.size();
+ auto index = firstTry ? 0 : fUnresolvedIndexes[0];
+ return fCodepoints[index];
+ }
+
+ SkSpan<const char> fText;
+ SkSpan<TextBlock> fStyles;
+ const char* fCurrentChar;
+ SkFont fFont;
+ SkScalar fLineHeight;
+ sk_sp<FontCollection> fFontCollection;
+ SkTHashMap<const char*, std::pair<SkFont, SkScalar>> fFontMapping;
+ SkTHashSet<std::pair<SkFont, SkScalar>, Hash> fResolvedFonts;
+ bool fHintingOn;
+ std::pair<SkFont, SkScalar> fFirstResolvedFont;
+
+ SkTArray<SkUnichar> fCodepoints;
+ SkTArray<const char*> fCharacters;
+ SkTArray<size_t> fUnresolvedIndexes;
+ SkTArray<SkUnichar> fUnresolvedCodepoints;
+ SkTHashMap<size_t, std::pair<SkFont, SkScalar>> fWhitespaces;
+ size_t fUnresolved;
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // FontIterator_DEFINED
diff --git a/modules/skparagraph/src/ParagraphBuilderImpl.cpp b/modules/skparagraph/src/ParagraphBuilderImpl.cpp
new file mode 100644
index 0000000..0a97527
--- /dev/null
+++ b/modules/skparagraph/src/ParagraphBuilderImpl.cpp
@@ -0,0 +1,106 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/ParagraphBuilderImpl.h"
+#include "include/core/SkPaint.h"
+#include "modules/skparagraph/include/ParagraphStyle.h"
+#include "modules/skparagraph/src/ParagraphImpl.h"
+#include "src/core/SkMakeUnique.h"
+#include "src/core/SkSpan.h"
+#include "unicode/unistr.h"
+
+namespace skia {
+namespace textlayout {
+
+std::shared_ptr<ParagraphBuilder> ParagraphBuilder::make(
+ ParagraphStyle style, sk_sp<FontCollection> fontCollection) {
+ return std::make_shared<ParagraphBuilderImpl>(style, fontCollection);
+}
+
+ParagraphBuilderImpl::ParagraphBuilderImpl(
+ ParagraphStyle style, sk_sp<FontCollection> fontCollection)
+ : ParagraphBuilder(style, fontCollection), fFontCollection(std::move(fontCollection)) {
+ this->setParagraphStyle(style);
+}
+
+ParagraphBuilderImpl::~ParagraphBuilderImpl() = default;
+
+void ParagraphBuilderImpl::setParagraphStyle(const ParagraphStyle& style) {
+ fParagraphStyle = style;
+ fTextStyles.push(fParagraphStyle.getTextStyle());
+ fStyledBlocks.emplace_back(fUtf8.size(), fUtf8.size(), fParagraphStyle.getTextStyle());
+}
+
+void ParagraphBuilderImpl::pushStyle(const TextStyle& style) {
+ this->endRunIfNeeded();
+
+ fTextStyles.push(style);
+ if (!fStyledBlocks.empty() && fStyledBlocks.back().fEnd == fUtf8.size() &&
+ fStyledBlocks.back().fStyle == style) {
+ // Just continue with the same style
+ } else {
+ // Go with the new style
+ fStyledBlocks.emplace_back(fUtf8.size(), fUtf8.size(), fTextStyles.top());
+ }
+}
+
+void ParagraphBuilderImpl::pop() {
+ this->endRunIfNeeded();
+
+ if (fTextStyles.size() > 1) {
+ fTextStyles.pop();
+ } else {
+ // In this case we use paragraph style and skip Pop operation
+ SkDebugf("SkParagraphBuilder.Pop() called too many times.\n");
+ }
+
+ auto top = fTextStyles.top();
+ fStyledBlocks.emplace_back(fUtf8.size(), fUtf8.size(), top);
+}
+
+TextStyle ParagraphBuilderImpl::peekStyle() {
+ this->endRunIfNeeded();
+
+ if (!fTextStyles.empty()) {
+ return fTextStyles.top();
+ } else {
+ SkDebugf("SkParagraphBuilder._styles is empty.\n");
+ return fParagraphStyle.getTextStyle();
+ }
+}
+
+void ParagraphBuilderImpl::addText(const std::u16string& text) {
+ icu::UnicodeString unicode;
+ unicode.setTo((UChar*)text.data());
+ std::string str;
+ unicode.toUTF8String(str);
+ // SkDebugf("Layout text16: '%s'\n", str.c_str());
+ fUtf8.insert(fUtf8.size(), str.c_str());
+}
+
+void ParagraphBuilderImpl::addText(const char* text) {
+ // SkDebugf("Layout text8: '%s'\n", text);
+ fUtf8.insert(fUtf8.size(), text);
+}
+
+void ParagraphBuilderImpl::endRunIfNeeded() {
+ if (fStyledBlocks.empty()) {
+ return;
+ }
+
+ auto& last = fStyledBlocks.back();
+ if (last.fStart == fUtf8.size()) {
+ fStyledBlocks.pop_back();
+ } else {
+ last.fEnd = fUtf8.size();
+ }
+}
+
+std::unique_ptr<Paragraph> ParagraphBuilderImpl::Build() {
+ if (!fUtf8.isEmpty()) {
+ this->endRunIfNeeded();
+ }
+ return skstd::make_unique<ParagraphImpl>(
+ fUtf8, fParagraphStyle, fStyledBlocks, fFontCollection);
+}
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/ParagraphBuilderImpl.h b/modules/skparagraph/src/ParagraphBuilderImpl.h
new file mode 100644
index 0000000..ca5ddde
--- /dev/null
+++ b/modules/skparagraph/src/ParagraphBuilderImpl.h
@@ -0,0 +1,67 @@
+// Copyright 2019 Google LLC.
+#ifndef ParagraphBuilderImpl_DEFINED
+#define ParagraphBuilderImpl_DEFINED
+
+#include <memory>
+#include <stack>
+#include <string>
+#include <tuple>
+#include "modules/skparagraph/include/FontCollection.h"
+#include "modules/skparagraph/include/Paragraph.h"
+#include "modules/skparagraph/include/ParagraphBuilder.h"
+#include "modules/skparagraph/include/ParagraphStyle.h"
+#include "modules/skparagraph/include/TextStyle.h"
+
+namespace skia {
+namespace textlayout {
+
+class ParagraphBuilderImpl : public ParagraphBuilder {
+public:
+ ParagraphBuilderImpl(ParagraphStyle style, sk_sp<FontCollection> fontCollection);
+
+ ~ParagraphBuilderImpl() override;
+
+ // Push a style to the stack. The corresponding text added with AddText will
+ // use the top-most style.
+ void pushStyle(const TextStyle& style) override;
+
+ // Remove a style from the stack. Useful to apply different styles to chunks
+ // of text such as bolding.
+ // Example:
+ // builder.PushStyle(normal_style);
+ // builder.AddText("Hello this is normal. ");
+ //
+ // builder.PushStyle(bold_style);
+ // builder.AddText("And this is BOLD. ");
+ //
+ // builder.Pop();
+ // builder.AddText(" Back to normal again.");
+ void pop() override;
+
+ TextStyle peekStyle() override;
+
+ // Adds text to the builder. Forms the proper runs to use the upper-most style
+ // on the style_stack_;
+ void addText(const std::u16string& text) override;
+
+ // Converts to u16string before adding.
+ void addText(const char* text) override;
+
+ void setParagraphStyle(const ParagraphStyle& style) override;
+
+ // Constructs a SkParagraph object that can be used to layout and paint the text to a SkCanvas.
+ std::unique_ptr<Paragraph> Build() override;
+
+private:
+ void endRunIfNeeded();
+
+ SkString fUtf8;
+ std::stack<TextStyle> fTextStyles;
+ std::vector<Block> fStyledBlocks;
+ sk_sp<FontCollection> fFontCollection;
+ ParagraphStyle fParagraphStyle;
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // ParagraphBuilderImpl_DEFINED
diff --git a/modules/skparagraph/src/ParagraphCache.h b/modules/skparagraph/src/ParagraphCache.h
new file mode 100644
index 0000000..cc3f9d5
--- /dev/null
+++ b/modules/skparagraph/src/ParagraphCache.h
@@ -0,0 +1,96 @@
+// Copyright 2019 Google LLC.
+#ifndef ParagraphCache_DEFINED
+#define ParagraphCache_DEFINED
+
+namespace skia {
+namespace textlayout {
+
+// Just the flutter input for now
+class ParagraphCacheKey {
+public:
+ ParagraphCacheKey(SkParagraphStyle paraStyle,
+ SkTHashMap<const char*, std::pair<SkFont, SkScalar>> mapping,
+ SkSpan<const char> utf8)
+ : fHash(0) {
+ fHash = mix(fHash, paraStyle.computeHash());
+ fHash = mix(fHash, computeHash(mapping));
+ fHash = mix(fHash, computeHash(utf8));
+ }
+
+ uint32_t hash() const { return fHash; }
+
+private:
+ uint32 computeHash(SkTHashMap<const char*, std::pair<SkFont, SkScalar>> mapping) {
+ uint32 hash = 0;
+ mapping.forEach([&hash](const char* ch, std::pair<SkFont, SkScalar> font) {
+ hash = mix(hash, t.computeHash());
+ });
+ for (auto& t : array) {
+ hash = mix(hash, ch);
+ hash = mix(hash, SkGoodHash(font.first));
+ hash = mix(hash, SkGoodHash(font.second));
+ }
+ return hash;
+ }
+
+ uint32 computeHash(SkSpan<const char> text) {}
+
+ uint32 computeHash(SkSpan<const char> text) {
+ uint32 hash = mix(0, text.size());
+ for (uint32 i = 0; i < text.size(); i += 2) {
+ uint32 data = text[i] | text[i + 1] << 16;
+ hash = mix(hash, data);
+ }
+ if (text.size() & 1) {
+ uint32 data = text.back();
+ hash = mix(hash, data);
+ }
+ return hash;
+ }
+
+ uint32 mix(uint32 hash, uint32 data) {
+ hash += data;
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+ return hash;
+ }
+
+ uint32 fHash;
+};
+
+class ParagraphCacheValue {
+public:
+ ParagraphCacheValue(std::shared<SkParagraph> paragraph,
+ SkTHashMap<const char*,
+ std::pair<SkFont, SkScalar>> mapping)
+ : fKey(ParagraphCacheKey(paragraph.getParagraphStyle(), mapping, paragraph.getText()))
+ , fFontCollection(collection)
+ , fParagraphStyle(paraStyle)
+ , fTextStyles(textStyles)
+ , fUtf8(utf8) {}
+
+ static const ParagraphCacheKey& GetKey(const ParagraphCacheValue& value) { return fKey; }
+ static uint32_t Hash(const ParagraphCacheKey& key) { return fKey.hash(); }
+
+private:
+ ParagraphCacheKey fKey;
+
+ std::shared<SkParagraph> fParagraph;
+ std::pair<SkFont, SkScalar>>fMapping;
+};
+
+class ParagraphCache : public SkTDynamicHash<ParagraphCacheValue, ParagraphCacheKey> {
+public:
+ Hash() : INHERITED() {}
+
+ // Promote protected methods to public for this test.
+ int capacity() const { return this->INHERITED::capacity(); }
+ int countCollisions(const int& key) const { return this->INHERITED::countCollisions(key); }
+
+private:
+ typedef SkTDynamicHash<ParagraphCacheValue, ParagraphCacheKey> INHERITED;
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // ParagraphCache_DEFINED
diff --git a/modules/skparagraph/src/ParagraphImpl.cpp b/modules/skparagraph/src/ParagraphImpl.cpp
new file mode 100644
index 0000000..25b944a
--- /dev/null
+++ b/modules/skparagraph/src/ParagraphImpl.cpp
@@ -0,0 +1,623 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/ParagraphImpl.h"
+#include <unicode/brkiter.h>
+#include <unicode/ubidi.h>
+#include <unicode/unistr.h>
+#include "include/core/SkBlurTypes.h"
+#include "include/core/SkCanvas.h"
+#include "include/core/SkFontMgr.h"
+#include "include/core/SkPictureRecorder.h"
+#include "modules/skparagraph/src/FontIterator.h"
+#include "modules/skparagraph/src/Run.h"
+#include "modules/skparagraph/src/TextWrapper.h"
+#include "src/core/SkSpan.h"
+#include "src/utils/SkUTF.h"
+
+namespace {
+
+SkSpan<const char> operator*(const SkSpan<const char>& a, const SkSpan<const char>& b) {
+ auto begin = SkTMax(a.begin(), b.begin());
+ auto end = SkTMin(a.end(), b.end());
+ return SkSpan<const char>(begin, end > begin ? end - begin : 0);
+}
+
+SkUnichar utf8_next(const char** ptr, const char* end) {
+ SkUnichar val = SkUTF::NextUTF8(ptr, end);
+ return val < 0 ? 0xFFFD : val;
+}
+
+class TextBreaker {
+public:
+ TextBreaker() : fPos(-1) {}
+
+ bool initialize(SkSpan<const char> text, UBreakIteratorType type) {
+ UErrorCode status = U_ZERO_ERROR;
+
+ fSize = text.size();
+ UText utf8UText = UTEXT_INITIALIZER;
+ utext_openUTF8(&utf8UText, text.begin(), text.size(), &status);
+ fAutoClose =
+ std::unique_ptr<UText, SkFunctionWrapper<UText*, UText, utext_close>>(&utf8UText);
+ if (U_FAILURE(status)) {
+ SkDebugf("Could not create utf8UText: %s", u_errorName(status));
+ return false;
+ }
+ fIterator = ubrk_open(type, "en", nullptr, 0, &status);
+ if (U_FAILURE(status)) {
+ SkDebugf("Could not create line break iterator: %s", u_errorName(status));
+ SK_ABORT("");
+ }
+
+ ubrk_setUText(fIterator, &utf8UText, &status);
+ if (U_FAILURE(status)) {
+ SkDebugf("Could not setText on break iterator: %s", u_errorName(status));
+ return false;
+ }
+
+ fPos = 0;
+ return true;
+ }
+
+ size_t first() {
+ fPos = ubrk_first(fIterator);
+ return eof() ? fSize : fPos;
+ }
+
+ size_t next() {
+ fPos = ubrk_next(fIterator);
+ return eof() ? fSize : fPos;
+ }
+
+ int32_t status() { return ubrk_getRuleStatus(fIterator); }
+
+ bool eof() { return fPos == icu::BreakIterator::DONE; }
+
+ ~TextBreaker() = default;
+
+private:
+ std::unique_ptr<UText, SkFunctionWrapper<UText*, UText, utext_close>> fAutoClose;
+ UBreakIterator* fIterator;
+ int32_t fPos;
+ size_t fSize;
+};
+} // namespace
+
+namespace skia {
+namespace textlayout {
+
+ParagraphImpl::ParagraphImpl(const std::u16string& utf16text,
+ ParagraphStyle style,
+ std::vector<Block>
+ blocks,
+ sk_sp<FontCollection>
+ fonts)
+ : Paragraph(std::move(style), std::move(fonts)), fPicture(nullptr) {
+ icu::UnicodeString unicode((UChar*)utf16text.data(), SkToS32(utf16text.size()));
+ std::string str;
+ unicode.toUTF8String(str);
+ fText = SkString(str.data(), str.size());
+ fTextSpan = SkSpan<const char>(fText.c_str(), fText.size());
+
+ fTextStyles.reserve(blocks.size());
+ for (auto& block : blocks) {
+ fTextStyles.emplace_back(
+ SkSpan<const char>(fTextSpan.begin() + block.fStart, block.fEnd - block.fStart),
+ block.fStyle);
+ }
+}
+
+ParagraphImpl::~ParagraphImpl() = default;
+
+void ParagraphImpl::layout(SkScalar width) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ this->resetContext();
+
+ this->resolveStrut();
+
+ if (!this->shapeTextIntoEndlessLine()) {
+ // Apply the last style to the empty text
+ FontIterator font(SkSpan<const char>(" "),
+ SkSpan<TextBlock>(&fTextStyles.back(), 1),
+ fFontCollection,
+ fParagraphStyle.hintingIsOn());
+ font.consume();
+ SkFontMetrics metrics;
+ font.currentFont().getMetrics(&metrics);
+ fHeight = LineMetrics(metrics).height();
+ return;
+ }
+
+ this->buildClusterTable();
+
+ this->breakShapedTextIntoLines(width);
+}
+
+void ParagraphImpl::resolveStrut() {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ auto strutStyle = this->paragraphStyle().getStrutStyle();
+ if (!strutStyle.getStrutEnabled()) {
+ return;
+ }
+
+ sk_sp<SkTypeface> typeface;
+ for (auto& fontFamily : strutStyle.getFontFamilies()) {
+ typeface = fFontCollection->matchTypeface(fontFamily.c_str(), strutStyle.getFontStyle());
+ if (typeface.get() != nullptr) {
+ break;
+ }
+ }
+ if (typeface.get() == nullptr) {
+ typeface = SkTypeface::MakeDefault();
+ }
+
+ SkFont font(typeface, strutStyle.getFontSize());
+ SkFontMetrics metrics;
+ font.getMetrics(&metrics);
+
+ fStrutMetrics = LineMetrics(metrics.fAscent * strutStyle.getHeight(),
+ metrics.fDescent * strutStyle.getHeight(),
+ strutStyle.getLeading() < 0
+ ? metrics.fLeading
+ : strutStyle.getLeading() * strutStyle.getFontSize());
+}
+
+void ParagraphImpl::paint(SkCanvas* canvas, SkScalar x, SkScalar y) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (nullptr == fPicture) {
+ // Build the picture lazily not until we actually have to paint (or never)
+ this->formatLines(fWidth);
+ this->paintLinesIntoPicture();
+ }
+
+ SkMatrix matrix = SkMatrix::MakeTrans(x, y);
+ canvas->drawPicture(fPicture, &matrix, nullptr);
+}
+
+void ParagraphImpl::resetContext() {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ fAlphabeticBaseline = 0;
+ fHeight = 0;
+ fWidth = 0;
+ fIdeographicBaseline = 0;
+ fMaxIntrinsicWidth = 0;
+ fMinIntrinsicWidth = 0;
+ fMaxLineWidth = 0;
+
+ fPicture = nullptr;
+ fRuns.reset();
+ fClusters.reset();
+ fLines.reset();
+}
+
+// Clusters in the order of the input text
+void ParagraphImpl::buildClusterTable() {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Find all possible (soft) line breaks
+ TextBreaker breaker;
+ if (!breaker.initialize(fTextSpan, UBRK_LINE)) {
+ return;
+ }
+ size_t currentPos = breaker.first();
+ SkTHashMap<const char*, bool> softLineBreaks;
+ while (!breaker.eof()) {
+ currentPos = breaker.next();
+ const char* ch = currentPos + fTextSpan.begin();
+ softLineBreaks.set(ch, breaker.status() == UBRK_LINE_HARD);
+ }
+
+ TextBlock* currentStyle = this->fTextStyles.begin();
+ SkScalar shift = 0;
+ // Cannot set SkSpan<SkCluster> until the array is done - can be moved around
+ std::vector<std::tuple<Run*, size_t, size_t>> toUpdate;
+
+ // Walk through all the run in the direction of input text
+ for (auto& run : fRuns) {
+ auto runStart = fClusters.size();
+ // Walk through the glyph in the direction of input text
+ run.iterateThroughClustersInTextOrder([&run, this, &softLineBreaks, ¤tStyle, &shift](
+ size_t glyphStart,
+ size_t glyphEnd,
+ size_t charStart,
+ size_t charEnd,
+ SkScalar width,
+ SkScalar height) {
+ SkASSERT(charEnd >= charStart);
+ SkSpan<const char> text(fTextSpan.begin() + charStart, charEnd - charStart);
+
+ auto& cluster = fClusters.emplace_back(&run, glyphStart, glyphEnd, text, width, height);
+
+ // Mark the line breaks
+ auto found = softLineBreaks.find(cluster.text().end());
+ if (found) {
+ cluster.setBreakType(*found ? Cluster::BreakType::HardLineBreak
+ : Cluster::BreakType::SoftLineBreak);
+ }
+ cluster.setIsWhiteSpaces();
+
+ // Shift the cluster
+ run.shift(&cluster, shift);
+
+ // Synchronize styles (one cluster can be covered by few styles)
+ while (!cluster.startsIn(currentStyle->text())) {
+ currentStyle++;
+ SkASSERT(currentStyle != this->fTextStyles.end());
+ }
+
+ // Take spacing styles in account
+ if (currentStyle->style().getWordSpacing() != 0 &&
+ fParagraphStyle.getTextAlign() != TextAlign::kJustify) {
+ if (cluster.isWhitespaces() && cluster.isSoftBreak()) {
+ shift +=
+ run.addSpacesAtTheEnd(currentStyle->style().getWordSpacing(), &cluster);
+ }
+ }
+ if (currentStyle->style().getLetterSpacing() != 0) {
+ shift += run.addSpacesEvenly(currentStyle->style().getLetterSpacing(), &cluster);
+ }
+ });
+
+ toUpdate.emplace_back(&run, runStart, fClusters.size() - runStart);
+ fMaxIntrinsicWidth += run.advance().fX;
+ }
+ fClusters.emplace_back(nullptr, 0, 0, SkSpan<const char>(), 0, 0);
+
+ // Set SkSpan<SkCluster> ranges for all the runs
+ for (auto update : toUpdate) {
+ auto run = std::get<0>(update);
+ auto start = std::get<1>(update);
+ auto size = std::get<2>(update);
+ run->setClusters(SkSpan<Cluster>(&fClusters[start], size));
+ }
+}
+
+bool ParagraphImpl::shapeTextIntoEndlessLine() {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ class ShapeHandler final : public SkShaper::RunHandler {
+ public:
+ explicit ShapeHandler(ParagraphImpl& paragraph, FontIterator* fontIterator)
+ : fParagraph(¶graph)
+ , fFontIterator(fontIterator)
+ , fAdvance(SkVector::Make(0, 0)) {}
+
+ SkVector advance() const { return fAdvance; }
+
+ private:
+ void beginLine() override {}
+
+ void runInfo(const RunInfo&) override {}
+
+ void commitRunInfo() override {}
+
+ Buffer runBuffer(const RunInfo& info) override {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ auto& run = fParagraph->fRuns.emplace_back(fParagraph->text(),
+ info,
+ fFontIterator->lineHeight(),
+ fParagraph->fRuns.count(),
+ fAdvance.fX);
+ return run.newRunBuffer();
+ }
+
+ void commitRunBuffer(const RunInfo&) override {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ auto& run = fParagraph->fRuns.back();
+ if (run.size() == 0) {
+ fParagraph->fRuns.pop_back();
+ return;
+ }
+ // Carve out the line text out of the entire run text
+ fAdvance.fX += run.advance().fX;
+ fAdvance.fY = SkMaxScalar(fAdvance.fY, run.descent() - run.ascent());
+ }
+
+ void commitLine() override {}
+
+ ParagraphImpl* fParagraph;
+ FontIterator* fFontIterator;
+ SkVector fAdvance;
+ };
+
+ if (fTextSpan.empty()) {
+ return false;
+ }
+
+ SkSpan<TextBlock> styles(fTextStyles.begin(), fTextStyles.size());
+ FontIterator font(fTextSpan, styles, fFontCollection, fParagraphStyle.hintingIsOn());
+ ShapeHandler handler(*this, &font);
+ std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder();
+ SkASSERT_RELEASE(shaper != nullptr);
+ auto bidi = SkShaper::MakeIcuBiDiRunIterator(
+ fTextSpan.begin(), fTextSpan.size(),
+ fParagraphStyle.getTextDirection() == TextDirection::kLtr ? (uint8_t)2 : (uint8_t)1);
+ if (bidi == nullptr) {
+ return false;
+ }
+ auto script = SkShaper::MakeHbIcuScriptRunIterator(fTextSpan.begin(), fTextSpan.size());
+ auto lang = SkShaper::MakeStdLanguageRunIterator(fTextSpan.begin(), fTextSpan.size());
+
+ shaper->shape(fTextSpan.begin(), fTextSpan.size(), font, *bidi, *script, *lang,
+ std::numeric_limits<SkScalar>::max(), &handler);
+ return true;
+}
+
+void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ TextWrapper textWrapper;
+ textWrapper.breakTextIntoLines(
+ this,
+ maxWidth,
+ [&](SkSpan<const char> text,
+ SkSpan<const char>
+ textWithSpaces,
+ Cluster* start,
+ Cluster* end,
+ size_t startPos,
+ size_t endPos,
+ SkVector offset,
+ SkVector advance,
+ LineMetrics metrics,
+ bool addEllipsis) {
+ // Add the line
+ // TODO: Take in account clipped edges
+ SkSpan<const Cluster> clusters(start, end - start + 1);
+ auto& line = this->addLine(offset, advance, text, textWithSpaces, clusters,
+ startPos, endPos, metrics);
+ if (addEllipsis) {
+ line.createEllipsis(maxWidth, fParagraphStyle.getEllipsis(), true);
+ }
+ });
+
+ fHeight = textWrapper.height();
+ fWidth = maxWidth; // fTextWrapper.width();
+ fMinIntrinsicWidth = textWrapper.minIntrinsicWidth();
+ fMaxIntrinsicWidth = textWrapper.maxIntrinsicWidth();
+ fAlphabeticBaseline = fLines.empty() ? 0 : fLines.front().alphabeticBaseline();
+ fIdeographicBaseline = fLines.empty() ? 0 : fLines.front().ideographicBaseline();
+}
+
+void ParagraphImpl::formatLines(SkScalar maxWidth) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ auto effectiveAlign = fParagraphStyle.effective_align();
+ for (auto& line : fLines) {
+ line.format(effectiveAlign, maxWidth, &line != &fLines.back());
+ }
+}
+
+void ParagraphImpl::paintLinesIntoPicture() {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkPictureRecorder recorder;
+ SkCanvas* textCanvas = recorder.beginRecording(fWidth, fHeight, nullptr, 0);
+
+ for (auto& line : fLines) {
+ line.paint(textCanvas);
+ }
+
+ fPicture = recorder.finishRecordingAsPicture();
+}
+
+SkSpan<const TextBlock> ParagraphImpl::findAllBlocks(SkSpan<const char> text) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ const TextBlock* begin = nullptr;
+ const TextBlock* end = nullptr;
+ for (auto& block : fTextStyles) {
+ if (block.text().end() <= text.begin()) {
+ continue;
+ }
+ if (block.text().begin() >= text.end()) {
+ break;
+ }
+ if (begin == nullptr) {
+ begin = █
+ }
+ end = █
+ }
+
+ return SkSpan<const TextBlock>(begin, end - begin + 1);
+}
+
+TextLine& ParagraphImpl::addLine(SkVector offset,
+ SkVector advance,
+ SkSpan<const char> text,
+ SkSpan<const char> textWithSpaces,
+ SkSpan<const Cluster> clusters,
+ size_t start,
+ size_t end,
+ LineMetrics sizes) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Define a list of styles that covers the line
+ auto blocks = findAllBlocks(text);
+
+ return fLines.emplace_back(offset, advance, blocks, text, textWithSpaces, clusters, start, end,
+ sizes);
+}
+
+// Returns a vector of bounding boxes that enclose all text between
+// start and end glyph indexes, including start and excluding end
+std::vector<TextBox> ParagraphImpl::getRectsForRange(unsigned start,
+ unsigned end,
+ RectHeightStyle rectHeightStyle,
+ RectWidthStyle rectWidthStyle) {
+ std::vector<TextBox> results;
+ if (start >= end || start > fTextSpan.size() || end == 0) {
+ return results;
+ }
+
+ // Calculate the utf8 substring
+ const char* first = fTextSpan.begin();
+ for (unsigned i = 0; i < start; ++i) {
+ utf8_next(&first, fTextSpan.end());
+ }
+ const char* last = first;
+ for (unsigned i = start; i < end; ++i) {
+ utf8_next(&last, fTextSpan.end());
+ }
+ SkSpan<const char> text(first, last - first);
+
+ for (auto& line : fLines) {
+ auto lineText = line.textWithSpaces();
+ auto intersect = lineText * text;
+ if (intersect.empty() && (!lineText.empty() || lineText.begin() != text.begin())) {
+ continue;
+ }
+
+ SkScalar runOffset = 0;
+ if (lineText.begin() != intersect.begin()) {
+ SkSpan<const char> before(lineText.begin(), intersect.begin() - lineText.begin());
+ runOffset = line.iterateThroughRuns(
+ before, 0, true,
+ [](Run*, size_t, size_t, SkRect, SkScalar, bool) { return true; });
+ }
+ auto firstBox = results.size();
+ line.iterateThroughRuns(intersect,
+ runOffset,
+ true,
+ [&results, &line](Run* run, size_t pos, size_t size, SkRect clip,
+ SkScalar shift, bool clippingNeeded) {
+ clip.offset(line.offset());
+ results.emplace_back(clip, run->leftToRight()
+ ? TextDirection::kLtr
+ : TextDirection::kRtl);
+ return true;
+ });
+
+ if (rectHeightStyle != RectHeightStyle::kTight) {
+ // Align all the rectangles
+ for (auto i = firstBox; i < results.size(); ++i) {
+ auto& rect = results[i].rect;
+ if (rectHeightStyle == RectHeightStyle::kMax) {
+ rect.fTop = line.offset().fY + line.roundingDelta();
+ rect.fBottom = line.offset().fY + line.height();
+
+ } else if (rectHeightStyle == RectHeightStyle::kIncludeLineSpacingTop) {
+ rect.fTop = line.offset().fY;
+
+ } else if (rectHeightStyle == RectHeightStyle::kIncludeLineSpacingMiddle) {
+ rect.fTop -= (rect.fTop - line.offset().fY) / 2;
+ rect.fBottom += (line.offset().fY + line.height() - rect.fBottom) / 2;
+
+ } else if (rectHeightStyle == RectHeightStyle::kIncludeLineSpacingBottom) {
+ rect.fBottom = line.offset().fY + line.height();
+ }
+ }
+ } else {
+ // Just leave the boxes the way they are
+ }
+
+ if (rectWidthStyle == RectWidthStyle::kMax) {
+ for (auto& i = firstBox; i < results.size(); ++i) {
+ auto clip = results[i].rect;
+ auto dir = results[i].direction;
+ if (clip.fLeft > line.offset().fX) {
+ SkRect left = SkRect::MakeXYWH(0, clip.fTop, clip.fLeft - line.offset().fX,
+ clip.fBottom);
+ results.insert(results.begin() + i, {left, dir});
+ ++i;
+ }
+ if (clip.fRight < line.offset().fX + line.width()) {
+ SkRect right = SkRect::MakeXYWH(clip.fRight - line.offset().fX,
+ clip.fTop,
+ line.width() - (clip.fRight - line.offset().fX),
+ clip.fBottom);
+ results.insert(results.begin() + i, {right, dir});
+ ++i;
+ }
+ }
+ }
+ }
+
+ return results;
+}
+// TODO: Deal with RTL here
+PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) {
+ PositionWithAffinity result(0, Affinity::kDownstream);
+ for (auto& line : fLines) {
+ // This is so far the the line vertically closest to our coordinates
+ // (or the first one, or the only one - all the same)
+ line.iterateThroughRuns(
+ line.textWithSpaces(),
+ 0,
+ true,
+ [dx, &result](Run* run, size_t pos, size_t size, SkRect clip, SkScalar shift,
+ bool clippingNeeded) {
+ if (dx < clip.fLeft) {
+ // All the other runs are placed right of this one
+ result = {SkToS32(run->fClusterIndexes[pos]), kDownstream};
+ return false;
+ }
+
+ if (dx >= clip.fRight) {
+ // We have to keep looking but just in case keep the last one as the closes
+ // so far
+ result = {SkToS32(run->fClusterIndexes[pos + size]), kUpstream};
+ return true;
+ }
+
+ // So we found the run that contains our coordinates
+ size_t found = pos;
+ for (size_t i = pos; i < pos + size; ++i) {
+ if (run->positionX(i) + shift > dx) {
+ break;
+ }
+ found = i;
+ }
+
+ if (found == pos) {
+ result = {SkToS32(run->fClusterIndexes[found]), kDownstream};
+ } else if (found == pos + size - 1) {
+ result = {SkToS32(run->fClusterIndexes[found]), kUpstream};
+ } else {
+ auto center = (run->positionX(found + 1) + run->positionX(found)) / 2;
+ if ((dx <= center + shift) == run->leftToRight()) {
+ result = {SkToS32(run->fClusterIndexes[found]), kDownstream};
+ } else {
+ result = {SkToS32(run->fClusterIndexes[found + 1]), kUpstream};
+ }
+ }
+ // No need to continue
+ return false;
+ });
+
+ // Let's figure out if we can stop looking
+ auto offsetY = line.offset().fY;
+ if (dy < offsetY) {
+ // The closest position on this line; next line is going to be even lower
+ break;
+ }
+ if (dy >= offsetY + line.height()) {
+ // We have the closest position on the lowest line so far, but we have to continue
+ continue;
+ }
+
+ // We hit the line; nothing else to do
+ break;
+ }
+
+ // SkDebugf("getGlyphPositionAtCoordinate(%f,%f) = %d\n", dx, dy, result.position);
+ return result;
+}
+
+// Finds the first and last glyphs that define a word containing
+// the glyph at index offset.
+// By "glyph" they mean a character index - indicated by Minikin's code
+SkRange<size_t> ParagraphImpl::getWordBoundary(unsigned offset) {
+ TextBreaker breaker;
+ if (!breaker.initialize(fTextSpan, UBRK_WORD)) {
+ return {0, 0};
+ }
+
+ size_t currentPos = breaker.first();
+ while (true) {
+ auto start = currentPos;
+ currentPos = breaker.next();
+ if (breaker.eof()) {
+ break;
+ }
+ if (start <= offset && currentPos > offset) {
+ return {start, currentPos};
+ }
+ }
+ return {0, 0};
+}
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/ParagraphImpl.h b/modules/skparagraph/src/ParagraphImpl.h
new file mode 100644
index 0000000..fca49ee
--- /dev/null
+++ b/modules/skparagraph/src/ParagraphImpl.h
@@ -0,0 +1,113 @@
+// Copyright 2019 Google LLC.
+#ifndef ParagraphImpl_DEFINED
+#define ParagraphImpl_DEFINED
+
+#include "modules/skparagraph/src/TextLine.h"
+#include "modules/skparagraph/src/Run.h"
+#include "include/core/SkPicture.h"
+#include "include/private//SkTHash.h"
+#include "modules/skparagraph/include/Paragraph.h"
+#include "modules/skparagraph/include/ParagraphStyle.h"
+#include "modules/skparagraph/include/TextStyle.h"
+
+class SkCanvas;
+
+namespace skia {
+namespace textlayout {
+
+template <typename T> bool operator==(const SkSpan<T>& a, const SkSpan<T>& b) {
+ return a.size() == b.size() && a.begin() == b.begin();
+}
+
+template <typename T> bool operator<=(const SkSpan<T>& a, const SkSpan<T>& b) {
+ return a.begin() >= b.begin() && a.end() <= b.end();
+}
+
+class ParagraphImpl final : public Paragraph {
+public:
+ ParagraphImpl(const SkString& text,
+ ParagraphStyle style,
+ std::vector<Block> blocks,
+ sk_sp<FontCollection> fonts)
+ : Paragraph(std::move(style), std::move(fonts))
+ , fText(text)
+ , fTextSpan(fText.c_str(), fText.size())
+ , fPicture(nullptr) {
+ fTextStyles.reserve(blocks.size());
+ for (auto& block : blocks) {
+ fTextStyles.emplace_back(
+ SkSpan<const char>(fTextSpan.begin() + block.fStart, block.fEnd - block.fStart),
+ block.fStyle);
+ }
+ }
+
+ ParagraphImpl(const std::u16string& utf16text,
+ ParagraphStyle style,
+ std::vector<Block> blocks,
+ sk_sp<FontCollection> fonts);
+ ~ParagraphImpl() override;
+
+ void layout(SkScalar width) override;
+ void paint(SkCanvas* canvas, SkScalar x, SkScalar y) override;
+ std::vector<TextBox> getRectsForRange(unsigned start,
+ unsigned end,
+ RectHeightStyle rectHeightStyle,
+ RectWidthStyle rectWidthStyle) override;
+ PositionWithAffinity getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) override;
+ SkRange<size_t> getWordBoundary(unsigned offset) override;
+ bool didExceedMaxLines() override {
+ return !fParagraphStyle.unlimited_lines() && fLines.size() > fParagraphStyle.getMaxLines();
+ }
+
+ size_t lineNumber() override { return fLines.size(); }
+
+ TextLine& addLine(SkVector offset, SkVector advance, SkSpan<const char> text,
+ SkSpan<const char> textWithSpaces, SkSpan<const Cluster> clusters,
+ size_t start, size_t end, LineMetrics sizes);
+
+ SkSpan<const char> text() const { return fTextSpan; }
+ SkSpan<Run> runs() { return SkSpan<Run>(fRuns.data(), fRuns.size()); }
+ SkSpan<TextBlock> styles() {
+ return SkSpan<TextBlock>(fTextStyles.data(), fTextStyles.size());
+ }
+ SkSpan<TextLine> lines() { return SkSpan<TextLine>(fLines.data(), fLines.size()); }
+ ParagraphStyle paragraphStyle() const { return fParagraphStyle; }
+ SkSpan<Cluster> clusters() { return SkSpan<Cluster>(fClusters.begin(), fClusters.size()); }
+ void formatLines(SkScalar maxWidth);
+
+ bool strutEnabled() const { return paragraphStyle().getStrutStyle().getStrutEnabled(); }
+ bool strutForceHeight() const {
+ return paragraphStyle().getStrutStyle().getForceStrutHeight();
+ }
+ LineMetrics strutMetrics() const { return fStrutMetrics; }
+
+private:
+ friend class ParagraphBuilder;
+
+ void resetContext();
+ void resolveStrut();
+ void buildClusterTable();
+ bool shapeTextIntoEndlessLine();
+ void breakShapedTextIntoLines(SkScalar maxWidth);
+ void paintLinesIntoPicture();
+
+ SkSpan<const TextBlock> findAllBlocks(SkSpan<const char> text);
+
+ // Input
+ SkTArray<TextBlock, true> fTextStyles;
+ SkString fText;
+ SkSpan<const char> fTextSpan;
+
+ // Internal structures
+ SkTArray<Run> fRuns;
+ SkTArray<Cluster, true> fClusters;
+ SkTArray<TextLine> fLines;
+ LineMetrics fStrutMetrics;
+
+ // Painting
+ sk_sp<SkPicture> fPicture;
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // ParagraphImpl_DEFINED
diff --git a/modules/skparagraph/src/ParagraphStyle.cpp b/modules/skparagraph/src/ParagraphStyle.cpp
new file mode 100644
index 0000000..c2f238a
--- /dev/null
+++ b/modules/skparagraph/src/ParagraphStyle.cpp
@@ -0,0 +1,44 @@
+// Copyright 2019 Google LLC.
+#include <string>
+#include "modules/skparagraph/include/ParagraphStyle.h"
+#include "unicode/unistr.h"
+
+namespace skia {
+namespace textlayout {
+
+StrutStyle::StrutStyle() {
+ fFontStyle = SkFontStyle::Normal();
+ fFontSize = 14;
+ fHeight = 1;
+ fLeading = -1;
+ fForceStrutHeight = false;
+ fStrutEnabled = false;
+}
+
+ParagraphStyle::ParagraphStyle() {
+ fTextAlign = TextAlign::kStart;
+ fTextDirection = TextDirection::kLtr;
+ fLinesLimit = std::numeric_limits<size_t>::max();
+ fHeight = 1;
+ fHintingIsOn = true;
+}
+
+TextAlign ParagraphStyle::effective_align() const {
+ if (fTextAlign == TextAlign::kStart) {
+ return (fTextDirection == TextDirection::kLtr) ? TextAlign::kLeft : TextAlign::kRight;
+ } else if (fTextAlign == TextAlign::kEnd) {
+ return (fTextDirection == TextDirection::kLtr) ? TextAlign::kRight : TextAlign::kLeft;
+ } else {
+ return fTextAlign;
+ }
+}
+
+void ParagraphStyle::setEllipsis(const std::u16string& ellipsis) {
+ icu::UnicodeString unicode;
+ unicode.setTo((UChar*)ellipsis.data());
+ std::string str;
+ unicode.toUTF8String(str);
+ fEllipsis = SkString(str.c_str());
+}
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/Run.cpp b/modules/skparagraph/src/Run.cpp
new file mode 100644
index 0000000..1773ec5
--- /dev/null
+++ b/modules/skparagraph/src/Run.cpp
@@ -0,0 +1,246 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/Run.h"
+#include <unicode/brkiter.h>
+#include "include/core/SkFontMetrics.h"
+
+namespace skia {
+namespace textlayout {
+
+Run::Run(SkSpan<const char> text,
+ const SkShaper::RunHandler::RunInfo& info,
+ SkScalar lineHeight,
+ size_t index,
+ SkScalar offsetX) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ fFont = info.fFont;
+ fHeightMultiplier = lineHeight;
+ fBidiLevel = info.fBidiLevel;
+ fAdvance = info.fAdvance;
+ fText = SkSpan<const char>(text.begin() + info.utf8Range.begin(), info.utf8Range.size());
+
+ fIndex = index;
+ fUtf8Range = info.utf8Range;
+ fOffset = SkVector::Make(offsetX, 0);
+ fGlyphs.push_back_n(info.glyphCount);
+ fPositions.push_back_n(info.glyphCount + 1);
+ fOffsets.push_back_n(info.glyphCount + 1, SkScalar(0));
+ fClusterIndexes.push_back_n(info.glyphCount + 1);
+ info.fFont.getMetrics(&fFontMetrics);
+ fSpaced = false;
+ // To make edge cases easier:
+ fPositions[info.glyphCount] = fOffset + fAdvance;
+ fClusterIndexes[info.glyphCount] = info.utf8Range.end();
+}
+
+SkShaper::RunHandler::Buffer Run::newRunBuffer() {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ return {fGlyphs.data(), fPositions.data(), nullptr, fClusterIndexes.data(), fOffset};
+}
+
+SkScalar Run::calculateWidth(size_t start, size_t end, bool clip) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkASSERT(start <= end);
+ // clip |= end == size(); // Clip at the end of the run?
+ SkScalar offset = 0;
+ if (fSpaced && end > start) {
+ offset = fOffsets[clip ? end - 1 : end] - fOffsets[start];
+ }
+ return fPositions[end].fX - fPositions[start].fX + offset;
+}
+
+void Run::copyTo(SkTextBlobBuilder& builder, size_t pos, size_t size, SkVector offset) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkASSERT(pos + size <= this->size());
+ const auto& blobBuffer = builder.allocRunPos(fFont, SkToInt(size));
+ sk_careful_memcpy(blobBuffer.glyphs, fGlyphs.data() + pos, size * sizeof(SkGlyphID));
+
+ if (fSpaced || offset.fX != 0 || offset.fY != 0) {
+ for (size_t i = 0; i < size; ++i) {
+ auto point = fPositions[i + pos];
+ if (fSpaced) {
+ point.fX += fOffsets[i + pos];
+ }
+ blobBuffer.points()[i] = point + offset;
+ }
+ } else {
+ // Good for the first line
+ sk_careful_memcpy(blobBuffer.points(), fPositions.data() + pos, size * sizeof(SkPoint));
+ }
+}
+
+// TODO: Make the search more effective
+std::tuple<bool, Cluster*, Cluster*> Run::findLimitingClusters(SkSpan<const char> text) {
+ if (text.empty()) {
+ Cluster* found = nullptr;
+ for (auto& cluster : fClusters) {
+ if (cluster.contains(text.begin())) {
+ found = &cluster;
+ break;
+ }
+ }
+ return std::make_tuple(found != nullptr, found, found);
+ }
+
+ auto first = text.begin();
+ auto last = text.end() - 1;
+
+ Cluster* start = nullptr;
+ Cluster* end = nullptr;
+ for (auto& cluster : fClusters) {
+ if (cluster.contains(first)) start = &cluster;
+ if (cluster.contains(last)) end = &cluster;
+ }
+ if (!leftToRight()) {
+ std::swap(start, end);
+ }
+
+ return std::make_tuple(start != nullptr && end != nullptr, start, end);
+}
+
+void Run::iterateThroughClustersInTextOrder(const ClusterVisitor& visitor) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Can't figure out how to do it with one code for both cases without 100 ifs
+ // Can't go through clusters because there are no cluster table yet
+ if (leftToRight()) {
+ size_t start = 0;
+ size_t cluster = this->clusterIndex(start);
+ for (size_t glyph = 1; glyph <= this->size(); ++glyph) {
+ auto nextCluster = this->clusterIndex(glyph);
+ if (nextCluster == cluster) {
+ continue;
+ }
+
+ visitor(start,
+ glyph,
+ cluster,
+ nextCluster,
+ this->calculateWidth(start, glyph, glyph == size()),
+ this->calculateHeight());
+
+ start = glyph;
+ cluster = nextCluster;
+ }
+ } else {
+ size_t glyph = this->size();
+ size_t cluster = this->fUtf8Range.begin();
+ for (int32_t start = this->size() - 1; start >= 0; --start) {
+ size_t nextCluster =
+ start == 0 ? this->fUtf8Range.end() : this->clusterIndex(start - 1);
+ if (nextCluster == cluster) {
+ continue;
+ }
+
+ visitor(start,
+ glyph,
+ cluster,
+ nextCluster,
+ this->calculateWidth(start, glyph, glyph == 0),
+ this->calculateHeight());
+
+ glyph = start;
+ cluster = nextCluster;
+ }
+ }
+}
+
+SkScalar Run::addSpacesAtTheEnd(SkScalar space, Cluster* cluster) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (cluster->endPos() == cluster->startPos()) {
+ return 0;
+ }
+
+ fOffsets[cluster->endPos() - 1] += space;
+ // Increment the run width
+ fSpaced = true;
+ fAdvance.fX += space;
+ // Increment the cluster width
+ cluster->space(space, space);
+
+ return space;
+}
+
+SkScalar Run::addSpacesEvenly(SkScalar space, Cluster* cluster) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Offset all the glyphs in the cluster
+ SkScalar shift = 0;
+ for (size_t i = cluster->startPos(); i < cluster->endPos(); ++i) {
+ fOffsets[i] += shift;
+ shift += space;
+ }
+ if (this->size() == cluster->endPos()) {
+ // To make calculations easier
+ fOffsets[cluster->endPos()] += shift;
+ }
+ // Increment the run width
+ fSpaced = true;
+ fAdvance.fX += shift;
+ // Increment the cluster width
+ cluster->space(shift, space);
+
+ return shift;
+}
+
+void Run::shift(const Cluster* cluster, SkScalar offset) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (offset == 0) {
+ return;
+ }
+
+ fSpaced = true;
+ for (size_t i = cluster->startPos(); i < cluster->endPos(); ++i) {
+ fOffsets[i] += offset;
+ }
+ if (this->size() == cluster->endPos()) {
+ // To make calculations easier
+ fOffsets[cluster->endPos()] += offset;
+ }
+}
+
+void Cluster::setIsWhiteSpaces() {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ auto pos = fText.end();
+ while (--pos >= fText.begin()) {
+ auto ch = *pos;
+ if (!u_isspace(ch) && u_charType(ch) != U_CONTROL_CHAR &&
+ u_charType(ch) != U_NON_SPACING_MARK) {
+ return;
+ }
+ }
+ fWhiteSpaces = true;
+}
+
+SkScalar Cluster::sizeToChar(const char* ch) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (ch < fText.begin() || ch >= fText.end()) {
+ return 0;
+ }
+ auto shift = ch - fText.begin();
+ auto ratio = shift * 1.0 / fText.size();
+
+ return SkDoubleToScalar(fWidth * ratio);
+}
+
+SkScalar Cluster::sizeFromChar(const char* ch) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (ch < fText.begin() || ch >= fText.end()) {
+ return 0;
+ }
+ auto shift = fText.end() - ch - 1;
+ auto ratio = shift * 1.0 / fText.size();
+
+ return SkDoubleToScalar(fWidth * ratio);
+}
+
+size_t Cluster::roundPos(SkScalar s) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ auto ratio = (s * 1.0) / fWidth;
+ return sk_double_floor2int(ratio * size());
+}
+
+SkScalar Cluster::trimmedWidth(size_t pos) const {
+ // Find the width until the pos and return the min between trimmedWidth and the width(pos)
+ return SkTMin(this->run()->positionX(pos) - this->run()->positionX(fStart), fWidth - fSpacing);
+}
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/Run.h b/modules/skparagraph/src/Run.h
new file mode 100644
index 0000000..fcaeeda
--- /dev/null
+++ b/modules/skparagraph/src/Run.h
@@ -0,0 +1,269 @@
+// Copyright 2019 Google LLC.
+#ifndef Run_DEFINED
+#define Run_DEFINED
+
+#include "include/core/SkFontMetrics.h"
+#include "include/core/SkPoint.h"
+#include "include/core/SkTextBlob.h"
+#include "modules/skshaper/include/SkShaper.h"
+#include "src/core/SkSpan.h"
+#include "src/core/SkTraceEvent.h"
+
+namespace skia {
+namespace textlayout {
+
+class Cluster;
+class Run {
+public:
+ Run() = default;
+ Run(SkSpan<const char> text,
+ const SkShaper::RunHandler::RunInfo& info,
+ SkScalar lineHeight,
+ size_t index,
+ SkScalar shiftX);
+ ~Run() {}
+
+ SkShaper::RunHandler::Buffer newRunBuffer();
+
+ size_t size() const { return fGlyphs.size(); }
+ void setWidth(SkScalar width) { fAdvance.fX = width; }
+ void setHeight(SkScalar height) { fAdvance.fY = height; }
+ void shift(SkScalar shiftX, SkScalar shiftY) {
+ fOffset.fX += shiftX;
+ fOffset.fY += shiftY;
+ }
+ SkVector advance() const {
+ return SkVector::Make(fAdvance.fX, fFontMetrics.fDescent - fFontMetrics.fAscent);
+ }
+ SkVector offset() const { return fOffset; }
+ SkScalar ascent() const { return fFontMetrics.fAscent; }
+ SkScalar descent() const { return fFontMetrics.fDescent; }
+ SkScalar leading() const { return fFontMetrics.fLeading; }
+ const SkFont& font() const { return fFont; }
+ bool leftToRight() const { return fBidiLevel % 2 == 0; }
+ size_t index() const { return fIndex; }
+ SkScalar lineHeight() const { return fHeightMultiplier; }
+ SkSpan<const char> text() const { return fText; }
+ size_t clusterIndex(size_t pos) const { return fClusterIndexes[pos]; }
+ SkScalar positionX(size_t pos) const { return fPositions[pos].fX + fOffsets[pos]; }
+ SkScalar offset(size_t index) const { return fOffsets[index]; }
+ SkSpan<Cluster> clusters() const { return fClusters; }
+ void setClusters(SkSpan<Cluster> clusters) { fClusters = clusters; }
+ SkRect clip() const {
+ return SkRect::MakeXYWH(fOffset.fX, fOffset.fY, fAdvance.fX, fAdvance.fY);
+ }
+
+ SkScalar addSpacesAtTheEnd(SkScalar space, Cluster* cluster);
+ SkScalar addSpacesEvenly(SkScalar space, Cluster* cluster);
+ void shift(const Cluster* cluster, SkScalar offset);
+
+ SkScalar calculateHeight() const { return fFontMetrics.fDescent - fFontMetrics.fAscent; }
+ SkScalar calculateWidth(size_t start, size_t end, bool clip) const;
+
+ void copyTo(SkTextBlobBuilder& builder, size_t pos, size_t size, SkVector offset) const;
+
+ using ClusterVisitor = std::function<void(size_t glyphStart,
+ size_t glyphEnd,
+ size_t charStart,
+ size_t charEnd,
+ SkScalar width,
+ SkScalar height)>;
+ void iterateThroughClustersInTextOrder(const ClusterVisitor& visitor);
+
+ std::tuple<bool, Cluster*, Cluster*> findLimitingClusters(SkSpan<const char> text);
+ SkSpan<const SkGlyphID> glyphs() {
+ return SkSpan<const SkGlyphID>(fGlyphs.begin(), fGlyphs.size());
+ }
+ SkSpan<const SkPoint> positions() {
+ return SkSpan<const SkPoint>(fPositions.begin(), fPositions.size());
+ }
+ SkSpan<const uint32_t> clusterIndexes() {
+ return SkSpan<const uint32_t>(fClusterIndexes.begin(), fClusterIndexes.size());
+ }
+
+private:
+ friend class ParagraphImpl;
+ friend class TextLine;
+ friend class LineMetrics;
+
+ SkFont fFont;
+ SkFontMetrics fFontMetrics;
+ SkScalar fHeightMultiplier;
+ size_t fIndex;
+ uint8_t fBidiLevel;
+ SkVector fAdvance;
+ SkSpan<const char> fText;
+ SkSpan<Cluster> fClusters;
+ SkVector fOffset;
+ SkShaper::RunHandler::Range fUtf8Range;
+ SkSTArray<128, SkGlyphID, false> fGlyphs;
+ SkSTArray<128, SkPoint, true> fPositions;
+ SkSTArray<128, uint32_t, true> fClusterIndexes;
+ SkSTArray<128, SkScalar, true> fOffsets; // For formatting (letter/word spacing, justification)
+ bool fSpaced;
+};
+
+class Cluster {
+public:
+ enum BreakType {
+ None,
+ CharacterBoundary, // not yet in use (UBRK_CHARACTER)
+ WordBoundary, // calculated for all clusters (UBRK_WORD)
+ WordBreakWithoutHyphen, // calculated only for hyphenated words
+ WordBreakWithHyphen,
+ SoftLineBreak, // calculated for all clusters (UBRK_LINE)
+ HardLineBreak, // calculated for all clusters (UBRK_LINE)
+ };
+
+ Cluster()
+ : fText(nullptr, 0)
+ , fRun(nullptr)
+ , fStart(0)
+ , fEnd()
+ , fWidth()
+ , fSpacing(0)
+ , fHeight()
+ , fWhiteSpaces(false)
+ , fBreakType(None) {}
+
+ Cluster(Run* run,
+ size_t start,
+ size_t end,
+ SkSpan<const char>
+ text,
+ SkScalar width,
+ SkScalar height)
+ : fText(text)
+ , fRun(run)
+ , fStart(start)
+ , fEnd(end)
+ , fWidth(width)
+ , fSpacing(0)
+ , fHeight(height)
+ , fWhiteSpaces(false)
+ , fBreakType(None) {}
+
+ ~Cluster() = default;
+
+ SkScalar sizeToChar(const char* ch) const;
+ SkScalar sizeFromChar(const char* ch) const;
+
+ size_t roundPos(SkScalar s) const;
+
+ void space(SkScalar shift, SkScalar space) {
+ fSpacing += space;
+ fWidth += shift;
+ }
+
+ void setBreakType(BreakType type) { fBreakType = type; }
+ void setIsWhiteSpaces(bool ws) { fWhiteSpaces = ws; }
+ bool isWhitespaces() const { return fWhiteSpaces; }
+ bool canBreakLineAfter() const {
+ return fBreakType == SoftLineBreak || fBreakType == HardLineBreak;
+ }
+ bool isHardBreak() const { return fBreakType == HardLineBreak; }
+ bool isSoftBreak() const { return fBreakType == SoftLineBreak; }
+ Run* run() const { return fRun; }
+ size_t startPos() const { return fStart; }
+ size_t endPos() const { return fEnd; }
+ SkScalar width() const { return fWidth; }
+ SkScalar trimmedWidth() const { return fWidth - fSpacing; }
+ SkScalar lastSpacing() const { return fSpacing; }
+ SkScalar height() const { return fHeight; }
+ SkSpan<const char> text() const { return fText; }
+ size_t size() const { return fEnd - fStart; }
+
+ SkScalar trimmedWidth(size_t pos) const;
+
+ void shift(SkScalar offset) const { this->run()->shift(this, offset); }
+
+ void setIsWhiteSpaces();
+
+ bool contains(const char* ch) const { return ch >= fText.begin() && ch < fText.end(); }
+
+ bool belongs(SkSpan<const char> text) const {
+ return fText.begin() >= text.begin() && fText.end() <= text.end();
+ }
+
+ bool startsIn(SkSpan<const char> text) const {
+ return fText.begin() >= text.begin() && fText.begin() < text.end();
+ }
+
+private:
+ SkSpan<const char> fText;
+
+ Run* fRun;
+ size_t fStart;
+ size_t fEnd;
+ SkScalar fWidth;
+ SkScalar fSpacing;
+ SkScalar fHeight;
+ bool fWhiteSpaces;
+ BreakType fBreakType;
+};
+
+class LineMetrics {
+public:
+ LineMetrics() { clean(); }
+
+ LineMetrics(SkScalar a, SkScalar d, SkScalar l) {
+ fAscent = a;
+ fDescent = d;
+ fLeading = l;
+ }
+
+ LineMetrics(const SkFontMetrics& fm) {
+ fAscent = fm.fAscent;
+ fDescent = fm.fDescent;
+ fLeading = fm.fLeading;
+ }
+
+ void add(Run* run) {
+ fAscent = SkTMin(fAscent, run->ascent() * run->lineHeight());
+ fDescent = SkTMax(fDescent, run->descent() * run->lineHeight());
+ fLeading = SkTMax(fLeading, run->leading() * run->lineHeight());
+ }
+
+ void add(LineMetrics other) {
+ fAscent = SkTMin(fAscent, other.fAscent);
+ fDescent = SkTMax(fDescent, other.fDescent);
+ fLeading = SkTMax(fLeading, other.fLeading);
+ }
+ void clean() {
+ fAscent = 0;
+ fDescent = 0;
+ fLeading = 0;
+ }
+
+ SkScalar delta() const { return height() - ideographicBaseline(); }
+
+ void updateLineMetrics(LineMetrics& metrics, bool forceHeight) {
+ if (forceHeight) {
+ metrics.fAscent = fAscent;
+ metrics.fDescent = fDescent;
+ metrics.fLeading = fLeading;
+ } else {
+ metrics.fAscent = SkTMin(metrics.fAscent, fAscent);
+ metrics.fDescent = SkTMax(metrics.fDescent, fDescent);
+ metrics.fLeading = SkTMax(metrics.fLeading, fLeading);
+ }
+ }
+
+ SkScalar runTop(Run* run) const { return fLeading / 2 - fAscent + run->ascent() + delta(); }
+ SkScalar height() const { return SkScalarRoundToInt(fDescent - fAscent + fLeading); }
+ SkScalar alphabeticBaseline() const { return fLeading / 2 - fAscent; }
+ SkScalar ideographicBaseline() const { return fDescent - fAscent + fLeading; }
+ SkScalar baseline() const { return fLeading / 2 - fAscent; }
+ SkScalar ascent() const { return fAscent; }
+ SkScalar descent() const { return fDescent; }
+ SkScalar leading() const { return fLeading; }
+
+private:
+ SkScalar fAscent;
+ SkScalar fDescent;
+ SkScalar fLeading;
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // Run_DEFINED
diff --git a/modules/skparagraph/src/TextLine.cpp b/modules/skparagraph/src/TextLine.cpp
new file mode 100644
index 0000000..c389fcb
--- /dev/null
+++ b/modules/skparagraph/src/TextLine.cpp
@@ -0,0 +1,683 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/TextLine.h"
+#include <unicode/brkiter.h>
+#include <unicode/ubidi.h>
+#include "modules/skparagraph/src/ParagraphImpl.h"
+
+#include "include/core/SkMaskFilter.h"
+#include "include/effects/SkDashPathEffect.h"
+#include "include/effects/SkDiscretePathEffect.h"
+#include "src/core/SkMakeUnique.h"
+
+namespace {
+
+SkSpan<const char> intersected(const SkSpan<const char>& a, const SkSpan<const char>& b) {
+ auto begin = SkTMax(a.begin(), b.begin());
+ auto end = SkTMin(a.end(), b.end());
+ return SkSpan<const char>(begin, end > begin ? end - begin : 0);
+}
+
+int32_t intersectedSize(SkSpan<const char> a, SkSpan<const char> b) {
+ if (a.begin() == nullptr || b.begin() == nullptr) {
+ return -1;
+ }
+ auto begin = SkTMax(a.begin(), b.begin());
+ auto end = SkTMin(a.end(), b.end());
+ return SkToS32(end - begin);
+}
+} // namespace
+
+namespace skia {
+namespace textlayout {
+
+SkTHashMap<SkFont, Run> TextLine::fEllipsisCache;
+
+TextLine::TextLine(SkVector offset, SkVector advance, SkSpan<const TextBlock> blocks,
+ SkSpan<const char> text, SkSpan<const char> textWithSpaces,
+ SkSpan<const Cluster> clusters, size_t startPos, size_t endPos,
+ LineMetrics sizes)
+ : fBlocks(blocks)
+ , fText(text)
+ , fTextWithSpaces(textWithSpaces)
+ , fClusters(clusters)
+ //, fStartPos(startPos)
+ //, fEndPos(endPos)
+ , fLogical()
+ , fShift(0)
+ , fAdvance(advance)
+ , fOffset(offset)
+ , fEllipsis(nullptr)
+ , fSizes(sizes) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Reorder visual runs
+ auto start = fClusters.begin();
+ auto end = fClusters.end() - 1;
+ size_t numRuns = end->run()->index() - start->run()->index() + 1;
+
+ // Get the logical order
+ std::vector<UBiDiLevel> runLevels;
+ for (auto run = start->run(); run <= end->run(); ++run) {
+ runLevels.emplace_back(run->fBidiLevel);
+ }
+
+ std::vector<int32_t> logicalOrder(numRuns);
+ ubidi_reorderVisual(runLevels.data(), SkToU32(numRuns), logicalOrder.data());
+
+ auto firstRun = start->run();
+ for (auto index : logicalOrder) {
+ fLogical.push_back(firstRun + index);
+ }
+
+ // TODO: use fStartPos and fEndPos really
+ // SkASSERT(fStartPos <= start->run()->size());
+ // SkASSERT(fEndPos <= end->run()->size());
+}
+
+TextLine::TextLine(TextLine&& other) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ this->fBlocks = other.fBlocks;
+ this->fText = other.fText;
+ this->fTextWithSpaces = other.fTextWithSpaces;
+ this->fLogical.reset();
+ this->fLogical = std::move(other.fLogical);
+ this->fShift = other.fShift;
+ this->fAdvance = other.fAdvance;
+ this->fOffset = other.fOffset;
+ this->fEllipsis = std::move(other.fEllipsis);
+ this->fSizes = other.sizes();
+ this->fClusters = other.fClusters;
+}
+
+void TextLine::paint(SkCanvas* textCanvas) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (this->empty()) {
+ return;
+ }
+
+ textCanvas->save();
+ textCanvas->translate(this->offset().fX, this->offset().fY);
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kBackground,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintBackground(textCanvas, text, style, offsetX);
+ });
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kShadow,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintShadow(textCanvas, text, style, offsetX);
+ });
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kForeground,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintText(textCanvas, text, style, offsetX);
+ });
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kDecorations,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintDecorations(textCanvas, text, style, offsetX);
+ });
+
+ textCanvas->restore();
+}
+
+void TextLine::format(TextAlign effectiveAlign, SkScalar maxWidth, bool notLastLine) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkScalar delta = maxWidth - this->width();
+ if (delta <= 0) {
+ return;
+ }
+
+ if (effectiveAlign == TextAlign::kJustify && notLastLine) {
+ this->justify(maxWidth);
+ } else if (effectiveAlign == TextAlign::kRight) {
+ this->shiftTo(delta);
+ } else if (effectiveAlign == TextAlign::kCenter) {
+ this->shiftTo(delta / 2);
+ }
+}
+
+void TextLine::scanStyles(StyleType style, const StyleVisitor& visitor) {
+ if (this->empty()) {
+ return;
+ }
+
+ this->iterateThroughStylesInTextOrder(
+ style, [this, visitor](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ visitor(text, style, offsetX);
+ return this->iterateThroughRuns(
+ text, offsetX, false,
+ [](Run*, int32_t, size_t, SkRect, SkScalar, bool) { return true; });
+ });
+}
+
+void TextLine::scanRuns(const RunVisitor& visitor) {
+ this->iterateThroughRuns(
+ fText, 0, false,
+ [visitor](Run* run, int32_t pos, size_t size, SkRect clip, SkScalar sc, bool b) {
+ visitor(run, pos, size, clip, sc, b);
+ return true;
+ });
+}
+
+SkScalar TextLine::paintText(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkPaint paint;
+ if (style.hasForeground()) {
+ paint = style.getForeground();
+ } else {
+ paint.setColor(style.getColor());
+ }
+
+ auto shiftDown = this->baseline();
+ return this->iterateThroughRuns(
+ text, offsetX, false,
+ [paint, canvas, shiftDown](Run* run, int32_t pos, size_t size, SkRect clip,
+ SkScalar shift, bool clippingNeeded) {
+ SkTextBlobBuilder builder;
+ run->copyTo(builder, SkToU32(pos), size, SkVector::Make(0, shiftDown));
+ canvas->save();
+ if (clippingNeeded) {
+ canvas->clipRect(clip);
+ }
+ canvas->translate(shift, 0);
+ canvas->drawTextBlob(builder.make(), 0, 0, paint);
+ canvas->restore();
+ return true;
+ });
+}
+
+SkScalar TextLine::paintBackground(SkCanvas* canvas, SkSpan<const char> text,
+ const TextStyle& style, SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ return this->iterateThroughRuns(text, offsetX, false,
+ [canvas, style](Run* run, int32_t pos, size_t size, SkRect clip,
+ SkScalar shift, bool clippingNeeded) {
+ if (style.hasBackground()) {
+ canvas->drawRect(clip, style.getBackground());
+ }
+ return true;
+ });
+}
+
+SkScalar TextLine::paintShadow(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (style.getShadowNumber() == 0) {
+ // Still need to calculate text advance
+ return iterateThroughRuns(
+ text, offsetX, false,
+ [](Run*, int32_t, size_t, SkRect, SkScalar, bool) { return true; });
+ }
+
+ SkScalar result = 0;
+ for (TextShadow shadow : style.getShadows()) {
+ if (!shadow.hasShadow()) continue;
+
+ SkPaint paint;
+ paint.setColor(shadow.fColor);
+ if (shadow.fBlurRadius != 0.0) {
+ auto filter = SkMaskFilter::MakeBlur(kNormal_SkBlurStyle,
+ SkDoubleToScalar(shadow.fBlurRadius), false);
+ paint.setMaskFilter(filter);
+ }
+
+ auto shiftDown = this->baseline();
+ result = this->iterateThroughRuns(
+ text, offsetX, false,
+ [canvas, shadow, paint, shiftDown](Run* run, size_t pos, size_t size, SkRect clip,
+ SkScalar shift, bool clippingNeeded) {
+ SkTextBlobBuilder builder;
+ run->copyTo(builder, pos, size, SkVector::Make(0, shiftDown));
+ canvas->save();
+ clip.offset(shadow.fOffset);
+ if (clippingNeeded) {
+ canvas->clipRect(clip);
+ }
+ canvas->translate(shift, 0);
+ canvas->drawTextBlob(builder.make(), shadow.fOffset.x(), shadow.fOffset.y(),
+ paint);
+ canvas->restore();
+ return true;
+ });
+ }
+
+ return result;
+}
+
+SkScalar TextLine::paintDecorations(SkCanvas* canvas, SkSpan<const char> text,
+ const TextStyle& style, SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ return this->iterateThroughRuns(
+ text, offsetX, false,
+ [this, canvas, style](Run* run, int32_t pos, size_t size, SkRect clip, SkScalar shift,
+ bool clippingNeeded) {
+ if (style.getDecoration() == TextDecoration::kNoDecoration) {
+ return true;
+ }
+
+ for (auto decoration : AllTextDecorations) {
+ if (style.getDecoration() && decoration == 0) {
+ continue;
+ }
+
+ SkScalar thickness = style.getDecorationThicknessMultiplier();
+ //
+ SkScalar position = 0;
+ switch (style.getDecoration()) {
+ case TextDecoration::kUnderline:
+ position = -run->ascent() + thickness;
+ break;
+ case TextDecoration::kOverline:
+ position = 0;
+ break;
+ case TextDecoration::kLineThrough: {
+ position = (run->descent() - run->ascent() - thickness) / 2;
+ break;
+ }
+ default:
+ // TODO: can we actually get here?
+ break;
+ }
+
+ auto width = clip.width();
+ SkScalar x = clip.left();
+ SkScalar y = clip.top() + position;
+
+ // Decoration paint (for now) and/or path
+ SkPaint paint;
+ SkPath path;
+ this->computeDecorationPaint(paint, clip, style, path);
+ paint.setStrokeWidth(thickness);
+
+ switch (style.getDecorationStyle()) {
+ case TextDecorationStyle::kWavy:
+ path.offset(x, y);
+ canvas->drawPath(path, paint);
+ break;
+ case TextDecorationStyle::kDouble: {
+ canvas->drawLine(x, y, x + width, y, paint);
+ SkScalar bottom = y + thickness * 2;
+ canvas->drawLine(x, bottom, x + width, bottom, paint);
+ break;
+ }
+ case TextDecorationStyle::kDashed:
+ case TextDecorationStyle::kDotted:
+ case TextDecorationStyle::kSolid:
+ canvas->drawLine(x, y, x + width, y, paint);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return true;
+ });
+}
+
+void TextLine::computeDecorationPaint(SkPaint& paint,
+ SkRect clip,
+ const TextStyle& style,
+ SkPath& path) const {
+ paint.setStyle(SkPaint::kStroke_Style);
+ if (style.getDecorationColor() == SK_ColorTRANSPARENT) {
+ paint.setColor(style.getColor());
+ } else {
+ paint.setColor(style.getDecorationColor());
+ }
+
+ SkScalar scaleFactor = style.getFontSize() / 14.f;
+
+ switch (style.getDecorationStyle()) {
+ case TextDecorationStyle::kSolid:
+ break;
+
+ case TextDecorationStyle::kDouble:
+ break;
+
+ // Note: the intervals are scaled by the thickness of the line, so it is
+ // possible to change spacing by changing the decoration_thickness
+ // property of TextStyle.
+ case TextDecorationStyle::kDotted: {
+ const SkScalar intervals[] = {1.0f * scaleFactor, 1.5f * scaleFactor,
+ 1.0f * scaleFactor, 1.5f * scaleFactor};
+ size_t count = sizeof(intervals) / sizeof(intervals[0]);
+ paint.setPathEffect(SkPathEffect::MakeCompose(
+ SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
+ SkDiscretePathEffect::Make(0, 0)));
+ break;
+ }
+ // Note: the intervals are scaled by the thickness of the line, so it is
+ // possible to change spacing by changing the decoration_thickness
+ // property of TextStyle.
+ case TextDecorationStyle::kDashed: {
+ const SkScalar intervals[] = {4.0f * scaleFactor, 2.0f * scaleFactor,
+ 4.0f * scaleFactor, 2.0f * scaleFactor};
+ size_t count = sizeof(intervals) / sizeof(intervals[0]);
+ paint.setPathEffect(SkPathEffect::MakeCompose(
+ SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
+ SkDiscretePathEffect::Make(0, 0)));
+ break;
+ }
+ case TextDecorationStyle::kWavy: {
+ int wave_count = 0;
+ SkScalar x_start = 0;
+ SkScalar wavelength = scaleFactor * style.getDecorationThicknessMultiplier();
+ auto width = clip.width();
+ path.moveTo(0, 0);
+ while (x_start + wavelength * 2 < width) {
+ path.rQuadTo(wavelength,
+ wave_count % 2 != 0 ? wavelength : -wavelength,
+ wavelength * 2,
+ 0);
+ x_start += wavelength * 2;
+ ++wave_count;
+ }
+ break;
+ }
+ }
+}
+
+void TextLine::justify(SkScalar maxWidth) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Count words and the extra spaces to spread across the line
+ // TODO: do it at the line breaking?..
+ size_t whitespacePatches = 0;
+ SkScalar textLen = 0;
+ bool whitespacePatch = false;
+ this->iterateThroughClustersInGlyphsOrder(
+ false, [&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster) {
+ if (cluster->isWhitespaces()) {
+ if (!whitespacePatch) {
+ whitespacePatch = true;
+ ++whitespacePatches;
+ }
+ } else {
+ whitespacePatch = false;
+ }
+ textLen += cluster->width();
+ return true;
+ });
+
+ if (whitespacePatches == 0) {
+ this->fShift = 0;
+ return;
+ }
+
+ SkScalar step = (maxWidth - textLen) / whitespacePatches;
+ SkScalar shift = 0;
+
+ // Spread the extra whitespaces
+ whitespacePatch = false;
+ this->iterateThroughClustersInGlyphsOrder(false, [&](const Cluster* cluster) {
+ if (cluster->isWhitespaces()) {
+ if (!whitespacePatch) {
+ shift += step;
+ whitespacePatch = true;
+ --whitespacePatches;
+ }
+ } else {
+ whitespacePatch = false;
+ }
+ cluster->shift(shift);
+ return true;
+ });
+
+ SkAssertResult(SkScalarNearlyEqual(shift, maxWidth - textLen));
+ SkASSERT(whitespacePatches == 0);
+ this->fShift = 0;
+ this->fAdvance.fX = maxWidth;
+}
+
+void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Replace some clusters with the ellipsis
+ // Go through the clusters in the reverse logical order
+ // taking off cluster by cluster until the ellipsis fits
+ SkScalar width = fAdvance.fX;
+ iterateThroughClustersInGlyphsOrder(
+ true, [this, &width, ellipsis, maxWidth](const Cluster* cluster) {
+ if (cluster->isWhitespaces()) {
+ width -= cluster->width();
+ return true;
+ }
+
+ // Shape the ellipsis
+ Run* cached = fEllipsisCache.find(cluster->run()->font());
+ if (cached == nullptr) {
+ cached = shapeEllipsis(ellipsis, cluster->run());
+ }
+ fEllipsis = skstd::make_unique<Run>(*cached);
+
+ // See if it fits
+ if (width + fEllipsis->advance().fX > maxWidth) {
+ width -= cluster->width();
+ // Continue if it's not
+ return true;
+ }
+
+ fEllipsis->shift(width, 0);
+ fAdvance.fX = width;
+ return false;
+ });
+}
+
+Run* TextLine::shapeEllipsis(const SkString& ellipsis, Run* run) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ class ShapeHandler final : public SkShaper::RunHandler {
+ public:
+ explicit ShapeHandler(SkScalar lineHeight) : fRun(nullptr), fLineHeight(lineHeight) {}
+ Run* run() { return fRun; }
+
+ private:
+ void beginLine() override {}
+
+ void runInfo(const RunInfo&) override {}
+
+ void commitRunInfo() override {}
+
+ Buffer runBuffer(const RunInfo& info) override {
+ fRun = fEllipsisCache.set(info.fFont,
+ Run(SkSpan<const char>(), info, fLineHeight, 0, 0));
+ return fRun->newRunBuffer();
+ }
+
+ void commitRunBuffer(const RunInfo& info) override {
+ fRun->fAdvance.fX = info.fAdvance.fX;
+ fRun->fAdvance.fY = fRun->descent() - fRun->ascent();
+ }
+
+ void commitLine() override {}
+
+ Run* fRun;
+ SkScalar fLineHeight;
+ };
+
+ ShapeHandler handler(run->lineHeight());
+ std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder();
+ SkASSERT_RELEASE(shaper != nullptr);
+ shaper->shape(ellipsis.c_str(), ellipsis.size(), run->font(), true,
+ std::numeric_limits<SkScalar>::max(), &handler);
+ handler.run()->fText = SkSpan<const char>(ellipsis.c_str(), ellipsis.size());
+ return handler.run();
+}
+
+SkRect TextLine::measureTextInsideOneRun(
+ SkSpan<const char> text, Run* run, size_t& pos, size_t& size, bool& clippingNeeded) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkASSERT(intersectedSize(run->text(), text) >= 0);
+
+ // Find [start:end] clusters for the text
+ bool found;
+ Cluster* start;
+ Cluster* end;
+ std::tie(found, start, end) = run->findLimitingClusters(text);
+ if (!found) {
+ SkASSERT(text.empty());
+ return SkRect::MakeEmpty();
+ }
+
+ pos = start->startPos();
+ size = end->endPos() - start->startPos();
+
+ // Calculate the clipping rectangle for the text with cluster edges
+ // There are 2 cases:
+ // EOL (when we expect the last cluster clipped without any spaces)
+ // Anything else (when we want the cluster width contain all the spaces -
+ // coming from letter spacing or word spacing or justification)
+ bool needsClipping = (run->leftToRight() ? end : start) == clusters().end() - 1;
+ SkRect clip =
+ SkRect::MakeXYWH(run->positionX(start->startPos()) - run->positionX(0),
+ sizes().runTop(run),
+ run->calculateWidth(start->startPos(), end->endPos(), needsClipping),
+ run->calculateHeight());
+
+ // Correct the width in case the text edges don't match clusters
+ // TODO: This is where we get smart about selecting a part of a cluster
+ // by shaping each grapheme separately and then use the result sizes
+ // to calculate the proportions
+ auto leftCorrection = start->sizeToChar(text.begin());
+ auto rightCorrection = end->sizeFromChar(text.end() - 1);
+ clip.fLeft += leftCorrection;
+ clip.fRight -= rightCorrection;
+ clippingNeeded = leftCorrection != 0 || rightCorrection != 0;
+
+ // SkDebugf("measureTextInsideOneRun: '%s'[%d:%d]\n", text.begin(), pos, pos + size);
+
+ return clip;
+}
+
+void TextLine::iterateThroughClustersInGlyphsOrder(bool reverse,
+ const ClustersVisitor& visitor) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ for (size_t r = 0; r != fLogical.size(); ++r) {
+ auto& run = fLogical[reverse ? fLogical.size() - r - 1 : r];
+ // Walk through the clusters in the logical order (or reverse)
+ auto normalOrder = run->leftToRight() != reverse;
+ auto start = normalOrder ? run->clusters().begin() : run->clusters().end() - 1;
+ auto end = normalOrder ? run->clusters().end() : run->clusters().begin() - 1;
+ for (auto cluster = start; cluster != end; normalOrder ? ++cluster : --cluster) {
+ if (!this->contains(cluster)) {
+ continue;
+ }
+ if (!visitor(cluster)) {
+ return;
+ }
+ }
+ }
+}
+
+// Walk through the runs in the logical order
+SkScalar TextLine::iterateThroughRuns(SkSpan<const char> text,
+ SkScalar runOffset,
+ bool includeEmptyText,
+ const RunVisitor& visitor) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+
+ SkScalar width = 0;
+ for (auto& run : fLogical) {
+ // Only skip the text if it does not even touch the run
+ if (intersectedSize(run->text(), text) < 0) {
+ continue;
+ }
+
+ SkSpan<const char> intersect = intersected(run->text(), text);
+ if (run->text().empty() || intersect.empty()) {
+ continue;
+ }
+
+ size_t pos;
+ size_t size;
+ bool clippingNeeded;
+ SkRect clip = this->measureTextInsideOneRun(intersect, run, pos, size, clippingNeeded);
+ if (clip.height() == 0) {
+ continue;
+ }
+
+ auto shift = runOffset - clip.fLeft;
+ clip.offset(shift, 0);
+ if (clip.fRight > fAdvance.fX) {
+ clip.fRight = fAdvance.fX;
+ clippingNeeded = true; // Correct the clip in case there was an ellipsis
+ } else if (run == fLogical.back() && this->ellipsis() != nullptr) {
+ clippingNeeded = true; // To avoid trouble
+ }
+
+ if (!visitor(run, pos, size, clip, shift - run->positionX(0), clippingNeeded)) {
+ return width;
+ }
+
+ width += clip.width();
+ runOffset += clip.width();
+ }
+
+ if (this->ellipsis() != nullptr) {
+ auto ellipsis = this->ellipsis();
+ if (!visitor(ellipsis, 0, ellipsis->size(), ellipsis->clip(), ellipsis->clip().fLeft,
+ false)) {
+ return width;
+ }
+ width += ellipsis->clip().width();
+ }
+
+ return width;
+}
+
+void TextLine::iterateThroughStylesInTextOrder(StyleType styleType,
+ const StyleVisitor& visitor) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ const char* start = nullptr;
+ size_t size = 0;
+ TextStyle prevStyle;
+
+ SkScalar offsetX = 0;
+ for (auto& block : fBlocks) {
+ auto intersect = intersected(block.text(), this->trimmedText());
+ if (intersect.empty()) {
+ if (start == nullptr) {
+ // This style is not applicable to the line
+ continue;
+ } else {
+ // We have found all the good styles already
+ break;
+ }
+ }
+
+ auto style = block.style();
+ if (start != nullptr && style.matchOneAttribute(styleType, prevStyle)) {
+ size += intersect.size();
+ continue;
+ } else if (size == 0) {
+ // First time only
+ prevStyle = style;
+ size = intersect.size();
+ start = intersect.begin();
+ continue;
+ }
+
+ auto width = visitor(SkSpan<const char>(start, size), prevStyle, offsetX);
+ offsetX += width;
+
+ // Start all over again
+ prevStyle = style;
+ start = intersect.begin();
+ size = intersect.size();
+ }
+
+ // The very last style
+ auto width = visitor(SkSpan<const char>(start, size), prevStyle, offsetX);
+ offsetX += width;
+
+ // This is a very important assert!
+ // It asserts that 2 different ways of calculation come with the same results
+ if (!SkScalarNearlyEqual(offsetX, this->width())) {
+ SkDebugf("ASSERT: %f != %f\n", offsetX, this->width());
+ }
+ SkASSERT(SkScalarNearlyEqual(offsetX, this->width()));
+}
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/TextLine.h b/modules/skparagraph/src/TextLine.h
new file mode 100644
index 0000000..de71e38
--- /dev/null
+++ b/modules/skparagraph/src/TextLine.h
@@ -0,0 +1,132 @@
+// Copyright 2019 Google LLC.
+#ifndef TextLine_DEFINED
+#define TextLine_DEFINED
+
+#include "modules/skparagraph/src/Run.h"
+#include "include/core/SkCanvas.h"
+#include "include/private/SkTArray.h"
+#include "include/private/SkTHash.h"
+#include "modules/skparagraph/include/DartTypes.h"
+#include "modules/skparagraph/include/TextStyle.h"
+#include "src/core/SkSpan.h"
+
+namespace skia {
+namespace textlayout {
+
+class TextBlock {
+public:
+ TextBlock() : fText(), fTextStyle() {}
+ TextBlock(SkSpan<const char> text, const TextStyle& style) : fText(text), fTextStyle(style) {}
+
+ SkSpan<const char> text() const { return fText; }
+ TextStyle style() const { return fTextStyle; }
+
+ void add(SkSpan<const char> tail) {
+ SkASSERT(fText.end() == tail.begin());
+ fText = SkSpan<const char>(fText.begin(), fText.size() + tail.size());
+ }
+
+protected:
+ SkSpan<const char> fText;
+ TextStyle fTextStyle;
+};
+
+class TextLine {
+public:
+ TextLine() = default;
+ TextLine(TextLine&&);
+ ~TextLine() = default;
+
+ TextLine(SkVector offset, SkVector advance, SkSpan<const TextBlock> blocks,
+ SkSpan<const char> text, SkSpan<const char> textWithSpaces,
+ SkSpan<const Cluster> clusters, size_t start, size_t end, LineMetrics sizes);
+
+ SkSpan<const char> trimmedText() const { return fText; }
+ SkSpan<const char> textWithSpaces() const { return fTextWithSpaces; }
+ SkSpan<const Cluster> clusters() const { return fClusters; }
+ SkVector offset() const { return fOffset + SkVector::Make(fShift, 0); }
+ Run* ellipsis() const { return fEllipsis.get(); }
+ LineMetrics sizes() const { return fSizes; }
+ bool empty() const { return fText.empty(); }
+
+ SkScalar shift() const { return fShift; }
+ SkScalar height() const { return fAdvance.fY; }
+ SkScalar width() const {
+ return fAdvance.fX + (fEllipsis != nullptr ? fEllipsis->fAdvance.fX : 0);
+ }
+ void shiftTo(SkScalar shift) { fShift = shift; }
+
+ SkScalar alphabeticBaseline() const { return fSizes.alphabeticBaseline(); }
+ SkScalar ideographicBaseline() const { return fSizes.ideographicBaseline(); }
+ SkScalar baseline() const { return fSizes.baseline(); }
+ SkScalar roundingDelta() const { return fSizes.delta(); }
+
+ using StyleVisitor = std::function<SkScalar(SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX)>;
+ void iterateThroughStylesInTextOrder(StyleType styleType, const StyleVisitor& visitor) const;
+
+ using RunVisitor = std::function<bool(Run* run, size_t pos, size_t size, SkRect clip,
+ SkScalar shift, bool clippingNeeded)>;
+ SkScalar iterateThroughRuns(SkSpan<const char> text,
+ SkScalar offsetX,
+ bool includeEmptyText,
+ const RunVisitor& visitor) const;
+
+ using ClustersVisitor = std::function<bool(const Cluster* cluster)>;
+ void iterateThroughClustersInGlyphsOrder(bool reverse, const ClustersVisitor& visitor) const;
+
+ void format(TextAlign effectiveAlign, SkScalar maxWidth, bool last);
+ void paint(SkCanvas* canvas);
+
+ void createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool ltr);
+
+ // For testing internal structures
+ void scanStyles(StyleType style, const StyleVisitor& visitor);
+ void scanRuns(const RunVisitor& visitor);
+
+private:
+ Run* shapeEllipsis(const SkString& ellipsis, Run* run);
+ void justify(SkScalar maxWidth);
+
+ SkRect measureTextInsideOneRun(SkSpan<const char> text,
+ Run* run,
+ size_t& pos,
+ size_t& size,
+ bool& clippingNeeded) const;
+
+ SkScalar paintText(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const;
+ SkScalar paintBackground(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const;
+ SkScalar paintShadow(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const;
+ SkScalar paintDecorations(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const;
+
+ void computeDecorationPaint(SkPaint& paint, SkRect clip, const TextStyle& style,
+ SkPath& path) const;
+
+ bool contains(const Cluster* cluster) const {
+ return cluster->text().begin() >= fText.begin() && cluster->text().end() <= fText.end();
+ }
+
+ SkSpan<const TextBlock> fBlocks;
+ SkSpan<const char> fText;
+ SkSpan<const char> fTextWithSpaces;
+ SkSpan<const Cluster> fClusters;
+ // TODO: To clip by glyph:
+ //size_t fStartPos;
+ //size_t fEndPos;
+ SkTArray<Run*, true> fLogical;
+ SkScalar fShift; // Shift to left - right - center
+ SkVector fAdvance; // Text size
+ SkVector fOffset; // Text position
+ std::unique_ptr<Run> fEllipsis; // In case the line ends with the ellipsis
+ LineMetrics fSizes; // Line metrics as a max of all run metrics
+
+ static SkTHashMap<SkFont, Run> fEllipsisCache; // All found so far shapes of ellipsis
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // TextLine_DEFINED
diff --git a/modules/skparagraph/src/TextShadow.cpp b/modules/skparagraph/src/TextShadow.cpp
new file mode 100644
index 0000000..00ef8f2
--- /dev/null
+++ b/modules/skparagraph/src/TextShadow.cpp
@@ -0,0 +1,30 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/include/TextShadow.h"
+#include "include/core/SkColor.h"
+
+namespace skia {
+namespace textlayout {
+
+TextShadow::TextShadow() = default;
+TextShadow::TextShadow(SkColor color, SkPoint offset, double blurRadius)
+ : fColor(color), fOffset(offset), fBlurRadius(blurRadius) {}
+
+bool TextShadow::operator==(const TextShadow& other) const {
+ if (fColor != other.fColor) return false;
+ if (fOffset != other.fOffset) return false;
+ if (fBlurRadius != other.fBlurRadius) return false;
+
+ return true;
+}
+
+bool TextShadow::operator!=(const TextShadow& other) const { return !(*this == other); }
+
+bool TextShadow::hasShadow() const {
+ if (!fOffset.isZero()) return true;
+ if (fBlurRadius != 0.0) return true;
+
+ return false;
+}
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/TextStyle.cpp b/modules/skparagraph/src/TextStyle.cpp
new file mode 100644
index 0000000..8ba3d2d
--- /dev/null
+++ b/modules/skparagraph/src/TextStyle.cpp
@@ -0,0 +1,133 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/include/TextStyle.h"
+#include "include/core/SkColor.h"
+#include "include/core/SkFontStyle.h"
+
+namespace skia {
+namespace textlayout {
+
+TextStyle::TextStyle() : fFontStyle() {
+ fFontFamilies.emplace_back(DEFAULT_FONT_FAMILY);
+ fColor = SK_ColorWHITE;
+ fDecoration = TextDecoration::kNoDecoration;
+ // Does not make sense to draw a transparent object, so we use it as a default
+ // value to indicate no decoration color was set.
+ fDecorationColor = SK_ColorTRANSPARENT;
+ fDecorationStyle = TextDecorationStyle::kSolid;
+ // Thickness is applied as a multiplier to the default thickness of the font.
+ fDecorationThicknessMultiplier = 1.0;
+ fFontSize = 14.0;
+ fLetterSpacing = 0.0;
+ fWordSpacing = 0.0;
+ fHeight = 1.0;
+ fHasBackground = false;
+ fHasForeground = false;
+ fTextBaseline = TextBaseline::kAlphabetic;
+}
+
+bool TextStyle::equals(const TextStyle& other) const {
+ if (fColor != other.fColor) {
+ return false;
+ }
+ if (fDecoration != other.fDecoration) {
+ return false;
+ }
+ if (fDecorationColor != other.fDecorationColor) {
+ return false;
+ }
+ if (fDecorationStyle != other.fDecorationStyle) {
+ return false;
+ }
+ if (fDecorationThicknessMultiplier != other.fDecorationThicknessMultiplier) {
+ return false;
+ }
+ if (!(fFontStyle == other.fFontStyle)) {
+ return false;
+ }
+ if (fFontFamilies != other.fFontFamilies) {
+ return false;
+ }
+ if (fLetterSpacing != other.fLetterSpacing) {
+ return false;
+ }
+ if (fWordSpacing != other.fWordSpacing) {
+ return false;
+ }
+ if (fHeight != other.fHeight) {
+ return false;
+ }
+ if (fFontSize != other.fFontSize) {
+ return false;
+ }
+ if (fLocale != other.fLocale) {
+ return false;
+ }
+ if (fHasForeground != other.fHasForeground || fForeground != other.fForeground) {
+ return false;
+ }
+ if (fHasBackground != other.fHasBackground || fBackground != other.fBackground) {
+ return false;
+ }
+ if (fTextShadows.size() != other.fTextShadows.size()) {
+ return false;
+ }
+
+ for (int32_t i = 0; i < (int32_t)fTextShadows.size(); ++i) {
+ if (fTextShadows[i] != other.fTextShadows[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool TextStyle::matchOneAttribute(StyleType styleType, const TextStyle& other) const {
+ switch (styleType) {
+ case kForeground:
+ if (fHasForeground) {
+ return other.fHasForeground && fForeground == other.fForeground;
+ } else {
+ return !other.fHasForeground && fColor == other.fColor;
+ }
+
+ case kBackground:
+ return (fHasBackground == other.fHasBackground && fBackground == other.fBackground);
+
+ case kShadow:
+ if (fTextShadows.size() != other.fTextShadows.size()) {
+ return false;
+ }
+
+ for (int32_t i = 0; i < SkToInt(fTextShadows.size()); ++i) {
+ if (fTextShadows[i] != other.fTextShadows[i]) {
+ return false;
+ }
+ }
+ return true;
+
+ case kDecorations:
+ return fDecoration == other.fDecoration && fDecorationColor == other.fDecorationColor &&
+ fDecorationStyle == other.fDecorationStyle &&
+ fDecorationThicknessMultiplier == other.fDecorationThicknessMultiplier;
+
+ case kLetterSpacing:
+ return fLetterSpacing == other.fLetterSpacing;
+
+ case kWordSpacing:
+ return fWordSpacing == other.fWordSpacing;
+
+ case kAllAttributes:
+ return this->equals(other);
+
+ case kFont:
+ return fFontStyle == other.fFontStyle && fFontFamilies == other.fFontFamilies &&
+ fFontSize == other.fFontSize && fHeight == other.fHeight;
+
+ default:
+ SkASSERT(false);
+ return false;
+ }
+}
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/TextWrapper.cpp b/modules/skparagraph/src/TextWrapper.cpp
new file mode 100644
index 0000000..f8adfd1
--- /dev/null
+++ b/modules/skparagraph/src/TextWrapper.cpp
@@ -0,0 +1,213 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/TextWrapper.h"
+#include "modules/skparagraph/src/ParagraphImpl.h"
+
+namespace skia {
+namespace textlayout {
+
+// Since we allow cluster clipping when they don't fit
+// we have to work with stretches - parts of clusters
+void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters) {
+ fWords.startFrom(fEndLine.startCluster(), fEndLine.startPos());
+ fClusters.startFrom(fEndLine.startCluster(), fEndLine.startPos());
+ fClip.startFrom(fEndLine.startCluster(), fEndLine.startPos());
+ for (auto cluster = fEndLine.endCluster(); cluster < endOfClusters; ++cluster) {
+ if (fWords.width() + fClusters.width() + cluster->width() > maxWidth) {
+ if (cluster->isWhitespaces()) {
+ break;
+ }
+ if (cluster->width() > maxWidth) {
+ // Break the cluster into parts by glyph position
+ auto delta = maxWidth - (fWords.width() + fClusters.width());
+ fClip.extend(cluster, cluster->roundPos(delta));
+ fTooLongCluster = true;
+ fTooLongWord = true;
+ break;
+ }
+
+ // Walk further to see if there is a too long word, cluster or glyph
+ SkScalar nextWordLength = fClusters.width();
+ for (auto further = cluster; further != endOfClusters; ++further) {
+ if (further->isSoftBreak() || further->isHardBreak()) {
+ break;
+ }
+ nextWordLength += further->width();
+ }
+ if (nextWordLength > maxWidth) {
+ // If the word is too long we can break it right now and hope it's enough
+ fTooLongWord = true;
+ }
+
+ // TODO: this is the place when we use hyphenation
+ fMinIntrinsicWidth = SkTMax(fMinIntrinsicWidth, nextWordLength);
+ break;
+ }
+
+ fClusters.extend(cluster);
+
+ // Keep adding clusters/words
+ if (fClusters.endOfWord()) {
+ fMinIntrinsicWidth = SkTMax(fMinIntrinsicWidth, getClustersTrimmedWidth());
+ fWords.extend(fClusters);
+ }
+
+ if ((fHardLineBreak = cluster->isHardBreak())) {
+ // Stop at the hard line break
+ break;
+ }
+ }
+}
+
+void TextWrapper::moveForward() {
+ do {
+ if (fWords.width() > 0) {
+ fEndLine.extend(fWords);
+ } else if (fClusters.width() > 0) {
+ fEndLine.extend(fClusters);
+ fTooLongWord = false;
+ } else if (fClip.width() > 0) {
+ fEndLine.extend(fClip);
+ fTooLongWord = false;
+ fTooLongCluster = false;
+ } else {
+ break;
+ }
+ } while (fTooLongWord || fTooLongCluster);
+}
+
+// Special case for start/end cluster since they can be clipped
+void TextWrapper::trimEndSpaces(bool includingClusters) {
+ // Remember the breaking position
+ fEndLine.saveBreak();
+ if (includingClusters) {
+ // Move the end of the line to the left
+ for (auto cluster = fEndLine.endCluster();
+ cluster >= fEndLine.startCluster() && cluster->isWhitespaces();
+ --cluster) {
+ fEndLine.trim(cluster);
+ }
+ }
+ fEndLine.trim();
+}
+
+SkScalar TextWrapper::getClustersTrimmedWidth() {
+ // Move the end of the line to the left
+ SkScalar width = fClusters.width();
+ auto cluster = fClusters.endCluster();
+ for (; cluster > fClusters.startCluster() && cluster->isWhitespaces(); --cluster) {
+ width -= cluster->width();
+ }
+ if (cluster >= fClusters.startCluster()) {
+ width -= (cluster->width() - cluster->trimmedWidth(cluster->endPos()));
+ }
+ return width;
+}
+
+// Trim the beginning spaces in case of soft line break
+void TextWrapper::trimStartSpaces(Cluster* endOfClusters) {
+ // Restore the breaking position
+ fEndLine.restoreBreak();
+ fEndLine.nextPos();
+ if (fHardLineBreak) {
+ // End of line is always end of cluster, but need to skip \n
+ fEndLine.startFrom(fEndLine.endCluster(), 0);
+ return;
+ }
+ if (fEndLine.endPos() != 0) {
+ // Clipping
+ fEndLine.startFrom(fEndLine.endCluster(), fEndLine.endPos());
+ return;
+ }
+
+ auto cluster = fEndLine.endCluster();
+ while (cluster < endOfClusters && cluster->isWhitespaces()) {
+ ++cluster;
+ }
+ fEndLine.startFrom(cluster, 0);
+}
+
+void TextWrapper::breakTextIntoLines(ParagraphImpl* parent,
+ SkScalar maxWidth,
+ const AddLineToParagraph& addLine) {
+ auto span = parent->clusters();
+ auto maxLines = parent->paragraphStyle().getMaxLines();
+ auto ellipsisStr = parent->paragraphStyle().getEllipsis();
+ auto textAlign = parent->paragraphStyle().effective_align();
+
+ fHeight = 0;
+ fMinIntrinsicWidth = 0;
+ fMaxIntrinsicWidth = 0;
+ fEndLine = TextStretch(span.begin(), span.begin());
+ auto end = &span.back();
+ while (fEndLine.endCluster() != end) {
+ reset();
+
+ lookAhead(maxWidth, end);
+ moveForward();
+
+ // Do not trim end spaces on the naturally last line of the left aligned text
+ trimEndSpaces(textAlign != TextAlign::kLeft || fEndLine.endCluster() < end - 1);
+
+ auto lastLine = maxLines == std::numeric_limits<size_t>::max() ||
+ fLineNumber >= maxLines;
+ auto needEllipsis =
+ lastLine &&
+ !fHardLineBreak &&
+ fEndLine.endCluster() < end - 1 &&
+ maxWidth != std::numeric_limits<SkScalar>::max() &&
+ !ellipsisStr.isEmpty();
+ // TODO: perform ellipsis work here
+ if (parent->strutEnabled()) {
+ // Make sure font metrics are not less than the strut
+ parent->strutMetrics().updateLineMetrics(fEndLine.metrics(),
+ parent->strutForceHeight());
+ }
+ fMaxIntrinsicWidth = SkMaxScalar(fMaxIntrinsicWidth, fEndLine.width());
+ // TODO: keep start/end/break info for text and runs but in a better way that below
+ SkSpan<const char> text(fEndLine.startCluster()->text().begin(),
+ fEndLine.endCluster()->text().end() - fEndLine.startCluster()->text().begin());
+ SkSpan<const char> textWithSpaces(fEndLine.startCluster()->text().begin(),
+ fEndLine.breakCluster()->text().end() - fEndLine.startCluster()->text().begin());
+ addLine(text, textWithSpaces,
+ fEndLine.startCluster(),
+ fEndLine.endCluster(),
+ fEndLine.startPos(),
+ fEndLine.endPos(),
+ SkVector::Make(0, fHeight),
+ SkVector::Make(fEndLine.width(), fEndLine.metrics().height()),
+ fEndLine.metrics(),
+ needEllipsis);
+
+ // Start a new line
+ fHeight += fEndLine.metrics().height();
+
+ trimStartSpaces(end);
+
+ if (needEllipsis || fLineNumber >= maxLines) {
+ break;
+ }
+ ++fLineNumber;
+ }
+
+ if (fHardLineBreak) {
+ // Last character is a line break
+ if (parent->strutEnabled()) {
+ // Make sure font metrics are not less than the strut
+ parent->strutMetrics().updateLineMetrics(fEndLine.metrics(),
+ parent->strutForceHeight());
+ }
+ SkSpan<const char> empty(fEndLine.breakCluster()->text().begin(), 0);
+ addLine(empty, empty,
+ fEndLine.breakCluster(),
+ fEndLine.breakCluster(),
+ 0,
+ 0,
+ SkVector::Make(0, fHeight),
+ SkVector::Make(0, fEndLine.metrics().height()),
+ fEndLine.metrics(),
+ false);
+ }
+}
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/TextWrapper.h b/modules/skparagraph/src/TextWrapper.h
new file mode 100644
index 0000000..2ae80a5
--- /dev/null
+++ b/modules/skparagraph/src/TextWrapper.h
@@ -0,0 +1,183 @@
+// Copyright 2019 Google LLC.
+#ifndef TextWrapper_DEFINED
+#define TextWrapper_DEFINED
+
+#include <string>
+#include "modules/skparagraph/src/TextLine.h"
+#include "src/core/SkSpan.h"
+
+namespace skia {
+namespace textlayout {
+
+class ParagraphImpl;
+
+class TextWrapper {
+ class ClusterPos {
+ public:
+ ClusterPos() : fCluster(nullptr), fPos(0) {}
+ ClusterPos(Cluster* cluster, size_t pos) : fCluster(cluster), fPos(pos) {}
+ Cluster* cluster() const { return fCluster; }
+ size_t position() const { return fPos; }
+ void move(bool up) {
+ fCluster += up ? 1 : -1;
+ fPos = up ? 0 : fCluster->endPos();
+ }
+ void setPosition(size_t pos) { fPos = pos; }
+ void clean() {
+ fCluster = nullptr;
+ fPos = 0;
+ }
+
+ private:
+ Cluster* fCluster;
+ size_t fPos;
+ };
+ class TextStretch {
+ public:
+ TextStretch() : fStart(), fEnd(), fWidth(0) {}
+ explicit TextStretch(Cluster* s, Cluster* e)
+ : fStart(s, 0), fEnd(e, e->endPos()), fMetrics(), fWidth(0) {
+ for (auto c = s; c <= e; ++c) {
+ if (c->run() != nullptr) {
+ fMetrics.add(c->run());
+ }
+ }
+ }
+
+ SkScalar width() const { return fWidth; }
+ Cluster* startCluster() const { return fStart.cluster(); }
+ Cluster* endCluster() const { return fEnd.cluster(); }
+ Cluster* breakCluster() const { return fBreak.cluster(); }
+ LineMetrics& metrics() { return fMetrics; }
+ size_t startPos() const { return fStart.position(); }
+ size_t endPos() const { return fEnd.position(); }
+ bool endOfCluster() { return fEnd.position() == fEnd.cluster()->endPos(); }
+ bool endOfWord() {
+ return endOfCluster() &&
+ (fEnd.cluster()->isHardBreak() || fEnd.cluster()->isSoftBreak());
+ }
+
+ void extend(TextStretch& stretch) {
+ fMetrics.add(stretch.fMetrics);
+ fEnd = stretch.fEnd;
+ fWidth += stretch.fWidth;
+ stretch.clean();
+ }
+
+ void extend(Cluster* cluster) {
+ fEnd = ClusterPos(cluster, cluster->endPos());
+ fMetrics.add(cluster->run());
+ fWidth += cluster->width();
+ }
+
+ void extend(Cluster* cluster, size_t pos) {
+ fEnd = ClusterPos(cluster, pos);
+ if (cluster->run() != nullptr) {
+ fMetrics.add(cluster->run());
+ }
+ }
+
+ void startFrom(Cluster* cluster, size_t pos) {
+ fStart = ClusterPos(cluster, pos);
+ fEnd = ClusterPos(cluster, pos);
+ if (cluster->run() != nullptr) {
+ fMetrics.add(cluster->run());
+ }
+ fWidth = 0;
+ }
+
+ void nextPos() {
+ if (fEnd.position() == fEnd.cluster()->endPos()) {
+ fEnd.move(true);
+ } else {
+ fEnd.setPosition(fEnd.cluster()->endPos());
+ }
+ }
+
+ void saveBreak() { fBreak = fEnd; }
+
+ void restoreBreak() { fEnd = fBreak; }
+
+ void trim() {
+ fWidth -= (fEnd.cluster()->width() - fEnd.cluster()->trimmedWidth(fEnd.position()));
+ }
+
+ void trim(Cluster* cluster) {
+ SkASSERT(fEnd.cluster() == cluster);
+ if (fEnd.cluster() > fStart.cluster()) {
+ fEnd.move(false);
+ fWidth -= cluster->width();
+ } else {
+ fWidth = 0;
+ }
+ }
+
+ void clean() {
+ fStart.clean();
+ fEnd.clean();
+ fWidth = 0;
+ fMetrics.clean();
+ }
+
+ private:
+ ClusterPos fStart;
+ ClusterPos fEnd;
+ ClusterPos fBreak;
+ LineMetrics fMetrics;
+ SkScalar fWidth;
+ };
+
+public:
+ TextWrapper() { fLineNumber = 1; }
+
+ using AddLineToParagraph = std::function<void(SkSpan<const char> text,
+ SkSpan<const char> textWithSpaces,
+ Cluster* start,
+ Cluster* end,
+ size_t startClip,
+ size_t endClip,
+ SkVector offset,
+ SkVector advance,
+ LineMetrics metrics,
+ bool addEllipsis)>;
+ void breakTextIntoLines(ParagraphImpl* parent,
+ SkScalar maxWidth,
+ const AddLineToParagraph& addLine);
+
+ SkScalar height() const { return fHeight; }
+ SkScalar minIntrinsicWidth() const { return fMinIntrinsicWidth; }
+ SkScalar maxIntrinsicWidth() const { return fMaxIntrinsicWidth; }
+
+private:
+ TextStretch fWords;
+ TextStretch fClusters;
+ TextStretch fClip;
+ TextStretch fEndLine;
+ size_t fLineNumber;
+ bool fTooLongWord;
+ bool fTooLongCluster;
+
+ bool fHardLineBreak;
+
+ SkScalar fHeight;
+ SkScalar fMinIntrinsicWidth;
+ SkScalar fMaxIntrinsicWidth;
+
+ void reset() {
+ fWords.clean();
+ fClusters.clean();
+ fClip.clean();
+ fTooLongCluster = false;
+ fTooLongWord = false;
+ }
+
+ void lookAhead(SkScalar maxWidth, Cluster* endOfClusters);
+ void moveForward();
+ void trimEndSpaces(bool includingClusters);
+ void trimStartSpaces(Cluster* endOfClusters);
+ SkScalar getClustersTrimmedWidth();
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // TextWrapper_DEFINED
diff --git a/modules/skparagraph/src/TypefaceFontProvider.cpp b/modules/skparagraph/src/TypefaceFontProvider.cpp
new file mode 100644
index 0000000..ed4d776
--- /dev/null
+++ b/modules/skparagraph/src/TypefaceFontProvider.cpp
@@ -0,0 +1,86 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/TypefaceFontProvider.h"
+#include <algorithm>
+#include "include/core/SkString.h"
+#include "include/core/SkTypeface.h"
+
+namespace skia {
+namespace textlayout {
+
+int TypefaceFontProvider::onCountFamilies() const { return fRegisteredFamilies.size(); }
+
+void TypefaceFontProvider::onGetFamilyName(int index, SkString* familyName) const {
+ SkASSERT(index < fRegisteredFamilies.count());
+ familyName->set(fRegisteredFamilies[index]->getFamilyName());
+}
+
+SkFontStyleSet* TypefaceFontProvider::onMatchFamily(const char familyName[]) const {
+ for (auto& family : fRegisteredFamilies) {
+ if (family->getFamilyName().equals(familyName)) {
+ return SkRef(family.get());
+ }
+ }
+ return nullptr;
+}
+
+size_t TypefaceFontProvider::registerTypeface(sk_sp<SkTypeface> typeface) {
+ if (typeface == nullptr) {
+ return 0;
+ }
+
+ SkString familyName;
+ typeface->getFamilyName(&familyName);
+
+ return registerTypeface(std::move(typeface), familyName);
+}
+
+size_t TypefaceFontProvider::registerTypeface(sk_sp<SkTypeface> typeface, const SkString& familyName) {
+ if (familyName.size() == 0) {
+ return 0;
+ }
+
+ TypefaceFontStyleSet* found = nullptr;
+ for (auto& family : fRegisteredFamilies) {
+ if (family->getFamilyName().equals(familyName)) {
+ found = family.get();
+ break;
+ }
+ }
+ if (found == nullptr) {
+ found = fRegisteredFamilies.emplace_back(sk_make_sp<TypefaceFontStyleSet>(familyName)).get();
+ }
+
+ found->appendTypeface(std::move(typeface));
+ return 1;
+}
+
+TypefaceFontStyleSet::TypefaceFontStyleSet(const SkString& familyName)
+ : fFamilyName(familyName) {}
+
+int TypefaceFontStyleSet::count() { return fStyles.size(); }
+
+void TypefaceFontStyleSet::getStyle(int index, SkFontStyle* style, SkString* name) {
+ SkASSERT(index < fStyles.count());
+ if (style) {
+ *style = fStyles[index]->fontStyle();
+ }
+ if (name) {
+ *name = fFamilyName;
+ }
+}
+
+SkTypeface* TypefaceFontStyleSet::createTypeface(int index) {
+ SkASSERT(index < fStyles.count());
+ return SkRef(fStyles[index].get());
+}
+
+SkTypeface* TypefaceFontStyleSet::matchStyle(const SkFontStyle& pattern) {
+ return this->matchStyleCSS3(pattern);
+}
+
+void TypefaceFontStyleSet::appendTypeface(sk_sp<SkTypeface> typeface) {
+ fStyles.emplace_back(std::move(typeface));
+}
+
+} // namespace textlayout
+} // namespace skia
diff --git a/modules/skparagraph/src/TypefaceFontProvider.h b/modules/skparagraph/src/TypefaceFontProvider.h
new file mode 100644
index 0000000..e72a58d
--- /dev/null
+++ b/modules/skparagraph/src/TypefaceFontProvider.h
@@ -0,0 +1,89 @@
+// Copyright 2019 Google LLC.
+#ifndef TypefaceFontProvider_DEFINED
+#define TypefaceFontProvider_DEFINED
+
+#include <include/private/SkTArray.h>
+#include <include/private/SkTHash.h>
+#include <string>
+#include <unordered_map>
+#include <vector>
+#include "include/core/SkFontMgr.h"
+#include "include/core/SkStream.h"
+#include "include/core/SkString.h"
+#include "src/core/SkFontDescriptor.h"
+
+namespace skia {
+namespace textlayout {
+
+class TypefaceFontStyleSet : public SkFontStyleSet {
+public:
+ explicit TypefaceFontStyleSet(const SkString& familyName);
+
+ int count() override;
+ void getStyle(int index, SkFontStyle*, SkString* name) override;
+ SkTypeface* createTypeface(int index) override;
+ SkTypeface* matchStyle(const SkFontStyle& pattern) override;
+
+ SkString getFamilyName() const { return fFamilyName; }
+ SkString getAlias() const { return fAlias; }
+ void appendTypeface(sk_sp<SkTypeface> typeface);
+
+private:
+ SkTArray<sk_sp<SkTypeface>> fStyles;
+ SkString fFamilyName;
+ SkString fAlias;
+};
+
+class TypefaceFontProvider : public SkFontMgr {
+public:
+ size_t registerTypeface(sk_sp<SkTypeface> typeface);
+ size_t registerTypeface(sk_sp<SkTypeface> typeface, const SkString& alias);
+
+ int onCountFamilies() const override;
+
+ void onGetFamilyName(int index, SkString* familyName) const override;
+
+ SkFontStyleSet* onMatchFamily(const char familyName[]) const override;
+
+ SkFontStyleSet* onCreateStyleSet(int index) const override { return nullptr; }
+ SkTypeface* onMatchFamilyStyle(const char familyName[],
+ const SkFontStyle& style) const override {
+ return nullptr;
+ }
+ SkTypeface* onMatchFamilyStyleCharacter(const char familyName[], const SkFontStyle& style,
+ const char* bcp47[], int bcp47Count,
+ SkUnichar character) const override {
+ return nullptr;
+ }
+ SkTypeface* onMatchFaceStyle(const SkTypeface* tf, const SkFontStyle& style) const override {
+ return nullptr;
+ }
+
+ sk_sp<SkTypeface> onMakeFromData(sk_sp<SkData>, int ttcIndex) const override { return nullptr; }
+ sk_sp<SkTypeface> onMakeFromStreamIndex(std::unique_ptr<SkStreamAsset>,
+ int ttcIndex) const override {
+ return nullptr;
+ }
+ sk_sp<SkTypeface> onMakeFromStreamArgs(std::unique_ptr<SkStreamAsset>,
+ const SkFontArguments&) const override {
+ return nullptr;
+ }
+ sk_sp<SkTypeface> onMakeFromFontData(std::unique_ptr<SkFontData>) const override {
+ return nullptr;
+ }
+ sk_sp<SkTypeface> onMakeFromFile(const char path[], int ttcIndex) const override {
+ return nullptr;
+ }
+
+ sk_sp<SkTypeface> onLegacyMakeTypeface(const char familyName[],
+ SkFontStyle style) const override {
+ return nullptr;
+ }
+
+private:
+ SkTArray<sk_sp<TypefaceFontStyleSet>> fRegisteredFamilies;
+};
+} // namespace textlayout
+} // namespace skia
+
+#endif // TypefaceFontProvider_DEFINED