Fixing incorrect boundaries calculations + 3 ways of drawing a paragraph

Bug: skia:10620
Change-Id: I70086013130b23435d4e7c5ba375731760deb174
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/311447
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
This commit is contained in:
Julia Lavrova 2020-07-28 13:44:27 -04:00 committed by Skia Commit-Bot
parent 6e0fa409e6
commit d279cee2f2
8 changed files with 134 additions and 123 deletions

View File

@ -144,6 +144,12 @@ enum class LineMetricStyle : uint8_t {
CSS
};
enum class DrawOptions {
kRecord,
kReplay,
kDirect
};
} // namespace textlayout
} // namespace skia

View File

@ -99,7 +99,6 @@ struct ParagraphStyle {
SkScalar getHeight() const { return fHeight; }
void setHeight(SkScalar height) { fHeight = height; }
TextHeightBehavior getTextHeightBehavior() const { return fTextHeightBehavior; }
void setTextHeightBehavior(TextHeightBehavior v) { fTextHeightBehavior = v; }
@ -110,6 +109,8 @@ struct ParagraphStyle {
TextAlign effective_align() const;
bool hintingIsOn() const { return fHintingIsOn; }
void turnHintingOff() { fHintingIsOn = false; }
DrawOptions getDrawOptions() { return fDrawingOptions; }
void setDrawOptions(DrawOptions value) { fDrawingOptions = value; }
private:
StrutStyle fStrutStyle;
@ -121,6 +122,7 @@ private:
SkScalar fHeight;
TextHeightBehavior fTextHeightBehavior;
bool fHintingIsOn;
DrawOptions fDrawingOptions = DrawOptions::kReplay;
};
} // namespace textlayout
} // namespace skia

View File

@ -2898,6 +2898,46 @@ private:
typedef Sample INHERITED;
};
class ParagraphView46 : public ParagraphView_Base {
protected:
SkString name() override { return SkString("Paragraph44"); }
void onDrawContent(SkCanvas* canvas) override {
SkString text;
for (auto i = 0; i < 150; ++i) text.append("XXXXXXXXXX");
canvas->drawColor(SK_ColorWHITE);
auto fontCollection = sk_make_sp<FontCollection>();
fontCollection->setDefaultFontManager(SkFontMgr::RefDefault());
fontCollection->enableFontFallback();
ParagraphStyle paragraph_style;
auto draw = [&](DrawOptions options) {
paragraph_style.setDrawOptions(options);
ParagraphBuilderImpl builder(paragraph_style, fontCollection);
TextStyle text_style;
text_style.setColor(SK_ColorBLACK);
text_style.setFontFamilies({SkString("Roboto")});
text_style.setFontSize(20);
builder.pushStyle(text_style);
builder.addText(text.c_str());
auto paragraph = builder.Build();
paragraph->layout(width() / 3);
paragraph->paint(canvas, 0, 0);
canvas->translate(width() / 3, 0);
};
draw(DrawOptions::kReplay);
draw(DrawOptions::kRecord);
draw(DrawOptions::kDirect);
}
private:
typedef Sample INHERITED;
};
} // namespace
//////////////////////////////////////////////////////////////////////////////
@ -2944,3 +2984,4 @@ DEF_SAMPLE(return new ParagraphView42();)
DEF_SAMPLE(return new ParagraphView43();)
DEF_SAMPLE(return new ParagraphView44();)
DEF_SAMPLE(return new ParagraphView45();)
DEF_SAMPLE(return new ParagraphView46();)

View File

