1 // Copyright 2019 Google LLC.
2 #include "include/core/SkBlurTypes.h"
3 #include "include/core/SkCanvas.h"
4 #include "include/core/SkFont.h"
5 #include "include/core/SkFontMetrics.h"
6 #include "include/core/SkMaskFilter.h"
7 #include "include/core/SkPaint.h"
8 #include "include/core/SkSpan.h"
9 #include "include/core/SkString.h"
10 #include "include/core/SkTextBlob.h"
11 #include "include/core/SkTypes.h"
12 #include "include/private/SkTemplates.h"
13 #include "include/private/SkTo.h"
14 #include "modules/skparagraph/include/DartTypes.h"
15 #include "modules/skparagraph/include/Metrics.h"
16 #include "modules/skparagraph/include/ParagraphStyle.h"
17 #include "modules/skparagraph/include/TextShadow.h"
18 #include "modules/skparagraph/include/TextStyle.h"
19 #include "modules/skparagraph/src/Decorations.h"
20 #include "modules/skparagraph/src/ParagraphImpl.h"
21 #include "modules/skparagraph/src/TextLine.h"
22 #include "modules/skshaper/include/SkShaper.h"
30 #include <type_traits>
34 namespace textlayout {
38 // TODO: deal with all the intersection functionality
39 TextRange intersected(const TextRange& a, const TextRange& b) {
40 if (a.start == b.start && a.end == b.end) return a;
41 auto begin = std::max(a.start, b.start);
42 auto end = std::min(a.end, b.end);
43 return end >= begin ? TextRange(begin, end) : EMPTY_TEXT;
46 SkScalar littleRound(SkScalar a) {
47 // This rounding is done to match Flutter tests. Must be removed..
48 return SkScalarRoundToScalar(a * 100.0)/100.0;
51 TextRange operator*(const TextRange& a, const TextRange& b) {
52 if (a.start == b.start && a.end == b.end) return a;
53 auto begin = std::max(a.start, b.start);
54 auto end = std::min(a.end, b.end);
55 return end > begin ? TextRange(begin, end) : EMPTY_TEXT;
58 int compareRound(SkScalar a, SkScalar b) {
59 // There is a rounding error that gets bigger when maxWidth gets bigger
60 // VERY long zalgo text (> 100000) on a VERY long line (> 10000)
61 // Canvas scaling affects it
62 // Letter spacing affects it
63 // It has to be relative to be useful
64 auto base = std::max(SkScalarAbs(a), SkScalarAbs(b));
65 auto diff = SkScalarAbs(a - b);
66 if (nearlyZero(base) || diff / base < 0.001f) {
70 auto ra = littleRound(a);
71 auto rb = littleRound(b);
81 TextLine::TextLine(ParagraphImpl* owner,
85 TextRange textExcludingSpaces,
87 TextRange textIncludingNewlines,
88 ClusterRange clusters,
89 ClusterRange clustersWithGhosts,
90 SkScalar widthWithSpaces,
91 InternalLineMetrics sizes)
94 , fTextExcludingSpaces(textExcludingSpaces)
96 , fTextIncludingNewlines(textIncludingNewlines)
97 , fClusterRange(clusters)
98 , fGhostClusterRange(clustersWithGhosts)
99 , fRunsInVisualOrder()
103 , fWidthWithSpaces(widthWithSpaces)
106 , fHasBackground(false)
108 , fHasDecorations(false)
109 , fAscentStyle(LineMetricStyle::CSS)
110 , fDescentStyle(LineMetricStyle::CSS)
111 , fTextBlobCachePopulated(false) {
112 // Reorder visual runs
113 auto& start = owner->cluster(fGhostClusterRange.start);
114 auto& end = owner->cluster(fGhostClusterRange.end - 1);
115 size_t numRuns = end.runIndex() - start.runIndex() + 1;
117 for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) {
118 auto b = fOwner->styles().begin() + index;
119 if (b->fStyle.hasBackground()) {
120 fHasBackground = true;
122 if (b->fStyle.getDecorationType() != TextDecoration::kNoDecoration) {
123 fHasDecorations = true;
125 if (b->fStyle.getShadowNumber() > 0) {
130 // Get the logical order
132 // This is just chosen to catch the common/fast cases. Feel free to tweak.
133 constexpr int kPreallocCount = 4;
134 SkAutoSTArray<kPreallocCount, SkUnicode::BidiLevel> runLevels(numRuns);
135 size_t runLevelsIndex = 0;
136 for (auto runIndex = start.runIndex(); runIndex <= end.runIndex(); ++runIndex) {
137 auto& run = fOwner->run(runIndex);
138 runLevels[runLevelsIndex++] = run.fBidiLevel;
140 InternalLineMetrics(run.fFontMetrics.fAscent, run.fFontMetrics.fDescent, run.fFontMetrics.fLeading));
142 SkASSERT(runLevelsIndex == numRuns);
144 SkAutoSTArray<kPreallocCount, int32_t> logicalOrder(numRuns);
146 // TODO: hide all these logic in SkUnicode?
147 fOwner->getUnicode()->reorderVisual(runLevels.data(), numRuns, logicalOrder.data());
148 auto firstRunIndex = start.runIndex();
149 for (auto index : logicalOrder) {
150 fRunsInVisualOrder.push_back(firstRunIndex + index);
153 // TODO: This is the fix for flutter. Must be removed...
154 for (auto cluster = &start; cluster <= &end; ++cluster) {
155 if (!cluster->run().isPlaceholder()) {
156 fShift += cluster->getHalfLetterSpacing();
162 void TextLine::paint(SkCanvas* textCanvas, SkScalar x, SkScalar y) {
163 if (fHasBackground) {
164 this->iterateThroughVisualRuns(false,
165 [textCanvas, x, y, this]
166 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
167 *runWidthInLine = this->iterateThroughSingleRunByStyles(
168 run, runOffsetInLine, textRange, StyleType::kBackground,
169 [textCanvas, x, y, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
170 this->paintBackground(textCanvas, x, y, textRange, style, context);
177 this->iterateThroughVisualRuns(false,
178 [textCanvas, x, y, this]
179 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
180 *runWidthInLine = this->iterateThroughSingleRunByStyles(
181 run, runOffsetInLine, textRange, StyleType::kShadow,
182 [textCanvas, x, y, this]
183 (TextRange textRange, const TextStyle& style, const ClipContext& context) {
184 this->paintShadow(textCanvas, x, y, textRange, style, context);
190 ensureTextBlobCachePopulated();
192 for (auto& record : fTextBlobCache) {
193 record.paint(textCanvas, x, y);
196 if (fHasDecorations) {
197 this->iterateThroughVisualRuns(false,
198 [textCanvas, x, y, this]
199 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
200 *runWidthInLine = this->iterateThroughSingleRunByStyles(
201 run, runOffsetInLine, textRange, StyleType::kDecorations,
202 [textCanvas, x, y, this]
203 (TextRange textRange, const TextStyle& style, const ClipContext& context) {
204 this->paintDecorations(textCanvas, x, y, textRange, style, context);
211 void TextLine::ensureTextBlobCachePopulated() {
212 if (fTextBlobCachePopulated) {
215 if (fBlockRange.width() == 1 &&
216 fRunsInVisualOrder.size() == 1 &&
217 fEllipsis == nullptr &&
218 fOwner->run(fRunsInVisualOrder[0]).placeholderStyle() == nullptr) {
219 if (fClusterRange.width() == 0) {
222 // Most common and most simple case
223 const auto& style = fOwner->block(fBlockRange.start).fStyle;
224 const auto& run = fOwner->run(fRunsInVisualOrder[0]);
225 auto clip = SkRect::MakeXYWH(0.0f, this->sizes().runTop(&run, this->fAscentStyle),
227 run.calculateHeight(this->fAscentStyle, this->fDescentStyle));
229 auto& start = fOwner->cluster(fClusterRange.start);
230 auto& end = fOwner->cluster(fClusterRange.end - 1);
231 SkASSERT(start.runIndex() == end.runIndex());
233 if (run.leftToRight()) {
234 glyphs = GlyphRange(start.startPos(),
235 end.isHardBreak() ? end.startPos() : end.endPos());
237 glyphs = GlyphRange(end.startPos(),
238 start.isHardBreak() ? start.startPos() : start.endPos());
240 ClipContext context = {/*run=*/&run,
241 /*pos=*/glyphs.start,
242 /*size=*/glyphs.width(),
243 /*fTextShift=*/-run.positionX(glyphs.start), // starting position
244 /*clip=*/clip, // entire line
245 /*fExcludedTrailingSpaces=*/0.0f, // no need for that
246 /*clippingNeeded=*/false}; // no need for that
247 this->buildTextBlob(fTextExcludingSpaces, style, context);
249 this->iterateThroughVisualRuns(false,
250 [this](const Run* run,
251 SkScalar runOffsetInLine,
253 SkScalar* runWidthInLine) {
254 if (run->placeholderStyle() != nullptr) {
255 *runWidthInLine = run->advance().fX;
258 *runWidthInLine = this->iterateThroughSingleRunByStyles(
262 StyleType::kForeground,
263 [this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
264 this->buildTextBlob(textRange, style, context);
269 fTextBlobCachePopulated = true;
272 void TextLine::format(TextAlign align, SkScalar maxWidth) {
273 SkScalar delta = maxWidth - this->width();
278 // We do nothing for left align
279 if (align == TextAlign::kJustify) {
280 if (!this->endsWithHardLineBreak()) {
281 this->justify(maxWidth);
282 } else if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) {
283 // Justify -> Right align
286 } else if (align == TextAlign::kRight) {
288 } else if (align == TextAlign::kCenter) {
293 void TextLine::scanStyles(StyleType styleType, const RunStyleVisitor& visitor) {
298 this->iterateThroughVisualRuns(false,
299 [this, visitor, styleType](const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) {
300 *width = this->iterateThroughSingleRunByStyles(
301 run, runOffset, textRange, styleType,
302 [visitor](TextRange textRange, const TextStyle& style, const ClipContext& context) {
303 visitor(textRange, style, context);
309 SkRect TextLine::extendHeight(const ClipContext& context) const {
310 SkRect result = context.clip;
311 result.fBottom += std::max(this->fMaxRunMetrics.height() - this->height(), 0.0f);
315 SkScalar TextLine::metricsWithoutMultiplier(TextHeightBehavior correction) {
317 if (this->fSizes.getForceStrut()) {
321 InternalLineMetrics result;
322 this->iterateThroughVisualRuns(true,
323 [&result](const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) {
324 InternalLineMetrics runMetrics(run->ascent(), run->descent(), run->leading());
325 result.add(runMetrics);
329 if (correction == TextHeightBehavior::kDisableFirstAscent) {
330 delta += (this->fSizes.fAscent - result.fAscent);
331 this->fSizes.fAscent = result.fAscent;
332 this->fAscentStyle = LineMetricStyle::Typographic;
333 } else if (correction == TextHeightBehavior::kDisableLastDescent) {
334 delta -= (this->fSizes.fDescent - result.fDescent);
335 this->fSizes.fDescent = result.fDescent;
336 this->fDescentStyle = LineMetricStyle::Typographic;
338 fAdvance.fY += delta;
342 void TextLine::buildTextBlob(TextRange textRange, const TextStyle& style, const ClipContext& context) {
343 if (context.run->placeholderStyle() != nullptr) {
347 fTextBlobCache.emplace_back();
348 TextBlobRecord& record = fTextBlobCache.back();
350 if (style.hasForeground()) {
351 record.fPaint = style.getForeground();
353 record.fPaint.setColor(style.getColor());
355 record.fVisitor_Run = context.run;
356 record.fVisitor_Pos = context.pos;
358 // TODO: This is the change for flutter, must be removed later
359 SkTextBlobBuilder builder;
360 context.run->copyTo(builder, SkToU32(context.pos), context.size);
361 record.fClippingNeeded = context.clippingNeeded;
362 if (context.clippingNeeded) {
363 record.fClipRect = extendHeight(context).makeOffset(this->offset());
365 record.fClipRect = context.clip.makeOffset(this->offset());
368 SkASSERT(nearlyEqual(context.run->baselineShift(), style.getBaselineShift()));
369 SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5);
370 record.fBlob = builder.make();
371 if (record.fBlob != nullptr) {
372 record.fBounds.joinPossiblyEmptyRect(record.fBlob->bounds());
375 record.fOffset = SkPoint::Make(this->offset().fX + context.fTextShift,
376 this->offset().fY + correctedBaseline);
379 void TextLine::TextBlobRecord::paint(SkCanvas* canvas, SkScalar x, SkScalar y) {
380 if (fClippingNeeded) {
382 canvas->clipRect(fClipRect.makeOffset(x, y));
384 canvas->drawTextBlob(fBlob, x + fOffset.x(), y + fOffset.y(), fPaint);
385 if (fClippingNeeded) {
390 void TextLine::paintBackground(SkCanvas* canvas,
394 const TextStyle& style,
395 const ClipContext& context) const {
396 if (style.hasBackground()) {
397 canvas->drawRect(context.clip.makeOffset(this->offset() + SkPoint::Make(x, y)),
398 style.getBackground());
402 void TextLine::paintShadow(SkCanvas* canvas,
406 const TextStyle& style,
407 const ClipContext& context) const {
408 SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5);
410 for (TextShadow shadow : style.getShadows()) {
411 if (!shadow.hasShadow()) continue;
414 paint.setColor(shadow.fColor);
415 if (shadow.fBlurSigma != 0.0) {
416 auto filter = SkMaskFilter::MakeBlur(kNormal_SkBlurStyle,
417 SkDoubleToScalar(shadow.fBlurSigma), false);
418 paint.setMaskFilter(filter);
421 SkTextBlobBuilder builder;
422 context.run->copyTo(builder, context.pos, context.size);
424 if (context.clippingNeeded) {
426 SkRect clip = extendHeight(context);
427 clip.offset(this->offset());
428 canvas->clipRect(clip);
430 auto blob = builder.make();
431 canvas->drawTextBlob(blob,
432 x + this->offset().fX + shadow.fOffset.x() + context.fTextShift,
433 y + this->offset().fY + shadow.fOffset.y() + correctedBaseline,
435 if (context.clippingNeeded) {
441 void TextLine::paintDecorations(SkCanvas* canvas, SkScalar x, SkScalar y, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
443 SkAutoCanvasRestore acr(canvas, true);
444 canvas->translate(x + this->offset().fX, y + this->offset().fY + style.getBaselineShift());
445 Decorations decorations;
446 SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5);
447 decorations.paint(canvas, style, context, correctedBaseline);
450 void TextLine::justify(SkScalar maxWidth) {
451 // Count words and the extra spaces to spread across the line
452 // TODO: do it at the line breaking?..
453 size_t whitespacePatches = 0;
454 SkScalar textLen = 0;
455 bool whitespacePatch = false;
456 this->iterateThroughClustersInGlyphsOrder(false, false,
457 [&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster, bool ghost) {
458 if (cluster->isWhitespaceBreak()) {
459 if (!whitespacePatch) {
460 whitespacePatch = true;
464 whitespacePatch = false;
466 textLen += cluster->width();
470 if (whitespacePatches == 0) {
474 SkScalar step = (maxWidth - textLen) / whitespacePatches;
477 // Deal with the ghost spaces
478 auto ghostShift = maxWidth - this->fAdvance.fX;
479 // Spread the extra whitespaces
480 whitespacePatch = false;
481 this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, bool ghost) {
484 if (cluster->run().leftToRight()) {
485 shiftCluster(cluster, ghostShift, ghostShift);
490 auto prevShift = shift;
491 if (cluster->isWhitespaceBreak()) {
492 if (!whitespacePatch) {
494 whitespacePatch = true;
498 whitespacePatch = false;
500 shiftCluster(cluster, shift, prevShift);
504 SkAssertResult(nearlyEqual(shift, maxWidth - textLen));
505 SkASSERT(whitespacePatches == 0);
507 this->fWidthWithSpaces += ghostShift;
508 this->fAdvance.fX = maxWidth;
511 void TextLine::shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift) {
513 auto& run = cluster->run();
514 auto start = cluster->startPos();
515 auto end = cluster->endPos();
517 if (end == run.size()) {
518 // Set the same shift for the fake last glyph (to avoid all extra checks)
522 if (run.fJustificationShifts.empty()) {
523 // Do not fill this array until needed
524 run.fJustificationShifts.push_back_n(run.size() + 1, { 0, 0 });
527 for (size_t pos = start; pos < end; ++pos) {
528 run.fJustificationShifts[pos] = { shift, prevShift };
532 void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) {
533 // Replace some clusters with the ellipsis
534 // Go through the clusters in the reverse logical order
535 // taking off cluster by cluster until the ellipsis fits
536 SkScalar width = fAdvance.fX;
537 // There is one case when we need to attach the ellipsis on the left:
538 // when the first few runs together wider than the ellipsis run are RTL
539 // In all the other cases we attach the ellipsis on the right
540 RunIndex leftRun = EMPTY_RUN;
541 std::unique_ptr<Run> ellipsisRun;
542 iterateThroughClustersInGlyphsOrder(
543 false, false, [&](const Cluster* cluster, bool ghost) {
544 if (cluster->run().leftToRight()) {
547 // Shape the ellipsis if the run has changed
548 if (leftRun != cluster->runIndex()) {
549 ellipsisRun = shapeEllipsis(ellipsis, cluster->run());
550 if (ellipsisRun->advance().fX > maxWidth) {
551 // Ellipsis is bigger than the entire line
554 ellipsisRun->fClusterStart = cluster->textRange().start;
555 ellipsisRun->setOwner(fOwner);
556 leftRun = cluster->runIndex();
559 if (width + ellipsisRun->advance().fX > maxWidth) {
560 width -= cluster->width();
561 // Continue if it's not
564 fEllipsis = std::move(ellipsisRun);
565 fEllipsis->fBidiLevel = 1;
566 fClusterRange.end = cluster - fOwner->clusters().data() + 1;
567 fGhostClusterRange.end = cluster - fOwner->clusters().data() + 1;
568 fText.end = cluster->textRange().end;
569 fTextIncludingNewlines.end = cluster->textRange().end;
570 fTextExcludingSpaces.end = cluster->textRange().end;
575 if (fEllipsis) return;
577 RunIndex rightRun = EMPTY_RUN;
578 iterateThroughClustersInGlyphsOrder(
579 true, false, [&](const Cluster* cluster, bool ghost) {
580 // Shape the ellipsis if the run has changed
581 if (rightRun != cluster->runIndex()) {
582 // Shape the ellipsis
583 ellipsisRun = shapeEllipsis(ellipsis, cluster->run());
584 if (ellipsisRun->advance().fX > maxWidth) {
585 // Ellipsis is bigger than the entire line
588 ellipsisRun->fClusterStart = cluster->textRange().start;
589 ellipsisRun->setOwner(fOwner);
590 rightRun = cluster->runIndex();
593 if (width + ellipsisRun->advance().fX > maxWidth) {
594 width -= cluster->width();
595 // Continue if it's not
598 fEllipsis = std::move(ellipsisRun);
599 fEllipsis->fBidiLevel = 0;
600 fClusterRange.end = cluster - fOwner->clusters().data() + 1;
601 fGhostClusterRange.end = fClusterRange.end;
602 fText.end = cluster->textRange().end;
603 fTextIncludingNewlines.end = cluster->textRange().end;
604 fTextExcludingSpaces.end = cluster->textRange().end;
608 if (fEllipsis) return;
610 // Weird situation: ellipsis does not fit; no ellipsis then
611 fClusterRange.end = fClusterRange.start;
612 fGhostClusterRange.end = fClusterRange.start;
613 fText.end = fText.start;
614 fTextIncludingNewlines.end = fTextIncludingNewlines.start;
615 fTextExcludingSpaces.end = fTextExcludingSpaces.start;
619 std::unique_ptr<Run> TextLine::shapeEllipsis(const SkString& ellipsis, const Run& run) {
621 class ShapeHandler final : public SkShaper::RunHandler {
623 ShapeHandler(SkScalar lineHeight, bool useHalfLeading, SkScalar baselineShift, const SkString& ellipsis)
624 : fRun(nullptr), fLineHeight(lineHeight), fUseHalfLeading(useHalfLeading), fBaselineShift(baselineShift), fEllipsis(ellipsis) {}
625 Run* run() & { return fRun.get(); }
626 std::unique_ptr<Run> run() && { return std::move(fRun); }
629 void beginLine() override {}
631 void runInfo(const RunInfo&) override {}
633 void commitRunInfo() override {}
635 Buffer runBuffer(const RunInfo& info) override {
637 fRun = std::make_unique<Run>(nullptr, info, 0, fLineHeight, fUseHalfLeading, fBaselineShift, 0, 0);
638 return fRun->newRunBuffer();
641 void commitRunBuffer(const RunInfo& info) override {
642 fRun->fAdvance.fX = info.fAdvance.fX;
643 fRun->fAdvance.fY = fRun->advance().fY;
644 fRun->fPlaceholderIndex = std::numeric_limits<size_t>::max();
645 fRun->fEllipsis = true;
648 void commitLine() override {}
650 std::unique_ptr<Run> fRun;
651 SkScalar fLineHeight;
652 bool fUseHalfLeading;
653 SkScalar fBaselineShift;
657 ShapeHandler handler(run.heightMultiplier(), run.useHalfLeading(), run.baselineShift(), ellipsis);
658 std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder();
659 SkASSERT_RELEASE(shaper != nullptr);
660 shaper->shape(ellipsis.c_str(), ellipsis.size(), run.font(), true,
661 std::numeric_limits<SkScalar>::max(), &handler);
662 handler.run()->fTextRange = TextRange(0, ellipsis.size());
663 handler.run()->fOwner = fOwner;
664 return std::move(handler).run();
667 TextLine::ClipContext TextLine::measureTextInsideOneRun(TextRange textRange,
669 SkScalar runOffsetInLine,
670 SkScalar textOffsetInRunInLine,
671 bool includeGhostSpaces,
672 bool limitToGraphemes) const {
673 ClipContext result = { run, 0, run->size(), 0, SkRect::MakeEmpty(), 0, false };
675 if (run->fEllipsis) {
676 // Both ellipsis and placeholders can only be measured as one glyph
677 result.fTextShift = runOffsetInLine;
678 result.clip = SkRect::MakeXYWH(runOffsetInLine,
679 sizes().runTop(run, this->fAscentStyle),
681 run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
683 } else if (run->isPlaceholder()) {
684 result.fTextShift = runOffsetInLine;
685 if (SkScalarIsFinite(run->fFontMetrics.fAscent)) {
686 result.clip = SkRect::MakeXYWH(runOffsetInLine,
687 sizes().runTop(run, this->fAscentStyle),
689 run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
691 result.clip = SkRect::MakeXYWH(runOffsetInLine, run->fFontMetrics.fAscent, run->advance().fX, 0);
695 // Find [start:end] clusters for the text
696 Cluster* start = nullptr;
697 Cluster* end = nullptr;
700 ClusterIndex startIndex;
701 ClusterIndex endIndex;
702 std::tie(found, startIndex, endIndex) = run->findLimitingClusters(textRange);
706 start = &fOwner->cluster(startIndex);
707 end = &fOwner->cluster(endIndex);
709 if (!limitToGraphemes) {
713 // Update textRange by cluster edges
714 if (run->leftToRight()) {
715 if (textRange.start != start->textRange().start) {
716 textRange.start = start->textRange().end;
718 textRange.end = end->textRange().end;
720 if (textRange.start != end->textRange().start) {
721 textRange.start = end->textRange().end;
723 textRange.end = start->textRange().end;
726 std::tie(found, startIndex, endIndex) = run->findLimitingGraphemes(textRange);
727 if (startIndex == textRange.start && endIndex == textRange.end) {
731 // Some clusters are inside graphemes and we need to adjust them
732 //SkDebugf("Correct range: [%d:%d) -> [%d:%d)\n", textRange.start, textRange.end, startIndex, endIndex);
733 textRange.start = startIndex;
734 textRange.end = endIndex;
736 // Move the start until it's on the grapheme edge (and glypheme, too)
739 result.pos = start->startPos();
740 result.size = (end->isHardBreak() ? end->startPos() : end->endPos()) - start->startPos();
742 auto textStartInRun = run->positionX(start->startPos());
743 auto textStartInLine = runOffsetInLine + textOffsetInRunInLine;
745 if (!run->fJustificationShifts.empty()) {
746 SkDebugf("Justification for [%d:%d)\n", textRange.start, textRange.end);
747 for (auto i = result.pos; i < result.pos + result.size; ++i) {
748 auto j = run->fJustificationShifts[i];
749 SkDebugf("[%d] = %f %f\n", i, j.fX, j.fY);
753 // Calculate the clipping rectangle for the text with cluster edges
754 // There are 2 cases:
755 // EOL (when we expect the last cluster clipped without any spaces)
756 // Anything else (when we want the cluster width contain all the spaces -
757 // coming from letter spacing or word spacing or justification)
760 sizes().runTop(run, this->fAscentStyle),
761 run->calculateWidth(result.pos, result.pos + result.size, false),
762 run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
764 // Correct the width in case the text edges don't match clusters
765 // TODO: This is where we get smart about selecting a part of a cluster
766 // by shaping each grapheme separately and then use the result sizes
767 // to calculate the proportions
768 auto leftCorrection = start->sizeToChar(textRange.start);
769 auto rightCorrection = end->sizeFromChar(textRange.end - 1);
770 result.clip.fLeft += leftCorrection;
771 result.clip.fRight -= rightCorrection;
772 result.clippingNeeded = leftCorrection != 0 || rightCorrection != 0;
774 textStartInLine -= leftCorrection;
775 result.clip.offset(textStartInLine, 0);
777 if (compareRound(result.clip.fRight, fAdvance.fX) > 0 && !includeGhostSpaces) {
778 // There are few cases when we need it.
779 // The most important one: we measure the text with spaces at the end (or at the beginning in RTL)
780 // and we should ignore these spaces
781 if (run->leftToRight()) {
782 // We only use this member for LTR
783 result.fExcludedTrailingSpaces = std::max(result.clip.fRight - fAdvance.fX, 0.0f);
784 result.clippingNeeded = true;
785 result.clip.fRight = fAdvance.fX;
789 if (result.clip.width() < 0) {
790 // Weird situation when glyph offsets move the glyph to the left
791 // (happens with zalgo texts, for instance)
792 result.clip.fRight = result.clip.fLeft;
795 // The text must be aligned with the lineOffset
796 result.fTextShift = textStartInLine - textStartInRun;
801 void TextLine::iterateThroughClustersInGlyphsOrder(bool reversed,
803 const ClustersVisitor& visitor) const {
804 // Walk through the clusters in the logical order (or reverse)
805 SkSpan<const size_t> runs(fRunsInVisualOrder.data(), fRunsInVisualOrder.size());
807 directional_for_each(runs, !reversed, [&](decltype(runs[0]) r) {
809 auto run = this->fOwner->run(r);
810 auto trimmedRange = fClusterRange.intersection(run.clusterRange());
811 auto trailedRange = fGhostClusterRange.intersection(run.clusterRange());
812 SkASSERT(trimmedRange.start == trailedRange.start);
814 auto trailed = fOwner->clusters(trailedRange);
815 auto trimmed = fOwner->clusters(trimmedRange);
816 directional_for_each(trailed, reversed != run.leftToRight(), [&](Cluster& cluster) {
818 bool ghost = &cluster >= trimmed.end();
819 if (!includeGhosts && ghost) {
822 if (!visitor(&cluster, ghost)) {
830 SkScalar TextLine::iterateThroughSingleRunByStyles(const Run* run,
834 const RunStyleVisitor& visitor) const {
836 if (run->fEllipsis) {
837 // Extra efforts to get the ellipsis text style
838 ClipContext clipContext = this->measureTextInsideOneRun(run->textRange(), run, runOffset,
840 TextRange testRange(run->fClusterStart, run->fClusterStart + run->textRange().width());
841 for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) {
842 auto block = fOwner->styles().begin() + index;
843 auto intersect = intersected(block->fRange, testRange);
844 if (intersect.width() > 0) {
845 visitor(testRange, block->fStyle, clipContext);
846 return run->advance().fX;
852 if (styleType == StyleType::kNone) {
853 ClipContext clipContext = this->measureTextInsideOneRun(textRange, run, runOffset,
855 if (clipContext.clip.height() > 0) {
856 visitor(textRange, TextStyle(), clipContext);
857 return clipContext.clip.width();
863 TextIndex start = EMPTY_INDEX;
865 const TextStyle* prevStyle = nullptr;
866 SkScalar textOffsetInRun = 0;
868 const BlockIndex blockRangeSize = fBlockRange.end - fBlockRange.start;
869 for (BlockIndex index = 0; index <= blockRangeSize; ++index) {
872 TextStyle* style = nullptr;
873 if (index < blockRangeSize) {
874 auto block = fOwner->styles().begin() +
875 (run->leftToRight() ? fBlockRange.start + index : fBlockRange.end - index - 1);
878 intersect = intersected(block->fRange, textRange);
879 if (intersect.width() == 0) {
880 if (start == EMPTY_INDEX) {
881 // This style is not applicable to the text yet
884 // We have found all the good styles already
885 // but we need to process the last one of them
886 intersect = TextRange(start, start + size);
887 index = fBlockRange.end;
891 style = &block->fStyle;
892 if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) {
893 size += intersect.width();
894 // RTL text intervals move backward
895 start = std::min(intersect.start, start);
897 } else if (start == EMPTY_INDEX ) {
900 size = intersect.width();
901 start = intersect.start;
905 } else if (prevStyle != nullptr) {
906 // This is the last style
911 // We have the style and the text
912 auto runStyleTextRange = TextRange(start, start + size);
914 ClipContext clipContext = this->measureTextInsideOneRun(runStyleTextRange, run, runOffset,
915 textOffsetInRun, false, true);
916 textOffsetInRun += clipContext.clip.width();
917 if (clipContext.clip.height() == 0) {
920 visitor(runStyleTextRange, *prevStyle, clipContext);
922 // Start all over again
924 start = intersect.start;
925 size = intersect.width();
927 return textOffsetInRun;
930 void TextLine::iterateThroughVisualRuns(bool includingGhostSpaces, const RunVisitor& visitor) const {
932 // Walk through all the runs that intersect with the line in visual order
934 SkScalar runOffset = 0;
935 SkScalar totalWidth = 0;
936 auto textRange = includingGhostSpaces ? this->textWithNewlines() : this->trimmedText();
938 if (this->ellipsis() != nullptr && !this->ellipsis()->leftToRight()) {
939 runOffset = this->ellipsis()->offset().fX;
940 if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) {
944 for (auto& runIndex : fRunsInVisualOrder) {
946 const auto run = &this->fOwner->run(runIndex);
947 auto lineIntersection = intersected(run->textRange(), textRange);
948 if (lineIntersection.width() == 0 && this->width() != 0) {
949 // TODO: deal with empty runs in a better way
952 if (!run->leftToRight() && runOffset == 0 && includingGhostSpaces) {
953 // runOffset does not take in account a possibility
954 // that RTL run could start before the line (trailing spaces)
955 // so we need to do runOffset -= "trailing whitespaces length"
956 TextRange whitespaces = intersected(
957 TextRange(fTextExcludingSpaces.end, fTextIncludingNewlines.end), run->fTextRange);
958 if (whitespaces.width() > 0) {
959 auto whitespacesLen = measureTextInsideOneRun(whitespaces, run, runOffset, 0, true, false).clip.width();
960 runOffset -= whitespacesLen;
965 if (!visitor(run, runOffset, lineIntersection, &width)) {
973 if (this->ellipsis() != nullptr && this->ellipsis()->leftToRight()) {
974 if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) {
979 // This is a very important assert!
980 // It asserts that 2 different ways of calculation come with the same results
981 if (!includingGhostSpaces && compareRound(totalWidth, this->width()) != 0) {
982 SkDebugf("ASSERT: %f != %f\n", totalWidth, this->width());
987 SkVector TextLine::offset() const {
988 return fOffset + SkVector::Make(fShift, 0);
991 LineMetrics TextLine::getMetrics() const {
994 // Fill out the metrics
995 fOwner->ensureUTF16Mapping();
996 result.fStartIndex = fOwner->getUTF16Index(fTextExcludingSpaces.start);
997 result.fEndExcludingWhitespaces = fOwner->getUTF16Index(fTextExcludingSpaces.end);
998 result.fEndIndex = fOwner->getUTF16Index(fText.end);
999 result.fEndIncludingNewline = fOwner->getUTF16Index(fTextIncludingNewlines.end);
1000 result.fHardBreak = endsWithHardLineBreak();
1001 result.fAscent = - fMaxRunMetrics.ascent();
1002 result.fDescent = fMaxRunMetrics.descent();
1003 result.fUnscaledAscent = - fMaxRunMetrics.ascent(); // TODO: implement
1004 result.fHeight = littleRound(fAdvance.fY);
1005 result.fWidth = littleRound(fAdvance.fX);
1006 result.fLeft = this->offset().fX;
1007 // This is Flutter definition of a baseline
1008 result.fBaseline = this->offset().fY + this->height() - this->sizes().descent();
1009 result.fLineNumber = this - fOwner->lines().begin();
1011 // Fill out the style parts
1012 this->iterateThroughVisualRuns(false,
1014 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
1015 if (run->placeholderStyle() != nullptr) {
1016 *runWidthInLine = run->advance().fX;
1019 *runWidthInLine = this->iterateThroughSingleRunByStyles(
1020 run, runOffsetInLine, textRange, StyleType::kForeground,
1021 [&result, &run](TextRange textRange, const TextStyle& style, const ClipContext& context) {
1022 SkFontMetrics fontMetrics;
1023 run->fFont.getMetrics(&fontMetrics);
1024 StyleMetrics styleMetrics(&style, fontMetrics);
1025 result.fLineMetrics.emplace(textRange.start, styleMetrics);
1033 bool TextLine::isFirstLine() {
1034 return this == &fOwner->lines().front();
1037 bool TextLine::isLastLine() {
1038 return this == &fOwner->lines().back();
1041 bool TextLine::endsWithHardLineBreak() const {
1042 // TODO: For some reason Flutter imagines a hard line break at the end of the last line.
1044 return fOwner->cluster(fGhostClusterRange.end - 1).isHardBreak() ||
1045 fEllipsis != nullptr ||
1046 fGhostClusterRange.end == fOwner->clusters().size() - 1;
1049 void TextLine::getRectsForRange(TextRange textRange0,
1050 RectHeightStyle rectHeightStyle,
1051 RectWidthStyle rectWidthStyle,
1052 std::vector<TextBox>& boxes)
1054 const Run* lastRun = nullptr;
1055 auto startBox = boxes.size();
1056 this->iterateThroughVisualRuns(true,
1057 [textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
1058 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
1059 *runWidthInLine = this->iterateThroughSingleRunByStyles(
1060 run, runOffsetInLine, textRange, StyleType::kNone,
1061 [run, runOffsetInLine, textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
1062 (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& lineContext) {
1064 auto intersect = textRange * textRange0;
1065 if (intersect.empty()) {
1069 auto paragraphStyle = fOwner->paragraphStyle();
1071 // Found a run that intersects with the text
1072 auto context = this->measureTextInsideOneRun(intersect, run, runOffsetInLine, 0, true, true);
1073 SkRect clip = context.clip;
1074 clip.offset(lineContext.fTextShift - context.fTextShift, 0);
1076 switch (rectHeightStyle) {
1077 case RectHeightStyle::kMax:
1078 // TODO: Change it once flutter rolls into google3
1079 // (probably will break things if changed before)
1080 clip.fBottom = this->height();
1081 clip.fTop = this->sizes().delta();
1083 case RectHeightStyle::kIncludeLineSpacingTop: {
1084 clip.fBottom = this->height();
1085 clip.fTop = this->sizes().delta();
1086 auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent();
1087 if (isFirstLine()) {
1088 clip.fTop += verticalShift;
1092 case RectHeightStyle::kIncludeLineSpacingMiddle: {
1093 clip.fBottom = this->height();
1094 clip.fTop = this->sizes().delta();
1095 auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent();
1096 clip.offset(0, verticalShift / 2.0);
1097 if (isFirstLine()) {
1098 clip.fTop += verticalShift / 2.0;
1101 clip.fBottom -= verticalShift / 2.0;
1105 case RectHeightStyle::kIncludeLineSpacingBottom: {
1106 clip.fBottom = this->height();
1107 clip.fTop = this->sizes().delta();
1108 auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent();
1109 clip.offset(0, verticalShift);
1111 clip.fBottom -= verticalShift;
1115 case RectHeightStyle::kStrut: {
1116 const auto& strutStyle = paragraphStyle.getStrutStyle();
1117 if (strutStyle.getStrutEnabled()
1118 && strutStyle.getFontSize() > 0) {
1119 auto strutMetrics = fOwner->strutMetrics();
1120 auto top = this->baseline();
1121 clip.fTop = top + strutMetrics.ascent();
1122 clip.fBottom = top + strutMetrics.descent();
1126 case RectHeightStyle::kTight: {
1127 if (run->fHeightMultiplier <= 0) {
1130 const auto effectiveBaseline = this->baseline() + this->sizes().delta();
1131 clip.fTop = effectiveBaseline + run->ascent();
1132 clip.fBottom = effectiveBaseline + run->descent();
1140 // Separate trailing spaces and move them in the default order of the paragraph
1141 // in case the run order and the paragraph order don't match
1142 SkRect trailingSpaces = SkRect::MakeEmpty();
1143 if (this->trimmedText().end <this->textWithNewlines().end && // Line has trailing space
1144 this->textWithNewlines().end == intersect.end && // Range is at the end of the line
1145 this->trimmedText().end > intersect.start) // Range has more than just spaces
1147 auto delta = this->spacesWidth();
1148 trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0);
1149 // There are trailing spaces in this run
1150 if (paragraphStyle.getTextAlign() == TextAlign::kJustify && isLastLine())
1152 // TODO: this is just a patch. Make it right later (when it's clear what and how)
1153 trailingSpaces = clip;
1154 if(run->leftToRight()) {
1155 trailingSpaces.fLeft = this->width();
1156 clip.fRight = this->width();
1158 trailingSpaces.fRight = 0;
1161 } else if (paragraphStyle.getTextDirection() == TextDirection::kRtl &&
1162 !run->leftToRight())
1165 trailingSpaces = clip;
1166 trailingSpaces.fLeft = - delta;
1167 trailingSpaces.fRight = 0;
1168 clip.fLeft += delta;
1169 } else if (paragraphStyle.getTextDirection() == TextDirection::kLtr &&
1173 trailingSpaces = clip;
1174 trailingSpaces.fLeft = this->width();
1175 trailingSpaces.fRight = trailingSpaces.fLeft + delta;
1176 clip.fRight -= delta;
1180 clip.offset(this->offset());
1181 if (trailingSpaces.width() > 0) {
1182 trailingSpaces.offset(this->offset());
1185 // Check if we can merge two boxes instead of adding a new one
1186 auto merge = [&lastRun, &context, &boxes](SkRect clip) {
1187 bool mergedBoxes = false;
1188 if (!boxes.empty() &&
1189 lastRun != nullptr &&
1190 context.run->leftToRight() == lastRun->leftToRight() &&
1191 lastRun->placeholderStyle() == nullptr &&
1192 context.run->placeholderStyle() == nullptr &&
1193 nearlyEqual(lastRun->heightMultiplier(),
1194 context.run->heightMultiplier()) &&
1195 lastRun->font() == context.run->font())
1197 auto& lastBox = boxes.back();
1198 if (nearlyEqual(lastBox.rect.fTop, clip.fTop) &&
1199 nearlyEqual(lastBox.rect.fBottom, clip.fBottom) &&
1200 (nearlyEqual(lastBox.rect.fLeft, clip.fRight) ||
1201 nearlyEqual(lastBox.rect.fRight, clip.fLeft)))
1203 lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft);
1204 lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight);
1208 lastRun = context.run;
1213 boxes.emplace_back(clip, context.run->getTextDirection());
1215 if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) {
1216 boxes.emplace_back(trailingSpaces, paragraphStyle.getTextDirection());
1219 if (rectWidthStyle == RectWidthStyle::kMax && !isLastLine()) {
1220 // Align the very left/right box horizontally
1221 auto lineStart = this->offset().fX;
1222 auto lineEnd = this->offset().fX + this->width();
1223 auto left = boxes[startBox];
1224 auto right = boxes.back();
1225 if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) {
1226 left.rect.fRight = left.rect.fLeft;
1227 left.rect.fLeft = 0;
1228 boxes.insert(boxes.begin() + startBox + 1, left);
1230 if (right.direction == TextDirection::kLtr &&
1231 right.rect.fRight >= lineEnd &&
1232 right.rect.fRight < fOwner->widthWithTrailingSpaces()) {
1233 right.rect.fLeft = right.rect.fRight;
1234 right.rect.fRight = fOwner->widthWithTrailingSpaces();
1235 boxes.emplace_back(right);
1243 for (auto& r : boxes) {
1244 r.rect.fLeft = littleRound(r.rect.fLeft);
1245 r.rect.fRight = littleRound(r.rect.fRight);
1246 r.rect.fTop = littleRound(r.rect.fTop);
1247 r.rect.fBottom = littleRound(r.rect.fBottom);
1251 PositionWithAffinity TextLine::getGlyphPositionAtCoordinate(SkScalar dx) {
1253 if (SkScalarNearlyZero(this->width()) && SkScalarNearlyZero(this->spacesWidth())) {
1254 // TODO: this is one of the flutter changes that have to go away eventually
1255 // Empty line is a special case in txtlib (but only when there are no spaces, too)
1256 auto utf16Index = fOwner->getUTF16Index(this->fTextExcludingSpaces.end);
1257 return { SkToS32(utf16Index) , kDownstream };
1260 PositionWithAffinity result(0, Affinity::kDownstream);
1261 this->iterateThroughVisualRuns(true,
1263 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
1264 bool keepLooking = true;
1265 *runWidthInLine = this->iterateThroughSingleRunByStyles(
1266 run, runOffsetInLine, textRange, StyleType::kNone,
1267 [this, run, dx, &result, &keepLooking]
1268 (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context0) {
1270 SkScalar offsetX = this->offset().fX;
1271 ClipContext context = context0;
1273 // Correct the clip size because libtxt counts trailing spaces
1274 if (run->leftToRight()) {
1275 context.clip.fRight += context.fExcludedTrailingSpaces; // extending clip to the right
1277 // Clip starts from 0; we cannot extend it to the left from that
1279 // This patch will help us to avoid a floating point error
1280 if (SkScalarNearlyEqual(context.clip.fRight, dx - offsetX, 0.01f)) {
1281 context.clip.fRight = dx - offsetX;
1284 if (dx <= context.clip.fLeft + offsetX) {
1285 // All the other runs are placed right of this one
1286 auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos));
1287 if (run->leftToRight()) {
1288 result = { SkToS32(utf16Index), kDownstream};
1289 keepLooking = false;
1291 result = { SkToS32(utf16Index), kDownstream};
1292 // If we haven't reached the end of the run we need to keep looking
1293 keepLooking = context.pos != 0;
1295 // For RTL we go another way
1296 return !run->leftToRight();
1299 if (dx >= context.clip.fRight + offsetX) {
1300 // We have to keep looking ; just in case keep the last one as the closest
1301 auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos + context.size));
1302 if (run->leftToRight()) {
1303 result = {SkToS32(utf16Index), kUpstream};
1305 result = {SkToS32(utf16Index), kDownstream};
1307 // For RTL we go another way
1308 return run->leftToRight();
1311 // So we found the run that contains our coordinates
1312 // Find the glyph position in the run that is the closest left of our point
1313 // TODO: binary search
1314 size_t found = context.pos;
1315 for (size_t index = context.pos; index < context.pos + context.size; ++index) {
1316 // TODO: this rounding is done to match Flutter tests. Must be removed..
1317 auto end = littleRound(context.run->positionX(index) + context.fTextShift + offsetX);
1320 } else if (end == dx && !context.run->leftToRight()) {
1321 // When we move RTL variable end points to the beginning of the code point which is included
1328 SkScalar glyphemePosLeft = context.run->positionX(found) + context.fTextShift + offsetX;
1329 SkScalar glyphemePosWidth = context.run->positionX(found + 1) - context.run->positionX(found);
1331 // Find the grapheme range that contains the point
1332 auto clusterIndex8 = context.run->globalClusterIndex(found);
1333 auto clusterEnd8 = context.run->globalClusterIndex(found + 1);
1335 SkScalar center = glyphemePosLeft + glyphemePosWidth / 2;
1336 if ((dx < center) == context.run->leftToRight()) {
1337 size_t utf16Index = context.run->leftToRight()
1338 ? fOwner->getUTF16Index(clusterIndex8)
1339 : fOwner->getUTF16Index(clusterEnd8) + 1;
1340 result = { SkToS32(utf16Index), kDownstream };
1342 size_t utf16Index = context.run->leftToRight()
1343 ? fOwner->getUTF16Index(clusterEnd8)
1344 : fOwner->getUTF16Index(clusterIndex8) + 1;
1345 result = { SkToS32(utf16Index), kUpstream };
1348 return keepLooking = false;
1357 void TextLine::getRectsForPlaceholders(std::vector<TextBox>& boxes) {
1358 this->iterateThroughVisualRuns(
1360 [&boxes, this](const Run* run, SkScalar runOffset, TextRange textRange,
1362 auto context = this->measureTextInsideOneRun(textRange, run, runOffset, 0, true, false);
1363 *width = context.clip.width();
1365 if (textRange.width() == 0) {
1368 if (!run->isPlaceholder()) {
1372 SkRect clip = context.clip;
1373 clip.offset(this->offset());
1375 clip.fLeft = littleRound(clip.fLeft);
1376 clip.fRight = littleRound(clip.fRight);
1377 clip.fTop = littleRound(clip.fTop);
1378 clip.fBottom = littleRound(clip.fBottom);
1379 boxes.emplace_back(clip, run->getTextDirection());
1383 } // namespace textlayout