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