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
paint(ParagraphPainter * painter,const TextStyle & textStyle,const TextLine::ClipContext & context,SkScalar baseline)32 void Decorations::paint(ParagraphPainter* painter, const TextStyle& textStyle, const TextLine::ClipContext& context, SkScalar baseline) {
33 if (textStyle.getDecorationType() == TextDecoration::kNoDecoration) {
34 return;
35 }
36
37 // Get thickness and position
38 #ifndef USE_SKIA_TXT
39 calculateThickness(textStyle, context.run->font().refTypeface());
40 #else
41 calculateThickness(textStyle, const_cast<RSFont&>(context.run->font()).GetTypeface());
42 #endif
43
44 for (auto decoration : AllTextDecorations) {
45 if ((textStyle.getDecorationType() & decoration) == 0) {
46 continue;
47 }
48
49 calculatePosition(decoration,
50 decoration == TextDecoration::kOverline
51 ? context.run->correctAscent() - context.run->ascent()
52 : context.run->correctAscent(), textStyle.getDecorationStyle(),
53 textStyle.getBaselineShift());
54
55 calculatePaint(textStyle);
56
57 auto width = context.clip.width();
58 SkScalar x = context.clip.left();
59 SkScalar y = (TextDecoration::kUnderline == decoration) ?
60 fPosition : (context.clip.top() + fPosition);
61
62 bool drawGaps = textStyle.getDecorationMode() == TextDecorationMode::kGaps &&
63 textStyle.getDecorationType() == TextDecoration::kUnderline;
64
65 switch (textStyle.getDecorationStyle()) {
66 case TextDecorationStyle::kWavy: {
67 if (drawGaps) {
68 calculateAvoidanceWaves(textStyle, context.clip);
69 fPath.Offset(x, y);
70 painter->drawPath(fPath, fDecorStyle);
71 break;
72 }
73 calculateWaves(textStyle, context.clip);
74 #ifndef USE_SKIA_TXT
75 fPath.offset(x, y);
76 #else
77 fPath.Offset(x, y);
78 #endif
79 painter->drawPath(fPath, fDecorStyle);
80 break;
81 }
82 case TextDecorationStyle::kDouble: {
83 SkScalar bottom = y + kDoubleDecorationSpacing * fThickness / 2.0;
84 if (drawGaps) {
85 SkScalar left = x - context.fTextShift;
86 painter->translate(context.fTextShift, 0);
87 calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness, textStyle);
88 painter->drawPath(fPath, fDecorStyle);
89 calculateGaps(context, SkRect::MakeXYWH(left, bottom, width, fThickness), baseline,
90 fThickness, textStyle);
91 painter->drawPath(fPath, fDecorStyle);
92 } else {
93 draw_line_as_rect(painter, x, y, width, fDecorStyle);
94 draw_line_as_rect(painter, x, bottom, width, fDecorStyle);
95 }
96 break;
97 }
98 case TextDecorationStyle::kDashed:
99 case TextDecorationStyle::kDotted:
100 if (drawGaps) {
101 SkScalar left = x - context.fTextShift;
102 painter->translate(context.fTextShift, 0);
103 calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness, textStyle);
104 painter->drawPath(fPath, fDecorStyle);
105 } else {
106 painter->drawLine(x, y, x + width, y, fDecorStyle);
107 }
108 break;
109 case TextDecorationStyle::kSolid:
110 if (drawGaps) {
111 SkScalar left = x - context.fTextShift;
112 painter->translate(context.fTextShift, 0);
113 SkRect rect = SkRect::MakeXYWH(left, y, width, fThickness);
114 calculateGaps(context, rect, baseline, fThickness, textStyle);
115 painter->drawPath(fPath, fDecorStyle);
116 } else {
117 draw_line_as_rect(painter, x, y, width, fDecorStyle);
118 }
119 break;
120 default:break;
121 }
122 }
123 }
124
ConvertDrawingStyle(SkPaint::Style skStyle)125 static RSDrawing::Paint::PaintStyle ConvertDrawingStyle(SkPaint::Style skStyle)
126 {
127 if (PAINT_STYLE.find(skStyle) != PAINT_STYLE.end()) {
128 return PAINT_STYLE.at(skStyle);
129 } else {
130 return RSDrawing::Paint::PaintStyle::PAINT_NONE;
131 }
132 }
133
ConvertDecorStyle(const ParagraphPainter::DecorationStyle & decorStyle)134 static RSDrawing::Paint ConvertDecorStyle(const ParagraphPainter::DecorationStyle& decorStyle)
135 {
136 const SkPaint& decorPaint = decorStyle.skPaint();
137 RSDrawing::Paint paint;
138 paint.SetStyle(ConvertDrawingStyle(decorPaint.getStyle()));
139 paint.SetAntiAlias(decorPaint.isAntiAlias());
140 paint.SetColor(decorPaint.getColor());
141 paint.SetWidth(decorPaint.getStrokeWidth());
142 if (decorStyle.getDashPathEffect().has_value()) {
143 auto dashPathEffect = decorStyle.getDashPathEffect().value();
144 RSDrawing::scalar intervals[] = {dashPathEffect.fOnLength, dashPathEffect.fOffLength,
145 dashPathEffect.fOnLength, dashPathEffect.fOffLength};
146 size_t count = sizeof(intervals) / sizeof(intervals[0]);
147 auto pathEffect1 = RSDrawing::PathEffect::CreateDashPathEffect(intervals, count, 0.0f);
148 auto pathEffect2 = RSDrawing::PathEffect::CreateDiscretePathEffect(0, 0);
149 auto pathEffect = RSDrawing::PathEffect::CreateComposePathEffect(*pathEffect1.get(), *pathEffect2.get());
150 paint.SetPathEffect(pathEffect);
151 }
152 return paint;
153 }
154
calculateGaps(const TextLine::ClipContext & context,const SkRect & rect,SkScalar baseline,SkScalar halo,const TextStyle & textStyle)155 void Decorations::calculateGaps(const TextLine::ClipContext& context, const SkRect& rect,
156 SkScalar baseline, SkScalar halo, const TextStyle& textStyle) {
157 // Create a special text blob for decorations
158 RSTextBlobBuilder builder;
159 context.run->copyTo(builder, SkToU32(context.pos), context.size);
160 auto blob = builder.Make();
161 if (!blob) {
162 // There is no text really
163 return;
164 }
165 SkScalar top = textStyle.getHeight() != 0 ? this->fDecorationContext.textBlobTop + baseline : rect.fTop;
166 // Since we do not shift down the text by {baseline}
167 // (it now happens on drawTextBlob but we do not draw text here)
168 // we have to shift up the bounds to compensate
169 // This baseline thing ends with getIntercepts
170 const SkScalar bounds[2] = {top - baseline, top + halo - baseline};
171 RSDrawing::Paint paint = ConvertDecorStyle(fDecorStyle);
172 auto count = blob->GetIntercepts(bounds, nullptr, &paint);
173 SkTArray<SkScalar> intersections(count);
174 intersections.resize(count);
175 blob->GetIntercepts(bounds, intersections.data(), &paint);
176
177 RSPath path;
178 auto start = rect.fLeft;
179 path.MoveTo(rect.fLeft, rect.fTop);
180 for (size_t i = 0; i < intersections.size(); i += 2) {
181 auto end = intersections[i] - halo;
182 if (end - start >= halo) {
183 start = intersections[i + 1] + halo;
184 path.LineTo(end, rect.fTop);
185 path.MoveTo(start, rect.fTop);
186 } else {
187 start = intersections[i + 1] + halo;
188 path.MoveTo(start, rect.fTop);
189 }
190 }
191 if (!intersections.empty() && (rect.fRight - start > halo)) {
192 path.LineTo(rect.fRight, rect.fTop);
193 }
194
195 if (intersections.empty()) {
196 path.LineTo(rect.fRight, rect.fTop);
197 }
198 fPath = path;
199 }
200
calculateAvoidanceWaves(const TextStyle & textStyle,SkRect clip)201 void Decorations::calculateAvoidanceWaves(const TextStyle& textStyle, SkRect clip) {
202 fPath.Reset();
203 int wave_count = 0;
204 const int step = 2;
205 const float zer = 0.01;
206 SkScalar x_start = 0;
207 SkScalar quarterWave = fThickness;
208 if (quarterWave <= zer) {
209 return;
210 }
211 fPath.MoveTo(0, 0);
212 while (x_start + quarterWave * step < clip.width()) {
213 fPath.RQuadTo(quarterWave,
214 wave_count % step != 0 ? quarterWave : -quarterWave,
215 quarterWave * step, 0);
216 x_start += quarterWave * step;
217 ++wave_count;
218 }
219
220 // The rest of the wave
221 auto remaining = clip.width() - x_start;
222 if (remaining > 0) {
223 double x1 = remaining / step;
224 double y1 = remaining / step * (wave_count % step == 0 ? -1 : 1);
225 double x2 = remaining;
226 double y2 = (remaining - remaining * remaining / (quarterWave * step)) *
227 (wave_count % step == 0 ? -1 : 1);
228 fPath.RQuadTo(x1, y1, x2, y2);
229 }
230 }
231
232 // This is how flutter calculates the thickness
233 #ifndef USE_SKIA_TXT
calculateThickness(TextStyle textStyle,sk_sp<SkTypeface> typeface)234 void Decorations::calculateThickness(TextStyle textStyle, sk_sp<SkTypeface> typeface) {
235 #else
236 void Decorations::calculateThickness(TextStyle textStyle, std::shared_ptr<RSTypeface> typeface) {
237 #endif
238
239 textStyle.setTypeface(typeface);
240 textStyle.getFontMetrics(&fFontMetrics);
241 if (textStyle.getDecoration().fType == TextDecoration::kUnderline &&
242 !SkScalarNearlyZero(fThickness)) {
243 return;
244 }
245
246 fThickness = textStyle.getFontSize() / 14.0f;
247
248 #ifndef USE_SKIA_TXT
249 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlineThicknessIsValid_Flag) &&
250 #else
251 if ((fFontMetrics.fFlags & RSFontMetrics::FontMetricsFlags::UNDERLINE_THICKNESS_IS_VALID_FLAG) &&
252 #endif
253 fFontMetrics.fUnderlineThickness > 0) {
254 fThickness = fFontMetrics.fUnderlineThickness;
255 }
256
257 if (textStyle.getDecorationType() == TextDecoration::kLineThrough) {
258 #ifndef USE_SKIA_TXT
259 if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag) &&
260 #else
261 if ((fFontMetrics.fFlags & RSFontMetrics::FontMetricsFlags::STRIKEOUT_THICKNESS_IS_VALID_FLAG) &&
262 #endif
263 fFontMetrics.fStrikeoutThickness > 0) {
264 fThickness = fFontMetrics.fStrikeoutThickness;
265 }
266 }
267 fThickness *= textStyle.getDecorationThicknessMultiplier();
268 }
269
270 // This is how flutter calculates the positioning
271 void Decorations::calculatePosition(TextDecoration decoration, SkScalar ascent,
272 const TextDecorationStyle textDecorationStyle, SkScalar textBaselineShift) {
273 switch (decoration) {
274 case TextDecoration::kUnderline:
275 fPosition = fDecorationContext.underlinePosition;
276 break;
277 case TextDecoration::kOverline:
278 fPosition = (textDecorationStyle == TextDecorationStyle::kWavy ? fThickness : fThickness / 2.0f) - ascent;
279 break;
280 case TextDecoration::kLineThrough: {
281 #ifndef USE_SKIA_TXT
282 fPosition = (fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutPositionIsValid_Flag)
283 #else
284 fPosition = (fFontMetrics.fFlags & RSFontMetrics::FontMetricsFlags::STRIKEOUT_POSITION_IS_VALID_FLAG)
285 #endif
286 ? fFontMetrics.fStrikeoutPosition
287 : fFontMetrics.fXHeight / -2;
288 fPosition -= ascent;
289 fPosition += textBaselineShift;
290 break;
291 }
292 default:SkASSERT(false);
293 break;
294 }
295 }
296
297 void Decorations::calculatePaint(const TextStyle& textStyle) {
298 std::optional<ParagraphPainter::DashPathEffect> dashPathEffect;
299 SkScalar scaleFactor = textStyle.getFontSize() / 14.f;
300 switch (textStyle.getDecorationStyle()) {
301 // Note: the intervals are scaled by the thickness of the line, so it is
302 // possible to change spacing by changing the decoration_thickness
303 // property of TextStyle.
304 case TextDecorationStyle::kDotted: {
305 dashPathEffect.emplace(1.0f * scaleFactor, 1.5f * scaleFactor);
306 break;
307 }
308 // Note: the intervals are scaled by the thickness of the line, so it is
309 // possible to change spacing by changing the decoration_thickness
310 // property of TextStyle.
311 case TextDecorationStyle::kDashed: {
312 dashPathEffect.emplace(4.0f * scaleFactor, 2.0f * scaleFactor);
313 break;
314 }
315 default: break;
316 }
317
318 SkColor color = (textStyle.getDecorationColor() == SK_ColorTRANSPARENT)
319 ? textStyle.getColor()
320 : textStyle.getDecorationColor();
321
322 fDecorStyle = ParagraphPainter::DecorationStyle(color, fThickness, dashPathEffect);
323 }
324
325 void Decorations::calculateWaves(const TextStyle& textStyle, SkRect clip) {
326
327 #ifndef USE_SKIA_TXT
328 fPath.reset();
329 #else
330 fPath.Reset();
331 #endif
332 int wave_count = 0;
333 SkScalar x_start = 0;
334 SkScalar quarterWave = fThickness;
335 #ifndef USE_SKIA_TXT
336 fPath.moveTo(0, 0);
337 #else
338 fPath.MoveTo(0, 0);
339 #endif
340
341 while (x_start + quarterWave * 2 < clip.width()) {
342 #ifndef USE_SKIA_TXT
343 fPath.rQuadTo(quarterWave,
344 wave_count % 2 != 0 ? quarterWave : -quarterWave,
345 quarterWave * 2,
346 0);
347 #else
348 fPath.RQuadTo(quarterWave,
349 wave_count % 2 != 0 ? quarterWave : -quarterWave,
350 quarterWave * 2,
351 0);
352 #endif
353 x_start += quarterWave * 2;
354 ++wave_count;
355 }
356
357 // The rest of the wave
358 auto remaining = clip.width() - x_start;
359 if (remaining > 0) {
360 double x1 = remaining / 2;
361 double y1 = remaining / 2 * (wave_count % 2 == 0 ? -1 : 1);
362 double x2 = remaining;
363 double y2 = (remaining - remaining * remaining / (quarterWave * 2)) *
364 (wave_count % 2 == 0 ? -1 : 1);
365 #ifndef USE_SKIA_TXT
366 fPath.rQuadTo(x1, y1, x2, y2);
367 #else
368 fPath.RQuadTo(x1, y1, x2, y2);
369 #endif
370 }
371 }
372
373 } // namespace textlayout
374 } // namespace skia
375