/src/skia/modules/skparagraph/src/TextLine.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | // Copyright 2019 Google LLC. |
2 | | |
3 | | #include "modules/skparagraph/src/TextLine.h" |
4 | | |
5 | | #include "include/core/SkBlurTypes.h" |
6 | | #include "include/core/SkFont.h" |
7 | | #include "include/core/SkFontMetrics.h" |
8 | | #include "include/core/SkMaskFilter.h" |
9 | | #include "include/core/SkPaint.h" |
10 | | #include "include/core/SkSpan.h" |
11 | | #include "include/core/SkString.h" |
12 | | #include "include/core/SkTextBlob.h" |
13 | | #include "include/core/SkTypes.h" |
14 | | #include "include/private/base/SkTemplates.h" |
15 | | #include "include/private/base/SkTo.h" |
16 | | #include "modules/skparagraph/include/DartTypes.h" |
17 | | #include "modules/skparagraph/include/Metrics.h" |
18 | | #include "modules/skparagraph/include/ParagraphPainter.h" |
19 | | #include "modules/skparagraph/include/ParagraphStyle.h" |
20 | | #include "modules/skparagraph/include/TextShadow.h" |
21 | | #include "modules/skparagraph/include/TextStyle.h" |
22 | | #include "modules/skparagraph/src/Decorations.h" |
23 | | #include "modules/skparagraph/src/ParagraphImpl.h" |
24 | | #include "modules/skparagraph/src/ParagraphPainterImpl.h" |
25 | | #include "modules/skshaper/include/SkShaper.h" |
26 | | #include "modules/skshaper/include/SkShaper_harfbuzz.h" |
27 | | #include "modules/skshaper/include/SkShaper_skunicode.h" |
28 | | |
29 | | #include <algorithm> |
30 | | #include <iterator> |
31 | | #include <limits> |
32 | | #include <map> |
33 | | #include <memory> |
34 | | #include <tuple> |
35 | | #include <type_traits> |
36 | | #include <utility> |
37 | | |
38 | | using namespace skia_private; |
39 | | |
40 | | namespace skia { |
41 | | namespace textlayout { |
42 | | |
43 | | namespace { |
44 | | |
45 | | // TODO: deal with all the intersection functionality |
46 | 0 | TextRange intersected(const TextRange& a, const TextRange& b) { |
47 | 0 | if (a.start == b.start && a.end == b.end) return a; |
48 | 0 | auto begin = std::max(a.start, b.start); |
49 | 0 | auto end = std::min(a.end, b.end); |
50 | 0 | return end >= begin ? TextRange(begin, end) : EMPTY_TEXT; |
51 | 0 | } |
52 | | |
53 | 0 | SkScalar littleRound(SkScalar a) { |
54 | | // This rounding is done to match Flutter tests. Must be removed.. |
55 | 0 | return SkScalarRoundToScalar(a * 100.0)/100.0; |
56 | 0 | } |
57 | | |
58 | 0 | TextRange operator*(const TextRange& a, const TextRange& b) { |
59 | 0 | if (a.start == b.start && a.end == b.end) return a; |
60 | 0 | auto begin = std::max(a.start, b.start); |
61 | 0 | auto end = std::min(a.end, b.end); |
62 | 0 | return end > begin ? TextRange(begin, end) : EMPTY_TEXT; |
63 | 0 | } |
64 | | |
65 | 0 | int compareRound(SkScalar a, SkScalar b, bool applyRoundingHack) { |
66 | | // There is a rounding error that gets bigger when maxWidth gets bigger |
67 | | // VERY long zalgo text (> 100000) on a VERY long line (> 10000) |
68 | | // Canvas scaling affects it |
69 | | // Letter spacing affects it |
70 | | // It has to be relative to be useful |
71 | 0 | auto base = std::max(SkScalarAbs(a), SkScalarAbs(b)); |
72 | 0 | auto diff = SkScalarAbs(a - b); |
73 | 0 | if (nearlyZero(base) || diff / base < 0.001f) { |
74 | 0 | return 0; |
75 | 0 | } |
76 | | |
77 | 0 | auto ra = a; |
78 | 0 | auto rb = b; |
79 | |
|
80 | 0 | if (applyRoundingHack) { |
81 | 0 | ra = littleRound(a); |
82 | 0 | rb = littleRound(b); |
83 | 0 | } |
84 | 0 | if (ra < rb) { |
85 | 0 | return -1; |
86 | 0 | } else { |
87 | 0 | return 1; |
88 | 0 | } |
89 | 0 | } |
90 | | |
91 | | } // namespace |
92 | | |
93 | | TextLine::TextLine(ParagraphImpl* owner, |
94 | | SkVector offset, |
95 | | SkVector advance, |
96 | | BlockRange blocks, |
97 | | TextRange textExcludingSpaces, |
98 | | TextRange text, |
99 | | TextRange textIncludingNewlines, |
100 | | ClusterRange clusters, |
101 | | ClusterRange clustersWithGhosts, |
102 | | SkScalar widthWithSpaces, |
103 | | InternalLineMetrics sizes) |
104 | | : fOwner(owner) |
105 | | , fBlockRange(blocks) |
106 | | , fTextExcludingSpaces(textExcludingSpaces) |
107 | | , fText(text) |
108 | | , fTextIncludingNewlines(textIncludingNewlines) |
109 | | , fClusterRange(clusters) |
110 | | , fGhostClusterRange(clustersWithGhosts) |
111 | | , fRunsInVisualOrder() |
112 | | , fAdvance(advance) |
113 | | , fOffset(offset) |
114 | | , fShift(0.0) |
115 | | , fWidthWithSpaces(widthWithSpaces) |
116 | | , fEllipsis(nullptr) |
117 | | , fSizes(sizes) |
118 | | , fHasBackground(false) |
119 | | , fHasShadows(false) |
120 | | , fHasDecorations(false) |
121 | | , fAscentStyle(LineMetricStyle::CSS) |
122 | | , fDescentStyle(LineMetricStyle::CSS) |
123 | 0 | , fTextBlobCachePopulated(false) { |
124 | | // Reorder visual runs |
125 | 0 | auto& start = owner->cluster(fGhostClusterRange.start); |
126 | 0 | auto& end = owner->cluster(fGhostClusterRange.end - 1); |
127 | 0 | size_t numRuns = end.runIndex() - start.runIndex() + 1; |
128 | |
|
129 | 0 | for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { |
130 | 0 | auto b = fOwner->styles().begin() + index; |
131 | 0 | if (b->fStyle.hasBackground()) { |
132 | 0 | fHasBackground = true; |
133 | 0 | } |
134 | 0 | if (b->fStyle.getDecorationType() != TextDecoration::kNoDecoration) { |
135 | 0 | fHasDecorations = true; |
136 | 0 | } |
137 | 0 | if (b->fStyle.getShadowNumber() > 0) { |
138 | 0 | fHasShadows = true; |
139 | 0 | } |
140 | 0 | } |
141 | | |
142 | | // Get the logical order |
143 | | |
144 | | // This is just chosen to catch the common/fast cases. Feel free to tweak. |
145 | 0 | constexpr int kPreallocCount = 4; |
146 | 0 | AutoSTArray<kPreallocCount, SkUnicode::BidiLevel> runLevels(numRuns); |
147 | 0 | std::vector<RunIndex> placeholdersInOriginalOrder; |
148 | 0 | size_t runLevelsIndex = 0; |
149 | | // Placeholders must be laid out using the original order in which they were added |
150 | | // in the input. The API does not provide a way to indicate that a placeholder |
151 | | // position was moved due to bidi reordering. |
152 | 0 | for (auto runIndex = start.runIndex(); runIndex <= end.runIndex(); ++runIndex) { |
153 | 0 | auto& run = fOwner->run(runIndex); |
154 | 0 | runLevels[runLevelsIndex++] = run.fBidiLevel; |
155 | 0 | fMaxRunMetrics.add( |
156 | 0 | InternalLineMetrics(run.correctAscent(), run.correctDescent(), run.fFontMetrics.fLeading)); |
157 | 0 | if (run.isPlaceholder()) { |
158 | 0 | placeholdersInOriginalOrder.push_back(runIndex); |
159 | 0 | } |
160 | 0 | } |
161 | 0 | SkASSERT(runLevelsIndex == numRuns); |
162 | |
|
163 | 0 | AutoSTArray<kPreallocCount, int32_t> logicalOrder(numRuns); |
164 | | |
165 | | // TODO: hide all these logic in SkUnicode? |
166 | 0 | fOwner->getUnicode()->reorderVisual(runLevels.data(), numRuns, logicalOrder.data()); |
167 | 0 | auto firstRunIndex = start.runIndex(); |
168 | 0 | auto placeholderIter = placeholdersInOriginalOrder.begin(); |
169 | 0 | for (auto index : logicalOrder) { |
170 | 0 | auto runIndex = firstRunIndex + index; |
171 | 0 | if (fOwner->run(runIndex).isPlaceholder()) { |
172 | 0 | fRunsInVisualOrder.push_back(*placeholderIter++); |
173 | 0 | } else { |
174 | 0 | fRunsInVisualOrder.push_back(runIndex); |
175 | 0 | } |
176 | 0 | } |
177 | | |
178 | | // TODO: This is the fix for flutter. Must be removed... |
179 | 0 | for (auto cluster = &start; cluster <= &end; ++cluster) { |
180 | 0 | if (!cluster->run().isPlaceholder()) { |
181 | 0 | fShift += cluster->getHalfLetterSpacing(); |
182 | 0 | break; |
183 | 0 | } |
184 | 0 | } |
185 | 0 | } |
186 | | |
187 | 0 | void TextLine::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) { |
188 | 0 | if (fHasBackground) { |
189 | 0 | this->iterateThroughVisualRuns(false, |
190 | 0 | [painter, x, y, this] |
191 | 0 | (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { |
192 | 0 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
193 | 0 | TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kBackground, |
194 | 0 | [painter, x, y, this](TextRange textRange, const TextStyle& style, const ClipContext& context) { |
195 | 0 | this->paintBackground(painter, x, y, textRange, style, context); |
196 | 0 | }); |
197 | 0 | return true; |
198 | 0 | }); |
199 | 0 | } |
200 | |
|
201 | 0 | if (fHasShadows) { |
202 | 0 | this->iterateThroughVisualRuns(false, |
203 | 0 | [painter, x, y, this] |
204 | 0 | (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { |
205 | 0 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
206 | 0 | TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kShadow, |
207 | 0 | [painter, x, y, this] |
208 | 0 | (TextRange textRange, const TextStyle& style, const ClipContext& context) { |
209 | 0 | this->paintShadow(painter, x, y, textRange, style, context); |
210 | 0 | }); |
211 | 0 | return true; |
212 | 0 | }); |
213 | 0 | } |
214 | |
|
215 | 0 | this->ensureTextBlobCachePopulated(); |
216 | |
|
217 | 0 | for (auto& record : fTextBlobCache) { |
218 | 0 | record.paint(painter, x, y); |
219 | 0 | } |
220 | |
|
221 | 0 | if (fHasDecorations) { |
222 | 0 | this->iterateThroughVisualRuns(false, |
223 | 0 | [painter, x, y, this] |
224 | 0 | (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { |
225 | 0 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
226 | 0 | TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kDecorations, |
227 | 0 | [painter, x, y, this] |
228 | 0 | (TextRange textRange, const TextStyle& style, const ClipContext& context) { |
229 | 0 | this->paintDecorations(painter, x, y, textRange, style, context); |
230 | 0 | }); |
231 | 0 | return true; |
232 | 0 | }); |
233 | 0 | } |
234 | 0 | } |
235 | | |
236 | 0 | void TextLine::ensureTextBlobCachePopulated() { |
237 | 0 | if (fTextBlobCachePopulated) { |
238 | 0 | return; |
239 | 0 | } |
240 | 0 | if (fBlockRange.width() == 1 && |
241 | 0 | fRunsInVisualOrder.size() == 1 && |
242 | 0 | fEllipsis == nullptr && |
243 | 0 | fOwner->run(fRunsInVisualOrder[0]).placeholderStyle() == nullptr) { |
244 | 0 | if (fClusterRange.width() == 0) { |
245 | 0 | return; |
246 | 0 | } |
247 | | // Most common and most simple case |
248 | 0 | const auto& style = fOwner->block(fBlockRange.start).fStyle; |
249 | 0 | const auto& run = fOwner->run(fRunsInVisualOrder[0]); |
250 | 0 | auto clip = SkRect::MakeXYWH(0.0f, this->sizes().runTop(&run, this->fAscentStyle), |
251 | 0 | fAdvance.fX, |
252 | 0 | run.calculateHeight(this->fAscentStyle, this->fDescentStyle)); |
253 | |
|
254 | 0 | auto& start = fOwner->cluster(fClusterRange.start); |
255 | 0 | auto& end = fOwner->cluster(fClusterRange.end - 1); |
256 | 0 | SkASSERT(start.runIndex() == end.runIndex()); |
257 | 0 | GlyphRange glyphs; |
258 | 0 | if (run.leftToRight()) { |
259 | 0 | glyphs = GlyphRange(start.startPos(), |
260 | 0 | end.isHardBreak() ? end.startPos() : end.endPos()); |
261 | 0 | } else { |
262 | 0 | glyphs = GlyphRange(end.startPos(), |
263 | 0 | start.isHardBreak() ? start.startPos() : start.endPos()); |
264 | 0 | } |
265 | 0 | ClipContext context = {/*run=*/&run, |
266 | 0 | /*pos=*/glyphs.start, |
267 | 0 | /*size=*/glyphs.width(), |
268 | 0 | /*fTextShift=*/-run.positionX(glyphs.start), // starting position |
269 | 0 | /*clip=*/clip, // entire line |
270 | 0 | /*fExcludedTrailingSpaces=*/0.0f, // no need for that |
271 | 0 | /*clippingNeeded=*/false}; // no need for that |
272 | 0 | this->buildTextBlob(fTextExcludingSpaces, style, context); |
273 | 0 | } else { |
274 | 0 | this->iterateThroughVisualRuns(false, |
275 | 0 | [this](const Run* run, |
276 | 0 | SkScalar runOffsetInLine, |
277 | 0 | TextRange textRange, |
278 | 0 | SkScalar* runWidthInLine) { |
279 | 0 | if (run->placeholderStyle() != nullptr) { |
280 | 0 | *runWidthInLine = run->advance().fX; |
281 | 0 | return true; |
282 | 0 | } |
283 | 0 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
284 | 0 | TextAdjustment::GlyphCluster, |
285 | 0 | run, |
286 | 0 | runOffsetInLine, |
287 | 0 | textRange, |
288 | 0 | StyleType::kForeground, |
289 | 0 | [this](TextRange textRange, const TextStyle& style, const ClipContext& context) { |
290 | 0 | this->buildTextBlob(textRange, style, context); |
291 | 0 | }); |
292 | 0 | return true; |
293 | 0 | }); |
294 | 0 | } |
295 | 0 | fTextBlobCachePopulated = true; |
296 | 0 | } |
297 | | |
298 | 0 | void TextLine::format(TextAlign align, SkScalar maxWidth) { |
299 | 0 | SkScalar delta = maxWidth - this->width(); |
300 | 0 | if (delta <= 0) { |
301 | 0 | return; |
302 | 0 | } |
303 | | |
304 | | // We do nothing for left align |
305 | 0 | if (align == TextAlign::kJustify) { |
306 | 0 | if (!this->endsWithHardLineBreak()) { |
307 | 0 | this->justify(maxWidth); |
308 | 0 | } else if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) { |
309 | | // Justify -> Right align |
310 | 0 | fShift = delta; |
311 | 0 | } |
312 | 0 | } else if (align == TextAlign::kRight) { |
313 | 0 | fShift = delta; |
314 | 0 | } else if (align == TextAlign::kCenter) { |
315 | 0 | fShift = delta / 2; |
316 | 0 | } |
317 | 0 | } |
318 | | |
319 | 0 | void TextLine::scanStyles(StyleType styleType, const RunStyleVisitor& visitor) { |
320 | 0 | if (this->empty()) { |
321 | 0 | return; |
322 | 0 | } |
323 | | |
324 | 0 | this->iterateThroughVisualRuns( |
325 | 0 | false, |
326 | 0 | [this, visitor, styleType]( |
327 | 0 | const Run* run, SkScalar runOffset, TextRange textRange, SkScalar* width) { |
328 | 0 | *width = this->iterateThroughSingleRunByStyles( |
329 | 0 | TextAdjustment::GlyphCluster, |
330 | 0 | run, |
331 | 0 | runOffset, |
332 | 0 | textRange, |
333 | 0 | styleType, |
334 | 0 | [visitor](TextRange textRange, |
335 | 0 | const TextStyle& style, |
336 | 0 | const ClipContext& context) { |
337 | 0 | visitor(textRange, style, context); |
338 | 0 | }); |
339 | 0 | return true; |
340 | 0 | }); |
341 | 0 | } |
342 | | |
343 | 0 | SkRect TextLine::extendHeight(const ClipContext& context) const { |
344 | 0 | SkRect result = context.clip; |
345 | 0 | result.fBottom += std::max(this->fMaxRunMetrics.height() - this->height(), 0.0f); |
346 | 0 | return result; |
347 | 0 | } |
348 | | |
349 | 0 | void TextLine::buildTextBlob(TextRange textRange, const TextStyle& style, const ClipContext& context) { |
350 | 0 | if (context.run->placeholderStyle() != nullptr) { |
351 | 0 | return; |
352 | 0 | } |
353 | | |
354 | 0 | fTextBlobCache.emplace_back(); |
355 | 0 | TextBlobRecord& record = fTextBlobCache.back(); |
356 | |
|
357 | 0 | if (style.hasForeground()) { |
358 | 0 | record.fPaint = style.getForegroundPaintOrID(); |
359 | 0 | } else { |
360 | 0 | std::get<SkPaint>(record.fPaint).setColor(style.getColor()); |
361 | 0 | } |
362 | 0 | record.fVisitor_Run = context.run; |
363 | 0 | record.fVisitor_Pos = context.pos; |
364 | | |
365 | | // TODO: This is the change for flutter, must be removed later |
366 | 0 | SkTextBlobBuilder builder; |
367 | 0 | context.run->copyTo(builder, SkToU32(context.pos), context.size); |
368 | 0 | record.fClippingNeeded = context.clippingNeeded; |
369 | 0 | if (context.clippingNeeded) { |
370 | 0 | record.fClipRect = extendHeight(context).makeOffset(this->offset()); |
371 | 0 | } else { |
372 | 0 | record.fClipRect = context.clip.makeOffset(this->offset()); |
373 | 0 | } |
374 | |
|
375 | 0 | SkASSERT(nearlyEqual(context.run->baselineShift(), style.getBaselineShift())); |
376 | 0 | SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5); |
377 | 0 | record.fBlob = builder.make(); |
378 | 0 | if (record.fBlob != nullptr) { |
379 | 0 | record.fBounds.joinPossiblyEmptyRect(record.fBlob->bounds()); |
380 | 0 | } |
381 | |
|
382 | 0 | record.fOffset = SkPoint::Make(this->offset().fX + context.fTextShift, |
383 | 0 | this->offset().fY + correctedBaseline); |
384 | 0 | } |
385 | | |
386 | 0 | void TextLine::TextBlobRecord::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) { |
387 | 0 | if (fClippingNeeded) { |
388 | 0 | painter->save(); |
389 | 0 | painter->clipRect(fClipRect.makeOffset(x, y)); |
390 | 0 | } |
391 | 0 | painter->drawTextBlob(fBlob, x + fOffset.x(), y + fOffset.y(), fPaint); |
392 | 0 | if (fClippingNeeded) { |
393 | 0 | painter->restore(); |
394 | 0 | } |
395 | 0 | } |
396 | | |
397 | | void TextLine::paintBackground(ParagraphPainter* painter, |
398 | | SkScalar x, |
399 | | SkScalar y, |
400 | | TextRange textRange, |
401 | | const TextStyle& style, |
402 | 0 | const ClipContext& context) const { |
403 | 0 | if (style.hasBackground()) { |
404 | 0 | painter->drawRect(context.clip.makeOffset(this->offset() + SkPoint::Make(x, y)), |
405 | 0 | style.getBackgroundPaintOrID()); |
406 | 0 | } |
407 | 0 | } |
408 | | |
409 | | void TextLine::paintShadow(ParagraphPainter* painter, |
410 | | SkScalar x, |
411 | | SkScalar y, |
412 | | TextRange textRange, |
413 | | const TextStyle& style, |
414 | 0 | const ClipContext& context) const { |
415 | 0 | SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + style.getBaselineShift() + 0.5); |
416 | |
|
417 | 0 | for (TextShadow shadow : style.getShadows()) { |
418 | 0 | if (!shadow.hasShadow()) continue; |
419 | | |
420 | 0 | SkTextBlobBuilder builder; |
421 | 0 | context.run->copyTo(builder, context.pos, context.size); |
422 | |
|
423 | 0 | if (context.clippingNeeded) { |
424 | 0 | painter->save(); |
425 | 0 | SkRect clip = extendHeight(context); |
426 | 0 | clip.offset(x, y); |
427 | 0 | clip.offset(this->offset()); |
428 | 0 | painter->clipRect(clip); |
429 | 0 | } |
430 | 0 | auto blob = builder.make(); |
431 | 0 | painter->drawTextShadow(blob, |
432 | 0 | x + this->offset().fX + shadow.fOffset.x() + context.fTextShift, |
433 | 0 | y + this->offset().fY + shadow.fOffset.y() + correctedBaseline, |
434 | 0 | shadow.fColor, |
435 | 0 | SkDoubleToScalar(shadow.fBlurSigma)); |
436 | 0 | if (context.clippingNeeded) { |
437 | 0 | painter->restore(); |
438 | 0 | } |
439 | 0 | } |
440 | 0 | } |
441 | | |
442 | 0 | void TextLine::paintDecorations(ParagraphPainter* painter, SkScalar x, SkScalar y, TextRange textRange, const TextStyle& style, const ClipContext& context) const { |
443 | 0 | ParagraphPainterAutoRestore ppar(painter); |
444 | 0 | painter->translate(x + this->offset().fX, y + this->offset().fY + style.getBaselineShift()); |
445 | 0 | Decorations decorations; |
446 | 0 | SkScalar correctedBaseline = SkScalarFloorToScalar(-this->sizes().rawAscent() + style.getBaselineShift() + 0.5); |
447 | 0 | decorations.paint(painter, style, context, correctedBaseline); |
448 | 0 | } |
449 | | |
450 | 0 | void TextLine::justify(SkScalar maxWidth) { |
451 | 0 | int whitespacePatches = 0; |
452 | 0 | SkScalar textLen = 0; |
453 | 0 | SkScalar whitespaceLen = 0; |
454 | 0 | bool whitespacePatch = false; |
455 | | // Take leading whitespaces width but do not increment a whitespace patch number |
456 | 0 | bool leadingWhitespaces = false; |
457 | 0 | this->iterateThroughClustersInGlyphsOrder(false, false, |
458 | 0 | [&](const Cluster* cluster, ClusterIndex index, bool ghost) { |
459 | 0 | if (cluster->isWhitespaceBreak()) { |
460 | 0 | if (index == 0) { |
461 | 0 | leadingWhitespaces = true; |
462 | 0 | } else if (!whitespacePatch && !leadingWhitespaces) { |
463 | | // We only count patches BETWEEN words, not before |
464 | 0 | ++whitespacePatches; |
465 | 0 | } |
466 | 0 | whitespacePatch = !leadingWhitespaces; |
467 | 0 | whitespaceLen += cluster->width(); |
468 | 0 | } else if (cluster->isIdeographic()) { |
469 | | // Whitespace break before and after |
470 | 0 | if (!whitespacePatch && index != 0) { |
471 | | // We only count patches BETWEEN words, not before |
472 | 0 | ++whitespacePatches; // before |
473 | 0 | } |
474 | 0 | whitespacePatch = true; |
475 | 0 | leadingWhitespaces = false; |
476 | 0 | ++whitespacePatches; // after |
477 | 0 | } else { |
478 | 0 | whitespacePatch = false; |
479 | 0 | leadingWhitespaces = false; |
480 | 0 | } |
481 | 0 | textLen += cluster->width(); |
482 | 0 | return true; |
483 | 0 | }); |
484 | |
|
485 | 0 | if (whitespacePatch) { |
486 | | // We only count patches BETWEEN words, not after |
487 | 0 | --whitespacePatches; |
488 | 0 | } |
489 | 0 | if (whitespacePatches == 0) { |
490 | 0 | if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) { |
491 | | // Justify -> Right align |
492 | 0 | fShift = maxWidth - textLen; |
493 | 0 | } |
494 | 0 | return; |
495 | 0 | } |
496 | | |
497 | 0 | SkScalar step = (maxWidth - textLen + whitespaceLen) / whitespacePatches; |
498 | 0 | SkScalar shift = 0.0f; |
499 | 0 | SkScalar prevShift = 0.0f; |
500 | | |
501 | | // Deal with the ghost spaces |
502 | 0 | auto ghostShift = maxWidth - this->fAdvance.fX; |
503 | | // Spread the extra whitespaces |
504 | 0 | whitespacePatch = false; |
505 | | // Do not break on leading whitespaces |
506 | 0 | leadingWhitespaces = false; |
507 | 0 | this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, ClusterIndex index, bool ghost) { |
508 | |
|
509 | 0 | if (ghost) { |
510 | 0 | if (cluster->run().leftToRight()) { |
511 | 0 | this->shiftCluster(cluster, ghostShift, ghostShift); |
512 | 0 | } |
513 | 0 | return true; |
514 | 0 | } |
515 | | |
516 | 0 | if (cluster->isWhitespaceBreak()) { |
517 | 0 | if (index == 0) { |
518 | 0 | leadingWhitespaces = true; |
519 | 0 | } else if (!whitespacePatch && !leadingWhitespaces) { |
520 | 0 | shift += step; |
521 | 0 | whitespacePatch = true; |
522 | 0 | --whitespacePatches; |
523 | 0 | } |
524 | 0 | shift -= cluster->width(); |
525 | 0 | } else if (cluster->isIdeographic()) { |
526 | 0 | if (!whitespacePatch && index != 0) { |
527 | 0 | shift += step; |
528 | 0 | --whitespacePatches; |
529 | 0 | } |
530 | 0 | whitespacePatch = false; |
531 | 0 | leadingWhitespaces = false; |
532 | 0 | } else { |
533 | 0 | whitespacePatch = false; |
534 | 0 | leadingWhitespaces = false; |
535 | 0 | } |
536 | 0 | this->shiftCluster(cluster, shift, prevShift); |
537 | 0 | prevShift = shift; |
538 | | // We skip ideographic whitespaces |
539 | 0 | if (!cluster->isWhitespaceBreak() && cluster->isIdeographic()) { |
540 | 0 | shift += step; |
541 | 0 | whitespacePatch = true; |
542 | 0 | --whitespacePatches; |
543 | 0 | } |
544 | 0 | return true; |
545 | 0 | }); |
546 | |
|
547 | 0 | if (whitespacePatch && whitespacePatches < 0) { |
548 | 0 | whitespacePatches++; |
549 | 0 | shift -= step; |
550 | 0 | } |
551 | |
|
552 | 0 | SkAssertResult(nearlyEqual(shift, maxWidth - textLen)); |
553 | 0 | SkASSERT(whitespacePatches == 0); |
554 | |
|
555 | 0 | this->fWidthWithSpaces += ghostShift; |
556 | 0 | this->fAdvance.fX = maxWidth; |
557 | 0 | } |
558 | | |
559 | 0 | void TextLine::shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift) { |
560 | |
|
561 | 0 | auto& run = cluster->run(); |
562 | 0 | auto start = cluster->startPos(); |
563 | 0 | auto end = cluster->endPos(); |
564 | |
|
565 | 0 | if (end == run.size()) { |
566 | | // Set the same shift for the fake last glyph (to avoid all extra checks) |
567 | 0 | ++end; |
568 | 0 | } |
569 | |
|
570 | 0 | if (run.fJustificationShifts.empty()) { |
571 | | // Do not fill this array until needed |
572 | 0 | run.fJustificationShifts.push_back_n(run.size() + 1, { 0, 0 }); |
573 | 0 | } |
574 | |
|
575 | 0 | for (size_t pos = start; pos < end; ++pos) { |
576 | 0 | run.fJustificationShifts[pos] = { shift, prevShift }; |
577 | 0 | } |
578 | 0 | } |
579 | | |
580 | 0 | void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) { |
581 | | // Replace some clusters with the ellipsis |
582 | | // Go through the clusters in the reverse logical order |
583 | | // taking off cluster by cluster until the ellipsis fits |
584 | 0 | SkScalar width = fAdvance.fX; |
585 | 0 | RunIndex lastRun = EMPTY_RUN; |
586 | 0 | std::unique_ptr<Run> ellipsisRun; |
587 | 0 | for (auto clusterIndex = fGhostClusterRange.end; clusterIndex > fGhostClusterRange.start; --clusterIndex) { |
588 | 0 | auto& cluster = fOwner->cluster(clusterIndex - 1); |
589 | | // Shape the ellipsis if the run has changed |
590 | 0 | if (lastRun != cluster.runIndex()) { |
591 | 0 | ellipsisRun = this->shapeEllipsis(ellipsis, &cluster); |
592 | 0 | if (ellipsisRun->advance().fX > maxWidth) { |
593 | | // Ellipsis is bigger than the entire line; no way we can add it at all |
594 | | // BUT! We can keep scanning in case the next run will give us better results |
595 | 0 | lastRun = EMPTY_RUN; |
596 | 0 | continue; |
597 | 0 | } else { |
598 | | // We may need to continue |
599 | 0 | lastRun = cluster.runIndex(); |
600 | 0 | } |
601 | 0 | } |
602 | | // See if it fits |
603 | 0 | if (width + ellipsisRun->advance().fX > maxWidth) { |
604 | 0 | width -= cluster.width(); |
605 | | // Continue if the ellipsis does not fit |
606 | 0 | continue; |
607 | 0 | } |
608 | | // We found enough room for the ellipsis |
609 | 0 | fAdvance.fX = width; |
610 | 0 | fEllipsis = std::move(ellipsisRun); |
611 | 0 | fEllipsis->setOwner(fOwner); |
612 | | |
613 | | // Let's update the line |
614 | 0 | fClusterRange.end = clusterIndex; |
615 | 0 | fGhostClusterRange.end = fClusterRange.end; |
616 | 0 | fEllipsis->fClusterStart = cluster.textRange().start; |
617 | 0 | fText.end = cluster.textRange().end; |
618 | 0 | fTextIncludingNewlines.end = cluster.textRange().end; |
619 | 0 | fTextExcludingSpaces.end = cluster.textRange().end; |
620 | 0 | break; |
621 | 0 | } |
622 | |
|
623 | 0 | if (!fEllipsis) { |
624 | | // Weird situation: ellipsis does not fit; no ellipsis then |
625 | 0 | fClusterRange.end = fClusterRange.start; |
626 | 0 | fGhostClusterRange.end = fClusterRange.start; |
627 | 0 | fText.end = fText.start; |
628 | 0 | fTextIncludingNewlines.end = fTextIncludingNewlines.start; |
629 | 0 | fTextExcludingSpaces.end = fTextExcludingSpaces.start; |
630 | 0 | fAdvance.fX = 0; |
631 | 0 | } |
632 | 0 | } |
633 | | |
634 | 0 | std::unique_ptr<Run> TextLine::shapeEllipsis(const SkString& ellipsis, const Cluster* cluster) { |
635 | |
|
636 | 0 | class ShapeHandler final : public SkShaper::RunHandler { |
637 | 0 | public: |
638 | 0 | ShapeHandler(SkScalar lineHeight, bool useHalfLeading, SkScalar baselineShift, const SkString& ellipsis) |
639 | 0 | : fRun(nullptr), fLineHeight(lineHeight), fUseHalfLeading(useHalfLeading), fBaselineShift(baselineShift), fEllipsis(ellipsis) {} |
640 | 0 | std::unique_ptr<Run> run() & { return std::move(fRun); } |
641 | |
|
642 | 0 | private: |
643 | 0 | void beginLine() override {} |
644 | |
|
645 | 0 | void runInfo(const RunInfo&) override {} |
646 | |
|
647 | 0 | void commitRunInfo() override {} |
648 | |
|
649 | 0 | Buffer runBuffer(const RunInfo& info) override { |
650 | 0 | SkASSERT(!fRun); |
651 | 0 | fRun = std::make_unique<Run>(nullptr, info, 0, fLineHeight, fUseHalfLeading, fBaselineShift, 0, 0); |
652 | 0 | return fRun->newRunBuffer(); |
653 | 0 | } |
654 | |
|
655 | 0 | void commitRunBuffer(const RunInfo& info) override { |
656 | 0 | fRun->fAdvance.fX = info.fAdvance.fX; |
657 | 0 | fRun->fAdvance.fY = fRun->advance().fY; |
658 | 0 | fRun->fPlaceholderIndex = std::numeric_limits<size_t>::max(); |
659 | 0 | fRun->fEllipsis = true; |
660 | 0 | } |
661 | |
|
662 | 0 | void commitLine() override {} |
663 | |
|
664 | 0 | std::unique_ptr<Run> fRun; |
665 | 0 | SkScalar fLineHeight; |
666 | 0 | bool fUseHalfLeading; |
667 | 0 | SkScalar fBaselineShift; |
668 | 0 | SkString fEllipsis; |
669 | 0 | }; |
670 | |
|
671 | 0 | const Run& run = cluster->run(); |
672 | 0 | TextStyle textStyle = fOwner->paragraphStyle().getTextStyle(); |
673 | 0 | for (auto i = fBlockRange.start; i < fBlockRange.end; ++i) { |
674 | 0 | auto& block = fOwner->block(i); |
675 | 0 | if (run.leftToRight() && cluster->textRange().end <= block.fRange.end) { |
676 | 0 | textStyle = block.fStyle; |
677 | 0 | break; |
678 | 0 | } else if (!run.leftToRight() && cluster->textRange().start <= block.fRange.end) { |
679 | 0 | textStyle = block.fStyle; |
680 | 0 | break; |
681 | 0 | } |
682 | 0 | } |
683 | |
|
684 | 0 | auto shaped = [&](sk_sp<SkTypeface> typeface, sk_sp<SkFontMgr> fallback) -> std::unique_ptr<Run> { |
685 | 0 | ShapeHandler handler(run.heightMultiplier(), run.useHalfLeading(), run.baselineShift(), ellipsis); |
686 | 0 | SkFont font(std::move(typeface), textStyle.getFontSize()); |
687 | 0 | font.setEdging(SkFont::Edging::kAntiAlias); |
688 | 0 | font.setHinting(SkFontHinting::kSlight); |
689 | 0 | font.setSubpixel(true); |
690 | |
|
691 | 0 | std::unique_ptr<SkShaper> shaper = SkShapers::HB::ShapeDontWrapOrReorder( |
692 | 0 | fOwner->getUnicode(), fallback ? fallback : SkFontMgr::RefEmpty()); |
693 | |
|
694 | 0 | const SkBidiIterator::Level defaultLevel = SkBidiIterator::kLTR; |
695 | 0 | const char* utf8 = ellipsis.c_str(); |
696 | 0 | size_t utf8Bytes = ellipsis.size(); |
697 | |
|
698 | 0 | std::unique_ptr<SkShaper::BiDiRunIterator> bidi = SkShapers::unicode::BidiRunIterator( |
699 | 0 | fOwner->getUnicode(), utf8, utf8Bytes, defaultLevel); |
700 | 0 | SkASSERT(bidi); |
701 | |
|
702 | 0 | std::unique_ptr<SkShaper::LanguageRunIterator> language = |
703 | 0 | SkShaper::MakeStdLanguageRunIterator(utf8, utf8Bytes); |
704 | 0 | SkASSERT(language); |
705 | |
|
706 | 0 | std::unique_ptr<SkShaper::ScriptRunIterator> script = |
707 | 0 | SkShapers::HB::ScriptRunIterator(utf8, utf8Bytes); |
708 | 0 | SkASSERT(script); |
709 | |
|
710 | 0 | std::unique_ptr<SkShaper::FontRunIterator> fontRuns = SkShaper::MakeFontMgrRunIterator( |
711 | 0 | utf8, utf8Bytes, font, fallback ? fallback : SkFontMgr::RefEmpty()); |
712 | 0 | SkASSERT(fontRuns); |
713 | |
|
714 | 0 | shaper->shape(utf8, |
715 | 0 | utf8Bytes, |
716 | 0 | *fontRuns, |
717 | 0 | *bidi, |
718 | 0 | *script, |
719 | 0 | *language, |
720 | 0 | nullptr, |
721 | 0 | 0, |
722 | 0 | std::numeric_limits<SkScalar>::max(), |
723 | 0 | &handler); |
724 | 0 | auto ellipsisRun = handler.run(); |
725 | 0 | ellipsisRun->fTextRange = TextRange(0, ellipsis.size()); |
726 | 0 | ellipsisRun->fOwner = fOwner; |
727 | 0 | return ellipsisRun; |
728 | 0 | }; |
729 | | |
730 | | // Check the current font |
731 | 0 | auto ellipsisRun = shaped(run.fFont.refTypeface(), nullptr); |
732 | 0 | if (ellipsisRun->isResolved()) { |
733 | 0 | return ellipsisRun; |
734 | 0 | } |
735 | | |
736 | | // Check all allowed fonts |
737 | 0 | std::vector<sk_sp<SkTypeface>> typefaces = fOwner->fontCollection()->findTypefaces( |
738 | 0 | textStyle.getFontFamilies(), textStyle.getFontStyle(), textStyle.getFontArguments()); |
739 | 0 | for (const auto& typeface : typefaces) { |
740 | 0 | ellipsisRun = shaped(typeface, nullptr); |
741 | 0 | if (ellipsisRun->isResolved()) { |
742 | 0 | return ellipsisRun; |
743 | 0 | } |
744 | 0 | } |
745 | | |
746 | | // Try the fallback |
747 | 0 | if (fOwner->fontCollection()->fontFallbackEnabled()) { |
748 | 0 | const char* ch = ellipsis.c_str(); |
749 | 0 | SkUnichar unicode = SkUTF::NextUTF8WithReplacement(&ch, |
750 | 0 | ellipsis.c_str() |
751 | 0 | + ellipsis.size()); |
752 | | // We do not expect emojis in ellipsis so if they appeat there |
753 | | // they will not be resolved with the pretiest color emoji font |
754 | 0 | auto typeface = fOwner->fontCollection()->defaultFallback( |
755 | 0 | unicode, |
756 | 0 | textStyle.getFontStyle(), |
757 | 0 | textStyle.getLocale()); |
758 | 0 | if (typeface) { |
759 | 0 | ellipsisRun = shaped(typeface, fOwner->fontCollection()->getFallbackManager()); |
760 | 0 | if (ellipsisRun->isResolved()) { |
761 | 0 | return ellipsisRun; |
762 | 0 | } |
763 | 0 | } |
764 | 0 | } |
765 | 0 | return ellipsisRun; |
766 | 0 | } |
767 | | |
768 | | TextLine::ClipContext TextLine::measureTextInsideOneRun(TextRange textRange, |
769 | | const Run* run, |
770 | | SkScalar runOffsetInLine, |
771 | | SkScalar textOffsetInRunInLine, |
772 | | bool includeGhostSpaces, |
773 | 0 | TextAdjustment textAdjustment) const { |
774 | 0 | ClipContext result = { run, 0, run->size(), 0, SkRect::MakeEmpty(), 0, false }; |
775 | |
|
776 | 0 | if (run->fEllipsis) { |
777 | | // Both ellipsis and placeholders can only be measured as one glyph |
778 | 0 | result.fTextShift = runOffsetInLine; |
779 | 0 | result.clip = SkRect::MakeXYWH(runOffsetInLine, |
780 | 0 | sizes().runTop(run, this->fAscentStyle), |
781 | 0 | run->advance().fX, |
782 | 0 | run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); |
783 | 0 | return result; |
784 | 0 | } else if (run->isPlaceholder()) { |
785 | 0 | result.fTextShift = runOffsetInLine; |
786 | 0 | if (SkIsFinite(run->fFontMetrics.fAscent)) { |
787 | 0 | result.clip = SkRect::MakeXYWH(runOffsetInLine, |
788 | 0 | sizes().runTop(run, this->fAscentStyle), |
789 | 0 | run->advance().fX, |
790 | 0 | run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); |
791 | 0 | } else { |
792 | 0 | result.clip = SkRect::MakeXYWH(runOffsetInLine, run->fFontMetrics.fAscent, run->advance().fX, 0); |
793 | 0 | } |
794 | 0 | return result; |
795 | 0 | } else if (textRange.empty()) { |
796 | 0 | return result; |
797 | 0 | } |
798 | | |
799 | 0 | TextRange originalTextRange(textRange); // We need it for proportional measurement |
800 | | // Find [start:end] clusters for the text |
801 | 0 | while (true) { |
802 | | // Update textRange by cluster edges (shift start up to the edge of the cluster) |
803 | | // TODO: remove this limitation? |
804 | 0 | TextRange updatedTextRange; |
805 | 0 | bool found; |
806 | 0 | std::tie(found, updatedTextRange.start, updatedTextRange.end) = |
807 | 0 | run->findLimitingGlyphClusters(textRange); |
808 | 0 | if (!found) { |
809 | 0 | return result; |
810 | 0 | } |
811 | | |
812 | 0 | if ((textAdjustment & TextAdjustment::Grapheme) == 0) { |
813 | 0 | textRange = updatedTextRange; |
814 | 0 | break; |
815 | 0 | } |
816 | | |
817 | | // Update text range by grapheme edges (shift start up to the edge of the grapheme) |
818 | 0 | std::tie(found, updatedTextRange.start, updatedTextRange.end) = |
819 | 0 | run->findLimitingGraphemes(updatedTextRange); |
820 | 0 | if (updatedTextRange == textRange) { |
821 | 0 | break; |
822 | 0 | } |
823 | | |
824 | | // Some clusters are inside graphemes and we need to adjust them |
825 | | //SkDebugf("Correct range: [%d:%d) -> [%d:%d)\n", textRange.start, textRange.end, startIndex, endIndex); |
826 | 0 | textRange = updatedTextRange; |
827 | | |
828 | | // Move the start until it's on the grapheme edge (and glypheme, too) |
829 | 0 | } |
830 | 0 | Cluster* start = &fOwner->cluster(fOwner->clusterIndex(textRange.start)); |
831 | 0 | Cluster* end = &fOwner->cluster(fOwner->clusterIndex(textRange.end - (textRange.width() == 0 ? 0 : 1))); |
832 | |
|
833 | 0 | if (!run->leftToRight()) { |
834 | 0 | std::swap(start, end); |
835 | 0 | } |
836 | 0 | result.pos = start->startPos(); |
837 | 0 | result.size = (end->isHardBreak() ? end->startPos() : end->endPos()) - start->startPos(); |
838 | 0 | auto textStartInRun = run->positionX(start->startPos()); |
839 | 0 | auto textStartInLine = runOffsetInLine + textOffsetInRunInLine; |
840 | 0 | if (!run->leftToRight()) { |
841 | 0 | std::swap(start, end); |
842 | 0 | } |
843 | | /* |
844 | | if (!run->fJustificationShifts.empty()) { |
845 | | SkDebugf("Justification for [%d:%d)\n", textRange.start, textRange.end); |
846 | | for (auto i = result.pos; i < result.pos + result.size; ++i) { |
847 | | auto j = run->fJustificationShifts[i]; |
848 | | SkDebugf("[%d] = %f %f\n", i, j.fX, j.fY); |
849 | | } |
850 | | } |
851 | | */ |
852 | | // Calculate the clipping rectangle for the text with cluster edges |
853 | | // There are 2 cases: |
854 | | // EOL (when we expect the last cluster clipped without any spaces) |
855 | | // Anything else (when we want the cluster width contain all the spaces - |
856 | | // coming from letter spacing or word spacing or justification) |
857 | 0 | result.clip = |
858 | 0 | SkRect::MakeXYWH(0, |
859 | 0 | sizes().runTop(run, this->fAscentStyle), |
860 | 0 | run->calculateWidth(result.pos, result.pos + result.size, false), |
861 | 0 | run->calculateHeight(this->fAscentStyle,this->fDescentStyle)); |
862 | | |
863 | | // Correct the width in case the text edges don't match clusters |
864 | | // TODO: This is where we get smart about selecting a part of a cluster |
865 | | // by shaping each grapheme separately and then use the result sizes |
866 | | // to calculate the proportions |
867 | 0 | auto leftCorrection = start->sizeToChar(originalTextRange.start); |
868 | 0 | auto rightCorrection = end->sizeFromChar(originalTextRange.end - 1); |
869 | | /* |
870 | | SkDebugf("[%d: %d) => [%d: %d), @%d, %d: [%f:%f) + [%f:%f) = ", // جَآَهُ |
871 | | originalTextRange.start, originalTextRange.end, textRange.start, textRange.end, |
872 | | result.pos, result.size, |
873 | | result.clip.fLeft, result.clip.fRight, leftCorrection, rightCorrection); |
874 | | */ |
875 | 0 | result.clippingNeeded = leftCorrection != 0 || rightCorrection != 0; |
876 | 0 | if (run->leftToRight()) { |
877 | 0 | result.clip.fLeft += leftCorrection; |
878 | 0 | result.clip.fRight -= rightCorrection; |
879 | 0 | textStartInLine -= leftCorrection; |
880 | 0 | } else { |
881 | 0 | result.clip.fRight -= leftCorrection; |
882 | 0 | result.clip.fLeft += rightCorrection; |
883 | 0 | textStartInLine -= rightCorrection; |
884 | 0 | } |
885 | |
|
886 | 0 | result.clip.offset(textStartInLine, 0); |
887 | | //SkDebugf("@%f[%f:%f)\n", textStartInLine, result.clip.fLeft, result.clip.fRight); |
888 | |
|
889 | 0 | if (compareRound(result.clip.fRight, fAdvance.fX, fOwner->getApplyRoundingHack()) > 0 && !includeGhostSpaces) { |
890 | | // There are few cases when we need it. |
891 | | // The most important one: we measure the text with spaces at the end (or at the beginning in RTL) |
892 | | // and we should ignore these spaces |
893 | 0 | if (fOwner->paragraphStyle().getTextDirection() == TextDirection::kLtr) { |
894 | | // We only use this member for LTR |
895 | 0 | result.fExcludedTrailingSpaces = std::max(result.clip.fRight - fAdvance.fX, 0.0f); |
896 | 0 | result.clippingNeeded = true; |
897 | 0 | result.clip.fRight = fAdvance.fX; |
898 | 0 | } |
899 | 0 | } |
900 | |
|
901 | 0 | if (result.clip.width() < 0) { |
902 | | // Weird situation when glyph offsets move the glyph to the left |
903 | | // (happens with zalgo texts, for instance) |
904 | 0 | result.clip.fRight = result.clip.fLeft; |
905 | 0 | } |
906 | | |
907 | | // The text must be aligned with the lineOffset |
908 | 0 | result.fTextShift = textStartInLine - textStartInRun; |
909 | |
|
910 | 0 | return result; |
911 | 0 | } |
912 | | |
913 | | void TextLine::iterateThroughClustersInGlyphsOrder(bool reversed, |
914 | | bool includeGhosts, |
915 | 0 | const ClustersVisitor& visitor) const { |
916 | | // Walk through the clusters in the logical order (or reverse) |
917 | 0 | SkSpan<const size_t> runs(fRunsInVisualOrder.data(), fRunsInVisualOrder.size()); |
918 | 0 | bool ignore = false; |
919 | 0 | ClusterIndex index = 0; |
920 | 0 | directional_for_each(runs, !reversed, [&](decltype(runs[0]) r) { |
921 | 0 | if (ignore) return; |
922 | 0 | auto run = this->fOwner->run(r); |
923 | 0 | auto trimmedRange = fClusterRange.intersection(run.clusterRange()); |
924 | 0 | auto trailedRange = fGhostClusterRange.intersection(run.clusterRange()); |
925 | 0 | SkASSERT(trimmedRange.start == trailedRange.start); |
926 | |
|
927 | 0 | auto trailed = fOwner->clusters(trailedRange); |
928 | 0 | auto trimmed = fOwner->clusters(trimmedRange); |
929 | 0 | directional_for_each(trailed, reversed != run.leftToRight(), [&](Cluster& cluster) { |
930 | 0 | if (ignore) return; |
931 | 0 | bool ghost = &cluster >= trimmed.end(); |
932 | 0 | if (!includeGhosts && ghost) { |
933 | 0 | return; |
934 | 0 | } |
935 | 0 | if (!visitor(&cluster, index++, ghost)) { |
936 | |
|
937 | 0 | ignore = true; |
938 | 0 | return; |
939 | 0 | } |
940 | 0 | }); |
941 | 0 | }); |
942 | 0 | } |
943 | | |
944 | | SkScalar TextLine::iterateThroughSingleRunByStyles(TextAdjustment textAdjustment, |
945 | | const Run* run, |
946 | | SkScalar runOffset, |
947 | | TextRange textRange, |
948 | | StyleType styleType, |
949 | 0 | const RunStyleVisitor& visitor) const { |
950 | 0 | auto correctContext = [&](TextRange textRange, SkScalar textOffsetInRun) -> ClipContext { |
951 | 0 | auto result = this->measureTextInsideOneRun( |
952 | 0 | textRange, run, runOffset, textOffsetInRun, false, textAdjustment); |
953 | 0 | if (styleType == StyleType::kDecorations) { |
954 | | // Decorations are drawn based on the real font metrics (regardless of styles and strut) |
955 | 0 | result.clip.fTop = this->sizes().runTop(run, LineMetricStyle::CSS); |
956 | 0 | result.clip.fBottom = result.clip.fTop + |
957 | 0 | run->calculateHeight(LineMetricStyle::CSS, LineMetricStyle::CSS); |
958 | 0 | } |
959 | 0 | return result; |
960 | 0 | }; |
961 | |
|
962 | 0 | if (run->fEllipsis) { |
963 | | // Extra efforts to get the ellipsis text style |
964 | 0 | ClipContext clipContext = correctContext(run->textRange(), 0.0f); |
965 | 0 | TextRange testRange(run->fClusterStart, run->fClusterStart + run->textRange().width()); |
966 | 0 | for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { |
967 | 0 | auto block = fOwner->styles().begin() + index; |
968 | 0 | auto intersect = intersected(block->fRange, testRange); |
969 | 0 | if (intersect.width() > 0) { |
970 | 0 | visitor(testRange, block->fStyle, clipContext); |
971 | 0 | return run->advance().fX; |
972 | 0 | } |
973 | 0 | } |
974 | 0 | SkASSERT(false); |
975 | 0 | } |
976 | | |
977 | 0 | if (styleType == StyleType::kNone) { |
978 | 0 | ClipContext clipContext = correctContext(textRange, 0.0f); |
979 | | // The placehoder can have height=0 or (exclusively) width=0 and still be a thing |
980 | 0 | if (clipContext.clip.height() > 0.0f || clipContext.clip.width() > 0.0f) { |
981 | 0 | visitor(textRange, TextStyle(), clipContext); |
982 | 0 | return clipContext.clip.width(); |
983 | 0 | } else { |
984 | 0 | return 0; |
985 | 0 | } |
986 | 0 | } |
987 | | |
988 | 0 | TextIndex start = EMPTY_INDEX; |
989 | 0 | size_t size = 0; |
990 | 0 | const TextStyle* prevStyle = nullptr; |
991 | 0 | SkScalar textOffsetInRun = 0; |
992 | |
|
993 | 0 | const BlockIndex blockRangeSize = fBlockRange.end - fBlockRange.start; |
994 | 0 | for (BlockIndex index = 0; index <= blockRangeSize; ++index) { |
995 | |
|
996 | 0 | TextRange intersect; |
997 | 0 | TextStyle* style = nullptr; |
998 | 0 | if (index < blockRangeSize) { |
999 | 0 | auto block = fOwner->styles().begin() + |
1000 | 0 | (run->leftToRight() ? fBlockRange.start + index : fBlockRange.end - index - 1); |
1001 | | |
1002 | | // Get the text |
1003 | 0 | intersect = intersected(block->fRange, textRange); |
1004 | 0 | if (intersect.width() == 0) { |
1005 | 0 | if (start == EMPTY_INDEX) { |
1006 | | // This style is not applicable to the text yet |
1007 | 0 | continue; |
1008 | 0 | } else { |
1009 | | // We have found all the good styles already |
1010 | | // but we need to process the last one of them |
1011 | 0 | intersect = TextRange(start, start + size); |
1012 | 0 | index = fBlockRange.end; |
1013 | 0 | } |
1014 | 0 | } else { |
1015 | | // Get the style |
1016 | 0 | style = &block->fStyle; |
1017 | 0 | if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) { |
1018 | 0 | size += intersect.width(); |
1019 | | // RTL text intervals move backward |
1020 | 0 | start = std::min(intersect.start, start); |
1021 | 0 | continue; |
1022 | 0 | } else if (start == EMPTY_INDEX ) { |
1023 | | // First time only |
1024 | 0 | prevStyle = style; |
1025 | 0 | size = intersect.width(); |
1026 | 0 | start = intersect.start; |
1027 | 0 | continue; |
1028 | 0 | } |
1029 | 0 | } |
1030 | 0 | } else if (prevStyle != nullptr) { |
1031 | | // This is the last style |
1032 | 0 | } else { |
1033 | 0 | break; |
1034 | 0 | } |
1035 | | |
1036 | | // We have the style and the text |
1037 | 0 | auto runStyleTextRange = TextRange(start, start + size); |
1038 | 0 | ClipContext clipContext = correctContext(runStyleTextRange, textOffsetInRun); |
1039 | 0 | textOffsetInRun += clipContext.clip.width(); |
1040 | 0 | if (clipContext.clip.height() == 0) { |
1041 | 0 | continue; |
1042 | 0 | } |
1043 | 0 | visitor(runStyleTextRange, *prevStyle, clipContext); |
1044 | | |
1045 | | // Start all over again |
1046 | 0 | prevStyle = style; |
1047 | 0 | start = intersect.start; |
1048 | 0 | size = intersect.width(); |
1049 | 0 | } |
1050 | 0 | return textOffsetInRun; |
1051 | 0 | } |
1052 | | |
1053 | 0 | void TextLine::iterateThroughVisualRuns(bool includingGhostSpaces, const RunVisitor& visitor) const { |
1054 | | |
1055 | | // Walk through all the runs that intersect with the line in visual order |
1056 | 0 | SkScalar width = 0; |
1057 | 0 | SkScalar runOffset = 0; |
1058 | 0 | SkScalar totalWidth = 0; |
1059 | 0 | auto textRange = includingGhostSpaces ? this->textWithNewlines() : this->trimmedText(); |
1060 | |
|
1061 | 0 | if (this->ellipsis() != nullptr && fOwner->paragraphStyle().getTextDirection() == TextDirection::kRtl) { |
1062 | 0 | runOffset = this->ellipsis()->offset().fX; |
1063 | 0 | if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) { |
1064 | 0 | } |
1065 | 0 | } |
1066 | |
|
1067 | 0 | for (auto& runIndex : fRunsInVisualOrder) { |
1068 | |
|
1069 | 0 | const auto run = &this->fOwner->run(runIndex); |
1070 | 0 | auto lineIntersection = intersected(run->textRange(), textRange); |
1071 | 0 | if (lineIntersection.width() == 0 && this->width() != 0) { |
1072 | | // TODO: deal with empty runs in a better way |
1073 | 0 | continue; |
1074 | 0 | } |
1075 | 0 | if (!run->leftToRight() && runOffset == 0 && includingGhostSpaces) { |
1076 | | // runOffset does not take in account a possibility |
1077 | | // that RTL run could start before the line (trailing spaces) |
1078 | | // so we need to do runOffset -= "trailing whitespaces length" |
1079 | 0 | TextRange whitespaces = intersected( |
1080 | 0 | TextRange(fTextExcludingSpaces.end, fTextIncludingNewlines.end), run->fTextRange); |
1081 | 0 | if (whitespaces.width() > 0) { |
1082 | 0 | auto whitespacesLen = measureTextInsideOneRun(whitespaces, run, runOffset, 0, true, TextAdjustment::GlyphCluster).clip.width(); |
1083 | 0 | runOffset -= whitespacesLen; |
1084 | 0 | } |
1085 | 0 | } |
1086 | 0 | runOffset += width; |
1087 | 0 | totalWidth += width; |
1088 | 0 | if (!visitor(run, runOffset, lineIntersection, &width)) { |
1089 | 0 | return; |
1090 | 0 | } |
1091 | 0 | } |
1092 | | |
1093 | 0 | runOffset += width; |
1094 | 0 | totalWidth += width; |
1095 | |
|
1096 | 0 | if (this->ellipsis() != nullptr && fOwner->paragraphStyle().getTextDirection() == TextDirection::kLtr) { |
1097 | 0 | if (visitor(ellipsis(), runOffset, ellipsis()->textRange(), &width)) { |
1098 | 0 | totalWidth += width; |
1099 | 0 | } |
1100 | 0 | } |
1101 | |
|
1102 | 0 | if (!includingGhostSpaces && compareRound(totalWidth, this->width(), fOwner->getApplyRoundingHack()) != 0) { |
1103 | | // This is a very important assert! |
1104 | | // It asserts that 2 different ways of calculation come with the same results |
1105 | 0 | SkDEBUGFAILF("ASSERT: %f != %f\n", totalWidth, this->width()); |
1106 | 0 | } |
1107 | 0 | } |
1108 | | |
1109 | 0 | SkVector TextLine::offset() const { |
1110 | 0 | return fOffset + SkVector::Make(fShift, 0); |
1111 | 0 | } |
1112 | | |
1113 | 0 | LineMetrics TextLine::getMetrics() const { |
1114 | 0 | LineMetrics result; |
1115 | 0 | SkASSERT(fOwner); |
1116 | | |
1117 | | // Fill out the metrics |
1118 | 0 | fOwner->ensureUTF16Mapping(); |
1119 | 0 | result.fStartIndex = fOwner->getUTF16Index(fTextExcludingSpaces.start); |
1120 | 0 | result.fEndExcludingWhitespaces = fOwner->getUTF16Index(fTextExcludingSpaces.end); |
1121 | 0 | result.fEndIndex = fOwner->getUTF16Index(fText.end); |
1122 | 0 | result.fEndIncludingNewline = fOwner->getUTF16Index(fTextIncludingNewlines.end); |
1123 | 0 | result.fHardBreak = endsWithHardLineBreak(); |
1124 | 0 | result.fAscent = - fMaxRunMetrics.ascent(); |
1125 | 0 | result.fDescent = fMaxRunMetrics.descent(); |
1126 | 0 | result.fUnscaledAscent = - fMaxRunMetrics.ascent(); // TODO: implement |
1127 | 0 | result.fHeight = fAdvance.fY; |
1128 | 0 | result.fWidth = fAdvance.fX; |
1129 | 0 | if (fOwner->getApplyRoundingHack()) { |
1130 | 0 | result.fHeight = littleRound(result.fHeight); |
1131 | 0 | result.fWidth = littleRound(result.fWidth); |
1132 | 0 | } |
1133 | 0 | result.fLeft = this->offset().fX; |
1134 | | // This is Flutter definition of a baseline |
1135 | 0 | result.fBaseline = this->offset().fY + this->height() - this->sizes().descent(); |
1136 | 0 | result.fLineNumber = this - fOwner->lines().begin(); |
1137 | | |
1138 | | // Fill out the style parts |
1139 | 0 | this->iterateThroughVisualRuns(false, |
1140 | 0 | [this, &result] |
1141 | 0 | (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { |
1142 | 0 | if (run->placeholderStyle() != nullptr) { |
1143 | 0 | *runWidthInLine = run->advance().fX; |
1144 | 0 | return true; |
1145 | 0 | } |
1146 | 0 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
1147 | 0 | TextAdjustment::GlyphCluster, run, runOffsetInLine, textRange, StyleType::kForeground, |
1148 | 0 | [&result, &run](TextRange textRange, const TextStyle& style, const ClipContext& context) { |
1149 | 0 | SkFontMetrics fontMetrics; |
1150 | 0 | run->fFont.getMetrics(&fontMetrics); |
1151 | 0 | StyleMetrics styleMetrics(&style, fontMetrics); |
1152 | 0 | result.fLineMetrics.emplace(textRange.start, styleMetrics); |
1153 | 0 | }); |
1154 | 0 | return true; |
1155 | 0 | }); |
1156 | |
|
1157 | 0 | return result; |
1158 | 0 | } |
1159 | | |
1160 | 0 | bool TextLine::isFirstLine() const { |
1161 | 0 | return this == &fOwner->lines().front(); |
1162 | 0 | } |
1163 | | |
1164 | 0 | bool TextLine::isLastLine() const { |
1165 | 0 | return this == &fOwner->lines().back(); |
1166 | 0 | } |
1167 | | |
1168 | 0 | bool TextLine::endsWithHardLineBreak() const { |
1169 | | // TODO: For some reason Flutter imagines a hard line break at the end of the last line. |
1170 | | // To be removed... |
1171 | 0 | return (fGhostClusterRange.width() > 0 && fOwner->cluster(fGhostClusterRange.end - 1).isHardBreak()) || |
1172 | 0 | fEllipsis != nullptr || |
1173 | 0 | fGhostClusterRange.end == fOwner->clusters().size() - 1; |
1174 | 0 | } |
1175 | | |
1176 | | void TextLine::getRectsForRange(TextRange textRange0, |
1177 | | RectHeightStyle rectHeightStyle, |
1178 | | RectWidthStyle rectWidthStyle, |
1179 | | std::vector<TextBox>& boxes) const |
1180 | 0 | { |
1181 | 0 | const Run* lastRun = nullptr; |
1182 | 0 | auto startBox = boxes.size(); |
1183 | 0 | this->iterateThroughVisualRuns(true, |
1184 | 0 | [textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this] |
1185 | 0 | (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { |
1186 | 0 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
1187 | 0 | TextAdjustment::GraphemeGluster, run, runOffsetInLine, textRange, StyleType::kNone, |
1188 | 0 | [run, runOffsetInLine, textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this] |
1189 | 0 | (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& lineContext) { |
1190 | |
|
1191 | 0 | auto intersect = textRange * textRange0; |
1192 | 0 | if (intersect.empty()) { |
1193 | 0 | return true; |
1194 | 0 | } |
1195 | | |
1196 | 0 | auto paragraphStyle = fOwner->paragraphStyle(); |
1197 | | |
1198 | | // Found a run that intersects with the text |
1199 | 0 | auto context = this->measureTextInsideOneRun( |
1200 | 0 | intersect, run, runOffsetInLine, 0, true, TextAdjustment::GraphemeGluster); |
1201 | 0 | SkRect clip = context.clip; |
1202 | 0 | clip.offset(lineContext.fTextShift - context.fTextShift, 0); |
1203 | |
|
1204 | 0 | switch (rectHeightStyle) { |
1205 | 0 | case RectHeightStyle::kMax: |
1206 | | // TODO: Change it once flutter rolls into google3 |
1207 | | // (probably will break things if changed before) |
1208 | 0 | clip.fBottom = this->height(); |
1209 | 0 | clip.fTop = this->sizes().delta(); |
1210 | 0 | break; |
1211 | 0 | case RectHeightStyle::kIncludeLineSpacingTop: { |
1212 | 0 | clip.fBottom = this->height(); |
1213 | 0 | clip.fTop = this->sizes().delta(); |
1214 | 0 | auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent(); |
1215 | 0 | if (isFirstLine()) { |
1216 | 0 | clip.fTop += verticalShift; |
1217 | 0 | } |
1218 | 0 | break; |
1219 | 0 | } |
1220 | 0 | case RectHeightStyle::kIncludeLineSpacingMiddle: { |
1221 | 0 | clip.fBottom = this->height(); |
1222 | 0 | clip.fTop = this->sizes().delta(); |
1223 | 0 | auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent(); |
1224 | 0 | clip.offset(0, verticalShift / 2.0); |
1225 | 0 | if (isFirstLine()) { |
1226 | 0 | clip.fTop += verticalShift / 2.0; |
1227 | 0 | } |
1228 | 0 | if (isLastLine()) { |
1229 | 0 | clip.fBottom -= verticalShift / 2.0; |
1230 | 0 | } |
1231 | 0 | break; |
1232 | 0 | } |
1233 | 0 | case RectHeightStyle::kIncludeLineSpacingBottom: { |
1234 | 0 | clip.fBottom = this->height(); |
1235 | 0 | clip.fTop = this->sizes().delta(); |
1236 | 0 | auto verticalShift = this->sizes().rawAscent() - this->sizes().ascent(); |
1237 | 0 | clip.offset(0, verticalShift); |
1238 | 0 | if (isLastLine()) { |
1239 | 0 | clip.fBottom -= verticalShift; |
1240 | 0 | } |
1241 | 0 | break; |
1242 | 0 | } |
1243 | 0 | case RectHeightStyle::kStrut: { |
1244 | 0 | const auto& strutStyle = paragraphStyle.getStrutStyle(); |
1245 | 0 | if (strutStyle.getStrutEnabled() |
1246 | 0 | && strutStyle.getFontSize() > 0) { |
1247 | 0 | auto strutMetrics = fOwner->strutMetrics(); |
1248 | 0 | auto top = this->baseline(); |
1249 | 0 | clip.fTop = top + strutMetrics.ascent(); |
1250 | 0 | clip.fBottom = top + strutMetrics.descent(); |
1251 | 0 | } |
1252 | 0 | } |
1253 | 0 | break; |
1254 | 0 | case RectHeightStyle::kTight: { |
1255 | 0 | if (run->fHeightMultiplier <= 0) { |
1256 | 0 | break; |
1257 | 0 | } |
1258 | 0 | const auto effectiveBaseline = this->baseline() + this->sizes().delta(); |
1259 | 0 | clip.fTop = effectiveBaseline + run->ascent(); |
1260 | 0 | clip.fBottom = effectiveBaseline + run->descent(); |
1261 | 0 | } |
1262 | 0 | break; |
1263 | 0 | default: |
1264 | 0 | SkASSERT(false); |
1265 | 0 | break; |
1266 | 0 | } |
1267 | | |
1268 | | // Separate trailing spaces and move them in the default order of the paragraph |
1269 | | // in case the run order and the paragraph order don't match |
1270 | 0 | SkRect trailingSpaces = SkRect::MakeEmpty(); |
1271 | 0 | if (this->trimmedText().end <this->textWithNewlines().end && // Line has trailing space |
1272 | 0 | this->textWithNewlines().end == intersect.end && // Range is at the end of the line |
1273 | 0 | this->trimmedText().end > intersect.start) // Range has more than just spaces |
1274 | 0 | { |
1275 | 0 | auto delta = this->spacesWidth(); |
1276 | 0 | trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0); |
1277 | | // There are trailing spaces in this run |
1278 | 0 | if (paragraphStyle.getTextAlign() == TextAlign::kJustify && isLastLine()) |
1279 | 0 | { |
1280 | | // TODO: this is just a patch. Make it right later (when it's clear what and how) |
1281 | 0 | trailingSpaces = clip; |
1282 | 0 | if(run->leftToRight()) { |
1283 | 0 | trailingSpaces.fLeft = this->width(); |
1284 | 0 | clip.fRight = this->width(); |
1285 | 0 | } else { |
1286 | 0 | trailingSpaces.fRight = 0; |
1287 | 0 | clip.fLeft = 0; |
1288 | 0 | } |
1289 | 0 | } else if (paragraphStyle.getTextDirection() == TextDirection::kRtl && |
1290 | 0 | !run->leftToRight()) |
1291 | 0 | { |
1292 | | // Split |
1293 | 0 | trailingSpaces = clip; |
1294 | 0 | trailingSpaces.fLeft = - delta; |
1295 | 0 | trailingSpaces.fRight = 0; |
1296 | 0 | clip.fLeft += delta; |
1297 | 0 | } else if (paragraphStyle.getTextDirection() == TextDirection::kLtr && |
1298 | 0 | run->leftToRight()) |
1299 | 0 | { |
1300 | | // Split |
1301 | 0 | trailingSpaces = clip; |
1302 | 0 | trailingSpaces.fLeft = this->width(); |
1303 | 0 | trailingSpaces.fRight = trailingSpaces.fLeft + delta; |
1304 | 0 | clip.fRight -= delta; |
1305 | 0 | } |
1306 | 0 | } |
1307 | |
|
1308 | 0 | clip.offset(this->offset()); |
1309 | 0 | if (trailingSpaces.width() > 0) { |
1310 | 0 | trailingSpaces.offset(this->offset()); |
1311 | 0 | } |
1312 | | |
1313 | | // Check if we can merge two boxes instead of adding a new one |
1314 | 0 | auto merge = [&lastRun, &context, &boxes](SkRect clip) { |
1315 | 0 | bool mergedBoxes = false; |
1316 | 0 | if (!boxes.empty() && |
1317 | 0 | lastRun != nullptr && |
1318 | 0 | context.run->leftToRight() == lastRun->leftToRight() && |
1319 | 0 | lastRun->placeholderStyle() == nullptr && |
1320 | 0 | context.run->placeholderStyle() == nullptr && |
1321 | 0 | nearlyEqual(lastRun->heightMultiplier(), |
1322 | 0 | context.run->heightMultiplier()) && |
1323 | 0 | lastRun->font() == context.run->font()) |
1324 | 0 | { |
1325 | 0 | auto& lastBox = boxes.back(); |
1326 | 0 | if (nearlyEqual(lastBox.rect.fTop, clip.fTop) && |
1327 | 0 | nearlyEqual(lastBox.rect.fBottom, clip.fBottom) && |
1328 | 0 | (nearlyEqual(lastBox.rect.fLeft, clip.fRight) || |
1329 | 0 | nearlyEqual(lastBox.rect.fRight, clip.fLeft))) |
1330 | 0 | { |
1331 | 0 | lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft); |
1332 | 0 | lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight); |
1333 | 0 | mergedBoxes = true; |
1334 | 0 | } |
1335 | 0 | } |
1336 | 0 | lastRun = context.run; |
1337 | 0 | return mergedBoxes; |
1338 | 0 | }; |
1339 | |
|
1340 | 0 | if (!merge(clip)) { |
1341 | 0 | boxes.emplace_back(clip, context.run->getTextDirection()); |
1342 | 0 | } |
1343 | 0 | if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) { |
1344 | 0 | boxes.emplace_back(trailingSpaces, paragraphStyle.getTextDirection()); |
1345 | 0 | } |
1346 | |
|
1347 | 0 | if (rectWidthStyle == RectWidthStyle::kMax && !isLastLine()) { |
1348 | | // Align the very left/right box horizontally |
1349 | 0 | auto lineStart = this->offset().fX; |
1350 | 0 | auto lineEnd = this->offset().fX + this->width(); |
1351 | 0 | auto left = boxes[startBox]; |
1352 | 0 | auto right = boxes.back(); |
1353 | 0 | if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) { |
1354 | 0 | left.rect.fRight = left.rect.fLeft; |
1355 | 0 | left.rect.fLeft = 0; |
1356 | 0 | boxes.insert(boxes.begin() + startBox + 1, left); |
1357 | 0 | } |
1358 | 0 | if (right.direction == TextDirection::kLtr && |
1359 | 0 | right.rect.fRight >= lineEnd && |
1360 | 0 | right.rect.fRight < fOwner->widthWithTrailingSpaces()) { |
1361 | 0 | right.rect.fLeft = right.rect.fRight; |
1362 | 0 | right.rect.fRight = fOwner->widthWithTrailingSpaces(); |
1363 | 0 | boxes.emplace_back(right); |
1364 | 0 | } |
1365 | 0 | } |
1366 | |
|
1367 | 0 | return true; |
1368 | 0 | }); |
1369 | 0 | return true; |
1370 | 0 | }); |
1371 | 0 | if (fOwner->getApplyRoundingHack()) { |
1372 | 0 | for (auto& r : boxes) { |
1373 | 0 | r.rect.fLeft = littleRound(r.rect.fLeft); |
1374 | 0 | r.rect.fRight = littleRound(r.rect.fRight); |
1375 | 0 | r.rect.fTop = littleRound(r.rect.fTop); |
1376 | 0 | r.rect.fBottom = littleRound(r.rect.fBottom); |
1377 | 0 | } |
1378 | 0 | } |
1379 | 0 | } |
1380 | | |
1381 | 0 | PositionWithAffinity TextLine::getGlyphPositionAtCoordinate(SkScalar dx) { |
1382 | |
|
1383 | 0 | if (SkScalarNearlyZero(this->width()) && SkScalarNearlyZero(this->spacesWidth())) { |
1384 | | // TODO: this is one of the flutter changes that have to go away eventually |
1385 | | // Empty line is a special case in txtlib (but only when there are no spaces, too) |
1386 | 0 | auto utf16Index = fOwner->getUTF16Index(this->fTextExcludingSpaces.end); |
1387 | 0 | return { SkToS32(utf16Index) , kDownstream }; |
1388 | 0 | } |
1389 | | |
1390 | 0 | PositionWithAffinity result(0, Affinity::kDownstream); |
1391 | 0 | this->iterateThroughVisualRuns(true, |
1392 | 0 | [this, dx, &result] |
1393 | 0 | (const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) { |
1394 | 0 | bool keepLooking = true; |
1395 | 0 | *runWidthInLine = this->iterateThroughSingleRunByStyles( |
1396 | 0 | TextAdjustment::GraphemeGluster, run, runOffsetInLine, textRange, StyleType::kNone, |
1397 | 0 | [this, run, dx, &result, &keepLooking] |
1398 | 0 | (TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context0) { |
1399 | |
|
1400 | 0 | SkScalar offsetX = this->offset().fX; |
1401 | 0 | ClipContext context = context0; |
1402 | | |
1403 | | // Correct the clip size because libtxt counts trailing spaces |
1404 | 0 | if (run->leftToRight()) { |
1405 | 0 | context.clip.fRight += context.fExcludedTrailingSpaces; // extending clip to the right |
1406 | 0 | } else { |
1407 | | // Clip starts from 0; we cannot extend it to the left from that |
1408 | 0 | } |
1409 | | // However, we need to offset the clip |
1410 | 0 | context.clip.offset(offsetX, 0.0f); |
1411 | | |
1412 | | // This patch will help us to avoid a floating point error |
1413 | 0 | if (SkScalarNearlyEqual(context.clip.fRight, dx, 0.01f)) { |
1414 | 0 | context.clip.fRight = dx; |
1415 | 0 | } |
1416 | |
|
1417 | 0 | if (dx <= context.clip.fLeft) { |
1418 | | // All the other runs are placed right of this one |
1419 | 0 | auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos)); |
1420 | 0 | if (run->leftToRight()) { |
1421 | 0 | result = { SkToS32(utf16Index), kDownstream}; |
1422 | 0 | keepLooking = false; |
1423 | 0 | } else { |
1424 | 0 | result = { SkToS32(utf16Index + 1), kUpstream}; |
1425 | | // If we haven't reached the end of the run we need to keep looking |
1426 | 0 | keepLooking = context.pos != 0; |
1427 | 0 | } |
1428 | | // For RTL we go another way |
1429 | 0 | return !run->leftToRight(); |
1430 | 0 | } |
1431 | | |
1432 | 0 | if (dx >= context.clip.fRight) { |
1433 | | // We have to keep looking ; just in case keep the last one as the closest |
1434 | 0 | auto utf16Index = fOwner->getUTF16Index(context.run->globalClusterIndex(context.pos + context.size)); |
1435 | 0 | if (run->leftToRight()) { |
1436 | 0 | result = {SkToS32(utf16Index), kUpstream}; |
1437 | 0 | } else { |
1438 | 0 | result = {SkToS32(utf16Index), kDownstream}; |
1439 | 0 | } |
1440 | | // For RTL we go another way |
1441 | 0 | return run->leftToRight(); |
1442 | 0 | } |
1443 | | |
1444 | | // So we found the run that contains our coordinates |
1445 | | // Find the glyph position in the run that is the closest left of our point |
1446 | | // TODO: binary search |
1447 | 0 | size_t found = context.pos; |
1448 | 0 | for (size_t index = context.pos; index < context.pos + context.size; ++index) { |
1449 | | // TODO: this rounding is done to match Flutter tests. Must be removed.. |
1450 | 0 | auto end = context.run->positionX(index) + context.fTextShift + offsetX; |
1451 | 0 | if (fOwner->getApplyRoundingHack()) { |
1452 | 0 | end = littleRound(end); |
1453 | 0 | } |
1454 | 0 | if (end > dx) { |
1455 | 0 | break; |
1456 | 0 | } else if (end == dx && !context.run->leftToRight()) { |
1457 | | // When we move RTL variable end points to the beginning of the code point which is included |
1458 | 0 | found = index; |
1459 | 0 | break; |
1460 | 0 | } |
1461 | 0 | found = index; |
1462 | 0 | } |
1463 | |
|
1464 | 0 | SkScalar glyphemePosLeft = context.run->positionX(found) + context.fTextShift + offsetX; |
1465 | 0 | SkScalar glyphemesWidth = context.run->positionX(found + 1) - context.run->positionX(found); |
1466 | | |
1467 | | // Find the grapheme range that contains the point |
1468 | 0 | auto clusterIndex8 = context.run->globalClusterIndex(found); |
1469 | 0 | auto clusterEnd8 = context.run->globalClusterIndex(found + 1); |
1470 | 0 | auto graphemes = fOwner->countSurroundingGraphemes({clusterIndex8, clusterEnd8}); |
1471 | |
|
1472 | 0 | SkScalar center = glyphemePosLeft + glyphemesWidth / 2; |
1473 | 0 | if (graphemes.size() > 1) { |
1474 | | // Calculate the position proportionally based on grapheme count |
1475 | 0 | SkScalar averageGraphemeWidth = glyphemesWidth / graphemes.size(); |
1476 | 0 | SkScalar delta = dx - glyphemePosLeft; |
1477 | 0 | int graphemeIndex = SkScalarNearlyZero(averageGraphemeWidth) |
1478 | 0 | ? 0 |
1479 | 0 | : SkScalarFloorToInt(delta / averageGraphemeWidth); |
1480 | 0 | auto graphemeCenter = glyphemePosLeft + graphemeIndex * averageGraphemeWidth + |
1481 | 0 | averageGraphemeWidth / 2; |
1482 | 0 | auto graphemeUtf8Index = graphemes[graphemeIndex]; |
1483 | 0 | if ((dx < graphemeCenter) == context.run->leftToRight()) { |
1484 | 0 | size_t utf16Index = fOwner->getUTF16Index(graphemeUtf8Index); |
1485 | 0 | result = { SkToS32(utf16Index), kDownstream }; |
1486 | 0 | } else { |
1487 | 0 | size_t utf16Index = fOwner->getUTF16Index(graphemeUtf8Index + 1); |
1488 | 0 | result = { SkToS32(utf16Index), kUpstream }; |
1489 | 0 | } |
1490 | | // Keep UTF16 index as is |
1491 | 0 | } else if ((dx < center) == context.run->leftToRight()) { |
1492 | 0 | size_t utf16Index = fOwner->getUTF16Index(clusterIndex8); |
1493 | 0 | result = { SkToS32(utf16Index), kDownstream }; |
1494 | 0 | } else { |
1495 | 0 | size_t utf16Index = context.run->leftToRight() |
1496 | 0 | ? fOwner->getUTF16Index(clusterEnd8) |
1497 | 0 | : fOwner->getUTF16Index(clusterIndex8) + 1; |
1498 | 0 | result = { SkToS32(utf16Index), kUpstream }; |
1499 | 0 | } |
1500 | |
|
1501 | 0 | return keepLooking = false; |
1502 | |
|
1503 | 0 | }); |
1504 | 0 | return keepLooking; |
1505 | 0 | } |
1506 | 0 | ); |
1507 | 0 | return result; |
1508 | 0 | } |
1509 | | |
1510 | 0 | void TextLine::getRectsForPlaceholders(std::vector<TextBox>& boxes) { |
1511 | 0 | this->iterateThroughVisualRuns( |
1512 | 0 | true, |
1513 | 0 | [&boxes, this](const Run* run, SkScalar runOffset, TextRange textRange, |
1514 | 0 | SkScalar* width) { |
1515 | 0 | auto context = this->measureTextInsideOneRun( |
1516 | 0 | textRange, run, runOffset, 0, true, TextAdjustment::GraphemeGluster); |
1517 | 0 | *width = context.clip.width(); |
1518 | |
|
1519 | 0 | if (textRange.width() == 0) { |
1520 | 0 | return true; |
1521 | 0 | } |
1522 | 0 | if (!run->isPlaceholder()) { |
1523 | 0 | return true; |
1524 | 0 | } |
1525 | | |
1526 | 0 | SkRect clip = context.clip; |
1527 | 0 | clip.offset(this->offset()); |
1528 | |
|
1529 | 0 | if (fOwner->getApplyRoundingHack()) { |
1530 | 0 | clip.fLeft = littleRound(clip.fLeft); |
1531 | 0 | clip.fRight = littleRound(clip.fRight); |
1532 | 0 | clip.fTop = littleRound(clip.fTop); |
1533 | 0 | clip.fBottom = littleRound(clip.fBottom); |
1534 | 0 | } |
1535 | 0 | boxes.emplace_back(clip, run->getTextDirection()); |
1536 | 0 | return true; |
1537 | 0 | }); |
1538 | 0 | } |
1539 | | } // namespace textlayout |
1540 | | } // namespace skia |