// Copyright 2019 Google LLC. #include "modules/skparagraph/src/TextLine.h" #include "include/core/SkBlurTypes.h" #include "include/core/SkFont.h" #include "include/core/SkFontMetrics.h" #include "include/core/SkMaskFilter.h" #include "include/core/SkPaint.h" #include "include/core/SkSpan.h" #include "include/core/SkString.h" #include "include/core/SkTextBlob.h" #include "include/core/SkTypes.h" #include "include/private/base/SkTemplates.h" #include "include/private/base/SkTo.h" #include "modules/skparagraph/include/DartTypes.h" #include "modules/skparagraph/include/Metrics.h" #include "modules/skparagraph/include/ParagraphPainter.h" #include "modules/skparagraph/include/ParagraphStyle.h" #include "modules/skparagraph/include/TextShadow.h" #include "modules/skparagraph/include/TextStyle.h" #include "modules/skparagraph/src/Decorations.h" #include "modules/skparagraph/src/ParagraphImpl.h" #include "modules/skparagraph/src/ParagraphPainterImpl.h" #include "modules/skshaper/include/SkShaper.h" #include "modules/skshaper/include/SkShaper_harfbuzz.h" #include "modules/skshaper/include/SkShaper_skunicode.h" #include #include #include #include #include #include #include #include using namespace skia_private; namespace skia { namespace textlayout { namespace { // TODO: deal with all the intersection functionality TextRange intersected(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; } SkScalar littleRound(SkScalar a) { // This rounding is done to match Flutter tests. Must be removed.. return SkScalarRoundToScalar(a * 100.0)/100.0; } 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; } int compareRound(SkScalar a, SkScalar b, bool applyRoundingHack) { // There is a rounding error that gets bigger when maxWidth gets bigger // VERY long zalgo text (> 100000) on a VERY long line (> 10000) // Canvas scaling affects it // Letter spacing affects it // It has to be relative to be useful auto base = std::max(SkScalarAbs(a), SkScalarAbs(b)); auto diff = SkScalarAbs(a - b); if (nearlyZero(base) || diff / base < 0.001f) { return 0; } auto ra = a; auto rb = b; if (applyRoundingHack) { ra = littleRound(a); rb = littleRound(b); } if (ra < rb) { return -1; } else { return 1; } } } // namespace TextLine::TextLine(ParagraphImpl* owner, SkVector offset, SkVector advance, BlockRange blocks, TextRange textExcludingSpaces, TextRange text, TextRange textIncludingNewlines, ClusterRange clusters, ClusterRange clustersWithGhosts, SkScalar widthWithSpaces, InternalLineMetrics sizes) : fOwner(owner) , fBlockRange(blocks) , fTextExcludingSpaces(textExcludingSpaces) , fText(text) , fTextIncludingNewlines(textIncludingNewlines) , fClusterRange(clusters) , fGhostClusterRange(clustersWithGhosts) , fRunsInVisualOrder() , fAdvance(advance) , fOffset(offset) , fShift(0.0) , fWidthWithSpaces(widthWithSpaces) , fEllipsis(nullptr) , fSizes(sizes) , fHasBackground(false) , fHasShadows(false) , fHasDecorations(false) , fAscentStyle(LineMetricStyle::CSS) , fDescentStyle(LineMetricStyle::CSS) , fTextBlobCachePopulated(false) { // Reorder visual runs auto& start = owner->cluster(fGhostClusterRange.start); auto& end = owner->cluster(fGhostClusterRange.end - 1); size_t numRuns = end.runIndex() - start.runIndex() + 1; for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { auto b = fOwner->styles().begin() + index; if (b->fStyle.hasBackground()) { fHasBackground = true; } if (b->fStyle.getDecorationType() != TextDecoration::kNoDecoration) { fHasDecorations = true; } if (b->fStyle.getShadowNumber() > 0) { fHasShadows = true; } } // Get the logical order // This is just chosen to catch the common/fast cases. Feel free to tweak. constexpr int kPreallocCount = 4; AutoSTArray runLevels(numRuns); std::vector placeholdersInOriginalOrder; size_t runLevelsIndex = 0; // Placeholders must be laid out using the original order in which they were added // in the input. The API does not provide a way to indicate that a placeholder // position was moved due to bidi reordering. for (auto runIndex = start.runIndex(); runIndex <= end.runIndex(); ++runIndex) { auto& run = fOwner->run(runIndex); runLevels[runLevelsIndex++] = run.fBidiLevel; fMaxRunMetrics.add( InternalLineMetrics(run.correctAscent(), run.correctDescent(), run.fFontMetrics.fLeading)); if (run.isPlaceholder()) { placeholdersInOriginalOrder.push_back(runIndex); } } SkASSERT(runLevelsIndex == numRuns); AutoSTArray logicalOrder(numRuns); // TODO: hide all these logic in SkUnicode? fOwner->getUnicode()->reorderVisual(runLevels.data(), numRuns, logicalOrder.data()); auto firstRunIndex = start.runIndex(); auto placeholderIter = placeholdersInOriginalOrder.begin(); for (auto index : logicalOrder) { auto runIndex = firstRunIndex + index; if (fOwner->run(runIndex).isPlaceholder()) { fRunsInVisualOrder.push_back(*placeholderIter++); } else { fRunsInVisualOrder.push_back(runIndex); } } // TODO: This is the fix for flutter. Must be removed... for (auto cluster = &start; cluster <= &end; ++cluster) { if (!cluster->run().isPlaceholder()) { fShift += cluster->getHalfLetterSpacing(); break; } } } void TextLine::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) { if (fHasBackground) { this->iterateThroughVisualRuns(false, [painter, x, y, this] (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { *runWidthInLine = this->iterateThroughSingleRunByStyles( TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kBackground, [painter, x, y, this](TextRange textRange, const TextStyle& style, const ClipContext& context) { this->paintBackground(painter, x, y, textRange, style, context); }); return true; }); } if (fHasShadows) { this->iterateThroughVisualRuns(false, [painter, x, y, this] (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { *runWidthInLine = this->iterateThroughSingleRunByStyles( TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kShadow, [painter, x, y, this] (TextRange textRange, const TextStyle& style, const ClipContext& context) { this->paintShadow(painter, x, y, textRange, style, context); }); return true; }); } this->ensureTextBlobCachePopulated(); for (auto& record : fTextBlobCache) { record.paint(painter, x, y); } if (fHasDecorations) { this->iterateThroughVisualRuns(false, [painter, x, y, this] (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { *runWidthInLine = this->iterateThroughSingleRunByStyles( TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kDecorations, [painter, x, y, this] (TextRange textRange, const TextStyle& style, const ClipContext& context) { this->paintDecorations(painter, x, y, textRange, style, context); }); return true; }); } } void TextLine::ensureTextBlobCachePopulated() { if (fTextBlobCachePopulated) { return; } if (fBlockRange.width() == 1 && fRunsInVisualOrder.size() == 1 && fEllipsis == nullptr && fOwner->run(fRunsInVisualOrder[0]).placeholderStyle() == nullptr) { if (fClusterRange.width() == 0) { return; } // Most common and most simple case const auto& style = fOwner->block(fBlockRange.start).fStyle; const auto& run = fOwner->run(fRunsInVisualOrder[0]); auto clip = SkRect::MakeXYWH(0.0f, this->sizes().runTop(&run, this->fAscentStyle), fAdvance.fX, run.calculateHeight(this->fAscentStyle, this->fDescentStyle)); auto& start = fOwner->cluster(fClusterRange.start); auto& end = fOwner->cluster(fClusterRange.end - 1); SkASSERT(start.runIndex() == end.runIndex()); GlyphRange glyphs; if (run.leftToRight()) { glyphs = GlyphRange(start.startPos(), end.isHardBreak() ? end.startPos() : end.endPos()); } else { glyphs = GlyphRange(end.startPos(), start.isHardBreak() ? start.startPos() : start.endPos()); } ClipContext context = {/*run=*/&run, /*pos=*/glyphs.start, /*size=*/glyphs.width(), /*fTextShift=*/-run.positionX(glyphs.start), // starting position /*clip=*/clip, // entire line /*fExcludedTrailingSpaces=*/0.0f, // no need for that /*clippingNeeded=*/false}; // no need for that this->buildTextBlob(fTextExcludingSpaces, style, context); } else { this->iterateThroughVisualRuns(false, [this](const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { if (run->placeholderStyle() != nullptr) { *runWidthInLine = run->advance().fX; return true; } *runWidthInLine = this->iterateThroughSingleRunByStyles( TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kForeground, [this](TextRange textRange, const TextStyle& style, const ClipContext& context) { this->buildTextBlob(textRange, style, context); }); return true; }); } fTextBlobCachePopulated = true; } void TextLine::format(TextAlign align, SkScalar maxWidth) { SkScalar delta = maxWidth - this->width(); if (delta <= 0) { return; } // We do nothing for left align if (align == TextAlign::kJustify) { if (!this->endsWithHardLineBreak()) { this->justify(maxWidth); } else if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) { // Justify -> Right align fShift = delta; } } else if (align == TextAlign::kRight) { fShift = delta; } else if (align == TextAlign::kCenter) { fShift = delta / 2; } } void TextLine::scanStyles(StyleType styleType, const RunStyleVisitor& visitor) { if (this->empty()) { return; } this->iterateThroughVisualRuns( false, [this, visitor, styleType]( const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) { *width = this->iterateThroughSingleRunByStyles( TextAdjustment::GlyphCluster, run, runOffset, textRange, styleType, [visitor](TextRange textRange, const TextStyle& style, const ClipContext& context) { visitor(textRange, style, context); }); return true; }); } SkRect TextLine::extendHeight(const ClipContext& context) const { SkRect result = context.clip; result.fBottom += std::max(this->fMaxRunMetrics.height() - this->height(), 0.0f); return result; } void TextLine::buildTextBlob(TextRange textRange, const TextStyle& style, const ClipContext& context) { if (context.run->placeholderStyle() != nullptr) { return; } fTextBlobCache.emplace_back(); TextBlobRecord& record = fTextBlobCache.back(); if (style.hasForeground()) { record.fPaint = style.getForegroundPaintOrID(); } else { std::get(record.fPaint).setColor(style.getColor()); } record.fVisitor_Run = context.run; record.fVisitor_Pos = context.pos; // TODO: This is the change for flutter, must be removed later SkTextBlobBuilder builder; context.run->copyTo(builder, SkToU32(context.pos), context.size); record.fClippingNeeded = context.clippingNeeded; if (context.clippingNeeded) { record.fClipRect = extendHeight(context).makeOffset(this->offset()); } else { record.fClipRect = context.clip.makeOffset(this->offset()); } SkASSERT(nearlyEqual(context.run->baselineShift(), style.getBaselineShift())); SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5); record.fBlob = builder.make(); if (record.fBlob != nullptr) { record.fBounds.joinPossiblyEmptyRect(record.fBlob->bounds()); } record.fOffset = SkPoint::Make(this->offset().fX + context.fTextShift, this->offset().fY + correctedBaseline); } void TextLine::TextBlobRecord::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) { if (fClippingNeeded) { painter->save(); painter->clipRect(fClipRect.makeOffset(x, y)); } painter->drawTextBlob(fBlob, x + fOffset.x(), y + fOffset.y(), fPaint); if (fClippingNeeded) { painter->restore(); } } void TextLine::paintBackground(ParagraphPainter* painter, SkScalar x, SkScalar y, TextRange textRange, const TextStyle& style, const ClipContext& context) const { if (style.hasBackground()) { painter->drawRect(context.clip.makeOffset(this->offset() + SkPoint::Make(x, y)), style.getBackgroundPaintOrID()); } } void TextLine::paintShadow(ParagraphPainter* painter, SkScalar x, SkScalar y, TextRange textRange, const TextStyle& style, const ClipContext& context) const { SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5); for (TextShadow shadow : style.getShadows()) { if (!shadow.hasShadow()) continue; SkTextBlobBuilder builder; context.run->copyTo(builder, context.pos, context.size); if (context.clippingNeeded) { painter->save(); SkRect clip = extendHeight(context); clip.offset(x, y); clip.offset(this->offset()); painter->clipRect(clip); } auto blob = builder.make(); painter->drawTextShadow(blob, x + this->offset().fX + shadow.fOffset.x() + context.fTextShift, y + this->offset().fY + shadow.fOffset.y() + correctedBaseline, shadow.fColor, SkDoubleToScalar(shadow.fBlurSigma)); if (context.clippingNeeded) { painter->restore(); } } } void TextLine::paintDecorations(ParagraphPainter* painter, SkScalar x, SkScalar y, TextRange textRange, const TextStyle& style, const ClipContext& context) const { ParagraphPainterAutoRestore ppar(painter); painter->translate(x + this->offset().fX, y + this->offset().fY + style.getBaselineShift()); Decorations decorations; SkScalar correctedBaseline = SkScalarFloorToScalar(-this->sizes().rawAscent() + style.getBaselineShift() + 0.5); decorations.paint(painter, style, context, correctedBaseline); } void TextLine::justify(SkScalar maxWidth) { int whitespacePatches = 0; SkScalar textLen = 0; SkScalar whitespaceLen = 0; bool whitespacePatch = false; // Take leading whitespaces width but do not increment a whitespace patch number bool leadingWhitespaces = false; this->iterateThroughClustersInGlyphsOrder(false, false, [&](const Cluster* cluster, ClusterIndex index, bool ghost) { if (cluster->isWhitespaceBreak()) { if (index == 0) { leadingWhitespaces = true; } else if (!whitespacePatch && !leadingWhitespaces) { // We only count patches BETWEEN words, not before ++whitespacePatches; } whitespacePatch = !leadingWhitespaces; whitespaceLen += cluster->width(); } else if (cluster->isIdeographic()) { // Whitespace break before and after if (!whitespacePatch && index != 0) { // We only count patches BETWEEN words, not before ++whitespacePatches; // before } whitespacePatch = true; leadingWhitespaces = false; ++whitespacePatches; // after } else { whitespacePatch = false; leadingWhitespaces = false; } textLen += cluster->width(); return true; }); if (whitespacePatch) { // We only count patches BETWEEN words, not after --whitespacePatches; } if (whitespacePatches == 0) { if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) { // Justify -> Right align fShift = maxWidth - textLen; } return; } SkScalar step = (maxWidth - textLen + whitespaceLen) / whitespacePatches; SkScalar shift = 0.0f; SkScalar prevShift = 0.0f; // Deal with the ghost spaces auto ghostShift = maxWidth - this->fAdvance.fX; // Spread the extra whitespaces whitespacePatch = false; // Do not break on leading whitespaces leadingWhitespaces = false; this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, ClusterIndex index, bool ghost) { if (ghost) { if (cluster->run().leftToRight()) { this->shiftCluster(cluster, ghostShift, ghostShift); } return true; } if (cluster->isWhitespaceBreak()) { if (index == 0) { leadingWhitespaces = true; } else if (!whitespacePatch && !leadingWhitespaces) { shift += step; whitespacePatch = true; --whitespacePatches; } shift -= cluster->width(); } else if (cluster->isIdeographic()) { if (!whitespacePatch && index != 0) { shift += step; --whitespacePatches; } whitespacePatch = false; leadingWhitespaces = false; } else { whitespacePatch = false; leadingWhitespaces = false; } this->shiftCluster(cluster, shift, prevShift); prevShift = shift; // We skip ideographic whitespaces if (!cluster->isWhitespaceBreak() && cluster->isIdeographic()) { shift += step; whitespacePatch = true; --whitespacePatches; } return true; }); if (whitespacePatch && whitespacePatches < 0) { whitespacePatches++; shift -= step; } SkAssertResult(nearlyEqual(shift, maxWidth - textLen)); SkASSERT(whitespacePatches == 0); this->fWidthWithSpaces += ghostShift; this->fAdvance.fX = maxWidth; } void TextLine::shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift) { auto& run = cluster->run(); auto start = cluster->startPos(); auto end = cluster->endPos(); if (end == run.size()) { // Set the same shift for the fake last glyph (to avoid all extra checks) ++end; } if (run.fJustificationShifts.empty()) { // Do not fill this array until needed run.fJustificationShifts.push_back_n(run.size() + 1, { 0, 0 }); } for (size_t pos = start; pos < end; ++pos) { run.fJustificationShifts[pos] = { shift, prevShift }; } } void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) { // 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; RunIndex lastRun = EMPTY_RUN; std::unique_ptr ellipsisRun; for (auto clusterIndex = fGhostClusterRange.end; clusterIndex > fGhostClusterRange.start; --clusterIndex) { auto& cluster = fOwner->cluster(clusterIndex - 1); // Shape the ellipsis if the run has changed if (lastRun != cluster.runIndex()) { ellipsisRun = this->shapeEllipsis(ellipsis, &cluster); if (ellipsisRun->advance().fX > maxWidth) { // Ellipsis is bigger than the entire line; no way we can add it at all // BUT! We can keep scanning in case the next run will give us better results lastRun = EMPTY_RUN; continue; } else { // We may need to continue lastRun = cluster.runIndex(); } } // See if it fits if (width + ellipsisRun->advance().fX > maxWidth) { width -= cluster.width(); // Continue if the ellipsis does not fit continue; } // We found enough room for the ellipsis fAdvance.fX = width; fEllipsis = std::move(ellipsisRun); fEllipsis->setOwner(fOwner); // Let's update the line fClusterRange.end = clusterIndex; fGhostClusterRange.end = fClusterRange.end; fEllipsis->fClusterStart = cluster.textRange().start; fText.end = cluster.textRange().end; fTextIncludingNewlines.end = cluster.textRange().end; fTextExcludingSpaces.end = cluster.textRange().end; break; } if (!fEllipsis) { // Weird situation: ellipsis does not fit; no ellipsis then fClusterRange.end = fClusterRange.start; fGhostClusterRange.end = fClusterRange.start; fText.end = fText.start; fTextIncludingNewlines.end = fTextIncludingNewlines.start; fTextExcludingSpaces.end = fTextExcludingSpaces.start; fAdvance.fX = 0; } } std::unique_ptr TextLine::shapeEllipsis(const SkString& ellipsis, const Cluster* cluster) { class ShapeHandler final : public SkShaper::RunHandler { public: ShapeHandler(SkScalar lineHeight, bool useHalfLeading, SkScalar baselineShift, const SkString& ellipsis) : fRun(nullptr), fLineHeight(lineHeight), fUseHalfLeading(useHalfLeading), fBaselineShift(baselineShift), fEllipsis(ellipsis) {} std::unique_ptr run() & { return std::move(fRun); } private: void beginLine() override {} void runInfo(const RunInfo&) override {} void commitRunInfo() override {} Buffer runBuffer(const RunInfo& info) override { SkASSERT(!fRun); fRun = std::make_unique(nullptr, info, 0, fLineHeight, fUseHalfLeading, fBaselineShift, 0, 0); return fRun->newRunBuffer(); } void commitRunBuffer(const RunInfo& info) override { fRun->fAdvance.fX = info.fAdvance.fX; fRun->fAdvance.fY = fRun->advance().fY; fRun->fPlaceholderIndex = std::numeric_limits::max(); fRun->fEllipsis = true; } void commitLine() override {} std::unique_ptr fRun; SkScalar fLineHeight; bool fUseHalfLeading; SkScalar fBaselineShift; SkString fEllipsis; }; const Run& run = cluster->run(); TextStyle textStyle = fOwner->paragraphStyle().getTextStyle(); for (auto i = fBlockRange.start; i < fBlockRange.end; ++i) { auto& block = fOwner->block(i); if (run.leftToRight() && cluster->textRange().end <= block.fRange.end) { textStyle = block.fStyle; break; } else if (!run.leftToRight() && cluster->textRange().start <= block.fRange.end) { textStyle = block.fStyle; break; } } auto shaped = [&](sk_sp typeface, sk_sp fallback) -> std::unique_ptr { ShapeHandler handler(run.heightMultiplier(), run.useHalfLeading(), run.baselineShift(), ellipsis); SkFont font(std::move(typeface), textStyle.getFontSize()); font.setEdging(SkFont::Edging::kAntiAlias); font.setHinting(SkFontHinting::kSlight); font.setSubpixel(true); std::unique_ptr shaper = SkShapers::HB::ShapeDontWrapOrReorder( fOwner->getUnicode(), fallback ? fallback : SkFontMgr::RefEmpty()); const SkBidiIterator::Level defaultLevel = SkBidiIterator::kLTR; const char* utf8 = ellipsis.c_str(); size_t utf8Bytes = ellipsis.size(); std::unique_ptr bidi = SkShapers::unicode::BidiRunIterator( fOwner->getUnicode(), utf8, utf8Bytes, defaultLevel); SkASSERT(bidi); std::unique_ptr language = SkShaper::MakeStdLanguageRunIterator(utf8, utf8Bytes); SkASSERT(language); std::unique_ptr script = SkShapers::HB::ScriptRunIterator(utf8, utf8Bytes); SkASSERT(script); std::unique_ptr fontRuns = SkShaper::MakeFontMgrRunIterator( utf8, utf8Bytes, font, fallback ? fallback : SkFontMgr::RefEmpty()); SkASSERT(fontRuns); shaper->shape(utf8, utf8Bytes, *fontRuns, *bidi, *script, *language, nullptr, 0, std::numeric_limits::max(), &handler); auto ellipsisRun = handler.run(); ellipsisRun->fTextRange = TextRange(0, ellipsis.size()); ellipsisRun->fOwner = fOwner; return ellipsisRun; }; // Check the current font auto ellipsisRun = shaped(run.fFont.refTypeface(), nullptr); if (ellipsisRun->isResolved()) { return ellipsisRun; } // Check all allowed fonts std::vector> typefaces = fOwner->fontCollection()->findTypefaces( textStyle.getFontFamilies(), textStyle.getFontStyle(), textStyle.getFontArguments()); for (const auto& typeface : typefaces) { ellipsisRun = shaped(typeface, nullptr); if (ellipsisRun->isResolved()) { return ellipsisRun; } } // Try the fallback if (fOwner->fontCollection()->fontFallbackEnabled()) { const char* ch = ellipsis.c_str(); SkUnichar unicode = SkUTF::NextUTF8WithReplacement(&ch, ellipsis.c_str() + ellipsis.size()); // We do not expect emojis in ellipsis so if they appeat there // they will not be resolved with the pretiest color emoji font auto typeface = fOwner->fontCollection()->defaultFallback( unicode, textStyle.getFontStyle(), textStyle.getLocale()); if (typeface) { ellipsisRun = shaped(typeface, fOwner->fontCollection()->getFallbackManager()); if (ellipsisRun->isResolved()) { return ellipsisRun; } } } return ellipsisRun; } TextLine::ClipContext TextLine::measureTextInsideOneRun(TextRange textRange, const Run* run, SkScalar runOffsetInLine, SkScalar textOffsetInRunInLine, bool includeGhostSpaces, TextAdjustment textAdjustment) const { ClipContext result = { run, 0, run->size(), 0, SkRect::MakeEmpty(), 0, false }; if (run->fEllipsis) { // Both ellipsis and placeholders can only be measured as one glyph result.fTextShift = runOffsetInLine; result.clip = SkRect::MakeXYWH(runOffsetInLine, sizes().runTop(run, this->fAscentStyle), run->advance().fX, run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); return result; } else if (run->isPlaceholder()) { result.fTextShift = runOffsetInLine; if (SkIsFinite(run->fFontMetrics.fAscent)) { result.clip = SkRect::MakeXYWH(runOffsetInLine, sizes().runTop(run, this->fAscentStyle), run->advance().fX, run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); } else { result.clip = SkRect::MakeXYWH(runOffsetInLine, run->fFontMetrics.fAscent, run->advance().fX, 0); } return result; } else if (textRange.empty()) { return result; } TextRange originalTextRange(textRange); // We need it for proportional measurement // Find [start:end] clusters for the text while (true) { // Update textRange by cluster edges (shift start up to the edge of the cluster) // TODO: remove this limitation? TextRange updatedTextRange; bool found; std::tie(found, updatedTextRange.start, updatedTextRange.end) = run->findLimitingGlyphClusters(textRange); if (!found) { return result; } if ((textAdjustment & TextAdjustment::Grapheme) == 0) { textRange = updatedTextRange; break; } // Update text range by grapheme edges (shift start up to the edge of the grapheme) std::tie(found, updatedTextRange.start, updatedTextRange.end) = run->findLimitingGraphemes(updatedTextRange); if (updatedTextRange == textRange) { break; } // Some clusters are inside graphemes and we need to adjust them //SkDebugf("Correct range: [%d:%d) -> [%d:%d)\n", textRange.start, textRange.end, startIndex, endIndex); textRange = updatedTextRange; // Move the start until it's on the grapheme edge (and glypheme, too) } Cluster* start = &fOwner->cluster(fOwner->clusterIndex(textRange.start)); Cluster* end = &fOwner->cluster(fOwner->clusterIndex(textRange.end - (textRange.width() == 0 ? 0 : 1))); if (!run->leftToRight()) { std::swap(start, end); } result.pos = start->startPos(); result.size = (end->isHardBreak() ? end->startPos() : end->endPos()) - start->startPos(); auto textStartInRun = run->positionX(start->startPos()); auto textStartInLine = runOffsetInLine + textOffsetInRunInLine; if (!run->leftToRight()) { std::swap(start, end); } /* if (!run->fJustificationShifts.empty()) { SkDebugf("Justification for [%d:%d)\n", textRange.start, textRange.end); for (auto i = result.pos; i < result.pos + result.size; ++i) { auto j = run->fJustificationShifts[i]; SkDebugf("[%d] = %f %f\n", i, j.fX, j.fY); } } */ // 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) result.clip = SkRect::MakeXYWH(0, sizes().runTop(run, this->fAscentStyle), run->calculateWidth(result.pos, result.pos + result.size, false), run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); // 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(originalTextRange.start); auto rightCorrection = end->sizeFromChar(originalTextRange.end - 1); /* SkDebugf("[%d: %d) => [%d: %d), @%d, %d: [%f:%f) + [%f:%f) = ", // جَآَهُ originalTextRange.start, originalTextRange.end, textRange.start, textRange.end, result.pos, result.size, result.clip.fLeft, result.clip.fRight, leftCorrection, rightCorrection); */ result.clippingNeeded = leftCorrection != 0 || rightCorrection != 0; if (run->leftToRight()) { result.clip.fLeft += leftCorrection; result.clip.fRight -= rightCorrection; textStartInLine -= leftCorrection; } else { result.clip.fRight -= leftCorrection; result.clip.fLeft += rightCorrection; textStartInLine -= rightCorrection; } result.clip.offset(textStartInLine, 0); //SkDebugf("@%f[%f:%f)\n", textStartInLine, result.clip.fLeft, result.clip.fRight); if (compareRound(result.clip.fRight, fAdvance.fX, fOwner->getApplyRoundingHack()) > 0 && !includeGhostSpaces) { // There are few cases when we need it. // The most important one: we measure the text with spaces at the end (or at the beginning in RTL) // and we should ignore these spaces if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kLtr) { // We only use this member for LTR result.fExcludedTrailingSpaces = std::max(result.clip.fRight - fAdvance.fX, 0.0f); result.clippingNeeded = true; result.clip.fRight = fAdvance.fX; } } if (result.clip.width() < 0) { // Weird situation when glyph offsets move the glyph to the left // (happens with zalgo texts, for instance) result.clip.fRight = result.clip.fLeft; } // The text must be aligned with the lineOffset result.fTextShift = textStartInLine - textStartInRun; return result; } void TextLine::iterateThroughClustersInGlyphsOrder(bool reversed, bool includeGhosts, const ClustersVisitor& visitor) const { // Walk through the clusters in the logical order (or reverse) SkSpan runs(fRunsInVisualOrder.data(), fRunsInVisualOrder.size()); bool ignore = false; ClusterIndex index = 0; directional_for_each(runs, !reversed, [&](decltype(runs[0]) r) { if (ignore) return; auto run = this->fOwner->run(r); auto trimmedRange = fClusterRange.intersection(run.clusterRange()); auto trailedRange = fGhostClusterRange.intersection(run.clusterRange()); SkASSERT(trimmedRange.start == trailedRange.start); auto trailed = fOwner->clusters(trailedRange); auto trimmed = fOwner->clusters(trimmedRange); directional_for_each(trailed, reversed != run.leftToRight(), [&](Cluster& cluster) { if (ignore) return; bool ghost = &cluster >= trimmed.end(); if (!includeGhosts && ghost) { return; } if (!visitor(&cluster, index++, ghost)) { ignore = true; return; } }); }); } SkScalar TextLine::iterateThroughSingleRunByStyles(TextAdjustment textAdjustment, const Run* run, SkScalar runOffset, TextRange textRange, StyleType styleType, const RunStyleVisitor& visitor) const { auto correctContext = [&](TextRange textRange, SkScalar textOffsetInRun) -> ClipContext { auto result = this->measureTextInsideOneRun( textRange, run, runOffset, textOffsetInRun, false, textAdjustment); if (styleType == StyleType::kDecorations) { // Decorations are drawn based on the real font metrics (regardless of styles and strut) result.clip.fTop = this->sizes().runTop(run, LineMetricStyle::CSS); result.clip.fBottom = result.clip.fTop + run->calculateHeight(LineMetricStyle::CSS, LineMetricStyle::CSS); } return result; }; if (run->fEllipsis) { // Extra efforts to get the ellipsis text style ClipContext clipContext = correctContext(run->textRange(), 0.0f); TextRange testRange(run->fClusterStart, run->fClusterStart + run->textRange().width()); for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { auto block = fOwner->styles().begin() + index; auto intersect = intersected(block->fRange, testRange); if (intersect.width() > 0) { visitor(testRange, block->fStyle, clipContext); return run->advance().fX; } } SkASSERT(false); } if (styleType == StyleType::kNone) { ClipContext clipContext = correctContext(textRange, 0.0f); // The placehoder can have height=0 or (exclusively) width=0 and still be a thing if (clipContext.clip.height() > 0.0f || clipContext.clip.width() > 0.0f) { visitor(textRange, TextStyle(), clipContext); return clipContext.clip.width(); } else { return 0; } } TextIndex start = EMPTY_INDEX; size_t size = 0; const TextStyle* prevStyle = nullptr; SkScalar textOffsetInRun = 0; const BlockIndex blockRangeSize = fBlockRange.end - fBlockRange.start; for (BlockIndex index = 0; index <= blockRangeSize; ++index) { TextRange intersect; TextStyle* style = nullptr; if (index < blockRangeSize) { auto block = fOwner->styles().begin() + (run->leftToRight() ? fBlockRange.start + index : fBlockRange.end - index - 1); // Get the text intersect = intersected(block->fRange, textRange); if (intersect.width() == 0) { if (start == EMPTY_INDEX) { // This style is not applicable to the text yet continue; } else { // We have found all the good styles already // but we need to process the last one of them intersect = TextRange(start, start + size); index = fBlockRange.end; } } else { // Get the style style = &block->fStyle; if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) { size += intersect.width(); // RTL text intervals move backward start = std::min(intersect.start, start); continue; } else if (start == EMPTY_INDEX ) { // First time only prevStyle = style; size = intersect.width(); start = intersect.start; continue; } } } else if (prevStyle != nullptr) { // This is the last style } else { break; } // We have the style and the text auto runStyleTextRange = TextRange(start, start + size); ClipContext clipContext = correctContext(runStyleTextRange, textOffsetInRun); textOffsetInRun += clipContext.clip.width(); if (clipContext.clip.height() == 0) { continue; } visitor(runStyleTextRange, *prevStyle, clipContext); // Start all over again prevStyle = style; start = intersect.start; size = intersect.width(); } return textOffsetInRun; } void TextLine::iterateThroughVisualRuns(bool includingGhostSpaces, const RunVisitor& visitor) const { // Walk through all the runs that intersect with the line in visual order SkScalar width = 0; SkScalar runOffset = 0; SkScalar totalWidth = 0; auto textRange = includingGhostSpaces ? this->textWithNewlines() : this->trimmedText(); if (this->ellipsis() != nullptr && fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) { runOffset = this->ellipsis()->offset().fX; if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) { } } for (auto& runIndex : fRunsInVisualOrder) { const auto run = &this->fOwner->run(runIndex); auto lineIntersection = intersected(run->textRange(), textRange); if (lineIntersection.width() == 0 && this->width() != 0) { // TODO: deal with empty runs in a better way continue; } if (!run->leftToRight() && runOffset == 0 && includingGhostSpaces) { // runOffset does not take in account a possibility // that RTL run could start before the line (trailing spaces) // so we need to do runOffset -= "trailing whitespaces length" TextRange whitespaces = intersected( TextRange(fTextExcludingSpaces.end, fTextIncludingNewlines.end), run->fTextRange); if (whitespaces.width() > 0) { auto whitespacesLen = measureTextInsideOneRun(whitespaces, run, runOffset, 0, true, TextAdjustment::GlyphCluster).clip.width(); runOffset -= whitespacesLen; } } runOffset += width; totalWidth += width; if (!visitor(run, runOffset, lineIntersection, &width)) { return; } } runOffset += width; totalWidth += width; if (this->ellipsis() != nullptr && fOwner->paragraphStyle().getTextDirection() == TextDirection::kLtr) { if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) { totalWidth += width; } } if (!includingGhostSpaces && compareRound(totalWidth, this->width(), fOwner->getApplyRoundingHack()) != 0) { // This is a very important assert! // It asserts that 2 different ways of calculation come with the same results SkDEBUGFAILF("ASSERT: %f != %f\n", totalWidth, this->width()); } } SkVector TextLine::offset() const { return fOffset + SkVector::Make(fShift, 0); } LineMetrics TextLine::getMetrics() const { LineMetrics result; SkASSERT(fOwner); // Fill out the metrics fOwner->ensureUTF16Mapping(); result.fStartIndex = fOwner->getUTF16Index(fTextExcludingSpaces.start); result.fEndExcludingWhitespaces = fOwner->getUTF16Index(fTextExcludingSpaces.end); result.fEndIndex = fOwner->getUTF16Index(fText.end); result.fEndIncludingNewline = fOwner->getUTF16Index(fTextIncludingNewlines.end); result.fHardBreak = endsWithHardLineBreak(); result.fAscent = - fMaxRunMetrics.ascent(); result.fDescent = fMaxRunMetrics.descent(); result.fUnscaledAscent = - fMaxRunMetrics.ascent(); // TODO: implement result.fHeight = fAdvance.fY; result.fWidth = fAdvance.fX; if (fOwner->getApplyRoundingHack()) { result.fHeight = littleRound(result.fHeight); result.fWidth = littleRound(result.fWidth); } result.fLeft = this->offset().fX; // This is Flutter definition of a baseline result.fBaseline = this->offset().fY + this->height() - this->sizes().descent(); result.fLineNumber = this - fOwner->lines().begin(); // Fill out the style parts this->iterateThroughVisualRuns(false, [this, &result] (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { if (run->placeholderStyle() != nullptr) { *runWidthInLine = run->advance().fX; return true; } *runWidthInLine = this->iterateThroughSingleRunByStyles( TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kForeground, [&result, &run](TextRange textRange, const TextStyle& style, const ClipContext& context) { SkFontMetrics fontMetrics; run->fFont.getMetrics(&fontMetrics); StyleMetrics styleMetrics(&style, fontMetrics); result.fLineMetrics.emplace(textRange.start, styleMetrics); }); return true; }); return result; } bool TextLine::isFirstLine() const { return this == &fOwner->lines().front(); } bool TextLine::isLastLine() const { return this == &fOwner->lines().back(); } bool TextLine::endsWithHardLineBreak() const { // TODO: For some reason Flutter imagines a hard line break at the end of the last line. // To be removed... return (fGhostClusterRange.width() > 0 && fOwner->cluster(fGhostClusterRange.end - 1).isHardBreak()) || fEllipsis != nullptr || fGhostClusterRange.end == fOwner->clusters().size() - 1; } void TextLine::getRectsForRange(TextRange textRange0, RectHeightStyle rectHeightStyle, RectWidthStyle rectWidthStyle, std::vector& boxes) const { const Run* lastRun = nullptr; auto startBox = boxes.size(); this->iterateThroughVisualRuns(true, [textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this] (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { *runWidthInLine = this->iterateThroughSingleRunByStyles( TextAdjustment::GraphemeGluster, run, runOffsetInLine, textRange, StyleType::kNone, [run, runOffsetInLine, textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this] (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& lineContext) { auto intersect = textRange * textRange0; if (intersect.empty()) { return true; } auto paragraphStyle = fOwner->paragraphStyle(); // Found a run that intersects with the text auto context = this->measureTextInsideOneRun( intersect, run, runOffsetInLine, 0, true, TextAdjustment::GraphemeGluster); SkRect clip = context.clip; clip.offset(lineContext.fTextShift - context.fTextShift, 0); switch (rectHeightStyle) { case RectHeightStyle::kMax: // TODO: Change it once flutter rolls into google3 // (probably will break things if changed before) clip.fBottom = this->height(); clip.fTop = this->sizes().delta(); break; case RectHeightStyle::kIncludeLineSpacingTop: { clip.fBottom = this->height(); clip.fTop = this->sizes().delta(); auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent(); if (isFirstLine()) { clip.fTop += verticalShift; } break; } case RectHeightStyle::kIncludeLineSpacingMiddle: { clip.fBottom = this->height(); clip.fTop = this->sizes().delta(); auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent(); clip.offset(0, verticalShift / 2.0); if (isFirstLine()) { clip.fTop += verticalShift / 2.0; } if (isLastLine()) { clip.fBottom -= verticalShift / 2.0; } break; } case RectHeightStyle::kIncludeLineSpacingBottom: { clip.fBottom = this->height(); clip.fTop = this->sizes().delta(); auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent(); clip.offset(0, verticalShift); if (isLastLine()) { clip.fBottom -= verticalShift; } break; } case RectHeightStyle::kStrut: { const auto& strutStyle = paragraphStyle.getStrutStyle(); if (strutStyle.getStrutEnabled() && strutStyle.getFontSize() > 0) { auto strutMetrics = fOwner->strutMetrics(); auto top = this->baseline(); clip.fTop = top + strutMetrics.ascent(); clip.fBottom = top + strutMetrics.descent(); } } break; case RectHeightStyle::kTight: { if (run->fHeightMultiplier <= 0) { break; } const auto effectiveBaseline = this->baseline() + this->sizes().delta(); clip.fTop = effectiveBaseline + run->ascent(); clip.fBottom = effectiveBaseline + run->descent(); } break; default: SkASSERT(false); break; } // Separate trailing spaces and move them in the default order of the paragraph // in case the run order and the paragraph order don't match SkRect trailingSpaces = SkRect::MakeEmpty(); if (this->trimmedText().end textWithNewlines().end && // Line has trailing space this->textWithNewlines().end == intersect.end && // Range is at the end of the line this->trimmedText().end > intersect.start) // Range has more than just spaces { auto delta = this->spacesWidth(); trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0); // There are trailing spaces in this run if (paragraphStyle.getTextAlign() == TextAlign::kJustify && isLastLine()) { // TODO: this is just a patch. Make it right later (when it's clear what and how) trailingSpaces = clip; if(run->leftToRight()) { trailingSpaces.fLeft = this->width(); clip.fRight = this->width(); } else { trailingSpaces.fRight = 0; clip.fLeft = 0; } } else if (paragraphStyle.getTextDirection() == TextDirection::kRtl && !run->leftToRight()) { // Split trailingSpaces = clip; trailingSpaces.fLeft = - delta; trailingSpaces.fRight = 0; clip.fLeft += delta; } else if (paragraphStyle.getTextDirection() == TextDirection::kLtr && run->leftToRight()) { // Split trailingSpaces = clip; trailingSpaces.fLeft = this->width(); trailingSpaces.fRight = trailingSpaces.fLeft + delta; clip.fRight -= delta; } } clip.offset(this->offset()); if (trailingSpaces.width() > 0) { trailingSpaces.offset(this->offset()); } // Check if we can merge two boxes instead of adding a new one auto merge = [&lastRun, &context, &boxes](SkRect clip) { bool mergedBoxes = false; if (!boxes.empty() && lastRun != nullptr && context.run->leftToRight() == lastRun->leftToRight() && lastRun->placeholderStyle() == nullptr && context.run->placeholderStyle() == nullptr && nearlyEqual(lastRun->heightMultiplier(), context.run->heightMultiplier()) && lastRun->font() == context.run->font()) { auto& lastBox = boxes.back(); if (nearlyEqual(lastBox.rect.fTop, clip.fTop) && nearlyEqual(lastBox.rect.fBottom, clip.fBottom) && (nearlyEqual(lastBox.rect.fLeft, clip.fRight) || nearlyEqual(lastBox.rect.fRight, clip.fLeft))) { lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft); lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight); mergedBoxes = true; } } lastRun = context.run; return mergedBoxes; }; if (!merge(clip)) { boxes.emplace_back(clip, context.run->getTextDirection()); } if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) { boxes.emplace_back(trailingSpaces, paragraphStyle.getTextDirection()); } if (rectWidthStyle == RectWidthStyle::kMax && !isLastLine()) { // Align the very left/right box horizontally auto lineStart = this->offset().fX; auto lineEnd = this->offset().fX + this->width(); auto left = boxes[startBox]; auto right = boxes.back(); if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) { left.rect.fRight = left.rect.fLeft; left.rect.fLeft = 0; boxes.insert(boxes.begin() + startBox + 1, left); } if (right.direction == TextDirection::kLtr && right.rect.fRight >= lineEnd && right.rect.fRight < fOwner->widthWithTrailingSpaces()) { right.rect.fLeft = right.rect.fRight; right.rect.fRight = fOwner->widthWithTrailingSpaces(); boxes.emplace_back(right); } } return true; }); return true; }); if (fOwner->getApplyRoundingHack()) { 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); } } } PositionWithAffinity TextLine::getGlyphPositionAtCoordinate(SkScalar dx) { if (SkScalarNearlyZero(this->width()) && SkScalarNearlyZero(this->spacesWidth())) { // TODO: this is one of the flutter changes that have to go away eventually // Empty line is a special case in txtlib (but only when there are no spaces, too) auto utf16Index = fOwner->getUTF16Index(this->fTextExcludingSpaces.end); return { SkToS32(utf16Index) , kDownstream }; } PositionWithAffinity result(0, Affinity::kDownstream); this->iterateThroughVisualRuns(true, [this, dx, &result] (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { bool keepLooking = true; *runWidthInLine = this->iterateThroughSingleRunByStyles( TextAdjustment::GraphemeGluster, run, runOffsetInLine, textRange, StyleType::kNone, [this, run, dx, &result, &keepLooking] (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context0) { SkScalar offsetX = this->offset().fX; ClipContext context = context0; // Correct the clip size because libtxt counts trailing spaces if (run->leftToRight()) { context.clip.fRight += context.fExcludedTrailingSpaces; // extending clip to the right } else { // Clip starts from 0; we cannot extend it to the left from that } // However, we need to offset the clip context.clip.offset(offsetX, 0.0f); // This patch will help us to avoid a floating point error if (SkScalarNearlyEqual(context.clip.fRight, dx, 0.01f)) { context.clip.fRight = dx; } if (dx <= context.clip.fLeft) { // All the other runs are placed right of this one auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos)); if (run->leftToRight()) { result = { SkToS32(utf16Index), kDownstream}; keepLooking = false; } else { result = { SkToS32(utf16Index + 1), kUpstream}; // If we haven't reached the end of the run we need to keep looking keepLooking = context.pos != 0; } // For RTL we go another way return !run->leftToRight(); } if (dx >= context.clip.fRight) { // We have to keep looking ; just in case keep the last one as the closest auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos + context.size)); if (run->leftToRight()) { result = {SkToS32(utf16Index), kUpstream}; } else { result = {SkToS32(utf16Index), kDownstream}; } // For RTL we go another way return run->leftToRight(); } // So we found the run that contains our coordinates // Find the glyph position in the run that is the closest left of our point // TODO: binary search size_t found = context.pos; for (size_t index = context.pos; index < context.pos + context.size; ++index) { // TODO: this rounding is done to match Flutter tests. Must be removed.. auto end = context.run->positionX(index) + context.fTextShift + offsetX; if (fOwner->getApplyRoundingHack()) { end = littleRound(end); } if (end > dx) { break; } else if (end == dx && !context.run->leftToRight()) { // When we move RTL variable end points to the beginning of the code point which is included found = index; break; } found = index; } SkScalar glyphemePosLeft = context.run->positionX(found) + context.fTextShift + offsetX; SkScalar glyphemesWidth = context.run->positionX(found + 1) - context.run->positionX(found); // Find the grapheme range that contains the point auto clusterIndex8 = context.run->globalClusterIndex(found); auto clusterEnd8 = context.run->globalClusterIndex(found + 1); auto graphemes = fOwner->countSurroundingGraphemes({clusterIndex8, clusterEnd8}); SkScalar center = glyphemePosLeft + glyphemesWidth / 2; if (graphemes.size() > 1) { // Calculate the position proportionally based on grapheme count SkScalar averageGraphemeWidth = glyphemesWidth / graphemes.size(); SkScalar delta = dx - glyphemePosLeft; int graphemeIndex = SkScalarNearlyZero(averageGraphemeWidth) ? 0 : SkScalarFloorToInt(delta / averageGraphemeWidth); auto graphemeCenter = glyphemePosLeft + graphemeIndex * averageGraphemeWidth + averageGraphemeWidth / 2; auto graphemeUtf8Index = graphemes[graphemeIndex]; if ((dx < graphemeCenter) == context.run->leftToRight()) { size_t utf16Index = fOwner->getUTF16Index(graphemeUtf8Index); result = { SkToS32(utf16Index), kDownstream }; } else { size_t utf16Index = fOwner->getUTF16Index(graphemeUtf8Index + 1); result = { SkToS32(utf16Index), kUpstream }; } // Keep UTF16 index as is } else if ((dx < center) == context.run->leftToRight()) { size_t utf16Index = fOwner->getUTF16Index(clusterIndex8); result = { SkToS32(utf16Index), kDownstream }; } else { size_t utf16Index = context.run->leftToRight() ? fOwner->getUTF16Index(clusterEnd8) : fOwner->getUTF16Index(clusterIndex8) + 1; result = { SkToS32(utf16Index), kUpstream }; } return keepLooking = false; }); return keepLooking; } ); return result; } void TextLine::getRectsForPlaceholders(std::vector& boxes) { this->iterateThroughVisualRuns( true, [&boxes, this](const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) { auto context = this->measureTextInsideOneRun( textRange, run, runOffset, 0, true, TextAdjustment::GraphemeGluster); *width = context.clip.width(); if (textRange.width() == 0) { return true; } if (!run->isPlaceholder()) { return true; } SkRect clip = context.clip; clip.offset(this->offset()); if (fOwner->getApplyRoundingHack()) { clip.fLeft = littleRound(clip.fLeft); clip.fRight = littleRound(clip.fRight); clip.fTop = littleRound(clip.fTop); clip.fBottom = littleRound(clip.fBottom); } boxes.emplace_back(clip, run->getTextDirection()); return true; }); } } // namespace textlayout } // namespace skia