@ -81,8 +81,7 @@ ParagraphImpl::ParagraphImpl(const SkString& text,
, fPicture(nullptr)
, fStrutMetrics(false)
, fOldWidth(0)
, fOldHeight(0)
, fOrigin(SkRect::MakeEmpty()) {
, fOldHeight(0) {
fICU = ::skia::SkUnicode::Make();
}
@ -184,8 +183,6 @@ void ParagraphImpl::layout(SkScalar rawWidth) {
if (fState < kFormatted) {
// Build the picture lazily not until we actually have to paint (or never)
this->formatLines(fWidth);
// We have to calculate the paragraph boundaries only after we format the lines
this->calculateBoundaries();
fState = kFormatted;
}
@ -207,14 +204,32 @@ void ParagraphImpl::layout(SkScalar rawWidth) {
void ParagraphImpl::paint(SkCanvas* canvas, SkScalar x, SkScalar y) {
if (fParagraphStyle.getDrawOptions() == DrawOptions::kDirect) {
// Paint the text without recording it
canvas->save();
canvas->translate(x, y);
this->paintLines(canvas);
canvas->restore();
return;
}
if (fState < kDrawn) {
// Record the picture anyway (but if we have some pieces in the cache they will be used)
this->paintLinesIntoPicture();
fState = kDrawn;
}
SkMatrix matrix = SkMatrix::Translate(x + fOrigin.fLeft, y + fOrigin.fTop);
canvas->drawPicture(fPicture, &matrix, nullptr);
if (fParagraphStyle.getDrawOptions() == DrawOptions::kReplay) {
// Replay the recorded picture
canvas->save();
canvas->translate(x, y);
fPicture->playback(canvas);
canvas->restore();
} else {
// Draw the picture
SkMatrix matrix = SkMatrix::Translate(x, y);
canvas->drawPicture(fPicture, &matrix, nullptr);
}
}
void ParagraphImpl::resetContext() {
@ -417,13 +432,6 @@ void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) {
auto& line = this->addLine(offset, advance, text, textWithSpaces, clusters, clustersWithGhosts, widthWithSpaces, metrics);
if (addEllipsis) {
line.createEllipsis(maxWidth, fParagraphStyle.getEllipsis(), true);
if (line.ellipsis() != nullptr) {
// Make sure the paragraph boundaries include its ellipsis
auto size = line.ellipsis()->advance();
auto offset = line.ellipsis()->offset();
SkRect boundaries = SkRect::MakeXYWH(offset.fX, offset.fY, size.fX, size.fY);
fOrigin.joinPossiblyEmptyRect(boundaries);
}
}
fLongestLine = std::max(fLongestLine, nearlyZero(advance.fX) ? widthWithSpaces : advance.fX);
@ -476,14 +484,21 @@ void ParagraphImpl::formatLines(SkScalar maxWidth) {
void ParagraphImpl::paintLinesIntoPicture() {
SkPictureRecorder recorder;
SkCanvas* textCanvas = recorder.beginRecording(fOrigin.width(), fOrigin.height(), nullptr, 0);
textCanvas->translate(-fOrigin.fLeft, -fOrigin.fTop);
SkCanvas* textCanvas = recorder.beginRecording(this->getMaxWidth(), this->getHeight(), nullptr, 0);
auto bounds = SkRect::MakeEmpty();
for (auto& line : fLines) {
line.paint(textCanvas);
auto boundaries = line.paint(textCanvas);
bounds.joinPossiblyEmptyRect(boundaries);
}
fPicture = recorder.finishRecordingAsPicture();
fPicture = recorder.finishRecordingAsPictureWithCull(bounds);
}
void ParagraphImpl::paintLines(SkCanvas* canvas) {
for (auto& line : fLines) {
line.paint(canvas);
}
}
void ParagraphImpl::resolveStrut() {
@ -539,12 +554,6 @@ BlockRange ParagraphImpl::findAllBlocks(TextRange textRange) {
return { begin, end + 1 };
}
void ParagraphImpl::calculateBoundaries() {
for (auto& line : fLines) {
fOrigin.joinPossiblyEmptyRect(line.calculateBoundaries());
}
}
TextLine& ParagraphImpl::addLine(SkVector offset,
SkVector advance,
TextRange text,

View File

@ -179,7 +179,6 @@ public:
void setState(InternalState state);
sk_sp<SkPicture> getPicture() { return fPicture; }
SkRect getBoundaries() const { return fOrigin; }
SkScalar widthWithTrailingSpaces() { return fMaxWidthWithTrailingSpaces; }
@ -193,6 +192,7 @@ public:
bool shapeTextIntoEndlessLine();
void breakShapedTextIntoLines(SkScalar maxWidth);
void paintLinesIntoPicture();
void paintLines(SkCanvas* canvas);
void updateTextAlign(TextAlign textAlign) override;
void updateText(size_t from, SkString text) override;
@ -229,8 +229,6 @@ private:
friend class TextWrapper;
friend class OneLineShaper;
void calculateBoundaries();
void computeEmptyMetrics();
// Input
@ -269,7 +267,6 @@ private:
SkScalar fOldWidth;
SkScalar fOldHeight;
SkScalar fMaxWidthWithTrailingSpaces;
SkRect fOrigin;
std::unique_ptr<SkUnicode> fICU;
};

View File

@ -159,78 +159,10 @@ TextLine::TextLine(ParagraphImpl* owner,
}
}
SkRect TextLine::calculateBoundaries() {
// For flutter: height and/or width and/or baseline! can be Inf
// (coming from placeholders - we should ignore it)
auto boundaries = SkRect::MakeWH(
SkScalarIsFinite(fAdvance.fX) ? fAdvance.fX : 0,
SkScalarIsFinite(fAdvance.fY) ? fAdvance.fY : 0);
auto baseline = SkScalarIsFinite(this->baseline()) ? this->baseline() : 0;
auto clusters = fOwner->clusters(fClusterRange);
Run* run = nullptr;
auto runShift = 0.0f;
auto clusterShift = 0.0f;
for (auto cluster = clusters.begin(); cluster != clusters.end(); ++cluster) {
if (run == nullptr || cluster->runIndex() != run->index()) {
run = &fOwner->run(cluster->runIndex());
runShift += clusterShift;
clusterShift = 0;
}
clusterShift += cluster->width();
for (auto i = cluster->startPos(); i < cluster->endPos(); ++i) {
auto posX = run->positionX(i);
auto posY = run->posY(i);
auto bounds = run->getBounds(i);
bounds.offset(posX + runShift, posY);
boundaries.joinPossiblyEmptyRect(bounds);
}
}
// We need to take in account all the shadows when we calculate the boundaries
// TODO: Need to find a better solution
if (fHasShadows) {
SkRect shadowRect = SkRect::MakeEmpty();
this->iterateThroughVisualRuns(false,
[this, &shadowRect, boundaries]
(const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
*runWidthInLine = this->iterateThroughSingleRunByStyles(
run, runOffsetInLine, textRange, StyleType::kShadow,
[&shadowRect, boundaries](TextRange textRange, const TextStyle& style, const ClipContext& context) {
for (TextShadow shadow : style.getShadows()) {
if (!shadow.hasShadow()) continue;
SkPaint paint;
paint.setColor(shadow.fColor);
if (shadow.fBlurRadius != 0.0) {
auto filter = SkMaskFilter::MakeBlur(
kNormal_SkBlurStyle,
SkDoubleToScalar(shadow.fBlurRadius),
false);
paint.setMaskFilter(filter);
SkRect bound;
paint.doComputeFastBounds(boundaries, &bound, SkPaint::Style::kFill_Style);
shadowRect.joinPossiblyEmptyRect(bound);
}
}
});
return true;
});
boundaries.fLeft += shadowRect.fLeft;
boundaries.fTop += shadowRect.fTop;
boundaries.fRight += shadowRect.fRight;
boundaries.fBottom += shadowRect.fBottom;
}
boundaries.offset(this->offset()); // Line offset from the beginning of the para
boundaries.offset(0, baseline); // Down by baseline
return boundaries;
}
void TextLine::paint(SkCanvas* textCanvas) {
SkRect TextLine::paint(SkCanvas* textCanvas) {
auto bounds = SkRect::MakeEmpty();
if (this->empty()) {
return;
return bounds;
}
if (fHasBackground) {
@ -248,19 +180,20 @@ void TextLine::paint(SkCanvas* textCanvas) {
if (fHasShadows) {
this->iterateThroughVisualRuns(false,
[textCanvas, this]
[textCanvas, &bounds, this]
(const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
*runWidthInLine = this->iterateThroughSingleRunByStyles(
run, runOffsetInLine, textRange, StyleType::kShadow,
[textCanvas, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
this->paintShadow(textCanvas, textRange, style, context);
[textCanvas, &bounds, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
auto shadowBounds = this->paintShadow(textCanvas, textRange, style, context);
bounds.joinPossiblyEmptyRect(shadowBounds);
});
return true;
});
}
this->iterateThroughVisualRuns(false,
[textCanvas, this]
[textCanvas, &bounds, this]
(const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
if (run->placeholderStyle() != nullptr) {
*runWidthInLine = run->advance().fX;
@ -268,8 +201,9 @@ void TextLine::paint(SkCanvas* textCanvas) {
}
*runWidthInLine = this->iterateThroughSingleRunByStyles(
run, runOffsetInLine, textRange, StyleType::kForeground,
[textCanvas, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
this->paintText(textCanvas, textRange, style, context);
[textCanvas, &bounds, this](TextRange textRange, const TextStyle& style, const ClipContext& context) {
auto textBounds = this->paintText(textCanvas, textRange, style, context);
bounds.joinPossiblyEmptyRect(textBounds);
});
return true;
});
@ -286,6 +220,8 @@ void TextLine::paint(SkCanvas* textCanvas) {
return true;
});
}
return bounds;
}
void TextLine::format(TextAlign align, SkScalar maxWidth) {
@ -358,10 +294,10 @@ SkScalar TextLine::metricsWithoutMultiplier(TextHeightBehavior correction) {
return delta;
}
void TextLine::paintText(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
SkRect TextLine::paintText(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
if (context.run->placeholderStyle() != nullptr) {
return;
return SkRect::MakeEmpty();
}
SkPaint paint;
@ -379,13 +315,23 @@ void TextLine::paintText(SkCanvas* canvas, TextRange textRange, const TextStyle&
canvas->clipRect(extendHeight(context).makeOffset(this->offset()));
}
SkRect textBounds = SkRect::MakeEmpty();
SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + 0.5);
canvas->drawTextBlob(builder.make(),
auto blob = builder.make();
if (blob != nullptr) {
auto bounds = blob->bounds();
bounds.offset(this->offset().fX, this->offset().fY);
textBounds.joinPossiblyEmptyRect(bounds);
}
canvas->drawTextBlob(blob,
this->offset().fX + context.fTextShift, this->offset().fY + correctedBaseline, paint);
if (context.clippingNeeded) {
canvas->restore();
}
return textBounds;
}
void TextLine::paintBackground(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
@ -394,8 +340,11 @@ void TextLine::paintBackground(SkCanvas* canvas, TextRange textRange, const Text
}
}
void TextLine::paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
auto shiftDown = this->baseline();
SkRect TextLine::paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {
SkScalar correctedBaseline = SkScalarFloorToScalar(this->baseline() + 0.5);
SkRect shadowBounds = SkRect::MakeEmpty();
for (TextShadow shadow : style.getShadows()) {
if (!shadow.hasShadow()) continue;
@ -416,15 +365,25 @@ void TextLine::paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyl
clip.offset(this->offset());
canvas->clipRect(clip);
}
canvas->drawTextBlob(builder.make(),
auto blob = builder.make();
if (blob != nullptr) {
auto bounds = blob->bounds();
bounds.offset(
this->offset().fX + shadow.fOffset.x(),
this->offset().fY + shadow.fOffset.y()
);
shadowBounds.joinPossiblyEmptyRect(bounds);
}
canvas->drawTextBlob(blob,
this->offset().fX + shadow.fOffset.x() + context.fTextShift,
this->offset().fY + shadow.fOffset.y() + shiftDown,
this->offset().fY + shadow.fOffset.y() + correctedBaseline,
paint);
if (context.clippingNeeded) {
canvas->restore();
}
}
return shadowBounds;
}
void TextLine::paintDecorations(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const {

View File

@ -84,7 +84,7 @@ public:
void iterateThroughClustersInGlyphsOrder(bool reverse, bool includeGhosts, const ClustersVisitor& visitor) const;
void format(TextAlign align, SkScalar maxWidth);
void paint(SkCanvas* canvas);
SkRect paint(SkCanvas* canvas);
void createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool ltr);
@ -109,8 +109,6 @@ public:
LineMetrics getMetrics() const;
SkRect calculateBoundaries();
SkRect extendHeight(const ClipContext& context) const;
SkScalar metricsWithoutMultiplier(TextHeightBehavior correction);
@ -123,9 +121,9 @@ private:
std::unique_ptr<Run> shapeEllipsis(const SkString& ellipsis, Run* run);
void justify(SkScalar maxWidth);
void paintText(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const;
SkRect paintText(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const;
void paintBackground(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const;
void paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyle& style, const ClipContext& context) const;
SkRect 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 shiftCluster(const Cluster* cluster, SkScalar shift, SkScalar prevShift);

View File

@ -5400,19 +5400,18 @@ DEF_TEST(SkParagraph_PlaceholderHeightInf, reporter) {
placeholder_style.fBaselineOffset = SK_ScalarInfinity;
ParagraphStyle paragraph_style;
paragraph_style.setDrawOptions(DrawOptions::kRecord);
ParagraphBuilderImpl builder(paragraph_style, fontCollection);
builder.pushStyle(text_style);
builder.addText("Limited by budget");
builder.addPlaceholder(placeholder_style);
auto paragraph = builder.Build();
paragraph->layout(SK_ScalarInfinity);
paragraph->paint(canvas.get(), 0, 0);
auto impl = static_cast<ParagraphImpl*>(paragraph.get());
REPORTER_ASSERT(reporter, SkScalarIsFinite(impl->getBoundaries().height()));
REPORTER_ASSERT(reporter, SkScalarIsFinite(impl->getBoundaries().width()));
paragraph->paint(canvas.get(), 0, 0);
REPORTER_ASSERT(reporter, SkScalarIsFinite(impl->getPicture()->cullRect().height()));
REPORTER_ASSERT(reporter, SkScalarIsFinite(impl->getPicture()->cullRect().width()));
}
DEF_TEST(SkParagraph_LineMetricsTextAlign, reporter) {