Skips in underline decorations

Bug: skia:10166
Change-Id: Ib1d71f69d8647840f71c8599ca3517cb575bf945
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/287620
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
This commit is contained in:
Julia Lavrova 2020-05-04 15:03:18 -04:00 committed by Skia Commit-Bot
parent 041232a789
commit 18db52f2ee
9 changed files with 350 additions and 170 deletions

View File

@ -50,6 +50,8 @@ constexpr TextDecoration AllTextDecorations[] = {
enum TextDecorationStyle { kSolid, kDouble, kDotted, kDashed, kWavy };
enum TextDecorationMode { kGaps, kThrough };
enum StyleType {
kNone,
kAllAttributes,
@ -64,12 +66,14 @@ enum StyleType {
struct Decoration {
TextDecoration fType;
TextDecorationMode fMode;
SkColor fColor;
TextDecorationStyle fStyle;
SkScalar fThicknessMultiplier;
bool operator==(const Decoration& other) const {
return this->fType == other.fType &&
this->fMode == other.fMode &&
this->fColor == other.fColor &&
this->fStyle == other.fStyle &&
this->fThicknessMultiplier == other.fThicknessMultiplier;
@ -181,12 +185,14 @@ public:
// Decorations
Decoration getDecoration() const { return fDecoration; }
TextDecoration getDecorationType() const { return fDecoration.fType; }
TextDecorationMode getDecorationMode() const { return fDecoration.fMode; }
SkColor getDecorationColor() const { return fDecoration.fColor; }
TextDecorationStyle getDecorationStyle() const { return fDecoration.fStyle; }
SkScalar getDecorationThicknessMultiplier() const {
return fDecoration.fThicknessMultiplier;
}
void setDecoration(TextDecoration decoration) { fDecoration.fType = decoration; }
void setDecorationMode(TextDecorationMode mode) { fDecoration.fMode = mode; }
void setDecorationStyle(TextDecorationStyle style) { fDecoration.fStyle = style; }
void setDecorationColor(SkColor color) { fDecoration.fColor = color; }
void setDecorationThicknessMultiplier(SkScalar m) { fDecoration.fThicknessMultiplier = m; }

View File

@ -19,6 +19,8 @@ skparagraph_public = [
]
skparagraph_sources = [
"$_src/Decorations.cpp",
"$_src/Decorations.h",
"$_src/FontCollection.cpp",
"$_src/Iterators.h",
"$_src/OneLineShaper.cpp",

View File

@ -0,0 +1,238 @@
// Copyright 2020 Google LLC.
#include "include/effects/SkDashPathEffect.h"
#include "include/effects/SkDiscretePathEffect.h"
#include "modules/skparagraph/src/Decorations.h"
namespace skia {
namespace textlayout {
static const float kDoubleDecorationSpacing = 3.0f;
void Decorations::paint(SkCanvas* canvas, const TextStyle& textStyle, const TextLine::ClipContext& context, SkScalar baseline, SkScalar shift) {
if (textStyle.getDecorationType() == TextDecoration::kNoDecoration) {
return;
}
// Get thickness and position
calculateThickness(textStyle, context.run->font().refTypeface());
for (auto decoration : AllTextDecorations) {
if ((textStyle.getDecorationType() & decoration) == 0) {
continue;
}
calculatePosition(decoration, context.run->correctAscent());
calculatePaint(textStyle);
auto width = context.clip.width();
SkScalar x = context.clip.left();
SkScalar y = context.clip.top() + fPosition;
bool drawGaps = textStyle.getDecorationMode() == TextDecorationMode::kGaps &&
textStyle.getDecorationType() == TextDecoration::kUnderline;
switch (textStyle.getDecorationStyle()) {
case TextDecorationStyle::kWavy: {
calculateWaves(textStyle, context.clip);
fPath.offset(x, y);
canvas->drawPath(fPath, fPaint);
break;
}
case TextDecorationStyle::kDouble: {
SkScalar bottom = y + kDoubleDecorationSpacing;
if (drawGaps) {
SkScalar left = x - context.fTextShift;
canvas->translate(context.fTextShift, 0);
calculateGaps(context, left, left + width, y, y + fThickness, baseline, fThickness);
canvas->drawPath(fPath, fPaint);
calculateGaps(context, left, left + width, bottom, bottom + fThickness, baseline, fThickness);
canvas->drawPath(fPath, fPaint);
} else {
canvas->drawLine(x, y, x + width, y, fPaint);
canvas->drawLine(x, bottom, x + width, bottom, fPaint);
}
break;
}
case TextDecorationStyle::kDashed:
case TextDecorationStyle::kDotted:
if (drawGaps) {
SkScalar left = x - context.fTextShift;
canvas->translate(context.fTextShift, 0);
calculateGaps(context, left, left + width, y, y + fThickness, baseline, 0);
canvas->drawPath(fPath, fPaint);
} else {
canvas->drawLine(x, y, x + width, y, fPaint);
}
break;
case TextDecorationStyle::kSolid:
if (drawGaps) {
SkScalar left = x - context.fTextShift;
canvas->translate(context.fTextShift, 0);
calculateGaps(context, left, left + width, y, y + fThickness, baseline, fThickness);
canvas->drawPath(fPath, fPaint);
} else {
canvas->drawLine(x, y, x + width, y, fPaint);
}
break;
default:break;
}
canvas->save();
canvas->restore();
}
}
void Decorations::calculateGaps(const TextLine::ClipContext& context, SkScalar x0, SkScalar x1, SkScalar y0, SkScalar y1, SkScalar baseline, SkScalar halo) {
fPath.reset();
// Create a special textblob for decorations
SkTextBlobBuilder builder;
context.run->copyTo(builder,
SkToU32(context.pos),
context.size,
SkVector::Make(0, baseline));
auto blob = builder.make();
const SkScalar bounds[2] = {y0, y1};
auto count = blob->getIntercepts(bounds, nullptr, &fPaint);
SkTArray<SkScalar> intersections(count);
intersections.resize(count);
blob->getIntercepts(bounds, intersections.data(), &fPaint);
auto start = x0;
fPath.moveTo({x0, y0});
for (int i = 0; i < intersections.count(); i += 2) {
auto end = intersections[i] - halo;
if (end - start >= halo) {
start = intersections[i + 1] + halo;
fPath.lineTo(end, y0).moveTo(start, y0);
}
}
if (!intersections.empty() && (x1 - start > halo)) {
fPath.lineTo(x1, y0);
}
}
// This is how flutter calculates the thickness
void Decorations::calculateThickness(TextStyle textStyle, sk_sp<SkTypeface> typeface) {
textStyle.setTypeface(typeface);
textStyle.getFontMetrics(&fFontMetrics);
fThickness = textStyle.getFontSize() / 14.0f;
if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlineThicknessIsValid_Flag) &&
fFontMetrics.fUnderlineThickness > 0) {
fThickness = fFontMetrics.fUnderlineThickness;
}
if (textStyle.getDecorationType() == TextDecoration::kLineThrough) {
if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag) &&
fFontMetrics.fStrikeoutThickness > 0) {
fThickness = fFontMetrics.fStrikeoutThickness;
}
}
fThickness *= textStyle.getDecorationThicknessMultiplier();
}
// This is how flutter calculates the positioning
void Decorations::calculatePosition(TextDecoration decoration, SkScalar ascent) {
switch (decoration) {
case TextDecoration::kUnderline:
if ((fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlinePositionIsValid_Flag) &&
fFontMetrics.fUnderlinePosition > 0) {
fPosition = fFontMetrics.fUnderlinePosition;
} else {
fPosition = fThickness;
}
fPosition -= ascent;
break;
case TextDecoration::kOverline:
fPosition = 0;
break;
case TextDecoration::kLineThrough: {
fPosition = (fFontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag)
? fFontMetrics.fStrikeoutPosition
: fFontMetrics.fXHeight / -2;
fPosition -= ascent;
break;
}
default:SkASSERT(false);
break;
}
}
void Decorations::calculatePaint(const TextStyle& textStyle) {
fPaint.reset();
fPaint.setStyle(SkPaint::kStroke_Style);
if (textStyle.getDecorationColor() == SK_ColorTRANSPARENT) {
fPaint.setColor(textStyle.getColor());
} else {
fPaint.setColor(textStyle.getDecorationColor());
}
fPaint.setAntiAlias(true);
fPaint.setStrokeWidth(fThickness);
SkScalar scaleFactor = textStyle.getFontSize() / 14.f;
switch (textStyle.getDecorationStyle()) {
// Note: the intervals are scaled by the thickness of the line, so it is
// possible to change spacing by changing the decoration_thickness
// property of TextStyle.
case TextDecorationStyle::kDotted: {
const SkScalar intervals[] = {1.0f * scaleFactor, 1.5f * scaleFactor,
1.0f * scaleFactor, 1.5f * scaleFactor};
size_t count = sizeof(intervals) / sizeof(intervals[0]);
fPaint.setPathEffect(SkPathEffect::MakeCompose(
SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
SkDiscretePathEffect::Make(0, 0)));
break;
}
// Note: the intervals are scaled by the thickness of the line, so it is
// possible to change spacing by changing the decoration_thickness
// property of TextStyle.
case TextDecorationStyle::kDashed: {
const SkScalar intervals[] = {4.0f * scaleFactor, 2.0f * scaleFactor,
4.0f * scaleFactor, 2.0f * scaleFactor};
size_t count = sizeof(intervals) / sizeof(intervals[0]);
fPaint.setPathEffect(SkPathEffect::MakeCompose(
SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
SkDiscretePathEffect::Make(0, 0)));
break;
}
default: break;
}
}
void Decorations::calculateWaves(const TextStyle& textStyle, SkRect clip) {
fPath.reset();
int wave_count = 0;
SkScalar x_start = 0;
SkScalar quarterWave = fThickness;
fPath.moveTo(0, 0);
while (x_start + quarterWave * 2 < clip.width()) {
fPath.rQuadTo(quarterWave,
wave_count % 2 != 0 ? quarterWave : -quarterWave,
quarterWave * 2,
0);
x_start += quarterWave * 2;
++wave_count;
}
// The rest of the wave
auto remaining = clip.width() - x_start;
if (remaining > 0) {
double x1 = remaining / 2;
double y1 = remaining / 2 * (wave_count % 2 == 0 ? -1 : 1);
double x2 = remaining;
double y2 = (remaining - remaining * remaining / (quarterWave * 2)) *
(wave_count % 2 == 0 ? -1 : 1);
fPath.rQuadTo(x1, y1, x2, y2);
}
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright 2020 Google LLC.
#ifndef Decorations_DEFINED
#define Decorations_DEFINED
#include "include/core/SkCanvas.h"
#include "include/core/SkPath.h"
#include "modules/skparagraph/include/TextStyle.h"
#include "modules/skparagraph/src/TextLine.h"
namespace skia {
namespace textlayout {
class Decorations {
public:
void paint(SkCanvas* canvas, const TextStyle& textStyle, const TextLine::ClipContext& context, SkScalar baseline, SkScalar shift);
private:
void calculateThickness(TextStyle textStyle, sk_sp<SkTypeface> typeface);
void calculatePosition(TextDecoration decoration, SkScalar ascent);
void calculatePaint(const TextStyle& textStyle);
void calculateWaves(const TextStyle& textStyle, SkRect clip);
void calculateGaps(const TextLine::ClipContext& context, SkScalar x0, SkScalar x1, SkScalar y0, SkScalar y1, SkScalar baseline, SkScalar halo);
SkScalar fThickness;
SkScalar fPosition;
SkFontMetrics fFontMetrics;
SkPaint fPaint;
SkPath fPath;
};
}
}
#endif

View File

@ -184,6 +184,7 @@ public:
void resetJustificationShifts() {
fJustificationShifts.reset();
}
private:
friend class ParagraphImpl;
friend class TextLine;
@ -212,7 +213,6 @@ private:
SkSTArray<128, SkPoint, true> fOffsets;
SkSTArray<128, uint32_t, true> fClusterIndexes;
SkSTArray<128, SkRect, true> fBounds;
SkSTArray<128, SkScalar, true> fShifts; // For formatting (letter/word spacing)
bool fSpaced;
};

View File

@ -2,6 +2,7 @@
#include "modules/skparagraph/src/TextLine.h"
#include <unicode/brkiter.h>
#include <unicode/ubidi.h>
#include "modules/skparagraph/src/Decorations.h"
#include "modules/skparagraph/src/ParagraphImpl.h"
#include "include/core/SkMaskFilter.h"
@ -388,170 +389,12 @@ void TextLine::paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyl
}
}
static const float kDoubleDecorationSpacing = 3.0f;
void TextLine::paintDecorations(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
if (style.getDecorationType() == TextDecoration::kNoDecoration) {
return;
}
canvas->save();
Decorations decorations;
SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + 0.5);
decorations.paint(canvas, style, context, correctedBaseline, this->fShift);
SkPaint paint;
paint.setStyle(SkPaint::kStroke_Style);
if (style.getDecorationColor() == SK_ColorTRANSPARENT) {
paint.setColor(style.getColor());
} else {
paint.setColor(style.getDecorationColor());
}
paint.setAntiAlias(true);
SkFontMetrics fontMetrics;
TextStyle combined = style;
combined.setTypeface(context.run->fFont.refTypeface());
combined.getFontMetrics(&fontMetrics);
SkScalar thickness;
if ((fontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlineThicknessIsValid_Flag) &&
fontMetrics.fUnderlineThickness > 0) {
thickness = fontMetrics.fUnderlineThickness;
} else {
thickness = style.getFontSize() / 14.0f;
}
paint.setStrokeWidth(thickness * style.getDecorationThicknessMultiplier());
for (auto decoration : AllTextDecorations) {
if ((style.getDecorationType() & decoration) == 0) {
continue;
}
SkScalar position = 0;
switch (decoration) {
case TextDecoration::kUnderline:
if ((fontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlinePositionIsValid_Flag) &&
fontMetrics.fUnderlinePosition > 0) {
position = fontMetrics.fUnderlinePosition;
} else {
position = thickness;
}
position += - context.run->correctAscent();
break;
case TextDecoration::kOverline:
position = 0;
break;
case TextDecoration::kLineThrough: {
if ((fontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag) &&
fontMetrics.fStrikeoutThickness > 0) {
paint.setStrokeWidth(fontMetrics.fStrikeoutThickness * style.getDecorationThicknessMultiplier());
}
position = (fontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag)
? fontMetrics.fStrikeoutPosition
: fontMetrics.fXHeight / -2;
position += - context.run->correctAscent();
break;
}
default:
SkASSERT(false);
break;
}
auto width = context.clip.width();
SkScalar x = context.clip.left();
SkScalar y = context.clip.top() + position;
// Decoration paint (for now) and/or path
SkPath path;
this->computeDecorationPaint(paint, context.clip, style, thickness, path);
switch (style.getDecorationStyle()) {
case TextDecorationStyle::kWavy:
path.offset(x, y);
canvas->drawPath(path, paint);
break;
case TextDecorationStyle::kDouble: {
canvas->drawLine(x, y, x + width, y, paint);
SkScalar bottom = y + kDoubleDecorationSpacing;
canvas->drawLine(x, bottom, x + width, bottom, paint);
break;
}
case TextDecorationStyle::kDashed:
case TextDecorationStyle::kDotted:
case TextDecorationStyle::kSolid:
canvas->drawLine(x, y, x + width, y, paint);
break;
default:
break;
}
}
canvas->restore();
}
void TextLine::computeDecorationPaint(SkPaint& paint,
SkRect clip,
const TextStyle& style,
SkScalar thickness,
SkPath& path) const {
SkScalar scaleFactor = style.getFontSize() / 14.f;
switch (style.getDecorationStyle()) {
case TextDecorationStyle::kSolid:
break;
case TextDecorationStyle::kDouble:
break;
// Note: the intervals are scaled by the thickness of the line, so it is
// possible to change spacing by changing the decoration_thickness
// property of TextStyle.
case TextDecorationStyle::kDotted: {
const SkScalar intervals[] = {1.0f * scaleFactor, 1.5f * scaleFactor,
1.0f * scaleFactor, 1.5f * scaleFactor};
size_t count = sizeof(intervals) / sizeof(intervals[0]);
paint.setPathEffect(SkPathEffect::MakeCompose(
SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
SkDiscretePathEffect::Make(0, 0)));
break;
}
// Note: the intervals are scaled by the thickness of the line, so it is
// possible to change spacing by changing the decoration_thickness
// property of TextStyle.
case TextDecorationStyle::kDashed: {
const SkScalar intervals[] = {4.0f * scaleFactor, 2.0f * scaleFactor,
4.0f * scaleFactor, 2.0f * scaleFactor};
size_t count = sizeof(intervals) / sizeof(intervals[0]);
paint.setPathEffect(SkPathEffect::MakeCompose(
SkDashPathEffect::Make(intervals, (int32_t)count, 0.0f),
SkDiscretePathEffect::Make(0, 0)));
break;
}
case TextDecorationStyle::kWavy: {
int wave_count = 0;
SkScalar x_start = 0;
SkScalar quarterWave = thickness * style.getDecorationThicknessMultiplier();
path.moveTo(0, 0);
while (x_start + quarterWave * 2 < clip.width()) {
path.rQuadTo(quarterWave,
wave_count % 2 != 0 ? quarterWave : -quarterWave,
quarterWave * 2,
0);
x_start += quarterWave * 2;
++wave_count;
}
// The rest of the wave
auto remaining = clip.width() - x_start;
if (remaining > 0) {
double x1 = remaining / 2;
double y1 = remaining / 2 * (wave_count % 2 == 0 ? -1 : 1);
double x2 = remaining;
double y2 = (remaining - remaining * remaining / (quarterWave * 2)) *
(wave_count % 2 == 0 ? -1 : 1);
path.rQuadTo(x1, y1, x2, y2);
}
break;
}
}
}
void TextLine::justify(SkScalar maxWidth) {

View File

@ -116,13 +116,6 @@ private:
void paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const;
void paintDecorations(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const;
void computeDecorationPaint(SkPaint& paint, SkRect clip, const TextStyle& style, SkScalar thickness,
SkPath& path) const;
bool contains(const Cluster* cluster) const {
return fTextRange.contains(cluster->textRange());
}
void shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift);
ParagraphImpl* fMaster;

View File

@ -15,6 +15,7 @@ TextStyle::TextStyle() : fFontStyle() {
// value to indicate no decoration color was set.
fDecoration.fColor = SK_ColorTRANSPARENT;
fDecoration.fStyle = TextDecorationStyle::kSolid;
fDecoration.fMode = TextDecorationMode::kGaps;
// Thickness is applied as a multiplier to the default thickness of the font.
fDecoration.fThicknessMultiplier = 1.0;
fFontSize = 14.0;

View File

@ -2538,6 +2538,68 @@ private:
typedef Sample INHERITED;
};
class ParagraphView38 : public ParagraphView_Base {
protected:
SkString name() override { return SkString("Paragraph38"); }
void onDrawContent(SkCanvas* canvas) override {
canvas->drawColor(SK_ColorWHITE);
auto fontCollection = sk_make_sp<FontCollection>();
fontCollection->setDefaultFontManager(SkFontMgr::RefDefault());
fontCollection->enableFontFallback();
ParagraphStyle paragraph_style;
paragraph_style.setTextAlign(TextAlign::kLeft);
ParagraphBuilderImpl builder(paragraph_style, fontCollection);
TextStyle text_style;
text_style.setColor(SK_ColorDKGRAY);
text_style.setFontFamilies({SkString("Roboto")});
text_style.setFontSize(40);
text_style.setDecoration(TextDecoration::kUnderline);
text_style.setDecorationMode(TextDecorationMode::kThrough);
text_style.setDecorationStyle(TextDecorationStyle::kDouble);
text_style.setDecorationColor(SK_ColorBLUE);
builder.pushStyle(text_style);
builder.addText("Double underline: {opopo}\n");
text_style.setDecorationMode(TextDecorationMode::kGaps);
text_style.setDecorationStyle(TextDecorationStyle::kDouble);
text_style.setDecorationColor(SK_ColorBLUE);
builder.pushStyle(text_style);
builder.addText("Double underline: {opopo}\n");
text_style.setDecorationStyle(TextDecorationStyle::kDotted);
text_style.setDecorationColor(SK_ColorRED);
builder.pushStyle(text_style);
builder.addText("Dotted underline: {ijiji}\n");
text_style.setDecorationStyle(TextDecorationStyle::kSolid);
text_style.setDecorationColor(SK_ColorGREEN);
builder.pushStyle(text_style);
builder.addText("Solid underline: {rqrqr}\n");
text_style.setDecorationStyle(TextDecorationStyle::kDashed);
text_style.setDecorationColor(SK_ColorMAGENTA);
builder.pushStyle(text_style);
builder.addText("Dashed underline: {zyzyz}\n");
text_style.setDecorationStyle(TextDecorationStyle::kWavy);
text_style.setDecorationColor(SK_ColorCYAN);
builder.pushStyle(text_style);
builder.addText("Wavy underline: {does not skip}\n");
auto paragraph = builder.Build();
paragraph->layout(width());
paragraph->paint(canvas, 0, 0);
}
private:
typedef Sample INHERITED;
};
//////////////////////////////////////////////////////////////////////////////
DEF_SAMPLE(return new ParagraphView1();)
DEF_SAMPLE(return new ParagraphView2();)
@ -2556,7 +2618,7 @@ DEF_SAMPLE(return new ParagraphView15();)
DEF_SAMPLE(return new ParagraphView16();)
DEF_SAMPLE(return new ParagraphView17();)
DEF_SAMPLE(return new ParagraphView18();)
DEF_SAMPLE(return new ParagraphView19();)
//DEF_SAMPLE(return new ParagraphView19();)
DEF_SAMPLE(return new ParagraphView20();)
DEF_SAMPLE(return new ParagraphView21();)
DEF_SAMPLE(return new ParagraphView22();)
@ -2575,3 +2637,4 @@ DEF_SAMPLE(return new ParagraphView34();)
DEF_SAMPLE(return new ParagraphView35();)
DEF_SAMPLE(return new ParagraphView36();)
DEF_SAMPLE(return new ParagraphView37();)
DEF_SAMPLE(return new ParagraphView38();)