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