1// Copyright 2020 Google LLC.
2#include "include/effects/SkDashPathEffect.h"
3#include "include/effects/SkDiscretePathEffect.h"
4#include "modules/skparagraph/src/Decorations.h"
5
6static void draw_line_as_rect(SkCanvas* canvas, SkScalar x, SkScalar y, SkScalar width,
7 const SkPaint& paint) {
8 SkASSERT(paint.getPathEffect() == nullptr);
9 SkASSERT(paint.getStrokeCap() == SkPaint::kButt_Cap);
10 SkASSERT(paint.getStrokeWidth() > 0); // this trick won't work for hairlines
11
12 SkPaint p(paint);
13 p.setStroke(false);
14 float radius = paint.getStrokeWidth() * 0.5f;
15 canvas->drawRect({x, y - radius, x + width, y + radius}, p);
16}
17
18namespace skia {
19namespace textlayout {
20
21static const float kDoubleDecorationSpacing = 3.0f;
22void Decorations::paint(SkCanvas* canvas, const TextStyle& textStyle, const TextLine::ClipContext& context, SkScalar baseline, SkPoint offset) {
23 if (textStyle.getDecorationType() == TextDecoration::kNoDecoration) {
24 return;
25 }
26
27 // Get thickness and position
28 calculateThickness(textStyle, context.run->font().refTypeface());
29
30 for (auto decoration : AllTextDecorations) {
31 if ((textStyle.getDecorationType() & decoration) == 0) {
32 continue;
33 }
34
35 calculatePosition(decoration, context.run->correctAscent());
36
37 calculatePaint(textStyle);
38
39 auto width = context.clip.width();
40 SkScalar x = context.clip.left();
41 SkScalar y = context.clip.top() + fPosition;
42
43 bool drawGaps = textStyle.getDecorationMode() == TextDecorationMode::kGaps &&
44 textStyle.getDecorationType() == TextDecoration::kUnderline;
45
46 switch (textStyle.getDecorationStyle()) {
47 case TextDecorationStyle::kWavy: {
48 calculateWaves(textStyle, context.clip);
49 fPath.offset(x, y);
50 canvas->drawPath(fPath, fPaint);
51 break;
52 }
53 case TextDecorationStyle::kDouble: {
54 SkScalar bottom = y + kDoubleDecorationSpacing;
55 if (drawGaps) {
56 SkScalar left = x - context.fTextShift;
57 canvas->translate(context.fTextShift, 0);
58 calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness);
59 canvas->drawPath(fPath, fPaint);
60 calculateGaps(context, SkRect::MakeXYWH(left, bottom, width, fThickness), baseline, fThickness);
61 canvas->drawPath(fPath, fPaint);
62 } else {
63 draw_line_as_rect(canvas, x, y, width, fPaint);
64 draw_line_as_rect(canvas, x, bottom, width, fPaint);
65 }
66 break;
67 }
68 case TextDecorationStyle::kDashed:
69 case TextDecorationStyle::kDotted:
70 if (drawGaps) {
71 SkScalar left = x - context.fTextShift;
72 canvas->translate(context.fTextShift, 0);
73 calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, 0);
74 canvas->drawPath(fPath, fPaint);
75 } else {
76 canvas->drawLine(x, y, x + width, y, fPaint);
77 }
78 break;
79 case TextDecorationStyle::kSolid:
80 if (drawGaps) {
81 SkScalar left = x - context.fTextShift;
82 canvas->translate(context.fTextShift, 0);
83 calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness);
84 canvas->drawPath(fPath, fPaint);
85 } else {
86 draw_line_as_rect(canvas, x, y, width, fPaint);
87 }
88 break;
89 default:break;
90 }
91 }
92}
93
94void Decorations::calculateGaps(const TextLine::ClipContext& context, const SkRect& rect, SkScalar baseline, SkScalar halo) {
95
96 fPath.reset();
97
98 // Create a special textblob for decorations
99 SkTextBlobBuilder builder;
100 context.run->copyTo(builder,
101 SkToU32(context.pos),
102 context.size);
103 auto blob = builder.make();
104
105 // Since we do not shift down the text by {baseline}
106 // (it now happens on drawTextBlob but we do not draw text here)
107 // we have to shift up the bounds to compensate
108 // This baseline thing ends with getIntercepts
109 const SkScalar bounds[2] = {rect.fTop - baseline, rect.fBottom - baseline};
110 auto count = blob->getIntercepts(bounds, nullptr, &fPaint);
111 SkTArray<SkScalar> intersections(count);
112 intersections.resize(count);
113 blob->getIntercepts(bounds, intersections.data(), &fPaint);
114
115 auto start = rect.fLeft;
116 fPath.moveTo(rect.fLeft, rect.fTop);
117 for (int i = 0; i < intersections.count(); i += 2) {
118 auto end = intersections[i] - halo;
119 if (end - start >= halo) {
120 start = intersections[i + 1] + halo;
121 fPath.lineTo(end, rect.fTop).moveTo(start, rect.fTop);
122 }
123 }
124 if (!intersections.empty() && (rect.fRight - start > halo)) {
125 fPath.lineTo(rect.fRight, rect.fTop);
126 }
127}
128
129// This is how flutter calculates the thickness
130void Decorations::calculateThickness(TextStyle textStyle, sk_sp<SkTypeface> typeface) {
131
132 textStyle.setTypeface(typeface);
133 textStyle.getFontMetrics(&fFontMetrics);
134
135 fThickness = textStyle.getFontSize() / 14.0f;
136
137 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlineThicknessIsValid_Flag) &&
138 fFontMetrics.fUnderlineThickness > 0) {
139 fThickness = fFontMetrics.fUnderlineThickness;
140 }
141
142 if (textStyle.getDecorationType() == TextDecoration::kLineThrough) {
143 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag) &&
144 fFontMetrics.fStrikeoutThickness > 0) {
145 fThickness = fFontMetrics.fStrikeoutThickness;
146 }
147 }
148 fThickness *= textStyle.getDecorationThicknessMultiplier();
149}
150
151// This is how flutter calculates the positioning
152void Decorations::calculatePosition(TextDecoration decoration, SkScalar ascent) {
153 switch (decoration) {
154 case TextDecoration::kUnderline:
155 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlinePositionIsValid_Flag) &&
156 fFontMetrics.fUnderlinePosition > 0) {
157 fPosition = fFontMetrics.fUnderlinePosition;
158 } else {
159 fPosition = fThickness;
160 }
161 fPosition -= ascent;
162 break;
163 case TextDecoration::kOverline:
164 fPosition = 0;
165 break;
166 case TextDecoration::kLineThrough: {
167 fPosition = (fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag)
168 ? fFontMetrics.fStrikeoutPosition
169 : fFontMetrics.fXHeight / -2;
170 fPosition -= ascent;
171 break;
172 }
173 default:SkASSERT(false);
174 break;
175 }
176}
177
178void Decorations::calculatePaint(const TextStyle& textStyle) {
179
180 fPaint.reset();
181
182 fPaint.setStyle(SkPaint::kStroke_Style);
183 if (textStyle.getDecorationColor() == SK_ColorTRANSPARENT) {
184 fPaint.setColor(textStyle.getColor());
185 } else {
186 fPaint.setColor(textStyle.getDecorationColor());
187 }
188 fPaint.setAntiAlias(true);
189 fPaint.setStrokeWidth(fThickness);
190
191 SkScalar scaleFactor = textStyle.getFontSize() / 14.f;
192 switch (textStyle.getDecorationStyle()) {
193 // Note: the intervals are scaled by the thickness of the line, so it is
194 // possible to change spacing by changing the decoration_thickness
195 // property of TextStyle.
196 case TextDecorationStyle::kDotted: {
197 const SkScalar intervals[] = {1.0f * scaleFactor, 1.5f * scaleFactor,
198 1.0f * scaleFactor, 1.5f * scaleFactor};
199 size_t count = sizeof(intervals) / sizeof(intervals[0]);
200 fPaint.setPathEffect(SkPathEffect::MakeCompose(
201 SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
202 SkDiscretePathEffect::Make(0, 0)));
203 break;
204 }
205 // Note: the intervals are scaled by the thickness of the line, so it is
206 // possible to change spacing by changing the decoration_thickness
207 // property of TextStyle.
208 case TextDecorationStyle::kDashed: {
209 const SkScalar intervals[] = {4.0f * scaleFactor, 2.0f * scaleFactor,
210 4.0f * scaleFactor, 2.0f * scaleFactor};
211 size_t count = sizeof(intervals) / sizeof(intervals[0]);
212 fPaint.setPathEffect(SkPathEffect::MakeCompose(
213 SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
214 SkDiscretePathEffect::Make(0, 0)));
215 break;
216 }
217 default: break;
218 }
219}
220
221void Decorations::calculateWaves(const TextStyle& textStyle, SkRect clip) {
222
223 fPath.reset();
224 int wave_count = 0;
225 SkScalar x_start = 0;
226 SkScalar quarterWave = fThickness;
227 fPath.moveTo(0, 0);
228 while (x_start + quarterWave * 2 < clip.width()) {
229 fPath.rQuadTo(quarterWave,
230 wave_count % 2 != 0 ? quarterWave : -quarterWave,
231 quarterWave * 2,
232 0);
233 x_start += quarterWave * 2;
234 ++wave_count;
235 }
236
237 // The rest of the wave
238 auto remaining = clip.width() - x_start;
239 if (remaining > 0) {
240 double x1 = remaining / 2;
241 double y1 = remaining / 2 * (wave_count % 2 == 0 ? -1 : 1);
242 double x2 = remaining;
243 double y2 = (remaining - remaining * remaining / (quarterWave * 2)) *
244 (wave_count % 2 == 0 ? -1 : 1);
245 fPath.rQuadTo(x1, y1, x2, y2);
246 }
247}
248
249} // namespace textlayout
250} // namespace skia
251