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 | |
6 | static 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 | |
18 | namespace skia { |
19 | namespace textlayout { |
20 | |
21 | static const float kDoubleDecorationSpacing = 3.0f; |
22 | void 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 | |
94 | void 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 |
130 | void 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 |
152 | void 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 | |
178 | void 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 | |
221 | void 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 | |