blob: c389fcbee36369617370dba62716027b97a5cbe8 [file] [log] [blame]
Julia Lavrovaa3552c52019-05-30 16:12:56 -04001// Copyright 2019 Google LLC.
2#include "modules/skparagraph/src/TextLine.h"
3#include <unicode/brkiter.h>
4#include <unicode/ubidi.h>
5#include "modules/skparagraph/src/ParagraphImpl.h"
6
7#include "include/core/SkMaskFilter.h"
8#include "include/effects/SkDashPathEffect.h"
9#include "include/effects/SkDiscretePathEffect.h"
10#include "src/core/SkMakeUnique.h"
11
12namespace {
13
14SkSpan<const char> intersected(const SkSpan<const char>& a, const SkSpan<const char>& b) {
15 auto begin = SkTMax(a.begin(), b.begin());
16 auto end = SkTMin(a.end(), b.end());
17 return SkSpan<const char>(begin, end > begin ? end - begin : 0);
18}
19
20int32_t intersectedSize(SkSpan<const char> a, SkSpan<const char> b) {
21 if (a.begin() == nullptr || b.begin() == nullptr) {
22 return -1;
23 }
24 auto begin = SkTMax(a.begin(), b.begin());
25 auto end = SkTMin(a.end(), b.end());
26 return SkToS32(end - begin);
27}
28} // namespace
29
30namespace skia {
31namespace textlayout {
32
33SkTHashMap<SkFont, Run> TextLine::fEllipsisCache;
34
35TextLine::TextLine(SkVector offset, SkVector advance, SkSpan<const TextBlock> blocks,
36 SkSpan<const char> text, SkSpan<const char> textWithSpaces,
37 SkSpan<const Cluster> clusters, size_t startPos, size_t endPos,
38 LineMetrics sizes)
39 : fBlocks(blocks)
40 , fText(text)
41 , fTextWithSpaces(textWithSpaces)
42 , fClusters(clusters)
43 //, fStartPos(startPos)
44 //, fEndPos(endPos)
45 , fLogical()
46 , fShift(0)
47 , fAdvance(advance)
48 , fOffset(offset)
49 , fEllipsis(nullptr)
50 , fSizes(sizes) {
51 TRACE_EVENT0("skia", TRACE_FUNC);
52 // Reorder visual runs
53 auto start = fClusters.begin();
54 auto end = fClusters.end() - 1;
55 size_t numRuns = end->run()->index() - start->run()->index() + 1;
56
57 // Get the logical order
58 std::vector<UBiDiLevel> runLevels;
59 for (auto run = start->run(); run <= end->run(); ++run) {
60 runLevels.emplace_back(run->fBidiLevel);
61 }
62
63 std::vector<int32_t> logicalOrder(numRuns);
64 ubidi_reorderVisual(runLevels.data(), SkToU32(numRuns), logicalOrder.data());
65
66 auto firstRun = start->run();
67 for (auto index : logicalOrder) {
68 fLogical.push_back(firstRun + index);
69 }
70
71 // TODO: use fStartPos and fEndPos really
72 // SkASSERT(fStartPos <= start->run()->size());
73 // SkASSERT(fEndPos <= end->run()->size());
74}
75
76TextLine::TextLine(TextLine&& other) {
77 TRACE_EVENT0("skia", TRACE_FUNC);
78 this->fBlocks = other.fBlocks;
79 this->fText = other.fText;
80 this->fTextWithSpaces = other.fTextWithSpaces;
81 this->fLogical.reset();
82 this->fLogical = std::move(other.fLogical);
83 this->fShift = other.fShift;
84 this->fAdvance = other.fAdvance;
85 this->fOffset = other.fOffset;
86 this->fEllipsis = std::move(other.fEllipsis);
87 this->fSizes = other.sizes();
88 this->fClusters = other.fClusters;
89}
90
91void TextLine::paint(SkCanvas* textCanvas) {
92 TRACE_EVENT0("skia", TRACE_FUNC);
93 if (this->empty()) {
94 return;
95 }
96
97 textCanvas->save();
98 textCanvas->translate(this->offset().fX, this->offset().fY);
99
100 this->iterateThroughStylesInTextOrder(
101 StyleType::kBackground,
102 [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
103 return this->paintBackground(textCanvas, text, style, offsetX);
104 });
105
106 this->iterateThroughStylesInTextOrder(
107 StyleType::kShadow,
108 [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
109 return this->paintShadow(textCanvas, text, style, offsetX);
110 });
111
112 this->iterateThroughStylesInTextOrder(
113 StyleType::kForeground,
114 [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
115 return this->paintText(textCanvas, text, style, offsetX);
116 });
117
118 this->iterateThroughStylesInTextOrder(
119 StyleType::kDecorations,
120 [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
121 return this->paintDecorations(textCanvas, text, style, offsetX);
122 });
123
124 textCanvas->restore();
125}
126
127void TextLine::format(TextAlign effectiveAlign, SkScalar maxWidth, bool notLastLine) {
128 TRACE_EVENT0("skia", TRACE_FUNC);
129 SkScalar delta = maxWidth - this->width();
130 if (delta <= 0) {
131 return;
132 }
133
134 if (effectiveAlign == TextAlign::kJustify && notLastLine) {
135 this->justify(maxWidth);
136 } else if (effectiveAlign == TextAlign::kRight) {
137 this->shiftTo(delta);
138 } else if (effectiveAlign == TextAlign::kCenter) {
139 this->shiftTo(delta / 2);
140 }
141}
142
143void TextLine::scanStyles(StyleType style, const StyleVisitor& visitor) {
144 if (this->empty()) {
145 return;
146 }
147
148 this->iterateThroughStylesInTextOrder(
149 style, [this, visitor](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
150 visitor(text, style, offsetX);
151 return this->iterateThroughRuns(
152 text, offsetX, false,
153 [](Run*, int32_t, size_t, SkRect, SkScalar, bool) { return true; });
154 });
155}
156
157void TextLine::scanRuns(const RunVisitor& visitor) {
158 this->iterateThroughRuns(
159 fText, 0, false,
160 [visitor](Run* run, int32_t pos, size_t size, SkRect clip, SkScalar sc, bool b) {
161 visitor(run, pos, size, clip, sc, b);
162 return true;
163 });
164}
165
166SkScalar TextLine::paintText(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
167 SkScalar offsetX) const {
168 TRACE_EVENT0("skia", TRACE_FUNC);
169 SkPaint paint;
170 if (style.hasForeground()) {
171 paint = style.getForeground();
172 } else {
173 paint.setColor(style.getColor());
174 }
175
176 auto shiftDown = this->baseline();
177 return this->iterateThroughRuns(
178 text, offsetX, false,
179 [paint, canvas, shiftDown](Run* run, int32_t pos, size_t size, SkRect clip,
180 SkScalar shift, bool clippingNeeded) {
181 SkTextBlobBuilder builder;
182 run->copyTo(builder, SkToU32(pos), size, SkVector::Make(0, shiftDown));
183 canvas->save();
184 if (clippingNeeded) {
185 canvas->clipRect(clip);
186 }
187 canvas->translate(shift, 0);
188 canvas->drawTextBlob(builder.make(), 0, 0, paint);
189 canvas->restore();
190 return true;
191 });
192}
193
194SkScalar TextLine::paintBackground(SkCanvas* canvas, SkSpan<const char> text,
195 const TextStyle& style, SkScalar offsetX) const {
196 TRACE_EVENT0("skia", TRACE_FUNC);
197 return this->iterateThroughRuns(text, offsetX, false,
198 [canvas, style](Run* run, int32_t pos, size_t size, SkRect clip,
199 SkScalar shift, bool clippingNeeded) {
200 if (style.hasBackground()) {
201 canvas->drawRect(clip, style.getBackground());
202 }
203 return true;
204 });
205}
206
207SkScalar TextLine::paintShadow(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
208 SkScalar offsetX) const {
209 TRACE_EVENT0("skia", TRACE_FUNC);
210 if (style.getShadowNumber() == 0) {
211 // Still need to calculate text advance
212 return iterateThroughRuns(
213 text, offsetX, false,
214 [](Run*, int32_t, size_t, SkRect, SkScalar, bool) { return true; });
215 }
216
217 SkScalar result = 0;
218 for (TextShadow shadow : style.getShadows()) {
219 if (!shadow.hasShadow()) continue;
220
221 SkPaint paint;
222 paint.setColor(shadow.fColor);
223 if (shadow.fBlurRadius != 0.0) {
224 auto filter = SkMaskFilter::MakeBlur(kNormal_SkBlurStyle,
225 SkDoubleToScalar(shadow.fBlurRadius), false);
226 paint.setMaskFilter(filter);
227 }
228
229 auto shiftDown = this->baseline();
230 result = this->iterateThroughRuns(
231 text, offsetX, false,
232 [canvas, shadow, paint, shiftDown](Run* run, size_t pos, size_t size, SkRect clip,
233 SkScalar shift, bool clippingNeeded) {
234 SkTextBlobBuilder builder;
235 run->copyTo(builder, pos, size, SkVector::Make(0, shiftDown));
236 canvas->save();
237 clip.offset(shadow.fOffset);
238 if (clippingNeeded) {
239 canvas->clipRect(clip);
240 }
241 canvas->translate(shift, 0);
242 canvas->drawTextBlob(builder.make(), shadow.fOffset.x(), shadow.fOffset.y(),
243 paint);
244 canvas->restore();
245 return true;
246 });
247 }
248
249 return result;
250}
251
252SkScalar TextLine::paintDecorations(SkCanvas* canvas, SkSpan<const char> text,
253 const TextStyle& style, SkScalar offsetX) const {
254 TRACE_EVENT0("skia", TRACE_FUNC);
255 return this->iterateThroughRuns(
256 text, offsetX, false,
257 [this, canvas, style](Run* run, int32_t pos, size_t size, SkRect clip, SkScalar shift,
258 bool clippingNeeded) {
259 if (style.getDecoration() == TextDecoration::kNoDecoration) {
260 return true;
261 }
262
263 for (auto decoration : AllTextDecorations) {
264 if (style.getDecoration() && decoration == 0) {
265 continue;
266 }
267
268 SkScalar thickness = style.getDecorationThicknessMultiplier();
269 //
270 SkScalar position = 0;
271 switch (style.getDecoration()) {
272 case TextDecoration::kUnderline:
273 position = -run->ascent() + thickness;
274 break;
275 case TextDecoration::kOverline:
276 position = 0;
277 break;
278 case TextDecoration::kLineThrough: {
279 position = (run->descent() - run->ascent() - thickness) / 2;
280 break;
281 }
282 default:
283 // TODO: can we actually get here?
284 break;
285 }
286
287 auto width = clip.width();
288 SkScalar x = clip.left();
289 SkScalar y = clip.top() + position;
290
291 // Decoration paint (for now) and/or path
292 SkPaint paint;
293 SkPath path;
294 this->computeDecorationPaint(paint, clip, style, path);
295 paint.setStrokeWidth(thickness);
296
297 switch (style.getDecorationStyle()) {
298 case TextDecorationStyle::kWavy:
299 path.offset(x, y);
300 canvas->drawPath(path, paint);
301 break;
302 case TextDecorationStyle::kDouble: {
303 canvas->drawLine(x, y, x + width, y, paint);
304 SkScalar bottom = y + thickness * 2;
305 canvas->drawLine(x, bottom, x + width, bottom, paint);
306 break;
307 }
308 case TextDecorationStyle::kDashed:
309 case TextDecorationStyle::kDotted:
310 case TextDecorationStyle::kSolid:
311 canvas->drawLine(x, y, x + width, y, paint);
312 break;
313 default:
314 break;
315 }
316 }
317
318 return true;
319 });
320}
321
322void TextLine::computeDecorationPaint(SkPaint& paint,
323 SkRect clip,
324 const TextStyle& style,
325 SkPath& path) const {
326 paint.setStyle(SkPaint::kStroke_Style);
327 if (style.getDecorationColor() == SK_ColorTRANSPARENT) {
328 paint.setColor(style.getColor());
329 } else {
330 paint.setColor(style.getDecorationColor());
331 }
332
333 SkScalar scaleFactor = style.getFontSize() / 14.f;
334
335 switch (style.getDecorationStyle()) {
336 case TextDecorationStyle::kSolid:
337 break;
338
339 case TextDecorationStyle::kDouble:
340 break;
341
342 // Note: the intervals are scaled by the thickness of the line, so it is
343 // possible to change spacing by changing the decoration_thickness
344 // property of TextStyle.
345 case TextDecorationStyle::kDotted: {
346 const SkScalar intervals[] = {1.0f * scaleFactor, 1.5f * scaleFactor,
347 1.0f * scaleFactor, 1.5f * scaleFactor};
348 size_t count = sizeof(intervals) / sizeof(intervals[0]);
349 paint.setPathEffect(SkPathEffect::MakeCompose(
350 SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
351 SkDiscretePathEffect::Make(0, 0)));
352 break;
353 }
354 // Note: the intervals are scaled by the thickness of the line, so it is
355 // possible to change spacing by changing the decoration_thickness
356 // property of TextStyle.
357 case TextDecorationStyle::kDashed: {
358 const SkScalar intervals[] = {4.0f * scaleFactor, 2.0f * scaleFactor,
359 4.0f * scaleFactor, 2.0f * scaleFactor};
360 size_t count = sizeof(intervals) / sizeof(intervals[0]);
361 paint.setPathEffect(SkPathEffect::MakeCompose(
362 SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
363 SkDiscretePathEffect::Make(0, 0)));
364 break;
365 }
366 case TextDecorationStyle::kWavy: {
367 int wave_count = 0;
368 SkScalar x_start = 0;
369 SkScalar wavelength = scaleFactor * style.getDecorationThicknessMultiplier();
370 auto width = clip.width();
371 path.moveTo(0, 0);
372 while (x_start + wavelength * 2 < width) {
373 path.rQuadTo(wavelength,
374 wave_count % 2 != 0 ? wavelength : -wavelength,
375 wavelength * 2,
376 0);
377 x_start += wavelength * 2;
378 ++wave_count;
379 }
380 break;
381 }
382 }
383}
384
385void TextLine::justify(SkScalar maxWidth) {
386 TRACE_EVENT0("skia", TRACE_FUNC);
387 // Count words and the extra spaces to spread across the line
388 // TODO: do it at the line breaking?..
389 size_t whitespacePatches = 0;
390 SkScalar textLen = 0;
391 bool whitespacePatch = false;
392 this->iterateThroughClustersInGlyphsOrder(
393 false, [&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster) {
394 if (cluster->isWhitespaces()) {
395 if (!whitespacePatch) {
396 whitespacePatch = true;
397 ++whitespacePatches;
398 }
399 } else {
400 whitespacePatch = false;
401 }
402 textLen += cluster->width();
403 return true;
404 });
405
406 if (whitespacePatches == 0) {
407 this->fShift = 0;
408 return;
409 }
410
411 SkScalar step = (maxWidth - textLen) / whitespacePatches;
412 SkScalar shift = 0;
413
414 // Spread the extra whitespaces
415 whitespacePatch = false;
416 this->iterateThroughClustersInGlyphsOrder(false, [&](const Cluster* cluster) {
417 if (cluster->isWhitespaces()) {
418 if (!whitespacePatch) {
419 shift += step;
420 whitespacePatch = true;
421 --whitespacePatches;
422 }
423 } else {
424 whitespacePatch = false;
425 }
426 cluster->shift(shift);
427 return true;
428 });
429
430 SkAssertResult(SkScalarNearlyEqual(shift, maxWidth - textLen));
431 SkASSERT(whitespacePatches == 0);
432 this->fShift = 0;
433 this->fAdvance.fX = maxWidth;
434}
435
436void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) {
437 TRACE_EVENT0("skia", TRACE_FUNC);
438 // Replace some clusters with the ellipsis
439 // Go through the clusters in the reverse logical order
440 // taking off cluster by cluster until the ellipsis fits
441 SkScalar width = fAdvance.fX;
442 iterateThroughClustersInGlyphsOrder(
443 true, [this, &width, ellipsis, maxWidth](const Cluster* cluster) {
444 if (cluster->isWhitespaces()) {
445 width -= cluster->width();
446 return true;
447 }
448
449 // Shape the ellipsis
450 Run* cached = fEllipsisCache.find(cluster->run()->font());
451 if (cached == nullptr) {
452 cached = shapeEllipsis(ellipsis, cluster->run());
453 }
454 fEllipsis = skstd::make_unique<Run>(*cached);
455
456 // See if it fits
457 if (width + fEllipsis->advance().fX > maxWidth) {
458 width -= cluster->width();
459 // Continue if it's not
460 return true;
461 }
462
463 fEllipsis->shift(width, 0);
464 fAdvance.fX = width;
465 return false;
466 });
467}
468
469Run* TextLine::shapeEllipsis(const SkString& ellipsis, Run* run) {
470 TRACE_EVENT0("skia", TRACE_FUNC);
471 class ShapeHandler final : public SkShaper::RunHandler {
472 public:
473 explicit ShapeHandler(SkScalar lineHeight) : fRun(nullptr), fLineHeight(lineHeight) {}
474 Run* run() { return fRun; }
475
476 private:
477 void beginLine() override {}
478
479 void runInfo(const RunInfo&) override {}
480
481 void commitRunInfo() override {}
482
483 Buffer runBuffer(const RunInfo& info) override {
484 fRun = fEllipsisCache.set(info.fFont,
485 Run(SkSpan<const char>(), info, fLineHeight, 0, 0));
486 return fRun->newRunBuffer();
487 }
488
489 void commitRunBuffer(const RunInfo& info) override {
490 fRun->fAdvance.fX = info.fAdvance.fX;
491 fRun->fAdvance.fY = fRun->descent() - fRun->ascent();
492 }
493
494 void commitLine() override {}
495
496 Run* fRun;
497 SkScalar fLineHeight;
498 };
499
500 ShapeHandler handler(run->lineHeight());
501 std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder();
502 SkASSERT_RELEASE(shaper != nullptr);
503 shaper->shape(ellipsis.c_str(), ellipsis.size(), run->font(), true,
504 std::numeric_limits<SkScalar>::max(), &handler);
505 handler.run()->fText = SkSpan<const char>(ellipsis.c_str(), ellipsis.size());
506 return handler.run();
507}
508
509SkRect TextLine::measureTextInsideOneRun(
510 SkSpan<const char> text, Run* run, size_t& pos, size_t& size, bool& clippingNeeded) const {
511 TRACE_EVENT0("skia", TRACE_FUNC);
512 SkASSERT(intersectedSize(run->text(), text) >= 0);
513
514 // Find [start:end] clusters for the text
515 bool found;
516 Cluster* start;
517 Cluster* end;
518 std::tie(found, start, end) = run->findLimitingClusters(text);
519 if (!found) {
520 SkASSERT(text.empty());
521 return SkRect::MakeEmpty();
522 }
523
524 pos = start->startPos();
525 size = end->endPos() - start->startPos();
526
527 // Calculate the clipping rectangle for the text with cluster edges
528 // There are 2 cases:
529 // EOL (when we expect the last cluster clipped without any spaces)
530 // Anything else (when we want the cluster width contain all the spaces -
531 // coming from letter spacing or word spacing or justification)
532 bool needsClipping = (run->leftToRight() ? end : start) == clusters().end() - 1;
533 SkRect clip =
534 SkRect::MakeXYWH(run->positionX(start->startPos()) - run->positionX(0),
535 sizes().runTop(run),
536 run->calculateWidth(start->startPos(), end->endPos(), needsClipping),
537 run->calculateHeight());
538
539 // Correct the width in case the text edges don't match clusters
540 // TODO: This is where we get smart about selecting a part of a cluster
541 // by shaping each grapheme separately and then use the result sizes
542 // to calculate the proportions
543 auto leftCorrection = start->sizeToChar(text.begin());
544 auto rightCorrection = end->sizeFromChar(text.end() - 1);
545 clip.fLeft += leftCorrection;
546 clip.fRight -= rightCorrection;
547 clippingNeeded = leftCorrection != 0 || rightCorrection != 0;
548
549 // SkDebugf("measureTextInsideOneRun: '%s'[%d:%d]\n", text.begin(), pos, pos + size);
550
551 return clip;
552}
553
554void TextLine::iterateThroughClustersInGlyphsOrder(bool reverse,
555 const ClustersVisitor& visitor) const {
556 TRACE_EVENT0("skia", TRACE_FUNC);
557 for (size_t r = 0; r != fLogical.size(); ++r) {
558 auto& run = fLogical[reverse ? fLogical.size() - r - 1 : r];
559 // Walk through the clusters in the logical order (or reverse)
560 auto normalOrder = run->leftToRight() != reverse;
561 auto start = normalOrder ? run->clusters().begin() : run->clusters().end() - 1;
562 auto end = normalOrder ? run->clusters().end() : run->clusters().begin() - 1;
563 for (auto cluster = start; cluster != end; normalOrder ? ++cluster : --cluster) {
564 if (!this->contains(cluster)) {
565 continue;
566 }
567 if (!visitor(cluster)) {
568 return;
569 }
570 }
571 }
572}
573
574// Walk through the runs in the logical order
575SkScalar TextLine::iterateThroughRuns(SkSpan<const char> text,
576 SkScalar runOffset,
577 bool includeEmptyText,
578 const RunVisitor& visitor) const {
579 TRACE_EVENT0("skia", TRACE_FUNC);
580
581 SkScalar width = 0;
582 for (auto& run : fLogical) {
583 // Only skip the text if it does not even touch the run
584 if (intersectedSize(run->text(), text) < 0) {
585 continue;
586 }
587
588 SkSpan<const char> intersect = intersected(run->text(), text);
589 if (run->text().empty() || intersect.empty()) {
590 continue;
591 }
592
593 size_t pos;
594 size_t size;
595 bool clippingNeeded;
596 SkRect clip = this->measureTextInsideOneRun(intersect, run, pos, size, clippingNeeded);
597 if (clip.height() == 0) {
598 continue;
599 }
600
601 auto shift = runOffset - clip.fLeft;
602 clip.offset(shift, 0);
603 if (clip.fRight > fAdvance.fX) {
604 clip.fRight = fAdvance.fX;
605 clippingNeeded = true; // Correct the clip in case there was an ellipsis
606 } else if (run == fLogical.back() && this->ellipsis() != nullptr) {
607 clippingNeeded = true; // To avoid trouble
608 }
609
610 if (!visitor(run, pos, size, clip, shift - run->positionX(0), clippingNeeded)) {
611 return width;
612 }
613
614 width += clip.width();
615 runOffset += clip.width();
616 }
617
618 if (this->ellipsis() != nullptr) {
619 auto ellipsis = this->ellipsis();
620 if (!visitor(ellipsis, 0, ellipsis->size(), ellipsis->clip(), ellipsis->clip().fLeft,
621 false)) {
622 return width;
623 }
624 width += ellipsis->clip().width();
625 }
626
627 return width;
628}
629
630void TextLine::iterateThroughStylesInTextOrder(StyleType styleType,
631 const StyleVisitor& visitor) const {
632 TRACE_EVENT0("skia", TRACE_FUNC);
633 const char* start = nullptr;
634 size_t size = 0;
635 TextStyle prevStyle;
636
637 SkScalar offsetX = 0;
638 for (auto& block : fBlocks) {
639 auto intersect = intersected(block.text(), this->trimmedText());
640 if (intersect.empty()) {
641 if (start == nullptr) {
642 // This style is not applicable to the line
643 continue;
644 } else {
645 // We have found all the good styles already
646 break;
647 }
648 }
649
650 auto style = block.style();
651 if (start != nullptr && style.matchOneAttribute(styleType, prevStyle)) {
652 size += intersect.size();
653 continue;
654 } else if (size == 0) {
655 // First time only
656 prevStyle = style;
657 size = intersect.size();
658 start = intersect.begin();
659 continue;
660 }
661
662 auto width = visitor(SkSpan<const char>(start, size), prevStyle, offsetX);
663 offsetX += width;
664
665 // Start all over again
666 prevStyle = style;
667 start = intersect.begin();
668 size = intersect.size();
669 }
670
671 // The very last style
672 auto width = visitor(SkSpan<const char>(start, size), prevStyle, offsetX);
673 offsetX += width;
674
675 // This is a very important assert!
676 // It asserts that 2 different ways of calculation come with the same results
677 if (!SkScalarNearlyEqual(offsetX, this->width())) {
678 SkDebugf("ASSERT: %f != %f\n", offsetX, this->width());
679 }
680 SkASSERT(SkScalarNearlyEqual(offsetX, this->width()));
681}
682} // namespace textlayout
683} // namespace skia