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