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