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