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