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/SkString.h"
9#include "include/core/SkTextBlob.h"
10#include "include/core/SkTypes.h"
11#include "include/private/SkTemplates.h"
12#include "include/private/SkTo.h"
13#include "modules/skparagraph/include/DartTypes.h"
14#include "modules/skparagraph/include/Metrics.h"
15#include "modules/skparagraph/include/ParagraphStyle.h"
16#include "modules/skparagraph/include/TextShadow.h"
17#include "modules/skparagraph/include/TextStyle.h"
18#include "modules/skparagraph/src/Decorations.h"
19#include "modules/skparagraph/src/ParagraphImpl.h"
20#include "modules/skparagraph/src/TextLine.h"
21#include "modules/skshaper/include/SkShaper.h"
22#include "src/core/SkSpan.h"
23
24#include <algorithm>
25#include <iterator>
26#include <limits>
27#include <map>
28#include <memory>
29#include <tuple>
30#include <type_traits>
31#include <utility>
32
33namespace skia {
34namespace textlayout {
35
36namespace {
37
38// TODO: deal with all the intersection functionality
39TextRange 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;
44}
45
46SkScalar littleRound(SkScalar a) {
47 // This rounding is done to match Flutter tests. Must be removed..
48 return SkScalarRoundToScalar(a * 100.0)/100.0;
49}
50
51TextRange 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;
56}
57
58int 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) {
67 return 0;
68 }
69
70 auto ra = littleRound(a);
71 auto rb = littleRound(b);
72 if (ra < rb) {
73 return -1;
74 } else {
75 return 1;
76 }
77}
78
79} // namespace
80
81TextLine::TextLine(ParagraphImpl* owner,
82 SkVector offset,
83 SkVector advance,
84 BlockRange blocks,
85 TextRange text,
86 TextRange textWithSpaces,
87 ClusterRange clusters,
88 ClusterRange clustersWithGhosts,
89 SkScalar widthWithSpaces,
90 InternalLineMetrics sizes)
91 : fOwner(owner)
92 , fBlockRange(blocks)
93 , fTextRange(text)
94 , fTextWithWhitespacesRange(textWithSpaces)
95 , fClusterRange(clusters)
96 , fGhostClusterRange(clustersWithGhosts)
97 , fRunsInVisualOrder()
98 , fAdvance(advance)
99 , fOffset(offset)
100 , fShift(0.0)
101 , fWidthWithSpaces(widthWithSpaces)
102 , fEllipsis(nullptr)
103 , fSizes(sizes)
104 , fHasBackground(false)
105 , fHasShadows(false)
106 , fHasDecorations(false)
107 , fAscentStyle(LineMetricStyle::CSS)
108 , fDescentStyle(LineMetricStyle::CSS) {
109 // Reorder visual runs
110 auto& start = owner->cluster(fGhostClusterRange.start);
111 auto& end = owner->cluster(fGhostClusterRange.end - 1);
112 size_t numRuns = end.runIndex() - start.runIndex() + 1;
113
114 for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) {
115 auto b = fOwner->styles().begin() + index;
116 if (b->fStyle.isPlaceholder()) {
117 continue;
118 }
119 if (b->fStyle.hasBackground()) {
120 fHasBackground = true;
121 }
122 if (b->fStyle.getDecorationType() != TextDecoration::kNoDecoration) {
123 fHasDecorations = true;
124 }
125 if (b->fStyle.getShadowNumber() > 0) {
126 fHasShadows = true;
127 }
128 }
129
130 // Get the logical order
131
132 // This is just chosen to catch the common/fast cases. Feel free to tweak.
133 constexpr int kPreallocCount = 4;
134 SkAutoSTArray<kPreallocCount, 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;
139 fMaxRunMetrics.add(
140 InternalLineMetrics(run.fFontMetrics.fAscent, run.fFontMetrics.fDescent, run.fFontMetrics.fLeading));
141 }
142 SkASSERT(runLevelsIndex == numRuns);
143
144 SkAutoSTArray<kPreallocCount, int32_t> logicalOrder(numRuns);
145
146 // TODO: hide all these logic in SkUnicode?
147 fOwner->getICU()->reorderVisual(runLevels.data(), numRuns, logicalOrder.data());
148 auto firstRunIndex = start.runIndex();
149 for (auto index : logicalOrder) {
150 fRunsInVisualOrder.push_back(firstRunIndex + index);
151 }
152
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();
157 break;
158 }
159 }
160}
161
162SkRect TextLine::calculateBoundaries() {
163
164 // For flutter: height and/or width and/or baseline! can be Inf
165 // (coming from placeholders - we should ignore it)
166 auto boundaries = SkRect::MakeWH(
167 SkScalarIsFinite(fAdvance.fX) ? fAdvance.fX : 0,
168 SkScalarIsFinite(fAdvance.fY) ? fAdvance.fY : 0);
169 auto baseline = SkScalarIsFinite(this->baseline()) ? this->baseline() : 0;
170 auto clusters = fOwner->clusters(fClusterRange);
171 Run* run = nullptr;
172 auto runShift = 0.0f;
173 auto clusterShift = 0.0f;
174 for (auto cluster = clusters.begin(); cluster != clusters.end(); ++cluster) {
175 if (run == nullptr || cluster->runIndex() != run->index()) {
176 run = &fOwner->run(cluster->runIndex());
177 runShift += clusterShift;
178 clusterShift = 0;
179 }
180 clusterShift += cluster->width();
181 for (auto i = cluster->startPos(); i < cluster->endPos(); ++i) {
182 auto posX = run->positionX(i);
183 auto posY = run->posY(i);
184 auto bounds = run->getBounds(i);
185 bounds.offset(posX + runShift, posY);
186 boundaries.joinPossiblyEmptyRect(bounds);
187 }
188 }
189
190 // We need to take in account all the shadows when we calculate the boundaries
191 // TODO: Need to find a better solution
192 if (fHasShadows) {
193 SkRect shadowRect = SkRect::MakeEmpty();
194 this->iterateThroughVisualRuns(false,
195 [this, &shadowRect, boundaries]
196 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
197 *runWidthInLine = this->iterateThroughSingleRunByStyles(
198 run, runOffsetInLine, textRange, StyleType::kShadow,
199 [&shadowRect, boundaries](TextRange textRange, const TextStyle& style, const ClipContext& context) {
200
201 for (TextShadow shadow : style.getShadows()) {
202 if (!shadow.hasShadow()) continue;
203 SkPaint paint;
204 paint.setColor(shadow.fColor);
205 if (shadow.fBlurRadius != 0.0) {
206 auto filter = SkMaskFilter::MakeBlur(
207 kNormal_SkBlurStyle,
208 SkDoubleToScalar(shadow.fBlurRadius),
209 false);
210 paint.setMaskFilter(filter);
211 SkRect bound;
212 paint.doComputeFastBounds(boundaries, &bound, SkPaint::Style::kFill_Style);
213 shadowRect.joinPossiblyEmptyRect(bound);
214 }
215 }
216 });
217 return true;
218 });
219 boundaries.fLeft += shadowRect.fLeft;
220 boundaries.fTop += shadowRect.fTop;
221 boundaries.fRight += shadowRect.fRight;
222 boundaries.fBottom += shadowRect.fBottom;
223 }
224
225 boundaries.offset(this->offset()); // Line offset from the beginning of the para
226 boundaries.offset(0, baseline); // Down by baseline
227
228 return boundaries;
229}
230
231void TextLine::paint(SkCanvas* textCanvas) {
232 if (this->empty()) {
233 return;
234 }
235
236 if (fHasBackground) {
237 this->iterateThroughVisualRuns(false,
238 [textCanvas, this]
239 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
240 *runWidthInLine = this->iterateThroughSingleRunByStyles(
241 run, runOffsetInLine, textRange, StyleType::kBackground,
242 [textCanvas, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
243 this->paintBackground(textCanvas, textRange, style, context);
244 });
245 return true;
246 });
247 }
248
249 if (fHasShadows) {
250 this->iterateThroughVisualRuns(false,
251 [textCanvas, this]
252 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
253 *runWidthInLine = this->iterateThroughSingleRunByStyles(
254 run, runOffsetInLine, textRange, StyleType::kShadow,
255 [textCanvas, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
256 this->paintShadow(textCanvas, textRange, style, context);
257 });
258 return true;
259 });
260 }
261
262 this->iterateThroughVisualRuns(false,
263 [textCanvas, this]
264 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
265 if (run->placeholderStyle() != nullptr) {
266 *runWidthInLine = run->advance().fX;
267 return true;
268 }
269 *runWidthInLine = this->iterateThroughSingleRunByStyles(
270 run, runOffsetInLine, textRange, StyleType::kForeground,
271 [textCanvas, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
272 this->paintText(textCanvas, textRange, style, context);
273 });
274 return true;
275 });
276
277 if (fHasDecorations) {
278 this->iterateThroughVisualRuns(false,
279 [textCanvas, this]
280 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
281 *runWidthInLine = this->iterateThroughSingleRunByStyles(
282 run, runOffsetInLine, textRange, StyleType::kDecorations,
283 [textCanvas, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
284 this->paintDecorations(textCanvas, textRange, style, context);
285 });
286 return true;
287 });
288 }
289}
290
291void TextLine::format(TextAlign align, SkScalar maxWidth) {
292 SkScalar delta = maxWidth - this->width();
293 if (delta <= 0) {
294 return;
295 }
296
297 // We do nothing for left align
298 if (align == TextAlign::kJustify) {
299 if (!this->endsWithHardLineBreak()) {
300 this->justify(maxWidth);
301 } else if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) {
302 // Justify -> Right align
303 fShift = delta;
304 }
305 } else if (align == TextAlign::kRight) {
306 fShift = delta;
307 } else if (align == TextAlign::kCenter) {
308 fShift = delta / 2;
309 }
310}
311
312void TextLine::scanStyles(StyleType styleType, const RunStyleVisitor& visitor) {
313 if (this->empty()) {
314 return;
315 }
316
317 this->iterateThroughVisualRuns(false,
318 [this, visitor, styleType](const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) {
319 *width = this->iterateThroughSingleRunByStyles(
320 run, runOffset, textRange, styleType,
321 [visitor](TextRange textRange, const TextStyle& style, const ClipContext& context) {
322 visitor(textRange, style, context);
323 });
324 return true;
325 });
326}
327
328SkRect TextLine::extendHeight(const ClipContext& context) const {
329 SkRect result = context.clip;
330 result.fBottom += std::max(this->fMaxRunMetrics.height() - this->height(), 0.0f);
331 return result;
332}
333
334SkScalar TextLine::metricsWithoutMultiplier(TextHeightBehavior correction) {
335
336 if (this->fSizes.getForceStrut()) {
337 return 0;
338 }
339
340 InternalLineMetrics result;
341 this->iterateThroughVisualRuns(true,
342 [&result](const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) {
343 InternalLineMetrics runMetrics(run->ascent(), run->descent(), run->leading());
344 result.add(runMetrics);
345 return true;
346 });
347 SkScalar delta = 0;
348 if (correction == TextHeightBehavior::kDisableFirstAscent) {
349 delta += (this->fSizes.fAscent - result.fAscent);
350 this->fSizes.fAscent -= delta;
351 this->fAscentStyle = LineMetricStyle::Typographic;
352 } else if (correction == TextHeightBehavior::kDisableLastDescent) {
353 delta -= (this->fSizes.fDescent - result.fDescent);
354 this->fSizes.fDescent -= delta;
355 this->fDescentStyle = LineMetricStyle::Typographic;
356 }
357 fAdvance.fY += delta;
358 return delta;
359}
360
361void TextLine::paintText(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
362
363 if (context.run->placeholderStyle() != nullptr) {
364 return;
365 }
366
367 SkPaint paint;
368 if (style.hasForeground()) {
369 paint = style.getForeground();
370 } else {
371 paint.setColor(style.getColor());
372 }
373
374 // TODO: This is the change for flutter, must be removed later
375 SkTextBlobBuilder builder;
376 context.run->copyTo(builder, SkToU32(context.pos), context.size);
377 if (context.clippingNeeded) {
378 canvas->save();
379 canvas->clipRect(extendHeight(context).makeOffset(this->offset()));
380 }
381
382 SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + 0.5);
383 canvas->drawTextBlob(builder.make(),
384 this->offset().fX + context.fTextShift, this->offset().fY + correctedBaseline, paint);
385
386 if (context.clippingNeeded) {
387 canvas->restore();
388 }
389}
390
391void TextLine::paintBackground(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
392 if (style.hasBackground()) {
393 canvas->drawRect(context.clip.makeOffset(this->offset()), style.getBackground());
394 }
395}
396
397void TextLine::paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
398 auto shiftDown = this->baseline();
399 for (TextShadow shadow : style.getShadows()) {
400 if (!shadow.hasShadow()) continue;
401
402 SkPaint paint;
403 paint.setColor(shadow.fColor);
404 if (shadow.fBlurRadius != 0.0) {
405 auto filter = SkMaskFilter::MakeBlur(kNormal_SkBlurStyle,
406 SkDoubleToScalar(shadow.fBlurRadius), false);
407 paint.setMaskFilter(filter);
408 }
409
410 SkTextBlobBuilder builder;
411 context.run->copyTo(builder, context.pos, context.size);
412
413 if (context.clippingNeeded) {
414 canvas->save();
415 SkRect clip = extendHeight(context);
416 clip.offset(this->offset());
417 canvas->clipRect(clip);
418 }
419 canvas->drawTextBlob(builder.make(),
420 this->offset().fX + shadow.fOffset.x() + context.fTextShift,
421 this->offset().fY + shadow.fOffset.y() + shiftDown,
422 paint);
423
424 if (context.clippingNeeded) {
425 canvas->restore();
426 }
427 }
428}
429
430void TextLine::paintDecorations(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
431
432 SkAutoCanvasRestore acr(canvas, true);
433 canvas->translate(this->offset().fX, this->offset().fY);
434 Decorations decorations;
435 SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + 0.5);
436 decorations.paint(canvas, style, context, correctedBaseline, this->offset());
437}
438
439void TextLine::justify(SkScalar maxWidth) {
440 // Count words and the extra spaces to spread across the line
441 // TODO: do it at the line breaking?..
442 size_t whitespacePatches = 0;
443 SkScalar textLen = 0;
444 bool whitespacePatch = false;
445 this->iterateThroughClustersInGlyphsOrder(false, false,
446 [&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster, bool ghost) {
447 if (cluster->isWhitespaces()) {
448 if (!whitespacePatch) {
449 whitespacePatch = true;
450 ++whitespacePatches;
451 }
452 } else {
453 whitespacePatch = false;
454 }
455 textLen += cluster->width();
456 return true;
457 });
458
459 if (whitespacePatches == 0) {
460 return;
461 }
462
463 SkScalar step = (maxWidth - textLen) / whitespacePatches;
464 SkScalar shift = 0;
465
466 // Deal with the ghost spaces
467 auto ghostShift = maxWidth - this->fAdvance.fX;
468 // Spread the extra whitespaces
469 whitespacePatch = false;
470 this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, bool ghost) {
471
472 if (ghost) {
473 if (cluster->run()->leftToRight()) {
474 shiftCluster(cluster, ghostShift, ghostShift);
475 }
476 return true;
477 }
478
479 auto prevShift = shift;
480 if (cluster->isWhitespaces()) {
481 if (!whitespacePatch) {
482 shift += step;
483 whitespacePatch = true;
484 --whitespacePatches;
485 }
486 } else {
487 whitespacePatch = false;
488 }
489 shiftCluster(cluster, shift, prevShift);
490 return true;
491 });
492
493 SkAssertResult(nearlyEqual(shift, maxWidth - textLen));
494 SkASSERT(whitespacePatches == 0);
495
496 this->fWidthWithSpaces += ghostShift;
497 this->fAdvance.fX = maxWidth;
498}
499
500void TextLine::shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift) {
501
502 auto run = cluster->run();
503 auto start = cluster->startPos();
504 auto end = cluster->endPos();
505
506 if (end == run->size()) {
507 // Set the same shift for the fake last glyph (to avoid all extra checks)
508 ++end;
509 }
510
511 if (run->fJustificationShifts.empty()) {
512 // Do not fill this array until needed
513 run->fJustificationShifts.push_back_n(run->size() + 1, { 0, 0 });
514 }
515
516 for (size_t pos = start; pos < end; ++pos) {
517 run->fJustificationShifts[pos] = { shift, prevShift };
518 }
519}
520
521void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) {
522 // Replace some clusters with the ellipsis
523 // Go through the clusters in the reverse logical order
524 // taking off cluster by cluster until the ellipsis fits
525 SkScalar width = fAdvance.fX;
526
527 auto attachEllipsis = [&](const Cluster* cluster){
528 // Shape the ellipsis
529 std::unique_ptr<Run> run = shapeEllipsis(ellipsis, cluster->run());
530 run->fClusterStart = cluster->textRange().start;
531 run->setOwner(fOwner);
532
533 // See if it fits
534 if (width + run->advance().fX > maxWidth) {
535 width -= cluster->width();
536 // Continue if it's not
537 return false;
538 }
539
540 fEllipsis = std::move(run);
541 fEllipsis->shift(width, 0);
542 fAdvance.fX = width;
543 return true;
544 };
545
546 iterateThroughClustersInGlyphsOrder(
547 true, false, [&](const Cluster* cluster, bool ghost) {
548 return !attachEllipsis(cluster);
549 });
550
551 if (!fEllipsis) {
552 // Weird situation: just the ellipsis on the line (if it fits)
553 attachEllipsis(&fOwner->cluster(clusters().start));
554 }
555}
556
557std::unique_ptr<Run> TextLine::shapeEllipsis(const SkString& ellipsis, Run* run) {
558
559 class ShapeHandler final : public SkShaper::RunHandler {
560 public:
561 ShapeHandler(SkScalar lineHeight, const SkString& ellipsis)
562 : fRun(nullptr), fLineHeight(lineHeight), fEllipsis(ellipsis) {}
563 Run* run() & { return fRun.get(); }
564 std::unique_ptr<Run> run() && { return std::move(fRun); }
565
566 private:
567 void beginLine() override {}
568
569 void runInfo(const RunInfo&) override {}
570
571 void commitRunInfo() override {}
572
573 Buffer runBuffer(const RunInfo& info) override {
574 SkASSERT(!fRun);
575 fRun = std::make_unique<Run>(nullptr, info, 0, fLineHeight, 0, 0);
576 return fRun->newRunBuffer();
577 }
578
579 void commitRunBuffer(const RunInfo& info) override {
580 fRun->fAdvance.fX = info.fAdvance.fX;
581 fRun->fAdvance.fY = fRun->advance().fY;
582 fRun->fPlaceholderIndex = std::numeric_limits<size_t>::max();
583 fRun->fEllipsis = true;
584 }
585
586 void commitLine() override {}
587
588 std::unique_ptr<Run> fRun;
589 SkScalar fLineHeight;
590 SkString fEllipsis;
591 };
592
593 ShapeHandler handler(run->heightMultiplier(), ellipsis);
594 std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder();
595 SkASSERT_RELEASE(shaper != nullptr);
596 shaper->shape(ellipsis.c_str(), ellipsis.size(), run->font(), true,
597 std::numeric_limits<SkScalar>::max(), &handler);
598 handler.run()->fTextRange = TextRange(0, ellipsis.size());
599 handler.run()->fOwner = fOwner;
600 return std::move(handler).run();
601}
602
603TextLine::ClipContext TextLine::measureTextInsideOneRun(TextRange textRange,
604 const Run* run,
605 SkScalar runOffsetInLine,
606 SkScalar textOffsetInRunInLine,
607 bool includeGhostSpaces,
608 bool limitToClusters) const {
609 ClipContext result = { run, 0, run->size(), 0, SkRect::MakeEmpty(), false };
610
611 if (run->fEllipsis) {
612 // Both ellipsis and placeholders can only be measured as one glyph
613 SkASSERT(textRange == run->textRange());
614 result.fTextShift = runOffsetInLine;
615 result.clip = SkRect::MakeXYWH(runOffsetInLine,
616 sizes().runTop(run, this->fAscentStyle),
617 run->advance().fX,
618 run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
619 return result;
620 } else if (run->isPlaceholder()) {
621 if (SkScalarIsFinite(run->fFontMetrics.fAscent)) {
622 result.clip = SkRect::MakeXYWH(runOffsetInLine,
623 sizes().runTop(run, this->fAscentStyle),
624 run->advance().fX,
625 run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
626 } else {
627 result.clip = SkRect::MakeXYWH(runOffsetInLine, run->fFontMetrics.fAscent, run->advance().fX, 0);
628 }
629 return result;
630 }
631 // Find [start:end] clusters for the text
632 bool found;
633 ClusterIndex startIndex;
634 ClusterIndex endIndex;
635 std::tie(found, startIndex, endIndex) = run->findLimitingClusters(textRange);
636 if (!found) {
637 SkASSERT(textRange.empty() || limitToClusters);
638 return result;
639 }
640
641 auto start = &fOwner->cluster(startIndex);
642 auto end = &fOwner->cluster(endIndex);
643 result.pos = start->startPos();
644 result.size = (end->isHardBreak() ? end->startPos() : end->endPos()) - start->startPos();
645
646 auto textStartInRun = run->positionX(start->startPos());
647 auto textStartInLine = runOffsetInLine + textOffsetInRunInLine;
648/*
649 if (!run->fJustificationShifts.empty()) {
650 SkDebugf("Justification for [%d:%d)\n", textRange.start, textRange.end);
651 for (auto i = result.pos; i < result.pos + result.size; ++i) {
652 auto j = run->fJustificationShifts[i];
653 SkDebugf("[%d] = %f %f\n", i, j.fX, j.fY);
654 }
655 }
656*/
657 // Calculate the clipping rectangle for the text with cluster edges
658 // There are 2 cases:
659 // EOL (when we expect the last cluster clipped without any spaces)
660 // Anything else (when we want the cluster width contain all the spaces -
661 // coming from letter spacing or word spacing or justification)
662 result.clip =
663 SkRect::MakeXYWH(0,
664 sizes().runTop(run, this->fAscentStyle),
665 run->calculateWidth(result.pos, result.pos + result.size, false),
666 run->calculateHeight(this->fAscentStyle,this->fDescentStyle));
667
668 // Correct the width in case the text edges don't match clusters
669 // TODO: This is where we get smart about selecting a part of a cluster
670 // by shaping each grapheme separately and then use the result sizes
671 // to calculate the proportions
672 auto leftCorrection = start->sizeToChar(textRange.start);
673 auto rightCorrection = end->sizeFromChar(textRange.end - 1);
674 result.clip.fLeft += leftCorrection;
675 result.clip.fRight -= rightCorrection;
676 result.clippingNeeded = leftCorrection != 0 || rightCorrection != 0;
677
678 textStartInLine -= leftCorrection;
679 result.clip.offset(textStartInLine, 0);
680
681 if (compareRound(result.clip.fRight, fAdvance.fX) > 0 && !includeGhostSpaces) {
682 // There are few cases when we need it.
683 // The most important one: we measure the text with spaces at the end
684 // and we should ignore these spaces
685 result.clippingNeeded = true;
686 result.clip.fRight = fAdvance.fX;
687 }
688
689 if (result.clip.width() < 0) {
690 // Weird situation when glyph offsets move the glyph to the left
691 // (happens with zalgo texts, for instance)
692 result.clip.fRight = result.clip.fLeft;
693 }
694
695 // The text must be aligned with the lineOffset
696 result.fTextShift = textStartInLine - textStartInRun;
697
698 return result;
699}
700
701void TextLine::iterateThroughClustersInGlyphsOrder(bool reversed,
702 bool includeGhosts,
703 const ClustersVisitor& visitor) const {
704 // Walk through the clusters in the logical order (or reverse)
705 SkSpan<const size_t> runs(fRunsInVisualOrder.data(), fRunsInVisualOrder.size());
706 bool ignore = false;
707 directional_for_each(runs, !reversed, [&](decltype(runs[0]) r) {
708 if (ignore) return;
709 auto run = this->fOwner->run(r);
710 auto trimmedRange = fClusterRange.intersection(run.clusterRange());
711 auto trailedRange = fGhostClusterRange.intersection(run.clusterRange());
712 SkASSERT(trimmedRange.start == trailedRange.start);
713
714 auto trailed = fOwner->clusters(trailedRange);
715 auto trimmed = fOwner->clusters(trimmedRange);
716 directional_for_each(trailed, reversed != run.leftToRight(), [&](Cluster& cluster) {
717 if (ignore) return;
718 bool ghost = &cluster >= trimmed.end();
719 if (!includeGhosts && ghost) {
720 return;
721 }
722 if (!visitor(&cluster, ghost)) {
723 ignore = true;
724 return;
725 }
726 });
727 });
728}
729
730SkScalar TextLine::iterateThroughSingleRunByStyles(const Run* run,
731 SkScalar runOffset,
732 TextRange textRange,
733 StyleType styleType,
734 const RunStyleVisitor& visitor) const {
735
736 if (run->fEllipsis) {
737 // Extra efforts to get the ellipsis text style
738 ClipContext clipContext = this->measureTextInsideOneRun(run->textRange(), run, runOffset,
739 0, false, false);
740 TextRange testRange(run->fClusterStart, run->fClusterStart + 1);
741 for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) {
742 auto block = fOwner->styles().begin() + index;
743 auto intersect = intersected(block->fRange, testRange);
744 if (intersect.width() > 0) {
745 visitor(textRange, block->fStyle, clipContext);
746 return run->advance().fX;
747 }
748 }
749 SkASSERT(false);
750 }
751
752 if (styleType == StyleType::kNone) {
753 ClipContext clipContext = this->measureTextInsideOneRun(textRange, run, runOffset,
754 0, false, false);
755 if (clipContext.clip.height() > 0) {
756 visitor(textRange, TextStyle(), clipContext);
757 return clipContext.clip.width();
758 } else {
759 return 0;
760 }
761 }
762
763 TextIndex start = EMPTY_INDEX;
764 size_t size = 0;
765 const TextStyle* prevStyle = nullptr;
766 SkScalar textOffsetInRun = 0;
767 for (BlockIndex index = fBlockRange.start; index <= fBlockRange.end; ++index) {
768
769 TextRange intersect;
770 TextStyle* style = nullptr;
771 if (index < fBlockRange.end) {
772 auto block = fOwner->styles().begin() + index;
773
774 // Get the text
775 intersect = intersected(block->fRange, textRange);
776 if (intersect.width() == 0) {
777 if (start == EMPTY_INDEX) {
778 // This style is not applicable to the text yet
779 continue;
780 } else {
781 // We have found all the good styles already
782 // but we need to process the last one of them
783 intersect = TextRange(start, start + size);
784 index = fBlockRange.end;
785 }
786 } else {
787 // Get the style
788 style = &block->fStyle;
789 if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) {
790 size += intersect.width();
791 continue;
792 } else if (start == EMPTY_INDEX ) {
793 // First time only
794 prevStyle = style;
795 size = intersect.width();
796 start = intersect.start;
797 continue;
798 }
799 }
800 } else if (prevStyle != nullptr) {
801 // This is the last style
802 } else {
803 break;
804 }
805
806 // We have the style and the text
807 auto runStyleTextRange = TextRange(start, start + size);
808 // Measure the text
809 ClipContext clipContext = this->measureTextInsideOneRun(runStyleTextRange, run, runOffset,
810 textOffsetInRun, false, false);
811 if (clipContext.clip.height() == 0) {
812 continue;
813 }
814 visitor(runStyleTextRange, *prevStyle, clipContext);
815 textOffsetInRun += clipContext.clip.width();
816
817 // Start all over again
818 prevStyle = style;
819 start = intersect.start;
820 size = intersect.width();
821 }
822 return textOffsetInRun;
823}
824
825void TextLine::iterateThroughVisualRuns(bool includingGhostSpaces, const RunVisitor& visitor) const {
826
827 // Walk through all the runs that intersect with the line in visual order
828 SkScalar width = 0;
829 SkScalar runOffset = 0;
830 SkScalar totalWidth = 0;
831 auto textRange = includingGhostSpaces ? this->textWithSpaces() : this->trimmedText();
832 for (auto& runIndex : fRunsInVisualOrder) {
833
834 const auto run = &this->fOwner->run(runIndex);
835 auto lineIntersection = intersected(run->textRange(), textRange);
836 if (lineIntersection.width() == 0 && this->width() != 0) {
837 // TODO: deal with empty runs in a better way
838 continue;
839 }
840 if (!run->leftToRight() && runOffset == 0 && includingGhostSpaces) {
841 // runOffset does not take in account a possibility
842 // that RTL run could start before the line (trailing spaces)
843 // so we need to do runOffset -= "trailing whitespaces length"
844 TextRange whitespaces = intersected(
845 TextRange(fTextRange.end, fTextWithWhitespacesRange.end), run->fTextRange);
846 if (whitespaces.width() > 0) {
847 auto whitespacesLen = measureTextInsideOneRun(whitespaces, run, runOffset, 0, true, false).clip.width();
848 runOffset -= whitespacesLen;
849 }
850 }
851 runOffset += width;
852 totalWidth += width;
853 if (!visitor(run, runOffset, lineIntersection, &width)) {
854 return;
855 }
856 }
857
858 runOffset += width;
859 totalWidth += width;
860
861 if (this->ellipsis() != nullptr) {
862 if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) {
863 totalWidth += width;
864 }
865 }
866
867 // This is a very important assert!
868 // It asserts that 2 different ways of calculation come with the same results
869 if (!includingGhostSpaces && compareRound(totalWidth, this->width()) != 0) {
870 SkDebugf("ASSERT: %f != %f\n", totalWidth, this->width());
871 SkASSERT(false);
872 }
873}
874
875SkVector TextLine::offset() const {
876 return fOffset + SkVector::Make(fShift, 0);
877}
878
879LineMetrics TextLine::getMetrics() const {
880 LineMetrics result;
881
882 // Fill out the metrics
883 result.fStartIndex = fTextRange.start;
884 result.fEndIndex = fTextWithWhitespacesRange.end;
885 result.fEndExcludingWhitespaces = fTextRange.end;
886 result.fEndIncludingNewline = fTextWithWhitespacesRange.end; // TODO: implement
887 result.fHardBreak = endsWithHardLineBreak();
888 result.fAscent = - fMaxRunMetrics.ascent();
889 result.fDescent = fMaxRunMetrics.descent();
890 result.fUnscaledAscent = - fMaxRunMetrics.ascent(); // TODO: implement
891 result.fHeight = littleRound(fAdvance.fY);
892 result.fWidth = littleRound(fAdvance.fX);
893 result.fLeft = this->offset().fX;
894 // This is Flutter definition of a baseline
895 result.fBaseline = this->offset().fY + this->height() - this->sizes().descent();
896 result.fLineNumber = this - fOwner->lines().begin();
897
898 // Fill out the style parts
899 this->iterateThroughVisualRuns(false,
900 [this, &result]
901 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
902 if (run->placeholderStyle() != nullptr) {
903 *runWidthInLine = run->advance().fX;
904 return true;
905 }
906 *runWidthInLine = this->iterateThroughSingleRunByStyles(
907 run, runOffsetInLine, textRange, StyleType::kForeground,
908 [&result, &run](TextRange textRange, const TextStyle& style, const ClipContext& context) {
909 SkFontMetrics fontMetrics;
910 run->fFont.getMetrics(&fontMetrics);
911 StyleMetrics styleMetrics(&style, fontMetrics);
912 result.fLineMetrics.emplace(textRange.start, styleMetrics);
913 });
914 return true;
915 });
916
917 return result;
918}
919
920bool TextLine::isFirstLine() {
921 return this == &fOwner->lines().front();
922}
923
924bool TextLine::isLastLine() {
925 return this == &fOwner->lines().back();
926}
927
928bool TextLine::endsWithHardLineBreak() const {
929 // TODO: For some reason Flutter imagines a hard line break at the end of the last line.
930 // To be removed...
931 return fOwner->cluster(fGhostClusterRange.end - 1).isHardBreak() ||
932 fEllipsis != nullptr ||
933 fGhostClusterRange.end == fOwner->clusters().size() - 1;
934}
935
936void TextLine::getRectsForRange(TextRange textRange0,
937 RectHeightStyle rectHeightStyle,
938 RectWidthStyle rectWidthStyle,
939 std::vector<TextBox>& boxes)
940{
941 const Run* lastRun = nullptr;
942 auto startBox = boxes.size();
943 this->iterateThroughVisualRuns(true,
944 [textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
945 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
946 *runWidthInLine = this->iterateThroughSingleRunByStyles(
947 run, runOffsetInLine, textRange, StyleType::kNone,
948 [run, runOffsetInLine, textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
949 (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& lineContext) {
950
951 auto intersect = textRange * textRange0;
952 if (intersect.empty()) {
953 return true;
954 }
955
956 auto paragraphStyle = fOwner->paragraphStyle();
957
958 // Found a run that intersects with the text
959 auto context = this->measureTextInsideOneRun(intersect, run, runOffsetInLine, 0, true, true);
960 SkRect clip = context.clip;
961 clip.offset(lineContext.fTextShift - context.fTextShift, 0);
962
963 switch (rectHeightStyle) {
964 case RectHeightStyle::kMax:
965 // TODO: Change it once flutter rolls into google3
966 // (probably will break things if changed before)
967 clip.fBottom = this->height();
968 clip.fTop = this->sizes().delta();
969 break;
970 case RectHeightStyle::kIncludeLineSpacingTop: {
971 if (isFirstLine()) {
972 auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic);
973 clip.fTop += verticalShift;
974 }
975 break;
976 }
977 case RectHeightStyle::kIncludeLineSpacingMiddle: {
978 auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic);
979 clip.fTop += isFirstLine() ? verticalShift : verticalShift / 2;
980 clip.fBottom += isLastLine() ? 0 : verticalShift / 2;
981 break;
982 }
983 case RectHeightStyle::kIncludeLineSpacingBottom: {
984 auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic);
985 clip.offset(0, verticalShift);
986 if (isLastLine()) {
987 clip.fBottom -= verticalShift;
988 }
989 break;
990 }
991 case RectHeightStyle::kStrut: {
992 const auto& strutStyle = paragraphStyle.getStrutStyle();
993 if (strutStyle.getStrutEnabled()
994 && strutStyle.getFontSize() > 0) {
995 auto strutMetrics = fOwner->strutMetrics();
996 auto top = this->baseline();
997 clip.fTop = top + strutMetrics.ascent();
998 clip.fBottom = top + strutMetrics.descent();
999 }
1000 }
1001 break;
1002 case RectHeightStyle::kTight: {
1003 if (run->fHeightMultiplier > 0) {
1004 // This is a special case when we do not need to take in account this height multiplier
1005 auto correctedHeight = clip.height() / run->fHeightMultiplier;
1006 auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic);
1007 clip.fTop += verticalShift;
1008 clip.fBottom = clip.fTop + correctedHeight;
1009 }
1010 }
1011 break;
1012 default:
1013 SkASSERT(false);
1014 break;
1015 }
1016
1017 // Separate trailing spaces and move them in the default order of the paragraph
1018 // in case the run order and the paragraph order don't match
1019 SkRect trailingSpaces = SkRect::MakeEmpty();
1020 if (this->trimmedText().end < this->textWithSpaces().end && // Line has trailing spaces
1021 this->textWithSpaces().end == intersect.end && // Range is at the end of the line
1022 this->trimmedText().end > intersect.start) // Range has more than just spaces
1023 {
1024 auto delta = this->spacesWidth();
1025 trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0);
1026 // There are trailing spaces in this run
1027 if (paragraphStyle.getTextAlign() == TextAlign::kJustify && isLastLine())
1028 {
1029 // TODO: this is just a patch. Make it right later (when it's clear what and how)
1030 trailingSpaces = clip;
1031 if(run->leftToRight()) {
1032 trailingSpaces.fLeft = this->width();
1033 clip.fRight = this->width();
1034 } else {
1035 trailingSpaces.fRight = 0;
1036 clip.fLeft = 0;
1037 }
1038 } else if (paragraphStyle.getTextDirection() == TextDirection::kRtl &&
1039 !run->leftToRight())
1040 {
1041 // Split
1042 trailingSpaces = clip;
1043 trailingSpaces.fLeft = - delta;
1044 trailingSpaces.fRight = 0;
1045 clip.fLeft += delta;
1046 } else if (paragraphStyle.getTextDirection() == TextDirection::kLtr &&
1047 run->leftToRight())
1048 {
1049 // Split
1050 trailingSpaces = clip;
1051 trailingSpaces.fLeft = this->width();
1052 trailingSpaces.fRight = trailingSpaces.fLeft + delta;
1053 clip.fRight -= delta;
1054 }
1055 }
1056
1057 clip.offset(this->offset());
1058 if (trailingSpaces.width() > 0) {
1059 trailingSpaces.offset(this->offset());
1060 }
1061
1062 // Check if we can merge two boxes instead of adding a new one
1063 auto merge = [&lastRun, &context, &boxes](SkRect clip) {
1064 bool mergedBoxes = false;
1065 if (!boxes.empty() &&
1066 lastRun != nullptr &&
1067 lastRun->placeholderStyle() == nullptr &&
1068 context.run->placeholderStyle() == nullptr &&
1069 nearlyEqual(lastRun->heightMultiplier(),
1070 context.run->heightMultiplier()) &&
1071 lastRun->font() == context.run->font())
1072 {
1073 auto& lastBox = boxes.back();
1074 if (nearlyEqual(lastBox.rect.fTop, clip.fTop) &&
1075 nearlyEqual(lastBox.rect.fBottom, clip.fBottom) &&
1076 (nearlyEqual(lastBox.rect.fLeft, clip.fRight) ||
1077 nearlyEqual(lastBox.rect.fRight, clip.fLeft)))
1078 {
1079 lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft);
1080 lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight);
1081 mergedBoxes = true;
1082 }
1083 }
1084 lastRun = context.run;
1085 return mergedBoxes;
1086 };
1087
1088 if (!merge(clip)) {
1089 boxes.emplace_back(clip, context.run->getTextDirection());
1090 }
1091 if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) {
1092 boxes.emplace_back(trailingSpaces, paragraphStyle.getTextDirection());
1093 }
1094
1095 if (rectWidthStyle == RectWidthStyle::kMax && !isLastLine()) {
1096 // Align the very left/right box horizontally
1097 auto lineStart = this->offset().fX;
1098 auto lineEnd = this->offset().fX + this->width();
1099 auto left = boxes[startBox];
1100 auto right = boxes.back();
1101 if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) {
1102 left.rect.fRight = left.rect.fLeft;
1103 left.rect.fLeft = 0;
1104 boxes.insert(boxes.begin() + startBox + 1, left);
1105 }
1106 if (right.direction == TextDirection::kLtr &&
1107 right.rect.fRight >= lineEnd &&
1108 right.rect.fRight < fOwner->widthWithTrailingSpaces()) {
1109 right.rect.fLeft = right.rect.fRight;
1110 right.rect.fRight = fOwner->widthWithTrailingSpaces();
1111 boxes.emplace_back(right);
1112 }
1113 }
1114
1115 return true;
1116 });
1117 return true;
1118 });
1119 for (auto& r : boxes) {
1120 r.rect.fLeft = littleRound(r.rect.fLeft);
1121 r.rect.fRight = littleRound(r.rect.fRight);
1122 r.rect.fTop = littleRound(r.rect.fTop);
1123 r.rect.fBottom = littleRound(r.rect.fBottom);
1124 }
1125}
1126
1127PositionWithAffinity TextLine::getGlyphPositionAtCoordinate(SkScalar dx) {
1128
1129 PositionWithAffinity result(0, Affinity::kDownstream);
1130 this->iterateThroughVisualRuns(true,
1131 [this, dx, &result]
1132 (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
1133 bool keepLooking = true;
1134 *runWidthInLine = this->iterateThroughSingleRunByStyles(
1135 run, runOffsetInLine, textRange, StyleType::kNone,
1136 [this, dx, &result, &keepLooking]
1137 (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context) {
1138
1139 SkScalar offsetX = this->offset().fX;
1140 if (dx < context.clip.fLeft + offsetX) {
1141 // All the other runs are placed right of this one
1142 auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos));
1143 result = { SkToS32(utf16Index), kDownstream };
1144 return keepLooking = false;
1145 }
1146
1147 if (dx >= context.clip.fRight + offsetX) {
1148 // We have to keep looking ; just in case keep the last one as the closest
1149 auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos + context.size));
1150 result = { SkToS32(utf16Index), kUpstream };
1151 return keepLooking = true;
1152 }
1153
1154 // So we found the run that contains our coordinates
1155 // Find the glyph position in the run that is the closest left of our point
1156 // TODO: binary search
1157 size_t found = context.pos;
1158 for (size_t index = context.pos; index < context.pos + context.size; ++index) {
1159 // TODO: this rounding is done to match Flutter tests. Must be removed..
1160 auto end = littleRound(context.run->positionX(index) + context.fTextShift + offsetX);
1161 if (end > dx) {
1162 break;
1163 }
1164 found = index;
1165 }
1166
1167 SkScalar glyphemePosLeft = context.run->positionX(found) + context.fTextShift + offsetX;
1168 SkScalar glyphemePosWidth = context.run->positionX(found + 1) - context.run->positionX(found);
1169
1170 // Find the grapheme range that contains the point
1171 auto clusterIndex8 = context.run->globalClusterIndex(found);
1172 auto clusterEnd8 = context.run->globalClusterIndex(found + 1);
1173 TextIndex graphemeUtf8Start = fOwner->findGraphemeStart(clusterIndex8);
1174 TextIndex graphemeUtf8Width = fOwner->findGraphemeStart(clusterEnd8) - graphemeUtf8Start;
1175 size_t utf16Index = fOwner->getUTF16Index(clusterIndex8);
1176
1177 SkScalar center = glyphemePosLeft + glyphemePosWidth / 2;
1178 bool insideGlypheme = false;
1179 if (graphemeUtf8Width > 1) {
1180 // TODO: the average width of a code unit (especially UTF-8) is meaningless.
1181 // Probably want the average width of a grapheme or codepoint?
1182 SkScalar averageUtf8Width = glyphemePosWidth / graphemeUtf8Width;
1183 SkScalar delta = dx - glyphemePosLeft;
1184 int insideUtf8Offset = SkScalarNearlyZero(averageUtf8Width)
1185 ? 0
1186 : SkScalarFloorToInt(delta / averageUtf8Width);
1187 insideGlypheme = averageUtf8Width < delta && delta < glyphemePosWidth - averageUtf8Width;
1188 center = glyphemePosLeft + averageUtf8Width * insideUtf8Offset + averageUtf8Width / 2;
1189 utf16Index += insideUtf8Offset; // TODO: adding a utf8 offset to a utf16 index
1190 }
1191 if ((dx < center) == context.run->leftToRight() || insideGlypheme) {
1192 result = { SkToS32(utf16Index), kDownstream };
1193 } else {
1194 result = { SkToS32(utf16Index + 1), kUpstream };
1195 }
1196
1197 return keepLooking = false;
1198
1199 });
1200 return keepLooking;
1201 }
1202 );
1203 return result;
1204}
1205
1206void TextLine::getRectsForPlaceholders(std::vector<TextBox>& boxes) {
1207 this->iterateThroughVisualRuns(
1208 true,
1209 [&boxes, this](const Run* run, SkScalar runOffset, TextRange textRange,
1210 SkScalar* width) {
1211 auto context = this->measureTextInsideOneRun(textRange, run, runOffset, 0, true, false);
1212 *width = context.clip.width();
1213
1214 if (textRange.width() == 0) {
1215 return true;
1216 }
1217 if (!run->isPlaceholder()) {
1218 return true;
1219 }
1220
1221 SkRect clip = context.clip;
1222 clip.offset(this->offset());
1223
1224 clip.fLeft = littleRound(clip.fLeft);
1225 clip.fRight = littleRound(clip.fRight);
1226 clip.fTop = littleRound(clip.fTop);
1227 clip.fBottom = littleRound(clip.fBottom);
1228 boxes.emplace_back(clip, run->getTextDirection());
1229 return true;
1230 });
1231}
1232} // namespace textlayout
1233} // namespace skia
1234