• 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 #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