// Copyright 2019 Google LLC. #include "modules/skparagraph/src/TextLine.h" #include #include #include "modules/skparagraph/src/ParagraphImpl.h" #include "include/core/SkMaskFilter.h" #include "include/effects/SkDashPathEffect.h" #include "include/effects/SkDiscretePathEffect.h" #include "src/core/SkMakeUnique.h" namespace skia { namespace textlayout { // TODO: deal with all the intersection functionality int32_t intersectedSize(TextRange a, TextRange b) { if (a.empty() || b.empty()) { return -1; } auto begin = SkTMax(a.start, b.start); auto end = SkTMin(a.end, b.end); return begin <= end ? SkToS32(end - begin) : -1; } TextRange intersected(const TextRange& a, const TextRange& b) { if (a.start == b.start && a.end == b.end) return a; auto begin = SkTMax(a.start, b.start); auto end = SkTMin(a.end, b.end); return end >= begin ? TextRange(begin, end) : EMPTY_TEXT; } SkTHashMap TextLine::fEllipsisCache; TextLine::TextLine(ParagraphImpl* master, SkVector offset, SkVector advance, BlockRange blocks, TextRange text, TextRange textWithSpaces, ClusterRange clusters, ClusterRange clustersWithGhosts, SkScalar widthWithSpaces, LineMetrics sizes) : fMaster(master) , fBlockRange(blocks) , fTextRange(text) , fTextWithWhitespacesRange(textWithSpaces) , fClusterRange(clusters) , fGhostClusterRange(clustersWithGhosts) , fLogical() , fAdvance(advance) , fOffset(offset) , fShift(0.0) , fWidthWithSpaces(widthWithSpaces) , fEllipsis(nullptr) , fSizes(sizes) , fHasBackground(false) , fHasShadows(false) , fHasDecorations(false) { // Reorder visual runs auto& start = master->cluster(fGhostClusterRange.start); auto& end = master->cluster(fGhostClusterRange.end - 1); size_t numRuns = end.runIndex() - start.runIndex() + 1; for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { auto b = fMaster->styles().begin() + index; if (b->fStyle.hasBackground()) { fHasBackground = true; } if (b->fStyle.getDecorationType() != TextDecoration::kNoDecoration) { fHasDecorations = true; } if (b->fStyle.getShadowNumber() > 0) { fHasShadows = true; } } // Get the logical order std::vector runLevels; for (auto runIndex = start.runIndex(); runIndex <= end.runIndex(); ++runIndex) { auto& run = fMaster->run(runIndex); runLevels.emplace_back(run.fBidiLevel); } std::vector logicalOrder(numRuns); ubidi_reorderVisual(runLevels.data(), SkToU32(numRuns), logicalOrder.data()); auto firstRunIndex = start.runIndex(); for (auto index : logicalOrder) { fLogical.push_back(firstRunIndex + index); } } void TextLine::paint(SkCanvas* textCanvas) { if (this->empty()) { return; } textCanvas->save(); textCanvas->translate(this->offset().fX, this->offset().fY); if (fHasBackground) { this->iterateThroughStylesInTextOrder( StyleType::kBackground, [this, textCanvas](TextRange textRange, const TextStyle& style, SkScalar offsetX) { return this->paintBackground(textCanvas, textRange, style, offsetX); }); } if (fHasShadows) { this->iterateThroughStylesInTextOrder( StyleType::kShadow, [textCanvas, this](TextRange textRange, const TextStyle& style, SkScalar offsetX) { return this->paintShadow(textCanvas, textRange, style, offsetX); }); } this->iterateThroughStylesInTextOrder( StyleType::kForeground, [textCanvas, this](TextRange textRange, const TextStyle& style, SkScalar offsetX) { return this->paintText(textCanvas, textRange, style, offsetX); }); if (fHasDecorations) { this->iterateThroughStylesInTextOrder( StyleType::kDecorations, [textCanvas, this](TextRange textRange, const TextStyle& style, SkScalar offsetX) { return this->paintDecorations(textCanvas, textRange, style, offsetX); }); } textCanvas->restore(); } void TextLine::format(TextAlign effectiveAlign, SkScalar maxWidth) { SkScalar delta = maxWidth - this->width(); if (delta <= 0) { return; } if (effectiveAlign == TextAlign::kJustify) { this->justify(maxWidth); } else if (effectiveAlign == TextAlign::kRight) { fShift = delta; } else if (effectiveAlign == TextAlign::kCenter) { fShift = delta / 2; } } TextAlign TextLine::assumedTextAlign() const { if (this->fMaster->paragraphStyle().getTextAlign() != TextAlign::kJustify) { return this->fMaster->paragraphStyle().effective_align(); } if (fClusterRange.empty()) { return TextAlign::kLeft; } else { auto run = this->fMaster->cluster(fClusterRange.end - 1).run(); return run->leftToRight() ? TextAlign::kLeft : TextAlign::kRight; } } void TextLine::scanStyles(StyleType style, const StyleVisitor& visitor) { if (this->empty()) { return; } this->iterateThroughStylesInTextOrder( style, [this, visitor](TextRange textRange, const TextStyle& style, SkScalar offsetX) { visitor(textRange, style, offsetX); return this->iterateThroughRuns( textRange, offsetX, false, [](Run*, int32_t, size_t, TextRange, SkRect, SkScalar, bool) { return true; }); }); } void TextLine::scanRuns(const RunVisitor& visitor) { this->iterateThroughRuns( fTextRange, 0, false, [visitor](Run* run, int32_t pos, size_t size, TextRange text, SkRect clip, SkScalar sc, bool b) { visitor(run, pos, size, text, clip, sc, b); return true; }); } SkScalar TextLine::paintText(SkCanvas* canvas, TextRange textRange, const TextStyle& style, SkScalar offsetX) const { SkPaint paint; if (style.hasForeground()) { paint = style.getForeground(); } else { paint.setColor(style.getColor()); } auto shiftDown = this->baseline(); return this->iterateThroughRuns( textRange, offsetX, false, [canvas, paint, shiftDown](Run* run, int32_t pos, size_t size, TextRange, SkRect clip, SkScalar shift, bool clippingNeeded) { SkTextBlobBuilder builder; run->copyTo(builder, SkToU32(pos), size, SkVector::Make(0, shiftDown)); canvas->save(); if (clippingNeeded) { canvas->clipRect(clip); } canvas->translate(shift, 0); canvas->drawTextBlob(builder.make(), 0, 0, paint); canvas->restore(); return true; }); } SkScalar TextLine::paintBackground(SkCanvas* canvas, TextRange textRange, const TextStyle& style, SkScalar offsetX) const { return this->iterateThroughRuns(textRange, offsetX, false, [canvas, &style](Run* run, int32_t pos, size_t size, TextRange, SkRect clip, SkScalar shift, bool clippingNeeded) { if (style.hasBackground()) { canvas->drawRect(clip, style.getBackground()); } return true; }); } SkScalar TextLine::paintShadow(SkCanvas* canvas, TextRange textRange, const TextStyle& style, SkScalar offsetX) const { auto shiftDown = this->baseline(); auto result = this->iterateThroughRuns( textRange, offsetX, false, [canvas, shiftDown, &style](Run* run, size_t pos, size_t size, TextRange, SkRect clip, SkScalar shift, bool clippingNeeded) { 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); } SkTextBlobBuilder builder; run->copyTo(builder, pos, size, SkVector::Make(0, shiftDown)); canvas->save(); clip.offset(shadow.fOffset); if (clippingNeeded) { canvas->clipRect(clip); } canvas->translate(shift, 0); canvas->drawTextBlob(builder.make(), shadow.fOffset.x(), shadow.fOffset.y(), paint); canvas->restore(); } return true; }); return result; } SkScalar TextLine::paintDecorations(SkCanvas* canvas, TextRange textRange, const TextStyle& style, SkScalar offsetX) const { return this->iterateThroughRuns( textRange, offsetX, false, [this, canvas, &style](Run* run, int32_t pos, size_t size, TextRange, SkRect clip, SkScalar shift, bool clippingNeeded) { if (style.getDecorationType() == TextDecoration::kNoDecoration) { return true; } for (auto decoration : AllTextDecorations) { if ((style.getDecorationType() & decoration) == 0) { continue; } SkScalar thickness = style.getDecorationThicknessMultiplier(); // SkScalar position = 0; switch (decoration) { case TextDecoration::kUnderline: position = -run->correctAscent() + thickness; break; case TextDecoration::kOverline: position = 0; break; case TextDecoration::kLineThrough: { position = (run->correctDescent() - run->correctAscent() - thickness) / 2; break; } default: // TODO: can we actually get here? SkASSERT(false); break; } auto width = clip.width(); SkScalar x = clip.left(); SkScalar y = clip.top() + position; // Decoration paint (for now) and/or path SkPaint paint; SkPath path; this->computeDecorationPaint(paint, clip, style, path); paint.setStrokeWidth(thickness); 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 + thickness * 2; 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; } } return true; }); } void TextLine::computeDecorationPaint(SkPaint& paint, SkRect clip, const TextStyle& style, SkPath& path) const { paint.setStyle(SkPaint::kStroke_Style); if (style.getDecorationColor() == SK_ColorTRANSPARENT) { paint.setColor(style.getColor()); } else { paint.setColor(style.getDecorationColor()); } 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 wavelength = scaleFactor * style.getDecorationThicknessMultiplier(); auto width = clip.width(); path.moveTo(0, 0); while (x_start + wavelength * 2 < width) { path.rQuadTo(wavelength, wave_count % 2 != 0 ? wavelength : -wavelength, wavelength * 2, 0); x_start += wavelength * 2; ++wave_count; } break; } } } void TextLine::justify(SkScalar maxWidth) { // Count words and the extra spaces to spread across the line // TODO: do it at the line breaking?.. size_t whitespacePatches = 0; SkScalar textLen = 0; bool whitespacePatch = false; this->iterateThroughClustersInGlyphsOrder(false, false, [&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster, ClusterIndex index, bool leftToRight, bool ghost) { if (cluster->isWhitespaces()) { if (!whitespacePatch) { whitespacePatch = true; ++whitespacePatches; } } else { whitespacePatch = false; } textLen += cluster->width(); return true; }); if (whitespacePatches == 0) { return; } SkScalar step = (maxWidth - textLen) / whitespacePatches; SkScalar shift = 0; // Deal with the ghost spaces auto ghostShift = maxWidth - this->fAdvance.fX; // Spread the extra whitespaces whitespacePatch = false; this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, ClusterIndex index, bool leftToRight, bool ghost) { if (ghost) { if (leftToRight) { fMaster->shiftCluster(index, ghostShift); } return true; } if (cluster->isWhitespaces()) { if (!whitespacePatch) { shift += step; whitespacePatch = true; --whitespacePatches; } } else { whitespacePatch = false; } fMaster->shiftCluster(index, shift); return true; }); SkAssertResult(SkScalarNearlyEqual(shift, maxWidth - textLen)); SkASSERT(whitespacePatches == 0); this->fWidthWithSpaces += ghostShift; this->fAdvance.fX = maxWidth; } void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) { // Replace some clusters with the ellipsis // Go through the clusters in the reverse logical order // taking off cluster by cluster until the ellipsis fits SkScalar width = fAdvance.fX; iterateThroughClustersInGlyphsOrder( true, false, [this, &width, ellipsis, maxWidth](const Cluster* cluster, ClusterIndex index, bool leftToRight, bool ghost) { if (cluster->isWhitespaces()) { width -= cluster->width(); return true; } // Shape the ellipsis Run* cached = fEllipsisCache.find(cluster->font()); if (cached == nullptr) { cached = shapeEllipsis(ellipsis, cluster->run()); } else { cached->setMaster(fMaster); } fEllipsis = std::make_shared(*cached); // See if it fits if (width + fEllipsis->advance().fX > maxWidth) { width -= cluster->width(); // Continue if it's not return true; } fEllipsis->shift(width, 0); fAdvance.fX = width; return false; }); } Run* TextLine::shapeEllipsis(const SkString& ellipsis, Run* run) { class ShapeHandler final : public SkShaper::RunHandler { public: ShapeHandler(SkScalar lineHeight, const SkString& ellipsis) : fRun(nullptr), fLineHeight(lineHeight), fEllipsis(ellipsis) {} Run* run() { return fRun; } private: void beginLine() override {} void runInfo(const RunInfo&) override {} void commitRunInfo() override {} Buffer runBuffer(const RunInfo& info) override { fRun = fEllipsisCache.set(info.fFont, Run(nullptr, info, fLineHeight, 0, 0)); return fRun->newRunBuffer(); } void commitRunBuffer(const RunInfo& info) override { fRun->fAdvance.fX = info.fAdvance.fX; fRun->fAdvance.fY = fRun->advance().fY; } void commitLine() override {} Run* fRun; SkScalar fLineHeight; SkString fEllipsis; }; ShapeHandler handler(run->lineHeight(), ellipsis); std::unique_ptr shaper = SkShaper::MakeShapeDontWrapOrReorder(); SkASSERT_RELEASE(shaper != nullptr); shaper->shape(ellipsis.c_str(), ellipsis.size(), run->font(), true, std::numeric_limits::max(), &handler); handler.run()->fTextRange = TextRange(0, ellipsis.size()); handler.run()->fMaster = fMaster; return handler.run(); } SkRect TextLine::measureTextInsideOneRun( TextRange textRange, Run* run, size_t& pos, size_t& size, bool includeGhostSpaces, bool& clippingNeeded) const { SkASSERT(intersectedSize(run->textRange(), textRange) >= 0); // Find [start:end] clusters for the text bool found; ClusterIndex startIndex; ClusterIndex endIndex; std::tie(found, startIndex, endIndex) = run->findLimitingClusters(textRange); if (!found) { SkASSERT(textRange.empty()); return SkRect::MakeEmpty(); } auto start = fMaster->clusters().begin() + startIndex; auto end = fMaster->clusters().begin() + endIndex; pos = start->startPos(); size = end->endPos() - start->startPos(); // Calculate the clipping rectangle for the text with cluster edges // There are 2 cases: // EOL (when we expect the last cluster clipped without any spaces) // Anything else (when we want the cluster width contain all the spaces - // coming from letter spacing or word spacing or justification) auto range = includeGhostSpaces ? fGhostClusterRange : fClusterRange; bool needsClipping = (run->leftToRight() ? endIndex == range.end - 1 : startIndex == range.end - 1); SkRect clip = SkRect::MakeXYWH(run->positionX(start->startPos()) - run->positionX(0), sizes().runTop(run), run->calculateWidth(start->startPos(), end->endPos(), needsClipping), run->calculateHeight()); // Correct the width in case the text edges don't match clusters // TODO: This is where we get smart about selecting a part of a cluster // by shaping each grapheme separately and then use the result sizes // to calculate the proportions auto leftCorrection = start->sizeToChar(textRange.start); auto rightCorrection = end->sizeFromChar(textRange.end - 1); clip.fLeft += leftCorrection; clip.fRight -= rightCorrection; clippingNeeded = leftCorrection != 0 || rightCorrection != 0; // SkDebugf("measureTextInsideOneRun: '%s'[%d:%d]\n", text.begin(), pos, pos + size); return clip; } void TextLine::iterateThroughClustersInGlyphsOrder(bool reverse, bool includeGhosts, const ClustersVisitor& visitor) const { // Walk through the clusters in the logical order (or reverse) for (size_t r = 0; r != fLogical.size(); ++r) { auto& runIndex = fLogical[reverse ? fLogical.size() - r - 1 : r]; auto run = this->fMaster->runs().begin() + runIndex; auto start = SkTMax(run->clusterRange().start, fClusterRange.start); auto end = SkTMin(run->clusterRange().end, fClusterRange.end); auto ghosts = SkTMin(run->clusterRange().end, fGhostClusterRange.end); if (run->leftToRight() != reverse) { for (auto index = start; index < ghosts; ++index) { if (index >= end && !includeGhosts) { break; } const auto& cluster = &fMaster->cluster(index); if (!visitor(cluster, index, run->leftToRight(), index >= end)) { return; } } } else { for (auto index = ghosts; index > start; --index) { if (index > end && !includeGhosts) { continue; } const auto& cluster = &fMaster->cluster(index - 1); if (!visitor(cluster, index - 1, run->leftToRight(), index > end)) { return; } } } } } SkScalar TextLine::calculateLeftVisualOffset(TextRange textRange) const { SkScalar partOfTheCurrentRun = 0; return this->iterateThroughRuns(this->textWithSpaces(), 0, true, [textRange, &partOfTheCurrentRun, this]( Run* run, size_t pos, size_t size, TextRange text, SkRect clip, SkScalar shift, bool clippingNeeded) { if (text.start > textRange.start || text.end <= textRange.start) { // This run does not even touch the text start } else { // This is the run TextRange part; if (run->leftToRight()) { part = {text.start, textRange.start}; } else if (textRange.end < text.end) { part = {textRange.end, text.end}; } if (part.width() == 0) { return false; } size_t pos; size_t size; bool clippingNeeded; SkRect partClip = this->measureTextInsideOneRun(part, run, pos, size, true, clippingNeeded); partOfTheCurrentRun = partClip.width(); return false; } return true; }) + partOfTheCurrentRun; } // Walk through the runs in the logical order SkScalar TextLine::iterateThroughRuns(TextRange textRange, SkScalar runOffset, bool includeGhostWhitespaces, const RunVisitor& visitor) const { TRACE_EVENT0("skia", TRACE_FUNC); SkScalar width = 0; for (auto& runIndex : fLogical) { const auto run = &this->fMaster->run(runIndex); // Only skip the text if it does not even touch the run auto intersection = intersectedSize(run->textRange(), textRange); if (intersection < 0 || (intersection == 0 && textRange.end != run->textRange().end)) { continue; } auto intersect = intersected(run->textRange(), textRange); if (run->textRange().empty() || intersect.empty()) { continue; } // Measure the text size_t pos; size_t size; bool clippingNeeded; SkRect clip = this->measureTextInsideOneRun(intersect, run, pos, size, includeGhostWhitespaces, clippingNeeded); if (clip.height() == 0) { continue; } // Clip the text auto shift = runOffset - clip.fLeft; clip.offset(shift, 0); if (includeGhostWhitespaces) { clippingNeeded = false; } else { clippingNeeded = true; if (clip.fRight > fAdvance.fX) { clip.fRight = fAdvance.fX; } } if (!visitor(run, pos, size, intersect, clip, shift - run->positionX(0), clippingNeeded)) { return width; } if (run->leftToRight() || &runIndex == &fLogical.back()) { width += clip.width(); runOffset += clip.width(); } else { width += run->advance().fX; runOffset += run->advance().fX; } } if (this->ellipsis() != nullptr) { auto ellipsis = this->ellipsis(); if (!visitor(ellipsis, 0, ellipsis->size(), ellipsis->textRange(), ellipsis->clip(), ellipsis->clip().fLeft, false)) { return width; } width += ellipsis->clip().width(); } return width; } void TextLine::iterateThroughStylesInTextOrder(StyleType styleType, const StyleVisitor& visitor) const { TextIndex start = EMPTY_INDEX; size_t size = 0; const TextStyle* prevStyle = nullptr; SkScalar offsetX = 0; for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) { auto block = fMaster->styles().begin() + index; auto intersect = intersected(block->fRange, this->trimmedText()); if (intersect.empty()) { if (start == EMPTY_INDEX) { // This style is not applicable to the line continue; } else { // We have found all the good styles already break; } } auto* style = &block->fStyle; if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) { size += intersect.width(); continue; } else if (start == EMPTY_INDEX ) { // First time only prevStyle = style; size = intersect.width(); start = intersect.start; continue; } auto width = visitor(TextRange(start, start + size), *prevStyle, offsetX); offsetX += width; // Start all over again prevStyle = style; start = intersect.start; size = intersect.width(); } if (prevStyle == nullptr) return; // The very last style auto width = visitor(TextRange(start, start + size), *prevStyle, offsetX); offsetX += width; // This is a very important assert! // It asserts that 2 different ways of calculation come with the same results if (!SkScalarNearlyEqual(offsetX, this->width())) { SkDebugf("ASSERT: %f != %f\n", offsetX, this->width()); } SkASSERT(SkScalarNearlyEqual(offsetX, this->width())); } SkVector TextLine::offset() const { return fOffset + SkVector::Make(fShift, 0); } } // namespace textlayout } // namespace skia