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