1 | // Copyright 2019 Google LLC. |
2 | #include "include/core/SkBlurTypes.h" |
3 | #include "include/core/SkCanvas.h" |
4 | #include "include/core/SkFont.h" |
5 | #include "include/core/SkFontMetrics.h" |
6 | #include "include/core/SkMaskFilter.h" |
7 | #include "include/core/SkPaint.h" |
8 | #include "include/core/SkString.h" |
9 | #include "include/core/SkTextBlob.h" |
10 | #include "include/core/SkTypes.h" |
11 | #include "include/private/SkTemplates.h" |
12 | #include "include/private/SkTo.h" |
13 | #include "modules/skparagraph/include/DartTypes.h" |
14 | #include "modules/skparagraph/include/Metrics.h" |
15 | #include "modules/skparagraph/include/ParagraphStyle.h" |
16 | #include "modules/skparagraph/include/TextShadow.h" |
17 | #include "modules/skparagraph/include/TextStyle.h" |
18 | #include "modules/skparagraph/src/Decorations.h" |
19 | #include "modules/skparagraph/src/ParagraphImpl.h" |
20 | #include "modules/skparagraph/src/TextLine.h" |
21 | #include "modules/skshaper/include/SkShaper.h" |
22 | #include "src/core/SkSpan.h" |
23 | |
24 | #include <algorithm> |
25 | #include <iterator> |
26 | #include <limits> |
27 | #include <map> |
28 | #include <memory> |
29 | #include <tuple> |
30 | #include <type_traits> |
31 | #include <utility> |
32 | |
33 | namespace skia { |
34 | namespace textlayout { |
35 | |
36 | namespace { |
37 | |
38 | // TODO: deal with all the intersection functionality |
39 | TextRange intersected(const TextRange& a, const TextRange& b) { |
40 | if (a.start == b.start && a.end == b.end) return a; |
41 | auto begin = std::max(a.start, b.start); |
42 | auto end = std::min(a.end, b.end); |
43 | return end >= begin ? TextRange(begin, end) : EMPTY_TEXT; |
44 | } |
45 | |
46 | SkScalar littleRound(SkScalar a) { |
47 | // This rounding is done to match Flutter tests. Must be removed.. |
48 | return SkScalarRoundToScalar(a * 100.0)/100.0; |
49 | } |
50 | |
51 | TextRange operator*(const TextRange& a, const TextRange& b) { |
52 | if (a.start == b.start && a.end == b.end) return a; |
53 | auto begin = std::max(a.start, b.start); |
54 | auto end = std::min(a.end, b.end); |
55 | return end > begin ? TextRange(begin, end) : EMPTY_TEXT; |
56 | } |
57 | |
58 | int compareRound(SkScalar a, SkScalar b) { |
59 | // There is a rounding error that gets bigger when maxWidth gets bigger |
60 | // VERY long zalgo text (> 100000) on a VERY long line (> 10000) |
61 | // Canvas scaling affects it |
62 | // Letter spacing affects it |
63 | // It has to be relative to be useful |
64 | auto base = std::max(SkScalarAbs(a), SkScalarAbs(b)); |
65 | auto diff = SkScalarAbs(a - b); |
66 | if (nearlyZero(base) || diff / base < 0.001f) { |
67 | return 0; |
68 | } |
69 | |
70 | auto ra = littleRound(a); |
71 | auto rb = littleRound(b); |
72 | if (ra < rb) { |
73 | return -1; |
74 | } else { |
75 | return 1; |
76 | } |
77 | } |
78 | |
79 | } // namespace |
80 | |
81 | TextLine::TextLine(ParagraphImpl* owner, |
82 | SkVector offset, |
83 | SkVector advance, |
84 | BlockRange blocks, |
85 | TextRange text, |
86 | TextRange textWithSpaces, |
87 | ClusterRange clusters, |
88 | ClusterRange clustersWithGhosts, |
89 | SkScalar widthWithSpaces, |
90 | InternalLineMetrics sizes) |
91 | : fOwner(owner) |
92 | , fBlockRange(blocks) |
93 | , fTextRange(text) |
94 | , fTextWithWhitespacesRange(textWithSpaces) |
95 | , fClusterRange(clusters) |
96 | , fGhostClusterRange(clustersWithGhosts) |
97 | , fRunsInVisualOrder() |
98 | , fAdvance(advance) |
99 | , fOffset(offset) |
100 | , fShift(0.0) |
101 | , fWidthWithSpaces(widthWithSpaces) |
102 | , fEllipsis(nullptr) |
103 | , fSizes(sizes) |
104 | , fHasBackground(false) |
105 | , fHasShadows(false) |
106 | , fHasDecorations(false) |
107 | , fAscentStyle(LineMetricStyle::CSS) |
108 | , fDescentStyle(LineMetricStyle::CSS) { |
109 | // Reorder visual runs |
110 | auto& start = owner->cluster(fGhostClusterRange.start); |
111 | auto& end = owner->cluster(fGhostClusterRange.end - 1); |
112 | size_t numRuns = end.runIndex() - start.runIndex() + 1; |
113 | |
114 | for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { |
115 | auto b = fOwner->styles().begin() + index; |
116 | if (b->fStyle.isPlaceholder()) { |
117 | continue; |
118 | } |
119 | if (b->fStyle.hasBackground()) { |
120 | fHasBackground = true; |
121 | } |
122 | if (b->fStyle.getDecorationType() != TextDecoration::kNoDecoration) { |
123 | fHasDecorations = true; |
124 | } |
125 | if (b->fStyle.getShadowNumber() > 0) { |
126 | fHasShadows = true; |
127 | } |
128 | } |
129 | |
130 | // Get the logical order |
131 | |
132 | // This is just chosen to catch the common/fast cases. Feel free to tweak. |
133 | constexpr int kPreallocCount = 4; |
134 | SkAutoSTArray<kPreallocCount, BidiLevel> runLevels(numRuns); |
135 | size_t runLevelsIndex = 0; |
136 | for (auto runIndex = start.runIndex(); runIndex <= end.runIndex(); ++runIndex) { |
137 | auto& run = fOwner->run(runIndex); |
138 | runLevels[runLevelsIndex++] = run.fBidiLevel; |
139 | fMaxRunMetrics.add( |
140 | InternalLineMetrics(run.fFontMetrics.fAscent, run.fFontMetrics.fDescent, run.fFontMetrics.fLeading)); |
141 | } |
142 | SkASSERT(runLevelsIndex == numRuns); |
143 | |
144 | SkAutoSTArray<kPreallocCount, int32_t> logicalOrder(numRuns); |
145 | |
146 | // TODO: hide all these logic in SkUnicode? |
147 | fOwner->getICU()->reorderVisual(runLevels.data(), numRuns, logicalOrder.data()); |
148 | auto firstRunIndex = start.runIndex(); |
149 | for (auto index : logicalOrder) { |
150 | fRunsInVisualOrder.push_back(firstRunIndex + index); |
151 | } |
152 | |
153 | // TODO: This is the fix for flutter. Must be removed... |
154 | for (auto cluster = &start; cluster != &end; ++cluster) { |
155 | if (!cluster->run()->isPlaceholder()) { |
156 | fShift += cluster->getHalfLetterSpacing(); |
157 | break; |
158 | } |
159 | } |
160 | } |
161 | |
162 | SkRect TextLine::calculateBoundaries() { |
163 | |
164 | // For flutter: height and/or width and/or baseline! can be Inf |
165 | // (coming from placeholders - we should ignore it) |
166 | auto boundaries = SkRect::MakeWH( |
167 | SkScalarIsFinite(fAdvance.fX) ? fAdvance.fX : 0, |
168 | SkScalarIsFinite(fAdvance.fY) ? fAdvance.fY : 0); |
169 | auto baseline = SkScalarIsFinite(this->baseline()) ? this->baseline() : 0; |
170 | auto clusters = fOwner->clusters(fClusterRange); |
171 | Run* run = nullptr; |
172 | auto runShift = 0.0f; |
173 | auto clusterShift = 0.0f; |
174 | for (auto cluster = clusters.begin(); cluster != clusters.end(); ++cluster) { |
175 | if (run == nullptr || cluster->runIndex() != run->index()) { |
176 | run = &fOwner->run(cluster->runIndex()); |
177 | runShift += clusterShift; |
178 | clusterShift = 0; |
179 | } |
180 | clusterShift += cluster->width(); |
181 | for (auto i = cluster->startPos(); i < cluster->endPos(); ++i) { |
182 | auto posX = run->positionX(i); |
183 | auto posY = run->posY(i); |
184 | auto bounds = run->getBounds(i); |
185 | bounds.offset(posX + runShift, posY); |
186 | boundaries.joinPossiblyEmptyRect(bounds); |
187 | } |
188 | } |
189 | |
190 | // We need to take in account all the shadows when we calculate the boundaries |
191 | // TODO: Need to find a better solution |
192 | if (fHasShadows) { |
193 | SkRect shadowRect = SkRect::MakeEmpty(); |
194 | this->iterateThroughVisualRuns(false, |
195 | [this, &shadowRect, boundaries] |
196 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
197 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
198 | run, runOffsetInLine, textRange, StyleType::kShadow, |
199 | [&shadowRect, boundaries](TextRange , const TextStyle& style, const ClipContext& context) { |
200 | |
201 | for (TextShadow shadow : style.getShadows()) { |
202 | if (!shadow.hasShadow()) continue; |
203 | SkPaint paint; |
204 | paint.setColor(shadow.fColor); |
205 | if (shadow.fBlurRadius != 0.0) { |
206 | auto filter = SkMaskFilter::MakeBlur( |
207 | kNormal_SkBlurStyle, |
208 | SkDoubleToScalar(shadow.fBlurRadius), |
209 | false); |
210 | paint.setMaskFilter(filter); |
211 | SkRect bound; |
212 | paint.doComputeFastBounds(boundaries, &bound, SkPaint::Style::kFill_Style); |
213 | shadowRect.joinPossiblyEmptyRect(bound); |
214 | } |
215 | } |
216 | }); |
217 | return true; |
218 | }); |
219 | boundaries.fLeft += shadowRect.fLeft; |
220 | boundaries.fTop += shadowRect.fTop; |
221 | boundaries.fRight += shadowRect.fRight; |
222 | boundaries.fBottom += shadowRect.fBottom; |
223 | } |
224 | |
225 | boundaries.offset(this->offset()); // Line offset from the beginning of the para |
226 | boundaries.offset(0, baseline); // Down by baseline |
227 | |
228 | return boundaries; |
229 | } |
230 | |
231 | void TextLine::paint(SkCanvas* textCanvas) { |
232 | if (this->empty()) { |
233 | return; |
234 | } |
235 | |
236 | if (fHasBackground) { |
237 | this->iterateThroughVisualRuns(false, |
238 | [textCanvas, this] |
239 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
240 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
241 | run, runOffsetInLine, textRange, StyleType::kBackground, |
242 | [textCanvas, this](TextRange , const TextStyle& style, const ClipContext& context) { |
243 | this->paintBackground(textCanvas, textRange, style, context); |
244 | }); |
245 | return true; |
246 | }); |
247 | } |
248 | |
249 | if (fHasShadows) { |
250 | this->iterateThroughVisualRuns(false, |
251 | [textCanvas, this] |
252 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
253 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
254 | run, runOffsetInLine, textRange, StyleType::kShadow, |
255 | [textCanvas, this](TextRange , const TextStyle& style, const ClipContext& context) { |
256 | this->paintShadow(textCanvas, textRange, style, context); |
257 | }); |
258 | return true; |
259 | }); |
260 | } |
261 | |
262 | this->iterateThroughVisualRuns(false, |
263 | [textCanvas, this] |
264 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
265 | if (run->placeholderStyle() != nullptr) { |
266 | *runWidthInLine = run->advance().fX; |
267 | return true; |
268 | } |
269 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
270 | run, runOffsetInLine, textRange, StyleType::kForeground, |
271 | [textCanvas, this](TextRange , const TextStyle& style, const ClipContext& context) { |
272 | this->paintText(textCanvas, textRange, style, context); |
273 | }); |
274 | return true; |
275 | }); |
276 | |
277 | if (fHasDecorations) { |
278 | this->iterateThroughVisualRuns(false, |
279 | [textCanvas, this] |
280 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
281 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
282 | run, runOffsetInLine, textRange, StyleType::kDecorations, |
283 | [textCanvas, this](TextRange , const TextStyle& style, const ClipContext& context) { |
284 | this->paintDecorations(textCanvas, textRange, style, context); |
285 | }); |
286 | return true; |
287 | }); |
288 | } |
289 | } |
290 | |
291 | void TextLine::format(TextAlign align, SkScalar maxWidth) { |
292 | SkScalar delta = maxWidth - this->width(); |
293 | if (delta <= 0) { |
294 | return; |
295 | } |
296 | |
297 | // We do nothing for left align |
298 | if (align == TextAlign::kJustify) { |
299 | if (!this->endsWithHardLineBreak()) { |
300 | this->justify(maxWidth); |
301 | } else if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) { |
302 | // Justify -> Right align |
303 | fShift = delta; |
304 | } |
305 | } else if (align == TextAlign::kRight) { |
306 | fShift = delta; |
307 | } else if (align == TextAlign::kCenter) { |
308 | fShift = delta / 2; |
309 | } |
310 | } |
311 | |
312 | void TextLine::scanStyles(StyleType styleType, const RunStyleVisitor& visitor) { |
313 | if (this->empty()) { |
314 | return; |
315 | } |
316 | |
317 | this->iterateThroughVisualRuns(false, |
318 | [this, visitor, styleType](const Run* run, SkScalar runOffset, TextRange , SkScalar* width) { |
319 | *width = this->iterateThroughSingleRunByStyles( |
320 | run, runOffset, textRange, styleType, |
321 | [visitor](TextRange , const TextStyle& style, const ClipContext& context) { |
322 | visitor(textRange, style, context); |
323 | }); |
324 | return true; |
325 | }); |
326 | } |
327 | |
328 | SkRect TextLine::extendHeight(const ClipContext& context) const { |
329 | SkRect result = context.clip; |
330 | result.fBottom += std::max(this->fMaxRunMetrics.height() - this->height(), 0.0f); |
331 | return result; |
332 | } |
333 | |
334 | SkScalar TextLine::metricsWithoutMultiplier(TextHeightBehavior correction) { |
335 | |
336 | if (this->fSizes.getForceStrut()) { |
337 | return 0; |
338 | } |
339 | |
340 | InternalLineMetrics result; |
341 | this->iterateThroughVisualRuns(true, |
342 | [&result](const Run* run, SkScalar runOffset, TextRange , SkScalar* width) { |
343 | InternalLineMetrics runMetrics(run->ascent(), run->descent(), run->leading()); |
344 | result.add(runMetrics); |
345 | return true; |
346 | }); |
347 | SkScalar delta = 0; |
348 | if (correction == TextHeightBehavior::kDisableFirstAscent) { |
349 | delta += (this->fSizes.fAscent - result.fAscent); |
350 | this->fSizes.fAscent -= delta; |
351 | this->fAscentStyle = LineMetricStyle::Typographic; |
352 | } else if (correction == TextHeightBehavior::kDisableLastDescent) { |
353 | delta -= (this->fSizes.fDescent - result.fDescent); |
354 | this->fSizes.fDescent -= delta; |
355 | this->fDescentStyle = LineMetricStyle::Typographic; |
356 | } |
357 | fAdvance.fY += delta; |
358 | return delta; |
359 | } |
360 | |
361 | void TextLine::paintText(SkCanvas* canvas, TextRange , const TextStyle& style, const ClipContext& context) const { |
362 | |
363 | if (context.run->placeholderStyle() != nullptr) { |
364 | return; |
365 | } |
366 | |
367 | SkPaint paint; |
368 | if (style.hasForeground()) { |
369 | paint = style.getForeground(); |
370 | } else { |
371 | paint.setColor(style.getColor()); |
372 | } |
373 | |
374 | // TODO: This is the change for flutter, must be removed later |
375 | SkTextBlobBuilder builder; |
376 | context.run->copyTo(builder, SkToU32(context.pos), context.size); |
377 | if (context.clippingNeeded) { |
378 | canvas->save(); |
379 | canvas->clipRect(extendHeight(context).makeOffset(this->offset())); |
380 | } |
381 | |
382 | SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + 0.5); |
383 | canvas->drawTextBlob(builder.make(), |
384 | this->offset().fX + context.fTextShift, this->offset().fY + correctedBaseline, paint); |
385 | |
386 | if (context.clippingNeeded) { |
387 | canvas->restore(); |
388 | } |
389 | } |
390 | |
391 | void TextLine::paintBackground(SkCanvas* canvas, TextRange , const TextStyle& style, const ClipContext& context) const { |
392 | if (style.hasBackground()) { |
393 | canvas->drawRect(context.clip.makeOffset(this->offset()), style.getBackground()); |
394 | } |
395 | } |
396 | |
397 | void TextLine::paintShadow(SkCanvas* canvas, TextRange , const TextStyle& style, const ClipContext& context) const { |
398 | auto shiftDown = this->baseline(); |
399 | for (TextShadow shadow : style.getShadows()) { |
400 | if (!shadow.hasShadow()) continue; |
401 | |
402 | SkPaint paint; |
403 | paint.setColor(shadow.fColor); |
404 | if (shadow.fBlurRadius != 0.0) { |
405 | auto filter = SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, |
406 | SkDoubleToScalar(shadow.fBlurRadius), false); |
407 | paint.setMaskFilter(filter); |
408 | } |
409 | |
410 | SkTextBlobBuilder builder; |
411 | context.run->copyTo(builder, context.pos, context.size); |
412 | |
413 | if (context.clippingNeeded) { |
414 | canvas->save(); |
415 | SkRect clip = extendHeight(context); |
416 | clip.offset(this->offset()); |
417 | canvas->clipRect(clip); |
418 | } |
419 | canvas->drawTextBlob(builder.make(), |
420 | this->offset().fX + shadow.fOffset.x() + context.fTextShift, |
421 | this->offset().fY + shadow.fOffset.y() + shiftDown, |
422 | paint); |
423 | |
424 | if (context.clippingNeeded) { |
425 | canvas->restore(); |
426 | } |
427 | } |
428 | } |
429 | |
430 | void TextLine::paintDecorations(SkCanvas* canvas, TextRange , const TextStyle& style, const ClipContext& context) const { |
431 | |
432 | SkAutoCanvasRestore acr(canvas, true); |
433 | canvas->translate(this->offset().fX, this->offset().fY); |
434 | Decorations decorations; |
435 | SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + 0.5); |
436 | decorations.paint(canvas, style, context, correctedBaseline, this->offset()); |
437 | } |
438 | |
439 | void TextLine::justify(SkScalar maxWidth) { |
440 | // Count words and the extra spaces to spread across the line |
441 | // TODO: do it at the line breaking?.. |
442 | size_t whitespacePatches = 0; |
443 | SkScalar textLen = 0; |
444 | bool whitespacePatch = false; |
445 | this->iterateThroughClustersInGlyphsOrder(false, false, |
446 | [&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster, bool ghost) { |
447 | if (cluster->isWhitespaces()) { |
448 | if (!whitespacePatch) { |
449 | whitespacePatch = true; |
450 | ++whitespacePatches; |
451 | } |
452 | } else { |
453 | whitespacePatch = false; |
454 | } |
455 | textLen += cluster->width(); |
456 | return true; |
457 | }); |
458 | |
459 | if (whitespacePatches == 0) { |
460 | return; |
461 | } |
462 | |
463 | SkScalar step = (maxWidth - textLen) / whitespacePatches; |
464 | SkScalar shift = 0; |
465 | |
466 | // Deal with the ghost spaces |
467 | auto ghostShift = maxWidth - this->fAdvance.fX; |
468 | // Spread the extra whitespaces |
469 | whitespacePatch = false; |
470 | this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, bool ghost) { |
471 | |
472 | if (ghost) { |
473 | if (cluster->run()->leftToRight()) { |
474 | shiftCluster(cluster, ghostShift, ghostShift); |
475 | } |
476 | return true; |
477 | } |
478 | |
479 | auto prevShift = shift; |
480 | if (cluster->isWhitespaces()) { |
481 | if (!whitespacePatch) { |
482 | shift += step; |
483 | whitespacePatch = true; |
484 | --whitespacePatches; |
485 | } |
486 | } else { |
487 | whitespacePatch = false; |
488 | } |
489 | shiftCluster(cluster, shift, prevShift); |
490 | return true; |
491 | }); |
492 | |
493 | SkAssertResult(nearlyEqual(shift, maxWidth - textLen)); |
494 | SkASSERT(whitespacePatches == 0); |
495 | |
496 | this->fWidthWithSpaces += ghostShift; |
497 | this->fAdvance.fX = maxWidth; |
498 | } |
499 | |
500 | void TextLine::shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift) { |
501 | |
502 | auto run = cluster->run(); |
503 | auto start = cluster->startPos(); |
504 | auto end = cluster->endPos(); |
505 | |
506 | if (end == run->size()) { |
507 | // Set the same shift for the fake last glyph (to avoid all extra checks) |
508 | ++end; |
509 | } |
510 | |
511 | if (run->fJustificationShifts.empty()) { |
512 | // Do not fill this array until needed |
513 | run->fJustificationShifts.push_back_n(run->size() + 1, { 0, 0 }); |
514 | } |
515 | |
516 | for (size_t pos = start; pos < end; ++pos) { |
517 | run->fJustificationShifts[pos] = { shift, prevShift }; |
518 | } |
519 | } |
520 | |
521 | void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) { |
522 | // Replace some clusters with the ellipsis |
523 | // Go through the clusters in the reverse logical order |
524 | // taking off cluster by cluster until the ellipsis fits |
525 | SkScalar width = fAdvance.fX; |
526 | |
527 | auto attachEllipsis = [&](const Cluster* cluster){ |
528 | // Shape the ellipsis |
529 | std::unique_ptr<Run> run = shapeEllipsis(ellipsis, cluster->run()); |
530 | run->fClusterStart = cluster->textRange().start; |
531 | run->setOwner(fOwner); |
532 | |
533 | // See if it fits |
534 | if (width + run->advance().fX > maxWidth) { |
535 | width -= cluster->width(); |
536 | // Continue if it's not |
537 | return false; |
538 | } |
539 | |
540 | fEllipsis = std::move(run); |
541 | fEllipsis->shift(width, 0); |
542 | fAdvance.fX = width; |
543 | return true; |
544 | }; |
545 | |
546 | iterateThroughClustersInGlyphsOrder( |
547 | true, false, [&](const Cluster* cluster, bool ghost) { |
548 | return !attachEllipsis(cluster); |
549 | }); |
550 | |
551 | if (!fEllipsis) { |
552 | // Weird situation: just the ellipsis on the line (if it fits) |
553 | attachEllipsis(&fOwner->cluster(clusters().start)); |
554 | } |
555 | } |
556 | |
557 | std::unique_ptr<Run> TextLine::shapeEllipsis(const SkString& ellipsis, Run* run) { |
558 | |
559 | class ShapeHandler final : public SkShaper::RunHandler { |
560 | public: |
561 | ShapeHandler(SkScalar lineHeight, const SkString& ellipsis) |
562 | : fRun(nullptr), fLineHeight(lineHeight), fEllipsis(ellipsis) {} |
563 | Run* run() & { return fRun.get(); } |
564 | std::unique_ptr<Run> run() && { return std::move(fRun); } |
565 | |
566 | private: |
567 | void beginLine() override {} |
568 | |
569 | void runInfo(const RunInfo&) override {} |
570 | |
571 | void commitRunInfo() override {} |
572 | |
573 | Buffer runBuffer(const RunInfo& info) override { |
574 | SkASSERT(!fRun); |
575 | fRun = std::make_unique<Run>(nullptr, info, 0, fLineHeight, 0, 0); |
576 | return fRun->newRunBuffer(); |
577 | } |
578 | |
579 | void commitRunBuffer(const RunInfo& info) override { |
580 | fRun->fAdvance.fX = info.fAdvance.fX; |
581 | fRun->fAdvance.fY = fRun->advance().fY; |
582 | fRun->fPlaceholderIndex = std::numeric_limits<size_t>::max(); |
583 | fRun->fEllipsis = true; |
584 | } |
585 | |
586 | void commitLine() override {} |
587 | |
588 | std::unique_ptr<Run> fRun; |
589 | SkScalar fLineHeight; |
590 | SkString fEllipsis; |
591 | }; |
592 | |
593 | ShapeHandler handler(run->heightMultiplier(), ellipsis); |
594 | std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder(); |
595 | SkASSERT_RELEASE(shaper != nullptr); |
596 | shaper->shape(ellipsis.c_str(), ellipsis.size(), run->font(), true, |
597 | std::numeric_limits<SkScalar>::max(), &handler); |
598 | handler.run()->fTextRange = TextRange(0, ellipsis.size()); |
599 | handler.run()->fOwner = fOwner; |
600 | return std::move(handler).run(); |
601 | } |
602 | |
603 | TextLine::ClipContext TextLine::measureTextInsideOneRun(TextRange , |
604 | const Run* run, |
605 | SkScalar runOffsetInLine, |
606 | SkScalar textOffsetInRunInLine, |
607 | bool includeGhostSpaces, |
608 | bool limitToClusters) const { |
609 | ClipContext result = { run, 0, run->size(), 0, SkRect::MakeEmpty(), false }; |
610 | |
611 | if (run->fEllipsis) { |
612 | // Both ellipsis and placeholders can only be measured as one glyph |
613 | SkASSERT(textRange == run->textRange()); |
614 | result.fTextShift = runOffsetInLine; |
615 | result.clip = SkRect::MakeXYWH(runOffsetInLine, |
616 | sizes().runTop(run, this->fAscentStyle), |
617 | run->advance().fX, |
618 | run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); |
619 | return result; |
620 | } else if (run->isPlaceholder()) { |
621 | if (SkScalarIsFinite(run->fFontMetrics.fAscent)) { |
622 | result.clip = SkRect::MakeXYWH(runOffsetInLine, |
623 | sizes().runTop(run, this->fAscentStyle), |
624 | run->advance().fX, |
625 | run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); |
626 | } else { |
627 | result.clip = SkRect::MakeXYWH(runOffsetInLine, run->fFontMetrics.fAscent, run->advance().fX, 0); |
628 | } |
629 | return result; |
630 | } |
631 | // Find [start:end] clusters for the text |
632 | bool found; |
633 | ClusterIndex startIndex; |
634 | ClusterIndex endIndex; |
635 | std::tie(found, startIndex, endIndex) = run->findLimitingClusters(textRange); |
636 | if (!found) { |
637 | SkASSERT(textRange.empty() || limitToClusters); |
638 | return result; |
639 | } |
640 | |
641 | auto start = &fOwner->cluster(startIndex); |
642 | auto end = &fOwner->cluster(endIndex); |
643 | result.pos = start->startPos(); |
644 | result.size = (end->isHardBreak() ? end->startPos() : end->endPos()) - start->startPos(); |
645 | |
646 | auto textStartInRun = run->positionX(start->startPos()); |
647 | auto textStartInLine = runOffsetInLine + textOffsetInRunInLine; |
648 | /* |
649 | if (!run->fJustificationShifts.empty()) { |
650 | SkDebugf("Justification for [%d:%d)\n", textRange.start, textRange.end); |
651 | for (auto i = result.pos; i < result.pos + result.size; ++i) { |
652 | auto j = run->fJustificationShifts[i]; |
653 | SkDebugf("[%d] = %f %f\n", i, j.fX, j.fY); |
654 | } |
655 | } |
656 | */ |
657 | // Calculate the clipping rectangle for the text with cluster edges |
658 | // There are 2 cases: |
659 | // EOL (when we expect the last cluster clipped without any spaces) |
660 | // Anything else (when we want the cluster width contain all the spaces - |
661 | // coming from letter spacing or word spacing or justification) |
662 | result.clip = |
663 | SkRect::MakeXYWH(0, |
664 | sizes().runTop(run, this->fAscentStyle), |
665 | run->calculateWidth(result.pos, result.pos + result.size, false), |
666 | run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); |
667 | |
668 | // Correct the width in case the text edges don't match clusters |
669 | // TODO: This is where we get smart about selecting a part of a cluster |
670 | // by shaping each grapheme separately and then use the result sizes |
671 | // to calculate the proportions |
672 | auto leftCorrection = start->sizeToChar(textRange.start); |
673 | auto rightCorrection = end->sizeFromChar(textRange.end - 1); |
674 | result.clip.fLeft += leftCorrection; |
675 | result.clip.fRight -= rightCorrection; |
676 | result.clippingNeeded = leftCorrection != 0 || rightCorrection != 0; |
677 | |
678 | textStartInLine -= leftCorrection; |
679 | result.clip.offset(textStartInLine, 0); |
680 | |
681 | if (compareRound(result.clip.fRight, fAdvance.fX) > 0 && !includeGhostSpaces) { |
682 | // There are few cases when we need it. |
683 | // The most important one: we measure the text with spaces at the end |
684 | // and we should ignore these spaces |
685 | result.clippingNeeded = true; |
686 | result.clip.fRight = fAdvance.fX; |
687 | } |
688 | |
689 | if (result.clip.width() < 0) { |
690 | // Weird situation when glyph offsets move the glyph to the left |
691 | // (happens with zalgo texts, for instance) |
692 | result.clip.fRight = result.clip.fLeft; |
693 | } |
694 | |
695 | // The text must be aligned with the lineOffset |
696 | result.fTextShift = textStartInLine - textStartInRun; |
697 | |
698 | return result; |
699 | } |
700 | |
701 | void TextLine::iterateThroughClustersInGlyphsOrder(bool reversed, |
702 | bool includeGhosts, |
703 | const ClustersVisitor& visitor) const { |
704 | // Walk through the clusters in the logical order (or reverse) |
705 | SkSpan<const size_t> runs(fRunsInVisualOrder.data(), fRunsInVisualOrder.size()); |
706 | bool ignore = false; |
707 | directional_for_each(runs, !reversed, [&](decltype(runs[0]) r) { |
708 | if (ignore) return; |
709 | auto run = this->fOwner->run(r); |
710 | auto trimmedRange = fClusterRange.intersection(run.clusterRange()); |
711 | auto trailedRange = fGhostClusterRange.intersection(run.clusterRange()); |
712 | SkASSERT(trimmedRange.start == trailedRange.start); |
713 | |
714 | auto trailed = fOwner->clusters(trailedRange); |
715 | auto trimmed = fOwner->clusters(trimmedRange); |
716 | directional_for_each(trailed, reversed != run.leftToRight(), [&](Cluster& cluster) { |
717 | if (ignore) return; |
718 | bool ghost = &cluster >= trimmed.end(); |
719 | if (!includeGhosts && ghost) { |
720 | return; |
721 | } |
722 | if (!visitor(&cluster, ghost)) { |
723 | ignore = true; |
724 | return; |
725 | } |
726 | }); |
727 | }); |
728 | } |
729 | |
730 | SkScalar TextLine::iterateThroughSingleRunByStyles(const Run* run, |
731 | SkScalar runOffset, |
732 | TextRange , |
733 | StyleType styleType, |
734 | const RunStyleVisitor& visitor) const { |
735 | |
736 | if (run->fEllipsis) { |
737 | // Extra efforts to get the ellipsis text style |
738 | ClipContext clipContext = this->measureTextInsideOneRun(run->textRange(), run, runOffset, |
739 | 0, false, false); |
740 | TextRange testRange(run->fClusterStart, run->fClusterStart + 1); |
741 | for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { |
742 | auto block = fOwner->styles().begin() + index; |
743 | auto intersect = intersected(block->fRange, testRange); |
744 | if (intersect.width() > 0) { |
745 | visitor(textRange, block->fStyle, clipContext); |
746 | return run->advance().fX; |
747 | } |
748 | } |
749 | SkASSERT(false); |
750 | } |
751 | |
752 | if (styleType == StyleType::kNone) { |
753 | ClipContext clipContext = this->measureTextInsideOneRun(textRange, run, runOffset, |
754 | 0, false, false); |
755 | if (clipContext.clip.height() > 0) { |
756 | visitor(textRange, TextStyle(), clipContext); |
757 | return clipContext.clip.width(); |
758 | } else { |
759 | return 0; |
760 | } |
761 | } |
762 | |
763 | TextIndex start = EMPTY_INDEX; |
764 | size_t size = 0; |
765 | const TextStyle* prevStyle = nullptr; |
766 | SkScalar textOffsetInRun = 0; |
767 | for (BlockIndex index = fBlockRange.start; index <= fBlockRange.end; ++index) { |
768 | |
769 | TextRange intersect; |
770 | TextStyle* style = nullptr; |
771 | if (index < fBlockRange.end) { |
772 | auto block = fOwner->styles().begin() + index; |
773 | |
774 | // Get the text |
775 | intersect = intersected(block->fRange, textRange); |
776 | if (intersect.width() == 0) { |
777 | if (start == EMPTY_INDEX) { |
778 | // This style is not applicable to the text yet |
779 | continue; |
780 | } else { |
781 | // We have found all the good styles already |
782 | // but we need to process the last one of them |
783 | intersect = TextRange(start, start + size); |
784 | index = fBlockRange.end; |
785 | } |
786 | } else { |
787 | // Get the style |
788 | style = &block->fStyle; |
789 | if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) { |
790 | size += intersect.width(); |
791 | continue; |
792 | } else if (start == EMPTY_INDEX ) { |
793 | // First time only |
794 | prevStyle = style; |
795 | size = intersect.width(); |
796 | start = intersect.start; |
797 | continue; |
798 | } |
799 | } |
800 | } else if (prevStyle != nullptr) { |
801 | // This is the last style |
802 | } else { |
803 | break; |
804 | } |
805 | |
806 | // We have the style and the text |
807 | auto = TextRange(start, start + size); |
808 | // Measure the text |
809 | ClipContext clipContext = this->measureTextInsideOneRun(runStyleTextRange, run, runOffset, |
810 | textOffsetInRun, false, false); |
811 | if (clipContext.clip.height() == 0) { |
812 | continue; |
813 | } |
814 | visitor(runStyleTextRange, *prevStyle, clipContext); |
815 | textOffsetInRun += clipContext.clip.width(); |
816 | |
817 | // Start all over again |
818 | prevStyle = style; |
819 | start = intersect.start; |
820 | size = intersect.width(); |
821 | } |
822 | return textOffsetInRun; |
823 | } |
824 | |
825 | void TextLine::iterateThroughVisualRuns(bool includingGhostSpaces, const RunVisitor& visitor) const { |
826 | |
827 | // Walk through all the runs that intersect with the line in visual order |
828 | SkScalar width = 0; |
829 | SkScalar runOffset = 0; |
830 | SkScalar totalWidth = 0; |
831 | auto = includingGhostSpaces ? this->textWithSpaces() : this->trimmedText(); |
832 | for (auto& runIndex : fRunsInVisualOrder) { |
833 | |
834 | const auto run = &this->fOwner->run(runIndex); |
835 | auto lineIntersection = intersected(run->textRange(), textRange); |
836 | if (lineIntersection.width() == 0 && this->width() != 0) { |
837 | // TODO: deal with empty runs in a better way |
838 | continue; |
839 | } |
840 | if (!run->leftToRight() && runOffset == 0 && includingGhostSpaces) { |
841 | // runOffset does not take in account a possibility |
842 | // that RTL run could start before the line (trailing spaces) |
843 | // so we need to do runOffset -= "trailing whitespaces length" |
844 | TextRange whitespaces = intersected( |
845 | TextRange(fTextRange.end, fTextWithWhitespacesRange.end), run->fTextRange); |
846 | if (whitespaces.width() > 0) { |
847 | auto whitespacesLen = measureTextInsideOneRun(whitespaces, run, runOffset, 0, true, false).clip.width(); |
848 | runOffset -= whitespacesLen; |
849 | } |
850 | } |
851 | runOffset += width; |
852 | totalWidth += width; |
853 | if (!visitor(run, runOffset, lineIntersection, &width)) { |
854 | return; |
855 | } |
856 | } |
857 | |
858 | runOffset += width; |
859 | totalWidth += width; |
860 | |
861 | if (this->ellipsis() != nullptr) { |
862 | if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) { |
863 | totalWidth += width; |
864 | } |
865 | } |
866 | |
867 | // This is a very important assert! |
868 | // It asserts that 2 different ways of calculation come with the same results |
869 | if (!includingGhostSpaces && compareRound(totalWidth, this->width()) != 0) { |
870 | SkDebugf("ASSERT: %f != %f\n" , totalWidth, this->width()); |
871 | SkASSERT(false); |
872 | } |
873 | } |
874 | |
875 | SkVector TextLine::offset() const { |
876 | return fOffset + SkVector::Make(fShift, 0); |
877 | } |
878 | |
879 | LineMetrics TextLine::getMetrics() const { |
880 | LineMetrics result; |
881 | |
882 | // Fill out the metrics |
883 | result.fStartIndex = fTextRange.start; |
884 | result.fEndIndex = fTextWithWhitespacesRange.end; |
885 | result.fEndExcludingWhitespaces = fTextRange.end; |
886 | result.fEndIncludingNewline = fTextWithWhitespacesRange.end; // TODO: implement |
887 | result.fHardBreak = endsWithHardLineBreak(); |
888 | result.fAscent = - fMaxRunMetrics.ascent(); |
889 | result.fDescent = fMaxRunMetrics.descent(); |
890 | result.fUnscaledAscent = - fMaxRunMetrics.ascent(); // TODO: implement |
891 | result.fHeight = littleRound(fAdvance.fY); |
892 | result.fWidth = littleRound(fAdvance.fX); |
893 | result.fLeft = this->offset().fX; |
894 | // This is Flutter definition of a baseline |
895 | result.fBaseline = this->offset().fY + this->height() - this->sizes().descent(); |
896 | result.fLineNumber = this - fOwner->lines().begin(); |
897 | |
898 | // Fill out the style parts |
899 | this->iterateThroughVisualRuns(false, |
900 | [this, &result] |
901 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
902 | if (run->placeholderStyle() != nullptr) { |
903 | *runWidthInLine = run->advance().fX; |
904 | return true; |
905 | } |
906 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
907 | run, runOffsetInLine, textRange, StyleType::kForeground, |
908 | [&result, &run](TextRange , const TextStyle& style, const ClipContext& context) { |
909 | SkFontMetrics fontMetrics; |
910 | run->fFont.getMetrics(&fontMetrics); |
911 | StyleMetrics styleMetrics(&style, fontMetrics); |
912 | result.fLineMetrics.emplace(textRange.start, styleMetrics); |
913 | }); |
914 | return true; |
915 | }); |
916 | |
917 | return result; |
918 | } |
919 | |
920 | bool TextLine::isFirstLine() { |
921 | return this == &fOwner->lines().front(); |
922 | } |
923 | |
924 | bool TextLine::isLastLine() { |
925 | return this == &fOwner->lines().back(); |
926 | } |
927 | |
928 | bool TextLine::endsWithHardLineBreak() const { |
929 | // TODO: For some reason Flutter imagines a hard line break at the end of the last line. |
930 | // To be removed... |
931 | return fOwner->cluster(fGhostClusterRange.end - 1).isHardBreak() || |
932 | fEllipsis != nullptr || |
933 | fGhostClusterRange.end == fOwner->clusters().size() - 1; |
934 | } |
935 | |
936 | void TextLine::getRectsForRange(TextRange , |
937 | RectHeightStyle rectHeightStyle, |
938 | RectWidthStyle rectWidthStyle, |
939 | std::vector<TextBox>& boxes) |
940 | { |
941 | const Run* lastRun = nullptr; |
942 | auto startBox = boxes.size(); |
943 | this->iterateThroughVisualRuns(true, |
944 | [textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this] |
945 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
946 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
947 | run, runOffsetInLine, textRange, StyleType::kNone, |
948 | [run, runOffsetInLine, textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this] |
949 | (TextRange , const TextStyle& style, const TextLine::ClipContext& lineContext) { |
950 | |
951 | auto intersect = textRange * textRange0; |
952 | if (intersect.empty()) { |
953 | return true; |
954 | } |
955 | |
956 | auto paragraphStyle = fOwner->paragraphStyle(); |
957 | |
958 | // Found a run that intersects with the text |
959 | auto context = this->measureTextInsideOneRun(intersect, run, runOffsetInLine, 0, true, true); |
960 | SkRect clip = context.clip; |
961 | clip.offset(lineContext.fTextShift - context.fTextShift, 0); |
962 | |
963 | switch (rectHeightStyle) { |
964 | case RectHeightStyle::kMax: |
965 | // TODO: Change it once flutter rolls into google3 |
966 | // (probably will break things if changed before) |
967 | clip.fBottom = this->height(); |
968 | clip.fTop = this->sizes().delta(); |
969 | break; |
970 | case RectHeightStyle::kIncludeLineSpacingTop: { |
971 | if (isFirstLine()) { |
972 | auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic); |
973 | clip.fTop += verticalShift; |
974 | } |
975 | break; |
976 | } |
977 | case RectHeightStyle::kIncludeLineSpacingMiddle: { |
978 | auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic); |
979 | clip.fTop += isFirstLine() ? verticalShift : verticalShift / 2; |
980 | clip.fBottom += isLastLine() ? 0 : verticalShift / 2; |
981 | break; |
982 | } |
983 | case RectHeightStyle::kIncludeLineSpacingBottom: { |
984 | auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic); |
985 | clip.offset(0, verticalShift); |
986 | if (isLastLine()) { |
987 | clip.fBottom -= verticalShift; |
988 | } |
989 | break; |
990 | } |
991 | case RectHeightStyle::kStrut: { |
992 | const auto& strutStyle = paragraphStyle.getStrutStyle(); |
993 | if (strutStyle.getStrutEnabled() |
994 | && strutStyle.getFontSize() > 0) { |
995 | auto strutMetrics = fOwner->strutMetrics(); |
996 | auto top = this->baseline(); |
997 | clip.fTop = top + strutMetrics.ascent(); |
998 | clip.fBottom = top + strutMetrics.descent(); |
999 | } |
1000 | } |
1001 | break; |
1002 | case RectHeightStyle::kTight: { |
1003 | if (run->fHeightMultiplier > 0) { |
1004 | // This is a special case when we do not need to take in account this height multiplier |
1005 | auto correctedHeight = clip.height() / run->fHeightMultiplier; |
1006 | auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic); |
1007 | clip.fTop += verticalShift; |
1008 | clip.fBottom = clip.fTop + correctedHeight; |
1009 | } |
1010 | } |
1011 | break; |
1012 | default: |
1013 | SkASSERT(false); |
1014 | break; |
1015 | } |
1016 | |
1017 | // Separate trailing spaces and move them in the default order of the paragraph |
1018 | // in case the run order and the paragraph order don't match |
1019 | SkRect trailingSpaces = SkRect::MakeEmpty(); |
1020 | if (this->trimmedText().end < this->textWithSpaces().end && // Line has trailing spaces |
1021 | this->textWithSpaces().end == intersect.end && // Range is at the end of the line |
1022 | this->trimmedText().end > intersect.start) // Range has more than just spaces |
1023 | { |
1024 | auto delta = this->spacesWidth(); |
1025 | trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0); |
1026 | // There are trailing spaces in this run |
1027 | if (paragraphStyle.getTextAlign() == TextAlign::kJustify && isLastLine()) |
1028 | { |
1029 | // TODO: this is just a patch. Make it right later (when it's clear what and how) |
1030 | trailingSpaces = clip; |
1031 | if(run->leftToRight()) { |
1032 | trailingSpaces.fLeft = this->width(); |
1033 | clip.fRight = this->width(); |
1034 | } else { |
1035 | trailingSpaces.fRight = 0; |
1036 | clip.fLeft = 0; |
1037 | } |
1038 | } else if (paragraphStyle.getTextDirection() == TextDirection::kRtl && |
1039 | !run->leftToRight()) |
1040 | { |
1041 | // Split |
1042 | trailingSpaces = clip; |
1043 | trailingSpaces.fLeft = - delta; |
1044 | trailingSpaces.fRight = 0; |
1045 | clip.fLeft += delta; |
1046 | } else if (paragraphStyle.getTextDirection() == TextDirection::kLtr && |
1047 | run->leftToRight()) |
1048 | { |
1049 | // Split |
1050 | trailingSpaces = clip; |
1051 | trailingSpaces.fLeft = this->width(); |
1052 | trailingSpaces.fRight = trailingSpaces.fLeft + delta; |
1053 | clip.fRight -= delta; |
1054 | } |
1055 | } |
1056 | |
1057 | clip.offset(this->offset()); |
1058 | if (trailingSpaces.width() > 0) { |
1059 | trailingSpaces.offset(this->offset()); |
1060 | } |
1061 | |
1062 | // Check if we can merge two boxes instead of adding a new one |
1063 | auto merge = [&lastRun, &context, &boxes](SkRect clip) { |
1064 | bool mergedBoxes = false; |
1065 | if (!boxes.empty() && |
1066 | lastRun != nullptr && |
1067 | lastRun->placeholderStyle() == nullptr && |
1068 | context.run->placeholderStyle() == nullptr && |
1069 | nearlyEqual(lastRun->heightMultiplier(), |
1070 | context.run->heightMultiplier()) && |
1071 | lastRun->font() == context.run->font()) |
1072 | { |
1073 | auto& lastBox = boxes.back(); |
1074 | if (nearlyEqual(lastBox.rect.fTop, clip.fTop) && |
1075 | nearlyEqual(lastBox.rect.fBottom, clip.fBottom) && |
1076 | (nearlyEqual(lastBox.rect.fLeft, clip.fRight) || |
1077 | nearlyEqual(lastBox.rect.fRight, clip.fLeft))) |
1078 | { |
1079 | lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft); |
1080 | lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight); |
1081 | mergedBoxes = true; |
1082 | } |
1083 | } |
1084 | lastRun = context.run; |
1085 | return mergedBoxes; |
1086 | }; |
1087 | |
1088 | if (!merge(clip)) { |
1089 | boxes.emplace_back(clip, context.run->getTextDirection()); |
1090 | } |
1091 | if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) { |
1092 | boxes.emplace_back(trailingSpaces, paragraphStyle.getTextDirection()); |
1093 | } |
1094 | |
1095 | if (rectWidthStyle == RectWidthStyle::kMax && !isLastLine()) { |
1096 | // Align the very left/right box horizontally |
1097 | auto lineStart = this->offset().fX; |
1098 | auto lineEnd = this->offset().fX + this->width(); |
1099 | auto left = boxes[startBox]; |
1100 | auto right = boxes.back(); |
1101 | if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) { |
1102 | left.rect.fRight = left.rect.fLeft; |
1103 | left.rect.fLeft = 0; |
1104 | boxes.insert(boxes.begin() + startBox + 1, left); |
1105 | } |
1106 | if (right.direction == TextDirection::kLtr && |
1107 | right.rect.fRight >= lineEnd && |
1108 | right.rect.fRight < fOwner->widthWithTrailingSpaces()) { |
1109 | right.rect.fLeft = right.rect.fRight; |
1110 | right.rect.fRight = fOwner->widthWithTrailingSpaces(); |
1111 | boxes.emplace_back(right); |
1112 | } |
1113 | } |
1114 | |
1115 | return true; |
1116 | }); |
1117 | return true; |
1118 | }); |
1119 | for (auto& r : boxes) { |
1120 | r.rect.fLeft = littleRound(r.rect.fLeft); |
1121 | r.rect.fRight = littleRound(r.rect.fRight); |
1122 | r.rect.fTop = littleRound(r.rect.fTop); |
1123 | r.rect.fBottom = littleRound(r.rect.fBottom); |
1124 | } |
1125 | } |
1126 | |
1127 | PositionWithAffinity TextLine::getGlyphPositionAtCoordinate(SkScalar dx) { |
1128 | |
1129 | PositionWithAffinity result(0, Affinity::kDownstream); |
1130 | this->iterateThroughVisualRuns(true, |
1131 | [this, dx, &result] |
1132 | (const Run* run, SkScalar runOffsetInLine, TextRange , SkScalar* runWidthInLine) { |
1133 | bool keepLooking = true; |
1134 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
1135 | run, runOffsetInLine, textRange, StyleType::kNone, |
1136 | [this, dx, &result, &keepLooking] |
1137 | (TextRange , const TextStyle& style, const TextLine::ClipContext& context) { |
1138 | |
1139 | SkScalar offsetX = this->offset().fX; |
1140 | if (dx < context.clip.fLeft + offsetX) { |
1141 | // All the other runs are placed right of this one |
1142 | auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos)); |
1143 | result = { SkToS32(utf16Index), kDownstream }; |
1144 | return keepLooking = false; |
1145 | } |
1146 | |
1147 | if (dx >= context.clip.fRight + offsetX) { |
1148 | // We have to keep looking ; just in case keep the last one as the closest |
1149 | auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos + context.size)); |
1150 | result = { SkToS32(utf16Index), kUpstream }; |
1151 | return keepLooking = true; |
1152 | } |
1153 | |
1154 | // So we found the run that contains our coordinates |
1155 | // Find the glyph position in the run that is the closest left of our point |
1156 | // TODO: binary search |
1157 | size_t found = context.pos; |
1158 | for (size_t index = context.pos; index < context.pos + context.size; ++index) { |
1159 | // TODO: this rounding is done to match Flutter tests. Must be removed.. |
1160 | auto end = littleRound(context.run->positionX(index) + context.fTextShift + offsetX); |
1161 | if (end > dx) { |
1162 | break; |
1163 | } |
1164 | found = index; |
1165 | } |
1166 | |
1167 | SkScalar glyphemePosLeft = context.run->positionX(found) + context.fTextShift + offsetX; |
1168 | SkScalar glyphemePosWidth = context.run->positionX(found + 1) - context.run->positionX(found); |
1169 | |
1170 | // Find the grapheme range that contains the point |
1171 | auto clusterIndex8 = context.run->globalClusterIndex(found); |
1172 | auto clusterEnd8 = context.run->globalClusterIndex(found + 1); |
1173 | TextIndex graphemeUtf8Start = fOwner->findGraphemeStart(clusterIndex8); |
1174 | TextIndex graphemeUtf8Width = fOwner->findGraphemeStart(clusterEnd8) - graphemeUtf8Start; |
1175 | size_t utf16Index = fOwner->getUTF16Index(clusterIndex8); |
1176 | |
1177 | SkScalar center = glyphemePosLeft + glyphemePosWidth / 2; |
1178 | bool insideGlypheme = false; |
1179 | if (graphemeUtf8Width > 1) { |
1180 | // TODO: the average width of a code unit (especially UTF-8) is meaningless. |
1181 | // Probably want the average width of a grapheme or codepoint? |
1182 | SkScalar averageUtf8Width = glyphemePosWidth / graphemeUtf8Width; |
1183 | SkScalar delta = dx - glyphemePosLeft; |
1184 | int insideUtf8Offset = SkScalarNearlyZero(averageUtf8Width) |
1185 | ? 0 |
1186 | : SkScalarFloorToInt(delta / averageUtf8Width); |
1187 | insideGlypheme = averageUtf8Width < delta && delta < glyphemePosWidth - averageUtf8Width; |
1188 | center = glyphemePosLeft + averageUtf8Width * insideUtf8Offset + averageUtf8Width / 2; |
1189 | utf16Index += insideUtf8Offset; // TODO: adding a utf8 offset to a utf16 index |
1190 | } |
1191 | if ((dx < center) == context.run->leftToRight() || insideGlypheme) { |
1192 | result = { SkToS32(utf16Index), kDownstream }; |
1193 | } else { |
1194 | result = { SkToS32(utf16Index + 1), kUpstream }; |
1195 | } |
1196 | |
1197 | return keepLooking = false; |
1198 | |
1199 | }); |
1200 | return keepLooking; |
1201 | } |
1202 | ); |
1203 | return result; |
1204 | } |
1205 | |
1206 | void TextLine::getRectsForPlaceholders(std::vector<TextBox>& boxes) { |
1207 | this->iterateThroughVisualRuns( |
1208 | true, |
1209 | [&boxes, this](const Run* run, SkScalar runOffset, TextRange , |
1210 | SkScalar* width) { |
1211 | auto context = this->measureTextInsideOneRun(textRange, run, runOffset, 0, true, false); |
1212 | *width = context.clip.width(); |
1213 | |
1214 | if (textRange.width() == 0) { |
1215 | return true; |
1216 | } |
1217 | if (!run->isPlaceholder()) { |
1218 | return true; |
1219 | } |
1220 | |
1221 | SkRect clip = context.clip; |
1222 | clip.offset(this->offset()); |
1223 | |
1224 | clip.fLeft = littleRound(clip.fLeft); |
1225 | clip.fRight = littleRound(clip.fRight); |
1226 | clip.fTop = littleRound(clip.fTop); |
1227 | clip.fBottom = littleRound(clip.fBottom); |
1228 | boxes.emplace_back(clip, run->getTextDirection()); |
1229 | return true; |
1230 | }); |
1231 | } |
1232 | } // namespace textlayout |
1233 | } // namespace skia |
1234 | |