xref: /aosp_15_r20/external/skia/modules/skparagraph/src/TextLine.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 // Copyright 2019 Google LLC.
2 
3 #include "modules/skparagraph/src/TextLine.h"
4 
5 #include "include/core/SkBlurTypes.h"
6 #include "include/core/SkFont.h"
7 #include "include/core/SkFontMetrics.h"
8 #include "include/core/SkMaskFilter.h"
9 #include "include/core/SkPaint.h"
10 #include "include/core/SkSpan.h"
11 #include "include/core/SkString.h"
12 #include "include/core/SkTextBlob.h"
13 #include "include/core/SkTypes.h"
14 #include "include/private/base/SkTemplates.h"
15 #include "include/private/base/SkTo.h"
16 #include "modules/skparagraph/include/DartTypes.h"
17 #include "modules/skparagraph/include/Metrics.h"
18 #include "modules/skparagraph/include/ParagraphPainter.h"
19 #include "modules/skparagraph/include/ParagraphStyle.h"
20 #include "modules/skparagraph/include/TextShadow.h"
21 #include "modules/skparagraph/include/TextStyle.h"
22 #include "modules/skparagraph/src/Decorations.h"
23 #include "modules/skparagraph/src/ParagraphImpl.h"
24 #include "modules/skparagraph/src/ParagraphPainterImpl.h"
25 #include "modules/skshaper/include/SkShaper.h"
26 #include "modules/skshaper/include/SkShaper_harfbuzz.h"
27 #include "modules/skshaper/include/SkShaper_skunicode.h"
28 
29 #include <algorithm>
30 #include <iterator>
31 #include <limits>
32 #include <map>
33 #include <memory>
34 #include <tuple>
35 #include <type_traits>
36 #include <utility>
37 
38 using namespace skia_private;
39 
40 namespace skia {
41 namespace textlayout {
42 
43 namespace {
44 
45 // TODO: deal with all the intersection functionality
intersected(const TextRange & a,const TextRange & b)46 TextRange intersected(const TextRange& a, const TextRange& b) {
47     if (a.start == b.start && a.end == b.end) return a;
48     auto begin = std::max(a.start, b.start);
49     auto end = std::min(a.end, b.end);
50     return end >= begin ? TextRange(begin, end) : EMPTY_TEXT;
51 }
52 
littleRound(SkScalar a)53 SkScalar littleRound(SkScalar a) {
54     // This rounding is done to match Flutter tests. Must be removed..
55   return SkScalarRoundToScalar(a * 100.0)/100.0;
56 }
57 
operator *(const TextRange & a,const TextRange & b)58 TextRange operator*(const TextRange& a, const TextRange& b) {
59     if (a.start == b.start && a.end == b.end) return a;
60     auto begin = std::max(a.start, b.start);
61     auto end = std::min(a.end, b.end);
62     return end > begin ? TextRange(begin, end) : EMPTY_TEXT;
63 }
64 
compareRound(SkScalar a,SkScalar b,bool applyRoundingHack)65 int compareRound(SkScalar a, SkScalar b, bool applyRoundingHack) {
66     // There is a rounding error that gets bigger when maxWidth gets bigger
67     // VERY long zalgo text (> 100000) on a VERY long line (> 10000)
68     // Canvas scaling affects it
69     // Letter spacing affects it
70     // It has to be relative to be useful
71     auto base = std::max(SkScalarAbs(a), SkScalarAbs(b));
72     auto diff = SkScalarAbs(a - b);
73     if (nearlyZero(base) || diff / base < 0.001f) {
74         return 0;
75     }
76 
77     auto ra = a;
78     auto rb = b;
79 
80     if (applyRoundingHack) {
81         ra = littleRound(a);
82         rb = littleRound(b);
83     }
84     if (ra < rb) {
85         return -1;
86     } else {
87         return 1;
88     }
89 }
90 
91 }  // namespace
92 
TextLine(ParagraphImpl * owner,SkVector offset,SkVector advance,BlockRange blocks,TextRange textExcludingSpaces,TextRange text,TextRange textIncludingNewlines,ClusterRange clusters,ClusterRange clustersWithGhosts,SkScalar widthWithSpaces,InternalLineMetrics sizes)93 TextLine::TextLine(ParagraphImpl* owner,
94                    SkVector offset,
95                    SkVector advance,
96                    BlockRange blocks,
97                    TextRange textExcludingSpaces,
98                    TextRange text,
99                    TextRange textIncludingNewlines,
100                    ClusterRange clusters,
101                    ClusterRange clustersWithGhosts,
102                    SkScalar widthWithSpaces,
103                    InternalLineMetrics sizes)
104         : fOwner(owner)
105         , fBlockRange(blocks)
106         , fTextExcludingSpaces(textExcludingSpaces)
107         , fText(text)
108         , fTextIncludingNewlines(textIncludingNewlines)
109         , fClusterRange(clusters)
110         , fGhostClusterRange(clustersWithGhosts)
111         , fRunsInVisualOrder()
112         , fAdvance(advance)
113         , fOffset(offset)
114         , fShift(0.0)
115         , fWidthWithSpaces(widthWithSpaces)
116         , fEllipsis(nullptr)
117         , fSizes(sizes)
118         , fHasBackground(false)
119         , fHasShadows(false)
120         , fHasDecorations(false)
121         , fAscentStyle(LineMetricStyle::CSS)
122         , fDescentStyle(LineMetricStyle::CSS)
123         , fTextBlobCachePopulated(false) {
124     // Reorder visual runs
125     auto& start = owner->cluster(fGhostClusterRange.start);
126     auto& end = owner->cluster(fGhostClusterRange.end - 1);
127     size_t numRuns = end.runIndex() - start.runIndex() + 1;
128 
129     for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) {
130         auto b = fOwner->styles().begin() + index;
131         if (b->fStyle.hasBackground()) {
132             fHasBackground = true;
133         }
134         if (b->fStyle.getDecorationType() != TextDecoration::kNoDecoration) {
135             fHasDecorations = true;
136         }
137         if (b->fStyle.getShadowNumber() > 0) {
138             fHasShadows = true;
139         }
140     }
141 
142     // Get the logical order
143 
144     // This is just chosen to catch the common/fast cases. Feel free to tweak.
145     constexpr int kPreallocCount = 4;
146     AutoSTArray<kPreallocCount, SkUnicode::BidiLevel> runLevels(numRuns);
147     std::vector<RunIndex> placeholdersInOriginalOrder;
148     size_t runLevelsIndex = 0;
149     // Placeholders must be laid out using the original order in which they were added
150     // in the input. The API does not provide a way to indicate that a placeholder
151     // position was moved due to bidi reordering.
152     for (auto runIndex = start.runIndex(); runIndex <= end.runIndex(); ++runIndex) {
153         auto& run = fOwner->run(runIndex);
154         runLevels[runLevelsIndex++] = run.fBidiLevel;
155         fMaxRunMetrics.add(
156             InternalLineMetrics(run.correctAscent(), run.correctDescent(), run.fFontMetrics.fLeading));
157         if (run.isPlaceholder()) {
158             placeholdersInOriginalOrder.push_back(runIndex);
159         }
160     }
161     SkASSERT(runLevelsIndex == numRuns);
162 
163     AutoSTArray<kPreallocCount, int32_t> logicalOrder(numRuns);
164 
165     // TODO: hide all these logic in SkUnicode?
166     fOwner->getUnicode()->reorderVisual(runLevels.data(), numRuns, logicalOrder.data());
167     auto firstRunIndex = start.runIndex();
168     auto placeholderIter = placeholdersInOriginalOrder.begin();
169     for (auto index : logicalOrder) {
170         auto runIndex = firstRunIndex + index;
171         if (fOwner->run(runIndex).isPlaceholder()) {
172             fRunsInVisualOrder.push_back(*placeholderIter++);
173         } else {
174             fRunsInVisualOrder.push_back(runIndex);
175         }
176     }
177 
178     // TODO: This is the fix for flutter. Must be removed...
179     for (auto cluster = &start; cluster <= &end; ++cluster) {
180         if (!cluster->run().isPlaceholder()) {
181             fShift += cluster->getHalfLetterSpacing();
182             break;
183         }
184     }
185 }
186 
paint(ParagraphPainter * painter,SkScalar x,SkScalar y)187 void TextLine::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) {
188     if (fHasBackground) {
189         this->iterateThroughVisualRuns(false,
190             [painter, x, y, this]
191             (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
192                 *runWidthInLine = this->iterateThroughSingleRunByStyles(
193                 TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kBackground,
194                 [painter, x, y, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
195                     this->paintBackground(painter, x, y, textRange, style, context);
196                 });
197             return true;
198             });
199     }
200 
201     if (fHasShadows) {
202         this->iterateThroughVisualRuns(false,
203             [painter, x, y, this]
204             (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
205             *runWidthInLine = this->iterateThroughSingleRunByStyles(
206                 TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kShadow,
207                 [painter, x, y, this]
208                 (TextRange textRange, const TextStyle& style, const ClipContext& context) {
209                     this->paintShadow(painter, x, y, textRange, style, context);
210                 });
211             return true;
212             });
213     }
214 
215     this->ensureTextBlobCachePopulated();
216 
217     for (auto& record : fTextBlobCache) {
218         record.paint(painter, x, y);
219     }
220 
221     if (fHasDecorations) {
222         this->iterateThroughVisualRuns(false,
223             [painter, x, y, this]
224             (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
225                 *runWidthInLine = this->iterateThroughSingleRunByStyles(
226                 TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kDecorations,
227                 [painter, x, y, this]
228                 (TextRange textRange, const TextStyle& style, const ClipContext& context) {
229                     this->paintDecorations(painter, x, y, textRange, style, context);
230                 });
231                 return true;
232         });
233     }
234 }
235 
ensureTextBlobCachePopulated()236 void TextLine::ensureTextBlobCachePopulated() {
237     if (fTextBlobCachePopulated) {
238         return;
239     }
240     if (fBlockRange.width() == 1 &&
241         fRunsInVisualOrder.size() == 1 &&
242         fEllipsis == nullptr &&
243         fOwner->run(fRunsInVisualOrder[0]).placeholderStyle() == nullptr) {
244         if (fClusterRange.width() == 0) {
245             return;
246         }
247         // Most common and most simple case
248         const auto& style = fOwner->block(fBlockRange.start).fStyle;
249         const auto& run = fOwner->run(fRunsInVisualOrder[0]);
250         auto clip = SkRect::MakeXYWH(0.0f, this->sizes().runTop(&run, this->fAscentStyle),
251                                      fAdvance.fX,
252                                      run.calculateHeight(this->fAscentStyle, this->fDescentStyle));
253 
254         auto& start = fOwner->cluster(fClusterRange.start);
255         auto& end = fOwner->cluster(fClusterRange.end - 1);
256         SkASSERT(start.runIndex() == end.runIndex());
257         GlyphRange glyphs;
258         if (run.leftToRight()) {
259             glyphs = GlyphRange(start.startPos(),
260                                 end.isHardBreak() ? end.startPos() : end.endPos());
261         } else {
262             glyphs = GlyphRange(end.startPos(),
263                                 start.isHardBreak() ? start.startPos() : start.endPos());
264         }
265         ClipContext context = {/*run=*/&run,
266                                /*pos=*/glyphs.start,
267                                /*size=*/glyphs.width(),
268                                /*fTextShift=*/-run.positionX(glyphs.start), // starting position
269                                /*clip=*/clip,                               // entire line
270                                /*fExcludedTrailingSpaces=*/0.0f,            // no need for that
271                                /*clippingNeeded=*/false};                   // no need for that
272         this->buildTextBlob(fTextExcludingSpaces, style, context);
273     } else {
274         this->iterateThroughVisualRuns(false,
275            [this](const Run* run,
276                   SkScalar runOffsetInLine,
277                   TextRange textRange,
278                   SkScalar* runWidthInLine) {
279                if (run->placeholderStyle() != nullptr) {
280                    *runWidthInLine = run->advance().fX;
281                    return true;
282                }
283                *runWidthInLine = this->iterateThroughSingleRunByStyles(
284                    TextAdjustment::GlyphCluster,
285                    run,
286                    runOffsetInLine,
287                    textRange,
288                    StyleType::kForeground,
289                    [this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
290                        this->buildTextBlob(textRange, style, context);
291                    });
292                return true;
293            });
294     }
295     fTextBlobCachePopulated = true;
296 }
297 
format(TextAlign align,SkScalar maxWidth)298 void TextLine::format(TextAlign align, SkScalar maxWidth) {
299     SkScalar delta = maxWidth - this->width();
300     if (delta <= 0) {
301         return;
302     }
303 
304     // We do nothing for left align
305     if (align == TextAlign::kJustify) {
306         if (!this->endsWithHardLineBreak()) {
307             this->justify(maxWidth);
308         } else if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) {
309             // Justify -> Right align
310             fShift = delta;
311         }
312     } else if (align == TextAlign::kRight) {
313         fShift = delta;
314     } else if (align == TextAlign::kCenter) {
315         fShift = delta / 2;
316     }
317 }
318 
scanStyles(StyleType styleType,const RunStyleVisitor & visitor)319 void TextLine::scanStyles(StyleType styleType, const RunStyleVisitor& visitor) {
320     if (this->empty()) {
321         return;
322     }
323 
324     this->iterateThroughVisualRuns(
325             false,
326             [this, visitor, styleType](
327                     const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) {
328                 *width = this->iterateThroughSingleRunByStyles(
329                         TextAdjustment::GlyphCluster,
330                         run,
331                         runOffset,
332                         textRange,
333                         styleType,
334                         [visitor](TextRange textRange,
335                                   const TextStyle& style,
336                                   const ClipContext& context) {
337                             visitor(textRange, style, context);
338                         });
339                 return true;
340             });
341 }
342 
extendHeight(const ClipContext & context) const343 SkRect TextLine::extendHeight(const ClipContext& context) const {
344     SkRect result = context.clip;
345     result.fBottom += std::max(this->fMaxRunMetrics.height() - this->height(), 0.0f);
346     return result;
347 }
348 
buildTextBlob(TextRange textRange,const TextStyle & style,const ClipContext & context)349 void TextLine::buildTextBlob(TextRange textRange, const TextStyle& style, const ClipContext& context) {
350     if (context.run->placeholderStyle() != nullptr) {
351         return;
352     }
353 
354     fTextBlobCache.emplace_back();
355     TextBlobRecord& record = fTextBlobCache.back();
356 
357     if (style.hasForeground()) {
358         record.fPaint = style.getForegroundPaintOrID();
359     } else {
360         std::get<SkPaint>(record.fPaint).setColor(style.getColor());
361     }
362     record.fVisitor_Run = context.run;
363     record.fVisitor_Pos = context.pos;
364 
365     // TODO: This is the change for flutter, must be removed later
366     SkTextBlobBuilder builder;
367     context.run->copyTo(builder, SkToU32(context.pos), context.size);
368     record.fClippingNeeded = context.clippingNeeded;
369     if (context.clippingNeeded) {
370         record.fClipRect = extendHeight(context).makeOffset(this->offset());
371     } else {
372         record.fClipRect = context.clip.makeOffset(this->offset());
373     }
374 
375     SkASSERT(nearlyEqual(context.run->baselineShift(), style.getBaselineShift()));
376     SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() +  0.5);
377     record.fBlob = builder.make();
378     if (record.fBlob != nullptr) {
379         record.fBounds.joinPossiblyEmptyRect(record.fBlob->bounds());
380     }
381 
382     record.fOffset = SkPoint::Make(this->offset().fX + context.fTextShift,
383                                    this->offset().fY + correctedBaseline);
384 }
385 
paint(ParagraphPainter * painter,SkScalar x,SkScalar y)386 void TextLine::TextBlobRecord::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) {
387     if (fClippingNeeded) {
388         painter->save();
389         painter->clipRect(fClipRect.makeOffset(x, y));
390     }
391     painter->drawTextBlob(fBlob, x + fOffset.x(), y + fOffset.y(), fPaint);
392     if (fClippingNeeded) {
393         painter->restore();
394     }
395 }
396 
paintBackground(ParagraphPainter * painter,SkScalar x,SkScalar y,TextRange textRange,const TextStyle & style,const ClipContext & context) const397 void TextLine::paintBackground(ParagraphPainter* painter,
398                                SkScalar x,
399                                SkScalar y,
400                                TextRange textRange,
401                                const TextStyle& style,
402                                const ClipContext& context) const {
403     if (style.hasBackground()) {
404         painter->drawRect(context.clip.makeOffset(this->offset() + SkPoint::Make(x, y)),
405                           style.getBackgroundPaintOrID());
406     }
407 }
408 
paintShadow(ParagraphPainter * painter,SkScalar x,SkScalar y,TextRange textRange,const TextStyle & style,const ClipContext & context) const409 void TextLine::paintShadow(ParagraphPainter* painter,
410                            SkScalar x,
411                            SkScalar y,
412                            TextRange textRange,
413                            const TextStyle& style,
414                            const ClipContext& context) const {
415     SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5);
416 
417     for (TextShadow shadow : style.getShadows()) {
418         if (!shadow.hasShadow()) continue;
419 
420         SkTextBlobBuilder builder;
421         context.run->copyTo(builder, context.pos, context.size);
422 
423         if (context.clippingNeeded) {
424             painter->save();
425             SkRect clip = extendHeight(context);
426             clip.offset(x, y);
427             clip.offset(this->offset());
428             painter->clipRect(clip);
429         }
430         auto blob = builder.make();
431         painter->drawTextShadow(blob,
432             x + this->offset().fX + shadow.fOffset.x() + context.fTextShift,
433             y + this->offset().fY + shadow.fOffset.y() + correctedBaseline,
434             shadow.fColor,
435             SkDoubleToScalar(shadow.fBlurSigma));
436         if (context.clippingNeeded) {
437             painter->restore();
438         }
439     }
440 }
441 
paintDecorations(ParagraphPainter * painter,SkScalar x,SkScalar y,TextRange textRange,const TextStyle & style,const ClipContext & context) const442 void TextLine::paintDecorations(ParagraphPainter* painter, SkScalar x, SkScalar y, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
443     ParagraphPainterAutoRestore ppar(painter);
444     painter->translate(x + this->offset().fX, y + this->offset().fY + style.getBaselineShift());
445     Decorations decorations;
446     SkScalar correctedBaseline = SkScalarFloorToScalar(-this->sizes().rawAscent() + style.getBaselineShift() + 0.5);
447     decorations.paint(painter, style, context, correctedBaseline);
448 }
449 
justify(SkScalar maxWidth)450 void TextLine::justify(SkScalar maxWidth) {
451     int whitespacePatches = 0;
452     SkScalar textLen = 0;
453     SkScalar whitespaceLen = 0;
454     bool whitespacePatch = false;
455     // Take leading whitespaces width but do not increment a whitespace patch number
456     bool leadingWhitespaces = false;
457     this->iterateThroughClustersInGlyphsOrder(false, false,
458         [&](const Cluster* cluster, ClusterIndex index, bool ghost) {
459             if (cluster->isWhitespaceBreak()) {
460                 if (index == 0) {
461                     leadingWhitespaces = true;
462                 } else if (!whitespacePatch && !leadingWhitespaces) {
463                     // We only count patches BETWEEN words, not before
464                     ++whitespacePatches;
465                 }
466                 whitespacePatch = !leadingWhitespaces;
467                 whitespaceLen += cluster->width();
468             } else if (cluster->isIdeographic()) {
469                 // Whitespace break before and after
470                 if (!whitespacePatch && index != 0) {
471                     // We only count patches BETWEEN words, not before
472                     ++whitespacePatches; // before
473                 }
474                 whitespacePatch = true;
475                 leadingWhitespaces = false;
476                 ++whitespacePatches;    // after
477             } else {
478                 whitespacePatch = false;
479                 leadingWhitespaces = false;
480             }
481             textLen += cluster->width();
482             return true;
483         });
484 
485     if (whitespacePatch) {
486         // We only count patches BETWEEN words, not after
487         --whitespacePatches;
488     }
489     if (whitespacePatches == 0) {
490         if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) {
491             // Justify -> Right align
492             fShift = maxWidth - textLen;
493         }
494         return;
495     }
496 
497     SkScalar step = (maxWidth - textLen + whitespaceLen) / whitespacePatches;
498     SkScalar shift = 0.0f;
499     SkScalar prevShift = 0.0f;
500 
501     // Deal with the ghost spaces
502     auto ghostShift = maxWidth - this->fAdvance.fX;
503     // Spread the extra whitespaces
504     whitespacePatch = false;
505     // Do not break on leading whitespaces
506     leadingWhitespaces = false;
507     this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, ClusterIndex index, bool ghost) {
508 
509         if (ghost) {
510             if (cluster->run().leftToRight()) {
511                 this->shiftCluster(cluster, ghostShift, ghostShift);
512             }
513             return true;
514         }
515 
516         if (cluster->isWhitespaceBreak()) {
517             if (index == 0) {
518                 leadingWhitespaces = true;
519             } else if (!whitespacePatch && !leadingWhitespaces) {
520                 shift += step;
521                 whitespacePatch = true;
522                 --whitespacePatches;
523             }
524             shift -= cluster->width();
525         } else if (cluster->isIdeographic()) {
526             if (!whitespacePatch && index != 0) {
527                 shift += step;
528                --whitespacePatches;
529             }
530             whitespacePatch = false;
531             leadingWhitespaces = false;
532         } else {
533             whitespacePatch = false;
534             leadingWhitespaces = false;
535         }
536         this->shiftCluster(cluster, shift, prevShift);
537         prevShift = shift;
538         // We skip ideographic whitespaces
539         if (!cluster->isWhitespaceBreak() && cluster->isIdeographic()) {
540             shift += step;
541             whitespacePatch = true;
542             --whitespacePatches;
543         }
544         return true;
545     });
546 
547     if (whitespacePatch && whitespacePatches < 0) {
548         whitespacePatches++;
549         shift -= step;
550     }
551 
552     SkAssertResult(nearlyEqual(shift, maxWidth - textLen));
553     SkASSERT(whitespacePatches == 0);
554 
555     this->fWidthWithSpaces += ghostShift;
556     this->fAdvance.fX = maxWidth;
557 }
558 
shiftCluster(const Cluster * cluster,SkScalar shift,SkScalar prevShift)559 void TextLine::shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift) {
560 
561     auto& run = cluster->run();
562     auto start = cluster->startPos();
563     auto end = cluster->endPos();
564 
565     if (end == run.size()) {
566         // Set the same shift for the fake last glyph (to avoid all extra checks)
567         ++end;
568     }
569 
570     if (run.fJustificationShifts.empty()) {
571         // Do not fill this array until needed
572         run.fJustificationShifts.push_back_n(run.size() + 1, { 0, 0 });
573     }
574 
575     for (size_t pos = start; pos < end; ++pos) {
576         run.fJustificationShifts[pos] = { shift, prevShift };
577     }
578 }
579 
createEllipsis(SkScalar maxWidth,const SkString & ellipsis,bool)580 void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) {
581     // Replace some clusters with the ellipsis
582     // Go through the clusters in the reverse logical order
583     // taking off cluster by cluster until the ellipsis fits
584     SkScalar width = fAdvance.fX;
585     RunIndex lastRun = EMPTY_RUN;
586     std::unique_ptr<Run> ellipsisRun;
587     for (auto clusterIndex = fGhostClusterRange.end; clusterIndex > fGhostClusterRange.start; --clusterIndex) {
588         auto& cluster = fOwner->cluster(clusterIndex - 1);
589         // Shape the ellipsis if the run has changed
590         if (lastRun != cluster.runIndex()) {
591             ellipsisRun = this->shapeEllipsis(ellipsis, &cluster);
592             if (ellipsisRun->advance().fX > maxWidth) {
593                 // Ellipsis is bigger than the entire line; no way we can add it at all
594                 // BUT! We can keep scanning in case the next run will give us better results
595                 lastRun = EMPTY_RUN;
596                 continue;
597             } else {
598                 // We may need to continue
599                 lastRun = cluster.runIndex();
600             }
601         }
602         // See if it fits
603         if (width + ellipsisRun->advance().fX > maxWidth) {
604             width -= cluster.width();
605             // Continue if the ellipsis does not fit
606             continue;
607         }
608         // We found enough room for the ellipsis
609         fAdvance.fX = width;
610         fEllipsis = std::move(ellipsisRun);
611         fEllipsis->setOwner(fOwner);
612 
613         // Let's update the line
614         fClusterRange.end = clusterIndex;
615         fGhostClusterRange.end = fClusterRange.end;
616         fEllipsis->fClusterStart = cluster.textRange().start;
617         fText.end = cluster.textRange().end;
618         fTextIncludingNewlines.end = cluster.textRange().end;
619         fTextExcludingSpaces.end = cluster.textRange().end;
620         break;
621     }
622 
623     if (!fEllipsis) {
624         // Weird situation: ellipsis does not fit; no ellipsis then
625         fClusterRange.end = fClusterRange.start;
626         fGhostClusterRange.end = fClusterRange.start;
627         fText.end = fText.start;
628         fTextIncludingNewlines.end = fTextIncludingNewlines.start;
629         fTextExcludingSpaces.end = fTextExcludingSpaces.start;
630         fAdvance.fX = 0;
631     }
632 }
633 
shapeEllipsis(const SkString & ellipsis,const Cluster * cluster)634 std::unique_ptr<Run> TextLine::shapeEllipsis(const SkString& ellipsis, const Cluster* cluster) {
635 
636     class ShapeHandler final : public SkShaper::RunHandler {
637     public:
638         ShapeHandler(SkScalar lineHeight, bool useHalfLeading, SkScalar baselineShift, const SkString& ellipsis)
639             : fRun(nullptr), fLineHeight(lineHeight), fUseHalfLeading(useHalfLeading), fBaselineShift(baselineShift), fEllipsis(ellipsis) {}
640         std::unique_ptr<Run> run() & { return std::move(fRun); }
641 
642     private:
643         void beginLine() override {}
644 
645         void runInfo(const RunInfo&) override {}
646 
647         void commitRunInfo() override {}
648 
649         Buffer runBuffer(const RunInfo& info) override {
650             SkASSERT(!fRun);
651             fRun = std::make_unique<Run>(nullptr, info, 0, fLineHeight, fUseHalfLeading, fBaselineShift, 0, 0);
652             return fRun->newRunBuffer();
653         }
654 
655         void commitRunBuffer(const RunInfo& info) override {
656             fRun->fAdvance.fX = info.fAdvance.fX;
657             fRun->fAdvance.fY = fRun->advance().fY;
658             fRun->fPlaceholderIndex = std::numeric_limits<size_t>::max();
659             fRun->fEllipsis = true;
660         }
661 
662         void commitLine() override {}
663 
664         std::unique_ptr<Run> fRun;
665         SkScalar fLineHeight;
666         bool fUseHalfLeading;
667         SkScalar fBaselineShift;
668         SkString fEllipsis;
669     };
670 
671     const Run& run = cluster->run();
672     TextStyle textStyle = fOwner->paragraphStyle().getTextStyle();
673     for (auto i = fBlockRange.start; i < fBlockRange.end; ++i) {
674         auto& block = fOwner->block(i);
675         if (run.leftToRight() && cluster->textRange().end <= block.fRange.end) {
676             textStyle = block.fStyle;
677             break;
678         } else if (!run.leftToRight() && cluster->textRange().start <= block.fRange.end) {
679             textStyle = block.fStyle;
680             break;
681         }
682     }
683 
684     auto shaped = [&](sk_sp<SkTypeface> typeface, sk_sp<SkFontMgr> fallback) -> std::unique_ptr<Run> {
685         ShapeHandler handler(run.heightMultiplier(), run.useHalfLeading(), run.baselineShift(), ellipsis);
686         SkFont font(std::move(typeface), textStyle.getFontSize());
687         font.setEdging(SkFont::Edging::kAntiAlias);
688         font.setHinting(SkFontHinting::kSlight);
689         font.setSubpixel(true);
690 
691         std::unique_ptr<SkShaper> shaper = SkShapers::HB::ShapeDontWrapOrReorder(
692                 fOwner->getUnicode(), fallback ? fallback : SkFontMgr::RefEmpty());
693 
694         const SkBidiIterator::Level defaultLevel = SkBidiIterator::kLTR;
695         const char* utf8 = ellipsis.c_str();
696         size_t utf8Bytes = ellipsis.size();
697 
698         std::unique_ptr<SkShaper::BiDiRunIterator> bidi = SkShapers::unicode::BidiRunIterator(
699                 fOwner->getUnicode(), utf8, utf8Bytes, defaultLevel);
700         SkASSERT(bidi);
701 
702         std::unique_ptr<SkShaper::LanguageRunIterator> language =
703                 SkShaper::MakeStdLanguageRunIterator(utf8, utf8Bytes);
704         SkASSERT(language);
705 
706         std::unique_ptr<SkShaper::ScriptRunIterator> script =
707                 SkShapers::HB::ScriptRunIterator(utf8, utf8Bytes);
708         SkASSERT(script);
709 
710         std::unique_ptr<SkShaper::FontRunIterator> fontRuns = SkShaper::MakeFontMgrRunIterator(
711                 utf8, utf8Bytes, font, fallback ? fallback : SkFontMgr::RefEmpty());
712         SkASSERT(fontRuns);
713 
714         shaper->shape(utf8,
715                       utf8Bytes,
716                       *fontRuns,
717                       *bidi,
718                       *script,
719                       *language,
720                       nullptr,
721                       0,
722                       std::numeric_limits<SkScalar>::max(),
723                       &handler);
724         auto ellipsisRun = handler.run();
725         ellipsisRun->fTextRange = TextRange(0, ellipsis.size());
726         ellipsisRun->fOwner = fOwner;
727         return ellipsisRun;
728     };
729 
730     // Check the current font
731     auto ellipsisRun = shaped(run.fFont.refTypeface(), nullptr);
732     if (ellipsisRun->isResolved()) {
733         return ellipsisRun;
734     }
735 
736     // Check all allowed fonts
737     std::vector<sk_sp<SkTypeface>> typefaces = fOwner->fontCollection()->findTypefaces(
738             textStyle.getFontFamilies(), textStyle.getFontStyle(), textStyle.getFontArguments());
739     for (const auto& typeface : typefaces) {
740         ellipsisRun = shaped(typeface, nullptr);
741         if (ellipsisRun->isResolved()) {
742             return ellipsisRun;
743         }
744     }
745 
746     // Try the fallback
747     if (fOwner->fontCollection()->fontFallbackEnabled()) {
748         const char* ch = ellipsis.c_str();
749       SkUnichar unicode = SkUTF::NextUTF8WithReplacement(&ch,
750                                                          ellipsis.c_str()
751                                                              + ellipsis.size());
752         // We do not expect emojis in ellipsis so if they appeat there
753         // they will not be resolved with the pretiest color emoji font
754         auto typeface = fOwner->fontCollection()->defaultFallback(
755                                             unicode,
756                                             textStyle.getFontStyle(),
757                                             textStyle.getLocale());
758         if (typeface) {
759             ellipsisRun = shaped(typeface, fOwner->fontCollection()->getFallbackManager());
760             if (ellipsisRun->isResolved()) {
761                 return ellipsisRun;
762             }
763         }
764     }
765     return ellipsisRun;
766 }
767 
measureTextInsideOneRun(TextRange textRange,const Run * run,SkScalar runOffsetInLine,SkScalar textOffsetInRunInLine,bool includeGhostSpaces,TextAdjustment textAdjustment) const768 TextLine::ClipContext TextLine::measureTextInsideOneRun(TextRange textRange,
769                                                         const Run* run,
770                                                         SkScalar runOffsetInLine,
771                                                         SkScalar textOffsetInRunInLine,
772                                                         bool includeGhostSpaces,
773                                                         TextAdjustment textAdjustment) const {
774     ClipContext result = { run, 0, run->size(), 0, SkRect::MakeEmpty(), 0, false };
775 
776     if (run->fEllipsis) {
777         // Both ellipsis and placeholders can only be measured as one glyph
778         result.fTextShift = runOffsetInLine;
779         result.clip = SkRect::MakeXYWH(runOffsetInLine,
780                                        sizes().runTop(run, this->fAscentStyle),
781                                        run->advance().fX,
782                                        run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
783         return result;
784     } else if (run->isPlaceholder()) {
785         result.fTextShift = runOffsetInLine;
786         if (SkIsFinite(run->fFontMetrics.fAscent)) {
787           result.clip = SkRect::MakeXYWH(runOffsetInLine,
788                                          sizes().runTop(run, this->fAscentStyle),
789                                          run->advance().fX,
790                                          run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
791         } else {
792             result.clip = SkRect::MakeXYWH(runOffsetInLine, run->fFontMetrics.fAscent, run->advance().fX, 0);
793         }
794         return result;
795     } else if (textRange.empty()) {
796         return result;
797     }
798 
799     TextRange originalTextRange(textRange); // We need it for proportional measurement
800     // Find [start:end] clusters for the text
801     while (true) {
802         // Update textRange by cluster edges (shift start up to the edge of the cluster)
803         // TODO: remove this limitation?
804         TextRange updatedTextRange;
805         bool found;
806         std::tie(found, updatedTextRange.start, updatedTextRange.end) =
807                                         run->findLimitingGlyphClusters(textRange);
808         if (!found) {
809             return result;
810         }
811 
812         if ((textAdjustment & TextAdjustment::Grapheme) == 0) {
813             textRange = updatedTextRange;
814             break;
815         }
816 
817         // Update text range by grapheme edges (shift start up to the edge of the grapheme)
818         std::tie(found, updatedTextRange.start, updatedTextRange.end) =
819                                     run->findLimitingGraphemes(updatedTextRange);
820         if (updatedTextRange == textRange) {
821             break;
822         }
823 
824         // Some clusters are inside graphemes and we need to adjust them
825         //SkDebugf("Correct range: [%d:%d) -> [%d:%d)\n", textRange.start, textRange.end, startIndex, endIndex);
826         textRange = updatedTextRange;
827 
828         // Move the start until it's on the grapheme edge (and glypheme, too)
829     }
830     Cluster* start = &fOwner->cluster(fOwner->clusterIndex(textRange.start));
831     Cluster* end = &fOwner->cluster(fOwner->clusterIndex(textRange.end - (textRange.width() == 0 ? 0 : 1)));
832 
833     if (!run->leftToRight()) {
834         std::swap(start, end);
835     }
836     result.pos = start->startPos();
837     result.size = (end->isHardBreak() ? end->startPos() : end->endPos()) - start->startPos();
838     auto textStartInRun = run->positionX(start->startPos());
839     auto textStartInLine = runOffsetInLine + textOffsetInRunInLine;
840     if (!run->leftToRight()) {
841         std::swap(start, end);
842     }
843 /*
844     if (!run->fJustificationShifts.empty()) {
845         SkDebugf("Justification for [%d:%d)\n", textRange.start, textRange.end);
846         for (auto i = result.pos; i < result.pos + result.size; ++i) {
847             auto j = run->fJustificationShifts[i];
848             SkDebugf("[%d] = %f %f\n", i, j.fX, j.fY);
849         }
850     }
851 */
852     // Calculate the clipping rectangle for the text with cluster edges
853     // There are 2 cases:
854     // EOL (when we expect the last cluster clipped without any spaces)
855     // Anything else (when we want the cluster width contain all the spaces -
856     // coming from letter spacing or word spacing or justification)
857     result.clip =
858             SkRect::MakeXYWH(0,
859                              sizes().runTop(run, this->fAscentStyle),
860                              run->calculateWidth(result.pos, result.pos + result.size, false),
861                              run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
862 
863     // Correct the width in case the text edges don't match clusters
864     // TODO: This is where we get smart about selecting a part of a cluster
865     //  by shaping each grapheme separately and then use the result sizes
866     //  to calculate the proportions
867     auto leftCorrection = start->sizeToChar(originalTextRange.start);
868     auto rightCorrection = end->sizeFromChar(originalTextRange.end - 1);
869     /*
870     SkDebugf("[%d: %d) => [%d: %d), @%d, %d: [%f:%f) + [%f:%f) = ", // جَآَهُ
871              originalTextRange.start, originalTextRange.end, textRange.start, textRange.end,
872              result.pos, result.size,
873              result.clip.fLeft, result.clip.fRight, leftCorrection, rightCorrection);
874      */
875     result.clippingNeeded = leftCorrection != 0 || rightCorrection != 0;
876     if (run->leftToRight()) {
877         result.clip.fLeft += leftCorrection;
878         result.clip.fRight -= rightCorrection;
879         textStartInLine -= leftCorrection;
880     } else {
881         result.clip.fRight -= leftCorrection;
882         result.clip.fLeft += rightCorrection;
883         textStartInLine -= rightCorrection;
884     }
885 
886     result.clip.offset(textStartInLine, 0);
887     //SkDebugf("@%f[%f:%f)\n", textStartInLine, result.clip.fLeft, result.clip.fRight);
888 
889     if (compareRound(result.clip.fRight, fAdvance.fX, fOwner->getApplyRoundingHack()) > 0 && !includeGhostSpaces) {
890         // There are few cases when we need it.
891         // The most important one: we measure the text with spaces at the end (or at the beginning in RTL)
892         // and we should ignore these spaces
893         if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kLtr) {
894             // We only use this member for LTR
895             result.fExcludedTrailingSpaces = std::max(result.clip.fRight - fAdvance.fX, 0.0f);
896             result.clippingNeeded = true;
897             result.clip.fRight = fAdvance.fX;
898         }
899     }
900 
901     if (result.clip.width() < 0) {
902         // Weird situation when glyph offsets move the glyph to the left
903         // (happens with zalgo texts, for instance)
904         result.clip.fRight = result.clip.fLeft;
905     }
906 
907     // The text must be aligned with the lineOffset
908     result.fTextShift = textStartInLine - textStartInRun;
909 
910     return result;
911 }
912 
iterateThroughClustersInGlyphsOrder(bool reversed,bool includeGhosts,const ClustersVisitor & visitor) const913 void TextLine::iterateThroughClustersInGlyphsOrder(bool reversed,
914                                                    bool includeGhosts,
915                                                    const ClustersVisitor& visitor) const {
916     // Walk through the clusters in the logical order (or reverse)
917     SkSpan<const size_t> runs(fRunsInVisualOrder.data(), fRunsInVisualOrder.size());
918     bool ignore = false;
919     ClusterIndex index = 0;
920     directional_for_each(runs, !reversed, [&](decltype(runs[0]) r) {
921         if (ignore) return;
922         auto run = this->fOwner->run(r);
923         auto trimmedRange = fClusterRange.intersection(run.clusterRange());
924         auto trailedRange = fGhostClusterRange.intersection(run.clusterRange());
925         SkASSERT(trimmedRange.start == trailedRange.start);
926 
927         auto trailed = fOwner->clusters(trailedRange);
928         auto trimmed = fOwner->clusters(trimmedRange);
929         directional_for_each(trailed, reversed != run.leftToRight(), [&](Cluster& cluster) {
930             if (ignore) return;
931             bool ghost =  &cluster >= trimmed.end();
932             if (!includeGhosts && ghost) {
933                 return;
934             }
935             if (!visitor(&cluster, index++, ghost)) {
936 
937                 ignore = true;
938                 return;
939             }
940         });
941     });
942 }
943 
iterateThroughSingleRunByStyles(TextAdjustment textAdjustment,const Run * run,SkScalar runOffset,TextRange textRange,StyleType styleType,const RunStyleVisitor & visitor) const944 SkScalar TextLine::iterateThroughSingleRunByStyles(TextAdjustment textAdjustment,
945                                                    const Run* run,
946                                                    SkScalar runOffset,
947                                                    TextRange textRange,
948                                                    StyleType styleType,
949                                                    const RunStyleVisitor& visitor) const {
950     auto correctContext = [&](TextRange textRange, SkScalar textOffsetInRun) -> ClipContext {
951         auto result = this->measureTextInsideOneRun(
952                 textRange, run, runOffset, textOffsetInRun, false, textAdjustment);
953         if (styleType == StyleType::kDecorations) {
954             // Decorations are drawn based on the real font metrics (regardless of styles and strut)
955             result.clip.fTop = this->sizes().runTop(run, LineMetricStyle::CSS);
956             result.clip.fBottom = result.clip.fTop +
957                                   run->calculateHeight(LineMetricStyle::CSS, LineMetricStyle::CSS);
958         }
959         return result;
960     };
961 
962     if (run->fEllipsis) {
963         // Extra efforts to get the ellipsis text style
964         ClipContext clipContext = correctContext(run->textRange(), 0.0f);
965         TextRange testRange(run->fClusterStart, run->fClusterStart + run->textRange().width());
966         for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) {
967            auto block = fOwner->styles().begin() + index;
968            auto intersect = intersected(block->fRange, testRange);
969            if (intersect.width() > 0) {
970                visitor(testRange, block->fStyle, clipContext);
971                return run->advance().fX;
972            }
973         }
974         SkASSERT(false);
975     }
976 
977     if (styleType == StyleType::kNone) {
978         ClipContext clipContext = correctContext(textRange, 0.0f);
979         // The placehoder can have height=0 or (exclusively) width=0 and still be a thing
980         if (clipContext.clip.height() > 0.0f || clipContext.clip.width() > 0.0f) {
981             visitor(textRange, TextStyle(), clipContext);
982             return clipContext.clip.width();
983         } else {
984             return 0;
985         }
986     }
987 
988     TextIndex start = EMPTY_INDEX;
989     size_t size = 0;
990     const TextStyle* prevStyle = nullptr;
991     SkScalar textOffsetInRun = 0;
992 
993     const BlockIndex blockRangeSize = fBlockRange.end - fBlockRange.start;
994     for (BlockIndex index = 0; index <= blockRangeSize; ++index) {
995 
996         TextRange intersect;
997         TextStyle* style = nullptr;
998         if (index < blockRangeSize) {
999             auto block = fOwner->styles().begin() +
1000                  (run->leftToRight() ? fBlockRange.start + index : fBlockRange.end - index - 1);
1001 
1002             // Get the text
1003             intersect = intersected(block->fRange, textRange);
1004             if (intersect.width() == 0) {
1005                 if (start == EMPTY_INDEX) {
1006                     // This style is not applicable to the text yet
1007                     continue;
1008                 } else {
1009                     // We have found all the good styles already
1010                     // but we need to process the last one of them
1011                     intersect = TextRange(start, start + size);
1012                     index = fBlockRange.end;
1013                 }
1014             } else {
1015                 // Get the style
1016                 style = &block->fStyle;
1017                 if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) {
1018                     size += intersect.width();
1019                     // RTL text intervals move backward
1020                     start = std::min(intersect.start, start);
1021                     continue;
1022                 } else if (start == EMPTY_INDEX ) {
1023                     // First time only
1024                     prevStyle = style;
1025                     size = intersect.width();
1026                     start = intersect.start;
1027                     continue;
1028                 }
1029             }
1030         } else if (prevStyle != nullptr) {
1031             // This is the last style
1032         } else {
1033             break;
1034         }
1035 
1036         // We have the style and the text
1037         auto runStyleTextRange = TextRange(start, start + size);
1038         ClipContext clipContext = correctContext(runStyleTextRange, textOffsetInRun);
1039         textOffsetInRun += clipContext.clip.width();
1040         if (clipContext.clip.height() == 0) {
1041             continue;
1042         }
1043         visitor(runStyleTextRange, *prevStyle, clipContext);
1044 
1045         // Start all over again
1046         prevStyle = style;
1047         start = intersect.start;
1048         size = intersect.width();
1049     }
1050     return textOffsetInRun;
1051 }
1052 
iterateThroughVisualRuns(bool includingGhostSpaces,const RunVisitor & visitor) const1053 void TextLine::iterateThroughVisualRuns(bool includingGhostSpaces, const RunVisitor& visitor) const {
1054 
1055     // Walk through all the runs that intersect with the line in visual order
1056     SkScalar width = 0;
1057     SkScalar runOffset = 0;
1058     SkScalar totalWidth = 0;
1059     auto textRange = includingGhostSpaces ? this->textWithNewlines() : this->trimmedText();
1060 
1061     if (this->ellipsis() != nullptr && fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) {
1062         runOffset = this->ellipsis()->offset().fX;
1063         if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) {
1064         }
1065     }
1066 
1067     for (auto& runIndex : fRunsInVisualOrder) {
1068 
1069         const auto run = &this->fOwner->run(runIndex);
1070         auto lineIntersection = intersected(run->textRange(), textRange);
1071         if (lineIntersection.width() == 0 && this->width() != 0) {
1072             // TODO: deal with empty runs in a better way
1073             continue;
1074         }
1075         if (!run->leftToRight() && runOffset == 0 && includingGhostSpaces) {
1076             // runOffset does not take in account a possibility
1077             // that RTL run could start before the line (trailing spaces)
1078             // so we need to do runOffset -= "trailing whitespaces length"
1079             TextRange whitespaces = intersected(
1080                     TextRange(fTextExcludingSpaces.end, fTextIncludingNewlines.end), run->fTextRange);
1081             if (whitespaces.width() > 0) {
1082                 auto whitespacesLen = measureTextInsideOneRun(whitespaces, run, runOffset, 0, true, TextAdjustment::GlyphCluster).clip.width();
1083                 runOffset -= whitespacesLen;
1084             }
1085         }
1086         runOffset += width;
1087         totalWidth += width;
1088         if (!visitor(run, runOffset, lineIntersection, &width)) {
1089             return;
1090         }
1091     }
1092 
1093     runOffset += width;
1094     totalWidth += width;
1095 
1096     if (this->ellipsis() != nullptr && fOwner->paragraphStyle().getTextDirection() == TextDirection::kLtr) {
1097         if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) {
1098             totalWidth += width;
1099         }
1100     }
1101 
1102     if (!includingGhostSpaces && compareRound(totalWidth, this->width(), fOwner->getApplyRoundingHack()) != 0) {
1103     // This is a very important assert!
1104     // It asserts that 2 different ways of calculation come with the same results
1105         SkDEBUGFAILF("ASSERT: %f != %f\n", totalWidth, this->width());
1106     }
1107 }
1108 
offset() const1109 SkVector TextLine::offset() const {
1110     return fOffset + SkVector::Make(fShift, 0);
1111 }
1112 
getMetrics() const1113 LineMetrics TextLine::getMetrics() const {
1114     LineMetrics result;
1115     SkASSERT(fOwner);
1116 
1117     // Fill out the metrics
1118     fOwner->ensureUTF16Mapping();
1119     result.fStartIndex = fOwner->getUTF16Index(fTextExcludingSpaces.start);
1120     result.fEndExcludingWhitespaces = fOwner->getUTF16Index(fTextExcludingSpaces.end);
1121     result.fEndIndex = fOwner->getUTF16Index(fText.end);
1122     result.fEndIncludingNewline = fOwner->getUTF16Index(fTextIncludingNewlines.end);
1123     result.fHardBreak = endsWithHardLineBreak();
1124     result.fAscent = - fMaxRunMetrics.ascent();
1125     result.fDescent = fMaxRunMetrics.descent();
1126     result.fUnscaledAscent = - fMaxRunMetrics.ascent(); // TODO: implement
1127     result.fHeight = fAdvance.fY;
1128     result.fWidth = fAdvance.fX;
1129     if (fOwner->getApplyRoundingHack()) {
1130         result.fHeight = littleRound(result.fHeight);
1131         result.fWidth = littleRound(result.fWidth);
1132     }
1133     result.fLeft = this->offset().fX;
1134     // This is Flutter definition of a baseline
1135     result.fBaseline = this->offset().fY + this->height() - this->sizes().descent();
1136     result.fLineNumber = this - fOwner->lines().begin();
1137 
1138     // Fill out the style parts
1139     this->iterateThroughVisualRuns(false,
1140         [this, &result]
1141         (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
1142         if (run->placeholderStyle() != nullptr) {
1143             *runWidthInLine = run->advance().fX;
1144             return true;
1145         }
1146         *runWidthInLine = this->iterateThroughSingleRunByStyles(
1147         TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kForeground,
1148         [&result, &run](TextRange textRange, const TextStyle& style, const ClipContext& context) {
1149             SkFontMetrics fontMetrics;
1150             run->fFont.getMetrics(&fontMetrics);
1151             StyleMetrics styleMetrics(&style, fontMetrics);
1152             result.fLineMetrics.emplace(textRange.start, styleMetrics);
1153         });
1154         return true;
1155     });
1156 
1157     return result;
1158 }
1159 
isFirstLine() const1160 bool TextLine::isFirstLine() const {
1161     return this == &fOwner->lines().front();
1162 }
1163 
isLastLine() const1164 bool TextLine::isLastLine() const {
1165     return this == &fOwner->lines().back();
1166 }
1167 
endsWithHardLineBreak() const1168 bool TextLine::endsWithHardLineBreak() const {
1169     // TODO: For some reason Flutter imagines a hard line break at the end of the last line.
1170     //  To be removed...
1171     return (fGhostClusterRange.width() > 0 && fOwner->cluster(fGhostClusterRange.end - 1).isHardBreak()) ||
1172            fEllipsis != nullptr ||
1173            fGhostClusterRange.end == fOwner->clusters().size() - 1;
1174 }
1175 
getRectsForRange(TextRange textRange0,RectHeightStyle rectHeightStyle,RectWidthStyle rectWidthStyle,std::vector<TextBox> & boxes) const1176 void TextLine::getRectsForRange(TextRange textRange0,
1177                                 RectHeightStyle rectHeightStyle,
1178                                 RectWidthStyle rectWidthStyle,
1179                                 std::vector<TextBox>& boxes) const
1180 {
1181     const Run* lastRun = nullptr;
1182     auto startBox = boxes.size();
1183     this->iterateThroughVisualRuns(true,
1184         [textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
1185         (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
1186         *runWidthInLine = this->iterateThroughSingleRunByStyles(
1187         TextAdjustment::GraphemeGluster, run, runOffsetInLine, textRange, StyleType::kNone,
1188         [run, runOffsetInLine, textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
1189         (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& lineContext) {
1190 
1191             auto intersect = textRange * textRange0;
1192             if (intersect.empty()) {
1193                 return true;
1194             }
1195 
1196             auto paragraphStyle = fOwner->paragraphStyle();
1197 
1198             // Found a run that intersects with the text
1199             auto context = this->measureTextInsideOneRun(
1200                     intersect, run, runOffsetInLine, 0, true, TextAdjustment::GraphemeGluster);
1201             SkRect clip = context.clip;
1202             clip.offset(lineContext.fTextShift - context.fTextShift, 0);
1203 
1204             switch (rectHeightStyle) {
1205                 case RectHeightStyle::kMax:
1206                     // TODO: Change it once flutter rolls into google3
1207                     //  (probably will break things if changed before)
1208                     clip.fBottom = this->height();
1209                     clip.fTop = this->sizes().delta();
1210                     break;
1211                 case RectHeightStyle::kIncludeLineSpacingTop: {
1212                     clip.fBottom = this->height();
1213                     clip.fTop = this->sizes().delta();
1214                     auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent();
1215                     if (isFirstLine()) {
1216                         clip.fTop += verticalShift;
1217                     }
1218                     break;
1219                 }
1220                 case RectHeightStyle::kIncludeLineSpacingMiddle: {
1221                     clip.fBottom = this->height();
1222                     clip.fTop = this->sizes().delta();
1223                     auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent();
1224                     clip.offset(0, verticalShift / 2.0);
1225                     if (isFirstLine()) {
1226                         clip.fTop += verticalShift / 2.0;
1227                     }
1228                     if (isLastLine()) {
1229                         clip.fBottom -= verticalShift / 2.0;
1230                     }
1231                     break;
1232                  }
1233                 case RectHeightStyle::kIncludeLineSpacingBottom: {
1234                     clip.fBottom = this->height();
1235                     clip.fTop = this->sizes().delta();
1236                     auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent();
1237                     clip.offset(0, verticalShift);
1238                     if (isLastLine()) {
1239                         clip.fBottom -= verticalShift;
1240                     }
1241                     break;
1242                 }
1243                 case RectHeightStyle::kStrut: {
1244                     const auto& strutStyle = paragraphStyle.getStrutStyle();
1245                     if (strutStyle.getStrutEnabled()
1246                         && strutStyle.getFontSize() > 0) {
1247                         auto strutMetrics = fOwner->strutMetrics();
1248                         auto top = this->baseline();
1249                         clip.fTop = top + strutMetrics.ascent();
1250                         clip.fBottom = top + strutMetrics.descent();
1251                     }
1252                 }
1253                 break;
1254                 case RectHeightStyle::kTight: {
1255                     if (run->fHeightMultiplier <= 0) {
1256                         break;
1257                     }
1258                     const auto effectiveBaseline = this->baseline() + this->sizes().delta();
1259                     clip.fTop = effectiveBaseline + run->ascent();
1260                     clip.fBottom = effectiveBaseline + run->descent();
1261                 }
1262                 break;
1263                 default:
1264                     SkASSERT(false);
1265                 break;
1266             }
1267 
1268             // Separate trailing spaces and move them in the default order of the paragraph
1269             // in case the run order and the paragraph order don't match
1270             SkRect trailingSpaces = SkRect::MakeEmpty();
1271             if (this->trimmedText().end <this->textWithNewlines().end && // Line has trailing space
1272                 this->textWithNewlines().end == intersect.end &&         // Range is at the end of the line
1273                 this->trimmedText().end > intersect.start)               // Range has more than just spaces
1274             {
1275                 auto delta = this->spacesWidth();
1276                 trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0);
1277                 // There are trailing spaces in this run
1278                 if (paragraphStyle.getTextAlign() == TextAlign::kJustify && isLastLine())
1279                 {
1280                     // TODO: this is just a patch. Make it right later (when it's clear what and how)
1281                     trailingSpaces = clip;
1282                     if(run->leftToRight()) {
1283                         trailingSpaces.fLeft = this->width();
1284                         clip.fRight = this->width();
1285                     } else {
1286                         trailingSpaces.fRight = 0;
1287                         clip.fLeft = 0;
1288                     }
1289                 } else if (paragraphStyle.getTextDirection() == TextDirection::kRtl &&
1290                     !run->leftToRight())
1291                 {
1292                     // Split
1293                     trailingSpaces = clip;
1294                     trailingSpaces.fLeft = - delta;
1295                     trailingSpaces.fRight = 0;
1296                     clip.fLeft += delta;
1297                 } else if (paragraphStyle.getTextDirection() == TextDirection::kLtr &&
1298                     run->leftToRight())
1299                 {
1300                     // Split
1301                     trailingSpaces = clip;
1302                     trailingSpaces.fLeft = this->width();
1303                     trailingSpaces.fRight = trailingSpaces.fLeft + delta;
1304                     clip.fRight -= delta;
1305                 }
1306             }
1307 
1308             clip.offset(this->offset());
1309             if (trailingSpaces.width() > 0) {
1310                 trailingSpaces.offset(this->offset());
1311             }
1312 
1313             // Check if we can merge two boxes instead of adding a new one
1314             auto merge = [&lastRun, &context, &boxes](SkRect clip) {
1315                 bool mergedBoxes = false;
1316                 if (!boxes.empty() &&
1317                     lastRun != nullptr &&
1318                     context.run->leftToRight() == lastRun->leftToRight() &&
1319                     lastRun->placeholderStyle() == nullptr &&
1320                     context.run->placeholderStyle() == nullptr &&
1321                     nearlyEqual(lastRun->heightMultiplier(),
1322                                 context.run->heightMultiplier()) &&
1323                     lastRun->font() == context.run->font())
1324                 {
1325                     auto& lastBox = boxes.back();
1326                     if (nearlyEqual(lastBox.rect.fTop, clip.fTop) &&
1327                         nearlyEqual(lastBox.rect.fBottom, clip.fBottom) &&
1328                             (nearlyEqual(lastBox.rect.fLeft, clip.fRight) ||
1329                              nearlyEqual(lastBox.rect.fRight, clip.fLeft)))
1330                     {
1331                         lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft);
1332                         lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight);
1333                         mergedBoxes = true;
1334                     }
1335                 }
1336                 lastRun = context.run;
1337                 return mergedBoxes;
1338             };
1339 
1340             if (!merge(clip)) {
1341                 boxes.emplace_back(clip, context.run->getTextDirection());
1342             }
1343             if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) {
1344                 boxes.emplace_back(trailingSpaces, paragraphStyle.getTextDirection());
1345             }
1346 
1347             if (rectWidthStyle == RectWidthStyle::kMax && !isLastLine()) {
1348                 // Align the very left/right box horizontally
1349                 auto lineStart = this->offset().fX;
1350                 auto lineEnd = this->offset().fX + this->width();
1351                 auto left = boxes[startBox];
1352                 auto right = boxes.back();
1353                 if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) {
1354                     left.rect.fRight = left.rect.fLeft;
1355                     left.rect.fLeft = 0;
1356                     boxes.insert(boxes.begin() + startBox + 1, left);
1357                 }
1358                 if (right.direction == TextDirection::kLtr &&
1359                     right.rect.fRight >= lineEnd &&
1360                     right.rect.fRight < fOwner->widthWithTrailingSpaces()) {
1361                     right.rect.fLeft = right.rect.fRight;
1362                     right.rect.fRight = fOwner->widthWithTrailingSpaces();
1363                     boxes.emplace_back(right);
1364                 }
1365             }
1366 
1367             return true;
1368         });
1369         return true;
1370     });
1371     if (fOwner->getApplyRoundingHack()) {
1372         for (auto& r : boxes) {
1373             r.rect.fLeft = littleRound(r.rect.fLeft);
1374             r.rect.fRight = littleRound(r.rect.fRight);
1375             r.rect.fTop = littleRound(r.rect.fTop);
1376             r.rect.fBottom = littleRound(r.rect.fBottom);
1377         }
1378     }
1379 }
1380 
getGlyphPositionAtCoordinate(SkScalar dx)1381 PositionWithAffinity TextLine::getGlyphPositionAtCoordinate(SkScalar dx) {
1382 
1383     if (SkScalarNearlyZero(this->width()) && SkScalarNearlyZero(this->spacesWidth())) {
1384         // TODO: this is one of the flutter changes that have to go away eventually
1385         //  Empty line is a special case in txtlib (but only when there are no spaces, too)
1386         auto utf16Index = fOwner->getUTF16Index(this->fTextExcludingSpaces.end);
1387         return { SkToS32(utf16Index) , kDownstream };
1388     }
1389 
1390     PositionWithAffinity result(0, Affinity::kDownstream);
1391     this->iterateThroughVisualRuns(true,
1392         [this, dx, &result]
1393         (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
1394             bool keepLooking = true;
1395             *runWidthInLine = this->iterateThroughSingleRunByStyles(
1396             TextAdjustment::GraphemeGluster, run, runOffsetInLine, textRange, StyleType::kNone,
1397             [this, run, dx, &result, &keepLooking]
1398             (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context0) {
1399 
1400                 SkScalar offsetX = this->offset().fX;
1401                 ClipContext context = context0;
1402 
1403                 // Correct the clip size because libtxt counts trailing spaces
1404                 if (run->leftToRight()) {
1405                     context.clip.fRight += context.fExcludedTrailingSpaces; // extending clip to the right
1406                 } else {
1407                     // Clip starts from 0; we cannot extend it to the left from that
1408                 }
1409                 // However, we need to offset the clip
1410                 context.clip.offset(offsetX, 0.0f);
1411 
1412                 // This patch will help us to avoid a floating point error
1413                 if (SkScalarNearlyEqual(context.clip.fRight, dx, 0.01f)) {
1414                     context.clip.fRight = dx;
1415                 }
1416 
1417                 if (dx <= context.clip.fLeft) {
1418                     // All the other runs are placed right of this one
1419                     auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos));
1420                     if (run->leftToRight()) {
1421                         result = { SkToS32(utf16Index), kDownstream};
1422                         keepLooking = false;
1423                     } else {
1424                         result = { SkToS32(utf16Index + 1), kUpstream};
1425                         // If we haven't reached the end of the run we need to keep looking
1426                         keepLooking = context.pos != 0;
1427                     }
1428                     // For RTL we go another way
1429                     return !run->leftToRight();
1430                 }
1431 
1432                 if (dx >= context.clip.fRight) {
1433                     // We have to keep looking ; just in case keep the last one as the closest
1434                     auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos + context.size));
1435                     if (run->leftToRight()) {
1436                         result = {SkToS32(utf16Index), kUpstream};
1437                     } else {
1438                         result = {SkToS32(utf16Index), kDownstream};
1439                     }
1440                     // For RTL we go another way
1441                     return run->leftToRight();
1442                 }
1443 
1444                 // So we found the run that contains our coordinates
1445                 // Find the glyph position in the run that is the closest left of our point
1446                 // TODO: binary search
1447                 size_t found = context.pos;
1448                 for (size_t index = context.pos; index < context.pos + context.size; ++index) {
1449                     // TODO: this rounding is done to match Flutter tests. Must be removed..
1450                     auto end = context.run->positionX(index) + context.fTextShift + offsetX;
1451                     if (fOwner->getApplyRoundingHack()) {
1452                         end = littleRound(end);
1453                     }
1454                     if (end > dx) {
1455                         break;
1456                     } else if (end == dx && !context.run->leftToRight()) {
1457                         // When we move RTL variable end points to the beginning of the code point which is included
1458                         found = index;
1459                         break;
1460                     }
1461                     found = index;
1462                 }
1463 
1464                 SkScalar glyphemePosLeft = context.run->positionX(found) + context.fTextShift + offsetX;
1465                 SkScalar glyphemesWidth = context.run->positionX(found + 1) - context.run->positionX(found);
1466 
1467                 // Find the grapheme range that contains the point
1468                 auto clusterIndex8 = context.run->globalClusterIndex(found);
1469                 auto clusterEnd8 = context.run->globalClusterIndex(found + 1);
1470                 auto graphemes = fOwner->countSurroundingGraphemes({clusterIndex8, clusterEnd8});
1471 
1472                 SkScalar center = glyphemePosLeft + glyphemesWidth / 2;
1473                 if (graphemes.size() > 1) {
1474                     // Calculate the position proportionally based on grapheme count
1475                     SkScalar averageGraphemeWidth = glyphemesWidth / graphemes.size();
1476                     SkScalar delta = dx - glyphemePosLeft;
1477                     int graphemeIndex = SkScalarNearlyZero(averageGraphemeWidth)
1478                                          ? 0
1479                                          : SkScalarFloorToInt(delta / averageGraphemeWidth);
1480                     auto graphemeCenter = glyphemePosLeft + graphemeIndex * averageGraphemeWidth +
1481                                           averageGraphemeWidth / 2;
1482                     auto graphemeUtf8Index = graphemes[graphemeIndex];
1483                     if ((dx < graphemeCenter) == context.run->leftToRight()) {
1484                         size_t utf16Index = fOwner->getUTF16Index(graphemeUtf8Index);
1485                         result = { SkToS32(utf16Index), kDownstream };
1486                     } else {
1487                         size_t utf16Index = fOwner->getUTF16Index(graphemeUtf8Index + 1);
1488                         result = { SkToS32(utf16Index), kUpstream };
1489                     }
1490                     // Keep UTF16 index as is
1491                 } else if ((dx < center) == context.run->leftToRight()) {
1492                     size_t utf16Index = fOwner->getUTF16Index(clusterIndex8);
1493                     result = { SkToS32(utf16Index), kDownstream };
1494                 } else {
1495                     size_t utf16Index = context.run->leftToRight()
1496                                                 ? fOwner->getUTF16Index(clusterEnd8)
1497                                                 : fOwner->getUTF16Index(clusterIndex8) + 1;
1498                     result = { SkToS32(utf16Index), kUpstream };
1499                 }
1500 
1501                 return keepLooking = false;
1502 
1503             });
1504             return keepLooking;
1505         }
1506     );
1507     return result;
1508 }
1509 
getRectsForPlaceholders(std::vector<TextBox> & boxes)1510 void TextLine::getRectsForPlaceholders(std::vector<TextBox>& boxes) {
1511     this->iterateThroughVisualRuns(
1512         true,
1513         [&boxes, this](const Run* run, SkScalar runOffset, TextRange textRange,
1514                         SkScalar* width) {
1515                 auto context = this->measureTextInsideOneRun(
1516                         textRange, run, runOffset, 0, true, TextAdjustment::GraphemeGluster);
1517                 *width = context.clip.width();
1518 
1519             if (textRange.width() == 0) {
1520                 return true;
1521             }
1522             if (!run->isPlaceholder()) {
1523                 return true;
1524             }
1525 
1526             SkRect clip = context.clip;
1527             clip.offset(this->offset());
1528 
1529             if (fOwner->getApplyRoundingHack()) {
1530                 clip.fLeft = littleRound(clip.fLeft);
1531                 clip.fRight = littleRound(clip.fRight);
1532                 clip.fTop = littleRound(clip.fTop);
1533                 clip.fBottom = littleRound(clip.fBottom);
1534             }
1535             boxes.emplace_back(clip, run->getTextDirection());
1536             return true;
1537         });
1538 }
1539 }  // namespace textlayout
1540 }  // namespace skia
1541