Reland "SkParagraph"
This is a reland of 10ad0b9b01e4b8a4721ae2ec1adee9ca7d0fe534
Original change's description:
> SkParagraph
>
> Change-Id: I0a4be75fd0c18021c201bcc1edfdfad8556edeff
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/192100
> Reviewed-by: Ben Wagner <bungeman@google.com>
> Reviewed-by: Mike Reed <reed@google.com>
> Commit-Queue: Julia Lavrova <jlavrova@google.com>
Change-Id: I46cf43eae693edf68e45345acd0eb39e04e02bfc
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/219863
Reviewed-by: Herb Derby <herb@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
diff --git a/modules/skparagraph/src/TextLine.cpp b/modules/skparagraph/src/TextLine.cpp
new file mode 100644
index 0000000..c389fcb
--- /dev/null
+++ b/modules/skparagraph/src/TextLine.cpp
@@ -0,0 +1,683 @@
+// Copyright 2019 Google LLC.
+#include "modules/skparagraph/src/TextLine.h"
+#include <unicode/brkiter.h>
+#include <unicode/ubidi.h>
+#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 {
+
+SkSpan<const char> intersected(const SkSpan<const char>& a, const SkSpan<const char>& b) {
+ auto begin = SkTMax(a.begin(), b.begin());
+ auto end = SkTMin(a.end(), b.end());
+ return SkSpan<const char>(begin, end > begin ? end - begin : 0);
+}
+
+int32_t intersectedSize(SkSpan<const char> a, SkSpan<const char> b) {
+ if (a.begin() == nullptr || b.begin() == nullptr) {
+ return -1;
+ }
+ auto begin = SkTMax(a.begin(), b.begin());
+ auto end = SkTMin(a.end(), b.end());
+ return SkToS32(end - begin);
+}
+} // namespace
+
+namespace skia {
+namespace textlayout {
+
+SkTHashMap<SkFont, Run> TextLine::fEllipsisCache;
+
+TextLine::TextLine(SkVector offset, SkVector advance, SkSpan<const TextBlock> blocks,
+ SkSpan<const char> text, SkSpan<const char> textWithSpaces,
+ SkSpan<const Cluster> clusters, size_t startPos, size_t endPos,
+ LineMetrics sizes)
+ : fBlocks(blocks)
+ , fText(text)
+ , fTextWithSpaces(textWithSpaces)
+ , fClusters(clusters)
+ //, fStartPos(startPos)
+ //, fEndPos(endPos)
+ , fLogical()
+ , fShift(0)
+ , fAdvance(advance)
+ , fOffset(offset)
+ , fEllipsis(nullptr)
+ , fSizes(sizes) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // Reorder visual runs
+ auto start = fClusters.begin();
+ auto end = fClusters.end() - 1;
+ size_t numRuns = end->run()->index() - start->run()->index() + 1;
+
+ // Get the logical order
+ std::vector<UBiDiLevel> runLevels;
+ for (auto run = start->run(); run <= end->run(); ++run) {
+ runLevels.emplace_back(run->fBidiLevel);
+ }
+
+ std::vector<int32_t> logicalOrder(numRuns);
+ ubidi_reorderVisual(runLevels.data(), SkToU32(numRuns), logicalOrder.data());
+
+ auto firstRun = start->run();
+ for (auto index : logicalOrder) {
+ fLogical.push_back(firstRun + index);
+ }
+
+ // TODO: use fStartPos and fEndPos really
+ // SkASSERT(fStartPos <= start->run()->size());
+ // SkASSERT(fEndPos <= end->run()->size());
+}
+
+TextLine::TextLine(TextLine&& other) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ this->fBlocks = other.fBlocks;
+ this->fText = other.fText;
+ this->fTextWithSpaces = other.fTextWithSpaces;
+ this->fLogical.reset();
+ this->fLogical = std::move(other.fLogical);
+ this->fShift = other.fShift;
+ this->fAdvance = other.fAdvance;
+ this->fOffset = other.fOffset;
+ this->fEllipsis = std::move(other.fEllipsis);
+ this->fSizes = other.sizes();
+ this->fClusters = other.fClusters;
+}
+
+void TextLine::paint(SkCanvas* textCanvas) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (this->empty()) {
+ return;
+ }
+
+ textCanvas->save();
+ textCanvas->translate(this->offset().fX, this->offset().fY);
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kBackground,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintBackground(textCanvas, text, style, offsetX);
+ });
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kShadow,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintShadow(textCanvas, text, style, offsetX);
+ });
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kForeground,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintText(textCanvas, text, style, offsetX);
+ });
+
+ this->iterateThroughStylesInTextOrder(
+ StyleType::kDecorations,
+ [textCanvas, this](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ return this->paintDecorations(textCanvas, text, style, offsetX);
+ });
+
+ textCanvas->restore();
+}
+
+void TextLine::format(TextAlign effectiveAlign, SkScalar maxWidth, bool notLastLine) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkScalar delta = maxWidth - this->width();
+ if (delta <= 0) {
+ return;
+ }
+
+ if (effectiveAlign == TextAlign::kJustify && notLastLine) {
+ this->justify(maxWidth);
+ } else if (effectiveAlign == TextAlign::kRight) {
+ this->shiftTo(delta);
+ } else if (effectiveAlign == TextAlign::kCenter) {
+ this->shiftTo(delta / 2);
+ }
+}
+
+void TextLine::scanStyles(StyleType style, const StyleVisitor& visitor) {
+ if (this->empty()) {
+ return;
+ }
+
+ this->iterateThroughStylesInTextOrder(
+ style, [this, visitor](SkSpan<const char> text, TextStyle style, SkScalar offsetX) {
+ visitor(text, style, offsetX);
+ return this->iterateThroughRuns(
+ text, offsetX, false,
+ [](Run*, int32_t, size_t, SkRect, SkScalar, bool) { return true; });
+ });
+}
+
+void TextLine::scanRuns(const RunVisitor& visitor) {
+ this->iterateThroughRuns(
+ fText, 0, false,
+ [visitor](Run* run, int32_t pos, size_t size, SkRect clip, SkScalar sc, bool b) {
+ visitor(run, pos, size, clip, sc, b);
+ return true;
+ });
+}
+
+SkScalar TextLine::paintText(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkPaint paint;
+ if (style.hasForeground()) {
+ paint = style.getForeground();
+ } else {
+ paint.setColor(style.getColor());
+ }
+
+ auto shiftDown = this->baseline();
+ return this->iterateThroughRuns(
+ text, offsetX, false,
+ [paint, canvas, shiftDown](Run* run, int32_t pos, size_t size, 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, SkSpan<const char> text,
+ const TextStyle& style, SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ return this->iterateThroughRuns(text, offsetX, false,
+ [canvas, style](Run* run, int32_t pos, size_t size, SkRect clip,
+ SkScalar shift, bool clippingNeeded) {
+ if (style.hasBackground()) {
+ canvas->drawRect(clip, style.getBackground());
+ }
+ return true;
+ });
+}
+
+SkScalar TextLine::paintShadow(SkCanvas* canvas, SkSpan<const char> text, const TextStyle& style,
+ SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ if (style.getShadowNumber() == 0) {
+ // Still need to calculate text advance
+ return iterateThroughRuns(
+ text, offsetX, false,
+ [](Run*, int32_t, size_t, SkRect, SkScalar, bool) { return true; });
+ }
+
+ SkScalar result = 0;
+ 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);
+ }
+
+ auto shiftDown = this->baseline();
+ result = this->iterateThroughRuns(
+ text, offsetX, false,
+ [canvas, shadow, paint, shiftDown](Run* run, size_t pos, size_t size, SkRect clip,
+ SkScalar shift, bool clippingNeeded) {
+ 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, SkSpan<const char> text,
+ const TextStyle& style, SkScalar offsetX) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ return this->iterateThroughRuns(
+ text, offsetX, false,
+ [this, canvas, style](Run* run, int32_t pos, size_t size, SkRect clip, SkScalar shift,
+ bool clippingNeeded) {
+ if (style.getDecoration() == TextDecoration::kNoDecoration) {
+ return true;
+ }
+
+ for (auto decoration : AllTextDecorations) {
+ if (style.getDecoration() && decoration == 0) {
+ continue;
+ }
+
+ SkScalar thickness = style.getDecorationThicknessMultiplier();
+ //
+ SkScalar position = 0;
+ switch (style.getDecoration()) {
+ case TextDecoration::kUnderline:
+ position = -run->ascent() + thickness;
+ break;
+ case TextDecoration::kOverline:
+ position = 0;
+ break;
+ case TextDecoration::kLineThrough: {
+ position = (run->descent() - run->ascent() - thickness) / 2;
+ break;
+ }
+ default:
+ // TODO: can we actually get here?
+ 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) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // 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, [&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster) {
+ if (cluster->isWhitespaces()) {
+ if (!whitespacePatch) {
+ whitespacePatch = true;
+ ++whitespacePatches;
+ }
+ } else {
+ whitespacePatch = false;
+ }
+ textLen += cluster->width();
+ return true;
+ });
+
+ if (whitespacePatches == 0) {
+ this->fShift = 0;
+ return;
+ }
+
+ SkScalar step = (maxWidth - textLen) / whitespacePatches;
+ SkScalar shift = 0;
+
+ // Spread the extra whitespaces
+ whitespacePatch = false;
+ this->iterateThroughClustersInGlyphsOrder(false, [&](const Cluster* cluster) {
+ if (cluster->isWhitespaces()) {
+ if (!whitespacePatch) {
+ shift += step;
+ whitespacePatch = true;
+ --whitespacePatches;
+ }
+ } else {
+ whitespacePatch = false;
+ }
+ cluster->shift(shift);
+ return true;
+ });
+
+ SkAssertResult(SkScalarNearlyEqual(shift, maxWidth - textLen));
+ SkASSERT(whitespacePatches == 0);
+ this->fShift = 0;
+ this->fAdvance.fX = maxWidth;
+}
+
+void TextLine::createEllipsis(SkScalar maxWidth, const SkString& ellipsis, bool) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ // 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, [this, &width, ellipsis, maxWidth](const Cluster* cluster) {
+ if (cluster->isWhitespaces()) {
+ width -= cluster->width();
+ return true;
+ }
+
+ // Shape the ellipsis
+ Run* cached = fEllipsisCache.find(cluster->run()->font());
+ if (cached == nullptr) {
+ cached = shapeEllipsis(ellipsis, cluster->run());
+ }
+ fEllipsis = skstd::make_unique<Run>(*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) {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ class ShapeHandler final : public SkShaper::RunHandler {
+ public:
+ explicit ShapeHandler(SkScalar lineHeight) : fRun(nullptr), fLineHeight(lineHeight) {}
+ 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(SkSpan<const char>(), info, fLineHeight, 0, 0));
+ return fRun->newRunBuffer();
+ }
+
+ void commitRunBuffer(const RunInfo& info) override {
+ fRun->fAdvance.fX = info.fAdvance.fX;
+ fRun->fAdvance.fY = fRun->descent() - fRun->ascent();
+ }
+
+ void commitLine() override {}
+
+ Run* fRun;
+ SkScalar fLineHeight;
+ };
+
+ ShapeHandler handler(run->lineHeight());
+ std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder();
+ SkASSERT_RELEASE(shaper != nullptr);
+ shaper->shape(ellipsis.c_str(), ellipsis.size(), run->font(), true,
+ std::numeric_limits<SkScalar>::max(), &handler);
+ handler.run()->fText = SkSpan<const char>(ellipsis.c_str(), ellipsis.size());
+ return handler.run();
+}
+
+SkRect TextLine::measureTextInsideOneRun(
+ SkSpan<const char> text, Run* run, size_t& pos, size_t& size, bool& clippingNeeded) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ SkASSERT(intersectedSize(run->text(), text) >= 0);
+
+ // Find [start:end] clusters for the text
+ bool found;
+ Cluster* start;
+ Cluster* end;
+ std::tie(found, start, end) = run->findLimitingClusters(text);
+ if (!found) {
+ SkASSERT(text.empty());
+ return SkRect::MakeEmpty();
+ }
+
+ 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)
+ bool needsClipping = (run->leftToRight() ? end : start) == clusters().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(text.begin());
+ auto rightCorrection = end->sizeFromChar(text.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,
+ const ClustersVisitor& visitor) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ for (size_t r = 0; r != fLogical.size(); ++r) {
+ auto& run = fLogical[reverse ? fLogical.size() - r - 1 : r];
+ // Walk through the clusters in the logical order (or reverse)
+ auto normalOrder = run->leftToRight() != reverse;
+ auto start = normalOrder ? run->clusters().begin() : run->clusters().end() - 1;
+ auto end = normalOrder ? run->clusters().end() : run->clusters().begin() - 1;
+ for (auto cluster = start; cluster != end; normalOrder ? ++cluster : --cluster) {
+ if (!this->contains(cluster)) {
+ continue;
+ }
+ if (!visitor(cluster)) {
+ return;
+ }
+ }
+ }
+}
+
+// Walk through the runs in the logical order
+SkScalar TextLine::iterateThroughRuns(SkSpan<const char> text,
+ SkScalar runOffset,
+ bool includeEmptyText,
+ const RunVisitor& visitor) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+
+ SkScalar width = 0;
+ for (auto& run : fLogical) {
+ // Only skip the text if it does not even touch the run
+ if (intersectedSize(run->text(), text) < 0) {
+ continue;
+ }
+
+ SkSpan<const char> intersect = intersected(run->text(), text);
+ if (run->text().empty() || intersect.empty()) {
+ continue;
+ }
+
+ size_t pos;
+ size_t size;
+ bool clippingNeeded;
+ SkRect clip = this->measureTextInsideOneRun(intersect, run, pos, size, clippingNeeded);
+ if (clip.height() == 0) {
+ continue;
+ }
+
+ auto shift = runOffset - clip.fLeft;
+ clip.offset(shift, 0);
+ if (clip.fRight > fAdvance.fX) {
+ clip.fRight = fAdvance.fX;
+ clippingNeeded = true; // Correct the clip in case there was an ellipsis
+ } else if (run == fLogical.back() && this->ellipsis() != nullptr) {
+ clippingNeeded = true; // To avoid trouble
+ }
+
+ if (!visitor(run, pos, size, clip, shift - run->positionX(0), clippingNeeded)) {
+ return width;
+ }
+
+ width += clip.width();
+ runOffset += clip.width();
+ }
+
+ if (this->ellipsis() != nullptr) {
+ auto ellipsis = this->ellipsis();
+ if (!visitor(ellipsis, 0, ellipsis->size(), ellipsis->clip(), ellipsis->clip().fLeft,
+ false)) {
+ return width;
+ }
+ width += ellipsis->clip().width();
+ }
+
+ return width;
+}
+
+void TextLine::iterateThroughStylesInTextOrder(StyleType styleType,
+ const StyleVisitor& visitor) const {
+ TRACE_EVENT0("skia", TRACE_FUNC);
+ const char* start = nullptr;
+ size_t size = 0;
+ TextStyle prevStyle;
+
+ SkScalar offsetX = 0;
+ for (auto& block : fBlocks) {
+ auto intersect = intersected(block.text(), this->trimmedText());
+ if (intersect.empty()) {
+ if (start == nullptr) {
+ // This style is not applicable to the line
+ continue;
+ } else {
+ // We have found all the good styles already
+ break;
+ }
+ }
+
+ auto style = block.style();
+ if (start != nullptr && style.matchOneAttribute(styleType, prevStyle)) {
+ size += intersect.size();
+ continue;
+ } else if (size == 0) {
+ // First time only
+ prevStyle = style;
+ size = intersect.size();
+ start = intersect.begin();
+ continue;
+ }
+
+ auto width = visitor(SkSpan<const char>(start, size), prevStyle, offsetX);
+ offsetX += width;
+
+ // Start all over again
+ prevStyle = style;
+ start = intersect.begin();
+ size = intersect.size();
+ }
+
+ // The very last style
+ auto width = visitor(SkSpan<const char>(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()));
+}
+} // namespace textlayout
+} // namespace skia