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