• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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