1 // Copyright 2019 Google LLC.
2
3 #include "include/core/SkCanvas.h"
4 #include "include/core/SkFontMetrics.h"
5 #include "include/core/SkMatrix.h"
6 #include "include/core/SkPictureRecorder.h"
7 #include "include/core/SkSpan.h"
8 #include "include/core/SkTypeface.h"
9 #include "include/private/base/SkTFitsIn.h"
10 #include "include/private/base/SkTo.h"
11 #include "modules/skparagraph/include/Metrics.h"
12 #include "modules/skparagraph/include/Paragraph.h"
13 #include "modules/skparagraph/include/ParagraphPainter.h"
14 #include "modules/skparagraph/include/ParagraphStyle.h"
15 #include "modules/skparagraph/include/TextStyle.h"
16 #include "modules/skparagraph/src/OneLineShaper.h"
17 #include "modules/skparagraph/src/ParagraphImpl.h"
18 #include "modules/skparagraph/src/ParagraphPainterImpl.h"
19 #include "modules/skparagraph/src/Run.h"
20 #include "modules/skparagraph/src/TextLine.h"
21 #include "modules/skparagraph/src/TextWrapper.h"
22 #include "src/base/SkUTF.h"
23 #include <math.h>
24 #include <algorithm>
25 #include <utility>
26
27
28 namespace skia {
29 namespace textlayout {
30
31 namespace {
32
littleRound(SkScalar a)33 SkScalar littleRound(SkScalar a) {
34 // This rounding is done to match Flutter tests. Must be removed..
35 auto val = std::fabs(a);
36 if (val < 10000) {
37 return SkScalarRoundToScalar(a * 100.0)/100.0;
38 } else if (val < 100000) {
39 return SkScalarRoundToScalar(a * 10.0)/10.0;
40 } else {
41 return SkScalarFloorToScalar(a);
42 }
43 }
44 } // namespace
45
operator *(const TextRange & a,const TextRange & b)46 TextRange operator*(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
Paragraph(ParagraphStyle style,sk_sp<FontCollection> fonts)53 Paragraph::Paragraph(ParagraphStyle style, sk_sp<FontCollection> fonts)
54 : fFontCollection(std::move(fonts))
55 , fParagraphStyle(std::move(style))
56 , fAlphabeticBaseline(0)
57 , fIdeographicBaseline(0)
58 , fHeight(0)
59 , fWidth(0)
60 , fMaxIntrinsicWidth(0)
61 , fMinIntrinsicWidth(0)
62 , fLongestLine(0)
63 , fExceededMaxLines(0)
64 { }
65
ParagraphImpl(const SkString & text,ParagraphStyle style,SkTArray<Block,true> blocks,SkTArray<Placeholder,true> placeholders,sk_sp<FontCollection> fonts,std::shared_ptr<SkUnicode> unicode)66 ParagraphImpl::ParagraphImpl(const SkString& text,
67 ParagraphStyle style,
68 SkTArray<Block, true> blocks,
69 SkTArray<Placeholder, true> placeholders,
70 sk_sp<FontCollection> fonts,
71 std::shared_ptr<SkUnicode> unicode)
72 : Paragraph(std::move(style), std::move(fonts))
73 , fTextStyles(std::move(blocks))
74 , fPlaceholders(std::move(placeholders))
75 , fText(text)
76 , fState(kUnknown)
77 , fUnresolvedGlyphs(0)
78 , fPicture(nullptr)
79 , fStrutMetrics(false)
80 , fOldWidth(0)
81 , fOldHeight(0)
82 , fUnicode(std::move(unicode))
83 , fHasLineBreaks(false)
84 , fHasWhitespacesInside(false)
85 , fTrailingSpaces(0)
86 {
87 SkASSERT(fUnicode);
88 }
89
ParagraphImpl(const std::u16string & utf16text,ParagraphStyle style,SkTArray<Block,true> blocks,SkTArray<Placeholder,true> placeholders,sk_sp<FontCollection> fonts,std::shared_ptr<SkUnicode> unicode)90 ParagraphImpl::ParagraphImpl(const std::u16string& utf16text,
91 ParagraphStyle style,
92 SkTArray<Block, true> blocks,
93 SkTArray<Placeholder, true> placeholders,
94 sk_sp<FontCollection> fonts,
95 std::shared_ptr<SkUnicode> unicode)
96 : ParagraphImpl(SkString(),
97 std::move(style),
98 std::move(blocks),
99 std::move(placeholders),
100 std::move(fonts),
101 std::move(unicode))
102 {
103 SkASSERT(fUnicode);
104 fText = SkUnicode::convertUtf16ToUtf8(utf16text);
105 }
106
107 ParagraphImpl::~ParagraphImpl() = default;
108
unresolvedGlyphs()109 int32_t ParagraphImpl::unresolvedGlyphs() {
110 if (fState < kShaped) {
111 return -1;
112 }
113
114 return fUnresolvedGlyphs;
115 }
116
layout(SkScalar rawWidth)117 void ParagraphImpl::layout(SkScalar rawWidth) {
118
119 // TODO: This rounding is done to match Flutter tests. Must be removed...
120 auto floorWidth = SkScalarFloorToScalar(rawWidth);
121
122 if ((!SkScalarIsFinite(rawWidth) || fLongestLine <= floorWidth) &&
123 fState >= kLineBroken &&
124 fLines.size() == 1 && fLines.front().ellipsis() == nullptr) {
125 // Most common case: one line of text (and one line is never justified, so no cluster shifts)
126 // We cannot mark it as kLineBroken because the new width can be bigger than the old width
127 fWidth = floorWidth;
128 fState = kShaped;
129 } else if (fState >= kLineBroken && fOldWidth != floorWidth) {
130 // We can use the results from SkShaper but have to do EVERYTHING ELSE again
131 fState = kShaped;
132 } else {
133 // Nothing changed case: we can reuse the data from the last layout
134 }
135
136 if (fState < kShaped) {
137 // Check if we have the text in the cache and don't need to shape it again
138 if (!fFontCollection->getParagraphCache()->findParagraph(this)) {
139 if (fState < kIndexed) {
140 // This only happens once at the first layout; the text is immutable
141 // and there is no reason to repeat it
142 if (this->computeCodeUnitProperties()) {
143 fState = kIndexed;
144 }
145 }
146 this->fRuns.clear();
147 this->fClusters.clear();
148 this->fClustersIndexFromCodeUnit.clear();
149 this->fClustersIndexFromCodeUnit.push_back_n(fText.size() + 1, EMPTY_INDEX);
150 if (!this->shapeTextIntoEndlessLine()) {
151 this->resetContext();
152 // TODO: merge the two next calls - they always come together
153 this->resolveStrut();
154 this->computeEmptyMetrics();
155 this->fLines.clear();
156
157 // Set the important values that are not zero
158 fWidth = floorWidth;
159 fHeight = fEmptyMetrics.height();
160 if (fParagraphStyle.getStrutStyle().getStrutEnabled() &&
161 fParagraphStyle.getStrutStyle().getForceStrutHeight()) {
162 fHeight = fStrutMetrics.height();
163 }
164 fAlphabeticBaseline = fEmptyMetrics.alphabeticBaseline();
165 fIdeographicBaseline = fEmptyMetrics.ideographicBaseline();
166 fLongestLine = FLT_MIN - FLT_MAX; // That is what flutter has
167 fMinIntrinsicWidth = 0;
168 fMaxIntrinsicWidth = 0;
169 this->fOldWidth = floorWidth;
170 this->fOldHeight = this->fHeight;
171
172 return;
173 } else {
174 // Add the paragraph to the cache
175 fFontCollection->getParagraphCache()->updateParagraph(this);
176 }
177 }
178 fState = kShaped;
179 }
180
181 if (fState == kShaped) {
182 this->resetContext();
183 this->resolveStrut();
184 this->computeEmptyMetrics();
185 this->fLines.clear();
186 this->breakShapedTextIntoLines(floorWidth);
187 fState = kLineBroken;
188 }
189
190 if (fState == kLineBroken) {
191 // Build the picture lazily not until we actually have to paint (or never)
192 this->resetShifts();
193 this->formatLines(fWidth);
194 fState = kFormatted;
195 }
196
197 this->fOldWidth = floorWidth;
198 this->fOldHeight = this->fHeight;
199
200 // TODO: This rounding is done to match Flutter tests. Must be removed...
201 fMinIntrinsicWidth = littleRound(fMinIntrinsicWidth);
202 fMaxIntrinsicWidth = littleRound(fMaxIntrinsicWidth);
203
204 // TODO: This is strictly Flutter thing. Must be factored out into some flutter code
205 if (fParagraphStyle.getMaxLines() == 1 ||
206 (fParagraphStyle.unlimited_lines() && fParagraphStyle.ellipsized())) {
207 fMinIntrinsicWidth = fMaxIntrinsicWidth;
208 }
209
210 // TODO: Since min and max are calculated differently it's possible to get a rounding error
211 // that would make min > max. Sort it out later, make it the same for now
212 if (fMaxIntrinsicWidth < fMinIntrinsicWidth) {
213 fMaxIntrinsicWidth = fMinIntrinsicWidth;
214 }
215
216 //SkDebugf("layout('%s', %f): %f %f\n", fText.c_str(), rawWidth, fMinIntrinsicWidth, fMaxIntrinsicWidth);
217 }
218
paint(SkCanvas * canvas,SkScalar x,SkScalar y)219 void ParagraphImpl::paint(SkCanvas* canvas, SkScalar x, SkScalar y) {
220 CanvasParagraphPainter painter(canvas);
221 paint(&painter, x, y);
222 }
223
paint(ParagraphPainter * painter,SkScalar x,SkScalar y)224 void ParagraphImpl::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) {
225 for (auto& line : fLines) {
226 line.paint(painter, x, y);
227 }
228 }
229
resetContext()230 void ParagraphImpl::resetContext() {
231 fAlphabeticBaseline = 0;
232 fHeight = 0;
233 fWidth = 0;
234 fIdeographicBaseline = 0;
235 fMaxIntrinsicWidth = 0;
236 fMinIntrinsicWidth = 0;
237 fLongestLine = 0;
238 fMaxWidthWithTrailingSpaces = 0;
239 fExceededMaxLines = false;
240 }
241
242 // shapeTextIntoEndlessLine is the thing that calls this method
computeCodeUnitProperties()243 bool ParagraphImpl::computeCodeUnitProperties() {
244
245 if (nullptr == fUnicode) {
246 return false;
247 }
248
249 // Get bidi regions
250 auto textDirection = fParagraphStyle.getTextDirection() == TextDirection::kLtr
251 ? SkUnicode::TextDirection::kLTR
252 : SkUnicode::TextDirection::kRTL;
253 if (!fUnicode->getBidiRegions(fText.c_str(), fText.size(), textDirection, &fBidiRegions)) {
254 return false;
255 }
256
257 // Collect all spaces and some extra information
258 // (and also substitute \t with a space while we are at it)
259 if (!fUnicode->computeCodeUnitFlags(&fText[0],
260 fText.size(),
261 this->paragraphStyle().getReplaceTabCharacters(),
262 &fCodeUnitProperties)) {
263 return false;
264 }
265
266 // Get some information about trailing spaces / hard line breaks
267 fTrailingSpaces = fText.size();
268 TextIndex firstWhitespace = EMPTY_INDEX;
269 for (int i = 0; i < fCodeUnitProperties.size(); ++i) {
270 auto flags = fCodeUnitProperties[i];
271 if (SkUnicode::isPartOfWhiteSpaceBreak(flags)) {
272 if (fTrailingSpaces == fText.size()) {
273 fTrailingSpaces = i;
274 }
275 if (firstWhitespace == EMPTY_INDEX) {
276 firstWhitespace = i;
277 }
278 } else {
279 fTrailingSpaces = fText.size();
280 }
281 if (SkUnicode::isHardLineBreak(flags)) {
282 fHasLineBreaks = true;
283 }
284 }
285
286 if (firstWhitespace < fTrailingSpaces) {
287 fHasWhitespacesInside = true;
288 }
289
290 return true;
291 }
292
is_ascii_7bit_space(int c)293 static bool is_ascii_7bit_space(int c) {
294 SkASSERT(c >= 0 && c <= 127);
295
296 // Extracted from https://en.wikipedia.org/wiki/Whitespace_character
297 //
298 enum WS {
299 kHT = 9,
300 kLF = 10,
301 kVT = 11,
302 kFF = 12,
303 kCR = 13,
304 kSP = 32, // too big to use as shift
305 };
306 #define M(shift) (1 << (shift))
307 constexpr uint32_t kSpaceMask = M(kHT) | M(kLF) | M(kVT) | M(kFF) | M(kCR);
308 // we check for Space (32) explicitly, since it is too large to shift
309 return (c == kSP) || (c <= 31 && (kSpaceMask & M(c)));
310 #undef M
311 }
312
Cluster(ParagraphImpl * owner,RunIndex runIndex,size_t start,size_t end,SkSpan<const char> text,SkScalar width,SkScalar height)313 Cluster::Cluster(ParagraphImpl* owner,
314 RunIndex runIndex,
315 size_t start,
316 size_t end,
317 SkSpan<const char> text,
318 SkScalar width,
319 SkScalar height)
320 : fOwner(owner)
321 , fRunIndex(runIndex)
322 , fTextRange(text.begin() - fOwner->text().begin(), text.end() - fOwner->text().begin())
323 , fGraphemeRange(EMPTY_RANGE)
324 , fStart(start)
325 , fEnd(end)
326 , fWidth(width)
327 , fHeight(height)
328 , fHalfLetterSpacing(0.0) {
329 size_t whiteSpacesBreakLen = 0;
330 size_t intraWordBreakLen = 0;
331
332 const char* ch = text.begin();
333 if (text.end() - ch == 1 && *(unsigned char*)ch <= 0x7F) {
334 // I am not even sure it's worth it if we do not save a unicode call
335 if (is_ascii_7bit_space(*ch)) {
336 ++whiteSpacesBreakLen;
337 }
338 } else {
339 for (auto i = fTextRange.start; i < fTextRange.end; ++i) {
340 if (fOwner->codeUnitHasProperty(i, SkUnicode::CodeUnitFlags::kPartOfWhiteSpaceBreak)) {
341 ++whiteSpacesBreakLen;
342 }
343 if (fOwner->codeUnitHasProperty(i, SkUnicode::CodeUnitFlags::kPartOfIntraWordBreak)) {
344 ++intraWordBreakLen;
345 }
346 }
347 }
348
349 fIsWhiteSpaceBreak = whiteSpacesBreakLen == fTextRange.width();
350 fIsIntraWordBreak = intraWordBreakLen == fTextRange.width();
351 fIsHardBreak = fOwner->codeUnitHasProperty(fTextRange.end,
352 SkUnicode::CodeUnitFlags::kHardLineBreakBefore);
353 }
354
calculateWidth(size_t start,size_t end,bool clip) const355 SkScalar Run::calculateWidth(size_t start, size_t end, bool clip) const {
356 SkASSERT(start <= end);
357 // clip |= end == size(); // Clip at the end of the run?
358 auto correction = 0.0f;
359 if (end > start && !fJustificationShifts.empty()) {
360 // This is not a typo: we are using Point as a pair of SkScalars
361 correction = fJustificationShifts[end - 1].fX -
362 fJustificationShifts[start].fY;
363 }
364 return posX(end) - posX(start) + correction;
365 }
366
367 // In some cases we apply spacing to glyphs first and then build the cluster table, in some we do
368 // the opposite - just to optimize the most common case.
applySpacingAndBuildClusterTable()369 void ParagraphImpl::applySpacingAndBuildClusterTable() {
370
371 // Check all text styles to see what we have to do (if anything)
372 size_t letterSpacingStyles = 0;
373 bool hasWordSpacing = false;
374 for (auto& block : fTextStyles) {
375 if (block.fRange.width() > 0) {
376 if (!SkScalarNearlyZero(block.fStyle.getLetterSpacing())) {
377 ++letterSpacingStyles;
378 }
379 if (!SkScalarNearlyZero(block.fStyle.getWordSpacing())) {
380 hasWordSpacing = true;
381 }
382 }
383 }
384
385 if (letterSpacingStyles == 0 && !hasWordSpacing) {
386 // We don't have to do anything about spacing (most common case)
387 this->buildClusterTable();
388 return;
389 }
390
391 if (letterSpacingStyles == 1 && !hasWordSpacing && fTextStyles.size() == 1 &&
392 fTextStyles[0].fRange.width() == fText.size() && fRuns.size() == 1) {
393 // We have to letter space the entire paragraph (second most common case)
394 auto& run = fRuns[0];
395 auto& style = fTextStyles[0].fStyle;
396 run.addSpacesEvenly(style.getLetterSpacing());
397 this->buildClusterTable();
398 // This is something Flutter requires
399 for (auto& cluster : fClusters) {
400 cluster.setHalfLetterSpacing(style.getLetterSpacing()/2);
401 }
402 return;
403 }
404
405 // The complex case: many text styles with spacing (possibly not adjusted to glyphs)
406 this->buildClusterTable();
407
408 // Walk through all the clusters in the direction of shaped text
409 // (we have to walk through the styles in the same order, too)
410 SkScalar shift = 0;
411 for (auto& run : fRuns) {
412
413 // Skip placeholder runs
414 if (run.isPlaceholder()) {
415 continue;
416 }
417 bool soFarWhitespacesOnly = true;
418 bool wordSpacingPending = false;
419 Cluster* lastSpaceCluster = nullptr;
420 run.iterateThroughClusters([this, &run, &shift, &soFarWhitespacesOnly, &wordSpacingPending, &lastSpaceCluster](Cluster* cluster) {
421 // Shift the cluster (shift collected from the previous clusters)
422 run.shift(cluster, shift);
423
424 // Synchronize styles (one cluster can be covered by few styles)
425 Block* currentStyle = fTextStyles.begin();
426 while (!cluster->startsIn(currentStyle->fRange)) {
427 currentStyle++;
428 SkASSERT(currentStyle != fTextStyles.end());
429 }
430
431 SkASSERT(!currentStyle->fStyle.isPlaceholder());
432
433 // Process word spacing
434 if (currentStyle->fStyle.getWordSpacing() != 0) {
435 if (cluster->isWhitespaceBreak() && cluster->isSoftBreak()) {
436 if (!soFarWhitespacesOnly) {
437 lastSpaceCluster = cluster;
438 wordSpacingPending = true;
439 }
440 } else if (wordSpacingPending) {
441 SkScalar spacing = currentStyle->fStyle.getWordSpacing();
442 run.addSpacesAtTheEnd(spacing, lastSpaceCluster);
443 run.shift(cluster, spacing);
444 shift += spacing;
445 wordSpacingPending = false;
446 }
447 }
448 // Process letter spacing
449 if (currentStyle->fStyle.getLetterSpacing() != 0) {
450 shift += run.addSpacesEvenly(currentStyle->fStyle.getLetterSpacing(), cluster);
451 }
452
453 if (soFarWhitespacesOnly && !cluster->isWhitespaceBreak()) {
454 soFarWhitespacesOnly = false;
455 }
456 });
457 }
458 }
459
460 // Clusters in the order of the input text
buildClusterTable()461 void ParagraphImpl::buildClusterTable() {
462 // It's possible that one grapheme includes few runs; we cannot handle it
463 // so we break graphemes by the runs instead
464 // It's not the ideal solution and has to be revisited later
465 int cluster_count = 1;
466 for (auto& run : fRuns) {
467 cluster_count += run.isPlaceholder() ? 1 : run.size();
468 fCodeUnitProperties[run.fTextRange.start] |= SkUnicode::CodeUnitFlags::kGraphemeStart;
469 fCodeUnitProperties[run.fTextRange.start] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
470 }
471 if (!fRuns.empty()) {
472 fCodeUnitProperties[fRuns.back().textRange().end] |= SkUnicode::CodeUnitFlags::kGraphemeStart;
473 fCodeUnitProperties[fRuns.back().textRange().end] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
474 }
475 fClusters.reserve_back(cluster_count);
476
477 // Walk through all the run in the direction of input text
478 for (auto& run : fRuns) {
479 auto runIndex = run.index();
480 auto runStart = fClusters.size();
481 if (run.isPlaceholder()) {
482 // Add info to cluster indexes table (text -> cluster)
483 for (auto i = run.textRange().start; i < run.textRange().end; ++i) {
484 fClustersIndexFromCodeUnit[i] = fClusters.size();
485 }
486 // There are no glyphs but we want to have one cluster
487 fClusters.emplace_back(this, runIndex, 0ul, 1ul, this->text(run.textRange()), run.advance().fX, run.advance().fY);
488 fCodeUnitProperties[run.textRange().start] |= SkUnicode::CodeUnitFlags::kSoftLineBreakBefore;
489 fCodeUnitProperties[run.textRange().end] |= SkUnicode::CodeUnitFlags::kSoftLineBreakBefore;
490 } else {
491 // Walk through the glyph in the direction of input text
492 run.iterateThroughClustersInTextOrder([runIndex, this](size_t glyphStart,
493 size_t glyphEnd,
494 size_t charStart,
495 size_t charEnd,
496 SkScalar width,
497 SkScalar height) {
498 SkASSERT(charEnd >= charStart);
499 // Add info to cluster indexes table (text -> cluster)
500 for (auto i = charStart; i < charEnd; ++i) {
501 fClustersIndexFromCodeUnit[i] = fClusters.size();
502 }
503 SkSpan<const char> text(fText.c_str() + charStart, charEnd - charStart);
504 fClusters.emplace_back(this, runIndex, glyphStart, glyphEnd, text, width, height);
505 fCodeUnitProperties[charStart] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
506 });
507 }
508 fCodeUnitProperties[run.textRange().start] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
509
510 run.setClusterRange(runStart, fClusters.size());
511 fMaxIntrinsicWidth += run.advance().fX;
512 }
513 fClustersIndexFromCodeUnit[fText.size()] = fClusters.size();
514 fClusters.emplace_back(this, EMPTY_RUN, 0, 0, this->text({fText.size(), fText.size()}), 0, 0);
515 }
516
shapeTextIntoEndlessLine()517 bool ParagraphImpl::shapeTextIntoEndlessLine() {
518
519 if (fText.size() == 0) {
520 return false;
521 }
522
523 fFontSwitches.clear();
524
525 OneLineShaper oneLineShaper(this);
526 auto result = oneLineShaper.shape();
527 fUnresolvedGlyphs = oneLineShaper.unresolvedGlyphs();
528
529 this->applySpacingAndBuildClusterTable();
530
531 return result;
532 }
533
breakShapedTextIntoLines(SkScalar maxWidth)534 void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) {
535
536 if (!fHasLineBreaks &&
537 !fHasWhitespacesInside &&
538 fPlaceholders.size() == 1 &&
539 fRuns.size() == 1 && fRuns[0].fAdvance.fX <= maxWidth) {
540 // This is a short version of a line breaking when we know that:
541 // 1. We have only one line of text
542 // 2. It's shaped into a single run
543 // 3. There are no placeholders
544 // 4. There are no linebreaks (which will format text into multiple lines)
545 // 5. There are no whitespaces so the minIntrinsicWidth=maxIntrinsicWidth
546 // (To think about that, the last condition is not quite right;
547 // we should calculate minIntrinsicWidth by soft line breaks.
548 // However, it's how it's done in Flutter now)
549 auto& run = this->fRuns[0];
550 auto advance = run.advance();
551 auto textRange = TextRange(0, this->text().size());
552 auto textExcludingSpaces = TextRange(0, fTrailingSpaces);
553 InternalLineMetrics metrics(this->strutForceHeight());
554 metrics.add(&run);
555 auto disableFirstAscent = this->paragraphStyle().getTextHeightBehavior() &
556 TextHeightBehavior::kDisableFirstAscent;
557 auto disableLastDescent = this->paragraphStyle().getTextHeightBehavior() &
558 TextHeightBehavior::kDisableLastDescent;
559 if (disableFirstAscent) {
560 metrics.fAscent = metrics.fRawAscent;
561 }
562 if (disableLastDescent) {
563 metrics.fDescent = metrics.fRawDescent;
564 }
565 if (this->strutEnabled()) {
566 this->strutMetrics().updateLineMetrics(metrics);
567 }
568 ClusterIndex trailingSpaces = fClusters.size();
569 do {
570 --trailingSpaces;
571 auto& cluster = fClusters[trailingSpaces];
572 if (!cluster.isWhitespaceBreak()) {
573 ++trailingSpaces;
574 break;
575 }
576 advance.fX -= cluster.width();
577 } while (trailingSpaces != 0);
578
579 advance.fY = metrics.height();
580 auto clusterRange = ClusterRange(0, trailingSpaces);
581 auto clusterRangeWithGhosts = ClusterRange(0, this->clusters().size() - 1);
582 this->addLine(SkPoint::Make(0, 0), advance,
583 textExcludingSpaces, textRange, textRange,
584 clusterRange, clusterRangeWithGhosts, run.advance().x(),
585 metrics);
586
587 fLongestLine = nearlyZero(advance.fX) ? run.advance().fX : advance.fX;
588 fHeight = advance.fY;
589 fWidth = maxWidth;
590 fMaxIntrinsicWidth = run.advance().fX;
591 fMinIntrinsicWidth = advance.fX;
592 fAlphabeticBaseline = fLines.empty() ? fEmptyMetrics.alphabeticBaseline() : fLines.front().alphabeticBaseline();
593 fIdeographicBaseline = fLines.empty() ? fEmptyMetrics.ideographicBaseline() : fLines.front().ideographicBaseline();
594 fExceededMaxLines = false;
595 return;
596 }
597
598 TextWrapper textWrapper;
599 textWrapper.breakTextIntoLines(
600 this,
601 maxWidth,
602 [&](TextRange textExcludingSpaces,
603 TextRange text,
604 TextRange textWithNewlines,
605 ClusterRange clusters,
606 ClusterRange clustersWithGhosts,
607 SkScalar widthWithSpaces,
608 size_t startPos,
609 size_t endPos,
610 SkVector offset,
611 SkVector advance,
612 InternalLineMetrics metrics,
613 bool addEllipsis) {
614 // TODO: Take in account clipped edges
615 auto& line = this->addLine(offset, advance, textExcludingSpaces, text, textWithNewlines, clusters, clustersWithGhosts, widthWithSpaces, metrics);
616 if (addEllipsis) {
617 line.createEllipsis(maxWidth, this->getEllipsis(), true);
618 }
619 fLongestLine = std::max(fLongestLine, nearlyZero(advance.fX) ? widthWithSpaces : advance.fX);
620 });
621
622 fHeight = textWrapper.height();
623 fWidth = maxWidth;
624 fMaxIntrinsicWidth = textWrapper.maxIntrinsicWidth();
625 fMinIntrinsicWidth = textWrapper.minIntrinsicWidth();
626 fAlphabeticBaseline = fLines.empty() ? fEmptyMetrics.alphabeticBaseline() : fLines.front().alphabeticBaseline();
627 fIdeographicBaseline = fLines.empty() ? fEmptyMetrics.ideographicBaseline() : fLines.front().ideographicBaseline();
628 fExceededMaxLines = textWrapper.exceededMaxLines();
629 }
630
formatLines(SkScalar maxWidth)631 void ParagraphImpl::formatLines(SkScalar maxWidth) {
632 auto effectiveAlign = fParagraphStyle.effective_align();
633 const bool isLeftAligned = effectiveAlign == TextAlign::kLeft
634 || (effectiveAlign == TextAlign::kJustify && fParagraphStyle.getTextDirection() == TextDirection::kLtr);
635
636 if (!SkScalarIsFinite(maxWidth) && !isLeftAligned) {
637 // Special case: clean all text in case of maxWidth == INF & align != left
638 // We had to go through shaping though because we need all the measurement numbers
639 fLines.clear();
640 return;
641 }
642
643 for (auto& line : fLines) {
644 line.format(effectiveAlign, maxWidth);
645 }
646 }
647
resolveStrut()648 void ParagraphImpl::resolveStrut() {
649 auto strutStyle = this->paragraphStyle().getStrutStyle();
650 if (!strutStyle.getStrutEnabled() || strutStyle.getFontSize() < 0) {
651 return;
652 }
653
654 std::vector<sk_sp<SkTypeface>> typefaces = fFontCollection->findTypefaces(strutStyle.getFontFamilies(), strutStyle.getFontStyle(), std::nullopt);
655 if (typefaces.empty()) {
656 SkDEBUGF("Could not resolve strut font\n");
657 return;
658 }
659
660 SkFont font(typefaces.front(), strutStyle.getFontSize());
661 SkFontMetrics metrics;
662 font.getMetrics(&metrics);
663
664 if (strutStyle.getHeightOverride()) {
665 auto strutHeight = metrics.fDescent - metrics.fAscent;
666 auto strutMultiplier = strutStyle.getHeight() * strutStyle.getFontSize();
667 fStrutMetrics = InternalLineMetrics(
668 (metrics.fAscent / strutHeight) * strutMultiplier,
669 (metrics.fDescent / strutHeight) * strutMultiplier,
670 strutStyle.getLeading() < 0 ? 0 : strutStyle.getLeading() * strutStyle.getFontSize(),
671 metrics.fAscent, metrics.fDescent, metrics.fLeading);
672 } else {
673 fStrutMetrics = InternalLineMetrics(
674 metrics.fAscent,
675 metrics.fDescent,
676 strutStyle.getLeading() < 0 ? 0 : strutStyle.getLeading() * strutStyle.getFontSize());
677 }
678 fStrutMetrics.setForceStrut(this->paragraphStyle().getStrutStyle().getForceStrutHeight());
679 }
680
findAllBlocks(TextRange textRange)681 BlockRange ParagraphImpl::findAllBlocks(TextRange textRange) {
682 BlockIndex begin = EMPTY_BLOCK;
683 BlockIndex end = EMPTY_BLOCK;
684 for (int index = 0; index < fTextStyles.size(); ++index) {
685 auto& block = fTextStyles[index];
686 if (block.fRange.end <= textRange.start) {
687 continue;
688 }
689 if (block.fRange.start >= textRange.end) {
690 break;
691 }
692 if (begin == EMPTY_BLOCK) {
693 begin = index;
694 }
695 end = index;
696 }
697
698 if (begin == EMPTY_INDEX || end == EMPTY_INDEX) {
699 // It's possible if some text is not covered with any text style
700 // Not in Flutter but in direct use of SkParagraph
701 return EMPTY_RANGE;
702 }
703
704 return { begin, end + 1 };
705 }
706
addLine(SkVector offset,SkVector advance,TextRange textExcludingSpaces,TextRange text,TextRange textIncludingNewLines,ClusterRange clusters,ClusterRange clustersWithGhosts,SkScalar widthWithSpaces,InternalLineMetrics sizes)707 TextLine& ParagraphImpl::addLine(SkVector offset,
708 SkVector advance,
709 TextRange textExcludingSpaces,
710 TextRange text,
711 TextRange textIncludingNewLines,
712 ClusterRange clusters,
713 ClusterRange clustersWithGhosts,
714 SkScalar widthWithSpaces,
715 InternalLineMetrics sizes) {
716 // Define a list of styles that covers the line
717 auto blocks = findAllBlocks(textExcludingSpaces);
718 return fLines.emplace_back(this, offset, advance, blocks,
719 textExcludingSpaces, text, textIncludingNewLines,
720 clusters, clustersWithGhosts, widthWithSpaces, sizes);
721 }
722
723 // Returns a vector of bounding boxes that enclose all text between
724 // start and end glyph indexes, including start and excluding end
getRectsForRange(unsigned start,unsigned end,RectHeightStyle rectHeightStyle,RectWidthStyle rectWidthStyle)725 std::vector<TextBox> ParagraphImpl::getRectsForRange(unsigned start,
726 unsigned end,
727 RectHeightStyle rectHeightStyle,
728 RectWidthStyle rectWidthStyle) {
729 std::vector<TextBox> results;
730 if (fText.isEmpty()) {
731 if (start == 0 && end > 0) {
732 // On account of implied "\n" that is always at the end of the text
733 //SkDebugf("getRectsForRange(%d, %d): %f\n", start, end, fHeight);
734 results.emplace_back(SkRect::MakeXYWH(0, 0, 0, fHeight), fParagraphStyle.getTextDirection());
735 }
736 return results;
737 }
738
739 ensureUTF16Mapping();
740
741 if (start >= end || start > SkToSizeT(fUTF8IndexForUTF16Index.size()) || end == 0) {
742 return results;
743 }
744
745 // Adjust the text to grapheme edges
746 // Apparently, text editor CAN move inside graphemes but CANNOT select a part of it.
747 // I don't know why - the solution I have here returns an empty box for every query that
748 // does not contain an end of a grapheme.
749 // Once a cursor is inside a complex grapheme I can press backspace and cause trouble.
750 // To avoid any problems, I will not allow any selection of a part of a grapheme.
751 // One flutter test fails because of it but the editing experience is correct
752 // (although you have to press the cursor many times before it moves to the next grapheme).
753 TextRange text(fText.size(), fText.size());
754 // TODO: This is probably a temp change that makes SkParagraph work as TxtLib
755 // (so we can compare the results). We now include in the selection box only the graphemes
756 // that belongs to the given [start:end) range entirely (not the ones that intersect with it)
757 if (start < SkToSizeT(fUTF8IndexForUTF16Index.size())) {
758 auto utf8 = fUTF8IndexForUTF16Index[start];
759 // If start points to a trailing surrogate, skip it
760 if (start > 0 && fUTF8IndexForUTF16Index[start - 1] == utf8) {
761 utf8 = fUTF8IndexForUTF16Index[start + 1];
762 }
763 text.start = this->findNextGraphemeBoundary(utf8);
764 }
765 if (end < SkToSizeT(fUTF8IndexForUTF16Index.size())) {
766 auto utf8 = this->findPreviousGraphemeBoundary(fUTF8IndexForUTF16Index[end]);
767 text.end = utf8;
768 }
769 //SkDebugf("getRectsForRange(%d,%d) -> (%d:%d)\n", start, end, text.start, text.end);
770 for (auto& line : fLines) {
771 auto lineText = line.textWithNewlines();
772 auto intersect = lineText * text;
773 if (intersect.empty() && lineText.start != text.start) {
774 continue;
775 }
776
777 line.getRectsForRange(intersect, rectHeightStyle, rectWidthStyle, results);
778 }
779 /*
780 SkDebugf("getRectsForRange(%d, %d)\n", start, end);
781 for (auto& r : results) {
782 r.rect.fLeft = littleRound(r.rect.fLeft);
783 r.rect.fRight = littleRound(r.rect.fRight);
784 r.rect.fTop = littleRound(r.rect.fTop);
785 r.rect.fBottom = littleRound(r.rect.fBottom);
786 SkDebugf("[%f:%f * %f:%f]\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom);
787 }
788 */
789 return results;
790 }
791
getRectsForPlaceholders()792 std::vector<TextBox> ParagraphImpl::getRectsForPlaceholders() {
793 std::vector<TextBox> boxes;
794 if (fText.isEmpty()) {
795 return boxes;
796 }
797 if (fPlaceholders.size() == 1) {
798 // We always have one fake placeholder
799 return boxes;
800 }
801 for (auto& line : fLines) {
802 line.getRectsForPlaceholders(boxes);
803 }
804 /*
805 SkDebugf("getRectsForPlaceholders('%s'): %d\n", fText.c_str(), boxes.size());
806 for (auto& r : boxes) {
807 r.rect.fLeft = littleRound(r.rect.fLeft);
808 r.rect.fRight = littleRound(r.rect.fRight);
809 r.rect.fTop = littleRound(r.rect.fTop);
810 r.rect.fBottom = littleRound(r.rect.fBottom);
811 SkDebugf("[%f:%f * %f:%f] %s\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom,
812 (r.direction == TextDirection::kLtr ? "left" : "right"));
813 }
814 */
815 return boxes;
816 }
817
818 // TODO: Optimize (save cluster <-> codepoint connection)
getGlyphPositionAtCoordinate(SkScalar dx,SkScalar dy)819 PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) {
820
821 if (fText.isEmpty()) {
822 return {0, Affinity::kDownstream};
823 }
824
825 ensureUTF16Mapping();
826
827 for (auto& line : fLines) {
828 // Let's figure out if we can stop looking
829 auto offsetY = line.offset().fY;
830 if (dy >= offsetY + line.height() && &line != &fLines.back()) {
831 // This line is not good enough
832 continue;
833 }
834
835 // This is so far the the line vertically closest to our coordinates
836 // (or the first one, or the only one - all the same)
837
838 auto result = line.getGlyphPositionAtCoordinate(dx);
839 //SkDebugf("getGlyphPositionAtCoordinate(%f, %f): %d %s\n", dx, dy, result.position,
840 // result.affinity == Affinity::kUpstream ? "up" : "down");
841 return result;
842 }
843
844 return {0, Affinity::kDownstream};
845 }
846
847 // Finds the first and last glyphs that define a word containing
848 // the glyph at index offset.
849 // By "glyph" they mean a character index - indicated by Minikin's code
getWordBoundary(unsigned offset)850 SkRange<size_t> ParagraphImpl::getWordBoundary(unsigned offset) {
851
852 if (fWords.empty()) {
853 if (!fUnicode->getWords(fText.c_str(), fText.size(), nullptr, &fWords)) {
854 return {0, 0 };
855 }
856 }
857
858 int32_t start = 0;
859 int32_t end = 0;
860 for (size_t i = 0; i < fWords.size(); ++i) {
861 auto word = fWords[i];
862 if (word <= offset) {
863 start = word;
864 end = word;
865 } else if (word > offset) {
866 end = word;
867 break;
868 }
869 }
870
871 //SkDebugf("getWordBoundary(%d): %d - %d\n", offset, start, end);
872 return { SkToU32(start), SkToU32(end) };
873 }
874
getLineMetrics(std::vector<LineMetrics> & metrics)875 void ParagraphImpl::getLineMetrics(std::vector<LineMetrics>& metrics) {
876 metrics.clear();
877 for (auto& line : fLines) {
878 metrics.emplace_back(line.getMetrics());
879 }
880 }
881
text(TextRange textRange)882 SkSpan<const char> ParagraphImpl::text(TextRange textRange) {
883 SkASSERT(textRange.start <= fText.size() && textRange.end <= fText.size());
884 auto start = fText.c_str() + textRange.start;
885 return SkSpan<const char>(start, textRange.width());
886 }
887
clusters(ClusterRange clusterRange)888 SkSpan<Cluster> ParagraphImpl::clusters(ClusterRange clusterRange) {
889 SkASSERT(clusterRange.start < SkToSizeT(fClusters.size()) &&
890 clusterRange.end <= SkToSizeT(fClusters.size()));
891 return SkSpan<Cluster>(&fClusters[clusterRange.start], clusterRange.width());
892 }
893
cluster(ClusterIndex clusterIndex)894 Cluster& ParagraphImpl::cluster(ClusterIndex clusterIndex) {
895 SkASSERT(clusterIndex < SkToSizeT(fClusters.size()));
896 return fClusters[clusterIndex];
897 }
898
runByCluster(ClusterIndex clusterIndex)899 Run& ParagraphImpl::runByCluster(ClusterIndex clusterIndex) {
900 auto start = cluster(clusterIndex);
901 return this->run(start.fRunIndex);
902 }
903
blocks(BlockRange blockRange)904 SkSpan<Block> ParagraphImpl::blocks(BlockRange blockRange) {
905 SkASSERT(blockRange.start < SkToSizeT(fTextStyles.size()) &&
906 blockRange.end <= SkToSizeT(fTextStyles.size()));
907 return SkSpan<Block>(&fTextStyles[blockRange.start], blockRange.width());
908 }
909
block(BlockIndex blockIndex)910 Block& ParagraphImpl::block(BlockIndex blockIndex) {
911 SkASSERT(blockIndex < SkToSizeT(fTextStyles.size()));
912 return fTextStyles[blockIndex];
913 }
914
setState(InternalState state)915 void ParagraphImpl::setState(InternalState state) {
916 if (fState <= state) {
917 fState = state;
918 return;
919 }
920
921 fState = state;
922 switch (fState) {
923 case kUnknown:
924 SkASSERT(false);
925 /*
926 // The text is immutable and so are all the text indexing properties
927 // taken from SkUnicode
928 fCodeUnitProperties.reset();
929 fWords.clear();
930 fBidiRegions.clear();
931 fUTF8IndexForUTF16Index.reset();
932 fUTF16IndexForUTF8Index.reset();
933 */
934 [[fallthrough]];
935
936 case kIndexed:
937 fRuns.clear();
938 fClusters.clear();
939 [[fallthrough]];
940
941 case kShaped:
942 fLines.clear();
943 [[fallthrough]];
944
945 case kLineBroken:
946 fPicture = nullptr;
947 [[fallthrough]];
948
949 default:
950 break;
951 }
952 }
953
computeEmptyMetrics()954 void ParagraphImpl::computeEmptyMetrics() {
955
956 // The empty metrics is used to define the height of the empty lines
957 // Unfortunately, Flutter has 2 different cases for that:
958 // 1. An empty line inside the text
959 // 2. An empty paragraph
960 // In the first case SkParagraph takes the metrics from the default paragraph style
961 // In the second case it should take it from the current text style
962 bool emptyParagraph = fRuns.empty();
963 TextStyle textStyle = paragraphStyle().getTextStyle();
964 if (emptyParagraph && !fTextStyles.empty()) {
965 textStyle = fTextStyles.back().fStyle;
966 }
967
968 auto typefaces = fontCollection()->findTypefaces(
969 textStyle.getFontFamilies(), textStyle.getFontStyle(), textStyle.getFontArguments());
970 auto typeface = typefaces.empty() ? nullptr : typefaces.front();
971
972 SkFont font(typeface, textStyle.getFontSize());
973 fEmptyMetrics = InternalLineMetrics(font, paragraphStyle().getStrutStyle().getForceStrutHeight());
974
975 if (!paragraphStyle().getStrutStyle().getForceStrutHeight() &&
976 textStyle.getHeightOverride()) {
977 const auto intrinsicHeight = fEmptyMetrics.height();
978 const auto strutHeight = textStyle.getHeight() * textStyle.getFontSize();
979 if (paragraphStyle().getStrutStyle().getHalfLeading()) {
980 fEmptyMetrics.update(
981 fEmptyMetrics.ascent(),
982 fEmptyMetrics.descent(),
983 fEmptyMetrics.leading() + strutHeight - intrinsicHeight);
984 } else {
985 const auto multiplier = strutHeight / intrinsicHeight;
986 fEmptyMetrics.update(
987 fEmptyMetrics.ascent() * multiplier,
988 fEmptyMetrics.descent() * multiplier,
989 fEmptyMetrics.leading() * multiplier);
990 }
991 }
992
993 if (emptyParagraph) {
994 // For an empty text we apply both TextHeightBehaviour flags
995 // In case of non-empty paragraph TextHeightBehaviour flags will be applied at the appropriate place
996 // We have to do it here because we skip wrapping for an empty text
997 auto disableFirstAscent = (paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableFirstAscent) == TextHeightBehavior::kDisableFirstAscent;
998 auto disableLastDescent = (paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableLastDescent) == TextHeightBehavior::kDisableLastDescent;
999 fEmptyMetrics.update(
1000 disableFirstAscent ? fEmptyMetrics.rawAscent() : fEmptyMetrics.ascent(),
1001 disableLastDescent ? fEmptyMetrics.rawDescent() : fEmptyMetrics.descent(),
1002 fEmptyMetrics.leading());
1003 }
1004
1005 if (fParagraphStyle.getStrutStyle().getStrutEnabled()) {
1006 fStrutMetrics.updateLineMetrics(fEmptyMetrics);
1007 }
1008 }
1009
getEllipsis() const1010 SkString ParagraphImpl::getEllipsis() const {
1011
1012 auto ellipsis8 = fParagraphStyle.getEllipsis();
1013 auto ellipsis16 = fParagraphStyle.getEllipsisUtf16();
1014 if (!ellipsis8.isEmpty()) {
1015 return ellipsis8;
1016 } else {
1017 return SkUnicode::convertUtf16ToUtf8(fParagraphStyle.getEllipsisUtf16());
1018 }
1019 }
1020
updateFontSize(size_t from,size_t to,SkScalar fontSize)1021 void ParagraphImpl::updateFontSize(size_t from, size_t to, SkScalar fontSize) {
1022
1023 SkASSERT(from == 0 && to == fText.size());
1024 auto defaultStyle = fParagraphStyle.getTextStyle();
1025 defaultStyle.setFontSize(fontSize);
1026 fParagraphStyle.setTextStyle(defaultStyle);
1027
1028 for (auto& textStyle : fTextStyles) {
1029 textStyle.fStyle.setFontSize(fontSize);
1030 }
1031
1032 fState = std::min(fState, kIndexed);
1033 fOldWidth = 0;
1034 fOldHeight = 0;
1035 }
1036
updateTextAlign(TextAlign textAlign)1037 void ParagraphImpl::updateTextAlign(TextAlign textAlign) {
1038 fParagraphStyle.setTextAlign(textAlign);
1039
1040 if (fState >= kLineBroken) {
1041 fState = kLineBroken;
1042 }
1043 }
1044
updateForegroundPaint(size_t from,size_t to,SkPaint paint)1045 void ParagraphImpl::updateForegroundPaint(size_t from, size_t to, SkPaint paint) {
1046 SkASSERT(from == 0 && to == fText.size());
1047 auto defaultStyle = fParagraphStyle.getTextStyle();
1048 defaultStyle.setForegroundColor(paint);
1049 fParagraphStyle.setTextStyle(defaultStyle);
1050
1051 for (auto& textStyle : fTextStyles) {
1052 textStyle.fStyle.setForegroundColor(paint);
1053 }
1054 }
1055
updateBackgroundPaint(size_t from,size_t to,SkPaint paint)1056 void ParagraphImpl::updateBackgroundPaint(size_t from, size_t to, SkPaint paint) {
1057 SkASSERT(from == 0 && to == fText.size());
1058 auto defaultStyle = fParagraphStyle.getTextStyle();
1059 defaultStyle.setBackgroundColor(paint);
1060 fParagraphStyle.setTextStyle(defaultStyle);
1061
1062 for (auto& textStyle : fTextStyles) {
1063 textStyle.fStyle.setBackgroundColor(paint);
1064 }
1065 }
1066
findPreviousGraphemeBoundary(TextIndex utf8)1067 TextIndex ParagraphImpl::findPreviousGraphemeBoundary(TextIndex utf8) {
1068 while (utf8 > 0 &&
1069 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) {
1070 --utf8;
1071 }
1072 return utf8;
1073 }
1074
findNextGraphemeBoundary(TextIndex utf8)1075 TextIndex ParagraphImpl::findNextGraphemeBoundary(TextIndex utf8) {
1076 while (utf8 < fText.size() &&
1077 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) {
1078 ++utf8;
1079 }
1080 return utf8;
1081 }
1082
findNextGlyphClusterBoundary(TextIndex utf8)1083 TextIndex ParagraphImpl::findNextGlyphClusterBoundary(TextIndex utf8) {
1084 while (utf8 < fText.size() &&
1085 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGlyphClusterStart) == 0) {
1086 ++utf8;
1087 }
1088 return utf8;
1089 }
1090
findPreviousGlyphClusterBoundary(TextIndex utf8)1091 TextIndex ParagraphImpl::findPreviousGlyphClusterBoundary(TextIndex utf8) {
1092 while (utf8 > 0 &&
1093 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGlyphClusterStart) == 0) {
1094 --utf8;
1095 }
1096 return utf8;
1097 }
1098
ensureUTF16Mapping()1099 void ParagraphImpl::ensureUTF16Mapping() {
1100 fillUTF16MappingOnce([&] {
1101 fUnicode->extractUtfConversionMapping(
1102 this->text(),
1103 [&](size_t index) { fUTF8IndexForUTF16Index.emplace_back(index); },
1104 [&](size_t index) { fUTF16IndexForUTF8Index.emplace_back(index); });
1105 });
1106 }
1107
visit(const Visitor & visitor)1108 void ParagraphImpl::visit(const Visitor& visitor) {
1109 int lineNumber = 0;
1110 for (auto& line : fLines) {
1111 line.ensureTextBlobCachePopulated();
1112 for (auto& rec : line.fTextBlobCache) {
1113 SkTextBlob::Iter iter(*rec.fBlob);
1114 SkTextBlob::Iter::ExperimentalRun run;
1115
1116 SkSTArray<128, uint32_t> clusterStorage;
1117 const Run* R = rec.fVisitor_Run;
1118 const uint32_t* clusterPtr = &R->fClusterIndexes[0];
1119
1120 if (R->fClusterStart > 0) {
1121 int count = R->fClusterIndexes.size();
1122 clusterStorage.reset(count);
1123 for (int i = 0; i < count; ++i) {
1124 clusterStorage[i] = R->fClusterStart + R->fClusterIndexes[i];
1125 }
1126 clusterPtr = &clusterStorage[0];
1127 }
1128 clusterPtr += rec.fVisitor_Pos;
1129
1130 while (iter.experimentalNext(&run)) {
1131 const Paragraph::VisitorInfo info = {
1132 run.font,
1133 rec.fOffset,
1134 rec.fClipRect.fRight,
1135 run.count,
1136 run.glyphs,
1137 run.positions,
1138 clusterPtr,
1139 0, // flags
1140 };
1141 visitor(lineNumber, &info);
1142 clusterPtr += run.count;
1143 }
1144 }
1145 visitor(lineNumber, nullptr); // signal end of line
1146 lineNumber += 1;
1147 }
1148 }
1149
1150 } // namespace textlayout
1151 } // namespace skia
1152