blob: 023b6d29dd0b000768b91e597a212c3970fb7cc1 [file] [log] [blame]
// Copyright 2021 Google LLC.
#include "experimental/sktext/include/Text.h"
#include <stack>
namespace skia {
namespace text {
std::unique_ptr<UnicodeText> Text::parse(SkSpan<uint16_t> utf16) {
auto unicodeText = std::unique_ptr<UnicodeText>(new UnicodeText());
unicodeText->fUnicode = std::move(SkUnicode::Make());
if (nullptr == unicodeText->fUnicode) {
return nullptr;
}
// Create utf8 -> utf16 conversion table
unicodeText->fText16 = std::u16string((char16_t*)utf16.data(), utf16.size());
unicodeText->fText8 = unicodeText->fUnicode->convertUtf16ToUtf8(unicodeText->fText16);
size_t utf16Index = 0;
unicodeText->fUTF16FromUTF8.push_back_n(unicodeText->fText8.size() + 1, utf16Index);
unicodeText->fUTF8FromUTF16.push_back_n(unicodeText->fText16.size() + 1, utf16Index);
// Fill out all code unit properties
unicodeText->fCodeUnitProperties.push_back_n(utf16.size() + 1, CodeUnitFlags::kNoCodeUnitFlag);
unicodeText->fUnicode->forEachCodepoint(unicodeText->fText8.c_str(), unicodeText->fText8.size(),
[&unicodeText, &utf16Index](SkUnichar unichar, int32_t start, int32_t end) {
for (auto i = start; i < end; ++i) {
unicodeText->fUTF16FromUTF8[i] = utf16Index;
}
unicodeText->fUTF8FromUTF16[utf16Index] = start;
++utf16Index;
});
unicodeText->fUTF16FromUTF8[unicodeText->fText8.size()] = unicodeText->fText16.size();
unicodeText->fUTF8FromUTF16[unicodeText->fText16.size()] = unicodeText->fText8.size();
// Get white spaces
// TODO: It's a bug. We need to operate on utf16 indexes everywhere
unicodeText->fUnicode->forEachCodepoint(unicodeText->fText8.c_str(), unicodeText->fText8.size(),
[&unicodeText](SkUnichar unichar, int32_t start, int32_t end) {
if (unicodeText->fUnicode->isWhitespace(unichar)) {
for (auto i = start; i < end; ++i) {
unicodeText->fCodeUnitProperties[i] |= CodeUnitFlags::kPartOfWhiteSpace;
}
}
});
// Get graphemes
unicodeText->fUnicode->forEachBreak((char16_t*)utf16.data(), utf16.size(), SkUnicode::BreakType::kGraphemes,
[&unicodeText](SkBreakIterator::Position pos, SkBreakIterator::Status){
unicodeText->fCodeUnitProperties[pos]|= CodeUnitFlags::kGraphemeStart;
});
// Get line breaks
unicodeText->fUnicode->forEachBreak((char16_t*)utf16.data(), utf16.size(), SkUnicode::BreakType::kLines,
[&unicodeText](SkBreakIterator::Position pos, SkBreakIterator::Status status) {
if (status == (SkBreakIterator::Status)SkUnicode::LineBreakType::kHardLineBreak) {
// Hard line breaks clears off all the other flags
// TODO: Treat \n as a formatting mark and do not pass it to SkShaper
unicodeText->fCodeUnitProperties[pos - 1] = CodeUnitFlags::kHardLineBreakBefore;
} else {
unicodeText->fCodeUnitProperties[pos] |= CodeUnitFlags::kSoftLineBreakBefore;
}
});
return std::move(unicodeText);
}
// Break text into pieces by font blocks and by formatting marks
// Formatting marks: \n (and possibly some other later)
std::unique_ptr<ShapedText> UnicodeText::shape(SkSpan<FontBlock> fontBlocks,
TextDirection textDirection) {
// TODO: Deal with all the cases
// A run can start
// 1. From the beginning of the text
// 2. From the beginning of the paragraph after newline
// 3. From the beginning of the font block
// 4. One shaping call can produce multiple runs (for which we should apply 1 - 3)
fRunGlyphStart = 0.0f;
fParagraphTextStart = 0;
std::vector<TextIndex> formattingMarks;
formattingMarks.emplace_back(fText8.size());
// Copy flags and find all the formatting marks
fShapedText = std::unique_ptr<ShapedText>(new ShapedText());
fShapedText->fGlyphUnitProperties.push_back_n(this->fCodeUnitProperties.size(), GlyphUnitFlags::kNoGlyphUnitFlag);
for (size_t i = 0; i < this->fCodeUnitProperties.size(); ++i) {
fShapedText->fGlyphUnitProperties[i] = (GlyphUnitFlags)this->fCodeUnitProperties[i];
if (this->isHardLineBreak(i)) {
formattingMarks.emplace_back(i);
}
}
// TODO: Deal with placeholders (have to be treated the same way to avoid all trouble with bidi)
// Break the text into a list of paragraphs by \n
// and shape each paragraph separately
// We start each paragraph from a new line
fRunGlyphStart = 0.0f;
FormattingFontIterator fontIter(fText8.size(), fontBlocks, SkSpan<TextIndex>(&formattingMarks[0], 1));
SkShaper::TrivialLanguageRunIterator langIter(fText8.c_str(), fText8.size());
std::unique_ptr<SkShaper::BiDiRunIterator> bidiIter(
SkShaper::MakeSkUnicodeBidiRunIterator(
this->fUnicode.get(), fText8.c_str(), fText8.size(), textDirection == TextDirection::kLtr ? 0 : 1));
std::unique_ptr<SkShaper::ScriptRunIterator> scriptIter(
SkShaper::MakeSkUnicodeHbScriptRunIterator(this->fUnicode.get(), fText8.c_str(), fText8.size()));
auto shaper = SkShaper::MakeShapeDontWrapOrReorder();
if (shaper == nullptr) {
// For instance, loadICU does not work. We have to stop the process
return nullptr;
}
shaper->shape(
fText8.c_str(), fText8.size(),
fontIter, *bidiIter, *scriptIter, langIter,
std::numeric_limits<SkScalar>::max(), this);
// Create a fake run for an empty text (to avoid all the checks)
if (fShapedText->fRuns.empty()) {
// We still need one empty run to keep the metrics
SkShaper::RunHandler::RunInfo emptyInfo {
this->createFont(fontBlocks.front()),
0,
SkVector::Make(0.0f, 0.0f),
0,
Range(0, 0)
};
fShapedText->fRuns.emplace_back(emptyInfo, 0, 0.0f);
fShapedText->fRuns.front().commit();
}
return std::move(fShapedText);
}
void UnicodeText::commitRunBuffer(const RunInfo&) {
fCurrentRun->commit();
// Convert utf8 range into utf16 range
fCurrentRun->convertUtf16Range([this](unsigned long index) {
return this->fUTF16FromUTF8[index + fParagraphTextStart];
});
// Convert all utf8 indexes into utf16 indexes (and also shift them to be on the entire text scale, too)
fCurrentRun->convertClusterIndexes([this](TextIndex clusterIndex) {
auto converted = this->fUTF16FromUTF8[clusterIndex + fParagraphTextStart];
fShapedText->fGlyphUnitProperties[converted] |= GlyphUnitFlags::kGlyphClusterStart;
if (this->hasProperty(converted, CodeUnitFlags::kGraphemeStart)) {
fShapedText->fGlyphUnitProperties[converted] |= GlyphUnitFlags::kGraphemeClusterStart;
}
return converted;
});
fShapedText->fRuns.emplace_back(std::move(*fCurrentRun));
fRunGlyphStart += fCurrentRun->width();
}
SkFont UnicodeText::createFont(const FontBlock& fontBlock) {
if (fontBlock.chain->count() == 0) {
return SkFont();
}
sk_sp<SkTypeface> typeface = fontBlock.chain->operator[](0);
SkFont font(std::move(typeface), fontBlock.chain->size());
font.setEdging(SkFont::Edging::kAntiAlias);
font.setHinting(SkFontHinting::kSlight);
font.setSubpixel(true);
return font;
}
std::unique_ptr<WrappedText> ShapedText::wrap(SkScalar width, SkScalar heightCurrentlyIgnored, SkUnicode* unicode) {
auto wrappedText = std::unique_ptr<WrappedText>(new WrappedText());
wrappedText->fRuns = this->fRuns;
wrappedText->fGlyphUnitProperties = this->fGlyphUnitProperties; // copy
// line : spaces : clusters
Stretch line;
Stretch spaces;
Stretch clusters;
Stretch cluster;
for (size_t runIndex = 0; runIndex < this->fRuns.size(); ++runIndex ) {
auto& run = this->fRuns[runIndex];
TextMetrics runMetrics(run.fFont);
if (!run.leftToRight()) {
cluster.setTextRange({ run.fUtf16Range.fStart, run.fUtf16Range.fEnd});
}
for (size_t glyphIndex = 0; glyphIndex < run.fPositions.size(); ++glyphIndex) {
auto textIndex = run.fClusters[glyphIndex];
if (cluster.isEmpty()) {
cluster = Stretch(GlyphPos(runIndex, glyphIndex), textIndex, runMetrics);
continue;
}
// The entire cluster belongs to a single run
SkASSERT(cluster.glyphStart().runIndex() == runIndex);
auto clusterWidth = run.calculateWidth(cluster.glyphStartIndex(), glyphIndex);
cluster.finish(glyphIndex, textIndex, clusterWidth);
auto isHardLineBreak = this->isHardLineBreak(cluster.textStart());
auto isSoftLineBreak = this->isSoftLineBreak(cluster.textStart());
auto isWhitespaces = this->isWhitespaces(cluster.textRange());
auto isEndOfText = run.leftToRight() ? textIndex == run.fUtf16Range.fEnd : textIndex == run.fUtf16Range.fStart;
if (isWhitespaces || isHardLineBreak) {
// This is the end of the word
if (!clusters.isEmpty()) {
line.moveTo(spaces);
line.moveTo(clusters);
spaces = clusters;
}
}
// line + spaces + clusters + cluster
if (isWhitespaces) {
// Whitespaces do not extend the line width
spaces.moveTo(cluster);
clusters = cluster;
continue;
} else if (isHardLineBreak) {
// Hard line break ends the line but does not extend the width
// Same goes for the end of the text
spaces.moveTo(cluster);
wrappedText->addLine(line, spaces, unicode, true);
line = spaces;
clusters = spaces;
continue;
} else if (!SkScalarIsFinite(width)) {
clusters.moveTo(cluster);
continue;
}
// Now let's find out if we can add the cluster to the line
if ((line.width() + spaces.width() + clusters.width() + cluster.width()) <= width) {
clusters.moveTo(cluster);
} else {
if (line.isEmpty()) {
if (spaces.isEmpty() && clusters.isEmpty()) {
// There is only this cluster and it's too long;
// we are drawing it anyway
line.moveTo(cluster);
} else {
// We break the only one word on the line by this cluster
line.moveTo(clusters);
}
} else {
// We move clusters + cluster on the next line
// TODO: Parametrise possible ways of breaking too long word
// (start it from a new line or squeeze the part of it on this line)
}
wrappedText->addLine(line, spaces, unicode, false);
line = spaces;
clusters.moveTo(cluster);
}
cluster = Stretch(GlyphPos(runIndex, glyphIndex), textIndex, runMetrics);
}
}
if (!clusters.isEmpty()) {
line.moveTo(spaces);
line.moveTo(clusters);
spaces = clusters;
} else if (wrappedText->fLines.empty()) {
// Empty text; we still need a line to avoid checking for empty lines every time
line.moveTo(cluster);
spaces.moveTo(cluster);
}
wrappedText->addLine(line, spaces, unicode, false);
wrappedText->fActualSize.fWidth = width;
return std::move(wrappedText);
}
void WrappedText::addLine(Stretch& stretch, Stretch& spaces, SkUnicode* unicode, bool hardLineBreak) {
// This is just chosen to catch the common/fast cases. Feel free to tweak.
constexpr int kPreallocCount = 4;
auto start = stretch.glyphStart().runIndex();
auto end = spaces.glyphEnd().runIndex();
auto numRuns = end - start + 1;
SkAutoSTArray<kPreallocCount, SkUnicode::BidiLevel> runLevels(numRuns);
size_t runLevelsIndex = 0;
for (auto runIndex = start; runIndex <= end; ++runIndex) {
auto& run = fRuns[runIndex];
runLevels[runLevelsIndex++] = run.bidiLevel();
}
SkASSERT(runLevelsIndex == numRuns);
SkAutoSTArray<kPreallocCount, int32_t> logicalOrder(numRuns);
SkSTArray<1, size_t, true> visualOrder;
unicode->reorderVisual(runLevels.data(), numRuns, logicalOrder.data());
auto firstRunIndex = start;
for (auto index : logicalOrder) {
visualOrder.push_back(firstRunIndex + index);
}
this->fLines.emplace_back(stretch, spaces, std::move(visualOrder), fActualSize.fHeight, hardLineBreak);
fActualSize.fHeight += stretch.textMetrics().height();
stretch.clean();
spaces.clean();
}
sk_sp<FormattedText> WrappedText::format(TextAlign textAlign, TextDirection textDirection) {
auto formattedText = sk_sp<FormattedText>(new FormattedText());
formattedText->fRuns = this->fRuns;
formattedText->fLines = this->fLines;
formattedText->fGlyphUnitProperties = this->fGlyphUnitProperties;
formattedText->fActualSize = this->fActualSize;
if (textAlign == TextAlign::kLeft) {
// Good by default
} else if (textAlign == TextAlign::kCenter) {
for (auto& line : formattedText->fLines) {
line.fHorizontalOffset = (this->fActualSize.width() - line.fTextWidth) / 2.0f;
}
formattedText->fActualSize.fWidth = this->fActualSize.fWidth;
} else {
// TODO: Implement all formatting features
}
return std::move(formattedText);
}
void FormattedText::visit(Visitor* visitor) const {
SkPoint offset = SkPoint::Make(0 , 0);
for (auto& line : this->fLines) {
offset.fX = 0;
visitor->onBeginLine(line.text());
for (auto index = 0; index < line.runsNumber(); ++index) {
auto runIndex = line.visualRun(index);
auto& run = this->fRuns[runIndex];
GlyphRange glyphRange(line.glyphRange(runIndex, run.size(), false /* excluding trailing spaces */));
// Update positions
SkAutoSTMalloc<256, SkPoint> positions(glyphRange.width() + 1);
SkPoint shift = SkPoint::Make(-run.fPositions[glyphRange.fStart].fX, line.baseline());
for (size_t i = glyphRange.fStart; i <= glyphRange.fEnd; ++i) {
positions[i - glyphRange.fStart] = run.fPositions[i] + shift + offset;
}
SkRect boundingRect = SkRect::MakeXYWH(shift.fX + offset.fX, offset.fY, run.fPositions[glyphRange.fEnd].fX , run.fTextMetrics.height());
visitor->onGlyphRun(run.fFont, run.getTextRange(glyphRange), boundingRect, glyphRange.width(), run.fGlyphs.data() + glyphRange.fStart, positions.data(), run.fOffsets.data() + glyphRange.fStart);
offset.fX += run.calculateWidth(glyphRange);
}
visitor->onEndLine(line.text());
offset.fY += line.height();
}
}
void FormattedText::visit(Visitor* visitor, SkSpan<TextIndex> textChunks) const {
// Decor blocks have to be sorted by text cannot intersect but can skip some parts of the text
// (in which case we use default text style from paragraph style)
// The edges of the decor blocks don't have to match glyph, grapheme or even unicode code point edges
// It's out responsibility to adjust them to some reasonable values
// [a:b) -> [c:d) where
// c is closest GG cluster edge to a from the left and d is closest GG cluster edge to b from the left
SkPoint offset = SkPoint::Make(0 , 0);
for (auto& line : this->fLines) {
offset.fX = 0;
visitor->onBeginLine(line.text());
for (auto index = 0; index < line.runsNumber(); ++index) {
auto runIndex = line.visualRun(index);
auto& run = this->fRuns[runIndex];
if (run.size() == 0) {
continue;
}
GlyphRange runGlyphRange(line.glyphRange(runIndex, run.size(), false /* excluding trailing spaces */));
SkPoint shift = SkPoint::Make(-run.fPositions[runGlyphRange.fStart].fX, line.baseline());
// The run edges are good (aligned to GGC)
// "ABCdef" -> "defCBA"
// "AB": red
// "Cd": green
// "ef": blue
// green[d] blue[ef] green [C] red [BA]
// chunks[text index] -> chunks [glyph index] -> walk through in glyph index order (the visual order)
run.forEachTextChunkInGlyphRange(textChunks, runGlyphRange, [&](TextRange textRange, GlyphRange glyphRange){
// Update positions & calculate the bounding rect
SkAutoSTMalloc<256, SkPoint> positions(glyphRange.width() + 1);
for (GlyphIndex i = glyphRange.fStart; i <= glyphRange.fEnd; ++i) {
positions[i - glyphRange.fStart] = run.fPositions[i] + shift + offset + SkPoint::Make(line.horizontalOffset(), 0.0f);
}
SkRect boundingRect = SkRect::MakeXYWH(positions[0].fX + line.horizontalOffset(), offset.fY, positions[glyphRange.width()].fX - positions[0].fX, run.fTextMetrics.height());
visitor->onGlyphRun(run.fFont, run.getTextRange(glyphRange), boundingRect, glyphRange.width(), run.fGlyphs.data() + glyphRange.fStart, positions.data(), run.fOffsets.data() + glyphRange.fStart);
});
offset.fX += run.calculateWidth(runGlyphRange);
}
visitor->onEndLine(line.text());
offset.fY += line.height();
}
}
// Find the element that includes textIndex
Position FormattedText::adjustedPosition(PositionType positionType, TextIndex textIndex) const {
Position position(positionType);
SkScalar shift = 0;
for (auto& line : fLines) {
position.fBoundaries.fTop = position.fBoundaries.fBottom;
position.fBoundaries.fBottom = line.verticalOffset() + line.height();
if (!line.text().contains(textIndex) && !line.whitespaces().contains(textIndex) ) {
continue;
}
shift = 0;
for (auto index = 0; index < line.runsNumber(); ++index) {
auto runIndex = line.visualRun(index);
auto& run = fRuns[runIndex];
GlyphRange runGlyphRange(line.glyphRange(runIndex, run.size(), true /* including trailing spaces */));
auto runWidth = run.calculateWidth(runGlyphRange);
if (!run.fUtf16Range.contains(textIndex)) {
shift += runWidth;
continue;
}
// Find the position left
GlyphIndex found = run.findGlyphIndexLeftOf(runGlyphRange, textIndex);
position.fLineIndex = lineIndex(&line);
position.fRun = &run;
position.fGlyphRange = GlyphRange(found, found == runGlyphRange.fEnd ? found : found + 1);
position.fTextRange = position.fRun->getTextRange(position.fGlyphRange);
this->adjustTextRange(&position);
position.fBoundaries.fLeft += shift + run.calculateWidth(runGlyphRange.fStart, position.fGlyphRange.fStart);
position.fBoundaries.fRight += shift + run.calculateWidth(runGlyphRange.fStart, position.fGlyphRange.fEnd);
return position;
}
// The cursor is not on the text anymore; position it after the last element
break;
}
position.fLineIndex = fLines.size() - 1;
position.fRun = this->visuallyLastRun(position.fLineIndex);
position.fGlyphRange = GlyphRange(position.fRun->size(), position.fRun->size());
position.fTextRange = position.fRun->getTextRange(position.fGlyphRange);
this->adjustTextRange(&position);
position.fBoundaries.fLeft = fLines.back().withWithTrailingSpaces();
position.fBoundaries.fRight = fLines.back().withWithTrailingSpaces();
return position;
}
Position FormattedText::adjustedPosition(PositionType positionType, SkPoint xy) const {
xy.fX = std::min(xy.fX, this->fActualSize.fWidth);
xy.fY = std::min(xy.fY, this->fActualSize.fHeight);
Position position(positionType);
position.fRun = this->visuallyLastRun(this->fLines.size() - 1);
for (auto& line : fLines) {
position.fBoundaries.fTop = position.fBoundaries.fBottom;
position.fBoundaries.fBottom = line.verticalOffset() + line.height();
position.fLineIndex = this->lineIndex(&line);
if (position.fBoundaries.fTop > xy.fY) {
// We are past the point vertically
break;
} else if (position.fBoundaries.fBottom <= xy.fY) {
// We haven't reached the point vertically yet
continue;
}
// We found the line that contains the point;
// let's walk through all its runs and find the element
SkScalar runOffsetInLine = 0;
for (auto index = 0; index < line.runsNumber(); ++index) {
auto runIndex = line.visualRun(index);
auto& run = fRuns[runIndex];
GlyphRange runGlyphRangeInLine = line.glyphRange(runIndex, run.size(), true /* including trailing space */);
SkScalar runWidthInLine = run.calculateWidth(runGlyphRangeInLine);
position.fRun = &run;
if (runOffsetInLine > xy.fX) {
// We are past the point horizontally
break;
} else if (runOffsetInLine + runWidthInLine < xy.fX) {
// We haven't reached the point horizontally yet
runOffsetInLine += runWidthInLine;
continue;
}
// We found the run that contains the point
// let's find the glyph
GlyphIndex found = runGlyphRangeInLine.fStart;
for (auto i = runGlyphRangeInLine.fStart; i <= runGlyphRangeInLine.fEnd; ++i) {
if (runOffsetInLine + run.calculateWidth(runGlyphRangeInLine.fStart, i) > xy.fX) {
break;
}
found = i;
}
position.fGlyphRange = GlyphRange(found, found == runGlyphRangeInLine.fEnd ? found : found + 1);
position.fTextRange = position.fRun->getTextRange(position.fGlyphRange);
this->adjustTextRange(&position);
position.fBoundaries.fLeft = runOffsetInLine + run.calculateWidth(runGlyphRangeInLine.fStart, position.fGlyphRange.fStart);
position.fBoundaries.fRight = runOffsetInLine + run.calculateWidth(runGlyphRangeInLine.fStart, position.fGlyphRange.fEnd);
return position;
}
// The cursor is not on the text anymore; position it after the last element of the last visual run of the current line
break;
}
position.fRun = this->visuallyLastRun(position.fLineIndex);
auto line = this->line(position.fLineIndex);
position.fGlyphRange.fStart =
position.fGlyphRange.fEnd = line->glyphRange(this->runIndex(position.fRun), position.fRun->size(), true /* including trailing spaces */).fEnd;
position.fTextRange = position.fRun->getTextRange(position.fGlyphRange);
this->adjustTextRange(&position);
position.fBoundaries.fLeft =
position.fBoundaries.fRight = line->withWithTrailingSpaces();
return position;
}
// Adjust the text positions to the position type
// (assuming for now that a grapheme cannot cross run edges; it's actually not true)
void FormattedText::adjustTextRange(Position* position) const {
// The textRange is aligned on a glyph cluster
if (position->fPositionType == PositionType::kGraphemeCluster) {
// Move left to the beginning of the run
while (position->fTextRange.fStart > position->fRun->fUtf8Range.begin() &&
!this->hasProperty(position->fTextRange.fStart, GlyphUnitFlags::kGraphemeStart)) {
--position->fTextRange.fStart;
}
// Update glyphRange, too
while (position->fRun->fClusters[position->fGlyphRange.fStart] > position->fTextRange.fStart) {
--position->fGlyphRange.fStart;
}
// Move right to the end of the run updating glyphRange, too
while (position->fTextRange.fEnd < position->fRun->fUtf8Range.end() &&
!this->hasProperty(position->fTextRange.fEnd, GlyphUnitFlags::kGraphemeStart)) {
++position->fTextRange.fEnd;
}
// Update glyphRange, too
while (position->fRun->fClusters[position->fGlyphRange.fEnd] < position->fTextRange.fEnd) {
++position->fGlyphRange.fEnd;
}
} else {
// TODO: Implement all the other position types
SkASSERT(false);
}
position->fTextRange = TextRange(position->fRun->fClusters[position->fGlyphRange.fStart], position->fRun->fClusters[position->fGlyphRange.fEnd]);
}
// The range is guaranteed on the same line
bool FormattedText::recalculateBoundaries(Position& position) const {
auto line = this->line(position.fLineIndex);
auto runIndex = this->runIndex(position.fRun);
GlyphIndex start = runIndex == line->glyphStart().runIndex() ? line->glyphStart().glyphIndex() : 0;
GlyphIndex end = runIndex == line->glyphTrailingEnd().runIndex() ? line->glyphTrailingEnd().glyphIndex() : position.fRun->fGlyphs.size();
SkASSERT (start <= position.fGlyphRange.fStart && end >= position.fGlyphRange.fEnd);
auto left = position.fRun->calculateWidth(start, position.fGlyphRange.fStart);
auto width = position.fRun->calculateWidth(position.fGlyphRange);
position.fBoundaries = SkRect::MakeXYWH(left, line->verticalOffset(), width, line->getMetrics().height());
return true;
}
const TextRun* FormattedText::visuallyPreviousRun(size_t lineIndex, const TextRun* run) const {
auto line = this->line(lineIndex);
auto runIndex = this->runIndex(run);
for (auto i = 0; i < line->runsNumber(); ++i) {
if (line->visualRun(i) == runIndex) {
if (i == 0) {
// Go to the previous line
if (lineIndex == 0) {
// This is the first line
return nullptr;
}
// Previous line, last visual run
line = this->line(--lineIndex);
i = line->runsNumber() - 1;
}
return &fRuns[line->visualRun(i)];
}
}
SkASSERT(false);
return nullptr;
}
const TextRun* FormattedText::visuallyNextRun(size_t lineIndex, const TextRun* run) const {
auto line = this->line(lineIndex);
auto runIndex = this->runIndex(run);
for (auto i = 0; i < line->runsNumber(); ++i) {
if (line->visualRun(i) == runIndex) {
if (i == line->runsNumber() - 1) {
// Go to the next line
if (lineIndex == fLines.size() - 1) {
// This is the last line
return nullptr;
}
// Next line, first visual run
line = this->line(++lineIndex);
i = 0;
}
return &fRuns[line->visualRun(i)];
}
}
SkASSERT(false);
return nullptr;
}
const TextRun* FormattedText::visuallyFirstRun(size_t lineIndex) const {
auto line = this->line(lineIndex);
return &fRuns[line->visualRun(0)];
}
const TextRun* FormattedText::visuallyLastRun(size_t lineIndex) const {
auto line = this->line(lineIndex);
return &fRuns[line->visualRun(line->runsNumber() - 1)];
}
Position FormattedText::previousElement(Position element) const {
if (element.fGlyphRange.fStart == 0) {
if (this->isVisuallyFirst(element.fLineIndex, element.fRun)) {
element.fGlyphRange = GlyphRange { 0, 0};
return element;
}
// We need to go to the visually previous run
// (skipping all the empty runs if there are any)
element.fRun = this->visuallyPreviousRun(element.fLineIndex, element.fRun);
// Set the glyph range after the last glyph
element.fGlyphRange = GlyphRange { element.fRun->fGlyphs.size(), element.fRun->fGlyphs.size()};
if (element.fRun == nullptr) {
return element;
}
}
auto& clusters = element.fRun->fClusters;
element.fGlyphRange = GlyphRange(element.fGlyphRange.fStart, element.fGlyphRange.fStart);
element.fTextRange = TextRange(clusters[element.fGlyphRange.fStart],
clusters[element.fGlyphRange.fStart]);
while (element.fGlyphRange.fStart > 0) {
// Shift left visually
element.fTextRange.fStart = clusters[--element.fGlyphRange.fStart];
if (element.fPositionType == PositionType::kGraphemeCluster) {
if (this->hasProperty(element.fTextRange.fStart, GlyphUnitFlags::kGraphemeStart)) {
break;
}
}
}
// Update the line
auto line = this->line(element.fLineIndex);
if (line->glyphStart().runIndex() == this->runIndex(element.fRun) &&
line->glyphStart().glyphIndex() > element.fGlyphRange.fStart) {
--element.fLineIndex;
}
// Either way we found us a grapheme cluster (just make sure of it)
SkASSERT(this->hasProperty(element.fTextRange.fStart, GlyphUnitFlags::kGraphemeStart));
return element;
}
Position FormattedText::nextElement(Position element) const {
if (element.fGlyphRange.fEnd == element.fRun->size()) {
// We need to go to the visually next run
// (skipping all the empty runs if there are any)
if (this->isVisuallyLast(element.fLineIndex, element.fRun)) {
element.fGlyphRange = GlyphRange { element.fRun->size(), element.fRun->size() };
return element;
}
element.fRun = this->visuallyNextRun(element.fLineIndex, element.fRun);
// Set the glyph range after the last glyph
element.fGlyphRange = GlyphRange { 0, 0};
if (element.fRun == nullptr) {
return element;
}
}
auto& clusters = element.fRun->fClusters;
element.fGlyphRange = GlyphRange(element.fGlyphRange.fEnd, element.fGlyphRange.fEnd);
element.fTextRange = TextRange(clusters[element.fGlyphRange.fEnd],
clusters[element.fGlyphRange.fEnd]);
while (element.fGlyphRange.fEnd < element.fRun->size()) {
// Shift left visually
element.fTextRange.fEnd = clusters[++element.fGlyphRange.fEnd];
if (element.fPositionType == PositionType::kGraphemeCluster) {
if (this->hasProperty(element.fTextRange.fEnd, GlyphUnitFlags::kGraphemeStart)) {
break;
}
}
}
// Update the line
auto line = this->line(element.fLineIndex);
if (line->glyphTrailingEnd().runIndex() == this->runIndex(element.fRun) &&
line->glyphTrailingEnd().glyphIndex() < element.fGlyphRange.fEnd) {
++element.fLineIndex;
}
// Either way we found us a grapheme cluster (just make sure of it)
SkASSERT(this->hasProperty(element.fTextRange.fEnd, GlyphUnitFlags::kGraphemeStart));
return element;
}
bool FormattedText::isFirstOnTheLine(Position element) const {
auto lineStart = this->line(element.fLineIndex)->glyphStart();
return lineStart.runIndex() == this->runIndex(element.fRun) &&
lineStart.glyphIndex() == element.fGlyphRange.fStart;
}
bool FormattedText::isLastOnTheLine(Position element) const {
auto lineEnd = this->line(element.fLineIndex)->glyphEnd();
return lineEnd.runIndex() == this->runIndex(element.fRun) &&
lineEnd.glyphIndex() == element.fGlyphRange.fStart;
}
Position FormattedText::firstElement(PositionType positionType) const {
Position beginningOfText(positionType);
// We need to go to the visually next run
// (skipping all the empty runs if there are any)
beginningOfText.fRun = this->visuallyFirstRun(0);
// Set the glyph range after the last glyph
beginningOfText.fGlyphRange = GlyphRange { 0, 0};
beginningOfText.fLineIndex = 0;
return beginningOfText;// this->nextElement(beginningOfText);
}
Position FormattedText::lastElement(PositionType positionType) const {
Position endOfText(positionType);
// We need to go to the visually next run
// (skipping all the empty runs if there are any)
endOfText.fRun = this->visuallyLastRun(fLines.size() - 1);
// Set the glyph range after the last glyph
endOfText.fGlyphRange = GlyphRange { endOfText.fRun->size(), endOfText.fRun->size() };
endOfText.fLineIndex = this->countLines() - 1;
return endOfText; // this->previousElement(endOfText);
}
bool FormattedText::isVisuallyFirst(size_t lineIndex, const TextRun* run) const {
auto firstLine = this->line(lineIndex);
return this->runIndex(run) == firstLine->visualRun(0);
}
bool FormattedText::isVisuallyLast(size_t lineIndex, const TextRun* run) const {
auto lastLine = this->line(lineIndex);
return this->runIndex(run) == lastLine->visualRun(lastLine->runsNumber() - 1);
}
} // namespace text
} // namespace skia