1 // Copyright 2019 Google LLC.
2 #include "modules/skparagraph/src/ParagraphImpl.h"
3 #include "modules/skparagraph/src/TextWrapper.h"
4
5 #ifdef OHOS_SUPPORT
6 #include "log.h"
7 #include "modules/skparagraph/include/Hyphenator.h"
8 #include "modules/skparagraph/src/TextTabAlign.h"
9 #include "TextParameter.h"
10 #endif
11
12 namespace skia {
13 namespace textlayout {
14
15 namespace {
16 const size_t BREAK_NUM_TWO = 2;
17
18 struct LineBreakerWithLittleRounding {
LineBreakerWithLittleRoundingskia::textlayout::__anon700d5ab20111::LineBreakerWithLittleRounding19 LineBreakerWithLittleRounding(SkScalar maxWidth, bool applyRoundingHack)
20 : fLower(maxWidth - 0.25f)
21 , fMaxWidth(maxWidth)
22 , fUpper(maxWidth + 0.25f)
23 , fApplyRoundingHack(applyRoundingHack) {}
24
breakLineskia::textlayout::__anon700d5ab20111::LineBreakerWithLittleRounding25 bool breakLine(SkScalar width) const {
26 if (width < fLower) {
27 return false;
28 } else if (width > fUpper) {
29 return true;
30 }
31
32 auto val = std::fabs(width);
33 SkScalar roundedWidth;
34 if (fApplyRoundingHack) {
35 if (val < 10000) {
36 roundedWidth = SkScalarRoundToScalar(width * 100) * (1.0f/100);
37 } else if (val < 100000) {
38 roundedWidth = SkScalarRoundToScalar(width * 10) * (1.0f/10);
39 } else {
40 roundedWidth = SkScalarFloorToScalar(width);
41 }
42 } else {
43 if (val < 10000) {
44 roundedWidth = SkScalarFloorToScalar(width * 100) * (1.0f/100);
45 } else if (val < 100000) {
46 roundedWidth = SkScalarFloorToScalar(width * 10) * (1.0f/10);
47 } else {
48 roundedWidth = SkScalarFloorToScalar(width);
49 }
50 }
51 return roundedWidth > fMaxWidth;
52 }
53
54 const SkScalar fLower, fMaxWidth, fUpper;
55 const bool fApplyRoundingHack;
56 };
57 } // namespace
58
59 #ifdef OHOS_SUPPORT
matchHyphenResult(const std::vector<uint8_t> & result,ParagraphImpl * owner,size_t & pos,SkScalar maxWidth,SkScalar len)60 void TextWrapper::matchHyphenResult(const std::vector<uint8_t>& result, ParagraphImpl* owner, size_t& pos,
61 SkScalar maxWidth, SkScalar len)
62 {
63 auto startPos = pos;
64 size_t ix = 0;
65
66 // Result array may have more code points than we have visible clusters
67 int32_t prevIx = -1;
68 // cumulatively iterate width vs breaks
69 for (const auto& breakPos : result) {
70 int32_t clusterIx = static_cast<int32_t>(owner->fClustersIndexFromCodeUnit[startPos + ix]);
71 if (clusterIx == prevIx) {
72 ++ix;
73 continue;
74 }
75 prevIx = clusterIx;
76 TEXT_LOGD("hyphen break width:%{public}f / %{public}f : %{public}f", len, maxWidth,
77 owner->cluster(clusterIx).width());
78 len += owner->cluster(clusterIx).width();
79 auto shouldBreak = (len > maxWidth);
80 if (breakPos & 0x1) {
81 // we need to break after previous char, but the result needs to be mapped
82 pos = startPos + ix;
83 }
84 ++ix;
85 if (shouldBreak) {
86 break;
87 }
88 }
89 }
90
tryBreakWord(Cluster * startCluster,Cluster * endOfClusters,SkScalar widthBeforeCluster,SkScalar maxWidth)91 size_t TextWrapper::tryBreakWord(Cluster *startCluster, Cluster *endOfClusters,
92 SkScalar widthBeforeCluster, SkScalar maxWidth)
93 {
94 auto startPos = startCluster->textRange().start;
95 auto endPos = startPos;
96 auto owner = startCluster->getOwner();
97 for (auto next = startCluster + 1; next != endOfClusters; next++) {
98 // find the end boundary of current word (hard/soft/whitespace break)
99 if (next->isWhitespaceBreak() || next->isHardBreak()) {
100 break;
101 } else {
102 endPos = next->textRange().end;
103 }
104 }
105
106 // Using end position cluster height as an estimate for reserved hyphen width
107 // hyphen may not be shaped at this point. End pos may be non-drawable, so prefer
108 // previous glyph before break
109 auto mappedEnd = owner->fClustersIndexFromCodeUnit[endPos];
110 auto len = widthBeforeCluster + owner->cluster(mappedEnd > 0 ? mappedEnd - 1 : 0).height();
111 // ToDo: We can stop here based on hyhpenation frequency setting
112 // but there is no need to proceed with hyphenation if we don't have space for even hyphen
113 if (std::isnan(len) || len >= maxWidth) {
114 return startCluster->textRange().start;
115 }
116
117 auto locale = owner->paragraphStyle().getTextStyle().getLocale();
118 auto result = Hyphenator::getInstance().findBreakPositions(locale, owner->fText, startPos, endPos);
119 endPos = startPos;
120 matchHyphenResult(result, owner, endPos, maxWidth, len);
121
122 return endPos;
123 }
124
lookAheadByHyphen(Cluster * endOfClusters,SkScalar widthBeforeCluster,SkScalar maxWidth)125 bool TextWrapper::lookAheadByHyphen(Cluster* endOfClusters, SkScalar widthBeforeCluster, SkScalar maxWidth)
126 {
127 auto startCluster = fClusters.startCluster();
128 while (startCluster != endOfClusters && startCluster->isWhitespaceBreak()) {
129 ++startCluster;
130 }
131 if (startCluster == endOfClusters) {
132 return false;
133 }
134 auto endPos = tryBreakWord(startCluster, endOfClusters, widthBeforeCluster - fClusters.width(), maxWidth);
135 // if break position found, set fClusters and fWords accrodingly and break
136 if (endPos > startCluster->textRange().start) {
137 // need to break before the mapped end cluster
138 auto owner = startCluster->getOwner();
139 fClusters = TextStretch(startCluster, &owner->cluster(owner->fClustersIndexFromCodeUnit[endPos]) - 1,
140 fClusters.metrics().getForceStrut());
141 fWords.extend(fClusters);
142 fBrokeLineWithHyphen = true;
143 return false;
144 }
145 // else let the existing implementation do its best efforts
146 return true;
147 }
148
149 // Since we allow cluster clipping when they don't fit
150 // we have to work with stretches - parts of clusters
lookAhead(SkScalar maxWidth,Cluster * endOfClusters,bool applyRoundingHack,WordBreakType wordBreakType,bool needEllipsis)151 void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters, bool applyRoundingHack,
152 WordBreakType wordBreakType, bool needEllipsis) {
153
154 reset();
155 fEndLine.metrics().clean();
156 fWords.startFrom(fEndLine.startCluster(), fEndLine.startPos());
157 fClusters.startFrom(fEndLine.startCluster(), fEndLine.startPos());
158 fClip.startFrom(fEndLine.startCluster(), fEndLine.startPos());
159
160 bool isFirstWord = true;
161 TextTabAlign textTabAlign(endOfClusters->getOwner()->paragraphStyle().getTextTab());
162 textTabAlign.init(maxWidth, endOfClusters);
163
164 LineBreakerWithLittleRounding breaker(maxWidth, applyRoundingHack);
165 Cluster* nextNonBreakingSpace = nullptr;
166 SkScalar totalFakeSpacing = 0.0;
167 bool attemptedHyphenate = false;
168
169 for (auto cluster = fEndLine.endCluster(); cluster < endOfClusters; ++cluster) {
170 totalFakeSpacing += (cluster->needAutoSpacing() && cluster != fEndLine.endCluster()) ?
171 (cluster - 1)->getFontSize() / AUTO_SPACING_WIDTH_RATIO : 0;
172 SkScalar widthBeforeCluster = fWords.width() + fClusters.width() + totalFakeSpacing;
173 if (cluster->isHardBreak()) {
174 if (cluster != fEndLine.endCluster()) {
175 isFirstWord = false;
176 }
177 } else if (
178 // TODO: Trying to deal with flutter rounding problem. Must be removed...
179 SkScalar width = cluster->width() + widthBeforeCluster;
180 (!isFirstWord || wordBreakType != WordBreakType::NORMAL) &&
181 breaker.breakLine(width)) {
182 // if the hyphenator has already run as balancing algorithm, use the cluster information
183 if (cluster->isHyphenBreak() && !needEllipsis) {
184 // we dont want to add the current cluster as the hyphenation algorithm marks breaks before a cluster
185 // however, if we cannot fit anything to a line, we need to break out here
186 if (fWords.empty() && fClusters.empty()) {
187 fClusters.extend(cluster);
188 fTooLongCluster = true;
189 break;
190 }
191 if (!fClusters.empty()) {
192 fWords.extend(fClusters);
193 fBrokeLineWithHyphen = true;
194 break;
195 }
196 // let hyphenator try before this if it is enabled
197 } else if (cluster->isWhitespaceBreak() && ((wordBreakType != WordBreakType::BREAK_HYPHEN) ||
198 (wordBreakType == WordBreakType::BREAK_HYPHEN && attemptedHyphenate && !needEllipsis))) {
199 // It's the end of the word
200 isFirstWord = false;
201 fClusters.extend(cluster);
202
203 bool tabAlignRet = false;
204 if (cluster->isTabulation()) {
205 tabAlignRet = textTabAlign.processTab(fWords, fClusters, cluster, totalFakeSpacing);
206 } else {
207 tabAlignRet = textTabAlign.processEndofWord(fWords, fClusters, cluster, totalFakeSpacing);
208 }
209 if (tabAlignRet) {
210 break;
211 }
212 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, this->getClustersTrimmedWidth());
213 fWords.extend(fClusters);
214 continue;
215 } else if (cluster->run().isPlaceholder()) {
216 isFirstWord = false;
217 if (!fClusters.empty()) {
218 // Placeholder ends the previous word
219 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, this->getClustersTrimmedWidth());
220 fWords.extend(fClusters);
221 }
222
223 if (cluster->width() > maxWidth && fWords.empty()) {
224 // Placeholder is the only text and it's longer than the line;
225 // it does not count in fMinIntrinsicWidth
226 fClusters.extend(cluster);
227 fTooLongCluster = true;
228 fTooLongWord = true;
229 } else {
230 // Placeholder does not fit the line; it will be considered again on the next line
231 }
232 break;
233 }
234
235 // should do this only if hyphenation is enabled
236 if (wordBreakType == WordBreakType::BREAK_HYPHEN && !attemptedHyphenate && !fClusters.empty() &&
237 !needEllipsis) {
238 attemptedHyphenate = true;
239 if (!lookAheadByHyphen(endOfClusters, widthBeforeCluster, breaker.fUpper)) {
240 break;
241 }
242 }
243
244 textTabAlign.processEndofLine(fWords, fClusters, cluster, totalFakeSpacing);
245
246 // Walk further to see if there is a too long word, cluster or glyph
247 SkScalar nextWordLength = fClusters.width();
248 SkScalar nextShortWordLength = nextWordLength;
249 for (auto further = cluster; further != endOfClusters; ++further) {
250 if (further->isSoftBreak() || further->isHardBreak() || further->isWhitespaceBreak()) {
251 break;
252 }
253 if (further->run().isPlaceholder()) {
254 // Placeholder ends the word
255 break;
256 }
257
258 if (nextWordLength > 0 && nextWordLength <= maxWidth && further->isIntraWordBreak()) {
259 // The cluster is spaces but not the end of the word in a normal sense
260 nextNonBreakingSpace = further;
261 nextShortWordLength = nextWordLength;
262 }
263
264 if (maxWidth == 0) {
265 // This is a tricky flutter case: layout(width:0) places 1 cluster on each line
266 nextWordLength = std::max(nextWordLength, further->width());
267 } else {
268 nextWordLength += further->width();
269 }
270 }
271 if (nextWordLength > maxWidth) {
272 if (nextNonBreakingSpace != nullptr) {
273 // We only get here if the non-breaking space improves our situation
274 // (allows us to break the text to fit the word)
275 if (SkScalar shortLength = fWords.width() + nextShortWordLength;
276 !breaker.breakLine(shortLength)) {
277 // We can add the short word to the existing line
278 fClusters = TextStretch(fClusters.startCluster(), nextNonBreakingSpace, fClusters.metrics().getForceStrut());
279 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextShortWordLength);
280 fWords.extend(fClusters);
281 } else {
282 // We can place the short word on the next line
283 fClusters.clean();
284 }
285 // Either way we are not in "word is too long" situation anymore
286 break;
287 }
288 // If the word is too long we can break it right now and hope it's enough
289 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextWordLength);
290 if (fClusters.endPos() - fClusters.startPos() > 1 ||
291 fWords.empty()) {
292 fTooLongWord = true;
293 } else {
294 // Even if the word is too long there is a very little space on this line.
295 // let's deal with it on the next line.
296 }
297 }
298
299 if (fWords.empty() && breaker.breakLine(cluster->width())) {
300 fClusters.extend(cluster);
301 fTooLongCluster = true;
302 fTooLongWord = true;
303 }
304 break;
305 }
306
307 if (cluster->isSoftBreak() || cluster->isWhitespaceBreak()) {
308 isFirstWord = false;
309 }
310
311 if (cluster->run().isPlaceholder()) {
312 if (!fClusters.empty()) {
313 // Placeholder ends the previous word (placeholders are ignored in trimming)
314 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth());
315 fWords.extend(fClusters);
316 }
317
318 // Placeholder is separate word and its width now is counted in minIntrinsicWidth
319 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, cluster->width());
320 fWords.extend(cluster);
321 } else {
322 if (cluster->isTabulation()) {
323 if (textTabAlign.processTab(fWords, fClusters, cluster, totalFakeSpacing)) {
324 break;
325 }
326 fClusters.extend(cluster);
327
328 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth());
329 fWords.extend(fClusters);
330 } else {
331 fClusters.extend(cluster);
332 if (fClusters.endOfWord()) { // Keep adding clusters/words
333 if (textTabAlign.processEndofWord(fWords, fClusters, cluster, totalFakeSpacing)) {
334 if (wordBreakType == WordBreakType::BREAK_ALL) {
335 fClusters.trim(cluster);
336 }
337 break;
338 }
339
340 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth());
341 fWords.extend(fClusters);
342 } else {
343 if (textTabAlign.processCluster(fWords, fClusters, cluster, totalFakeSpacing)) {
344 fClusters.trim(cluster);
345 break;
346 }
347 }
348 }
349 }
350
351 if ((fHardLineBreak = cluster->isHardBreak())) {
352 // Stop at the hard line break
353 break;
354 }
355 }
356 }
357
moveForward(bool hasEllipsis,bool breakAll)358 void TextWrapper::moveForward(bool hasEllipsis, bool breakAll) {
359
360 // We normally break lines by words.
361 // The only way we may go to clusters is if the word is too long or
362 // it's the first word and it has an ellipsis attached to it.
363 // If nothing fits we show the clipping.
364 fTooLongWord = breakAll;
365 if (!fWords.empty()) {
366 fEndLine.extend(fWords);
367 #ifdef SK_IGNORE_SKPARAGRAPH_ELLIPSIS_FIX
368 if (!fTooLongWord || hasEllipsis) { // Ellipsis added to a word
369 #else
370 if (!fTooLongWord && !hasEllipsis) { // Ellipsis added to a grapheme
371 #endif
372 return;
373 }
374 }
375 if (!fClusters.empty()) {
376 fEndLine.extend(fClusters);
377 if (!fTooLongCluster) {
378 return;
379 }
380 }
381
382 if (!fClip.empty()) {
383 // Flutter: forget the clipped cluster but keep the metrics
384 fEndLine.metrics().add(fClip.metrics());
385 }
386 }
387
388 // Special case for start/end cluster since they can be clipped
389 void TextWrapper::trimEndSpaces(TextAlign align) {
390 // Remember the breaking position
391 fEndLine.saveBreak();
392 // Skip all space cluster at the end
393 for (auto cluster = fEndLine.endCluster();
394 cluster >= fEndLine.startCluster() && cluster->isWhitespaceBreak();
395 --cluster) {
396 fEndLine.trim(cluster);
397 }
398 fEndLine.trim();
399 }
400
401 SkScalar TextWrapper::getClustersTrimmedWidth() {
402 // Move the end of the line to the left
403 SkScalar width = 0;
404 bool trailingSpaces = true;
405 for (auto cluster = fClusters.endCluster(); cluster >= fClusters.startCluster(); --cluster) {
406 if (cluster->run().isPlaceholder()) {
407 continue;
408 }
409 if (trailingSpaces) {
410 if (!cluster->isWhitespaceBreak()) {
411 width += cluster->trimmedWidth(cluster->endPos());
412 trailingSpaces = false;
413 }
414 continue;
415 }
416 width += cluster->width();
417 }
418 return width;
419 }
420
421 // Trim the beginning spaces in case of soft line break
422 std::tuple<Cluster*, size_t, SkScalar> TextWrapper::trimStartSpaces(Cluster* endOfClusters) {
423
424 if (fHardLineBreak) {
425 // End of line is always end of cluster, but need to skip \n
426 auto width = fEndLine.width();
427 auto cluster = fEndLine.endCluster() + 1;
428 while (cluster < fEndLine.breakCluster() && cluster->isWhitespaceBreak()) {
429 width += cluster->width();
430 ++cluster;
431 }
432 return std::make_tuple(fEndLine.breakCluster() + 1, 0, width);
433 }
434
435 // breakCluster points to the end of the line;
436 // It's a soft line break so we need to move lineStart forward skipping all the spaces
437 auto width = fEndLine.widthWithGhostSpaces();
438 auto cluster = fEndLine.breakCluster() + 1;
439 while (cluster < endOfClusters && cluster->isWhitespaceBreak() && !cluster->isTabulation()) {
440 width += cluster->width();
441 ++cluster;
442 }
443
444 return std::make_tuple(cluster, 0, width);
445 }
446
447 // calculate heuristics for different variants and select the least bad
448
449 // calculate the total space required
450 // define the goal for line numbers (max vs space required).
451 // If text could fit, it has substantially larger score compared to nicer wrap with overflow
452
453 // iterate: select nontrivial candidates with some maximum offset and set the penalty / benefit of variants
454 // goals: 0) fit maximum amount of text
455 // 1) fill lines
456 // 2) make line lengths even
457 // 2.5) define a cost for hyphenation - not done
458 // 3) try to make it fast
459
460 constexpr int64_t MINIMUM_FILL_RATIO = 75;
461 constexpr int64_t MINIMUM_FILL_RATIO_SQUARED = MINIMUM_FILL_RATIO * MINIMUM_FILL_RATIO;
462 constexpr int64_t GOOD_ENOUGH_LINE_SCORE = 95 * 95;
463 constexpr int64_t UNDERFLOW_SCORE = 100;
464 constexpr float BALANCED_LAST_LINE_MULTIPLIER = 1.4f;
465 constexpr int64_t BEST_LOCAL_SCORE = -1000000;
466 constexpr float WIDTH_TOLERANCE = 5.f;
467 constexpr int64_t PARAM_2 = 2;
468 constexpr int64_t PARAM_10000 = 10000;
469
470 // mkay, this makes an assumption that we do the scoring runs in a single thread and holds the variables during
471 // recursion
472 struct TextWrapScorer {
473 TextWrapScorer(SkScalar maxWidth, ParagraphImpl& parent, size_t maxLines)
474 : maxWidth_(maxWidth), currentTarget_(maxWidth), maxLines_(maxLines), parent_(parent)
475 {
476 CalculateCumulativeLen(parent);
477
478 if (parent_.getLineBreakStrategy() == LineBreakStrategy::BALANCED) {
479 // calculate target width before breaks
480 int64_t targetLines = 1 + cumulativeLen_ / maxWidth_;
481 currentTarget_ = cumulativeLen_ / targetLines;
482 }
483
484 GenerateBreaks(parent);
485 }
486
487 void GenerateBreaks(ParagraphImpl& parent)
488 {
489 // we trust that clusters are sorted on parent
490 bool prevWasWhitespace = false;
491 SkScalar currentWidth = 0;
492 size_t currentCount = 0; // in principle currentWidth != 0 should provide same result
493 SkScalar cumulativeLen = 0;
494 for (size_t ix = 0; ix < parent.clusters().size(); ix++) {
495 auto& cluster = parent.clusters()[ix];
496 auto len = cluster.width();
497 cumulativeLen += len;
498 currentWidth += len;
499 currentCount++;
500 if (cluster.isWhitespaceBreak()) {
501 breaks_.emplace_back(cumulativeLen, Break::BreakType::BREAKTYPE_WHITE_SPACE, prevWasWhitespace);
502 prevWasWhitespace = true;
503 currentWidth = 0;
504 currentCount = 0;
505 } else if (cluster.isHardBreak()) {
506 breaks_.emplace_back(cumulativeLen, Break::BreakType::BREAKTYPE_HARD, false);
507 prevWasWhitespace = true;
508 currentWidth = 0;
509 currentCount = 0;
510 } else if (cluster.isHyphenBreak()) {
511 breaks_.emplace_back(cumulativeLen - cluster.width() + cluster.height(),
512 Break::BreakType::BREAKTYPE_HYPHEN, false);
513 breaks_.back().reservedSpace = cluster.height();
514 prevWasWhitespace = true;
515 currentWidth = 0;
516 currentCount = 0;
517 } else if (cluster.isIntraWordBreak()) {
518 breaks_.emplace_back(cumulativeLen, Break::BreakType::BREAKTYPE_INTRA, false);
519 prevWasWhitespace = true;
520 currentWidth = 0;
521 currentCount = 0;
522 } else if (currentWidth > currentTarget_) {
523 if (currentCount > 1) {
524 cumulativeLen -= cluster.width();
525 ix--;
526 }
527 breaks_.emplace_back(cumulativeLen, Break::BreakType::BREAKTYPE_FORCED, false);
528 prevWasWhitespace = false;
529 currentWidth = 0;
530 currentCount = 0;
531 } else {
532 prevWasWhitespace = false;
533 }
534 }
535 }
536
537 void CalculateCumulativeLen(ParagraphImpl& parent)
538 {
539 auto startCluster = &parent.cluster(0);
540 auto endCluster = &parent.cluster(0);
541 auto locale = parent.paragraphStyle().getTextStyle().getLocale();
542 for (size_t clusterIx = 0; clusterIx < parent.clusters().size(); clusterIx++) {
543 if (parent.getLineBreakStrategy() == LineBreakStrategy::BALANCED) {
544 auto& cluster = parent.cluster(clusterIx);
545 auto len = cluster.width();
546 cumulativeLen_ += len;
547 }
548 CalculateHyphenPos(clusterIx, startCluster, endCluster, parent, locale);
549 }
550 }
551
552 void CalculateHyphenPos(size_t clusterIx, Cluster*& startCluster, Cluster*& endCluster, ParagraphImpl& parent,
553 const SkString& locale)
554 {
555 auto& cluster = parent.cluster(clusterIx);
556 const bool hyphenEnabled = parent.getWordBreakType() == WordBreakType::BREAK_HYPHEN;
557 bool isWhitespace = (cluster.isHardBreak() || cluster.isWhitespaceBreak() || cluster.isTabulation());
558 if (hyphenEnabled && !fPrevWasWhitespace && isWhitespace && endCluster > startCluster) {
559 fPrevWasWhitespace = true;
560 auto results = Hyphenator::getInstance().findBreakPositions(
561 locale, parent.fText, startCluster->textRange().start, endCluster->textRange().end);
562 CheckHyphenBreak(results, parent, startCluster);
563 if (clusterIx + 1 < parent.clusters().size()) {
564 startCluster = &cluster + 1;
565 }
566 } else if (!isWhitespace) {
567 fPrevWasWhitespace = false;
568 endCluster = &cluster;
569 } else { // fix "one character + space and then the actual target"
570 uint32_t i = 1;
571 while (clusterIx + i < parent.clusters().size()) {
572 if (!parent.cluster(clusterIx + i).isWordBreak()) {
573 startCluster = &cluster + i;
574 break;
575 } else {
576 i++;
577 }
578 }
579 }
580 }
581
582 void CheckHyphenBreak(std::vector<uint8_t> results, ParagraphImpl& parent, Cluster*& startCluster)
583 {
584 size_t prevClusterIx = 0;
585 for (size_t resultIx = 0; resultIx < results.size(); resultIx++) {
586 if (results[resultIx] & 0x1) {
587 auto clusterPos = parent.clusterIndex(startCluster->textRange().start + resultIx);
588 if (clusterPos != prevClusterIx) {
589 parent.cluster(clusterPos).enableHyphenBreak();
590 prevClusterIx = clusterPos;
591 }
592 }
593 }
594 }
595
596 struct RecursiveParam {
597 int64_t targetLines;
598 size_t maxLines;
599 size_t lineNumber;
600 SkScalar begin;
601 SkScalar remainingTextWidth;
602 SkScalar currentMax;
603 size_t breakPos;
604 };
605
606 void Run() {
607 int64_t targetLines = 1 + cumulativeLen_ / maxWidth_;
608
609 if (parent_.getLineBreakStrategy() == LineBreakStrategy::BALANCED) {
610 currentTarget_ = cumulativeLen_ / targetLines;
611 }
612
613 if (targetLines < PARAM_2) {
614 // need to have at least two lines for algo to do anything useful
615 return;
616 }
617 CalculateRecursive(RecursiveParam{
618 targetLines, maxLines_, 0, 0.f, cumulativeLen_,
619 });
620 LOGD("cache_: %{public}zu", cache_.size());
621 }
622
623 int64_t CalculateRecursive(RecursiveParam param)
624 {
625 if (param.maxLines == 0 || param.remainingTextWidth <= 1.f) {
626 return BEST_LOCAL_SCORE;
627 }
628
629 // This should come precalculated
630 param.currentMax = maxWidth_ - parent_.detectIndents(param.lineNumber);
631 if (nearlyZero(param.currentMax)) {
632 return BEST_LOCAL_SCORE;
633 }
634
635 // trim possible spaces at the beginning of line
636 while ((param.lineNumber > 0) && (lastBreakPos_ + 1 < breaks_.size()) &&
637 (breaks_[lastBreakPos_ + 1].subsequentWhitespace)) {
638 param.remainingTextWidth += (param.begin - breaks_[++lastBreakPos_].width);
639 param.begin = breaks_[lastBreakPos_].width;
640 }
641
642 if (lastBreakPos_ < breaks_.size() && breaks_[lastBreakPos_].type == Break::BreakType::BREAKTYPE_FORCED) {
643 lastBreakPos_++;
644 }
645 param.breakPos = lastBreakPos_;
646
647 while (param.breakPos < breaks_.size() && breaks_[param.breakPos].width < (param.begin + param.currentMax)) {
648 param.breakPos++;
649 }
650
651 if (param.breakPos == lastBreakPos_ && param.remainingTextWidth > param.currentMax) {
652 // If we were unable to find a break that matches the criteria, insert new one
653 // This may happen if there is a long word and per line indent for this particular line
654 if (param.breakPos + 1 > breaks_.size()) {
655 breaks_.emplace_back(param.begin + param.currentMax, Break::BreakType::BREAKTYPE_FORCED, false);
656 } else {
657 breaks_.insert(breaks_.cbegin() + param.breakPos + 1, Break(param.begin + param.currentMax,
658 Break::BreakType::BREAKTYPE_FORCED, false));
659 }
660 param.breakPos += BREAK_NUM_TWO;
661 }
662
663 LOGD("Line %{public}lu about to loop %{public}f, %{public}lu, %{public}lu, max: %{public}f",
664 static_cast<unsigned long>(param.lineNumber), param.begin, static_cast<unsigned long>(param.breakPos),
665 static_cast<unsigned long>(lastBreakPos_), maxWidth_);
666
667 return FindOptimalSolutionForCurrentLine(param);
668 }
669
670 std::vector<SkScalar>& GetResult()
671 {
672 return current_;
673 }
674
675 SkScalar calculateCurrentWidth(RecursiveParam& param, bool looped)
676 {
677 SkScalar newWidth = param.currentMax;
678
679 if (param.breakPos > 0 && param.begin < breaks_[param.breakPos - 1].width) {
680 newWidth = std::min(breaks_[--param.breakPos].width - param.begin, param.currentMax);
681 }
682
683 if (looped
684 && ((lastBreakPos_ == param.breakPos)
685 || (newWidth / param.currentMax * UNDERFLOW_SCORE < MINIMUM_FILL_RATIO))) {
686 LOGD("line %{public}lu breaking %{public}f, %{public}lu, %{public}f/%{public}f",
687 static_cast<unsigned long>(param.lineNumber), param.begin, static_cast<unsigned long>(param.breakPos),
688 newWidth, maxWidth_);
689 return 0;
690 }
691
692 lastBreakPos_ = param.breakPos;
693
694 return std::min(newWidth, param.remainingTextWidth);
695 }
696
697 int64_t FindOptimalSolutionForCurrentLine(RecursiveParam& param)
698 {
699 // have this in reversed order to avoid extra insertions
700 std::vector<SkScalar> currentBest;
701 bool looped = false;
702 int64_t score = 0;
703 int64_t overallScore = score;
704 int64_t bestLocalScore = BEST_LOCAL_SCORE;
705 do {
706 // until the given threshold is crossed (minimum line fill rate)
707 // re-break this line, if a result is different, calculate score
708 SkScalar currentWidth = calculateCurrentWidth(param, looped);
709 if (currentWidth == 0) {
710 break;
711 }
712 Index index { param.lineNumber, param.begin, currentWidth };
713
714 // check cache
715 const auto& ite = cache_.find(index);
716 if (ite != cache_.cend()) {
717 cacheHits_++;
718 current_ = ite->second.widths;
719 overallScore = ite->second.score;
720 UpdateSolution(bestLocalScore, overallScore, currentBest);
721 looped = true;
722 continue;
723 }
724 SkScalar scoref = std::min(1.f, abs(currentTarget_ - currentWidth) / currentTarget_);
725 score = int64_t((1.f - scoref) * UNDERFLOW_SCORE);
726 score *= score;
727
728 current_.clear();
729 overallScore = score;
730
731 // Handle last line
732 if (breaks_[param.breakPos].type == Break::BreakType::BREAKTYPE_HYPHEN) {
733 auto copy = currentWidth - breaks_[param.breakPos].reservedSpace;
734 // name is bit confusing as the method enters also recursion
735 // with hyphen break this never is the last line
736 if (!HandleLastLine(param, overallScore, copy, score)) {
737 break;
738 }
739 } else { // real last line may update currentWidth
740 if (!HandleLastLine(param, overallScore, currentWidth, score)) {
741 break;
742 }
743 }
744 // we have exceeded target number of lines, add some penalty
745 if (param.targetLines < 0) {
746 overallScore += param.targetLines * PARAM_10000; // MINIMUM_FILL_RATIO;
747 }
748
749 // We always hold the best possible score of children at this point
750 current_.push_back(currentWidth);
751 cache_[index] = { overallScore, current_ };
752
753 UpdateSolution(bestLocalScore, overallScore, currentBest);
754 looped = true;
755 } while (score > MINIMUM_FILL_RATIO_SQUARED &&
756 !(param.lineNumber == 0 && bestLocalScore > param.targetLines * GOOD_ENOUGH_LINE_SCORE));
757 current_ = currentBest;
758 return bestLocalScore;
759 }
760
761 bool HandleLastLine(RecursiveParam& param, int64_t& overallScore, SkScalar& currentWidth, int64_t&score)
762 {
763 // Handle last line
764 if (abs(currentWidth - param.remainingTextWidth) < 1.f) {
765 // this is last line, with high-quality wrapping, relax the score a bit
766 if (parent_.getLineBreakStrategy() == LineBreakStrategy::HIGH_QUALITY) {
767 overallScore = std::max(MINIMUM_FILL_RATIO, overallScore);
768 } else {
769 overallScore *= BALANCED_LAST_LINE_MULTIPLIER;
770 }
771
772 // let's break the loop, under no same condition / fill-rate added rows can result to a better
773 // score.
774 currentWidth = param.currentMax;
775 score = MINIMUM_FILL_RATIO_SQUARED - 1;
776 LOGD("last line %{public}lu reached", static_cast<unsigned long>(param.lineNumber));
777 return true;
778 }
779 if (((param.remainingTextWidth - currentWidth) / maxWidth_) < param.maxLines) {
780 // recursively calculate best score for children
781 overallScore += CalculateRecursive(RecursiveParam{
782 param.targetLines - 1,
783 param.maxLines > param.lineNumber ? param.maxLines - param.lineNumber : 0,
784 param.lineNumber + 1,
785 param.begin + currentWidth,
786 param.remainingTextWidth - currentWidth
787 });
788 lastBreakPos_ = param.breakPos; // restore our ix
789 return true;
790 }
791
792 // the text is not going to fit anyway (anymore), no need to push it
793 return false;
794 }
795
796 void UpdateSolution(int64_t& bestLocalScore, const int64_t overallScore, std::vector<SkScalar>& currentBest)
797 {
798 if (overallScore > bestLocalScore) {
799 bestLocalScore = overallScore;
800 currentBest = current_;
801 }
802 }
803
804 private:
805 struct Index {
806 size_t lineNumber { 0 };
807 SkScalar begin { 0 };
808 SkScalar width { 0 };
809 bool operator==(const Index& other) const
810 {
811 return (lineNumber == other.lineNumber && fabs(begin - other.begin) < WIDTH_TOLERANCE &&
812 fabs(width - other.width) < WIDTH_TOLERANCE);
813 }
814 bool operator<(const Index& other) const
815 {
816 return lineNumber < other.lineNumber ||
817 (lineNumber == other.lineNumber && other.begin - begin > WIDTH_TOLERANCE) ||
818 (lineNumber == other.lineNumber && fabs(begin - other.begin) < WIDTH_TOLERANCE &&
819 other.width - width > WIDTH_TOLERANCE);
820 }
821 };
822
823 struct Score {
824 int64_t score { 0 };
825 // in reversed order
826 std::vector<SkScalar> widths;
827 };
828
829 // to be seen if unordered map would be better fit
830 std::map<Index, Score> cache_;
831
832 SkScalar maxWidth_ { 0 };
833 SkScalar currentTarget_ { 0 };
834 SkScalar cumulativeLen_ { 0 };
835 size_t maxLines_ { 0 };
836 ParagraphImpl& parent_;
837 std::vector<SkScalar> current_;
838
839 struct Break {
840 enum class BreakType {
841 BREAKTYPE_NONE,
842 BREAKTYPE_HARD,
843 BREAKTYPE_WHITE_SPACE,
844 BREAKTYPE_INTRA,
845 BREAKTYPE_FORCED,
846 BREAKTYPE_HYPHEN
847 };
848 Break(SkScalar w, BreakType t, bool ssws) : width(w), type(t), subsequentWhitespace(ssws) {}
849
850 SkScalar width { 0.f };
851 BreakType type { BreakType::BREAKTYPE_NONE };
852 bool subsequentWhitespace { false };
853 SkScalar reservedSpace { 0.f };
854 };
855
856 std::vector<Break> breaks_;
857 size_t lastBreakPos_ { 0 };
858
859 uint64_t cacheHits_ { 0 };
860 bool fPrevWasWhitespace{false};
861 };
862
863 uint64_t TextWrapper::CalculateBestScore(std::vector<SkScalar>& widthOut, SkScalar maxWidth,
864 ParagraphImpl* parent, size_t maxLines) {
865 if (maxLines == 0 || !parent || nearlyZero(maxWidth)) {
866 return -1;
867 }
868
869 TextWrapScorer* scorer = new TextWrapScorer(maxWidth, *parent, maxLines);
870 scorer->Run();
871 while (scorer && scorer->GetResult().size()) {
872 auto width = scorer->GetResult().back();
873 widthOut.push_back(width);
874 LOGD("width %{public}f", width);
875 scorer->GetResult().pop_back();
876 }
877
878 delete scorer;
879 return 0;
880 }
881
882 void TextWrapper::updateMetricsWithPlaceholder(std::vector<Run*>& runs, bool iterateByCluster) {
883 if (!iterateByCluster) {
884 Run* lastRun = nullptr;
885 for (auto& run : runs) {
886 if (run == lastRun) {
887 continue;
888 }
889 lastRun = run;
890 if (lastRun != nullptr && lastRun->placeholderStyle() != nullptr) {
891 SkASSERT(lastRun->size() == 1);
892 // Update the placeholder metrics so we can get the placeholder positions later
893 // and the line metrics (to make sure the placeholder fits)
894 lastRun->updateMetrics(&fEndLine.metrics());
895 }
896 }
897 return;
898 }
899 runs.clear();
900 // Deal with placeholder clusters == runs[@size==1]
901 Run* lastRun = nullptr;
902 for (auto cluster = fEndLine.startCluster(); cluster <= fEndLine.endCluster(); ++cluster) {
903 auto r = cluster->runOrNull();
904 if (r == lastRun) {
905 continue;
906 }
907 lastRun = r;
908 if (lastRun != nullptr && lastRun->placeholderStyle() != nullptr) {
909 SkASSERT(lastRun->size() == 1);
910 // Update the placeholder metrics so we can get the placeholder positions later
911 // and the line metrics (to make sure the placeholder fits)
912 lastRun->updateMetrics(&fEndLine.metrics());
913 runs.emplace_back(lastRun);
914 }
915 }
916 }
917
918 // TODO: refactor the code for line ending (with/without ellipsis)
919 void TextWrapper::breakTextIntoLines(ParagraphImpl* parent,
920 SkScalar maxWidth,
921 const AddLineToParagraph& addLine) {
922 fHeight = 0;
923 fMinIntrinsicWidth = std::numeric_limits<SkScalar>::min();
924 fMaxIntrinsicWidth = std::numeric_limits<SkScalar>::min();
925
926 auto span = parent->clusters();
927 if (span.empty()) {
928 return;
929 }
930 auto maxLines = parent->paragraphStyle().getMaxLines();
931 auto align = parent->paragraphStyle().effective_align();
932 auto unlimitedLines = maxLines == std::numeric_limits<size_t>::max();
933 auto endlessLine = !SkScalarIsFinite(maxWidth);
934 auto hasEllipsis = parent->paragraphStyle().ellipsized();
935
936 auto disableFirstAscent = parent->paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableFirstAscent;
937 auto disableLastDescent = parent->paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableLastDescent;
938 bool firstLine = true; // We only interested in fist line if we have to disable the first ascent
939
940 // Resolve balanced line widths
941 std::vector<SkScalar> balancedWidths;
942
943 // if word breaking strategy is nontrivial (balanced / optimal), AND word break mode is not BREAK_ALL
944 if (parent->getWordBreakType() != WordBreakType::BREAK_ALL &&
945 parent->getLineBreakStrategy() != LineBreakStrategy::GREEDY) {
946 if (CalculateBestScore(balancedWidths, maxWidth, parent, maxLines) < 0) {
947 // if the line breaking strategy returns a negative score, the algorithm could not fit or break the text
948 // fall back to default, greedy algorithm
949 balancedWidths.clear();
950 }
951 LOGD("Got %{public}lu", static_cast<unsigned long>(balancedWidths.size()));
952 }
953
954 SkScalar softLineMaxIntrinsicWidth = 0;
955 fEndLine = TextStretch(span.begin(), span.begin(), parent->strutForceHeight());
956 auto end = span.end() - 1;
957 auto start = span.begin();
958 InternalLineMetrics maxRunMetrics;
959 bool needEllipsis = false;
960 SkScalar newWidth = maxWidth;
961 SkScalar noIndentWidth = maxWidth;
962 while (fEndLine.endCluster() != end) {
963 noIndentWidth = maxWidth - parent->detectIndents(fLineNumber - 1);
964 if (maxLines == 1 && parent->paragraphStyle().getEllipsisMod() == EllipsisModal::HEAD) {
965 newWidth = FLT_MAX;
966 } else if (!balancedWidths.empty() && fLineNumber - 1 < balancedWidths.size()) {
967 newWidth = balancedWidths[fLineNumber - 1];
968 } else {
969 newWidth = maxWidth - parent->detectIndents(fLineNumber - 1);
970 }
971 auto lastLine = (hasEllipsis && unlimitedLines) || fLineNumber >= maxLines;
972 needEllipsis = hasEllipsis && !endlessLine && lastLine;
973
974 this->lookAhead(newWidth, end, parent->getApplyRoundingHack(), parent->getWordBreakType(), needEllipsis);
975
976 this->moveForward(needEllipsis, parent->getWordBreakType() == WordBreakType::BREAK_ALL);
977 if (fEndLine.endCluster() >= fEndLine.startCluster() || maxLines > 1) {
978 needEllipsis &= fEndLine.endCluster() < end - 1; // Only if we have some text to ellipsize
979 }
980
981 // Do not trim end spaces on the naturally last line of the left aligned text
982 this->trimEndSpaces(align);
983
984 // For soft line breaks add to the line all the spaces next to it
985 Cluster* startLine;
986 size_t pos;
987 SkScalar widthWithSpaces;
988 std::tie(startLine, pos, widthWithSpaces) = this->trimStartSpaces(end);
989
990 if (needEllipsis && !fHardLineBreak) {
991 // This is what we need to do to preserve a space before the ellipsis
992 fEndLine.restoreBreak();
993 widthWithSpaces = fEndLine.widthWithGhostSpaces();
994 } else if (fBrokeLineWithHyphen) {
995 fEndLine.shiftWidth(fEndLine.endCluster()->width());
996 }
997
998 // If the line is empty with the hard line break, let's take the paragraph font (flutter???)
999 if (fEndLine.metrics().isClean()) {
1000 fEndLine.setMetrics(parent->getEmptyMetrics());
1001 }
1002
1003 std::vector<Run*> runs;
1004 updateMetricsWithPlaceholder(runs, true);
1005 // update again for some case
1006 // such as :
1007 // placeholderA(width: 100, height: 100, align: bottom) placeholderB(width: 200, height: 200, align: top)
1008 // without second update: the placeholderA bottom will be set 0, and placeholderB bottom will be set 100
1009 // so the fEndline bottom will be set 100, is not equal placeholderA bottom
1010 updateMetricsWithPlaceholder(runs, false);
1011 // Before we update the line metrics with struts,
1012 // let's save it for GetRectsForRange(RectHeightStyle::kMax)
1013 maxRunMetrics = fEndLine.metrics();
1014 maxRunMetrics.fForceStrut = false;
1015
1016 // TODO: keep start/end/break info for text and runs but in a better way that below
1017 TextRange textExcludingSpaces(fEndLine.startCluster()->textRange().start, fEndLine.endCluster()->textRange().end);
1018 TextRange text(fEndLine.startCluster()->textRange().start, fEndLine.breakCluster()->textRange().start);
1019 TextRange textIncludingNewlines(fEndLine.startCluster()->textRange().start, startLine->textRange().start);
1020 if (startLine == end) {
1021 textIncludingNewlines.end = parent->text().size();
1022 text.end = parent->text().size();
1023 }
1024 ClusterRange clusters(fEndLine.startCluster() - start, fEndLine.endCluster() - start + 1);
1025 ClusterRange clustersWithGhosts(fEndLine.startCluster() - start, startLine - start);
1026
1027 if (disableFirstAscent && firstLine) {
1028 fEndLine.metrics().fAscent = fEndLine.metrics().fRawAscent;
1029 }
1030 if (disableLastDescent && (lastLine || (startLine == end && !fHardLineBreak))) {
1031 fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
1032 }
1033
1034 if (parent->strutEnabled()) {
1035 // Make sure font metrics are not less than the strut
1036 parent->strutMetrics().updateLineMetrics(fEndLine.metrics());
1037 }
1038
1039 SkScalar lineHeight = fEndLine.metrics().height();
1040 if (fHardLineBreak && !lastLine && parent->paragraphStyle().getParagraphSpacing() > 0) {
1041 lineHeight += parent->paragraphStyle().getParagraphSpacing();
1042 }
1043 firstLine = false;
1044
1045 if (fEndLine.empty()) {
1046 // Correct text and clusters (make it empty for an empty line)
1047 textExcludingSpaces.end = textExcludingSpaces.start;
1048 clusters.end = clusters.start;
1049 }
1050
1051 // In case of a force wrapping we don't have a break cluster and have to use the end cluster
1052 text.end = std::max(text.end, textExcludingSpaces.end);
1053
1054 if (parent->paragraphStyle().getEllipsisMod() == EllipsisModal::HEAD && hasEllipsis) {
1055 needEllipsis = maxLines <= 1;
1056 if (needEllipsis) {
1057 fHardLineBreak = false;
1058 }
1059 }
1060
1061 SkScalar offsetX = parent->detectIndents(fLineNumber - 1);
1062 addLine(textExcludingSpaces,
1063 text,
1064 textIncludingNewlines, clusters, clustersWithGhosts, widthWithSpaces,
1065 fEndLine.startPos(),
1066 fEndLine.endPos(),
1067 SkVector::Make(offsetX, fHeight),
1068 SkVector::Make(fEndLine.width(), lineHeight),
1069 fEndLine.metrics(),
1070 needEllipsis,
1071 offsetX,
1072 noIndentWidth);
1073
1074 softLineMaxIntrinsicWidth += widthWithSpaces;
1075
1076 fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
1077 if (fHardLineBreak) {
1078 softLineMaxIntrinsicWidth = 0;
1079 }
1080 // Start a new line
1081 fHeight += lineHeight;
1082 if (!fHardLineBreak || startLine != end) {
1083 fEndLine.clean();
1084 }
1085 fEndLine.startFrom(startLine, pos);
1086 parent->fMaxWidthWithTrailingSpaces = std::max(parent->fMaxWidthWithTrailingSpaces, widthWithSpaces);
1087
1088 if (hasEllipsis && unlimitedLines) {
1089 // There is one case when we need an ellipsis on a separate line
1090 // after a line break when width is infinite
1091 if (!fHardLineBreak) {
1092 break;
1093 }
1094 } else if (lastLine) {
1095 // There is nothing more to draw
1096 fHardLineBreak = false;
1097 break;
1098 }
1099
1100 ++fLineNumber;
1101 }
1102 if (parent->paragraphStyle().getIsEndAddParagraphSpacing() &&
1103 parent->paragraphStyle().getParagraphSpacing() > 0) {
1104 fHeight += parent->paragraphStyle().getParagraphSpacing();
1105 }
1106
1107 // We finished formatting the text but we need to scan the rest for some numbers
1108 // TODO: make it a case of a normal flow
1109 if (fEndLine.endCluster() != nullptr) {
1110 auto lastWordLength = 0.0f;
1111 auto cluster = fEndLine.endCluster();
1112 while (cluster != end || cluster->endPos() < end->endPos()) {
1113 fExceededMaxLines = true;
1114 if (cluster->isHardBreak()) {
1115 // Hard line break ends the word and the line
1116 fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
1117 softLineMaxIntrinsicWidth = 0;
1118 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1119 lastWordLength = 0;
1120 } else if (cluster->isWhitespaceBreak()) {
1121 // Whitespaces end the word
1122 softLineMaxIntrinsicWidth += cluster->width();
1123 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1124 lastWordLength = 0;
1125 } else if (cluster->run().isPlaceholder()) {
1126 // Placeholder ends the previous word and creates a separate one
1127 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1128 // Placeholder width now counts in fMinIntrinsicWidth
1129 softLineMaxIntrinsicWidth += cluster->width();
1130 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, cluster->width());
1131 lastWordLength = 0;
1132 } else {
1133 // Nothing out of ordinary - just add this cluster to the word and to the line
1134 softLineMaxIntrinsicWidth += cluster->width();
1135 lastWordLength += cluster->width();
1136 }
1137 ++cluster;
1138 }
1139 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1140 fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
1141
1142 if (parent->lines().empty()) {
1143 // In case we could not place even a single cluster on the line
1144 if (disableFirstAscent) {
1145 fEndLine.metrics().fAscent = fEndLine.metrics().fRawAscent;
1146 }
1147 if (disableLastDescent && !fHardLineBreak) {
1148 fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
1149 }
1150 fHeight = std::max(fHeight, fEndLine.metrics().height());
1151 }
1152 }
1153
1154 if (fHardLineBreak) {
1155 if (disableLastDescent) {
1156 fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
1157 }
1158
1159 // Last character is a line break
1160 if (parent->strutEnabled()) {
1161 // Make sure font metrics are not less than the strut
1162 parent->strutMetrics().updateLineMetrics(fEndLine.metrics());
1163 }
1164
1165 ClusterRange clusters(fEndLine.breakCluster() - start, fEndLine.endCluster() - start);
1166 addLine(fEndLine.breakCluster()->textRange(),
1167 fEndLine.breakCluster()->textRange(),
1168 fEndLine.endCluster()->textRange(),
1169 clusters,
1170 clusters,
1171 0,
1172 0,
1173 0,
1174 SkVector::Make(0, fHeight),
1175 SkVector::Make(0, fEndLine.metrics().height()),
1176 fEndLine.metrics(),
1177 needEllipsis,
1178 parent->detectIndents(fLineNumber - 1),
1179 noIndentWidth);
1180 fHeight += fEndLine.metrics().height();
1181 parent->lines().back().setMaxRunMetrics(maxRunMetrics);
1182 }
1183
1184 if (parent->lines().empty()) {
1185 return;
1186 }
1187 // Correct line metric styles for the first and for the last lines if needed
1188 if (disableFirstAscent) {
1189 parent->lines().front().setAscentStyle(LineMetricStyle::Typographic);
1190 }
1191 if (disableLastDescent) {
1192 parent->lines().back().setDescentStyle(LineMetricStyle::Typographic);
1193 }
1194 }
1195 #else
1196 // Since we allow cluster clipping when they don't fit
1197 // we have to work with stretches - parts of clusters
1198 void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters, bool applyRoundingHack) {
1199 reset();
1200 fEndLine.metrics().clean();
1201 fWords.startFrom(fEndLine.startCluster(), fEndLine.startPos());
1202 fClusters.startFrom(fEndLine.startCluster(), fEndLine.startPos());
1203 fClip.startFrom(fEndLine.startCluster(), fEndLine.startPos());
1204 LineBreakerWithLittleRounding breaker(maxWidth, applyRoundingHack);
1205 Cluster* nextNonBreakingSpace = nullptr;
1206 for (auto cluster = fEndLine.endCluster(); cluster < endOfClusters; ++cluster) {
1207 if (cluster->isHardBreak()) {
1208 } else if (
1209 SkScalar width = fWords.width() + fClusters.width() + cluster->width();
1210 breaker.breakLine(width)) {
1211 if (cluster->isWhitespaceBreak()) {
1212 // It's the end of the word
1213 fClusters.extend(cluster);
1214 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, this->getClustersTrimmedWidth());
1215 fWords.extend(fClusters);
1216 continue;
1217 } else if (cluster->run().isPlaceholder()) {
1218 if (!fClusters.empty()) {
1219 // Placeholder ends the previous word
1220 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, this->getClustersTrimmedWidth());
1221 fWords.extend(fClusters);
1222 }
1223 if (cluster->width() > maxWidth && fWords.empty()) {
1224 // Placeholder is the only text and it's longer than the line;
1225 // it does not count in fMinIntrinsicWidth
1226 fClusters.extend(cluster);
1227 fTooLongCluster = true;
1228 fTooLongWord = true;
1229 } else {
1230 // Placeholder does not fit the line; it will be considered again on the next line
1231 }
1232 break;
1233 }
1234 // Walk further to see if there is a too long word, cluster or glyph
1235 SkScalar nextWordLength = fClusters.width();
1236 SkScalar nextShortWordLength = nextWordLength;
1237 for (auto further = cluster; further != endOfClusters; ++further) {
1238 if (further->isSoftBreak() || further->isHardBreak() || further->isWhitespaceBreak()) {
1239 break;
1240 }
1241 if (further->run().isPlaceholder()) {
1242 // Placeholder ends the word
1243 break;
1244 }
1245 if (nextWordLength > 0 && nextWordLength <= maxWidth && further->isIntraWordBreak()) {
1246 // The cluster is spaces but not the end of the word in a normal sense
1247 nextNonBreakingSpace = further;
1248 nextShortWordLength = nextWordLength;
1249 }
1250 if (maxWidth == 0) {
1251 // This is a tricky flutter case: layout(width:0) places 1 cluster on each line
1252 nextWordLength = std::max(nextWordLength, further->width());
1253 } else {
1254 nextWordLength += further->width();
1255 }
1256 }
1257 if (nextWordLength > maxWidth) {
1258 if (nextNonBreakingSpace != nullptr) {
1259 // We only get here if the non-breaking space improves our situation
1260 // (allows us to break the text to fit the word)
1261 if (SkScalar shortLength = fWords.width() + nextShortWordLength;
1262 !breaker.breakLine(shortLength)) {
1263 // We can add the short word to the existing line
1264 fClusters = TextStretch(fClusters.startCluster(), nextNonBreakingSpace,
1265 fClusters.metrics().getForceStrut());
1266 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextShortWordLength);
1267 fWords.extend(fClusters);
1268 } else {
1269 // We can place the short word on the next line
1270 fClusters.clean();
1271 }
1272 // Either way we are not in "word is too long" situation anymore
1273 break;
1274 }
1275 // If the word is too long we can break it right now and hope it's enough
1276 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextWordLength);
1277 if (fClusters.endPos() - fClusters.startPos() > 1 ||
1278 fWords.empty()) {
1279 fTooLongWord = true;
1280 } else {
1281 // Even if the word is too long there is a very little space on this line.
1282 // let's deal with it on the next line.
1283 }
1284 }
1285 if (cluster->width() > maxWidth) {
1286 fClusters.extend(cluster);
1287 fTooLongCluster = true;
1288 fTooLongWord = true;
1289 }
1290 break;
1291 }
1292 if (cluster->run().isPlaceholder()) {
1293 if (!fClusters.empty()) {
1294 // Placeholder ends the previous word (placeholders are ignored in trimming)
1295 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth());
1296 fWords.extend(fClusters);
1297 }
1298 // Placeholder is separate word and its width now is counted in minIntrinsicWidth
1299 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, cluster->width());
1300 fWords.extend(cluster);
1301 } else {
1302 fClusters.extend(cluster);
1303 // Keep adding clusters/words
1304 if (fClusters.endOfWord()) {
1305 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth());
1306 fWords.extend(fClusters);
1307 }
1308 }
1309 if ((fHardLineBreak = cluster->isHardBreak())) {
1310 // Stop at the hard line break
1311 break;
1312 }
1313 }
1314 }
1315 void TextWrapper::moveForward(bool hasEllipsis) {
1316 // We normally break lines by words.
1317 // The only way we may go to clusters is if the word is too long or
1318 // it's the first word and it has an ellipsis attached to it.
1319 // If nothing fits we show the clipping.
1320 if (!fWords.empty()) {
1321 fEndLine.extend(fWords);
1322 #ifdef SK_IGNORE_SKPARAGRAPH_ELLIPSIS_FIX
1323 if (!fTooLongWord || hasEllipsis) { // Ellipsis added to a word
1324 #else
1325 if (!fTooLongWord && !hasEllipsis) { // Ellipsis added to a grapheme
1326 #endif
1327 return;
1328 }
1329 }
1330 if (!fClusters.empty()) {
1331 fEndLine.extend(fClusters);
1332 if (!fTooLongCluster) {
1333 return;
1334 }
1335 }
1336 if (!fClip.empty()) {
1337 // Flutter: forget the clipped cluster but keep the metrics
1338 fEndLine.metrics().add(fClip.metrics());
1339 }
1340 }
1341 // Special case for start/end cluster since they can be clipped
1342 void TextWrapper::trimEndSpaces(TextAlign align) {
1343 // Remember the breaking position
1344 fEndLine.saveBreak();
1345 // Skip all space cluster at the end
1346 for (auto cluster = fEndLine.endCluster();
1347 cluster >= fEndLine.startCluster() && cluster->isWhitespaceBreak();
1348 --cluster) {
1349 fEndLine.trim(cluster);
1350 }
1351 fEndLine.trim();
1352 }
1353 SkScalar TextWrapper::getClustersTrimmedWidth() {
1354 // Move the end of the line to the left
1355 SkScalar width = 0;
1356 bool trailingSpaces = true;
1357 for (auto cluster = fClusters.endCluster(); cluster >= fClusters.startCluster(); --cluster) {
1358 if (cluster->run().isPlaceholder()) {
1359 continue;
1360 }
1361 if (trailingSpaces) {
1362 if (!cluster->isWhitespaceBreak()) {
1363 width += cluster->trimmedWidth(cluster->endPos());
1364 trailingSpaces = false;
1365 }
1366 continue;
1367 }
1368 width += cluster->width();
1369 }
1370 return width;
1371 }
1372 // Trim the beginning spaces in case of soft line break
1373 std::tuple<Cluster*, size_t, SkScalar> TextWrapper::trimStartSpaces(Cluster* endOfClusters) {
1374 if (fHardLineBreak) {
1375 // End of line is always end of cluster, but need to skip \n
1376 auto width = fEndLine.width();
1377 auto cluster = fEndLine.endCluster() + 1;
1378 while (cluster < fEndLine.breakCluster() && cluster->isWhitespaceBreak()) {
1379 width += cluster->width();
1380 ++cluster;
1381 }
1382 return std::make_tuple(fEndLine.breakCluster() + 1, 0, width);
1383 }
1384 // breakCluster points to the end of the line;
1385 // It's a soft line break so we need to move lineStart forward skipping all the spaces
1386 auto width = fEndLine.widthWithGhostSpaces();
1387 auto cluster = fEndLine.breakCluster() + 1;
1388 while (cluster < endOfClusters && cluster->isWhitespaceBreak()) {
1389 width += cluster->width();
1390 ++cluster;
1391 }
1392 if (fEndLine.breakCluster()->isWhitespaceBreak() && fEndLine.breakCluster() < endOfClusters) {
1393 // In case of a soft line break by the whitespace
1394 // fBreak should point to the beginning of the next line
1395 // (it only matters when there are trailing spaces)
1396 fEndLine.shiftBreak();
1397 }
1398 return std::make_tuple(cluster, 0, width);
1399 }
1400 void TextWrapper::breakTextIntoLines(ParagraphImpl* parent,
1401 SkScalar maxWidth,
1402 const AddLineToParagraph& addLine) {
1403 fHeight = 0;
1404 fMinIntrinsicWidth = std::numeric_limits<SkScalar>::min();
1405 fMaxIntrinsicWidth = std::numeric_limits<SkScalar>::min();
1406 auto span = parent->clusters();
1407 if (span.empty()) {
1408 return;
1409 }
1410 auto maxLines = parent->paragraphStyle().getMaxLines();
1411 auto align = parent->paragraphStyle().effective_align();
1412 auto unlimitedLines = maxLines == std::numeric_limits<size_t>::max();
1413 auto endlessLine = !SkScalarIsFinite(maxWidth);
1414 auto hasEllipsis = parent->paragraphStyle().ellipsized();
1415 auto disableFirstAscent = parent->paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableFirstAscent;
1416 auto disableLastDescent = parent->paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableLastDescent;
1417 bool firstLine = true; // We only interested in fist line if we have to disable the first ascent
1418 SkScalar softLineMaxIntrinsicWidth = 0;
1419 fEndLine = TextStretch(span.begin(), span.begin(), parent->strutForceHeight());
1420 auto end = span.end() - 1;
1421 auto start = span.begin();
1422 InternalLineMetrics maxRunMetrics;
1423 bool needEllipsis = false;
1424 while (fEndLine.endCluster() != end) {
1425 this->lookAhead(maxWidth, end, parent->getApplyRoundingHack());
1426 auto lastLine = (hasEllipsis && unlimitedLines) || fLineNumber >= maxLines;
1427 needEllipsis = hasEllipsis && !endlessLine && lastLine;
1428 this->moveForward(needEllipsis);
1429 needEllipsis &= fEndLine.endCluster() < end - 1; // Only if we have some text to ellipsize
1430 // Do not trim end spaces on the naturally last line of the left aligned text
1431 this->trimEndSpaces(align);
1432 // For soft line breaks add to the line all the spaces next to it
1433 Cluster* startLine;
1434 size_t pos;
1435 SkScalar widthWithSpaces;
1436 std::tie(startLine, pos, widthWithSpaces) = this->trimStartSpaces(end);
1437 if (needEllipsis && !fHardLineBreak) {
1438 // This is what we need to do to preserve a space before the ellipsis
1439 fEndLine.restoreBreak();
1440 widthWithSpaces = fEndLine.widthWithGhostSpaces();
1441 }
1442 // If the line is empty with the hard line break, let's take the paragraph font (flutter???)
1443 if (fEndLine.metrics().isClean()) {
1444 fEndLine.setMetrics(parent->getEmptyMetrics());
1445 }
1446 // Deal with placeholder clusters == runs[@size==1]
1447 Run* lastRun = nullptr;
1448 for (auto cluster = fEndLine.startCluster(); cluster <= fEndLine.endCluster(); ++cluster) {
1449 auto r = cluster->runOrNull();
1450 if (r == lastRun) {
1451 continue;
1452 }
1453 lastRun = r;
1454 if (lastRun->placeholderStyle() != nullptr) {
1455 SkASSERT(lastRun->size() == 1);
1456 // Update the placeholder metrics so we can get the placeholder positions later
1457 // and the line metrics (to make sure the placeholder fits)
1458 lastRun->updateMetrics(&fEndLine.metrics());
1459 }
1460 }
1461 // Before we update the line metrics with struts,
1462 // let's save it for GetRectsForRange(RectHeightStyle::kMax)
1463 maxRunMetrics = fEndLine.metrics();
1464 maxRunMetrics.fForceStrut = false;
1465 TextRange textExcludingSpaces(fEndLine.startCluster()->textRange().start, fEndLine.endCluster()->textRange().end);
1466 TextRange text(fEndLine.startCluster()->textRange().start, fEndLine.breakCluster()->textRange().start);
1467 TextRange textIncludingNewlines(fEndLine.startCluster()->textRange().start, startLine->textRange().start);
1468 if (startLine == end) {
1469 textIncludingNewlines.end = parent->text().size();
1470 text.end = parent->text().size();
1471 }
1472 ClusterRange clusters(fEndLine.startCluster() - start, fEndLine.endCluster() - start + 1);
1473 ClusterRange clustersWithGhosts(fEndLine.startCluster() - start, startLine - start);
1474 if (disableFirstAscent && firstLine) {
1475 fEndLine.metrics().fAscent = fEndLine.metrics().fRawAscent;
1476 }
1477 if (disableLastDescent && (lastLine || (startLine == end && !fHardLineBreak ))) {
1478 fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
1479 }
1480 if (parent->strutEnabled()) {
1481 // Make sure font metrics are not less than the strut
1482 parent->strutMetrics().updateLineMetrics(fEndLine.metrics());
1483 }
1484 SkScalar lineHeight = fEndLine.metrics().height();
1485 firstLine = false;
1486 if (fEndLine.empty()) {
1487 // Correct text and clusters (make it empty for an empty line)
1488 textExcludingSpaces.end = textExcludingSpaces.start;
1489 clusters.end = clusters.start;
1490 }
1491 // In case of a force wrapping we don't have a break cluster and have to use the end cluster
1492 text.end = std::max(text.end, textExcludingSpaces.end);
1493 addLine(textExcludingSpaces,
1494 text,
1495 textIncludingNewlines, clusters, clustersWithGhosts, widthWithSpaces,
1496 fEndLine.startPos(),
1497 fEndLine.endPos(),
1498 SkVector::Make(0, fHeight),
1499 SkVector::Make(fEndLine.width(), lineHeight),
1500 fEndLine.metrics(),
1501 needEllipsis && !fHardLineBreak);
1502 softLineMaxIntrinsicWidth += widthWithSpaces;
1503 fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
1504 if (fHardLineBreak) {
1505 softLineMaxIntrinsicWidth = 0;
1506 }
1507 // Start a new line
1508 fHeight += lineHeight;
1509 if (!fHardLineBreak || startLine != end) {
1510 fEndLine.clean();
1511 }
1512 fEndLine.startFrom(startLine, pos);
1513 parent->fMaxWidthWithTrailingSpaces = std::max(parent->fMaxWidthWithTrailingSpaces, widthWithSpaces);
1514 if (hasEllipsis && unlimitedLines) {
1515 // There is one case when we need an ellipsis on a separate line
1516 // after a line break when width is infinite
1517 if (!fHardLineBreak) {
1518 break;
1519 }
1520 } else if (lastLine) {
1521 // There is nothing more to draw
1522 fHardLineBreak = false;
1523 break;
1524 }
1525 ++fLineNumber;
1526 }
1527 // We finished formatting the text but we need to scan the rest for some numbers
1528 if (fEndLine.endCluster() != nullptr) {
1529 auto lastWordLength = 0.0f;
1530 auto cluster = fEndLine.endCluster();
1531 while (cluster != end || cluster->endPos() < end->endPos()) {
1532 fExceededMaxLines = true;
1533 if (cluster->isHardBreak()) {
1534 // Hard line break ends the word and the line
1535 fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
1536 softLineMaxIntrinsicWidth = 0;
1537 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1538 lastWordLength = 0;
1539 } else if (cluster->isWhitespaceBreak()) {
1540 // Whitespaces end the word
1541 softLineMaxIntrinsicWidth += cluster->width();
1542 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1543 lastWordLength = 0;
1544 } else if (cluster->run().isPlaceholder()) {
1545 // Placeholder ends the previous word and creates a separate one
1546 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1547 // Placeholder width now counts in fMinIntrinsicWidth
1548 softLineMaxIntrinsicWidth += cluster->width();
1549 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, cluster->width());
1550 lastWordLength = 0;
1551 } else {
1552 // Nothing out of ordinary - just add this cluster to the word and to the line
1553 softLineMaxIntrinsicWidth += cluster->width();
1554 lastWordLength += cluster->width();
1555 }
1556 ++cluster;
1557 }
1558 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
1559 fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
1560 if (parent->lines().empty()) {
1561 // In case we could not place even a single cluster on the line
1562 if (disableFirstAscent) {
1563 fEndLine.metrics().fAscent = fEndLine.metrics().fRawAscent;
1564 }
1565 if (disableLastDescent && !fHardLineBreak) {
1566 fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
1567 }
1568 fHeight = std::max(fHeight, fEndLine.metrics().height());
1569 }
1570 }
1571 if (fHardLineBreak) {
1572 if (disableLastDescent) {
1573 fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
1574 }
1575 // Last character is a line break
1576 if (parent->strutEnabled()) {
1577 // Make sure font metrics are not less than the strut
1578 parent->strutMetrics().updateLineMetrics(fEndLine.metrics());
1579 }
1580 ClusterRange clusters(fEndLine.breakCluster() - start, fEndLine.endCluster() - start);
1581 addLine(fEndLine.breakCluster()->textRange(),
1582 fEndLine.breakCluster()->textRange(),
1583 fEndLine.endCluster()->textRange(),
1584 clusters,
1585 clusters,
1586 0,
1587 0,
1588 0,
1589 SkVector::Make(0, fHeight),
1590 SkVector::Make(0, fEndLine.metrics().height()),
1591 fEndLine.metrics(),
1592 needEllipsis);
1593 fHeight += fEndLine.metrics().height();
1594 parent->lines().back().setMaxRunMetrics(maxRunMetrics);
1595 }
1596 if (parent->lines().empty()) {
1597 return;
1598 }
1599 // Correct line metric styles for the first and for the last lines if needed
1600 if (disableFirstAscent) {
1601 parent->lines().front().setAscentStyle(LineMetricStyle::Typographic);
1602 }
1603 if (disableLastDescent) {
1604 parent->lines().back().setDescentStyle(LineMetricStyle::Typographic);
1605 }
1606 }
1607 #endif
1608 } // namespace textlayout
1609 } // namespace skia
1610