Coverage Report

Created: 2021-08-22 09:07

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