1 // Copyright 2020 Google LLC.
2 #include <map>
3 #include "include/core/SkPathBuilder.h"
4 #include "modules/skparagraph/src/Decorations.h"
5
6 namespace skia {
7 namespace textlayout {
8 static const std::map<SkPaint::Style, RSDrawing::Paint::PaintStyle> PAINT_STYLE = {
9 {SkPaint::kFill_Style, RSDrawing::Paint::PaintStyle::PAINT_FILL},
10 {SkPaint::kStroke_Style, RSDrawing::Paint::PaintStyle::PAINT_STROKE},
11 {SkPaint::kStrokeAndFill_Style, RSDrawing::Paint::PaintStyle::PAINT_FILL_STROKE},
12 };
13 namespace {
draw_line_as_rect(ParagraphPainter * painter,SkScalar x,SkScalar y,SkScalar width,const ParagraphPainter::DecorationStyle & decorStyle)14 void draw_line_as_rect(ParagraphPainter* painter, SkScalar x, SkScalar y, SkScalar width,
15 const ParagraphPainter::DecorationStyle& decorStyle) {
16 SkASSERT(decorStyle.skPaint().getPathEffect() == nullptr);
17 SkASSERT(decorStyle.skPaint().getStrokeCap() == SkPaint::kButt_Cap);
18 SkASSERT(decorStyle.skPaint().getStrokeWidth() > 0); // this trick won't work for hairlines
19
20 SkScalar radius = decorStyle.getStrokeWidth() * 0.5f;
21 painter->drawFilledRect({x, y - radius, x + width, y + radius}, decorStyle);
22 }
23
24 const SkScalar kDoubleDecorationSpacing = 3.0f;
25 } // namespace
26
calculateThickness(const TextStyle & textStyle,const TextLine::ClipContext & context)27 SkScalar Decorations::calculateThickness(const TextStyle& textStyle, const TextLine::ClipContext& context) {
28 calculateThickness(textStyle, const_cast<RSFont&>(context.run->font()).GetTypeface());
29 return fThickness;
30 }
31
32 #ifdef OHOS_SUPPORT
updateDecorationPosition(TextDecoration decorationMode,SkScalar baselineShift,const TextLine::ClipContext & context,SkScalar & positionY)33 void Decorations::updateDecorationPosition(TextDecoration decorationMode, SkScalar baselineShift,
34 const TextLine::ClipContext& context, SkScalar& positionY) {
35 switch (getVerticalAlignment()) {
36 case TextVerticalAlign::TOP:
37 if (decorationMode == TextDecoration::kOverline) {
38 positionY = context.run->getTopInGroup() - baselineShift;
39 }
40 break;
41 case TextVerticalAlign::CENTER:
42 if (decorationMode == TextDecoration::kLineThrough) {
43 // Line through is in the middle of the line
44 positionY = fDecorationContext.lineHeight / 2 - baselineShift;
45 }
46 break;
47 case TextVerticalAlign::BOTTOM:
48 if (decorationMode == TextDecoration::kUnderline) {
49 positionY = fDecorationContext.lineHeight - baselineShift;
50 }
51 break;
52 default:
53 break;
54 }
55 }
56 #endif
57
paint(ParagraphPainter * painter,const TextStyle & textStyle,const TextLine::ClipContext & context,SkScalar baseline)58 void Decorations::paint(ParagraphPainter* painter, const TextStyle& textStyle, const TextLine::ClipContext& context, SkScalar baseline) {
59 if (textStyle.getDecorationType() == TextDecoration::kNoDecoration) {
60 return;
61 }
62
63 // Get thickness and position
64 #ifndef USE_SKIA_TXT
65 calculateThickness(textStyle, context.run->font().refTypeface());
66 #else
67 calculateThickness(textStyle, const_cast<RSFont&>(context.run->font()).GetTypeface());
68 #endif
69
70 for (auto decoration : AllTextDecorations) {
71 if ((textStyle.getDecorationType() & decoration) == 0) {
72 continue;
73 }
74
75 #ifdef OHOS_SUPPORT
76 SkScalar textBaselineShift = 0.0f;
77 if (getVerticalAlignment() == TextVerticalAlign::BASELINE) {
78 textBaselineShift = textStyle.getTotalVerticalShift();
79 } else {
80 textBaselineShift = context.run->baselineShift();
81 }
82 calculatePosition(decoration,
83 decoration == TextDecoration::kOverline
84 ? context.run->correctAscent() - context.run->ascent()
85 : context.run->correctAscent(),
86 textStyle, textBaselineShift, *context.run);
87 #else
88 calculatePosition(decoration,
89 decoration == TextDecoration::kOverline
90 ? context.run->correctAscent() - context.run->ascent()
91 : context.run->correctAscent(), textStyle.getDecorationStyle(),
92 textStyle.getBaselineShift());
93 #endif
94
95 calculatePaint(textStyle);
96
97 auto width = context.clip.width();
98
99 #ifdef OHOS_SUPPORT
100 if (context.fIsTrimTrailingSpaceWidth) {
101 width = std::max(width - context.fTrailingSpaceWidth, 0.0f);
102 }
103 #endif
104 SkScalar x = context.clip.left();
105 SkScalar y = (TextDecoration::kUnderline == decoration) ?
106 fPosition : (context.clip.top() + fPosition);
107 #ifdef OHOS_SUPPORT
108 updateDecorationPosition(decoration, textBaselineShift, context, y);
109 baseline += context.run->getVerticalAlignShift();
110 #endif
111
112 bool drawGaps = textStyle.getDecorationMode() == TextDecorationMode::kGaps &&
113 textStyle.getDecorationType() == TextDecoration::kUnderline;
114
115 switch (textStyle.getDecorationStyle()) {
116 case TextDecorationStyle::kWavy: {
117 if (drawGaps) {
118 calculateAvoidanceWaves(textStyle, context.clip);
119 fPath.Offset(x, y);
120 painter->drawPath(fPath, fDecorStyle);
121 break;
122 }
123 calculateWaves(textStyle, context.clip);
124 #ifndef USE_SKIA_TXT
125 fPath.offset(x, y);
126 #else
127 fPath.Offset(x, y);
128 #endif
129 painter->drawPath(fPath, fDecorStyle);
130 break;
131 }
132 case TextDecorationStyle::kDouble: {
133 SkScalar bottom = y + kDoubleDecorationSpacing * fThickness / 2.0;
134 if (drawGaps) {
135 SkScalar left = x - context.fTextShift;
136 painter->translate(context.fTextShift, 0);
137 calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness, textStyle);
138 painter->drawPath(fPath, fDecorStyle);
139 calculateGaps(context, SkRect::MakeXYWH(left, bottom, width, fThickness), baseline,
140 fThickness, textStyle);
141 painter->drawPath(fPath, fDecorStyle);
142 } else {
143 draw_line_as_rect(painter, x, y, width, fDecorStyle);
144 draw_line_as_rect(painter, x, bottom, width, fDecorStyle);
145 }
146 break;
147 }
148 case TextDecorationStyle::kDashed:
149 case TextDecorationStyle::kDotted:
150 if (drawGaps) {
151 SkScalar left = x - context.fTextShift;
152 painter->translate(context.fTextShift, 0);
153 calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness, textStyle);
154 painter->drawPath(fPath, fDecorStyle);
155 } else {
156 painter->drawLine(x, y, x + width, y, fDecorStyle);
157 }
158 break;
159 case TextDecorationStyle::kSolid:
160 if (drawGaps) {
161 SkScalar left = x - context.fTextShift;
162 painter->translate(context.fTextShift, 0);
163 SkRect rect = SkRect::MakeXYWH(left, y, width, fThickness);
164 calculateGaps(context, rect, baseline, fThickness, textStyle);
165 painter->drawPath(fPath, fDecorStyle);
166 } else {
167 draw_line_as_rect(painter, x, y, width, fDecorStyle);
168 }
169 break;
170 default:break;
171 }
172 }
173 }
174
ConvertDrawingStyle(SkPaint::Style skStyle)175 static RSDrawing::Paint::PaintStyle ConvertDrawingStyle(SkPaint::Style skStyle)
176 {
177 if (PAINT_STYLE.find(skStyle) != PAINT_STYLE.end()) {
178 return PAINT_STYLE.at(skStyle);
179 } else {
180 return RSDrawing::Paint::PaintStyle::PAINT_NONE;
181 }
182 }
183
ConvertDecorStyle(const ParagraphPainter::DecorationStyle & decorStyle)184 static RSDrawing::Paint ConvertDecorStyle(const ParagraphPainter::DecorationStyle& decorStyle)
185 {
186 const SkPaint& decorPaint = decorStyle.skPaint();
187 RSDrawing::Paint paint;
188 paint.SetStyle(ConvertDrawingStyle(decorPaint.getStyle()));
189 paint.SetAntiAlias(decorPaint.isAntiAlias());
190 paint.SetColor(decorPaint.getColor());
191 paint.SetWidth(decorPaint.getStrokeWidth());
192 if (decorStyle.getDashPathEffect().has_value()) {
193 auto dashPathEffect = decorStyle.getDashPathEffect().value();
194 RSDrawing::scalar intervals[] = {dashPathEffect.fOnLength, dashPathEffect.fOffLength,
195 dashPathEffect.fOnLength, dashPathEffect.fOffLength};
196 size_t count = sizeof(intervals) / sizeof(intervals[0]);
197 auto pathEffect1 = RSDrawing::PathEffect::CreateDashPathEffect(intervals, count, 0.0f);
198 auto pathEffect2 = RSDrawing::PathEffect::CreateDiscretePathEffect(0, 0);
199 auto pathEffect = RSDrawing::PathEffect::CreateComposePathEffect(*pathEffect1.get(), *pathEffect2.get());
200 paint.SetPathEffect(pathEffect);
201 }
202 return paint;
203 }
204
calculateGaps(const TextLine::ClipContext & context,const SkRect & rect,SkScalar baseline,SkScalar halo,const TextStyle & textStyle)205 void Decorations::calculateGaps(const TextLine::ClipContext& context, const SkRect& rect,
206 SkScalar baseline, SkScalar halo, const TextStyle& textStyle) {
207 // Create a special text blob for decorations
208 RSTextBlobBuilder builder;
209 context.run->copyTo(builder, SkToU32(context.pos), context.size);
210 auto blob = builder.Make();
211 if (!blob) {
212 // There is no text really
213 return;
214 }
215 SkScalar top = textStyle.getHeight() != 0 ? this->fDecorationContext.textBlobTop + baseline : rect.fTop;
216 // Since we do not shift down the text by {baseline}
217 // (it now happens on drawTextBlob but we do not draw text here)
218 // we have to shift up the bounds to compensate
219 // This baseline thing ends with getIntercepts
220 const SkScalar bounds[2] = {top - baseline, top + halo - baseline};
221 RSDrawing::Paint paint = ConvertDecorStyle(fDecorStyle);
222 auto count = blob->GetIntercepts(bounds, nullptr, &paint);
223 SkTArray<SkScalar> intersections(count);
224 intersections.resize(count);
225 blob->GetIntercepts(bounds, intersections.data(), &paint);
226
227 RSPath path;
228 auto start = rect.fLeft;
229 path.MoveTo(rect.fLeft, rect.fTop);
230 for (size_t i = 0; i < intersections.size(); i += 2) {
231 auto end = intersections[i] - halo;
232 if (end - start >= halo) {
233 start = intersections[i + 1] + halo;
234 path.LineTo(end, rect.fTop);
235 path.MoveTo(start, rect.fTop);
236 } else {
237 start = intersections[i + 1] + halo;
238 path.MoveTo(start, rect.fTop);
239 }
240 }
241 if (!intersections.empty() && (rect.fRight - start > halo)) {
242 path.LineTo(rect.fRight, rect.fTop);
243 }
244
245 if (intersections.empty()) {
246 path.LineTo(rect.fRight, rect.fTop);
247 }
248 fPath = path;
249 }
250
calculateAvoidanceWaves(const TextStyle & textStyle,SkRect clip)251 void Decorations::calculateAvoidanceWaves(const TextStyle& textStyle, SkRect clip) {
252 fPath.Reset();
253 int wave_count = 0;
254 const int step = 2;
255 const float zer = 0.01;
256 SkScalar x_start = 0;
257 SkScalar quarterWave = fThickness;
258 if (quarterWave <= zer) {
259 return;
260 }
261 fPath.MoveTo(0, 0);
262 while (x_start + quarterWave * step < clip.width()) {
263 fPath.RQuadTo(quarterWave,
264 wave_count % step != 0 ? quarterWave : -quarterWave,
265 quarterWave * step, 0);
266 x_start += quarterWave * step;
267 ++wave_count;
268 }
269
270 // The rest of the wave
271 auto remaining = clip.width() - x_start;
272 if (remaining > 0) {
273 double x1 = remaining / step;
274 double y1 = remaining / step * (wave_count % step == 0 ? -1 : 1);
275 double x2 = remaining;
276 double y2 = (remaining - remaining * remaining / (quarterWave * step)) *
277 (wave_count % step == 0 ? -1 : 1);
278 fPath.RQuadTo(x1, y1, x2, y2);
279 }
280 }
281
282 // This is how flutter calculates the thickness
283 #ifndef USE_SKIA_TXT
calculateThickness(TextStyle textStyle,sk_sp<SkTypeface> typeface)284 void Decorations::calculateThickness(TextStyle textStyle, sk_sp<SkTypeface> typeface) {
285 #else
286 void Decorations::calculateThickness(TextStyle textStyle, std::shared_ptr<RSTypeface> typeface) {
287 #endif
288
289 textStyle.setTypeface(typeface);
290 textStyle.getFontMetrics(&fFontMetrics);
291 if (textStyle.getDecoration().fType == TextDecoration::kUnderline &&
292 !SkScalarNearlyZero(fThickness)) {
293 return;
294 }
295
296 #ifdef OHOS_SUPPORT
297 fThickness = textStyle.getFontSize() * UNDER_LINE_THICKNESS_RATIO;
298 #else
299 fThickness = textStyle.getFontSize() / 14.0f;
300
301 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlineThicknessIsValid_Flag) &&
302 fFontMetrics.fUnderlineThickness > 0) {
303 fThickness = fFontMetrics.fUnderlineThickness;
304 }
305 if (textStyle.getDecorationType() == TextDecoration::kLineThrough) {
306 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag) &&
307 fFontMetrics.fStrikeoutThickness > 0) {
308 fThickness = fFontMetrics.fStrikeoutThickness;
309 }
310 }
311 #endif
312 fThickness *= textStyle.getDecorationThicknessMultiplier();
313 }
314
315 // This is how flutter calculates the positioning
316 #ifdef OHOS_SUPPORT
317 void Decorations::calculatePosition(TextDecoration decoration, SkScalar ascent,
318 const TextStyle& textStyle, SkScalar textBaselineShift, const Run& run) {
319 switch (decoration) {
320 case TextDecoration::kUnderline:
321 fPosition = fDecorationContext.underlinePosition + run.getVerticalAlignShift();
322 break;
323 case TextDecoration::kOverline:
324 fPosition = (textStyle.getDecorationStyle() == TextDecorationStyle::kWavy ?
325 fThickness : fThickness / 2.0f) - ascent;
326 break;
327 case TextDecoration::kLineThrough:
328 fPosition = LINE_THROUGH_TOP * textStyle.getCorrectFontSize() - ascent + textBaselineShift;
329 break;
330 default:
331 break;
332 }
333 }
334 #else
335 void Decorations::calculatePosition(TextDecoration decoration, SkScalar ascent) {
336 switch (decoration) {
337 case TextDecoration::kUnderline:
338 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlinePositionIsValid_Flag) &&
339 fFontMetrics.fUnderlinePosition > 0) {
340 fPosition = fFontMetrics.fUnderlinePosition;
341 } else {
342 fPosition = fThickness;
343 }
344 fPosition -= ascent;
345 break;
346 case TextDecoration::kOverline:
347 fPosition = - ascent;
348 break;
349 case TextDecoration::kLineThrough: {
350 fPosition = (fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutPositionIsValid_Flag)
351 ? fFontMetrics.fStrikeoutPosition
352 : fFontMetrics.fXHeight / -2;
353 fPosition -= ascent;
354 break;
355 }
356 default:SkASSERT(false);
357 break;
358 }
359 }
360 #endif
361
362 void Decorations::calculatePaint(const TextStyle& textStyle) {
363 std::optional<ParagraphPainter::DashPathEffect> dashPathEffect;
364 SkScalar scaleFactor = textStyle.getFontSize() / 14.f;
365 switch (textStyle.getDecorationStyle()) {
366 // Note: the intervals are scaled by the thickness of the line, so it is
367 // possible to change spacing by changing the decoration_thickness
368 // property of TextStyle.
369 case TextDecorationStyle::kDotted: {
370 dashPathEffect.emplace(1.0f * scaleFactor, 1.5f * scaleFactor);
371 break;
372 }
373 // Note: the intervals are scaled by the thickness of the line, so it is
374 // possible to change spacing by changing the decoration_thickness
375 // property of TextStyle.
376 case TextDecorationStyle::kDashed: {
377 dashPathEffect.emplace(4.0f * scaleFactor, 2.0f * scaleFactor);
378 break;
379 }
380 default: break;
381 }
382
383 SkColor color = (textStyle.getDecorationColor() == SK_ColorTRANSPARENT)
384 ? textStyle.getColor()
385 : textStyle.getDecorationColor();
386
387 fDecorStyle = ParagraphPainter::DecorationStyle(color, fThickness, dashPathEffect);
388 }
389
390 void Decorations::calculateWaves(const TextStyle& textStyle, SkRect clip) {
391 #ifdef OHOS_SUPPORT
392 if (SkScalarNearlyZero(fThickness) || fThickness < 0) {
393 return;
394 }
395 #endif
396 #ifndef USE_SKIA_TXT
397 fPath.reset();
398 #else
399 fPath.Reset();
400 #endif
401 int wave_count = 0;
402 SkScalar x_start = 0;
403 SkScalar quarterWave = fThickness;
404 #ifndef USE_SKIA_TXT
405 fPath.moveTo(0, 0);
406 #else
407 fPath.MoveTo(0, 0);
408 #endif
409
410 while (x_start + quarterWave * 2 < clip.width()) {
411 #ifndef USE_SKIA_TXT
412 fPath.rQuadTo(quarterWave,
413 wave_count % 2 != 0 ? quarterWave : -quarterWave,
414 quarterWave * 2,
415 0);
416 #else
417 fPath.RQuadTo(quarterWave,
418 wave_count % 2 != 0 ? quarterWave : -quarterWave,
419 quarterWave * 2,
420 0);
421 #endif
422 x_start += quarterWave * 2;
423 ++wave_count;
424 }
425
426 // The rest of the wave
427 auto remaining = clip.width() - x_start;
428 if (remaining > 0) {
429 double x1 = remaining / 2;
430 double y1 = remaining / 2 * (wave_count % 2 == 0 ? -1 : 1);
431 double x2 = remaining;
432 double y2 = (remaining - remaining * remaining / (quarterWave * 2)) *
433 (wave_count % 2 == 0 ? -1 : 1);
434 #ifndef USE_SKIA_TXT
435 fPath.rQuadTo(x1, y1, x2, y2);
436 #else
437 fPath.RQuadTo(x1, y1, x2, y2);
438 #endif
439 }
440 }
441
442 } // namespace textlayout
443 } // namespace skia
444