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