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