1 | // Copyright 2019 Google LLC. |
2 | |
3 | #include "include/core/SkCanvas.h" |
4 | #include "include/core/SkFontMetrics.h" |
5 | #include "include/core/SkMatrix.h" |
6 | #include "include/core/SkPictureRecorder.h" |
7 | #include "include/core/SkTypeface.h" |
8 | #include "include/private/SkTFitsIn.h" |
9 | #include "include/private/SkTo.h" |
10 | #include "modules/skparagraph/include/Metrics.h" |
11 | #include "modules/skparagraph/include/Paragraph.h" |
12 | #include "modules/skparagraph/include/ParagraphStyle.h" |
13 | #include "modules/skparagraph/include/TextStyle.h" |
14 | #include "modules/skparagraph/src/OneLineShaper.h" |
15 | #include "modules/skparagraph/src/ParagraphImpl.h" |
16 | #include "modules/skparagraph/src/ParagraphUtil.h" |
17 | #include "modules/skparagraph/src/Run.h" |
18 | #include "modules/skparagraph/src/TextLine.h" |
19 | #include "modules/skparagraph/src/TextWrapper.h" |
20 | #include "src/core/SkSpan.h" |
21 | #include "src/utils/SkUTF.h" |
22 | |
23 | #if defined(SK_USING_THIRD_PARTY_ICU) |
24 | #include "third_party/icu/SkLoadICU.h" |
25 | #endif |
26 | |
27 | #include <math.h> |
28 | #include <algorithm> |
29 | #include <utility> |
30 | |
31 | |
32 | namespace skia { |
33 | namespace textlayout { |
34 | |
35 | namespace { |
36 | |
37 | SkScalar littleRound(SkScalar a) { |
38 | // This rounding is done to match Flutter tests. Must be removed.. |
39 | auto val = std::fabs(a); |
40 | if (val < 10000) { |
41 | return SkScalarRoundToScalar(a * 100.0)/100.0; |
42 | } else if (val < 100000) { |
43 | return SkScalarRoundToScalar(a * 10.0)/10.0; |
44 | } else { |
45 | return SkScalarFloorToScalar(a); |
46 | } |
47 | } |
48 | } // namespace |
49 | |
50 | TextRange operator*(const TextRange& a, const TextRange& b) { |
51 | if (a.start == b.start && a.end == b.end) return a; |
52 | auto begin = std::max(a.start, b.start); |
53 | auto end = std::min(a.end, b.end); |
54 | return end > begin ? TextRange(begin, end) : EMPTY_TEXT; |
55 | } |
56 | |
57 | Paragraph::Paragraph(ParagraphStyle style, sk_sp<FontCollection> fonts) |
58 | : fFontCollection(std::move(fonts)) |
59 | , fParagraphStyle(std::move(style)) |
60 | , fAlphabeticBaseline(0) |
61 | , fIdeographicBaseline(0) |
62 | , fHeight(0) |
63 | , fWidth(0) |
64 | , fMaxIntrinsicWidth(0) |
65 | , fMinIntrinsicWidth(0) |
66 | , fLongestLine(0) |
67 | , fExceededMaxLines(0) |
68 | { } |
69 | |
70 | ParagraphImpl::ParagraphImpl(const SkString& text, |
71 | ParagraphStyle style, |
72 | SkTArray<Block, true> blocks, |
73 | SkTArray<Placeholder, true> placeholders, |
74 | sk_sp<FontCollection> fonts) |
75 | : Paragraph(std::move(style), std::move(fonts)) |
76 | , fTextStyles(std::move(blocks)) |
77 | , fPlaceholders(std::move(placeholders)) |
78 | , fText(text) |
79 | , fState(kUnknown) |
80 | , fUnresolvedGlyphs(0) |
81 | , fPicture(nullptr) |
82 | , fStrutMetrics(false) |
83 | , fOldWidth(0) |
84 | , fOldHeight(0) |
85 | , fOrigin(SkRect::MakeEmpty()) { |
86 | fICU = ::skia::SkUnicode::Make(); |
87 | } |
88 | |
89 | ParagraphImpl::ParagraphImpl(const std::u16string& utf16text, |
90 | ParagraphStyle style, |
91 | SkTArray<Block, true> blocks, |
92 | SkTArray<Placeholder, true> placeholders, |
93 | sk_sp<FontCollection> fonts) |
94 | : ParagraphImpl(SkStringFromU16String(utf16text), |
95 | std::move(style), |
96 | std::move(blocks), |
97 | std::move(placeholders), |
98 | std::move(fonts)) |
99 | { } |
100 | |
101 | ParagraphImpl::~ParagraphImpl() = default; |
102 | |
103 | int32_t ParagraphImpl::unresolvedGlyphs() { |
104 | if (fState < kShaped) { |
105 | return -1; |
106 | } |
107 | |
108 | return fUnresolvedGlyphs; |
109 | } |
110 | |
111 | void ParagraphImpl::layout(SkScalar rawWidth) { |
112 | |
113 | // TODO: This rounding is done to match Flutter tests. Must be removed... |
114 | auto floorWidth = SkScalarFloorToScalar(rawWidth); |
115 | |
116 | if ((!SkScalarIsFinite(rawWidth) || fLongestLine <= floorWidth) && |
117 | fState >= kLineBroken && |
118 | fLines.size() == 1 && fLines.front().ellipsis() == nullptr) { |
119 | // Most common case: one line of text (and one line is never justified, so no cluster shifts) |
120 | fWidth = floorWidth; |
121 | fState = kLineBroken; |
122 | } else if (fState >= kLineBroken && fOldWidth != floorWidth) { |
123 | // We can use the results from SkShaper but have to do EVERYTHING ELSE again |
124 | fState = kShaped; |
125 | } else { |
126 | // Nothing changed case: we can reuse the data from the last layout |
127 | } |
128 | |
129 | if (fState < kShaped) { |
130 | this->fCodeUnitProperties.reset(); |
131 | this->fCodeUnitProperties.push_back_n(fText.size() + 1, CodeUnitFlags::kNoCodeUnitFlag); |
132 | this->fWords.clear(); |
133 | this->fBidiRegions.clear(); |
134 | this->fUTF8IndexForUTF16Index.reset(); |
135 | this->fUTF16IndexForUTF8Index.reset(); |
136 | this->fRuns.reset(); |
137 | if (!this->shapeTextIntoEndlessLine()) { |
138 | this->resetContext(); |
139 | // TODO: merge the two next calls - they always come together |
140 | this->resolveStrut(); |
141 | this->computeEmptyMetrics(); |
142 | this->fLines.reset(); |
143 | |
144 | // Set the important values that are not zero |
145 | fWidth = floorWidth; |
146 | fHeight = fEmptyMetrics.height(); |
147 | if (fParagraphStyle.getStrutStyle().getStrutEnabled() && |
148 | fParagraphStyle.getStrutStyle().getForceStrutHeight()) { |
149 | fHeight = fStrutMetrics.height(); |
150 | } |
151 | fAlphabeticBaseline = fEmptyMetrics.alphabeticBaseline(); |
152 | fIdeographicBaseline = fEmptyMetrics.ideographicBaseline(); |
153 | fLongestLine = FLT_MIN - FLT_MAX; // That is what flutter has |
154 | fMinIntrinsicWidth = 0; |
155 | fMaxIntrinsicWidth = 0; |
156 | this->fOldWidth = floorWidth; |
157 | this->fOldHeight = this->fHeight; |
158 | |
159 | return; |
160 | } |
161 | fState = kShaped; |
162 | } |
163 | |
164 | if (fState < kMarked) { |
165 | this->fClusters.reset(); |
166 | this->resetShifts(); |
167 | this->fClustersIndexFromCodeUnit.reset(); |
168 | this->fClustersIndexFromCodeUnit.push_back_n(fText.size() + 1, EMPTY_INDEX); |
169 | this->buildClusterTable(); |
170 | fState = kClusterized; |
171 | this->spaceGlyphs(); |
172 | fState = kMarked; |
173 | } |
174 | |
175 | if (fState < kLineBroken) { |
176 | this->resetContext(); |
177 | this->resolveStrut(); |
178 | this->computeEmptyMetrics(); |
179 | this->fLines.reset(); |
180 | this->breakShapedTextIntoLines(floorWidth); |
181 | fState = kLineBroken; |
182 | } |
183 | |
184 | if (fState < kFormatted) { |
185 | // Build the picture lazily not until we actually have to paint (or never) |
186 | this->formatLines(fWidth); |
187 | // We have to calculate the paragraph boundaries only after we format the lines |
188 | this->calculateBoundaries(); |
189 | fState = kFormatted; |
190 | } |
191 | |
192 | this->fOldWidth = floorWidth; |
193 | this->fOldHeight = this->fHeight; |
194 | |
195 | // TODO: This rounding is done to match Flutter tests. Must be removed... |
196 | fMinIntrinsicWidth = littleRound(fMinIntrinsicWidth); |
197 | fMaxIntrinsicWidth = littleRound(fMaxIntrinsicWidth); |
198 | |
199 | // TODO: This is strictly Flutter thing. Must be factored out into some flutter code |
200 | if (fParagraphStyle.getMaxLines() == 1 || |
201 | (fParagraphStyle.unlimited_lines() && fParagraphStyle.ellipsized())) { |
202 | fMinIntrinsicWidth = fMaxIntrinsicWidth; |
203 | } |
204 | |
205 | //SkDebugf("layout('%s', %f): %f %f\n", fText.c_str(), rawWidth, fMinIntrinsicWidth, fMaxIntrinsicWidth); |
206 | } |
207 | |
208 | void ParagraphImpl::paint(SkCanvas* canvas, SkScalar x, SkScalar y) { |
209 | |
210 | if (fState < kDrawn) { |
211 | // Record the picture anyway (but if we have some pieces in the cache they will be used) |
212 | this->paintLinesIntoPicture(); |
213 | fState = kDrawn; |
214 | } |
215 | |
216 | SkMatrix matrix = SkMatrix::Translate(x + fOrigin.fLeft, y + fOrigin.fTop); |
217 | canvas->drawPicture(fPicture, &matrix, nullptr); |
218 | } |
219 | |
220 | void ParagraphImpl::resetContext() { |
221 | fAlphabeticBaseline = 0; |
222 | fHeight = 0; |
223 | fWidth = 0; |
224 | fIdeographicBaseline = 0; |
225 | fMaxIntrinsicWidth = 0; |
226 | fMinIntrinsicWidth = 0; |
227 | fLongestLine = 0; |
228 | fMaxWidthWithTrailingSpaces = 0; |
229 | fExceededMaxLines = false; |
230 | } |
231 | |
232 | // shapeTextIntoEndlessLine is the thing that calls this method |
233 | // (that contains all ICU dependencies except for words) |
234 | bool ParagraphImpl::computeCodeUnitProperties() { |
235 | |
236 | #if defined(SK_USING_THIRD_PARTY_ICU) |
237 | if (!SkLoadICU()) { |
238 | return false; |
239 | } |
240 | #endif |
241 | |
242 | // Get bidi regions |
243 | Direction textDirection = fParagraphStyle.getTextDirection() == TextDirection::kLtr |
244 | ? Direction::kLTR |
245 | : Direction::kRTL; |
246 | if (!fICU->getBidiRegions(fText.c_str(), fText.size(), textDirection, &fBidiRegions)) { |
247 | return false; |
248 | } |
249 | |
250 | // Get white spaces |
251 | std::vector<Position> whitespaces; |
252 | if (!fICU->getWhitespaces(fText.c_str(), fText.size(), &whitespaces)) { |
253 | return false; |
254 | } |
255 | for (auto whitespace : whitespaces) { |
256 | fCodeUnitProperties[whitespace] |= CodeUnitFlags::kPartOfWhiteSpace; |
257 | } |
258 | |
259 | // Get line breaks |
260 | std::vector<LineBreakBefore> lineBreaks; |
261 | if (!fICU->getLineBreaks(fText.c_str(), fText.size(), &lineBreaks)) { |
262 | return false; |
263 | } |
264 | for (auto& lineBreak : lineBreaks) { |
265 | fCodeUnitProperties[lineBreak.pos] |= lineBreak.breakType == LineBreakType::kHardLineBreak |
266 | ? CodeUnitFlags::kHardLineBreakBefore |
267 | : CodeUnitFlags::kSoftLineBreakBefore; |
268 | } |
269 | |
270 | // Get graphemes |
271 | std::vector<Position> graphemes; |
272 | if (!fICU->getGraphemes(fText.c_str(), fText.size(), &graphemes)) { |
273 | return false; |
274 | } |
275 | for (auto pos : graphemes) { |
276 | fCodeUnitProperties[pos] |= CodeUnitFlags::kGraphemeStart; |
277 | } |
278 | |
279 | return true; |
280 | } |
281 | |
282 | // Clusters in the order of the input text |
283 | void ParagraphImpl::buildClusterTable() { |
284 | |
285 | // Walk through all the run in the direction of input text |
286 | for (auto& run : fRuns) { |
287 | auto runIndex = run.index(); |
288 | auto runStart = fClusters.size(); |
289 | if (run.isPlaceholder()) { |
290 | // Add info to cluster indexes table (text -> cluster) |
291 | for (auto i = run.textRange().start; i < run.textRange().end; ++i) { |
292 | fClustersIndexFromCodeUnit[i] = fClusters.size(); |
293 | } |
294 | // There are no glyphs but we want to have one cluster |
295 | fClusters.emplace_back(this, runIndex, 0ul, 1ul, this->text(run.textRange()), run.advance().fX, run.advance().fY); |
296 | fCodeUnitProperties[run.textRange().start] |= CodeUnitFlags::kSoftLineBreakBefore; |
297 | fCodeUnitProperties[run.textRange().end] |= CodeUnitFlags::kSoftLineBreakBefore; |
298 | } else { |
299 | fClusters.reserve(fClusters.size() + run.size()); |
300 | // Walk through the glyph in the direction of input text |
301 | run.iterateThroughClustersInTextOrder([runIndex, this](size_t glyphStart, |
302 | size_t glyphEnd, |
303 | size_t charStart, |
304 | size_t charEnd, |
305 | SkScalar width, |
306 | SkScalar height) { |
307 | SkASSERT(charEnd >= charStart); |
308 | // Add info to cluster indexes table (text -> cluster) |
309 | for (auto i = charStart; i < charEnd; ++i) { |
310 | fClustersIndexFromCodeUnit[i] = fClusters.size(); |
311 | } |
312 | SkSpan<const char> text(fText.c_str() + charStart, charEnd - charStart); |
313 | fClusters.emplace_back(this, runIndex, glyphStart, glyphEnd, text, width, height); |
314 | }); |
315 | } |
316 | |
317 | run.setClusterRange(runStart, fClusters.size()); |
318 | fMaxIntrinsicWidth += run.advance().fX; |
319 | } |
320 | fClustersIndexFromCodeUnit[fText.size()] = fClusters.size(); |
321 | fClusters.emplace_back(this, EMPTY_RUN, 0, 0, this->text({fText.size(), fText.size()}), 0, 0); |
322 | } |
323 | |
324 | void ParagraphImpl::spaceGlyphs() { |
325 | |
326 | // Walk through all the clusters in the direction of shaped text |
327 | // (we have to walk through the styles in the same order, too) |
328 | SkScalar shift = 0; |
329 | for (auto& run : fRuns) { |
330 | |
331 | // Skip placeholder runs |
332 | if (run.isPlaceholder()) { |
333 | continue; |
334 | } |
335 | |
336 | bool soFarWhitespacesOnly = true; |
337 | run.iterateThroughClusters([this, &run, &shift, &soFarWhitespacesOnly](Cluster* cluster) { |
338 | // Shift the cluster (shift collected from the previous clusters) |
339 | run.shift(cluster, shift); |
340 | |
341 | // Synchronize styles (one cluster can be covered by few styles) |
342 | Block* currentStyle = this->fTextStyles.begin(); |
343 | while (!cluster->startsIn(currentStyle->fRange)) { |
344 | currentStyle++; |
345 | SkASSERT(currentStyle != this->fTextStyles.end()); |
346 | } |
347 | |
348 | SkASSERT(!currentStyle->fStyle.isPlaceholder()); |
349 | |
350 | // Process word spacing |
351 | if (currentStyle->fStyle.getWordSpacing() != 0) { |
352 | if (cluster->isWhitespaces() && cluster->isSoftBreak()) { |
353 | if (!soFarWhitespacesOnly) { |
354 | shift += run.addSpacesAtTheEnd(currentStyle->fStyle.getWordSpacing(), cluster); |
355 | } |
356 | } |
357 | } |
358 | // Process letter spacing |
359 | if (currentStyle->fStyle.getLetterSpacing() != 0) { |
360 | shift += run.addSpacesEvenly(currentStyle->fStyle.getLetterSpacing(), cluster); |
361 | } |
362 | |
363 | if (soFarWhitespacesOnly && !cluster->isWhitespaces()) { |
364 | soFarWhitespacesOnly = false; |
365 | } |
366 | }); |
367 | } |
368 | } |
369 | |
370 | bool ParagraphImpl::shapeTextIntoEndlessLine() { |
371 | |
372 | if (fText.size() == 0) { |
373 | return false; |
374 | } |
375 | |
376 | // Check the font-resolved text against the cache |
377 | if (fFontCollection->getParagraphCache()->findParagraph(this)) { |
378 | return true; |
379 | } |
380 | |
381 | if (!computeCodeUnitProperties()) { |
382 | return false; |
383 | } |
384 | |
385 | fFontSwitches.reset(); |
386 | |
387 | OneLineShaper oneLineShaper(this); |
388 | auto result = oneLineShaper.shape(); |
389 | fUnresolvedGlyphs = oneLineShaper.unresolvedGlyphs(); |
390 | |
391 | if (!result) { |
392 | return false; |
393 | } else { |
394 | // Add the paragraph to the cache |
395 | fFontCollection->getParagraphCache()->updateParagraph(this); |
396 | return true; |
397 | } |
398 | } |
399 | |
400 | void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) { |
401 | TextWrapper textWrapper; |
402 | textWrapper.breakTextIntoLines( |
403 | this, |
404 | maxWidth, |
405 | [&](TextRange text, |
406 | TextRange textWithSpaces, |
407 | ClusterRange clusters, |
408 | ClusterRange clustersWithGhosts, |
409 | SkScalar widthWithSpaces, |
410 | size_t startPos, |
411 | size_t endPos, |
412 | SkVector offset, |
413 | SkVector advance, |
414 | InternalLineMetrics metrics, |
415 | bool addEllipsis) { |
416 | // TODO: Take in account clipped edges |
417 | auto& line = this->addLine(offset, advance, text, textWithSpaces, clusters, clustersWithGhosts, widthWithSpaces, metrics); |
418 | if (addEllipsis) { |
419 | line.createEllipsis(maxWidth, fParagraphStyle.getEllipsis(), true); |
420 | if (line.ellipsis() != nullptr) { |
421 | // Make sure the paragraph boundaries include its ellipsis |
422 | auto size = line.ellipsis()->advance(); |
423 | auto offset = line.ellipsis()->offset(); |
424 | SkRect boundaries = SkRect::MakeXYWH(offset.fX, offset.fY, size.fX, size.fY); |
425 | fOrigin.joinPossiblyEmptyRect(boundaries); |
426 | } |
427 | } |
428 | |
429 | fLongestLine = std::max(fLongestLine, nearlyZero(advance.fX) ? widthWithSpaces : advance.fX); |
430 | }); |
431 | |
432 | fHeight = textWrapper.height(); |
433 | fWidth = maxWidth; |
434 | fMaxIntrinsicWidth = textWrapper.maxIntrinsicWidth(); |
435 | fMinIntrinsicWidth = textWrapper.minIntrinsicWidth(); |
436 | fAlphabeticBaseline = fLines.empty() ? fEmptyMetrics.alphabeticBaseline() : fLines.front().alphabeticBaseline(); |
437 | fIdeographicBaseline = fLines.empty() ? fEmptyMetrics.ideographicBaseline() : fLines.front().ideographicBaseline(); |
438 | fExceededMaxLines = textWrapper.exceededMaxLines(); |
439 | |
440 | // Correct the first and the last line ascents/descents if required |
441 | if ((fParagraphStyle.getTextHeightBehavior() & TextHeightBehavior::kDisableFirstAscent) != 0) { |
442 | auto& firstLine = fLines.front(); |
443 | auto delta = firstLine.metricsWithoutMultiplier(TextHeightBehavior::kDisableFirstAscent); |
444 | if (!SkScalarNearlyZero(delta)) { |
445 | fHeight += delta; |
446 | // Shift all the lines up |
447 | for (auto& line : fLines) { |
448 | if (line.isFirstLine()) continue; |
449 | line.shiftVertically(delta); |
450 | } |
451 | } |
452 | } |
453 | |
454 | if ((fParagraphStyle.getTextHeightBehavior() & TextHeightBehavior::kDisableLastDescent) != 0) { |
455 | auto& lastLine = fLines.back(); |
456 | auto delta = lastLine.metricsWithoutMultiplier(TextHeightBehavior::kDisableLastDescent); |
457 | // It's the last line. There is nothing below to shift |
458 | fHeight += delta; |
459 | } |
460 | } |
461 | |
462 | void ParagraphImpl::formatLines(SkScalar maxWidth) { |
463 | auto effectiveAlign = fParagraphStyle.effective_align(); |
464 | |
465 | if (!SkScalarIsFinite(maxWidth) && effectiveAlign != TextAlign::kLeft) { |
466 | // Special case: clean all text in case of maxWidth == INF & align != left |
467 | // We had to go through shaping though because we need all the measurement numbers |
468 | fLines.reset(); |
469 | return; |
470 | } |
471 | |
472 | for (auto& line : fLines) { |
473 | line.format(effectiveAlign, maxWidth); |
474 | } |
475 | } |
476 | |
477 | void ParagraphImpl::paintLinesIntoPicture() { |
478 | SkPictureRecorder recorder; |
479 | SkCanvas* textCanvas = recorder.beginRecording(fOrigin.width(), fOrigin.height(), nullptr, 0); |
480 | textCanvas->translate(-fOrigin.fLeft, -fOrigin.fTop); |
481 | |
482 | for (auto& line : fLines) { |
483 | line.paint(textCanvas); |
484 | } |
485 | |
486 | fPicture = recorder.finishRecordingAsPicture(); |
487 | } |
488 | |
489 | void ParagraphImpl::resolveStrut() { |
490 | auto strutStyle = this->paragraphStyle().getStrutStyle(); |
491 | if (!strutStyle.getStrutEnabled() || strutStyle.getFontSize() < 0) { |
492 | return; |
493 | } |
494 | |
495 | std::vector<sk_sp<SkTypeface>> typefaces = fFontCollection->findTypefaces(strutStyle.getFontFamilies(), strutStyle.getFontStyle()); |
496 | if (typefaces.empty()) { |
497 | SkDEBUGF("Could not resolve strut font\n" ); |
498 | return; |
499 | } |
500 | |
501 | SkFont font(typefaces.front(), strutStyle.getFontSize()); |
502 | SkFontMetrics metrics; |
503 | font.getMetrics(&metrics); |
504 | |
505 | if (strutStyle.getHeightOverride()) { |
506 | auto strutHeight = metrics.fDescent - metrics.fAscent; |
507 | auto strutMultiplier = strutStyle.getHeight() * strutStyle.getFontSize(); |
508 | fStrutMetrics = InternalLineMetrics( |
509 | (metrics.fAscent / strutHeight) * strutMultiplier, |
510 | (metrics.fDescent / strutHeight) * strutMultiplier, |
511 | strutStyle.getLeading() < 0 ? 0 : strutStyle.getLeading() * strutStyle.getFontSize()); |
512 | } else { |
513 | fStrutMetrics = InternalLineMetrics( |
514 | metrics.fAscent, |
515 | metrics.fDescent, |
516 | strutStyle.getLeading() < 0 ? 0 |
517 | : strutStyle.getLeading() * strutStyle.getFontSize()); |
518 | } |
519 | fStrutMetrics.setForceStrut(this->paragraphStyle().getStrutStyle().getForceStrutHeight()); |
520 | } |
521 | |
522 | BlockRange ParagraphImpl::findAllBlocks(TextRange ) { |
523 | BlockIndex begin = EMPTY_BLOCK; |
524 | BlockIndex end = EMPTY_BLOCK; |
525 | for (size_t index = 0; index < fTextStyles.size(); ++index) { |
526 | auto& block = fTextStyles[index]; |
527 | if (block.fRange.end <= textRange.start) { |
528 | continue; |
529 | } |
530 | if (block.fRange.start >= textRange.end) { |
531 | break; |
532 | } |
533 | if (begin == EMPTY_BLOCK) { |
534 | begin = index; |
535 | } |
536 | end = index; |
537 | } |
538 | |
539 | return { begin, end + 1 }; |
540 | } |
541 | |
542 | void ParagraphImpl::calculateBoundaries() { |
543 | for (auto& line : fLines) { |
544 | fOrigin.joinPossiblyEmptyRect(line.calculateBoundaries()); |
545 | } |
546 | } |
547 | |
548 | TextLine& ParagraphImpl::addLine(SkVector offset, |
549 | SkVector advance, |
550 | TextRange text, |
551 | TextRange textWithSpaces, |
552 | ClusterRange clusters, |
553 | ClusterRange clustersWithGhosts, |
554 | SkScalar widthWithSpaces, |
555 | InternalLineMetrics sizes) { |
556 | // Define a list of styles that covers the line |
557 | auto blocks = findAllBlocks(text); |
558 | return fLines.emplace_back(this, offset, advance, blocks, text, textWithSpaces, clusters, clustersWithGhosts, widthWithSpaces, sizes); |
559 | } |
560 | |
561 | // Returns a vector of bounding boxes that enclose all text between |
562 | // start and end glyph indexes, including start and excluding end |
563 | std::vector<TextBox> ParagraphImpl::getRectsForRange(unsigned start, |
564 | unsigned end, |
565 | RectHeightStyle rectHeightStyle, |
566 | RectWidthStyle rectWidthStyle) { |
567 | std::vector<TextBox> results; |
568 | if (fText.isEmpty()) { |
569 | if (start == 0 && end > 0) { |
570 | // On account of implied "\n" that is always at the end of the text |
571 | //SkDebugf("getRectsForRange(%d, %d): %f\n", start, end, fHeight); |
572 | results.emplace_back(SkRect::MakeXYWH(0, 0, 0, fHeight), fParagraphStyle.getTextDirection()); |
573 | } |
574 | return results; |
575 | } |
576 | |
577 | ensureUTF16Mapping(); |
578 | |
579 | if (start >= end || start > fUTF8IndexForUTF16Index.size() || end == 0) { |
580 | return results; |
581 | } |
582 | |
583 | // Adjust the text to grapheme edges |
584 | // Apparently, text editor CAN move inside graphemes but CANNOT select a part of it. |
585 | // I don't know why - the solution I have here returns an empty box for every query that |
586 | // does not contain an end of a grapheme. |
587 | // Once a cursor is inside a complex grapheme I can press backspace and cause trouble. |
588 | // To avoid any problems, I will not allow any selection of a part of a grapheme. |
589 | // One flutter test fails because of it but the editing experience is correct |
590 | // (although you have to press the cursor many times before it moves to the next grapheme). |
591 | TextRange text(fText.size(), fText.size()); |
592 | if (start < fUTF8IndexForUTF16Index.size()) { |
593 | text.start = findGraphemeStart(fUTF8IndexForUTF16Index[start]); |
594 | } |
595 | if (end < fUTF8IndexForUTF16Index.size()) { |
596 | text.end = findGraphemeStart(fUTF8IndexForUTF16Index[end]); |
597 | } |
598 | |
599 | for (auto& line : fLines) { |
600 | auto lineText = line.textWithSpaces(); |
601 | auto intersect = lineText * text; |
602 | if (intersect.empty() && lineText.start != text.start) { |
603 | continue; |
604 | } |
605 | |
606 | line.getRectsForRange(intersect, rectHeightStyle, rectWidthStyle, results); |
607 | } |
608 | /* |
609 | SkDebugf("getRectsForRange(%d, %d)\n", start, end); |
610 | for (auto& r : results) { |
611 | r.rect.fLeft = littleRound(r.rect.fLeft); |
612 | r.rect.fRight = littleRound(r.rect.fRight); |
613 | r.rect.fTop = littleRound(r.rect.fTop); |
614 | r.rect.fBottom = littleRound(r.rect.fBottom); |
615 | SkDebugf("[%f:%f * %f:%f]\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom); |
616 | } |
617 | */ |
618 | return results; |
619 | } |
620 | |
621 | std::vector<TextBox> ParagraphImpl::getRectsForPlaceholders() { |
622 | std::vector<TextBox> boxes; |
623 | if (fText.isEmpty()) { |
624 | return boxes; |
625 | } |
626 | if (fPlaceholders.size() == 1) { |
627 | // We always have one fake placeholder |
628 | return boxes; |
629 | } |
630 | for (auto& line : fLines) { |
631 | line.getRectsForPlaceholders(boxes); |
632 | } |
633 | /* |
634 | SkDebugf("getRectsForPlaceholders('%s'): %d\n", fText.c_str(), boxes.size()); |
635 | for (auto& r : boxes) { |
636 | r.rect.fLeft = littleRound(r.rect.fLeft); |
637 | r.rect.fRight = littleRound(r.rect.fRight); |
638 | r.rect.fTop = littleRound(r.rect.fTop); |
639 | r.rect.fBottom = littleRound(r.rect.fBottom); |
640 | SkDebugf("[%f:%f * %f:%f] %s\n", r.rect.fLeft, r.rect.fRight, r.rect.fTop, r.rect.fBottom, |
641 | (r.direction == TextDirection::kLtr ? "left" : "right")); |
642 | } |
643 | */ |
644 | return boxes; |
645 | } |
646 | |
647 | // TODO: Optimize (save cluster <-> codepoint connection) |
648 | PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) { |
649 | |
650 | if (fText.isEmpty()) { |
651 | return {0, Affinity::kDownstream}; |
652 | } |
653 | |
654 | ensureUTF16Mapping(); |
655 | |
656 | for (auto& line : fLines) { |
657 | // Let's figure out if we can stop looking |
658 | auto offsetY = line.offset().fY; |
659 | if (dy >= offsetY + line.height() && &line != &fLines.back()) { |
660 | // This line is not good enough |
661 | continue; |
662 | } |
663 | |
664 | // This is so far the the line vertically closest to our coordinates |
665 | // (or the first one, or the only one - all the same) |
666 | |
667 | auto result = line.getGlyphPositionAtCoordinate(dx); |
668 | //SkDebugf("getGlyphPositionAtCoordinate(%f, %f): %d %s\n", dx, dy, result.position, |
669 | // result.affinity == Affinity::kUpstream ? "up" : "down"); |
670 | return result; |
671 | } |
672 | |
673 | return {0, Affinity::kDownstream}; |
674 | } |
675 | |
676 | // Finds the first and last glyphs that define a word containing |
677 | // the glyph at index offset. |
678 | // By "glyph" they mean a character index - indicated by Minikin's code |
679 | SkRange<size_t> ParagraphImpl::getWordBoundary(unsigned offset) { |
680 | |
681 | if (fWords.empty()) { |
682 | if (!fICU->getWords(fText.c_str(), fText.size(), &fWords)) { |
683 | return {0, 0 }; |
684 | } |
685 | } |
686 | |
687 | int32_t start = 0; |
688 | int32_t end = 0; |
689 | for (size_t i = 0; i < fWords.size(); ++i) { |
690 | auto word = fWords[i]; |
691 | if (word <= offset) { |
692 | start = word; |
693 | end = word; |
694 | } else if (word > offset) { |
695 | end = word; |
696 | break; |
697 | } |
698 | } |
699 | |
700 | //SkDebugf("getWordBoundary(%d): %d - %d\n", offset, start, end); |
701 | return { SkToU32(start), SkToU32(end) }; |
702 | } |
703 | |
704 | void ParagraphImpl::forEachCodeUnitPropertyRange(CodeUnitFlags property, CodeUnitRangeVisitor visitor) { |
705 | |
706 | size_t first = 0; |
707 | for (size_t i = 1; i < fText.size(); ++i) { |
708 | auto properties = fCodeUnitProperties[i]; |
709 | if (properties & property) { |
710 | visitor({first, i}); |
711 | first = i; |
712 | } |
713 | |
714 | } |
715 | visitor({first, fText.size()}); |
716 | } |
717 | |
718 | size_t ParagraphImpl::getWhitespacesLength(TextRange ) { |
719 | size_t len = 0; |
720 | for (auto i = textRange.start; i < textRange.end; ++i) { |
721 | auto properties = fCodeUnitProperties[i]; |
722 | if (properties & CodeUnitFlags::kPartOfWhiteSpace) { |
723 | ++len; |
724 | } |
725 | } |
726 | return len; |
727 | } |
728 | |
729 | void ParagraphImpl::getLineMetrics(std::vector<LineMetrics>& metrics) { |
730 | metrics.clear(); |
731 | for (auto& line : fLines) { |
732 | metrics.emplace_back(line.getMetrics()); |
733 | } |
734 | } |
735 | |
736 | SkSpan<const char> ParagraphImpl::text(TextRange ) { |
737 | SkASSERT(textRange.start <= fText.size() && textRange.end <= fText.size()); |
738 | auto start = fText.c_str() + textRange.start; |
739 | return SkSpan<const char>(start, textRange.width()); |
740 | } |
741 | |
742 | SkSpan<Cluster> ParagraphImpl::clusters(ClusterRange clusterRange) { |
743 | SkASSERT(clusterRange.start < fClusters.size() && clusterRange.end <= fClusters.size()); |
744 | return SkSpan<Cluster>(&fClusters[clusterRange.start], clusterRange.width()); |
745 | } |
746 | |
747 | Cluster& ParagraphImpl::cluster(ClusterIndex clusterIndex) { |
748 | SkASSERT(clusterIndex < fClusters.size()); |
749 | return fClusters[clusterIndex]; |
750 | } |
751 | |
752 | Run& ParagraphImpl::runByCluster(ClusterIndex clusterIndex) { |
753 | auto start = cluster(clusterIndex); |
754 | return this->run(start.fRunIndex); |
755 | } |
756 | |
757 | SkSpan<Block> ParagraphImpl::blocks(BlockRange blockRange) { |
758 | SkASSERT(blockRange.start < fTextStyles.size() && blockRange.end <= fTextStyles.size()); |
759 | return SkSpan<Block>(&fTextStyles[blockRange.start], blockRange.width()); |
760 | } |
761 | |
762 | Block& ParagraphImpl::block(BlockIndex blockIndex) { |
763 | SkASSERT(blockIndex < fTextStyles.size()); |
764 | return fTextStyles[blockIndex]; |
765 | } |
766 | |
767 | void ParagraphImpl::setState(InternalState state) { |
768 | if (fState <= state) { |
769 | fState = state; |
770 | return; |
771 | } |
772 | |
773 | fState = state; |
774 | switch (fState) { |
775 | case kUnknown: |
776 | fRuns.reset(); |
777 | fCodeUnitProperties.reset(); |
778 | fCodeUnitProperties.push_back_n(fText.size() + 1, kNoCodeUnitFlag); |
779 | fWords.clear(); |
780 | fBidiRegions.clear(); |
781 | fUTF8IndexForUTF16Index.reset(); |
782 | fUTF16IndexForUTF8Index.reset(); |
783 | [[fallthrough]]; |
784 | |
785 | case kShaped: |
786 | fClusters.reset(); |
787 | [[fallthrough]]; |
788 | |
789 | case kClusterized: |
790 | case kMarked: |
791 | case kLineBroken: |
792 | this->resetContext(); |
793 | this->resolveStrut(); |
794 | this->computeEmptyMetrics(); |
795 | this->resetShifts(); |
796 | fLines.reset(); |
797 | [[fallthrough]]; |
798 | |
799 | case kFormatted: |
800 | fPicture = nullptr; |
801 | [[fallthrough]]; |
802 | |
803 | case kDrawn: |
804 | default: |
805 | break; |
806 | } |
807 | } |
808 | |
809 | void ParagraphImpl::computeEmptyMetrics() { |
810 | auto defaultTextStyle = paragraphStyle().getTextStyle(); |
811 | |
812 | auto typefaces = fontCollection()->findTypefaces( |
813 | defaultTextStyle.getFontFamilies(), defaultTextStyle.getFontStyle()); |
814 | auto typeface = typefaces.empty() ? nullptr : typefaces.front(); |
815 | |
816 | SkFont font(typeface, defaultTextStyle.getFontSize()); |
817 | |
818 | fEmptyMetrics = InternalLineMetrics(font, paragraphStyle().getStrutStyle().getForceStrutHeight()); |
819 | if (!paragraphStyle().getStrutStyle().getForceStrutHeight() && |
820 | defaultTextStyle.getHeightOverride()) { |
821 | auto multiplier = |
822 | defaultTextStyle.getHeight() * defaultTextStyle.getFontSize() / fEmptyMetrics.height(); |
823 | fEmptyMetrics = InternalLineMetrics(fEmptyMetrics.ascent() * multiplier, |
824 | fEmptyMetrics.descent() * multiplier, |
825 | fEmptyMetrics.leading() * multiplier); |
826 | } |
827 | |
828 | if (fParagraphStyle.getStrutStyle().getStrutEnabled()) { |
829 | fStrutMetrics.updateLineMetrics(fEmptyMetrics); |
830 | } |
831 | } |
832 | |
833 | void ParagraphImpl::updateText(size_t from, SkString text) { |
834 | fText.remove(from, from + text.size()); |
835 | fText.insert(from, text); |
836 | fState = kUnknown; |
837 | fOldWidth = 0; |
838 | fOldHeight = 0; |
839 | } |
840 | |
841 | void ParagraphImpl::updateFontSize(size_t from, size_t to, SkScalar fontSize) { |
842 | |
843 | SkASSERT(from == 0 && to == fText.size()); |
844 | auto defaultStyle = fParagraphStyle.getTextStyle(); |
845 | defaultStyle.setFontSize(fontSize); |
846 | fParagraphStyle.setTextStyle(defaultStyle); |
847 | |
848 | for (auto& textStyle : fTextStyles) { |
849 | textStyle.fStyle.setFontSize(fontSize); |
850 | } |
851 | |
852 | fState = kUnknown; |
853 | fOldWidth = 0; |
854 | fOldHeight = 0; |
855 | } |
856 | |
857 | void ParagraphImpl::updateTextAlign(TextAlign textAlign) { |
858 | fParagraphStyle.setTextAlign(textAlign); |
859 | |
860 | if (fState >= kLineBroken) { |
861 | fState = kLineBroken; |
862 | } |
863 | } |
864 | |
865 | void ParagraphImpl::updateForegroundPaint(size_t from, size_t to, SkPaint paint) { |
866 | SkASSERT(from == 0 && to == fText.size()); |
867 | auto defaultStyle = fParagraphStyle.getTextStyle(); |
868 | defaultStyle.setForegroundColor(paint); |
869 | fParagraphStyle.setTextStyle(defaultStyle); |
870 | |
871 | for (auto& textStyle : fTextStyles) { |
872 | textStyle.fStyle.setForegroundColor(paint); |
873 | } |
874 | } |
875 | |
876 | void ParagraphImpl::updateBackgroundPaint(size_t from, size_t to, SkPaint paint) { |
877 | SkASSERT(from == 0 && to == fText.size()); |
878 | auto defaultStyle = fParagraphStyle.getTextStyle(); |
879 | defaultStyle.setBackgroundColor(paint); |
880 | fParagraphStyle.setTextStyle(defaultStyle); |
881 | |
882 | for (auto& textStyle : fTextStyles) { |
883 | textStyle.fStyle.setBackgroundColor(paint); |
884 | } |
885 | } |
886 | |
887 | TextIndex ParagraphImpl::findGraphemeStart(TextIndex index) { |
888 | if (index == fText.size()) { |
889 | return index; |
890 | } |
891 | while (index > 0 && |
892 | (fCodeUnitProperties[index] & CodeUnitFlags::kGraphemeStart) == 0) { |
893 | --index; |
894 | } |
895 | return index; |
896 | } |
897 | |
898 | void ParagraphImpl::ensureUTF16Mapping() { |
899 | if (!fUTF16IndexForUTF8Index.empty()) { |
900 | return; |
901 | } |
902 | // Fill out code points 16 |
903 | auto ptr = fText.c_str(); |
904 | auto end = fText.c_str() + fText.size(); |
905 | while (ptr < end) { |
906 | |
907 | size_t index = ptr - fText.c_str(); |
908 | SkUnichar u = SkUTF::NextUTF8(&ptr, end); |
909 | |
910 | // All utf8 units refer to the same codepoint |
911 | size_t next = ptr - fText.c_str(); |
912 | for (auto i = index; i < next; ++i) { |
913 | fUTF16IndexForUTF8Index.emplace_back(fUTF8IndexForUTF16Index.size()); |
914 | } |
915 | SkASSERT(fUTF16IndexForUTF8Index.size() == next); |
916 | |
917 | // One or two codepoints refer to the same text index |
918 | uint16_t buffer[2]; |
919 | size_t count = SkUTF::ToUTF16(u, buffer); |
920 | fUTF8IndexForUTF16Index.emplace_back(index); |
921 | if (count > 1) { |
922 | fUTF8IndexForUTF16Index.emplace_back(index); |
923 | } |
924 | } |
925 | fUTF16IndexForUTF8Index.emplace_back(fUTF8IndexForUTF16Index.size()); |
926 | fUTF8IndexForUTF16Index.emplace_back(fText.size()); |
927 | } |
928 | |
929 | } // namespace textlayout |
930 | } // namespace skia |
931 | |