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