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