xref: /aosp_15_r20/external/skia/modules/skparagraph/src/TextWrapper.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 // Copyright 2019 Google LLC.
2 #include "modules/skparagraph/src/ParagraphImpl.h"
3 #include "modules/skparagraph/src/TextWrapper.h"
4 
5 namespace skia {
6 namespace textlayout {
7 
8 namespace {
9 struct LineBreakerWithLittleRounding {
LineBreakerWithLittleRoundingskia::textlayout::__anonb82136770111::LineBreakerWithLittleRounding10     LineBreakerWithLittleRounding(SkScalar maxWidth, bool applyRoundingHack)
11         : fLower(maxWidth - 0.25f)
12         , fMaxWidth(maxWidth)
13         , fUpper(maxWidth + 0.25f)
14         , fApplyRoundingHack(applyRoundingHack) {}
15 
breakLineskia::textlayout::__anonb82136770111::LineBreakerWithLittleRounding16     bool breakLine(SkScalar width) const {
17         if (width < fLower) {
18             return false;
19         } else if (width > fUpper) {
20             return true;
21         }
22 
23         auto val = std::fabs(width);
24         SkScalar roundedWidth;
25         if (fApplyRoundingHack) {
26             if (val < 10000) {
27                 roundedWidth = SkScalarRoundToScalar(width * 100) * (1.0f/100);
28             } else if (val < 100000) {
29                 roundedWidth = SkScalarRoundToScalar(width *  10) * (1.0f/10);
30             } else {
31                 roundedWidth = SkScalarFloorToScalar(width);
32             }
33         } else {
34             if (val < 10000) {
35                 roundedWidth = SkScalarFloorToScalar(width * 100) * (1.0f/100);
36             } else if (val < 100000) {
37                 roundedWidth = SkScalarFloorToScalar(width *  10) * (1.0f/10);
38             } else {
39                 roundedWidth = SkScalarFloorToScalar(width);
40             }
41         }
42         return roundedWidth > fMaxWidth;
43     }
44 
45     const SkScalar fLower, fMaxWidth, fUpper;
46     const bool fApplyRoundingHack;
47 };
48 }  // namespace
49 
50 // Since we allow cluster clipping when they don't fit
51 // we have to work with stretches - parts of clusters
lookAhead(SkScalar maxWidth,Cluster * endOfClusters,bool applyRoundingHack)52 void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters, bool applyRoundingHack) {
53 
54     reset();
55     fEndLine.metrics().clean();
56     fWords.startFrom(fEndLine.startCluster(), fEndLine.startPos());
57     fClusters.startFrom(fEndLine.startCluster(), fEndLine.startPos());
58     fClip.startFrom(fEndLine.startCluster(), fEndLine.startPos());
59 
60     LineBreakerWithLittleRounding breaker(maxWidth, applyRoundingHack);
61     Cluster* nextNonBreakingSpace = nullptr;
62     for (auto cluster = fEndLine.endCluster(); cluster < endOfClusters; ++cluster) {
63         if (cluster->isHardBreak()) {
64         } else if (
65                 // TODO: Trying to deal with flutter rounding problem. Must be removed...
66                 SkScalar width = fWords.width() + fClusters.width() + cluster->width();
67                 breaker.breakLine(width)) {
68             if (cluster->isWhitespaceBreak()) {
69                 // It's the end of the word
70                 fClusters.extend(cluster);
71                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, this->getClustersTrimmedWidth());
72                 fWords.extend(fClusters);
73                 continue;
74             } else if (cluster->run().isPlaceholder()) {
75                 if (!fClusters.empty()) {
76                     // Placeholder ends the previous word
77                     fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, this->getClustersTrimmedWidth());
78                     fWords.extend(fClusters);
79                 }
80 
81                 if (cluster->width() > maxWidth && fWords.empty()) {
82                     // Placeholder is the only text and it's longer than the line;
83                     // it does not count in fMinIntrinsicWidth
84                     fClusters.extend(cluster);
85                     fTooLongCluster = true;
86                     fTooLongWord = true;
87                 } else {
88                     // Placeholder does not fit the line; it will be considered again on the next line
89                 }
90                 break;
91             }
92 
93             // Walk further to see if there is a too long word, cluster or glyph
94             SkScalar nextWordLength = fClusters.width();
95             SkScalar nextShortWordLength = nextWordLength;
96             for (auto further = cluster; further != endOfClusters; ++further) {
97                 if (further->isSoftBreak() || further->isHardBreak() || further->isWhitespaceBreak()) {
98                     break;
99                 }
100                 if (further->run().isPlaceholder()) {
101                   // Placeholder ends the word
102                   break;
103                 }
104 
105                 if (nextWordLength > 0 && nextWordLength <= maxWidth && further->isIntraWordBreak()) {
106                     // The cluster is spaces but not the end of the word in a normal sense
107                     nextNonBreakingSpace = further;
108                     nextShortWordLength = nextWordLength;
109                 }
110 
111                 if (maxWidth == 0) {
112                     // This is a tricky flutter case: layout(width:0) places 1 cluster on each line
113                     nextWordLength = std::max(nextWordLength, further->width());
114                 } else {
115                     nextWordLength += further->width();
116                 }
117             }
118             if (nextWordLength > maxWidth) {
119                 if (nextNonBreakingSpace != nullptr) {
120                     // We only get here if the non-breaking space improves our situation
121                     // (allows us to break the text to fit the word)
122                     if (SkScalar shortLength = fWords.width() + nextShortWordLength;
123                         !breaker.breakLine(shortLength)) {
124                         // We can add the short word to the existing line
125                         fClusters = TextStretch(fClusters.startCluster(), nextNonBreakingSpace, fClusters.metrics().getForceStrut());
126                         fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextShortWordLength);
127                         fWords.extend(fClusters);
128                     } else {
129                         // We can place the short word on the next line
130                         fClusters.clean();
131                     }
132                     // Either way we are not in "word is too long" situation anymore
133                     break;
134                 }
135                 // If the word is too long we can break it right now and hope it's enough
136                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextWordLength);
137                 if (fClusters.endPos() - fClusters.startPos() > 1 ||
138                     fWords.empty()) {
139                     fTooLongWord = true;
140                 } else {
141                     // Even if the word is too long there is a very little space on this line.
142                     // let's deal with it on the next line.
143                 }
144             }
145 
146             if (cluster->width() > maxWidth) {
147                 fClusters.extend(cluster);
148                 fTooLongCluster = true;
149                 fTooLongWord = true;
150             }
151             break;
152         }
153 
154         if (cluster->run().isPlaceholder()) {
155             if (!fClusters.empty()) {
156                 // Placeholder ends the previous word (placeholders are ignored in trimming)
157                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth());
158                 fWords.extend(fClusters);
159             }
160 
161             // Placeholder is separate word and its width now is counted in minIntrinsicWidth
162             fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, cluster->width());
163             fWords.extend(cluster);
164         } else {
165             fClusters.extend(cluster);
166 
167             // Keep adding clusters/words
168             if (fClusters.endOfWord()) {
169                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth());
170                 fWords.extend(fClusters);
171             }
172         }
173 
174         if ((fHardLineBreak = cluster->isHardBreak())) {
175             // Stop at the hard line break
176             break;
177         }
178     }
179 }
180 
moveForward(bool hasEllipsis)181 void TextWrapper::moveForward(bool hasEllipsis) {
182 
183     // We normally break lines by words.
184     // The only way we may go to clusters is if the word is too long or
185     // it's the first word and it has an ellipsis attached to it.
186     // If nothing fits we show the clipping.
187     if (!fWords.empty()) {
188         fEndLine.extend(fWords);
189 #ifdef SK_IGNORE_SKPARAGRAPH_ELLIPSIS_FIX
190         if (!fTooLongWord || hasEllipsis) { // Ellipsis added to a word
191 #else
192         if (!fTooLongWord && !hasEllipsis) { // Ellipsis added to a grapheme
193 #endif
194             return;
195         }
196     }
197     if (!fClusters.empty()) {
198         fEndLine.extend(fClusters);
199         if (!fTooLongCluster) {
200             return;
201         }
202     }
203 
204     if (!fClip.empty()) {
205         // Flutter: forget the clipped cluster but keep the metrics
206         fEndLine.metrics().add(fClip.metrics());
207     }
208 }
209 
210 // Special case for start/end cluster since they can be clipped
211 void TextWrapper::trimEndSpaces(TextAlign align) {
212     // Remember the breaking position
213     fEndLine.saveBreak();
214     // Skip all space cluster at the end
215     for (auto cluster = fEndLine.endCluster();
216          cluster >= fEndLine.startCluster() && cluster->isWhitespaceBreak();
217          --cluster) {
218         fEndLine.trim(cluster);
219     }
220     fEndLine.trim();
221 }
222 
223 SkScalar TextWrapper::getClustersTrimmedWidth() {
224     // Move the end of the line to the left
225     SkScalar width = 0;
226     bool trailingSpaces = true;
227     for (auto cluster = fClusters.endCluster(); cluster >= fClusters.startCluster(); --cluster) {
228         if (cluster->run().isPlaceholder()) {
229             continue;
230         }
231         if (trailingSpaces) {
232             if (!cluster->isWhitespaceBreak()) {
233                 width += cluster->trimmedWidth(cluster->endPos());
234                 trailingSpaces = false;
235             }
236             continue;
237         }
238         width += cluster->width();
239     }
240     return width;
241 }
242 
243 // Trim the beginning spaces in case of soft line break
244 std::tuple<Cluster*, size_t, SkScalar> TextWrapper::trimStartSpaces(Cluster* endOfClusters) {
245 
246     if (fHardLineBreak) {
247         // End of line is always end of cluster, but need to skip \n
248         auto width = fEndLine.width();
249         auto cluster = fEndLine.endCluster() + 1;
250         while (cluster < fEndLine.breakCluster() && cluster->isWhitespaceBreak())  {
251             width += cluster->width();
252             ++cluster;
253         }
254         return std::make_tuple(fEndLine.breakCluster() + 1, 0, width);
255     }
256 
257     // breakCluster points to the end of the line;
258     // It's a soft line break so we need to move lineStart forward skipping all the spaces
259     auto width = fEndLine.widthWithGhostSpaces();
260     auto cluster = fEndLine.breakCluster() + 1;
261     while (cluster < endOfClusters && cluster->isWhitespaceBreak()) {
262         width += cluster->width();
263         ++cluster;
264     }
265 
266     if (fEndLine.breakCluster()->isWhitespaceBreak() && fEndLine.breakCluster() < endOfClusters) {
267         // In case of a soft line break by the whitespace
268         // fBreak should point to the beginning of the next line
269         // (it only matters when there are trailing spaces)
270         fEndLine.shiftBreak();
271     }
272 
273     return std::make_tuple(cluster, 0, width);
274 }
275 
276 // TODO: refactor the code for line ending (with/without ellipsis)
277 void TextWrapper::breakTextIntoLines(ParagraphImpl* parent,
278                                      SkScalar maxWidth,
279                                      const AddLineToParagraph& addLine) {
280     fHeight = 0;
281     fMinIntrinsicWidth = std::numeric_limits<SkScalar>::min();
282     fMaxIntrinsicWidth = std::numeric_limits<SkScalar>::min();
283 
284     auto span = parent->clusters();
285     if (span.empty()) {
286         return;
287     }
288     auto maxLines = parent->paragraphStyle().getMaxLines();
289     auto align = parent->paragraphStyle().effective_align();
290     auto unlimitedLines = maxLines == std::numeric_limits<size_t>::max();
291     auto endlessLine = !SkIsFinite(maxWidth);
292     auto hasEllipsis = parent->paragraphStyle().ellipsized();
293 
294     auto disableFirstAscent = parent->paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableFirstAscent;
295     auto disableLastDescent = parent->paragraphStyle().getTextHeightBehavior() & TextHeightBehavior::kDisableLastDescent;
296     bool firstLine = true; // We only interested in fist line if we have to disable the first ascent
297 
298     SkScalar softLineMaxIntrinsicWidth = 0;
299     fEndLine = TextStretch(span.begin(), span.begin(), parent->strutForceHeight());
300     auto end = span.end() - 1;
301     auto start = span.begin();
302     InternalLineMetrics maxRunMetrics;
303     bool needEllipsis = false;
304     while (fEndLine.endCluster() != end) {
305 
306         this->lookAhead(maxWidth, end, parent->getApplyRoundingHack());
307 
308         auto lastLine = (hasEllipsis && unlimitedLines) || fLineNumber >= maxLines;
309         needEllipsis = hasEllipsis && !endlessLine && lastLine;
310 
311         this->moveForward(needEllipsis);
312         needEllipsis &= fEndLine.endCluster() < end - 1; // Only if we have some text to ellipsize
313 
314         // Do not trim end spaces on the naturally last line of the left aligned text
315         this->trimEndSpaces(align);
316 
317         // For soft line breaks add to the line all the spaces next to it
318         Cluster* startLine;
319         size_t pos;
320         SkScalar widthWithSpaces;
321         std::tie(startLine, pos, widthWithSpaces) = this->trimStartSpaces(end);
322 
323         if (needEllipsis && !fHardLineBreak) {
324             // This is what we need to do to preserve a space before the ellipsis
325             fEndLine.restoreBreak();
326             widthWithSpaces = fEndLine.widthWithGhostSpaces();
327         }
328 
329         // If the line is empty with the hard line break, let's take the paragraph font (flutter???)
330         if (fEndLine.metrics().isClean()) {
331             fEndLine.setMetrics(parent->getEmptyMetrics());
332         }
333 
334         // Deal with placeholder clusters == runs[@size==1]
335         Run* lastRun = nullptr;
336         for (auto cluster = fEndLine.startCluster(); cluster <= fEndLine.endCluster(); ++cluster) {
337             auto r = cluster->runOrNull();
338             if (r == lastRun) {
339                 continue;
340             }
341             lastRun = r;
342             if (lastRun->placeholderStyle() != nullptr) {
343                 SkASSERT(lastRun->size() == 1);
344                 // Update the placeholder metrics so we can get the placeholder positions later
345                 // and the line metrics (to make sure the placeholder fits)
346                 lastRun->updateMetrics(&fEndLine.metrics());
347             }
348         }
349 
350         // Before we update the line metrics with struts,
351         // let's save it for GetRectsForRange(RectHeightStyle::kMax)
352         maxRunMetrics = fEndLine.metrics();
353         maxRunMetrics.fForceStrut = false;
354 
355         // TODO: keep start/end/break info for text and runs but in a better way that below
356         TextRange textExcludingSpaces(fEndLine.startCluster()->textRange().start, fEndLine.endCluster()->textRange().end);
357         TextRange text(fEndLine.startCluster()->textRange().start, fEndLine.breakCluster()->textRange().start);
358         TextRange textIncludingNewlines(fEndLine.startCluster()->textRange().start, startLine->textRange().start);
359         if (startLine == end) {
360             textIncludingNewlines.end = parent->text().size();
361             text.end = parent->text().size();
362         }
363         ClusterRange clusters(fEndLine.startCluster() - start, fEndLine.endCluster() - start + 1);
364         ClusterRange clustersWithGhosts(fEndLine.startCluster() - start, startLine - start);
365 
366         if (disableFirstAscent && firstLine) {
367             fEndLine.metrics().fAscent = fEndLine.metrics().fRawAscent;
368         }
369         if (disableLastDescent && (lastLine || (startLine == end && !fHardLineBreak ))) {
370             fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
371         }
372 
373         if (parent->strutEnabled()) {
374             // Make sure font metrics are not less than the strut
375             parent->strutMetrics().updateLineMetrics(fEndLine.metrics());
376         }
377 
378         SkScalar lineHeight = fEndLine.metrics().height();
379         firstLine = false;
380 
381         if (fEndLine.empty()) {
382             // Correct text and clusters (make it empty for an empty line)
383             textExcludingSpaces.end = textExcludingSpaces.start;
384             clusters.end = clusters.start;
385         }
386 
387         // In case of a force wrapping we don't have a break cluster and have to use the end cluster
388         text.end = std::max(text.end, textExcludingSpaces.end);
389 
390         addLine(textExcludingSpaces,
391                 text,
392                 textIncludingNewlines, clusters, clustersWithGhosts, widthWithSpaces,
393                 fEndLine.startPos(),
394                 fEndLine.endPos(),
395                 SkVector::Make(0, fHeight),
396                 SkVector::Make(fEndLine.width(), lineHeight),
397                 fEndLine.metrics(),
398                 needEllipsis && !fHardLineBreak);
399 
400         softLineMaxIntrinsicWidth += widthWithSpaces;
401 
402         fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
403         if (fHardLineBreak) {
404             softLineMaxIntrinsicWidth = 0;
405         }
406         // Start a new line
407         fHeight += lineHeight;
408         if (!fHardLineBreak || startLine != end) {
409             fEndLine.clean();
410         }
411         fEndLine.startFrom(startLine, pos);
412         parent->fMaxWidthWithTrailingSpaces = std::max(parent->fMaxWidthWithTrailingSpaces, widthWithSpaces);
413 
414         if (hasEllipsis && unlimitedLines) {
415             // There is one case when we need an ellipsis on a separate line
416             // after a line break when width is infinite
417             if (!fHardLineBreak) {
418                 break;
419             }
420         } else if (lastLine) {
421             // There is nothing more to draw
422             fHardLineBreak = false;
423             break;
424         }
425 
426         ++fLineNumber;
427     }
428 
429     // We finished formatting the text but we need to scan the rest for some numbers
430     // TODO: make it a case of a normal flow
431     if (fEndLine.endCluster() != nullptr) {
432         auto lastWordLength = 0.0f;
433         auto cluster = fEndLine.endCluster();
434         while (cluster != end || cluster->endPos() < end->endPos()) {
435             fExceededMaxLines = true;
436             if (cluster->isHardBreak()) {
437                 // Hard line break ends the word and the line
438                 fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
439                 softLineMaxIntrinsicWidth = 0;
440                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
441                 lastWordLength = 0;
442             } else if (cluster->isWhitespaceBreak()) {
443                 // Whitespaces end the word
444                 softLineMaxIntrinsicWidth += cluster->width();
445                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
446                 lastWordLength = 0;
447             } else if (cluster->run().isPlaceholder()) {
448                 // Placeholder ends the previous word and creates a separate one
449                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
450                 // Placeholder width now counts in fMinIntrinsicWidth
451                 softLineMaxIntrinsicWidth += cluster->width();
452                 fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, cluster->width());
453                 lastWordLength = 0;
454             } else {
455                 // Nothing out of ordinary - just add this cluster to the word and to the line
456                 softLineMaxIntrinsicWidth += cluster->width();
457                 lastWordLength += cluster->width();
458             }
459             ++cluster;
460         }
461         fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength);
462         fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth);
463 
464         if (parent->lines().empty()) {
465             // In case we could not place even a single cluster on the line
466             if (disableFirstAscent) {
467                 fEndLine.metrics().fAscent = fEndLine.metrics().fRawAscent;
468             }
469             if (disableLastDescent && !fHardLineBreak) {
470                 fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
471             }
472             fHeight = std::max(fHeight, fEndLine.metrics().height());
473         }
474     }
475 
476     if (fHardLineBreak) {
477         if (disableLastDescent) {
478             fEndLine.metrics().fDescent = fEndLine.metrics().fRawDescent;
479         }
480 
481         // Last character is a line break
482         if (parent->strutEnabled()) {
483             // Make sure font metrics are not less than the strut
484             parent->strutMetrics().updateLineMetrics(fEndLine.metrics());
485         }
486 
487         ClusterRange clusters(fEndLine.breakCluster() - start, fEndLine.endCluster() - start);
488         addLine(fEndLine.breakCluster()->textRange(),
489                 fEndLine.breakCluster()->textRange(),
490                 fEndLine.endCluster()->textRange(),
491                 clusters,
492                 clusters,
493                 0,
494                 0,
495                 0,
496                 SkVector::Make(0, fHeight),
497                 SkVector::Make(0, fEndLine.metrics().height()),
498                 fEndLine.metrics(),
499                 needEllipsis);
500         fHeight += fEndLine.metrics().height();
501         parent->lines().back().setMaxRunMetrics(maxRunMetrics);
502     }
503 
504     if (parent->lines().empty()) {
505         return;
506     }
507     // Correct line metric styles for the first and for the last lines if needed
508     if (disableFirstAscent) {
509         parent->lines().front().setAscentStyle(LineMetricStyle::Typographic);
510     }
511     if (disableLastDescent) {
512         parent->lines().back().setDescentStyle(LineMetricStyle::Typographic);
513     }
514 }
515 
516 }  // namespace textlayout
517 }  // namespace skia
518