| // Copyright 2019 Google LLC | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkFontMetrics.h" | |
| #include "include/core/SkMatrix.h" | |
| #include "include/core/SkPath.h" | |
| #include "include/core/SkPathBuilder.h" | |
| #include "include/core/SkPictureRecorder.h" | |
| #include "include/core/SkSpan.h" | |
| #include "include/core/SkTypeface.h" | |
| #include "include/private/SkTFitsIn.h" | |
| #include "include/private/SkTo.h" | |
| #include "modules/skparagraph/include/Metrics.h" | |
| #include "modules/skparagraph/include/Paragraph.h" | |
| #include "modules/skparagraph/include/ParagraphPainter.h" | |
| #include "modules/skparagraph/include/ParagraphStyle.h" | |
| #include "modules/skparagraph/include/TextStyle.h" | |
| #include "modules/skparagraph/src/OneLineShaper.h" | |
| #include "modules/skparagraph/src/ParagraphImpl.h" | |
| #include "modules/skparagraph/src/ParagraphPainterImpl.h" | |
| #include "modules/skparagraph/src/Run.h" | |
| #include "modules/skparagraph/src/TextLine.h" | |
| #include "modules/skparagraph/src/TextWrapper.h" | |
| #include "modules/skunicode/include/SkUnicode.h" | |
| #include "src/core/SkTextBlobPriv.h" | |
| #include "src/core/SkUTF.h" | |
| #include <algorithm> | |
| #include <cfloat> | |
| #include <cmath> | |
| #include <utility> | |
| using namespace skia_private; | |
| namespace skia { | |
| namespace textlayout { | |
| namespace { | |
| SkScalar littleRound(SkScalar a) { | |
| // This rounding is done to match Flutter tests. Must be removed.. | |
| auto val = std::fabs(a); | |
| if (val < 10000) { | |
| return SkScalarRoundToScalar(a * 100.0)/100.0; | |
| } else if (val < 100000) { | |
| return SkScalarRoundToScalar(a * 10.0)/10.0; | |
| } else { | |
| return SkScalarFloorToScalar(a); | |
| } | |
| } | |
| } // namespace | |
| TextRange operator*(const TextRange& a, const TextRange& b) { | |
| if (a.start == b.start && a.end == b.end) return a; | |
| auto begin = std::max(a.start, b.start); | |
| auto end = std::min(a.end, b.end); | |
| return end > begin ? TextRange(begin, end) : EMPTY_TEXT; | |
| } | |
| Paragraph::Paragraph(ParagraphStyle style, sk_sp<FontCollection> fonts) | |
| : fFontCollection(std::move(fonts)) | |
| , fParagraphStyle(std::move(style)) | |
| , fAlphabeticBaseline(0) | |
| , fIdeographicBaseline(0) | |
| , fHeight(0) | |
| , fWidth(0) | |
| , fMaxIntrinsicWidth(0) | |
| , fMinIntrinsicWidth(0) | |
| , fLongestLine(0) | |
| , fExceededMaxLines(0) | |
| { | |
| SkASSERT(fFontCollection); | |
| } | |
| ParagraphImpl::ParagraphImpl(const SkString& text, | |
| ParagraphStyle style, | |
| TArray<Block> blocks, | |
| TArray<Placeholder> placeholders, | |
| sk_sp<FontCollection> fonts, | |
| sk_sp<SkUnicode> unicode) | |
| : Paragraph(std::move(style), std::move(fonts)) | |
| , fTextStyles(std::move(blocks)) | |
| , fPlaceholders(std::move(placeholders)) | |
| , fText(text) | |
| , fState(kUnknown) | |
| , fUnresolvedGlyphs(0) | |
| , fPicture(nullptr) | |
| , fStrutMetrics(false) | |
| , fOldWidth(0) | |
| , fOldHeight(0) | |
| , fUnicode(std::move(unicode)) | |
| , fHasLineBreaks(false) | |
| , fHasWhitespacesInside(false) | |
| , fTrailingSpaces(0) | |
| { | |
| SkASSERT(fUnicode); | |
| } | |
| ParagraphImpl::ParagraphImpl(const std::u16string& utf16text, | |
| ParagraphStyle style, | |
| TArray<Block> blocks, | |
| TArray<Placeholder> placeholders, | |
| sk_sp<FontCollection> fonts, | |
| sk_sp<SkUnicode> unicode) | |
| : ParagraphImpl(SkString(), | |
| std::move(style), | |
| std::move(blocks), | |
| std::move(placeholders), | |
| std::move(fonts), | |
| std::move(unicode)) | |
| { | |
| SkASSERT(fUnicode); | |
| fText = SkUnicode::convertUtf16ToUtf8(utf16text); | |
| } | |
| ParagraphImpl::~ParagraphImpl() = default; | |
| int32_t ParagraphImpl::unresolvedGlyphs() { | |
| if (fState < kShaped) { | |
| return -1; | |
| } | |
| return fUnresolvedGlyphs; | |
| } | |
| std::unordered_set<SkUnichar> ParagraphImpl::unresolvedCodepoints() { | |
| return fUnresolvedCodepoints; | |
| } | |
| void ParagraphImpl::addUnresolvedCodepoints(TextRange textRange) { | |
| fUnicode->forEachCodepoint( | |
| &fText[textRange.start], textRange.width(), | |
| [&](SkUnichar unichar, int32_t start, int32_t end, int32_t count) { | |
| fUnresolvedCodepoints.emplace(unichar); | |
| } | |
| ); | |
| } | |
| void ParagraphImpl::layout(SkScalar rawWidth) { | |
| // TODO: This rounding is done to match Flutter tests. Must be removed... | |
| auto floorWidth = rawWidth; | |
| if (getApplyRoundingHack()) { | |
| floorWidth = SkScalarFloorToScalar(floorWidth); | |
| } | |
| if ((!SkIsFinite(rawWidth) || fLongestLine <= floorWidth) && | |
| fState >= kLineBroken && | |
| fLines.size() == 1 && fLines.front().ellipsis() == nullptr) { | |
| // Most common case: one line of text (and one line is never justified, so no cluster shifts) | |
| // We cannot mark it as kLineBroken because the new width can be bigger than the old width | |
| fWidth = floorWidth; | |
| fState = kShaped; | |
| } else if (fState >= kLineBroken && fOldWidth != floorWidth) { | |
| // We can use the results from SkShaper but have to do EVERYTHING ELSE again | |
| fState = kShaped; | |
| } else { | |
| // Nothing changed case: we can reuse the data from the last layout | |
| } | |
| if (fState < kShaped) { | |
| // Check if we have the text in the cache and don't need to shape it again | |
| if (!fFontCollection->getParagraphCache()->findParagraph(this)) { | |
| if (fState < kIndexed) { | |
| // This only happens once at the first layout; the text is immutable | |
| // and there is no reason to repeat it | |
| if (this->computeCodeUnitProperties()) { | |
| fState = kIndexed; | |
| } | |
| } | |
| this->fRuns.clear(); | |
| this->fClusters.clear(); | |
| this->fClustersIndexFromCodeUnit.clear(); | |
| this->fClustersIndexFromCodeUnit.push_back_n(fText.size() + 1, EMPTY_INDEX); | |
| if (!this->shapeTextIntoEndlessLine()) { | |
| this->resetContext(); | |
| // TODO: merge the two next calls - they always come together | |
| this->resolveStrut(); | |
| this->computeEmptyMetrics(); | |
| this->fLines.clear(); | |
| // Set the important values that are not zero | |
| fWidth = floorWidth; | |
| fHeight = fEmptyMetrics.height(); | |
| if (fParagraphStyle.getStrutStyle().getStrutEnabled() && | |
| fParagraphStyle.getStrutStyle().getForceStrutHeight()) { | |
| fHeight = fStrutMetrics.height(); | |
| } | |
| fAlphabeticBaseline = fEmptyMetrics.alphabeticBaseline(); | |
| fIdeographicBaseline = fEmptyMetrics.ideographicBaseline(); | |
| fLongestLine = FLT_MIN - FLT_MAX; // That is what flutter has | |
| fMinIntrinsicWidth = 0; | |
| fMaxIntrinsicWidth = 0; | |
| this->fOldWidth = floorWidth; | |
| this->fOldHeight = this->fHeight; | |
| return; | |
| } else { | |
| // Add the paragraph to the cache | |
| fFontCollection->getParagraphCache()->updateParagraph(this); | |
| } | |
| } | |
| fState = kShaped; | |
| } | |
| if (fState == kShaped) { | |
| this->resetContext(); | |
| this->resolveStrut(); | |
| this->computeEmptyMetrics(); | |
| this->fLines.clear(); | |
| this->breakShapedTextIntoLines(floorWidth); | |
| fState = kLineBroken; | |
| } | |
| if (fState == kLineBroken) { | |
| // Build the picture lazily not until we actually have to paint (or never) | |
| this->resetShifts(); | |
| this->formatLines(fWidth); | |
| fState = kFormatted; | |
| } | |
| this->fOldWidth = floorWidth; | |
| this->fOldHeight = this->fHeight; | |
| if (getApplyRoundingHack()) { | |
| // TODO: This rounding is done to match Flutter tests. Must be removed... | |
| fMinIntrinsicWidth = littleRound(fMinIntrinsicWidth); | |
| fMaxIntrinsicWidth = littleRound(fMaxIntrinsicWidth); | |
| } | |
| // TODO: This is strictly Flutter thing. Must be factored out into some flutter code | |
| if (fParagraphStyle.getMaxLines() == 1 || | |
| (fParagraphStyle.unlimited_lines() && fParagraphStyle.ellipsized())) { | |
| fMinIntrinsicWidth = fMaxIntrinsicWidth; | |
| } | |
| // TODO: Since min and max are calculated differently it's possible to get a rounding error | |
| // that would make min > max. Sort it out later, make it the same for now | |
| if (fMaxIntrinsicWidth < fMinIntrinsicWidth) { | |
| fMaxIntrinsicWidth = fMinIntrinsicWidth; | |
| } | |
| //SkDebugf("layout('%s', %f): %f %f\n", fText.c_str(), rawWidth, fMinIntrinsicWidth, fMaxIntrinsicWidth); | |
| } | |
| void ParagraphImpl::paint(SkCanvas* canvas, SkScalar x, SkScalar y) { | |
| CanvasParagraphPainter painter(canvas); | |
| paint(&painter, x, y); | |
| } | |
| void ParagraphImpl::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) { | |
| for (auto& line : fLines) { | |
| line.paint(painter, x, y); | |
| } | |
| } | |
| void ParagraphImpl::resetContext() { | |
| fAlphabeticBaseline = 0; | |
| fHeight = 0; | |
| fWidth = 0; | |
| fIdeographicBaseline = 0; | |
| fMaxIntrinsicWidth = 0; | |
| fMinIntrinsicWidth = 0; | |
| fLongestLine = 0; | |
| fMaxWidthWithTrailingSpaces = 0; | |
| fExceededMaxLines = false; | |
| } | |
| // shapeTextIntoEndlessLine is the thing that calls this method | |
| bool ParagraphImpl::computeCodeUnitProperties() { | |
| if (nullptr == fUnicode) { | |
| return false; | |
| } | |
| // Get bidi regions | |
| auto textDirection = fParagraphStyle.getTextDirection() == TextDirection::kLtr | |
| ? SkUnicode::TextDirection::kLTR | |
| : SkUnicode::TextDirection::kRTL; | |
| if (!fUnicode->getBidiRegions(fText.c_str(), fText.size(), textDirection, &fBidiRegions)) { | |
| return false; | |
| } | |
| // Collect all spaces and some extra information | |
| // (and also substitute \t with a space while we are at it) | |
| if (!fUnicode->computeCodeUnitFlags(&fText[0], | |
| fText.size(), | |
| this->paragraphStyle().getReplaceTabCharacters(), | |
| &fCodeUnitProperties)) { | |
| return false; | |
| } | |
| // Get some information about trailing spaces / hard line breaks | |
| fTrailingSpaces = fText.size(); | |
| TextIndex firstWhitespace = EMPTY_INDEX; | |
| for (int i = 0; i < fCodeUnitProperties.size(); ++i) { | |
| auto flags = fCodeUnitProperties[i]; | |
| if (SkUnicode::hasPartOfWhiteSpaceBreakFlag(flags)) { | |
| if (fTrailingSpaces == fText.size()) { | |
| fTrailingSpaces = i; | |
| } | |
| if (firstWhitespace == EMPTY_INDEX) { | |
| firstWhitespace = i; | |
| } | |
| } else { | |
| fTrailingSpaces = fText.size(); | |
| } | |
| if (SkUnicode::hasHardLineBreakFlag(flags)) { | |
| fHasLineBreaks = true; | |
| } | |
| } | |
| if (firstWhitespace < fTrailingSpaces) { | |
| fHasWhitespacesInside = true; | |
| } | |
| return true; | |
| } | |
| static bool is_ascii_7bit_space(int c) { | |
| SkASSERT(c >= 0 && c <= 127); | |
| // Extracted from https://en.wikipedia.org/wiki/Whitespace_character | |
| // | |
| enum WS { | |
| kHT = 9, | |
| kLF = 10, | |
| kVT = 11, | |
| kFF = 12, | |
| kCR = 13, | |
| kSP = 32, // too big to use as shift | |
| }; | |
| #define M(shift) (1 << (shift)) | |
| constexpr uint32_t kSpaceMask = M(kHT) | M(kLF) | M(kVT) | M(kFF) | M(kCR); | |
| // we check for Space (32) explicitly, since it is too large to shift | |
| return (c == kSP) || (c <= 31 && (kSpaceMask & M(c))); | |
| #undef M | |
| } | |
| Cluster::Cluster(ParagraphImpl* owner, | |
| RunIndex runIndex, | |
| size_t start, | |
| size_t end, | |
| SkSpan<const char> text, | |
| SkScalar width, | |
| SkScalar height) | |
| : fOwner(owner) | |
| , fRunIndex(runIndex) | |
| , fTextRange(text.begin() - fOwner->text().begin(), text.end() - fOwner->text().begin()) | |
| , fGraphemeRange(EMPTY_RANGE) | |
| , fStart(start) | |
| , fEnd(end) | |
| , fWidth(width) | |
| , fHeight(height) | |
| , fHalfLetterSpacing(0.0) | |
| , fIsIdeographic(false) { | |
| size_t whiteSpacesBreakLen = 0; | |
| size_t intraWordBreakLen = 0; | |
| const char* ch = text.data(); | |
| const char* chEnd = ch + text.size(); | |
| if (chEnd - ch == 1 && *(const unsigned char*)ch <= 0x7F) { | |
| // I am not even sure it's worth it if we do not save a unicode call | |
| if (is_ascii_7bit_space(*ch)) { | |
| ++whiteSpacesBreakLen; | |
| } | |
| } else { | |
| for (auto i = fTextRange.start; i < fTextRange.end; ++i) { | |
| if (fOwner->codeUnitHasProperty(i, SkUnicode::CodeUnitFlags::kPartOfWhiteSpaceBreak)) { | |
| ++whiteSpacesBreakLen; | |
| } | |
| if (fOwner->codeUnitHasProperty(i, SkUnicode::CodeUnitFlags::kPartOfIntraWordBreak)) { | |
| ++intraWordBreakLen; | |
| } | |
| if (fOwner->codeUnitHasProperty(i, SkUnicode::CodeUnitFlags::kIdeographic)) { | |
| fIsIdeographic = true; | |
| } | |
| } | |
| } | |
| fIsWhiteSpaceBreak = whiteSpacesBreakLen == fTextRange.width(); | |
| fIsIntraWordBreak = intraWordBreakLen == fTextRange.width(); | |
| fIsHardBreak = fOwner->codeUnitHasProperty(fTextRange.end, | |
| SkUnicode::CodeUnitFlags::kHardLineBreakBefore); | |
| } | |
| SkScalar Run::calculateWidth(size_t start, size_t end, bool clip) const { | |
| SkASSERT(start <= end); | |
| // clip |= end == size(); // Clip at the end of the run? | |
| auto correction = 0.0f; | |
| if (end > start && !fJustificationShifts.empty()) { | |
| // This is not a typo: we are using Point as a pair of SkScalars | |
| correction = fJustificationShifts[end - 1].fX - | |
| fJustificationShifts[start].fY; | |
| } | |
| return posX(end) - posX(start) + correction; | |
| } | |
| // In some cases we apply spacing to glyphs first and then build the cluster table, in some we do | |
| // the opposite - just to optimize the most common case. | |
| void ParagraphImpl::applySpacingAndBuildClusterTable() { | |
| // Check all text styles to see what we have to do (if anything) | |
| size_t letterSpacingStyles = 0; | |
| bool hasWordSpacing = false; | |
| for (auto& block : fTextStyles) { | |
| if (block.fRange.width() > 0) { | |
| if (!SkScalarNearlyZero(block.fStyle.getLetterSpacing())) { | |
| ++letterSpacingStyles; | |
| } | |
| if (!SkScalarNearlyZero(block.fStyle.getWordSpacing())) { | |
| hasWordSpacing = true; | |
| } | |
| } | |
| } | |
| if (letterSpacingStyles == 0 && !hasWordSpacing) { | |
| // We don't have to do anything about spacing (most common case) | |
| this->buildClusterTable(); | |
| return; | |
| } | |
| // Simple case: we have to letter space the entire paragraph (second most common case) | |
| // With one exception (there are whitespaces, and we have to space them as CSS spec requires) | |
| // which is treated as a common case down below | |
| if (letterSpacingStyles == 1 && !hasWordSpacing && fTextStyles.size() == 1 && | |
| fTextStyles[0].fRange.width() == fText.size() && fRuns.size() == 1 && | |
| !(this->fHasWhitespacesInside && this->fParagraphStyle.getLetterSpacingByCSSSpec()) | |
| ) { | |
| auto& run = fRuns[0]; | |
| auto& style = fTextStyles[0].fStyle; | |
| if (!run.isCursiveScript()) { | |
| // Do not apply letter spacing for script languages (no exception here) | |
| run.addLetterSpacesEvenly(style.getLetterSpacing()); | |
| } | |
| this->buildClusterTable(); | |
| // This is something Flutter requires | |
| for (auto& cluster : fClusters) { | |
| cluster.setHalfLetterSpacing(style.getLetterSpacing() / 2); | |
| } | |
| return; | |
| } | |
| // The complex case: many text styles with spacing (possibly not adjusted to glyphs) | |
| this->buildClusterTable(); | |
| // Walk through all the clusters in the direction of shaped text | |
| // (we have to walk through the styles in the same order, too) | |
| // Not breaking the iteration on every run! | |
| SkScalar shift = 0; | |
| bool soFarWhitespacesOnly = true; | |
| bool wordSpacingPending = false; | |
| Cluster* lastSpaceCluster = nullptr; | |
| for (auto& run : fRuns) { | |
| // Skip placeholder runs | |
| if (run.isPlaceholder()) { | |
| continue; | |
| } | |
| run.iterateThroughClusters([this, &run, &shift, &soFarWhitespacesOnly, &wordSpacingPending, &lastSpaceCluster](Cluster* cluster) { | |
| // Shift the cluster (shift collected from the previous clusters) | |
| run.shift(cluster, shift); | |
| // Synchronize styles (one cluster can be covered by few styles) | |
| Block* currentStyle = fTextStyles.begin(); | |
| while (!cluster->startsIn(currentStyle->fRange)) { | |
| currentStyle++; | |
| SkASSERT(currentStyle != fTextStyles.end()); | |
| } | |
| SkASSERT(!currentStyle->fStyle.isPlaceholder()); | |
| // Process word spacing | |
| if (currentStyle->fStyle.getWordSpacing() != 0) { | |
| if (cluster->isWhitespaceBreak() && cluster->isSoftBreak()) { | |
| if (!soFarWhitespacesOnly) { | |
| lastSpaceCluster = cluster; | |
| wordSpacingPending = true; | |
| } | |
| } else if (wordSpacingPending) { | |
| SkScalar spacing = currentStyle->fStyle.getWordSpacing(); | |
| if (cluster->fRunIndex != lastSpaceCluster->fRunIndex) { | |
| // If the last space cluster belongs to the previous run | |
| // we have to extend that cluster and that run | |
| lastSpaceCluster->run().addSpacesAtTheEnd(spacing, lastSpaceCluster); | |
| lastSpaceCluster->run().extend(lastSpaceCluster, spacing); | |
| } else { | |
| run.addSpacesAtTheEnd(spacing, lastSpaceCluster); | |
| } | |
| run.shift(cluster, spacing); | |
| shift += spacing; | |
| wordSpacingPending = false; | |
| } | |
| } | |
| // We do not apply letter spacing for script languages | |
| // except for one case (cluster is whitespace, and we have to follow CSS spec) | |
| if ((currentStyle->fStyle.getLetterSpacing() != 0)) { | |
| if (!run.isCursiveScript() || | |
| (cluster->isWhitespaceBreak() && | |
| this->fParagraphStyle.getLetterSpacingByCSSSpec())) { | |
| shift += | |
| run.addLetterSpacesEvenly(currentStyle->fStyle.getLetterSpacing(), cluster); | |
| } | |
| } | |
| if (soFarWhitespacesOnly && !cluster->isWhitespaceBreak()) { | |
| soFarWhitespacesOnly = false; | |
| } | |
| }); | |
| } | |
| } | |
| // Clusters in the order of the input text | |
| void ParagraphImpl::buildClusterTable() { | |
| // It's possible that one grapheme includes few runs; we cannot handle it | |
| // so we break graphemes by the runs instead | |
| // It's not the ideal solution and has to be revisited later | |
| int cluster_count = 1; | |
| for (auto& run : fRuns) { | |
| // A 0-glyph run still produces one 0-width cluster covering its text range | |
| cluster_count += (run.isPlaceholder() || run.size() == 0) ? 1 : run.size(); | |
| fCodeUnitProperties[run.fTextRange.start] |= SkUnicode::CodeUnitFlags::kGraphemeStart; | |
| fCodeUnitProperties[run.fTextRange.start] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart; | |
| } | |
| if (!fRuns.empty()) { | |
| fCodeUnitProperties[fRuns.back().textRange().end] |= SkUnicode::CodeUnitFlags::kGraphemeStart; | |
| fCodeUnitProperties[fRuns.back().textRange().end] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart; | |
| } | |
| fClusters.reserve_exact(fClusters.size() + cluster_count); | |
| // Walk through all the run in the direction of input text | |
| for (auto& run : fRuns) { | |
| auto runIndex = run.index(); | |
| auto runStart = fClusters.size(); | |
| if (run.isPlaceholder()) { | |
| // Add info to cluster indexes table (text -> cluster) | |
| for (auto i = run.textRange().start; i < run.textRange().end; ++i) { | |
| fClustersIndexFromCodeUnit[i] = fClusters.size(); | |
| } | |
| // There are no glyphs but we want to have one cluster | |
| fClusters.emplace_back(this, runIndex, 0ul, 1ul, this->text(run.textRange()), run.advance().fX, run.advance().fY); | |
| fCodeUnitProperties[run.textRange().start] |= SkUnicode::CodeUnitFlags::kSoftLineBreakBefore; | |
| fCodeUnitProperties[run.textRange().end] |= SkUnicode::CodeUnitFlags::kSoftLineBreakBefore; | |
| } else { | |
| // Walk through the glyph in the direction of input text | |
| run.iterateThroughClustersInTextOrder([runIndex, this](size_t glyphStart, | |
| size_t glyphEnd, | |
| size_t charStart, | |
| size_t charEnd, | |
| SkScalar width, | |
| SkScalar height) { | |
| SkASSERT(charEnd >= charStart); | |
| if (charStart >= charEnd) { | |
| // Empty cluster text ranges produce no code units or glyphs and are ignored | |
| return; | |
| } | |
| bool hasZeroWidth = false; | |
| bool hasVisible = false; | |
| const char* p = fText.c_str() + charStart; | |
| const char* end = fText.c_str() + charEnd; | |
| while (p < end) { | |
| SkUnichar u = SkUTF::NextUTF8(&p, end); | |
| if (u < 0) { | |
| break; | |
| } | |
| if (fUnicode->isDefaultIgnorable(u)) { | |
| hasZeroWidth = true; | |
| } else { | |
| hasVisible = true; | |
| } | |
| } | |
| if (!hasZeroWidth || !hasVisible) { | |
| for (auto i = charStart; i < charEnd; ++i) { | |
| fClustersIndexFromCodeUnit[i] = fClusters.size(); | |
| } | |
| SkSpan<const char> text(fText.c_str() + charStart, charEnd - charStart); | |
| fClusters.emplace_back( | |
| this, runIndex, glyphStart, glyphEnd, text, width, height); | |
| fCodeUnitProperties[charStart] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart; | |
| return; | |
| } | |
| // Split compound clusters into visible glyphs and zero-width sub-clusters so | |
| // non-printing codepoints do not expand hit-test bounds or shift caret positioning | |
| struct SubCluster { | |
| TextRange range; | |
| bool isZeroWidth; | |
| }; | |
| skia_private::STArray<8, SubCluster> subClusters; | |
| size_t segStart = charStart; | |
| while (segStart < charEnd) { | |
| size_t segEnd = segStart + 1; | |
| while (segEnd < charEnd && | |
| (fCodeUnitProperties[segEnd] & | |
| SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) { | |
| ++segEnd; | |
| } | |
| bool segZeroWidth = true; | |
| const char* segPtr = fText.c_str() + segStart; | |
| const char* segEndPtr = fText.c_str() + segEnd; | |
| while (segPtr < segEndPtr) { | |
| SkUnichar u = SkUTF::NextUTF8(&segPtr, segEndPtr); | |
| if (u < 0 || !fUnicode->isDefaultIgnorable(u)) { | |
| segZeroWidth = false; | |
| break; | |
| } | |
| } | |
| if (!subClusters.empty() && subClusters.back().isZeroWidth == segZeroWidth) { | |
| subClusters.back().range.end = segEnd; | |
| } else { | |
| subClusters.push_back({ TextRange(segStart, segEnd), segZeroWidth }); | |
| } | |
| segStart = segEnd; | |
| } | |
| size_t visibleCount = 0; | |
| for (const auto& sub : subClusters) { | |
| if (!sub.isZeroWidth) { | |
| ++visibleCount; | |
| } | |
| } | |
| const bool ltr = this->run(runIndex).leftToRight(); | |
| bool visibleEmitted = false; | |
| for (const auto& sub : subClusters) { | |
| size_t gStart = glyphStart; | |
| size_t gEnd = glyphEnd; | |
| SkScalar subWidth = 0; | |
| if (sub.isZeroWidth) { | |
| if (ltr) { | |
| gStart = visibleEmitted ? glyphEnd : glyphStart; | |
| } else { | |
| gStart = visibleEmitted ? glyphStart : glyphEnd; | |
| } | |
| gEnd = gStart; | |
| subWidth = 0; | |
| } else { | |
| visibleEmitted = true; | |
| subWidth = visibleCount > 1 ? width / visibleCount : width; | |
| } | |
| for (auto i = sub.range.start; i < sub.range.end; ++i) { | |
| fClustersIndexFromCodeUnit[i] = fClusters.size(); | |
| } | |
| SkSpan<const char> subText(fText.c_str() + sub.range.start, sub.range.width()); | |
| fClusters.emplace_back( | |
| this, runIndex, gStart, gEnd, subText, subWidth, height); | |
| fCodeUnitProperties[sub.range.start] |= | |
| SkUnicode::CodeUnitFlags::kGlyphClusterStart; | |
| } | |
| }); | |
| } | |
| fCodeUnitProperties[run.textRange().start] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart; | |
| run.setClusterRange(runStart, fClusters.size()); | |
| fMaxIntrinsicWidth += run.advance().fX; | |
| } | |
| fClustersIndexFromCodeUnit[fText.size()] = fClusters.size(); | |
| fClusters.emplace_back(this, EMPTY_RUN, 0, 0, this->text({fText.size(), fText.size()}), 0, 0); | |
| // Invariant: guarantee no code unit remains EMPTY_INDEX in fClustersIndexFromCodeUnit. | |
| // Any unmapped code units (0-glyph runs or dropped controls) attach to the last valid cluster | |
| ClusterIndex lastValidCluster = 0; | |
| for (size_t i = 0; i <= fText.size(); ++i) { | |
| if (fClustersIndexFromCodeUnit[i] != EMPTY_INDEX) { | |
| lastValidCluster = fClustersIndexFromCodeUnit[i]; | |
| } else { | |
| fClustersIndexFromCodeUnit[i] = lastValidCluster; | |
| } | |
| } | |
| } | |
| bool ParagraphImpl::shapeTextIntoEndlessLine() { | |
| if (fText.size() == 0) { | |
| return false; | |
| } | |
| fUnresolvedCodepoints.clear(); | |
| fFontSwitches.clear(); | |
| OneLineShaper oneLineShaper(this); | |
| auto result = oneLineShaper.shape(); | |
| fUnresolvedGlyphs = oneLineShaper.unresolvedGlyphs(); | |
| this->applySpacingAndBuildClusterTable(); | |
| return result; | |
| } | |
| void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) { | |
| if (!fHasLineBreaks && | |
| !fHasWhitespacesInside && | |
| fPlaceholders.size() == 1 && | |
| fRuns.size() == 1 && fRuns[0].fAdvance.fX <= maxWidth) { | |
| // This is a short version of a line breaking when we know that: | |
| // 1. We have only one line of text | |
| // 2. It's shaped into a single run | |
| // 3. There are no placeholders | |
| // 4. There are no linebreaks (which will format text into multiple lines) | |
| // 5. There are no whitespaces so the minIntrinsicWidth=maxIntrinsicWidth | |
| // (To think about that, the last condition is not quite right; | |
| // we should calculate minIntrinsicWidth by soft line breaks. | |
| // However, it's how it's done in Flutter now) | |
| auto& run = this->fRuns[0]; | |
| auto advance = run.advance(); | |
| auto textRange = TextRange(0, this->text().size()); | |
| auto textExcludingSpaces = TextRange(0, fTrailingSpaces); | |
| InternalLineMetrics metrics(this->strutForceHeight()); | |
| metrics.add(&run); | |
| auto disableFirstAscent = this->paragraphStyle().getTextHeightBehavior() & | |
| TextHeightBehavior::kDisableFirstAscent; | |
| auto disableLastDescent = this->paragraphStyle().getTextHeightBehavior() & | |
| TextHeightBehavior::kDisableLastDescent; | |
| if (disableFirstAscent) { | |
| metrics.fAscent = metrics.fRawAscent; | |
| } | |
| if (disableLastDescent) { | |
| metrics.fDescent = metrics.fRawDescent; | |
| } | |
| if (this->strutEnabled()) { | |
| this->strutMetrics().updateLineMetrics(metrics); | |
| } | |
| ClusterIndex trailingSpaces = fClusters.size(); | |
| do { | |
| --trailingSpaces; | |
| auto& cluster = fClusters[trailingSpaces]; | |
| if (!cluster.isWhitespaceBreak()) { | |
| ++trailingSpaces; | |
| break; | |
| } | |
| advance.fX -= cluster.width(); | |
| } while (trailingSpaces != 0); | |
| advance.fY = metrics.height(); | |
| auto clusterRange = ClusterRange(0, trailingSpaces); | |
| auto clusterRangeWithGhosts = ClusterRange(0, this->clusters().size() - 1); | |
| this->addLine(SkPoint::Make(0, 0), advance, | |
| textExcludingSpaces, textRange, textRange, | |
| clusterRange, clusterRangeWithGhosts, run.advance().x(), | |
| metrics); | |
| fLongestLine = nearlyZero(advance.fX) ? run.advance().fX : advance.fX; | |
| fHeight = advance.fY; | |
| fWidth = maxWidth; | |
| fMaxIntrinsicWidth = run.advance().fX; | |
| fMinIntrinsicWidth = advance.fX; | |
| fAlphabeticBaseline = fLines.empty() ? fEmptyMetrics.alphabeticBaseline() : fLines.front().alphabeticBaseline(); | |
| fIdeographicBaseline = fLines.empty() ? fEmptyMetrics.ideographicBaseline() : fLines.front().ideographicBaseline(); | |
| fExceededMaxLines = false; | |
| return; | |
| } | |
| TextWrapper textWrapper; | |
| textWrapper.breakTextIntoLines( | |
| this, | |
| maxWidth, | |
| [&](TextRange textExcludingSpaces, | |
| TextRange text, | |
| TextRange textWithNewlines, | |
| ClusterRange clusters, | |
| ClusterRange clustersWithGhosts, | |
| SkScalar widthWithSpaces, | |
| size_t startPos, | |
| size_t endPos, | |
| SkVector offset, | |
| SkVector advance, | |
| InternalLineMetrics metrics, | |
| bool addEllipsis) { | |
| // TODO: Take in account clipped edges | |
| auto& line = this->addLine(offset, advance, textExcludingSpaces, text, textWithNewlines, clusters, clustersWithGhosts, widthWithSpaces, metrics); | |
| if (addEllipsis) { | |
| line.createEllipsis(maxWidth, this->getEllipsis(), true); | |
| } | |
| if (this->paragraphStyle().getRenderSoftHyphens()) { | |
| line.createSoftHyphen(); | |
| } | |
| fLongestLine = std::max(fLongestLine, nearlyZero(line.width()) ? widthWithSpaces : line.width()); | |
| }); | |
| fHeight = textWrapper.height(); | |
| fWidth = maxWidth; | |
| fMaxIntrinsicWidth = textWrapper.maxIntrinsicWidth(); | |
| fMinIntrinsicWidth = textWrapper.minIntrinsicWidth(); | |
| fAlphabeticBaseline = fLines.empty() ? fEmptyMetrics.alphabeticBaseline() : fLines.front().alphabeticBaseline(); | |
| fIdeographicBaseline = fLines.empty() ? fEmptyMetrics.ideographicBaseline() : fLines.front().ideographicBaseline(); | |
| fExceededMaxLines = textWrapper.exceededMaxLines(); | |
| } | |
| void ParagraphImpl::formatLines(SkScalar maxWidth) { | |
| auto effectiveAlign = fParagraphStyle.effective_align(); | |
| const bool isLeftAligned = effectiveAlign == TextAlign::kLeft | |
| || (effectiveAlign == TextAlign::kJustify && fParagraphStyle.getTextDirection() == TextDirection::kLtr); | |
| if (!SkIsFinite(maxWidth) && !isLeftAligned) { | |
| // Special case: clean all text in case of maxWidth == INF & align != left | |
| // We had to go through shaping though because we need all the measurement numbers | |
| fLines.clear(); | |
| return; | |
| } | |
| for (auto& line : fLines) { | |
| line.format(effectiveAlign, maxWidth); | |
| } | |
| } | |
| void ParagraphImpl::resolveStrut() { | |
| auto strutStyle = this->paragraphStyle().getStrutStyle(); | |
| if (!strutStyle.getStrutEnabled() || strutStyle.getFontSize() < 0) { | |
| return; | |
| } | |
| std::vector<sk_sp<SkTypeface>> typefaces = fFontCollection->findTypefaces(strutStyle.getFontFamilies(), strutStyle.getFontStyle(), std::nullopt); | |
| if (typefaces.empty()) { | |
| SkDEBUGF("Could not resolve strut font\n"); | |
| return; | |
| } | |
| SkFont font(typefaces.front(), strutStyle.getFontSize()); | |
| SkFontMetrics metrics; | |
| font.getMetrics(&metrics); | |
| const SkScalar strutLeading = strutStyle.getLeading() < 0 ? 0 : strutStyle.getLeading() * strutStyle.getFontSize(); | |
| if (strutStyle.getHeightOverride()) { | |
| SkScalar strutAscent = 0.0f; | |
| SkScalar strutDescent = 0.0f; | |
| // The half leading flag doesn't take effect unless there's height override. | |
| if (strutStyle.getHalfLeading()) { | |
| const auto occupiedHeight = metrics.fDescent - metrics.fAscent; | |
| auto flexibleHeight = strutStyle.getHeight() * strutStyle.getFontSize() - occupiedHeight; | |
| // Distribute the flexible height evenly over and under. | |
| flexibleHeight /= 2; | |
| strutAscent = metrics.fAscent - flexibleHeight; | |
| strutDescent = metrics.fDescent + flexibleHeight; | |
| } else { | |
| const SkScalar strutMetricsHeight = metrics.fDescent - metrics.fAscent + metrics.fLeading; | |
| const auto strutHeightMultiplier = strutMetricsHeight == 0 | |
| ? strutStyle.getHeight() | |
| : strutStyle.getHeight() * strutStyle.getFontSize() / strutMetricsHeight; | |
| strutAscent = metrics.fAscent * strutHeightMultiplier; | |
| strutDescent = metrics.fDescent * strutHeightMultiplier; | |
| } | |
| fStrutMetrics = InternalLineMetrics( | |
| strutAscent, | |
| strutDescent, | |
| strutLeading, | |
| metrics.fAscent, metrics.fDescent, metrics.fLeading); | |
| } else { | |
| fStrutMetrics = InternalLineMetrics( | |
| metrics.fAscent, | |
| metrics.fDescent, | |
| strutLeading); | |
| } | |
| fStrutMetrics.setForceStrut(this->paragraphStyle().getStrutStyle().getForceStrutHeight()); | |
| } | |
| BlockRange ParagraphImpl::findAllBlocks(TextRange textRange) { | |
| BlockIndex begin = EMPTY_BLOCK; | |
| BlockIndex end = EMPTY_BLOCK; | |
| for (int index = 0; index < fTextStyles.size(); ++index) { | |
| auto& block = fTextStyles[index]; | |
| if (block.fRange.end <= textRange.start) { | |
| continue; | |
| } | |
| if (block.fRange.start >= textRange.end) { | |
| break; | |
| } | |
| if (begin == EMPTY_BLOCK) { | |
| begin = index; | |
| } | |
| end = index; | |
| } | |
| if (begin == EMPTY_INDEX || end == EMPTY_INDEX) { | |
| // It's possible if some text is not covered with any text style | |
| // Not in Flutter but in direct use of SkParagraph | |
| return EMPTY_RANGE; | |
| } | |
| return { begin, end + 1 }; | |
| } | |
| TextLine& ParagraphImpl::addLine(SkVector offset, | |
| SkVector advance, | |
| TextRange textExcludingSpaces, | |
| TextRange text, | |
| TextRange textIncludingNewLines, | |
| ClusterRange clusters, | |
| ClusterRange clustersWithGhosts, | |
| SkScalar widthWithSpaces, | |
| InternalLineMetrics sizes) { | |
| // Define a list of styles that covers the line | |
| auto blocks = findAllBlocks(textExcludingSpaces); | |
| return fLines.emplace_back(this, offset, advance, blocks, | |
| textExcludingSpaces, text, textIncludingNewLines, | |
| clusters, clustersWithGhosts, widthWithSpaces, 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 (fText.isEmpty()) { | |
| if (start == 0 && end > 0) { | |
| // On account of implied "\n" that is always at the end of the text | |
| //SkDebugf("getRectsForRange(%d, %d): %f\n", start, end, fHeight); | |
| results.emplace_back(SkRect::MakeXYWH(0, 0, 0, fHeight), fParagraphStyle.getTextDirection()); | |
| } | |
| return results; | |
| } | |
| this->ensureUTF16Mapping(); | |
| if (start >= end || start > SkToSizeT(fUTF8IndexForUTF16Index.size()) || end == 0) { | |
| return results; | |
| } | |
| // Adjust the text to grapheme edges | |
| // Apparently, text editor CAN move inside graphemes but CANNOT select a part of it. | |
| // I don't know why - the solution I have here returns an empty box for every query that | |
| // does not contain an end of a grapheme. | |
| // Once a cursor is inside a complex grapheme I can press backspace and cause trouble. | |
| // To avoid any problems, I will not allow any selection of a part of a grapheme. | |
| // One flutter test fails because of it but the editing experience is correct | |
| // (although you have to press the cursor many times before it moves to the next grapheme). | |
| TextRange text(fText.size(), fText.size()); | |
| // TODO: This is probably a temp change that makes SkParagraph work as TxtLib | |
| // (so we can compare the results). We now include in the selection box only the graphemes | |
| // that belongs to the given [start:end) range entirely (not the ones that intersect with it) | |
| if (start < SkToSizeT(fUTF8IndexForUTF16Index.size())) { | |
| auto utf8 = fUTF8IndexForUTF16Index[start]; | |
| // If start points to a trailing surrogate, skip it | |
| if (start > 0 && fUTF8IndexForUTF16Index[start - 1] == utf8) { | |
| utf8 = fUTF8IndexForUTF16Index[start + 1]; | |
| } | |
| text.start = this->findNextGraphemeBoundary(utf8); | |
| } | |
| if (end < SkToSizeT(fUTF8IndexForUTF16Index.size())) { | |
| auto utf8 = this->findPreviousGraphemeBoundary(fUTF8IndexForUTF16Index[end]); | |
| text.end = utf8; | |
| } | |
| //SkDebugf("getRectsForRange(%d,%d) -> (%d:%d)\n", start, end, text.start, text.end); | |
| for (auto& line : fLines) { | |
| auto lineText = line.textWithNewlines(); | |
| auto intersect = lineText * text; | |
| if (intersect.empty() && lineText.start != text.start) { | |
| continue; | |
| } | |
| line.getRectsForRange(intersect, rectHeightStyle, rectWidthStyle, results); | |
| } | |
| /* | |
| SkDebugf("getRectsForRange(%d, %d)\n", start, end); | |
| for (auto& r : results) { | |
| r.rect.fLeft = littleRound(r.rect.fLeft); | |
| r.rect.fRight = littleRound(r.rect.fRight); | |
| r.rect.fTop = littleRound(r.rect.fTop); | |
| r.rect.fBottom = littleRound(r.rect.fBottom); | |
| SkDebugf("[%f:%f * %f:%f]\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom); | |
| } | |
| */ | |
| return results; | |
| } | |
| std::vector<TextBox> ParagraphImpl::getRectsForPlaceholders() { | |
| std::vector<TextBox> boxes; | |
| if (fText.isEmpty()) { | |
| return boxes; | |
| } | |
| if (fPlaceholders.size() == 1) { | |
| // We always have one fake placeholder | |
| return boxes; | |
| } | |
| for (auto& line : fLines) { | |
| line.getRectsForPlaceholders(boxes); | |
| } | |
| /* | |
| SkDebugf("getRectsForPlaceholders('%s'): %d\n", fText.c_str(), boxes.size()); | |
| for (auto& r : boxes) { | |
| r.rect.fLeft = littleRound(r.rect.fLeft); | |
| r.rect.fRight = littleRound(r.rect.fRight); | |
| r.rect.fTop = littleRound(r.rect.fTop); | |
| r.rect.fBottom = littleRound(r.rect.fBottom); | |
| SkDebugf("[%f:%f * %f:%f] %s\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom, | |
| (r.direction == TextDirection::kLtr ? "left" : "right")); | |
| } | |
| */ | |
| return boxes; | |
| } | |
| // TODO: Optimize (save cluster <-> codepoint connection) | |
| PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) { | |
| if (fText.isEmpty()) { | |
| return {0, Affinity::kDownstream}; | |
| } | |
| this->ensureUTF16Mapping(); | |
| for (auto& line : fLines) { | |
| // Let's figure out if we can stop looking | |
| auto offsetY = line.offset().fY; | |
| if (dy >= offsetY + line.height() && &line != &fLines.back()) { | |
| // This line is not good enough | |
| continue; | |
| } | |
| // This is so far the the line vertically closest to our coordinates | |
| // (or the first one, or the only one - all the same) | |
| auto result = line.getGlyphPositionAtCoordinate(dx); | |
| //SkDebugf("getGlyphPositionAtCoordinate(%f, %f): %d %s\n", dx, dy, result.position, | |
| // result.affinity == Affinity::kUpstream ? "up" : "down"); | |
| return result; | |
| } | |
| return {0, Affinity::kDownstream}; | |
| } | |
| // 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) { | |
| if (fWords.empty()) { | |
| if (!fUnicode->getWords(fText.c_str(), fText.size(), nullptr, &fWords)) { | |
| return {0, 0 }; | |
| } | |
| } | |
| int32_t start = 0; | |
| int32_t end = 0; | |
| for (size_t i = 0; i < fWords.size(); ++i) { | |
| auto word = fWords[i]; | |
| if (word <= offset) { | |
| start = word; | |
| end = word; | |
| } else if (word > offset) { | |
| end = word; | |
| break; | |
| } | |
| } | |
| //SkDebugf("getWordBoundary(%d): %d - %d\n", offset, start, end); | |
| return { SkToU32(start), SkToU32(end) }; | |
| } | |
| void ParagraphImpl::getLineMetrics(std::vector<LineMetrics>& metrics) { | |
| metrics.clear(); | |
| for (auto& line : fLines) { | |
| metrics.emplace_back(line.getMetrics()); | |
| } | |
| } | |
| SkSpan<const char> ParagraphImpl::text(TextRange textRange) { | |
| SkASSERT(textRange.start <= fText.size() && textRange.end <= fText.size()); | |
| auto start = fText.c_str() + textRange.start; | |
| return SkSpan<const char>(start, textRange.width()); | |
| } | |
| SkSpan<Cluster> ParagraphImpl::clusters(ClusterRange clusterRange) { | |
| SkASSERT(clusterRange.start < SkToSizeT(fClusters.size()) && | |
| clusterRange.end <= SkToSizeT(fClusters.size())); | |
| return SkSpan<Cluster>(&fClusters[clusterRange.start], clusterRange.width()); | |
| } | |
| Cluster& ParagraphImpl::cluster(ClusterIndex clusterIndex) { | |
| SkASSERT(clusterIndex < SkToSizeT(fClusters.size())); | |
| return fClusters[clusterIndex]; | |
| } | |
| Run& ParagraphImpl::runByCluster(ClusterIndex clusterIndex) { | |
| auto start = cluster(clusterIndex); | |
| return this->run(start.fRunIndex); | |
| } | |
| SkSpan<Block> ParagraphImpl::blocks(BlockRange blockRange) { | |
| SkASSERT(blockRange.start < SkToSizeT(fTextStyles.size()) && | |
| blockRange.end <= SkToSizeT(fTextStyles.size())); | |
| return SkSpan<Block>(&fTextStyles[blockRange.start], blockRange.width()); | |
| } | |
| Block& ParagraphImpl::block(BlockIndex blockIndex) { | |
| SkASSERT(blockIndex < SkToSizeT(fTextStyles.size())); | |
| return fTextStyles[blockIndex]; | |
| } | |
| void ParagraphImpl::setState(InternalState state) { | |
| if (fState <= state) { | |
| fState = state; | |
| return; | |
| } | |
| fState = state; | |
| switch (fState) { | |
| case kUnknown: | |
| SkASSERT(false); | |
| /* | |
| // The text is immutable and so are all the text indexing properties | |
| // taken from SkUnicode | |
| fCodeUnitProperties.reset(); | |
| fWords.clear(); | |
| fBidiRegions.clear(); | |
| fUTF8IndexForUTF16Index.reset(); | |
| fUTF16IndexForUTF8Index.reset(); | |
| */ | |
| [[fallthrough]]; | |
| case kIndexed: | |
| fRuns.clear(); | |
| fClusters.clear(); | |
| [[fallthrough]]; | |
| case kShaped: | |
| fLines.clear(); | |
| [[fallthrough]]; | |
| case kLineBroken: | |
| fPicture = nullptr; | |
| [[fallthrough]]; | |
| default: | |
| break; | |
| } | |
| } | |
| void ParagraphImpl::computeEmptyMetrics() { | |
| // The empty metrics is used to define the height of the empty lines | |
| // Unfortunately, Flutter has 2 different cases for that: | |
| // 1. An empty line inside the text | |
| // 2. An empty paragraph | |
| // In the first case SkParagraph takes the metrics from the default paragraph style | |
| // In the second case it should take it from the current text style | |
| bool emptyParagraph = fRuns.empty(); | |
| TextStyle textStyle = paragraphStyle().getTextStyle(); | |
| if (emptyParagraph && !fTextStyles.empty()) { | |
| textStyle = fTextStyles.back().fStyle; | |
| } | |
| auto typefaces = fontCollection()->findTypefaces( | |
| textStyle.getFontFamilies(), textStyle.getFontStyle(), textStyle.getFontArguments()); | |
| auto typeface = typefaces.empty() ? nullptr : typefaces.front(); | |
| SkFont font(typeface, textStyle.getFontSize()); | |
| fEmptyMetrics = InternalLineMetrics(font, paragraphStyle().getStrutStyle().getForceStrutHeight()); | |
| if (!paragraphStyle().getStrutStyle().getForceStrutHeight() && | |
| textStyle.getHeightOverride()) { | |
| const auto intrinsicHeight = fEmptyMetrics.height(); | |
| const auto strutHeight = textStyle.getHeight() * textStyle.getFontSize(); | |
| if (paragraphStyle().getStrutStyle().getHalfLeading()) { | |
| fEmptyMetrics.update( | |
| fEmptyMetrics.ascent(), | |
| fEmptyMetrics.descent(), | |
| fEmptyMetrics.leading() + strutHeight - intrinsicHeight); | |
| } else { | |
| const auto multiplier = strutHeight / intrinsicHeight; | |
| fEmptyMetrics.update( | |
| fEmptyMetrics.ascent() * multiplier, | |
| fEmptyMetrics.descent() * multiplier, | |
| fEmptyMetrics.leading() * multiplier); | |
| } | |
| } | |
| if (emptyParagraph) { | |
| // For an empty text we apply both TextHeightBehaviour flags | |
| // In case of non-empty paragraph TextHeightBehaviour flags will be applied at the appropriate place | |
| // We have to do it here because we skip wrapping for an empty text | |
| auto disableFirstAscent = (paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableFirstAscent) == TextHeightBehavior::kDisableFirstAscent; | |
| auto disableLastDescent = (paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableLastDescent) == TextHeightBehavior::kDisableLastDescent; | |
| fEmptyMetrics.update( | |
| disableFirstAscent ? fEmptyMetrics.rawAscent() : fEmptyMetrics.ascent(), | |
| disableLastDescent ? fEmptyMetrics.rawDescent() : fEmptyMetrics.descent(), | |
| fEmptyMetrics.leading()); | |
| } | |
| if (fParagraphStyle.getStrutStyle().getStrutEnabled()) { | |
| fStrutMetrics.updateLineMetrics(fEmptyMetrics); | |
| } | |
| } | |
| SkString ParagraphImpl::getEllipsis() const { | |
| auto ellipsis8 = fParagraphStyle.getEllipsis(); | |
| auto ellipsis16 = fParagraphStyle.getEllipsisUtf16(); | |
| if (!ellipsis8.isEmpty()) { | |
| return ellipsis8; | |
| } else { | |
| return SkUnicode::convertUtf16ToUtf8(fParagraphStyle.getEllipsisUtf16()); | |
| } | |
| } | |
| void ParagraphImpl::updateFontSize(size_t from, size_t to, SkScalar fontSize) { | |
| SkASSERT(from == 0 && to == fText.size()); | |
| auto defaultStyle = fParagraphStyle.getTextStyle(); | |
| defaultStyle.setFontSize(fontSize); | |
| fParagraphStyle.setTextStyle(defaultStyle); | |
| for (auto& textStyle : fTextStyles) { | |
| textStyle.fStyle.setFontSize(fontSize); | |
| } | |
| fState = std::min(fState, kIndexed); | |
| fOldWidth = 0; | |
| fOldHeight = 0; | |
| } | |
| void ParagraphImpl::updateTextAlign(TextAlign textAlign) { | |
| fParagraphStyle.setTextAlign(textAlign); | |
| if (fState >= kLineBroken) { | |
| fState = kLineBroken; | |
| } | |
| } | |
| void ParagraphImpl::updateForegroundPaint(size_t from, size_t to, SkPaint paint) { | |
| SkASSERT(from == 0 && to == fText.size()); | |
| auto defaultStyle = fParagraphStyle.getTextStyle(); | |
| defaultStyle.setForegroundColor(paint); | |
| fParagraphStyle.setTextStyle(defaultStyle); | |
| for (auto& textStyle : fTextStyles) { | |
| textStyle.fStyle.setForegroundColor(paint); | |
| } | |
| } | |
| void ParagraphImpl::updateBackgroundPaint(size_t from, size_t to, SkPaint paint) { | |
| SkASSERT(from == 0 && to == fText.size()); | |
| auto defaultStyle = fParagraphStyle.getTextStyle(); | |
| defaultStyle.setBackgroundColor(paint); | |
| fParagraphStyle.setTextStyle(defaultStyle); | |
| for (auto& textStyle : fTextStyles) { | |
| textStyle.fStyle.setBackgroundColor(paint); | |
| } | |
| } | |
| TArray<TextIndex> ParagraphImpl::countSurroundingGraphemes(TextRange textRange) const { | |
| textRange = textRange.intersection({0, fText.size()}); | |
| TArray<TextIndex> graphemes; | |
| if ((fCodeUnitProperties[textRange.start] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) { | |
| // Count the previous partial grapheme | |
| graphemes.emplace_back(textRange.start); | |
| } | |
| for (auto index = textRange.start; index < textRange.end; ++index) { | |
| if ((fCodeUnitProperties[index] & SkUnicode::CodeUnitFlags::kGraphemeStart) != 0) { | |
| graphemes.emplace_back(index); | |
| } | |
| } | |
| return graphemes; | |
| } | |
| TextIndex ParagraphImpl::findPreviousGraphemeBoundary(TextIndex utf8) const { | |
| while (utf8 > 0 && | |
| (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) { | |
| --utf8; | |
| } | |
| return utf8; | |
| } | |
| TextIndex ParagraphImpl::findNextGraphemeBoundary(TextIndex utf8) const { | |
| while (utf8 < fText.size() && | |
| (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) { | |
| ++utf8; | |
| } | |
| return utf8; | |
| } | |
| TextIndex ParagraphImpl::findNextGlyphClusterBoundary(TextIndex utf8) const { | |
| while (utf8 < fText.size() && | |
| (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGlyphClusterStart) == 0) { | |
| ++utf8; | |
| } | |
| return utf8; | |
| } | |
| TextIndex ParagraphImpl::findPreviousGlyphClusterBoundary(TextIndex utf8) const { | |
| while (utf8 > 0 && | |
| (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGlyphClusterStart) == 0) { | |
| --utf8; | |
| } | |
| return utf8; | |
| } | |
| void ParagraphImpl::ensureUTF16Mapping() { | |
| fillUTF16MappingOnce([&] { | |
| SkUnicode::extractUtfConversionMapping( | |
| this->text(), | |
| [&](size_t index) { fUTF8IndexForUTF16Index.emplace_back(index); }, | |
| [&](size_t index) { fUTF16IndexForUTF8Index.emplace_back(index); }); | |
| }); | |
| } | |
| void ParagraphImpl::visit(const Visitor& visitor) { | |
| int lineNumber = 0; | |
| for (auto& line : fLines) { | |
| line.ensureTextBlobCachePopulated(); | |
| for (auto& rec : line.fTextBlobCache) { | |
| if (rec.fBlob == nullptr) { | |
| continue; | |
| } | |
| SkTextBlob::Iter iter(*rec.fBlob); | |
| SkTextBlob::Iter::ExperimentalRun run; | |
| STArray<128, uint32_t> clusterStorage; | |
| const Run* R = rec.fVisitor_Run; | |
| const uint32_t* clusterPtr = &R->fClusterIndexes[0]; | |
| if (R->fClusterStart > 0) { | |
| int count = R->fClusterIndexes.size(); | |
| clusterStorage.reset(count); | |
| for (int i = 0; i < count; ++i) { | |
| clusterStorage[i] = R->fClusterStart + R->fClusterIndexes[i]; | |
| } | |
| clusterPtr = &clusterStorage[0]; | |
| } | |
| clusterPtr += rec.fVisitor_Pos; | |
| while (iter.experimentalNext(&run)) { | |
| const Paragraph::VisitorInfo info = { | |
| run.font, | |
| rec.fOffset, | |
| rec.fClipRect.fRight, | |
| run.count, | |
| run.glyphs, | |
| run.positions, | |
| clusterPtr, | |
| 0, // flags | |
| }; | |
| visitor(lineNumber, &info); | |
| clusterPtr += run.count; | |
| } | |
| } | |
| visitor(lineNumber, nullptr); // signal end of line | |
| lineNumber += 1; | |
| } | |
| } | |
| int ParagraphImpl::getLineNumberAt(TextIndex codeUnitIndex) const { | |
| if (codeUnitIndex >= fText.size()) { | |
| return -1; | |
| } | |
| size_t startLine = 0; | |
| size_t endLine = fLines.size() - 1; | |
| if (fLines.empty() || fLines[endLine].textWithNewlines().end <= codeUnitIndex) { | |
| return -1; | |
| } | |
| while (endLine > startLine) { | |
| // startLine + 1 <= endLine, so we have startLine <= midLine <= endLine - 1. | |
| const size_t midLine = (endLine + startLine) / 2; | |
| const TextRange midLineRange = fLines[midLine].textWithNewlines(); | |
| if (codeUnitIndex < midLineRange.start) { | |
| endLine = midLine - 1; | |
| } else if (midLineRange.end <= codeUnitIndex) { | |
| startLine = midLine + 1; | |
| } else { | |
| return midLine; | |
| } | |
| } | |
| SkASSERT(startLine == endLine); | |
| return startLine; | |
| } | |
| int ParagraphImpl::getLineNumberAtUTF16Offset(size_t codeUnitIndex) { | |
| this->ensureUTF16Mapping(); | |
| if (codeUnitIndex >= SkToSizeT(fUTF8IndexForUTF16Index.size())) { | |
| return -1; | |
| } | |
| const TextIndex utf8 = fUTF8IndexForUTF16Index[codeUnitIndex]; | |
| return getLineNumberAt(utf8); | |
| } | |
| bool ParagraphImpl::getLineMetricsAt(int lineNumber, LineMetrics* lineMetrics) const { | |
| if (lineNumber < 0 || lineNumber >= fLines.size()) { | |
| return false; | |
| } | |
| auto& line = fLines[lineNumber]; | |
| if (lineMetrics) { | |
| *lineMetrics = line.getMetrics(); | |
| } | |
| return true; | |
| } | |
| TextRange ParagraphImpl::getActualTextRange(int lineNumber, bool includeSpaces) const { | |
| if (lineNumber < 0 || lineNumber >= fLines.size()) { | |
| return EMPTY_TEXT; | |
| } | |
| auto& line = fLines[lineNumber]; | |
| return includeSpaces ? line.text() : line.trimmedText(); | |
| } | |
| bool ParagraphImpl::getGlyphClusterAt(TextIndex codeUnitIndex, GlyphClusterInfo* glyphInfo) { | |
| const int lineNumber = getLineNumberAt(codeUnitIndex); | |
| if (lineNumber == -1) { | |
| return false; | |
| } | |
| auto& line = fLines[lineNumber]; | |
| for (auto c = line.clustersWithSpaces().start; c < line.clustersWithSpaces().end; ++c) { | |
| auto& cluster = fClusters[c]; | |
| if (cluster.contains(codeUnitIndex)) { | |
| std::vector<TextBox> boxes; | |
| line.getRectsForRange(cluster.textRange(), | |
| RectHeightStyle::kTight, | |
| RectWidthStyle::kTight, | |
| boxes); | |
| if (!boxes.empty()) { | |
| if (glyphInfo) { | |
| *glyphInfo = {boxes[0].rect, cluster.textRange(), boxes[0].direction}; | |
| } | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| bool ParagraphImpl::getClosestGlyphClusterAt(SkScalar dx, | |
| SkScalar dy, | |
| GlyphClusterInfo* glyphInfo) { | |
| const PositionWithAffinity res = this->getGlyphPositionAtCoordinate(dx, dy); | |
| SkASSERT(res.position != 0 || res.affinity != Affinity::kUpstream); | |
| const size_t utf16Offset = res.position + (res.affinity == Affinity::kDownstream ? 0 : -1); | |
| this->ensureUTF16Mapping(); | |
| SkASSERT(utf16Offset < SkToSizeT(fUTF8IndexForUTF16Index.size())); | |
| return this->getGlyphClusterAt(fUTF8IndexForUTF16Index[utf16Offset], glyphInfo); | |
| } | |
| bool ParagraphImpl::getGlyphInfoAtUTF16Offset(size_t codeUnitIndex, GlyphInfo* glyphInfo) { | |
| this->ensureUTF16Mapping(); | |
| if (codeUnitIndex >= SkToSizeT(fUTF8IndexForUTF16Index.size())) { | |
| return false; | |
| } | |
| const TextIndex utf8 = fUTF8IndexForUTF16Index[codeUnitIndex]; | |
| const int lineNumber = getLineNumberAt(utf8); | |
| if (lineNumber == -1) { | |
| return false; | |
| } | |
| if (glyphInfo == nullptr) { | |
| return true; | |
| } | |
| const TextLine& line = fLines[lineNumber]; | |
| const TextIndex startIndex = findPreviousGraphemeBoundary(utf8); | |
| const TextIndex endIndex = findNextGraphemeBoundary(utf8 + 1); | |
| const ClusterIndex glyphClusterIndex = clusterIndex(utf8); | |
| const Cluster& glyphCluster = cluster(glyphClusterIndex); | |
| // `startIndex` and `endIndex` must be on the same line. | |
| std::vector<TextBox> boxes; | |
| line.getRectsForRange({startIndex, endIndex}, RectHeightStyle::kTight, RectWidthStyle::kTight, boxes); | |
| // TODO: currently placeholders with height=0 and width=0 are ignored so boxes | |
| // can be empty. These placeholders should still be reported for their | |
| // offset information. | |
| if (glyphInfo && !boxes.empty()) { | |
| *glyphInfo = { | |
| boxes[0].rect, | |
| { fUTF16IndexForUTF8Index[startIndex], fUTF16IndexForUTF8Index[endIndex] }, | |
| boxes[0].direction, | |
| glyphCluster.run().isEllipsis(), | |
| }; | |
| } | |
| return true; | |
| } | |
| bool ParagraphImpl::getClosestUTF16GlyphInfoAt(SkScalar dx, SkScalar dy, GlyphInfo* glyphInfo) { | |
| const PositionWithAffinity res = this->getGlyphPositionAtCoordinate(dx, dy); | |
| SkASSERT(res.position != 0 || res.affinity != Affinity::kUpstream); | |
| const size_t utf16Offset = res.position + (res.affinity == Affinity::kDownstream ? 0 : -1); | |
| return getGlyphInfoAtUTF16Offset(utf16Offset, glyphInfo); | |
| } | |
| SkFont ParagraphImpl::getFontAt(TextIndex codeUnitIndex) const { | |
| for (auto& run : fRuns) { | |
| const auto textRange = run.textRange(); | |
| if (textRange.start <= codeUnitIndex && codeUnitIndex < textRange.end) { | |
| return run.font(); | |
| } | |
| } | |
| return SkFont(); | |
| } | |
| SkFont ParagraphImpl::getFontAtUTF16Offset(size_t codeUnitIndex) { | |
| ensureUTF16Mapping(); | |
| if (codeUnitIndex >= SkToSizeT(fUTF8IndexForUTF16Index.size())) { | |
| return SkFont(); | |
| } | |
| const TextIndex utf8 = fUTF8IndexForUTF16Index[codeUnitIndex]; | |
| for (auto& run : fRuns) { | |
| const auto textRange = run.textRange(); | |
| if (textRange.start <= utf8 && utf8 < textRange.end) { | |
| return run.font(); | |
| } | |
| } | |
| return SkFont(); | |
| } | |
| std::vector<Paragraph::FontInfo> ParagraphImpl::getFonts() const { | |
| std::vector<FontInfo> results; | |
| for (auto& run : fRuns) { | |
| results.emplace_back(run.font(), run.textRange()); | |
| } | |
| return results; | |
| } | |
| void ParagraphImpl::extendedVisit(const ExtendedVisitor& visitor) { | |
| int lineNumber = 0; | |
| for (auto& line : fLines) { | |
| line.iterateThroughVisualRuns( | |
| false, | |
| [&](const Run* run, | |
| SkScalar runOffsetInLine, | |
| TextRange textRange, | |
| SkScalar* runWidthInLine) { | |
| *runWidthInLine = line.iterateThroughSingleRunByStyles( | |
| TextLine::TextAdjustment::GlyphCluster, | |
| run, | |
| runOffsetInLine, | |
| textRange, | |
| StyleType::kNone, | |
| [&](TextRange textRange, | |
| const TextStyle& style, | |
| const TextLine::ClipContext& context) { | |
| // 0-glyph runs have no glyph or position entries to pass to the visitor | |
| if (context.size == 0) { | |
| return; | |
| } | |
| SkScalar correctedBaseline = SkScalarFloorToScalar( | |
| line.baseline() + style.getBaselineShift() + 0.5); | |
| SkPoint offset = | |
| SkPoint::Make(line.offset().fX + context.fTextShift, | |
| line.offset().fY + correctedBaseline); | |
| SkRect rect = context.clip.makeOffset(line.offset()); | |
| AutoSTArray<16, SkRect> glyphBounds; | |
| glyphBounds.reset(SkToInt(run->size())); | |
| run->font().getBounds(run->glyphs(), glyphBounds, nullptr); | |
| STArray<128, uint32_t> clusterStorage; | |
| const uint32_t* clusterPtr = run->clusterIndexes().data(); | |
| if (run->fClusterStart > 0) { | |
| clusterStorage.reset(context.size); | |
| for (size_t i = 0; i < context.size; ++i) { | |
| clusterStorage[i] = | |
| run->fClusterStart + run->fClusterIndexes[i]; | |
| } | |
| clusterPtr = &clusterStorage[0]; | |
| } | |
| const Paragraph::ExtendedVisitorInfo info = { | |
| run->font(), | |
| offset, | |
| SkSize::Make(rect.width(), rect.height()), | |
| SkToS16(context.size), | |
| &run->glyphs()[context.pos], | |
| &run->fPositions[context.pos], | |
| &glyphBounds[context.pos], | |
| clusterPtr, | |
| 0, // flags | |
| }; | |
| visitor(lineNumber, &info); | |
| }); | |
| return true; | |
| }); | |
| visitor(lineNumber, nullptr); // signal end of line | |
| lineNumber += 1; | |
| } | |
| } | |
| int ParagraphImpl::getPath(int lineNumber, SkPath* dest) { | |
| SkPathBuilder builder; | |
| int notConverted = 0; | |
| auto& line = fLines[lineNumber]; | |
| line.iterateThroughVisualRuns( | |
| false, | |
| [&](const Run* run, | |
| SkScalar runOffsetInLine, | |
| TextRange textRange, | |
| SkScalar* runWidthInLine) { | |
| *runWidthInLine = line.iterateThroughSingleRunByStyles( | |
| TextLine::TextAdjustment::GlyphCluster, | |
| run, | |
| runOffsetInLine, | |
| textRange, | |
| StyleType::kNone, | |
| [&](TextRange textRange, | |
| const TextStyle& style, | |
| const TextLine::ClipContext& context) { | |
| // 0-glyph runs have no glyph or position entries to convert to paths | |
| if (context.size == 0) { | |
| return; | |
| } | |
| const SkFont& font = run->font(); | |
| SkScalar correctedBaseline = SkScalarFloorToScalar( | |
| line.baseline() + style.getBaselineShift() + 0.5); | |
| SkPoint offset = | |
| SkPoint::Make(line.offset().fX + context.fTextShift, | |
| line.offset().fY + correctedBaseline); | |
| SkRect rect = context.clip.makeOffset(offset); | |
| struct Rec { | |
| SkPathBuilder* fBuilder; | |
| SkPoint fOffset; | |
| const SkPoint* fPos; | |
| int fNotConverted; | |
| } rec = | |
| {&builder, SkPoint::Make(rect.left(), rect.top()), &run->positions()[context.pos], 0}; | |
| font.getPaths({&run->glyphs()[context.pos], context.size}, | |
| [](const SkPath* path, const SkMatrix& mx, void* ctx) { | |
| Rec* rec = reinterpret_cast<Rec*>(ctx); | |
| if (path) { | |
| SkMatrix total = mx; | |
| total.postTranslate(rec->fPos->fX + rec->fOffset.fX, | |
| rec->fPos->fY + rec->fOffset.fY); | |
| rec->fBuilder->addPath(*path, total); | |
| } else { | |
| rec->fNotConverted++; | |
| } | |
| rec->fPos += 1; // move to the next glyph's position | |
| }, &rec); | |
| notConverted += rec.fNotConverted; | |
| }); | |
| *dest = builder.detach(); | |
| return true; | |
| }); | |
| return notConverted; | |
| } | |
| SkPath Paragraph::GetPath(SkTextBlob* textBlob) { | |
| SkPathBuilder builder; | |
| SkTextBlobRunIterator iter(textBlob); | |
| while (!iter.done()) { | |
| SkFont font = iter.font(); | |
| struct Rec { SkPathBuilder* fBuilder; SkPoint fOffset; const SkPoint* fPos; } rec = | |
| {&builder, {textBlob->bounds().left(), textBlob->bounds().top()}, | |
| iter.points()}; | |
| font.getPaths({iter.glyphs(), iter.glyphCount()}, | |
| [](const SkPath* src, const SkMatrix& mx, void* ctx) { | |
| Rec* rec = (Rec*)ctx; | |
| if (src) { | |
| SkMatrix tmp(mx); | |
| tmp.postTranslate(rec->fPos->fX - rec->fOffset.fX, | |
| rec->fPos->fY - rec->fOffset.fY); | |
| rec->fBuilder->addPath(*src, tmp); | |
| } | |
| rec->fPos += 1; | |
| }, | |
| &rec); | |
| iter.next(); | |
| } | |
| return builder.detach(); | |
| } | |
| bool ParagraphImpl::containsEmoji(SkTextBlob* textBlob) { | |
| bool result = false; | |
| SkTextBlobRunIterator iter(textBlob); | |
| while (!iter.done() && !result) { | |
| // Walk through all the text by codepoints | |
| this->getUnicode()->forEachCodepoint(iter.text(), iter.textSize(), | |
| [&](SkUnichar unichar, int32_t start, int32_t end, int32_t count) { | |
| if (this->getUnicode()->isEmoji(unichar)) { | |
| result = true; | |
| } | |
| }); | |
| iter.next(); | |
| } | |
| return result; | |
| } | |
| bool ParagraphImpl::containsColorFontOrBitmap(SkTextBlob* textBlob) { | |
| SkTextBlobRunIterator iter(textBlob); | |
| bool flag = false; | |
| while (!iter.done() && !flag) { | |
| iter.font().getPaths( | |
| {(const SkGlyphID*) iter.glyphs(), iter.glyphCount()}, | |
| [](const SkPath* path, const SkMatrix& mx, void* ctx) { | |
| if (path == nullptr) { | |
| bool* flag1 = (bool*)ctx; | |
| *flag1 = true; | |
| } | |
| }, &flag); | |
| iter.next(); | |
| } | |
| return flag; | |
| } | |
| } // namespace textlayout | |
| } // namespace skia |