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/SkString.h"
9 #include "include/core/SkTypes.h"
10 #include "include/core/SkTypeface.h"
11 #include "include/private/SkTFitsIn.h"
12 #include "include/private/SkTo.h"
13 #include "modules/skparagraph/include/Metrics.h"
14 #include "modules/skparagraph/include/Paragraph.h"
15 #include "modules/skparagraph/include/ParagraphPainter.h"
16 #include "modules/skparagraph/include/ParagraphStyle.h"
17 #include "modules/skparagraph/include/TextStyle.h"
18 #include "modules/skparagraph/src/OneLineShaper.h"
19 #include "modules/skparagraph/src/ParagraphImpl.h"
20 #include "modules/skparagraph/src/ParagraphPainterImpl.h"
21 #include "modules/skparagraph/src/Run.h"
22 #include "modules/skparagraph/src/TextLine.h"
23 #include "modules/skparagraph/src/TextWrapper.h"
24 #include "src/utils/SkUTF.h"
25 #include <math.h>
26 #include <algorithm>
27 #include <utility>
28
29 namespace skia {
30 namespace textlayout {
31
32 namespace {
33 constexpr int PARAM_DOUBLE = 2;
34
littleRound(SkScalar a)35 SkScalar littleRound(SkScalar a) {
36 // This rounding is done to match Flutter tests. Must be removed..
37 auto val = std::fabs(a);
38 if (val < 10000) {
39 return SkScalarRoundToScalar(a * 100.0)/100.0;
40 } else if (val < 100000) {
41 return SkScalarRoundToScalar(a * 10.0)/10.0;
42 } else {
43 return SkScalarFloorToScalar(a);
44 }
45 }
46 } // namespace
47
operator *(const TextRange & a,const TextRange & b)48 TextRange operator*(const TextRange& a, const TextRange& b) {
49 if (a.start == b.start && a.end == b.end) return a;
50 auto begin = std::max(a.start, b.start);
51 auto end = std::min(a.end, b.end);
52 return end > begin ? TextRange(begin, end) : EMPTY_TEXT;
53 }
54
Paragraph(ParagraphStyle style,sk_sp<FontCollection> fonts)55 Paragraph::Paragraph(ParagraphStyle style, sk_sp<FontCollection> fonts)
56 : fFontCollection(std::move(fonts))
57 , fParagraphStyle(std::move(style))
58 , fAlphabeticBaseline(0)
59 , fIdeographicBaseline(0)
60 , fHeight(0)
61 , fWidth(0)
62 , fMaxIntrinsicWidth(0)
63 , fMinIntrinsicWidth(0)
64 , fLongestLine(0)
65 , fExceededMaxLines(0)
66 { }
67
ParagraphImpl(const SkString & text,ParagraphStyle style,SkTArray<Block,true> blocks,SkTArray<Placeholder,true> placeholders,sk_sp<FontCollection> fonts,std::shared_ptr<SkUnicode> unicode)68 ParagraphImpl::ParagraphImpl(const SkString& text,
69 ParagraphStyle style,
70 SkTArray<Block, true> blocks,
71 SkTArray<Placeholder, true> placeholders,
72 sk_sp<FontCollection> fonts,
73 std::shared_ptr<SkUnicode> unicode)
74 : Paragraph(std::move(style), std::move(fonts))
75 , fTextStyles(std::move(blocks))
76 , fPlaceholders(std::move(placeholders))
77 , fText(text)
78 , fState(kUnknown)
79 , fUnresolvedGlyphs(0)
80 , fPicture(nullptr)
81 , fStrutMetrics(false)
82 , fOldWidth(0)
83 , fOldHeight(0)
84 , fUnicode(std::move(unicode))
85 , fHasLineBreaks(false)
86 , fHasWhitespacesInside(false)
87 , fTrailingSpaces(0)
88 {
89 SkASSERT(fUnicode);
90 }
91
ParagraphImpl(const std::u16string & utf16text,ParagraphStyle style,SkTArray<Block,true> blocks,SkTArray<Placeholder,true> placeholders,sk_sp<FontCollection> fonts,std::shared_ptr<SkUnicode> unicode)92 ParagraphImpl::ParagraphImpl(const std::u16string& utf16text,
93 ParagraphStyle style,
94 SkTArray<Block, true> blocks,
95 SkTArray<Placeholder, true> placeholders,
96 sk_sp<FontCollection> fonts,
97 std::shared_ptr<SkUnicode> unicode)
98 : ParagraphImpl(SkString(),
99 std::move(style),
100 std::move(blocks),
101 std::move(placeholders),
102 std::move(fonts),
103 std::move(unicode))
104 {
105 SkASSERT(fUnicode);
106 fText = SkUnicode::convertUtf16ToUtf8(utf16text);
107 }
108
109 ParagraphImpl::~ParagraphImpl() = default;
110
unresolvedGlyphs()111 int32_t ParagraphImpl::unresolvedGlyphs() {
112 if (fState < kShaped) {
113 return -1;
114 }
115
116 return fUnresolvedGlyphs;
117 }
118
unresolvedCodepoints()119 std::unordered_set<SkUnichar> ParagraphImpl::unresolvedCodepoints() {
120 return fUnresolvedCodepoints;
121 }
122
addUnresolvedCodepoints(TextRange textRange)123 void ParagraphImpl::addUnresolvedCodepoints(TextRange textRange) {
124 fUnicode->forEachCodepoint(
125 &fText[textRange.start], textRange.width(),
126 [&](SkUnichar unichar, int32_t start, int32_t end, int32_t count) {
127 fUnresolvedCodepoints.emplace(unichar);
128 }
129 );
130 }
131
middleEllipsisDeal()132 void ParagraphImpl::middleEllipsisDeal()
133 {
134 isMiddleEllipsis = false;
135 const SkString& ell = this->getEllipsis();
136 const char *ellStr = ell.c_str();
137 size_t start = 0;
138 size_t end = 0;
139 if (fRuns.begin()->leftToRight()) {
140 if (ltrTextSize[0].phraseWidth >= fOldMaxWidth) {
141 fText.reset();
142 fText.set(ellStr);
143 end = 1;
144 } else {
145 scanTextCutPoint(ltrTextSize, start, end);
146 if (end) {
147 fText.remove(ltrTextSize[start].charbegin, ltrTextSize[end].charOver - ltrTextSize[start].charbegin);
148 fText.insert(ltrTextSize[start].charbegin, ellStr);
149 }
150 }
151 ltrTextSize.clear();
152 } else {
153 scanTextCutPoint(rtlTextSize, start, end);
154 if (end) {
155 fText.remove(rtlTextSize[start - 1].charbegin,
156 rtlTextSize[end + PARAM_DOUBLE].charbegin - rtlTextSize[start - 1].charbegin);
157 fText.insert(rtlTextSize[start - 1].charbegin, ellStr);
158 rtlTextSize.clear();
159 }
160 }
161
162 // end = 0 means the text does not exceed the width limit
163 if (end != 0) {
164 this->fBidiRegions.clear();
165 this->fCodeUnitProperties.reset();
166 this->fClustersIndexFromCodeUnit.reset();
167 this->computeCodeUnitProperties();
168 this->fRuns.reset();
169 this->fClusters.reset();
170 this->fClustersIndexFromCodeUnit.reset();
171 this->fClustersIndexFromCodeUnit.push_back_n(fText.size() + 1, EMPTY_INDEX);
172 this->shapeTextIntoEndlessLine();
173 }
174 this->resetContext();
175 this->resolveStrut();
176 this->computeEmptyMetrics();
177 this->fLines.reset();
178 this->breakShapedTextIntoLines(fOldMaxWidth);
179 this->fText.reset();
180 }
181
scanRTLTextCutPoint(const std::vector<TextCutRecord> & rawTextSize,size_t & start,size_t & end)182 void ParagraphImpl::scanRTLTextCutPoint(const std::vector<TextCutRecord>& rawTextSize, size_t& start, size_t& end)
183 {
184 float measureWidth = runTimeEllipsisWidth;
185 size_t left = 0;
186 size_t right = rawTextSize.size() - 1;
187 while (left < rawTextSize.size() && measureWidth < fOldMaxWidth && left <= right) {
188 measureWidth += rawTextSize[left++].phraseWidth;
189 if (right > left && measureWidth < fOldMaxWidth) {
190 measureWidth += rawTextSize[right--].phraseWidth;
191 }
192 }
193
194 if (right < left) {
195 right = left;
196 }
197
198 if (measureWidth >= fOldMaxWidth || fParagraphStyle.getTextOverflower()) {
199 start = left;
200 end = right;
201 } else {
202 start = 0;
203 end = 0;
204 }
205 }
206
scanLTRTextCutPoint(const std::vector<TextCutRecord> & rawTextSize,size_t & start,size_t & end)207 void ParagraphImpl::scanLTRTextCutPoint(const std::vector<TextCutRecord>& rawTextSize, size_t& start, size_t& end)
208 {
209 float measureWidth = runTimeEllipsisWidth;
210 size_t begin = 0;
211 size_t last = rawTextSize.size() - 1;
212 bool rightExit = false;
213 while (begin < last && !rightExit && measureWidth < fOldMaxWidth) {
214 measureWidth += rawTextSize[begin++].phraseWidth;
215 if (measureWidth > fOldMaxWidth) {
216 --begin;
217 break;
218 }
219 if (last > begin && measureWidth < fOldMaxWidth) {
220 measureWidth += rawTextSize[last--].phraseWidth;
221 if (measureWidth > fOldMaxWidth) {
222 rightExit = true;
223 ++last;
224 }
225 }
226 }
227
228 if (measureWidth >= fOldMaxWidth || fParagraphStyle.getTextOverflower()) {
229 start = begin;
230 end = last;
231 } else {
232 start = 0;
233 end = 0;
234 }
235 }
236
scanTextCutPoint(const std::vector<TextCutRecord> & rawTextSize,size_t & start,size_t & end)237 void ParagraphImpl::scanTextCutPoint(const std::vector<TextCutRecord>& rawTextSize, size_t& start, size_t& end)
238 {
239 if (allTextWidth <= fOldMaxWidth || !rawTextSize.size()) {
240 allTextWidth = 0;
241 return;
242 }
243
244 if (fRuns.begin()->leftToRight()) {
245 scanLTRTextCutPoint(rawTextSize, start, end);
246 } else {
247 scanRTLTextCutPoint(rawTextSize, start, end);
248 }
249 }
250
layout(SkScalar rawWidth)251 void ParagraphImpl::layout(SkScalar rawWidth) {
252 fLineNumber = 1;
253 // TODO: This rounding is done to match Flutter tests. Must be removed...
254 auto floorWidth = rawWidth;
255 LOGD("paragraph layout begin, addr = %{public}p, rawWidth = %{public}f, fText size = %{public}lu",
256 this, rawWidth, static_cast<unsigned long>(fText.size()));
257
258 if (fParagraphStyle.getMaxLines() == 1 &&
259 fParagraphStyle.getEllipsisMod() == EllipsisModal::MIDDLE) {
260 fOldMaxWidth = rawWidth;
261 isMiddleEllipsis = true;
262 }
263 if (getApplyRoundingHack()) {
264 floorWidth = SkScalarFloorToScalar(floorWidth);
265 }
266
267 if (fParagraphStyle.getMaxLines() == 0) {
268 fText.reset();
269 }
270
271 if ((!SkScalarIsFinite(rawWidth) || fLongestLine <= floorWidth) &&
272 fState >= kLineBroken &&
273 fLines.size() == 1 && fLines.front().ellipsis() == nullptr) {
274 // Most common case: one line of text (and one line is never justified, so no cluster shifts)
275 // We cannot mark it as kLineBroken because the new width can be bigger than the old width
276 fWidth = floorWidth;
277 fState = kShaped;
278 } else if (fState >= kLineBroken && fOldWidth != floorWidth) {
279 // We can use the results from SkShaper but have to do EVERYTHING ELSE again
280 fState = kShaped;
281 } else {
282 // Nothing changed case: we can reuse the data from the last layout
283 }
284
285 if (fState < kShaped) {
286 // Check if we have the text in the cache and don't need to shape it again
287 if (!fFontCollection->getParagraphCache()->findParagraph(this)) {
288 if (fState < kIndexed) {
289 // This only happens once at the first layout; the text is immutable
290 // and there is no reason to repeat it
291 if (this->computeCodeUnitProperties()) {
292 fState = kIndexed;
293 }
294 }
295 this->fRuns.reset();
296 this->fClusters.reset();
297 this->fClustersIndexFromCodeUnit.reset();
298 this->fClustersIndexFromCodeUnit.push_back_n(fText.size() + 1, EMPTY_INDEX);
299 if (!this->shapeTextIntoEndlessLine()) {
300 this->resetContext();
301 // TODO: merge the two next calls - they always come together
302 this->resolveStrut();
303 this->computeEmptyMetrics();
304 this->fLines.reset();
305
306 // Set the important values that are not zero
307 fWidth = floorWidth;
308 fHeight = fEmptyMetrics.height();
309 if (fParagraphStyle.getStrutStyle().getStrutEnabled() &&
310 fParagraphStyle.getStrutStyle().getForceStrutHeight()) {
311 fHeight = fStrutMetrics.height();
312 }
313 if (fParagraphStyle.getMaxLines() == 0) {
314 fHeight = 0;
315 }
316 fAlphabeticBaseline = fEmptyMetrics.alphabeticBaseline();
317 fIdeographicBaseline = fEmptyMetrics.ideographicBaseline();
318 fLongestLine = FLT_MIN - FLT_MAX; // That is what flutter has
319 fMinIntrinsicWidth = 0;
320 fMaxIntrinsicWidth = 0;
321 this->fOldWidth = floorWidth;
322 this->fOldHeight = this->fHeight;
323 LOGD("paragraph layout end, addr = %{public}p, shape failed, fHeight = %{public}f", this, fHeight);
324 return;
325 } else {
326 // Add the paragraph to the cache
327 fFontCollection->getParagraphCache()->updateParagraph(this);
328 }
329 }
330 fState = kShaped;
331 }
332
333 if (fParagraphStyle.getMaxLines() == 1 &&
334 fParagraphStyle.getEllipsisMod() == EllipsisModal::MIDDLE) {
335 middleEllipsisDeal();
336 fState = kLineBroken;
337 }
338
339 if (fState == kShaped) {
340 this->resetContext();
341 this->resolveStrut();
342 this->computeEmptyMetrics();
343 this->fLines.reset();
344 this->breakShapedTextIntoLines(floorWidth);
345 fState = kLineBroken;
346 }
347
348 if (fState == kLineBroken) {
349 // Build the picture lazily not until we actually have to paint (or never)
350 this->resetShifts();
351 this->formatLines(fWidth);
352 fState = kFormatted;
353 }
354
355 if (fParagraphStyle.getMaxLines() == 0) {
356 fHeight = 0;
357 }
358
359 this->fOldWidth = floorWidth;
360 this->fOldHeight = this->fHeight;
361
362 if (getApplyRoundingHack()) {
363 // TODO: This rounding is done to match Flutter tests. Must be removed...
364 fMinIntrinsicWidth = littleRound(fMinIntrinsicWidth);
365 fMaxIntrinsicWidth = littleRound(fMaxIntrinsicWidth);
366 }
367
368 // TODO: This is strictly Flutter thing. Must be factored out into some flutter code
369 if (fParagraphStyle.getMaxLines() == 1 ||
370 (fParagraphStyle.unlimited_lines() && fParagraphStyle.ellipsized())) {
371 fMinIntrinsicWidth = fMaxIntrinsicWidth;
372 }
373
374 // TODO: Since min and max are calculated differently it's possible to get a rounding error
375 // that would make min > max. Sort it out later, make it the same for now
376 if (fMaxIntrinsicWidth < fMinIntrinsicWidth) {
377 fMaxIntrinsicWidth = fMinIntrinsicWidth;
378 }
379
380 fLineNumber = std::max(size_t(1), fLines.size());
381 //SkDebugf("layout('%s', %f): %f %f\n", fText.c_str(), rawWidth, fMinIntrinsicWidth, fMaxIntrinsicWidth);
382 LOGD("paragraph layout end, addr = %{public}p, fLineNumber = %{public}lu, fHeight = %{public}f",
383 this, static_cast<unsigned long>(fLineNumber), fHeight);
384 }
385
paint(SkCanvas * canvas,SkScalar x,SkScalar y)386 void ParagraphImpl::paint(SkCanvas* canvas, SkScalar x, SkScalar y) {
387 CanvasParagraphPainter painter(canvas);
388 paint(&painter, x, y);
389 }
390
paint(ParagraphPainter * painter,SkScalar x,SkScalar y)391 void ParagraphImpl::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) {
392 LOGD("paragraph paint begin, addr = %{public}p", this);
393 for (auto& line : fLines) {
394 line.paint(painter, x, y);
395 }
396 LOGD("paragraph paint end, addr = %{public}p", this);
397 }
398
resetContext()399 void ParagraphImpl::resetContext() {
400 fAlphabeticBaseline = 0;
401 fHeight = 0;
402 fWidth = 0;
403 fIdeographicBaseline = 0;
404 fMaxIntrinsicWidth = 0;
405 fMinIntrinsicWidth = 0;
406 fLongestLine = 0;
407 fMaxWidthWithTrailingSpaces = 0;
408 fExceededMaxLines = false;
409 }
410
411 // shapeTextIntoEndlessLine is the thing that calls this method
computeCodeUnitProperties()412 bool ParagraphImpl::computeCodeUnitProperties() {
413
414 if (nullptr == fUnicode) {
415 return false;
416 }
417
418 // Get bidi regions
419 auto textDirection = fParagraphStyle.getTextDirection() == TextDirection::kLtr
420 ? SkUnicode::TextDirection::kLTR
421 : SkUnicode::TextDirection::kRTL;
422 if (!fUnicode->getBidiRegions(fText.c_str(), fText.size(), textDirection, &fBidiRegions)) {
423 return false;
424 }
425
426 // Collect all spaces and some extra information
427 // (and also substitute \t with a space while we are at it)
428 if (!fUnicode->computeCodeUnitFlags(&fText[0],
429 fText.size(),
430 this->paragraphStyle().getReplaceTabCharacters(),
431 &fCodeUnitProperties)) {
432 return false;
433 }
434
435 // Get some information about trailing spaces / hard line breaks
436 fTrailingSpaces = fText.size();
437 TextIndex firstWhitespace = EMPTY_INDEX;
438 for (int i = 0; i < fCodeUnitProperties.size(); ++i) {
439 auto flags = fCodeUnitProperties[i];
440 if (SkUnicode::isPartOfWhiteSpaceBreak(flags)) {
441 if (fTrailingSpaces == fText.size()) {
442 fTrailingSpaces = i;
443 }
444 if (firstWhitespace == EMPTY_INDEX) {
445 firstWhitespace = i;
446 }
447 } else {
448 fTrailingSpaces = fText.size();
449 }
450 if (SkUnicode::isHardLineBreak(flags)) {
451 fHasLineBreaks = true;
452 }
453 }
454
455 if (firstWhitespace < fTrailingSpaces) {
456 fHasWhitespacesInside = true;
457 }
458
459 return true;
460 }
461
is_ascii_7bit_space(int c)462 static bool is_ascii_7bit_space(int c) {
463 SkASSERT(c >= 0 && c <= 127);
464
465 // Extracted from https://en.wikipedia.org/wiki/Whitespace_character
466 //
467 enum WS {
468 kHT = 9,
469 kLF = 10,
470 kVT = 11,
471 kFF = 12,
472 kCR = 13,
473 kSP = 32, // too big to use as shift
474 };
475 #define M(shift) (1 << (shift))
476 constexpr uint32_t kSpaceMask = M(kHT) | M(kLF) | M(kVT) | M(kFF) | M(kCR);
477 // we check for Space (32) explicitly, since it is too large to shift
478 return (c == kSP) || (c <= 31 && (kSpaceMask & M(c)));
479 #undef M
480 }
481
Cluster(ParagraphImpl * owner,RunIndex runIndex,size_t start,size_t end,SkSpan<const char> text,SkScalar width,SkScalar height)482 Cluster::Cluster(ParagraphImpl* owner,
483 RunIndex runIndex,
484 size_t start,
485 size_t end,
486 SkSpan<const char> text,
487 SkScalar width,
488 SkScalar height)
489 : fOwner(owner)
490 , fRunIndex(runIndex)
491 , fTextRange(text.begin() - fOwner->text().begin(), text.end() - fOwner->text().begin())
492 , fGraphemeRange(EMPTY_RANGE)
493 , fStart(start)
494 , fEnd(end)
495 , fWidth(width)
496 , fHeight(height)
497 , fHalfLetterSpacing(0.0) {
498 size_t whiteSpacesBreakLen = 0;
499 size_t intraWordBreakLen = 0;
500
501 const char* ch = text.begin();
502 if (text.end() - ch == 1 && *(unsigned char*)ch <= 0x7F) {
503 // I am not even sure it's worth it if we do not save a unicode call
504 if (is_ascii_7bit_space(*ch)) {
505 ++whiteSpacesBreakLen;
506 }
507 } else {
508 for (auto i = fTextRange.start; i < fTextRange.end; ++i) {
509 if (fOwner->codeUnitHasProperty(i, SkUnicode::CodeUnitFlags::kPartOfWhiteSpaceBreak)) {
510 ++whiteSpacesBreakLen;
511 }
512 if (fOwner->codeUnitHasProperty(i, SkUnicode::CodeUnitFlags::kPartOfIntraWordBreak)) {
513 ++intraWordBreakLen;
514 }
515 }
516 }
517
518 fIsWhiteSpaceBreak = whiteSpacesBreakLen == fTextRange.width();
519 fIsIntraWordBreak = intraWordBreakLen == fTextRange.width();
520 fIsHardBreak = fOwner->codeUnitHasProperty(fTextRange.end,
521 SkUnicode::CodeUnitFlags::kHardLineBreakBefore);
522 }
523
calculateWidth(size_t start,size_t end,bool clip) const524 SkScalar Run::calculateWidth(size_t start, size_t end, bool clip) const {
525 SkASSERT(start <= end);
526 // clip |= end == size(); // Clip at the end of the run?
527 auto correction = 0.0f;
528 if (end > start && !fJustificationShifts.empty()) {
529 // This is not a typo: we are using Point as a pair of SkScalars
530 correction = fJustificationShifts[end - 1].fX -
531 fJustificationShifts[start].fY;
532 }
533 return posX(end) - posX(start) + correction;
534 }
535
536 // In some cases we apply spacing to glyphs first and then build the cluster table, in some we do
537 // the opposite - just to optimize the most common case.
applySpacingAndBuildClusterTable()538 void ParagraphImpl::applySpacingAndBuildClusterTable() {
539
540 // Check all text styles to see what we have to do (if anything)
541 size_t letterSpacingStyles = 0;
542 bool hasWordSpacing = false;
543 for (auto& block : fTextStyles) {
544 if (block.fRange.width() > 0) {
545 if (!SkScalarNearlyZero(block.fStyle.getLetterSpacing())) {
546 ++letterSpacingStyles;
547 }
548 if (!SkScalarNearlyZero(block.fStyle.getWordSpacing())) {
549 hasWordSpacing = true;
550 }
551 }
552 }
553
554 if (letterSpacingStyles == 0 && !hasWordSpacing) {
555 // We don't have to do anything about spacing (most common case)
556 this->buildClusterTable();
557 return;
558 }
559
560 if (letterSpacingStyles == 1 && !hasWordSpacing && fTextStyles.size() == 1 &&
561 fTextStyles[0].fRange.width() == fText.size() && fRuns.size() == 1) {
562 // We have to letter space the entire paragraph (second most common case)
563 auto& run = fRuns[0];
564 auto& style = fTextStyles[0].fStyle;
565 run.addSpacesEvenly(style.getLetterSpacing());
566 this->buildClusterTable();
567 // This is something Flutter requires
568 for (auto& cluster : fClusters) {
569 cluster.setHalfLetterSpacing(style.getLetterSpacing()/2);
570 }
571 return;
572 }
573
574 // The complex case: many text styles with spacing (possibly not adjusted to glyphs)
575 this->buildClusterTable();
576
577 // Walk through all the clusters in the direction of shaped text
578 // (we have to walk through the styles in the same order, too)
579 SkScalar shift = 0;
580 for (auto& run : fRuns) {
581
582 // Skip placeholder runs
583 if (run.isPlaceholder()) {
584 continue;
585 }
586 bool soFarWhitespacesOnly = true;
587 bool wordSpacingPending = false;
588 Cluster* lastSpaceCluster = nullptr;
589 run.iterateThroughClusters([this, &run, &shift, &soFarWhitespacesOnly, &wordSpacingPending, &lastSpaceCluster](Cluster* cluster) {
590 // Shift the cluster (shift collected from the previous clusters)
591 run.shift(cluster, shift);
592
593 // Synchronize styles (one cluster can be covered by few styles)
594 Block* currentStyle = fTextStyles.begin();
595 while (!cluster->startsIn(currentStyle->fRange)) {
596 currentStyle++;
597 SkASSERT(currentStyle != fTextStyles.end());
598 }
599
600 SkASSERT(!currentStyle->fStyle.isPlaceholder());
601
602 // Process word spacing
603 if (currentStyle->fStyle.getWordSpacing() != 0) {
604 if (cluster->isWhitespaceBreak() && cluster->isSoftBreak()) {
605 if (!soFarWhitespacesOnly) {
606 lastSpaceCluster = cluster;
607 wordSpacingPending = true;
608 }
609 } else if (wordSpacingPending) {
610 SkScalar spacing = currentStyle->fStyle.getWordSpacing();
611 run.addSpacesAtTheEnd(spacing, lastSpaceCluster);
612 run.shift(cluster, spacing);
613 shift += spacing;
614 wordSpacingPending = false;
615 }
616 }
617 // Process letter spacing
618 if (currentStyle->fStyle.getLetterSpacing() != 0) {
619 shift += run.addSpacesEvenly(currentStyle->fStyle.getLetterSpacing(), cluster);
620 }
621
622 if (soFarWhitespacesOnly && !cluster->isWhitespaceBreak()) {
623 soFarWhitespacesOnly = false;
624 }
625 });
626 }
627 }
628
middleEllipsisAddText(size_t charStart,size_t charEnd,SkScalar & allTextWidth,SkScalar width,bool isLeftToRight)629 void ParagraphImpl::middleEllipsisAddText(size_t charStart,
630 size_t charEnd,
631 SkScalar& allTextWidth,
632 SkScalar width,
633 bool isLeftToRight) {
634 if (isMiddleEllipsis) {
635 TextCutRecord textCount;
636 textCount.charbegin = charStart;
637 textCount.charOver = charEnd;
638 textCount.phraseWidth = width;
639 allTextWidth += width;
640 if (isLeftToRight) {
641 this->ltrTextSize.emplace_back(textCount);
642 } else {
643 this->rtlTextSize.emplace_back(textCount);
644 }
645 }
646 }
647
setRunTimeEllipsisWidthForMiddleEllipsis()648 void ParagraphImpl::setRunTimeEllipsisWidthForMiddleEllipsis() {
649 if (isMiddleEllipsis && fParagraphStyle.getEllipsisMod() == EllipsisModal::MIDDLE) {
650 float ellipsisWidth = 0.0;
651 TextLine textLine;
652 const SkString& ellipsis = this->getEllipsis();
653 std::unique_ptr<Run> ellipsisRun;
654 textLine.setParagraphImpl(this);
655 ellipsisRun = textLine.shapeEllipsis(ellipsis, &this->cluster(0));
656 runTimeEllipsisWidth = ellipsisRun->fAdvanceX();
657 ellipsisWidth = ellipsisRun->fAdvanceX();
658 ellipsisRun.reset();
659 }
660 }
661
662 // Clusters in the order of the input text
buildClusterTable()663 void ParagraphImpl::buildClusterTable() {
664 // It's possible that one grapheme includes few runs; we cannot handle it
665 // so we break graphemes by the runs instead
666 // It's not the ideal solution and has to be revisited later
667 int cluster_count = 1;
668 for (auto& run : fRuns) {
669 cluster_count += run.isPlaceholder() ? 1 : run.size();
670 fCodeUnitProperties[run.fTextRange.start] |= SkUnicode::CodeUnitFlags::kGraphemeStart;
671 fCodeUnitProperties[run.fTextRange.start] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
672 }
673 if (!fRuns.empty()) {
674 fCodeUnitProperties[fRuns.back().textRange().end] |= SkUnicode::CodeUnitFlags::kGraphemeStart;
675 fCodeUnitProperties[fRuns.back().textRange().end] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
676 }
677 fClusters.reserve_back(fClusters.size() + cluster_count);
678
679 // Walk through all the run in the direction of input text
680 for (auto& run : fRuns) {
681 auto runIndex = run.index();
682 auto runStart = fClusters.size();
683 if (run.isPlaceholder()) {
684 // Add info to cluster indexes table (text -> cluster)
685 for (auto i = run.textRange().start; i < run.textRange().end; ++i) {
686 fClustersIndexFromCodeUnit[i] = fClusters.size();
687 }
688 // There are no glyphs but we want to have one cluster
689 fClusters.emplace_back(this, runIndex, 0ul, 1ul, this->text(run.textRange()), run.advance().fX, run.advance().fY);
690 fCodeUnitProperties[run.textRange().start] |= SkUnicode::CodeUnitFlags::kSoftLineBreakBefore;
691 fCodeUnitProperties[run.textRange().end] |= SkUnicode::CodeUnitFlags::kSoftLineBreakBefore;
692 } else {
693 // Walk through the glyph in the direction of input text
694 run.iterateThroughClustersInTextOrder([&run, runIndex, this](size_t glyphStart,
695 size_t glyphEnd,
696 size_t charStart,
697 size_t charEnd,
698 SkScalar width,
699 SkScalar height) {
700 SkASSERT(charEnd >= charStart);
701 // Add info to cluster indexes table (text -> cluster)
702 for (auto i = charStart; i < charEnd; ++i) {
703 fClustersIndexFromCodeUnit[i] = fClusters.size();
704 }
705
706 middleEllipsisAddText(charStart, charEnd, allTextWidth, width, run.leftToRight());
707 SkSpan<const char> text(fText.c_str() + charStart, charEnd - charStart);
708 fClusters.emplace_back(this, runIndex, glyphStart, glyphEnd, text, width, height);
709 fCodeUnitProperties[charStart] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
710 });
711 }
712 fCodeUnitProperties[run.textRange().start] |= SkUnicode::CodeUnitFlags::kGlyphClusterStart;
713
714 run.setClusterRange(runStart, fClusters.size());
715 fMaxIntrinsicWidth += run.advance().fX;
716 }
717 fClustersIndexFromCodeUnit[fText.size()] = fClusters.size();
718 fClusters.emplace_back(this, EMPTY_RUN, 0, 0, this->text({fText.size(), fText.size()}), 0, 0);
719 setRunTimeEllipsisWidthForMiddleEllipsis();
720 }
721
shapeTextIntoEndlessLine()722 bool ParagraphImpl::shapeTextIntoEndlessLine() {
723
724 if (fText.size() == 0) {
725 return false;
726 }
727
728 fUnresolvedCodepoints.clear();
729 fFontSwitches.reset();
730
731 OneLineShaper oneLineShaper(this);
732 auto result = oneLineShaper.shape();
733 fUnresolvedGlyphs = oneLineShaper.unresolvedGlyphs();
734
735 this->applySpacingAndBuildClusterTable();
736
737 return result;
738 }
739
setIndents(const std::vector<SkScalar> & indents)740 void ParagraphImpl::setIndents(const std::vector<SkScalar>& indents)
741 {
742 fIndents = indents;
743 }
744
detectIndents(size_t index)745 SkScalar ParagraphImpl::detectIndents(size_t index)
746 {
747 SkScalar indent = 0.0;
748 if (fIndents.size() > 0 && index < fIndents.size()) {
749 indent = fIndents[index];
750 } else {
751 indent = fIndents.size() > 0 ? fIndents.back() : 0.0;
752 }
753
754 return indent;
755 }
756
breakShapedTextIntoLines(SkScalar maxWidth)757 void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) {
758
759 if (!fHasLineBreaks &&
760 !fHasWhitespacesInside &&
761 fPlaceholders.size() == 1 &&
762 fRuns.size() == 1 && fRuns[0].fAdvance.fX <= maxWidth - (this->detectIndents(std::numeric_limits<size_t>::max()))) {
763 // This is a short version of a line breaking when we know that:
764 // 1. We have only one line of text
765 // 2. It's shaped into a single run
766 // 3. There are no placeholders
767 // 4. There are no linebreaks (which will format text into multiple lines)
768 // 5. There are no whitespaces so the minIntrinsicWidth=maxIntrinsicWidth
769 // (To think about that, the last condition is not quite right;
770 // we should calculate minIntrinsicWidth by soft line breaks.
771 // However, it's how it's done in Flutter now)
772 auto& run = this->fRuns[0];
773 auto advance = run.advance();
774 auto textRange = TextRange(0, this->text().size());
775 auto textExcludingSpaces = TextRange(0, fTrailingSpaces);
776 InternalLineMetrics metrics(this->strutForceHeight());
777 metrics.add(&run);
778 auto disableFirstAscent = this->paragraphStyle().getTextHeightBehavior() &
779 TextHeightBehavior::kDisableFirstAscent;
780 auto disableLastDescent = this->paragraphStyle().getTextHeightBehavior() &
781 TextHeightBehavior::kDisableLastDescent;
782 if (disableFirstAscent) {
783 metrics.fAscent = metrics.fRawAscent;
784 }
785 if (disableLastDescent) {
786 metrics.fDescent = metrics.fRawDescent;
787 }
788 if (this->strutEnabled()) {
789 this->strutMetrics().updateLineMetrics(metrics);
790 }
791 ClusterIndex trailingSpaces = fClusters.size();
792 do {
793 --trailingSpaces;
794 auto& cluster = fClusters[trailingSpaces];
795 if (!cluster.isWhitespaceBreak()) {
796 ++trailingSpaces;
797 break;
798 }
799 advance.fX -= cluster.width();
800 } while (trailingSpaces != 0);
801
802 advance.fY = metrics.height();
803 auto clusterRange = ClusterRange(0, trailingSpaces);
804 auto clusterRangeWithGhosts = ClusterRange(0, this->clusters().size() - 1);
805
806 TextAlign align = fParagraphStyle.effective_align();
807 SkScalar offsetX = (align == TextAlign::kLeft || align == TextAlign::kJustify) ?
808 this->detectIndents(0) : 0.0f;
809 this->addLine(SkPoint::Make(offsetX, 0), advance,
810 textExcludingSpaces, textRange, textRange,
811 clusterRange, clusterRangeWithGhosts, run.advance().x(),
812 metrics);
813
814 fLongestLine = std::max(run.advance().fX, advance.fX);
815 fHeight = advance.fY;
816 fWidth = maxWidth;
817 fMaxIntrinsicWidth = run.advance().fX;
818 fMinIntrinsicWidth = advance.fX;
819 fAlphabeticBaseline = fLines.empty() ? fEmptyMetrics.alphabeticBaseline() : fLines.front().alphabeticBaseline();
820 fIdeographicBaseline = fLines.empty() ? fEmptyMetrics.ideographicBaseline() : fLines.front().ideographicBaseline();
821 fExceededMaxLines = false;
822 return;
823 }
824
825 TextWrapper textWrapper;
826 textWrapper.breakTextIntoLines(
827 this,
828 maxWidth,
829 [&](TextRange textExcludingSpaces,
830 TextRange text,
831 TextRange textWithNewlines,
832 ClusterRange clusters,
833 ClusterRange clustersWithGhosts,
834 SkScalar widthWithSpaces,
835 size_t startPos,
836 size_t endPos,
837 SkVector offset,
838 SkVector advance,
839 InternalLineMetrics metrics,
840 bool addEllipsis) {
841 // TODO: Take in account clipped edges
842 auto& line = this->addLine(offset, advance, textExcludingSpaces, text, textWithNewlines, clusters, clustersWithGhosts, widthWithSpaces, metrics);
843 if (addEllipsis && this->paragraphStyle().getEllipsisMod() == EllipsisModal::TAIL) {
844 line.createEllipsis(maxWidth, this->getEllipsis(), true, this->getWordBreakType());
845 } else if (addEllipsis && this->paragraphStyle().getEllipsisMod() == EllipsisModal::HEAD) {
846 line.createHeadEllipsis(maxWidth, this->getEllipsis(), true);
847 }
848 fLongestLine = std::max(fLongestLine, std::max(advance.fX, widthWithSpaces));
849 });
850
851 fHeight = textWrapper.height();
852 fWidth = maxWidth;
853 fMaxIntrinsicWidth = textWrapper.maxIntrinsicWidth();
854 fMinIntrinsicWidth = textWrapper.minIntrinsicWidth();
855 fAlphabeticBaseline = fLines.empty() ? fEmptyMetrics.alphabeticBaseline() : fLines.front().alphabeticBaseline();
856 fIdeographicBaseline = fLines.empty() ? fEmptyMetrics.ideographicBaseline() : fLines.front().ideographicBaseline();
857 fExceededMaxLines = textWrapper.exceededMaxLines();
858 }
859
formatLines(SkScalar maxWidth)860 void ParagraphImpl::formatLines(SkScalar maxWidth) {
861 auto effectiveAlign = fParagraphStyle.effective_align();
862 const bool isLeftAligned = effectiveAlign == TextAlign::kLeft
863 || (effectiveAlign == TextAlign::kJustify && fParagraphStyle.getTextDirection() == TextDirection::kLtr);
864
865 if (!SkScalarIsFinite(maxWidth) && !isLeftAligned) {
866 // Special case: clean all text in case of maxWidth == INF & align != left
867 // We had to go through shaping though because we need all the measurement numbers
868 fLines.reset();
869 return;
870 }
871
872 for (auto& line : fLines) {
873 line.format(effectiveAlign, maxWidth);
874 }
875 }
876
resolveStrut()877 void ParagraphImpl::resolveStrut() {
878 auto strutStyle = this->paragraphStyle().getStrutStyle();
879 if (!strutStyle.getStrutEnabled() || strutStyle.getFontSize() < 0) {
880 return;
881 }
882
883 std::vector<sk_sp<SkTypeface>> typefaces = fFontCollection->findTypefaces(strutStyle.getFontFamilies(), strutStyle.getFontStyle(), std::nullopt);
884 if (typefaces.empty()) {
885 SkDEBUGF("Could not resolve strut font\n");
886 return;
887 }
888
889 SkFont font(typefaces.front(), strutStyle.getFontSize());
890 SkFontMetrics metrics;
891 font.getMetrics(&metrics);
892
893 if (strutStyle.getHeightOverride()) {
894 auto strutHeight = metrics.fDescent - metrics.fAscent;
895 auto strutMultiplier = strutStyle.getHeight() * strutStyle.getFontSize();
896 fStrutMetrics = InternalLineMetrics(
897 (metrics.fAscent / strutHeight) * strutMultiplier,
898 (metrics.fDescent / strutHeight) * strutMultiplier,
899 strutStyle.getLeading() < 0 ? 0 : strutStyle.getLeading() * strutStyle.getFontSize(),
900 metrics.fAscent, metrics.fDescent, metrics.fLeading);
901 } else {
902 fStrutMetrics = InternalLineMetrics(
903 metrics.fAscent,
904 metrics.fDescent,
905 strutStyle.getLeading() < 0 ? 0 : strutStyle.getLeading() * strutStyle.getFontSize());
906 }
907 fStrutMetrics.setForceStrut(this->paragraphStyle().getStrutStyle().getForceStrutHeight());
908 }
909
findAllBlocks(TextRange textRange)910 BlockRange ParagraphImpl::findAllBlocks(TextRange textRange) {
911 BlockIndex begin = EMPTY_BLOCK;
912 BlockIndex end = EMPTY_BLOCK;
913 for (int index = 0; index < fTextStyles.size(); ++index) {
914 auto& block = fTextStyles[index];
915 if (block.fRange.end <= textRange.start) {
916 continue;
917 }
918 if (block.fRange.start >= textRange.end) {
919 break;
920 }
921 if (begin == EMPTY_BLOCK) {
922 begin = index;
923 }
924 end = index;
925 }
926
927 if (begin == EMPTY_INDEX || end == EMPTY_INDEX) {
928 // It's possible if some text is not covered with any text style
929 // Not in Flutter but in direct use of SkParagraph
930 return EMPTY_RANGE;
931 }
932
933 return { begin, end + 1 };
934 }
935
addLine(SkVector offset,SkVector advance,TextRange textExcludingSpaces,TextRange text,TextRange textIncludingNewLines,ClusterRange clusters,ClusterRange clustersWithGhosts,SkScalar widthWithSpaces,InternalLineMetrics sizes)936 TextLine& ParagraphImpl::addLine(SkVector offset,
937 SkVector advance,
938 TextRange textExcludingSpaces,
939 TextRange text,
940 TextRange textIncludingNewLines,
941 ClusterRange clusters,
942 ClusterRange clustersWithGhosts,
943 SkScalar widthWithSpaces,
944 InternalLineMetrics sizes) {
945 // Define a list of styles that covers the line
946 auto blocks = findAllBlocks(textExcludingSpaces);
947 return fLines.emplace_back(this, offset, advance, blocks,
948 textExcludingSpaces, text, textIncludingNewLines,
949 clusters, clustersWithGhosts, widthWithSpaces, sizes);
950 }
951
952 // Returns a vector of bounding boxes that enclose all text between
953 // start and end glyph indexes, including start and excluding end
getRectsForRange(unsigned start,unsigned end,RectHeightStyle rectHeightStyle,RectWidthStyle rectWidthStyle)954 std::vector<TextBox> ParagraphImpl::getRectsForRange(unsigned start,
955 unsigned end,
956 RectHeightStyle rectHeightStyle,
957 RectWidthStyle rectWidthStyle) {
958 std::vector<TextBox> results;
959 if (fText.isEmpty()) {
960 if (start == 0 && end > 0) {
961 // On account of implied "\n" that is always at the end of the text
962 //SkDebugf("getRectsForRange(%d, %d): %f\n", start, end, fHeight);
963 results.emplace_back(SkRect::MakeXYWH(0, 0, 0, fHeight), fParagraphStyle.getTextDirection());
964 }
965 return results;
966 }
967
968 this->ensureUTF16Mapping();
969
970 if (start >= end || start > SkToSizeT(fUTF8IndexForUTF16Index.size()) || end == 0) {
971 return results;
972 }
973
974 // Adjust the text to grapheme edges
975 // Apparently, text editor CAN move inside graphemes but CANNOT select a part of it.
976 // I don't know why - the solution I have here returns an empty box for every query that
977 // does not contain an end of a grapheme.
978 // Once a cursor is inside a complex grapheme I can press backspace and cause trouble.
979 // To avoid any problems, I will not allow any selection of a part of a grapheme.
980 // One flutter test fails because of it but the editing experience is correct
981 // (although you have to press the cursor many times before it moves to the next grapheme).
982 TextRange text(fText.size(), fText.size());
983 // TODO: This is probably a temp change that makes SkParagraph work as TxtLib
984 // (so we can compare the results). We now include in the selection box only the graphemes
985 // that belongs to the given [start:end) range entirely (not the ones that intersect with it)
986 if (start < SkToSizeT(fUTF8IndexForUTF16Index.size())) {
987 auto utf8 = fUTF8IndexForUTF16Index[start];
988 // If start points to a trailing surrogate, skip it
989 if (start > 0 && fUTF8IndexForUTF16Index[start - 1] == utf8) {
990 utf8 = fUTF8IndexForUTF16Index[start + 1];
991 }
992 text.start = this->findNextGraphemeBoundary(utf8);
993 }
994 if (end < SkToSizeT(fUTF8IndexForUTF16Index.size())) {
995 auto utf8 = this->findPreviousGraphemeBoundary(fUTF8IndexForUTF16Index[end]);
996 text.end = utf8;
997 }
998 //SkDebugf("getRectsForRange(%d,%d) -> (%d:%d)\n", start, end, text.start, text.end);
999 for (auto& line : fLines) {
1000 auto lineText = line.textWithNewlines();
1001 auto intersect = lineText * text;
1002 if (intersect.empty() && lineText.start != text.start) {
1003 continue;
1004 }
1005
1006 line.getRectsForRange(intersect, rectHeightStyle, rectWidthStyle, results);
1007 }
1008 /*
1009 SkDebugf("getRectsForRange(%d, %d)\n", start, end);
1010 for (auto& r : results) {
1011 r.rect.fLeft = littleRound(r.rect.fLeft);
1012 r.rect.fRight = littleRound(r.rect.fRight);
1013 r.rect.fTop = littleRound(r.rect.fTop);
1014 r.rect.fBottom = littleRound(r.rect.fBottom);
1015 SkDebugf("[%f:%f * %f:%f]\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom);
1016 }
1017 */
1018 return results;
1019 }
1020
getRectsForPlaceholders()1021 std::vector<TextBox> ParagraphImpl::getRectsForPlaceholders() {
1022 std::vector<TextBox> boxes;
1023 if (fText.isEmpty()) {
1024 return boxes;
1025 }
1026 if (fPlaceholders.size() == 1) {
1027 // We always have one fake placeholder
1028 return boxes;
1029 }
1030 for (auto& line : fLines) {
1031 line.getRectsForPlaceholders(boxes);
1032 }
1033 /*
1034 SkDebugf("getRectsForPlaceholders('%s'): %d\n", fText.c_str(), boxes.size());
1035 for (auto& r : boxes) {
1036 r.rect.fLeft = littleRound(r.rect.fLeft);
1037 r.rect.fRight = littleRound(r.rect.fRight);
1038 r.rect.fTop = littleRound(r.rect.fTop);
1039 r.rect.fBottom = littleRound(r.rect.fBottom);
1040 SkDebugf("[%f:%f * %f:%f] %s\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom,
1041 (r.direction == TextDirection::kLtr ? "left" : "right"));
1042 }
1043 */
1044 return boxes;
1045 }
1046
1047 // TODO: Optimize (save cluster <-> codepoint connection)
getGlyphPositionAtCoordinate(SkScalar dx,SkScalar dy)1048 PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) {
1049
1050 if (fText.isEmpty()) {
1051 return {0, Affinity::kDownstream};
1052 }
1053
1054 this->ensureUTF16Mapping();
1055
1056 for (auto& line : fLines) {
1057 // Let's figure out if we can stop looking
1058 auto offsetY = line.offset().fY;
1059 if (dy >= offsetY + line.height() && &line != &fLines.back()) {
1060 // This line is not good enough
1061 continue;
1062 }
1063
1064 // This is so far the the line vertically closest to our coordinates
1065 // (or the first one, or the only one - all the same)
1066
1067 auto result = line.getGlyphPositionAtCoordinate(dx);
1068 //SkDebugf("getGlyphPositionAtCoordinate(%f, %f): %d %s\n", dx, dy, result.position,
1069 // result.affinity == Affinity::kUpstream ? "up" : "down");
1070 return result;
1071 }
1072
1073 return {0, Affinity::kDownstream};
1074 }
1075
1076 // Finds the first and last glyphs that define a word containing
1077 // the glyph at index offset.
1078 // By "glyph" they mean a character index - indicated by Minikin's code
getWordBoundary(unsigned offset)1079 SkRange<size_t> ParagraphImpl::getWordBoundary(unsigned offset) {
1080
1081 if (fWords.empty()) {
1082 if (!fUnicode->getWords(fText.c_str(), fText.size(), nullptr, &fWords)) {
1083 return {0, 0 };
1084 }
1085 }
1086
1087 int32_t start = 0;
1088 int32_t end = 0;
1089 for (size_t i = 0; i < fWords.size(); ++i) {
1090 auto word = fWords[i];
1091 if (word <= offset) {
1092 start = word;
1093 end = word;
1094 } else if (word > offset) {
1095 end = word;
1096 break;
1097 }
1098 }
1099
1100 //SkDebugf("getWordBoundary(%d): %d - %d\n", offset, start, end);
1101 return { SkToU32(start), SkToU32(end) };
1102 }
1103
getLineMetrics(std::vector<LineMetrics> & metrics)1104 void ParagraphImpl::getLineMetrics(std::vector<LineMetrics>& metrics) {
1105 metrics.clear();
1106 for (auto& line : fLines) {
1107 metrics.emplace_back(line.getMetrics());
1108 }
1109 }
1110
text(TextRange textRange)1111 SkSpan<const char> ParagraphImpl::text(TextRange textRange) {
1112 SkASSERT(textRange.start <= fText.size() && textRange.end <= fText.size());
1113 auto start = fText.c_str() + textRange.start;
1114 return SkSpan<const char>(start, textRange.width());
1115 }
1116
clusters(ClusterRange clusterRange)1117 SkSpan<Cluster> ParagraphImpl::clusters(ClusterRange clusterRange) {
1118 SkASSERT(clusterRange.start < SkToSizeT(fClusters.size()) &&
1119 clusterRange.end <= SkToSizeT(fClusters.size()));
1120 return SkSpan<Cluster>(&fClusters[clusterRange.start], clusterRange.width());
1121 }
1122
cluster(ClusterIndex clusterIndex)1123 Cluster& ParagraphImpl::cluster(ClusterIndex clusterIndex) {
1124 SkASSERT(clusterIndex < SkToSizeT(fClusters.size()));
1125 return fClusters[clusterIndex];
1126 }
1127
runByCluster(ClusterIndex clusterIndex)1128 Run& ParagraphImpl::runByCluster(ClusterIndex clusterIndex) {
1129 auto start = cluster(clusterIndex);
1130 return this->run(start.fRunIndex);
1131 }
1132
blocks(BlockRange blockRange)1133 SkSpan<Block> ParagraphImpl::blocks(BlockRange blockRange) {
1134 SkASSERT(blockRange.start < SkToSizeT(fTextStyles.size()) &&
1135 blockRange.end <= SkToSizeT(fTextStyles.size()));
1136 return SkSpan<Block>(&fTextStyles[blockRange.start], blockRange.width());
1137 }
1138
block(BlockIndex blockIndex)1139 Block& ParagraphImpl::block(BlockIndex blockIndex) {
1140 SkASSERT(blockIndex < SkToSizeT(fTextStyles.size()));
1141 return fTextStyles[blockIndex];
1142 }
1143
setState(InternalState state)1144 void ParagraphImpl::setState(InternalState state) {
1145 if (fState <= state) {
1146 fState = state;
1147 return;
1148 }
1149
1150 fState = state;
1151 switch (fState) {
1152 case kUnknown:
1153 SkASSERT(false);
1154 /*
1155 // The text is immutable and so are all the text indexing properties
1156 // taken from SkUnicode
1157 fCodeUnitProperties.reset();
1158 fWords.clear();
1159 fBidiRegions.clear();
1160 fUTF8IndexForUTF16Index.reset();
1161 fUTF16IndexForUTF8Index.reset();
1162 */
1163 [[fallthrough]];
1164
1165 case kIndexed:
1166 fRuns.reset();
1167 fClusters.reset();
1168 [[fallthrough]];
1169
1170 case kShaped:
1171 fLines.reset();
1172 [[fallthrough]];
1173
1174 case kLineBroken:
1175 fPicture = nullptr;
1176 [[fallthrough]];
1177
1178 default:
1179 break;
1180 }
1181 }
1182
computeEmptyMetrics()1183 void ParagraphImpl::computeEmptyMetrics() {
1184
1185 // The empty metrics is used to define the height of the empty lines
1186 // Unfortunately, Flutter has 2 different cases for that:
1187 // 1. An empty line inside the text
1188 // 2. An empty paragraph
1189 // In the first case SkParagraph takes the metrics from the default paragraph style
1190 // In the second case it should take it from the current text style
1191 bool emptyParagraph = fRuns.empty();
1192 TextStyle textStyle = paragraphStyle().getTextStyle();
1193 if (emptyParagraph && !fTextStyles.empty()) {
1194 textStyle = fTextStyles.back().fStyle;
1195 }
1196
1197 auto typefaces = fontCollection()->findTypefaces(
1198 textStyle.getFontFamilies(), textStyle.getFontStyle(), textStyle.getFontArguments());
1199 auto typeface = typefaces.empty() ? nullptr : typefaces.front();
1200
1201 SkFont font(typeface, textStyle.getFontSize());
1202 fEmptyMetrics = InternalLineMetrics(font, paragraphStyle().getStrutStyle().getForceStrutHeight());
1203
1204 if (!paragraphStyle().getStrutStyle().getForceStrutHeight() &&
1205 textStyle.getHeightOverride()) {
1206 const auto intrinsicHeight = fEmptyMetrics.height();
1207 const auto strutHeight = textStyle.getHeight() * textStyle.getFontSize();
1208 if (paragraphStyle().getStrutStyle().getHalfLeading()) {
1209 fEmptyMetrics.update(
1210 fEmptyMetrics.ascent(),
1211 fEmptyMetrics.descent(),
1212 fEmptyMetrics.leading() + strutHeight - intrinsicHeight);
1213 } else {
1214 const auto multiplier = strutHeight / intrinsicHeight;
1215 fEmptyMetrics.update(
1216 fEmptyMetrics.ascent() * multiplier,
1217 fEmptyMetrics.descent() * multiplier,
1218 fEmptyMetrics.leading() * multiplier);
1219 }
1220 }
1221
1222 if (emptyParagraph) {
1223 // For an empty text we apply both TextHeightBehaviour flags
1224 // In case of non-empty paragraph TextHeightBehaviour flags will be applied at the appropriate place
1225 // We have to do it here because we skip wrapping for an empty text
1226 auto disableFirstAscent = (paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableFirstAscent) == TextHeightBehavior::kDisableFirstAscent;
1227 auto disableLastDescent = (paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableLastDescent) == TextHeightBehavior::kDisableLastDescent;
1228 fEmptyMetrics.update(
1229 disableFirstAscent ? fEmptyMetrics.rawAscent() : fEmptyMetrics.ascent(),
1230 disableLastDescent ? fEmptyMetrics.rawDescent() : fEmptyMetrics.descent(),
1231 fEmptyMetrics.leading());
1232 }
1233
1234 if (fParagraphStyle.getStrutStyle().getStrutEnabled()) {
1235 fStrutMetrics.updateLineMetrics(fEmptyMetrics);
1236 }
1237 }
1238
getEllipsis() const1239 SkString ParagraphImpl::getEllipsis() const {
1240
1241 auto ellipsis8 = fParagraphStyle.getEllipsis();
1242 auto ellipsis16 = fParagraphStyle.getEllipsisUtf16();
1243 if (!ellipsis8.isEmpty()) {
1244 return ellipsis8;
1245 } else {
1246 return SkUnicode::convertUtf16ToUtf8(fParagraphStyle.getEllipsisUtf16());
1247 }
1248 }
1249
getWordBreakType() const1250 WordBreakType ParagraphImpl::getWordBreakType() const {
1251 return fParagraphStyle.getStrutStyle().getWordBreakType();
1252 }
1253
updateFontSize(size_t from,size_t to,SkScalar fontSize)1254 void ParagraphImpl::updateFontSize(size_t from, size_t to, SkScalar fontSize) {
1255
1256 SkASSERT(from == 0 && to == fText.size());
1257 auto defaultStyle = fParagraphStyle.getTextStyle();
1258 defaultStyle.setFontSize(fontSize);
1259 fParagraphStyle.setTextStyle(defaultStyle);
1260
1261 for (auto& textStyle : fTextStyles) {
1262 textStyle.fStyle.setFontSize(fontSize);
1263 }
1264
1265 fState = std::min(fState, kIndexed);
1266 fOldWidth = 0;
1267 fOldHeight = 0;
1268 }
1269
updateTextAlign(TextAlign textAlign)1270 void ParagraphImpl::updateTextAlign(TextAlign textAlign) {
1271 fParagraphStyle.setTextAlign(textAlign);
1272
1273 if (fState >= kLineBroken) {
1274 fState = kLineBroken;
1275 }
1276 }
1277
updateForegroundPaint(size_t from,size_t to,SkPaint paint)1278 void ParagraphImpl::updateForegroundPaint(size_t from, size_t to, SkPaint paint) {
1279 SkASSERT(from == 0 && to == fText.size());
1280 auto defaultStyle = fParagraphStyle.getTextStyle();
1281 defaultStyle.setForegroundColor(paint);
1282 fParagraphStyle.setTextStyle(defaultStyle);
1283
1284 for (auto& textStyle : fTextStyles) {
1285 textStyle.fStyle.setForegroundColor(paint);
1286 }
1287 }
1288
updateBackgroundPaint(size_t from,size_t to,SkPaint paint)1289 void ParagraphImpl::updateBackgroundPaint(size_t from, size_t to, SkPaint paint) {
1290 SkASSERT(from == 0 && to == fText.size());
1291 auto defaultStyle = fParagraphStyle.getTextStyle();
1292 defaultStyle.setBackgroundColor(paint);
1293 fParagraphStyle.setTextStyle(defaultStyle);
1294
1295 for (auto& textStyle : fTextStyles) {
1296 textStyle.fStyle.setBackgroundColor(paint);
1297 }
1298 }
1299
countSurroundingGraphemes(TextRange textRange) const1300 SkTArray<TextIndex> ParagraphImpl::countSurroundingGraphemes(TextRange textRange) const {
1301 textRange = textRange.intersection({0, fText.size()});
1302 SkTArray<TextIndex> graphemes;
1303 if ((fCodeUnitProperties[textRange.start] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) {
1304 // Count the previous partial grapheme
1305 graphemes.emplace_back(textRange.start);
1306 }
1307 for (auto index = textRange.start; index < textRange.end; ++index) {
1308 if ((fCodeUnitProperties[index] & SkUnicode::CodeUnitFlags::kGraphemeStart) != 0) {
1309 graphemes.emplace_back(index);
1310 }
1311 }
1312 return graphemes;
1313 }
1314
findPreviousGraphemeBoundary(TextIndex utf8) const1315 TextIndex ParagraphImpl::findPreviousGraphemeBoundary(TextIndex utf8) const {
1316 while (utf8 > 0 &&
1317 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) {
1318 --utf8;
1319 }
1320 return utf8;
1321 }
1322
findNextGraphemeBoundary(TextIndex utf8) const1323 TextIndex ParagraphImpl::findNextGraphemeBoundary(TextIndex utf8) const {
1324 while (utf8 < fText.size() &&
1325 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGraphemeStart) == 0) {
1326 ++utf8;
1327 }
1328 return utf8;
1329 }
1330
findNextGlyphClusterBoundary(TextIndex utf8) const1331 TextIndex ParagraphImpl::findNextGlyphClusterBoundary(TextIndex utf8) const {
1332 while (utf8 < fText.size() &&
1333 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGlyphClusterStart) == 0) {
1334 ++utf8;
1335 }
1336 return utf8;
1337 }
1338
findPreviousGlyphClusterBoundary(TextIndex utf8) const1339 TextIndex ParagraphImpl::findPreviousGlyphClusterBoundary(TextIndex utf8) const {
1340 while (utf8 > 0 &&
1341 (fCodeUnitProperties[utf8] & SkUnicode::CodeUnitFlags::kGlyphClusterStart) == 0) {
1342 --utf8;
1343 }
1344 return utf8;
1345 }
1346
ensureUTF16Mapping()1347 void ParagraphImpl::ensureUTF16Mapping() {
1348 fillUTF16MappingOnce([&] {
1349 fUnicode->extractUtfConversionMapping(
1350 this->text(),
1351 [&](size_t index) { fUTF8IndexForUTF16Index.emplace_back(index); },
1352 [&](size_t index) { fUTF16IndexForUTF8Index.emplace_back(index); });
1353 });
1354 }
1355
visit(const Visitor & visitor)1356 void ParagraphImpl::visit(const Visitor& visitor) {
1357 int lineNumber = 0;
1358 for (auto& line : fLines) {
1359 line.ensureTextBlobCachePopulated();
1360 for (auto& rec : line.fTextBlobCache) {
1361 SkTextBlob::Iter iter(*rec.fBlob);
1362 SkTextBlob::Iter::ExperimentalRun run;
1363
1364 SkSTArray<128, uint32_t> clusterStorage;
1365 const Run* R = rec.fVisitor_Run;
1366 const uint32_t* clusterPtr = &R->fClusterIndexes[0];
1367
1368 if (R->fClusterStart > 0) {
1369 int count = R->fClusterIndexes.size();
1370 clusterStorage.reset(count);
1371 for (int i = 0; i < count; ++i) {
1372 clusterStorage[i] = R->fClusterStart + R->fClusterIndexes[i];
1373 }
1374 clusterPtr = &clusterStorage[0];
1375 }
1376 clusterPtr += rec.fVisitor_Pos;
1377
1378 while (iter.experimentalNext(&run)) {
1379 const Paragraph::VisitorInfo info = {
1380 run.font,
1381 rec.fOffset,
1382 rec.fClipRect.fRight,
1383 run.count,
1384 run.glyphs,
1385 run.positions,
1386 clusterPtr,
1387 0, // flags
1388 };
1389 visitor(lineNumber, &info);
1390 clusterPtr += run.count;
1391 }
1392 }
1393 visitor(lineNumber, nullptr); // signal end of line
1394 lineNumber += 1;
1395 }
1396 }
1397
getLineNumberAt(TextIndex codeUnitIndex) const1398 int ParagraphImpl::getLineNumberAt(TextIndex codeUnitIndex) const {
1399 for (auto i = 0; i < fLines.size(); ++i) {
1400 auto& line = fLines[i];
1401 if (line.text().contains({codeUnitIndex, codeUnitIndex + 1})) {
1402 return i;
1403 }
1404 }
1405 return -1;
1406 }
1407
getLineMetricsAt(int lineNumber,LineMetrics * lineMetrics) const1408 bool ParagraphImpl::getLineMetricsAt(int lineNumber, LineMetrics* lineMetrics) const {
1409 if (lineNumber < 0 || lineNumber >= fLines.size()) {
1410 return false;
1411 }
1412 auto& line = fLines[lineNumber];
1413 if (lineMetrics) {
1414 *lineMetrics = line.getMetrics();
1415 }
1416 return true;
1417 }
1418
getActualTextRange(int lineNumber,bool includeSpaces) const1419 TextRange ParagraphImpl::getActualTextRange(int lineNumber, bool includeSpaces) const {
1420 if (lineNumber < 0 || lineNumber >= fLines.size()) {
1421 return EMPTY_TEXT;
1422 }
1423 auto& line = fLines[lineNumber];
1424 return includeSpaces ? line.text() : line.trimmedText();
1425 }
1426
getGlyphClusterAt(TextIndex codeUnitIndex,GlyphClusterInfo * glyphInfo)1427 bool ParagraphImpl::getGlyphClusterAt(TextIndex codeUnitIndex, GlyphClusterInfo* glyphInfo) {
1428 for (auto i = 0; i < fLines.size(); ++i) {
1429 auto& line = fLines[i];
1430 if (!line.text().contains({codeUnitIndex, codeUnitIndex})) {
1431 continue;
1432 }
1433 for (auto c = line.clustersWithSpaces().start; c < line.clustersWithSpaces().end; ++c) {
1434 auto& cluster = fClusters[c];
1435 if (cluster.contains(codeUnitIndex)) {
1436 std::vector<TextBox> boxes;
1437 line.getRectsForRange(cluster.textRange(),
1438 RectHeightStyle::kTight,
1439 RectWidthStyle::kTight,
1440 boxes);
1441 if (boxes.size() > 0) {
1442 if (glyphInfo) {
1443 *glyphInfo = {boxes[0].rect, cluster.textRange(), boxes[0].direction};
1444 }
1445 return true;
1446 }
1447 }
1448 }
1449 return false;
1450 }
1451 return false;
1452 }
1453
getClosestGlyphClusterAt(SkScalar dx,SkScalar dy,GlyphClusterInfo * glyphInfo)1454 bool ParagraphImpl::getClosestGlyphClusterAt(SkScalar dx,
1455 SkScalar dy,
1456 GlyphClusterInfo* glyphInfo) {
1457 auto res = this->getGlyphPositionAtCoordinate(dx, dy);
1458 auto textIndex = res.position + (res.affinity == Affinity::kDownstream ? 0 : 1);
1459 GlyphClusterInfo gci;
1460 if (this->getGlyphClusterAt(textIndex, glyphInfo ? glyphInfo : &gci)) {
1461 return true;
1462 } else {
1463 return false;
1464 }
1465 }
1466
getFontAt(TextIndex codeUnitIndex) const1467 SkFont ParagraphImpl::getFontAt(TextIndex codeUnitIndex) const {
1468 for (auto& run : fRuns) {
1469 if (run.textRange().contains({codeUnitIndex, codeUnitIndex})) {
1470 return run.font();
1471 }
1472 }
1473 return SkFont();
1474 }
1475
getFonts() const1476 std::vector<Paragraph::FontInfo> ParagraphImpl::getFonts() const {
1477 std::vector<FontInfo> results;
1478 for (auto& run : fRuns) {
1479 results.emplace_back(run.font(), run.textRange());
1480 }
1481 return results;
1482 }
1483
measureText()1484 SkFontMetrics ParagraphImpl::measureText() {
1485 SkFontMetrics metrics;
1486 if (fRuns.empty()) {
1487 return metrics;
1488 }
1489
1490 const auto& firstFont = fRuns.front().font();
1491 SkRect firstBounds;
1492 auto firstStr = text(fRuns.front().textRange());
1493 firstFont.getMetrics(&metrics);
1494 firstFont.measureText(firstStr.data(), firstStr.size(), SkTextEncoding::kUTF8, &firstBounds, nullptr);
1495 fGlyphsBoundsTop = firstBounds.top();
1496 fGlyphsBoundsBottom = firstBounds.bottom();
1497 fGlyphsBoundsLeft = firstBounds.left();
1498 SkScalar realWidth = 0;
1499 for (size_t i = 0; i < fRuns.size(); ++i) {
1500 auto run = fRuns[i];
1501 const auto& font = run.font();
1502 SkRect bounds;
1503 auto str = text(run.textRange());
1504 auto advance = font.measureText(str.data(), str.size(), SkTextEncoding::kUTF8, &bounds, nullptr);
1505 realWidth += advance;
1506 if (i == 0) {
1507 realWidth -= ((advance - (bounds.right() - bounds.left())) / 2);
1508 }
1509 if (i == (fRuns.size() - 1)) {
1510 realWidth -= ((advance - (bounds.right() - bounds.left())) / 2);
1511 }
1512 fGlyphsBoundsTop = std::min(fGlyphsBoundsTop, bounds.top());
1513 fGlyphsBoundsBottom = std::max(fGlyphsBoundsBottom, bounds.bottom());
1514 }
1515 fGlyphsBoundsRight = realWidth + fGlyphsBoundsLeft;
1516 return metrics;
1517 }
1518
1519 } // namespace textlayout
1520 } // namespace skia
1521