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