| // Copyright 2021 Google LLC. |
| #include "experimental/sktext/include/Text.h" |
| #include "experimental/sktext/src/LogicalRun.h" |
| #include "experimental/sktext/src/VisualRun.h" |
| #include <memory> |
| #include <stack> |
| |
| using namespace skia_private; |
| |
| namespace skia { |
| namespace text { |
| UnicodeText::UnicodeText(std::unique_ptr<SkUnicode> unicode, SkSpan<uint16_t> utf16) |
| : fText16(std::u16string((char16_t*)utf16.data(), utf16.size())) |
| , fUnicode(std::move(unicode)) { |
| initialize(utf16); |
| } |
| |
| UnicodeText::UnicodeText(std::unique_ptr<SkUnicode> unicode, const SkString& utf8) |
| : fUnicode(std::move(unicode)) { |
| fText16 = fUnicode->convertUtf8ToUtf16(utf8); |
| initialize(SkSpan<uint16_t>((uint16_t*)fText16.data(), fText16.size())); |
| } |
| |
| bool UnicodeText::isWhitespaces(TextRange range) const { |
| for (auto i = range.fStart; i < range.fEnd; ++i) { |
| if (!this->hasProperty(i, SkUnicode::CodeUnitFlags::kPartOfWhiteSpaceBreak)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void UnicodeText::initialize(SkSpan<uint16_t> utf16) { |
| if (!fUnicode) { |
| SkASSERT(fUnicode); |
| return; |
| } |
| |
| if (!fUnicode->computeCodeUnitFlags( |
| (char16_t*)utf16.data(), utf16.size(), false, &fCodeUnitProperties)) { |
| return; |
| } |
| } |
| |
| std::unique_ptr<FontResolvedText> UnicodeText::resolveFonts(SkSpan<FontBlock> blocks) { |
| |
| auto fontResolvedText = std::make_unique<FontResolvedText>(); |
| |
| TextRange adjustedBlock(0, 0); |
| TextIndex index = 0; |
| for (auto& block : blocks) { |
| |
| index += block.charCount; |
| adjustedBlock.fStart = adjustedBlock.fEnd; |
| adjustedBlock.fEnd = index; |
| if (adjustedBlock.fStart >= adjustedBlock.fEnd) { |
| // The last block adjustment went over the entire block |
| continue; |
| } |
| |
| // Move the end of the block to the right until it's on the grapheme edge |
| while (adjustedBlock.fEnd < this->fText16.size() && |
| !this->hasProperty(adjustedBlock.fEnd, SkUnicode::CodeUnitFlags::kGraphemeStart)) { |
| ++adjustedBlock.fEnd; |
| } |
| SkASSERT(block.type == BlockType::kFontChain); |
| fontResolvedText->resolveChain(this, adjustedBlock, *block.chain); |
| } |
| |
| std::sort(fontResolvedText->fResolvedFonts.begin(), fontResolvedText->fResolvedFonts.end(), |
| [](const ResolvedFontBlock& a, const ResolvedFontBlock& b) { |
| return a.textRange.fStart < b.textRange.fStart; |
| }); |
| /* |
| SkDebugf("Resolved:\n"); |
| for (auto& f : fontResolvedText->fResolvedFonts) { |
| SkDebugf("[%d:%d)\n", f.textRange.fStart, f.textRange.fEnd); |
| } |
| */ |
| return fontResolvedText; |
| } |
| |
| bool FontResolvedText::resolveChain(UnicodeText* unicodeText, TextRange textRange, const FontChain& fontChain) { |
| |
| std::deque<TextRange> unresolvedTexts; |
| unresolvedTexts.push_back(textRange); |
| for (auto fontIndex = 0; fontIndex < fontChain.count(); ++fontIndex) { |
| auto typeface = fontChain[fontIndex]; |
| |
| std::deque<TextRange> newUnresolvedTexts; |
| // Check all text range that have not been resolved yet |
| while (!unresolvedTexts.empty()) { |
| // Take the first unresolved |
| auto unresolvedText = unresolvedTexts.front(); |
| unresolvedTexts.pop_front(); |
| |
| // Resolve font for the entire grapheme |
| auto start = newUnresolvedTexts.size(); |
| unicodeText->forEachGrapheme(unresolvedText, [&](TextRange grapheme) { |
| auto count = typeface->textToGlyphs(unicodeText->getText16().data() + grapheme.fStart, grapheme.width() * 2, SkTextEncoding::kUTF16, nullptr, 0); |
| AutoTArray<SkGlyphID> glyphs(count); |
| typeface->textToGlyphs(unicodeText->getText16().data() + grapheme.fStart, grapheme.width() * 2, SkTextEncoding::kUTF16, glyphs.data(), count); |
| for (auto i = 0; i < count; ++i) { |
| if (glyphs[i] == 0) { |
| if (newUnresolvedTexts.empty() || newUnresolvedTexts.back().fEnd < grapheme.fStart) { |
| // It's a new unresolved block |
| newUnresolvedTexts.push_back(grapheme); |
| } else { |
| // Let's extend the last unresolved block |
| newUnresolvedTexts.back().fEnd = grapheme.fEnd; |
| } |
| break; |
| } |
| } |
| }); |
| // Let's fill the resolved blocks with the current font |
| TextRange resolvedText(unresolvedText.fStart, unresolvedText.fStart); |
| for (auto newUnresolvedText : newUnresolvedTexts) { |
| if (start > 0) { |
| --start; |
| continue; |
| } |
| resolvedText.fEnd = newUnresolvedText.fStart; |
| if (resolvedText.width() > 0) { |
| // Add another resolved block |
| fResolvedFonts.emplace_back(resolvedText, typeface, fontChain.fontSize(), SkFontStyle::Normal()); |
| } |
| resolvedText.fStart = newUnresolvedText.fEnd; |
| } |
| resolvedText.fEnd = unresolvedText.fEnd; |
| if (resolvedText.width() > 0) { |
| // Add the last resolved block |
| fResolvedFonts.emplace_back(resolvedText, typeface, fontChain.fontSize(), SkFontStyle::Normal()); |
| } |
| } |
| |
| // Try the next font in chain |
| SkASSERT(unresolvedTexts.empty()); |
| unresolvedTexts = std::move(newUnresolvedTexts); |
| } |
| |
| return unresolvedTexts.empty(); |
| } |
| |
| // Font iterator that finds all formatting marks |
| // and breaks runs on them (so we can select and interpret them later) |
| class FormattingFontIterator final : public SkShaper::FontRunIterator { |
| public: |
| FormattingFontIterator(TextIndex textCount, |
| SkSpan<ResolvedFontBlock> fontBlocks, |
| SkSpan<TextIndex> marks) |
| : fTextCount(textCount) |
| , fFontBlocks(fontBlocks) |
| , fFormattingMarks(marks) |
| , fCurrentBlock(fontBlocks.begin()) |
| , fCurrentMark(marks.begin()) |
| , fCurrentFontIndex(fCurrentBlock->textRange.fEnd) { |
| fCurrentFont = this->createFont(*fCurrentBlock); |
| } |
| void consume() override { |
| SkASSERT(fCurrentBlock < fFontBlocks.end()); |
| SkASSERT(fCurrentMark < fFormattingMarks.end()); |
| if (fCurrentFontIndex > *fCurrentMark) { |
| ++fCurrentMark; |
| return; |
| } |
| if (fCurrentFontIndex == *fCurrentMark) { |
| ++fCurrentMark; |
| } |
| ++fCurrentBlock; |
| if (fCurrentBlock < fFontBlocks.end()) { |
| fCurrentFontIndex = fCurrentBlock->textRange.fEnd; |
| fCurrentFont = this->createFont(*fCurrentBlock); |
| } |
| } |
| size_t endOfCurrentRun() const override { |
| SkASSERT(fCurrentMark != fFormattingMarks.end() || fCurrentBlock != fFontBlocks.end()); |
| if (fCurrentMark == fFormattingMarks.end()) { |
| return fCurrentFontIndex; |
| } else if (fCurrentBlock == fFontBlocks.end()) { |
| return *fCurrentMark; |
| } else { |
| return std::min(fCurrentFontIndex, *fCurrentMark); |
| } |
| } |
| bool atEnd() const override { |
| return (fCurrentBlock == fFontBlocks.end() || fCurrentFontIndex == fTextCount) && |
| (fCurrentMark == fFormattingMarks.end() || *fCurrentMark == fTextCount); |
| } |
| const SkFont& currentFont() const override { return fCurrentFont; } |
| SkFont createFont(const ResolvedFontBlock& resolvedFont) const { |
| SkFont font(resolvedFont.typeface, resolvedFont.size); |
| font.setEdging(SkFont::Edging::kAntiAlias); |
| font.setHinting(SkFontHinting::kSlight); |
| font.setSubpixel(true); |
| return font; |
| } |
| private: |
| TextIndex const fTextCount; |
| SkSpan<ResolvedFontBlock> fFontBlocks; |
| SkSpan<TextIndex> fFormattingMarks; |
| ResolvedFontBlock* fCurrentBlock; |
| TextIndex* fCurrentMark; |
| TextIndex fCurrentFontIndex; |
| SkFont fCurrentFont; |
| }; |
| |
| std::unique_ptr<ShapedText> FontResolvedText::shape(UnicodeText* unicodeText, |
| TextDirection textDirection) { |
| // Get utf8 <-> utf16 conversion tables. |
| // We need to pass to SkShaper indices in utf8 and then convert them back to utf16 for SkText |
| auto text16 = unicodeText->getText16(); |
| auto text8 = SkUnicode::convertUtf16ToUtf8(std::u16string(text16.data(), text16.size())); |
| size_t utf16Index = 0; |
| SkTArray<size_t, true> UTF16FromUTF8; |
| SkTArray<size_t, true> UTF8FromUTF16; |
| UTF16FromUTF8.push_back_n(text8.size() + 1, utf16Index); |
| UTF8FromUTF16.push_back_n(text16.size() + 1, utf16Index); |
| unicodeText->getUnicode()->forEachCodepoint(text8.c_str(), text8.size(), |
| [&](SkUnichar unichar, int32_t start, int32_t end, int32_t count) { |
| // utf8 index group of 1, 2 or 3 can be represented with one utf16 index group |
| for (auto i = start; i < end; ++i) { |
| UTF16FromUTF8[i] = utf16Index; |
| } |
| // utf16 index group of 1 or 2 can refer to the same group of utf8 indices |
| for (; count != 0; --count) { |
| UTF8FromUTF16[utf16Index++] = start; |
| } |
| }); |
| UTF16FromUTF8[text8.size()] = text16.size(); |
| UTF8FromUTF16[text16.size()] = text8.size(); |
| // Break text into pieces by font blocks and by formatting marks |
| // Formatting marks: \n (and possibly some other later) |
| std::vector<size_t> formattingMarks; |
| for (size_t i = 0; i < text16.size(); ++i) { |
| if (unicodeText->isHardLineBreak(i)) { |
| formattingMarks.emplace_back(UTF8FromUTF16[i]); |
| formattingMarks.emplace_back(UTF8FromUTF16[i + 1]); |
| ++i; |
| } |
| } |
| formattingMarks.emplace_back(text8.size()/* UTF8FromUTF16[text16.size() */); |
| // Convert fontBlocks from utf16 to utf8 |
| SkTArray<ResolvedFontBlock, true> fontBlocks8; |
| for (auto& fb : fResolvedFonts) { |
| TextRange text8(UTF8FromUTF16[fb.textRange.fStart], UTF8FromUTF16[fb.textRange.fEnd]); |
| fontBlocks8.emplace_back(text8, fb.typeface, fb.size, fb.style); |
| } |
| auto shapedText = std::make_unique<ShapedText>(); |
| // Shape the text |
| FormattingFontIterator fontIter(text8.size(), |
| SkSpan<ResolvedFontBlock>(fontBlocks8.data(), fontBlocks8.size()), |
| SkSpan<TextIndex>(&formattingMarks[0], formattingMarks.size())); |
| SkShaper::TrivialLanguageRunIterator langIter(text8.c_str(), text8.size()); |
| std::unique_ptr<SkShaper::BiDiRunIterator> bidiIter( |
| SkShaper::MakeSkUnicodeBidiRunIterator( |
| unicodeText->getUnicode(), text8.c_str(), text8.size(), textDirection == TextDirection::kLtr ? 0 : 1)); |
| std::unique_ptr<SkShaper::ScriptRunIterator> scriptIter( |
| SkShaper::MakeSkUnicodeHbScriptRunIterator(text8.c_str(), text8.size())); |
| auto shaper = SkShaper::MakeShapeDontWrapOrReorder(unicodeText->getUnicode()->copy()); |
| if (shaper == nullptr) { |
| // For instance, loadICU does not work. We have to stop the process |
| return nullptr; |
| } |
| shaper->shape( |
| text8.c_str(), text8.size(), |
| fontIter, *bidiIter, *scriptIter, langIter, |
| std::numeric_limits<SkScalar>::max(), shapedText.get()); |
| if (shapedText->fLogicalRuns.empty()) { |
| // Create a fake run for an empty text (to avoid all the checks) |
| SkShaper::RunHandler::RunInfo emptyInfo { |
| fontIter.createFont(fResolvedFonts.front()), |
| 0, |
| SkVector::Make(0.0f, 0.0f), |
| 0, |
| SkShaper::RunHandler::Range(0, 0) |
| }; |
| shapedText->fLogicalRuns.emplace_back(emptyInfo, 0, 0.0f); |
| shapedText->fLogicalRuns.front().commit(); |
| } |
| // Fill out all code unit properties |
| for (auto& logicalRun : shapedText->fLogicalRuns) { |
| // Convert utf8 range into utf16 range |
| logicalRun.convertUtf16Range([&](unsigned long index8) { |
| return UTF16FromUTF8[index8]; |
| }); |
| // Convert all utf8 indexes into utf16 indexes (and also shift them to be on the entire text scale, too) |
| logicalRun.convertClusterIndexes([&](TextIndex clusterIndex8) { |
| return UTF16FromUTF8[clusterIndex8]; |
| }); |
| // Detect and mark line break runs |
| if (logicalRun.getTextRange().width() == 1 && |
| logicalRun.size() == 1 && |
| unicodeText->isHardLineBreak(logicalRun.getTextRange().fStart)) { |
| logicalRun.setRunType(LogicalRunType::kLineBreak); |
| } |
| } |
| return shapedText; |
| } |
| |
| // TODO: Implement the vertical restriction (height) and add ellipsis |
| std::unique_ptr<WrappedText> ShapedText::wrap(UnicodeText* unicodeText, float width, float height) { |
| auto wrappedText = std::unique_ptr<WrappedText>(new WrappedText()); |
| // line + spaces + clusters |
| Stretch line; |
| Stretch spaces; |
| Stretch clusters; |
| Stretch cluster; |
| for (size_t runIndex = 0; runIndex < this->fLogicalRuns.size(); ++runIndex ) { |
| auto& run = this->fLogicalRuns[runIndex]; |
| if (run.getRunType() == LogicalRunType::kLineBreak) { |
| // This is the end of the word, the end of the line |
| if (!clusters.isEmpty()) { |
| line.moveTo(spaces); |
| line.moveTo(clusters); |
| spaces = clusters; |
| } |
| this->addLine(wrappedText.get(), unicodeText->getUnicode(), line, spaces, true); |
| line = spaces; |
| clusters = spaces; |
| continue; |
| } |
| TextMetrics runMetrics(run.fFont); |
| |
| // Let's wrap the text |
| GlyphRange clusterGlyphs; |
| DirTextRange clusterText(EMPTY_RANGE, run.leftToRight()); |
| for (size_t glyphIndex = 0; glyphIndex < run.fPositions.size(); ++glyphIndex) { |
| auto textIndex = run.fClusters[glyphIndex]; |
| |
| if (clusterText == EMPTY_RANGE) { |
| // The beginning of a new line (or the first one) |
| clusterText = DirTextRange(textIndex, textIndex, run.leftToRight()); |
| clusterGlyphs = GlyphRange(glyphIndex, glyphIndex); |
| |
| Stretch empty(GlyphPos(runIndex, glyphIndex), textIndex, runMetrics); |
| line = empty; |
| spaces = empty; |
| clusters = empty; |
| continue; |
| } |
| |
| if (textIndex == clusterText.fStart) { |
| // Skip until the next cluster |
| continue; |
| } |
| |
| // Finish the cluster (notice that it belongs to a single run) |
| clusterText.fStart = clusterText.fEnd; |
| clusterText.fEnd = textIndex; |
| clusterGlyphs.fStart = clusterGlyphs.fEnd; |
| clusterGlyphs.fEnd = glyphIndex; |
| cluster = Stretch(runIndex, clusterGlyphs, clusterText.normalized(), run.calculateWidth(clusterGlyphs), runMetrics); |
| |
| auto isWhitespaces = unicodeText->isWhitespaces(cluster.textRange()); |
| // line + spaces + clusters + cluster |
| if (isWhitespaces) { |
| // This is the end of the word |
| if (!clusters.isEmpty()) { |
| line.moveTo(spaces); |
| line.moveTo(clusters); |
| spaces = clusters; |
| } |
| spaces.moveTo(cluster); |
| clusters = cluster; |
| // Whitespaces do not extend the line width so no wrapping |
| continue; |
| } else if (!SkScalarIsFinite(width)) { |
| // No wrapping - the endless line |
| clusters.moveTo(cluster); |
| continue; |
| } |
| // Now let's find out if we can add the cluster to the line |
| auto currentWidth = line.width() + spaces.width() + clusters.width() + cluster.width(); |
| if (currentWidth > width) { |
| // Finally, the wrapping case |
| 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) |
| } |
| this->addLine(wrappedText.get(), unicodeText->getUnicode(), line, spaces, false); |
| line = spaces; |
| } |
| clusters.moveTo(cluster); |
| |
| clusterGlyphs.fStart = clusterGlyphs.fEnd; |
| clusterText.fStart = clusterText.fEnd; |
| } |
| } |
| |
| // Deal with the last line |
| if (!clusters.isEmpty()) { |
| line.moveTo(spaces); |
| line.moveTo(clusters); |
| spaces = clusters; |
| } else if (wrappedText->fVisualLines.empty()) { |
| // Empty text; we still need a line to avoid checking for empty lines every time |
| line.moveTo(cluster); |
| spaces.moveTo(cluster); |
| } |
| this->addLine(wrappedText.get(), unicodeText->getUnicode(), line, spaces, false); |
| wrappedText->fActualSize.fWidth = width; |
| return wrappedText; |
| } |
| |
| SkTArray<int32_t> ShapedText::getVisualOrder(SkUnicode* unicode, RunIndex startRun, RunIndex endRun) { |
| auto numRuns = endRun - startRun + 1; |
| SkTArray<int32_t> results; |
| results.push_back_n(numRuns); |
| if (numRuns == 0) { |
| return results; |
| } |
| SkTArray<SkUnicode::BidiLevel> runLevels; |
| runLevels.push_back_n(numRuns); |
| size_t runLevelsIndex = 0; |
| for (RunIndex runIndex = startRun; runIndex <= endRun; ++runIndex) { |
| runLevels[runLevelsIndex++] = fLogicalRuns[runIndex].bidiLevel(); |
| } |
| SkASSERT(runLevelsIndex == numRuns); |
| unicode->reorderVisual(runLevels.data(), numRuns, results.data()); |
| return results; |
| } |
| |
| // TODO: Fill line fOffset.fY |
| void ShapedText::addLine(WrappedText* wrappedText, SkUnicode* unicode, Stretch& stretch, Stretch& spaces, bool hardLineBreak) { |
| auto spacesStart = spaces.glyphStart(); |
| Stretch lineStretch = stretch; |
| lineStretch.moveTo(spaces); |
| auto startRun = lineStretch.glyphStart().runIndex(); |
| auto endRun = lineStretch.glyphEnd().runIndex(); |
| // Reorder and cut (if needed) runs so they fit the line |
| auto visualOrder = this->getVisualOrder(unicode, startRun, endRun); |
| // Walk through the line's runs in visual order |
| auto firstRunIndex = startRun; |
| auto runStart = wrappedText->fVisualRuns.size(); |
| SkScalar runOffsetInLine = 0.0f; |
| for (auto visualIndex : visualOrder) { |
| auto& logicalRun = fLogicalRuns[firstRunIndex + visualIndex]; |
| if (logicalRun.getRunType() == LogicalRunType::kLineBreak) { |
| SkASSERT(false); |
| } |
| bool isFirstRun = startRun == (firstRunIndex + visualIndex); |
| bool isLastRun = endRun == (firstRunIndex + visualIndex); |
| bool isSpaceRun = spacesStart.runIndex() == (firstRunIndex + visualIndex); |
| auto glyphStart = isFirstRun ? lineStretch.glyphStart().glyphIndex() : 0; |
| auto glyphEnd = isLastRun ? lineStretch.glyphEnd().glyphIndex() : logicalRun.size(); |
| auto glyphSize = glyphEnd - glyphStart; |
| auto glyphSpaces = isSpaceRun ? spacesStart.glyphIndex() : glyphEnd; |
| if (glyphSpaces > glyphStart) { |
| auto textStart = isFirstRun ? lineStretch.textRange().fStart : logicalRun.fUtf16Range.fStart; |
| auto textEnd = isLastRun ? lineStretch.textRange().fEnd : logicalRun.fUtf16Range.fEnd; |
| wrappedText->fVisualRuns.emplace_back(TextRange(textStart, textEnd), |
| glyphSpaces - glyphStart, |
| logicalRun.fFont, |
| lineStretch.textMetrics().baseline(), |
| SkPoint::Make(runOffsetInLine, wrappedText->fActualSize.fHeight), |
| logicalRun.leftToRight(), |
| SkSpan<SkPoint>(&logicalRun.fPositions[glyphStart], glyphSize + 1), |
| SkSpan<SkGlyphID>(&logicalRun.fGlyphs[glyphStart], glyphSize), |
| SkSpan<uint32_t>((uint32_t*)&logicalRun.fClusters[glyphStart], glyphSize + 1)); |
| } |
| runOffsetInLine += logicalRun.calculateWidth(glyphStart, glyphEnd); |
| } |
| auto runRange = wrappedText->fVisualRuns.size() == runStart |
| ? SkSpan<VisualRun>(nullptr, 0) |
| : SkSpan<VisualRun>(&wrappedText->fVisualRuns[runStart], wrappedText->fVisualRuns.size() - runStart); |
| wrappedText->fVisualLines.emplace_back(lineStretch.textRange(), hardLineBreak, wrappedText->fActualSize.fHeight, runRange); |
| wrappedText->fActualSize.fHeight += lineStretch.textMetrics().height(); |
| wrappedText->fActualSize.fWidth = std::max(wrappedText->fActualSize.fWidth, lineStretch.width()); |
| stretch.clean(); |
| spaces.clean(); |
| } |
| |
| void WrappedText::format(TextAlign textAlign, TextDirection textDirection) { |
| if (fAligned == textAlign) { |
| return; |
| } |
| SkScalar verticalOffset = 0.0f; |
| for (auto& line : this->fVisualLines) { |
| if (textAlign == TextAlign::kLeft) { |
| // Good by default |
| } else if (textAlign == TextAlign::kCenter) { |
| line.fOffset.fX = (this->fActualSize.width() - line.fActualWidth) / 2.0f; |
| } else { |
| // TODO: Implement all formatting features |
| } |
| line.fOffset.fY = verticalOffset; |
| verticalOffset += line.fTextMetrics.height(); |
| } |
| } |
| |
| void WrappedText::visit(Visitor* visitor) const { |
| size_t lineIndex = 0; |
| SkScalar verticalOffset = 0.0f; |
| for (auto& line : fVisualLines) { |
| visitor->onBeginLine(lineIndex, line.text(), line.isHardBreak(), SkRect::MakeXYWH(0, verticalOffset, line.fActualWidth, line.fTextMetrics.height())); |
| // Select the runs that are on the line |
| size_t glyphCount = 0ul; |
| for (auto& run : line.fRuns) { |
| auto diff = line.fTextMetrics.above() - run.fTextMetrics.above(); |
| SkRect boundingRect = SkRect::MakeXYWH(line.fOffset.fX + run.fPositions[0].fX, line.fOffset.fY + diff, run.width(), run.fTextMetrics.height()); |
| visitor->onGlyphRun(run.fFont, run.dirTextRange(), boundingRect, run.trailingSpacesStart(), |
| run.size(), run.fGlyphs.data(), run.fPositions.data(), run.fClusters.data()); |
| glyphCount += run.size(); |
| } |
| visitor->onEndLine(lineIndex, line.text(), line.trailingSpaces(), glyphCount); |
| verticalOffset += line.fTextMetrics.height(); |
| ++lineIndex; |
| } |
| } |
| |
| std::vector<TextIndex> WrappedText::chunksToBlocks(SkSpan<size_t> chunks) { |
| std::vector<TextIndex> blocks; |
| blocks.reserve(chunks.size() + 1); |
| TextIndex index = 0; |
| for (auto chunk : chunks) { |
| blocks.emplace_back(index); |
| index += chunk; |
| } |
| blocks.emplace_back(index); |
| return blocks; |
| } |
| |
| SkSpan<TextIndex> WrappedText::limitBlocks(TextRange textRange, SkSpan<TextIndex> blocks) { |
| TextRange limited = EMPTY_RANGE; |
| for (auto i = 0ul; i < blocks.size(); ++i) { |
| auto block = blocks[i]; |
| if (textRange.fEnd < block) { |
| continue; |
| } else if (textRange.fStart >= block) { |
| break; |
| } else if (limited.fStart == EMPTY_INDEX) { |
| limited.fStart = i; |
| } |
| limited.fEnd = i; |
| } |
| |
| return SkSpan<TextIndex>(&blocks[textRange.fStart], textRange.width()); |
| } |
| |
| void WrappedText::visit(UnicodeText* unicodeText, Visitor* visitor, PositionType positionType, SkSpan<size_t> chunks) 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 |
| auto textBlocks = WrappedText::chunksToBlocks(chunks); |
| SkScalar verticalOffset = 0.0f; |
| LineIndex lineIndex = 0ul; |
| size_t glyphCount = 0ul; |
| for (auto& line : fVisualLines) { |
| visitor->onBeginLine(lineIndex, line.text(), line.isHardBreak(), SkRect::MakeXYWH(0, verticalOffset, line.fActualWidth, line.fTextMetrics.height())); |
| RunIndex runIndex = 0ul; |
| auto lineBlocks = WrappedText::limitBlocks(line.fText, SkSpan<TextIndex>(textBlocks.data(), textBlocks.size())); |
| for (auto& run : fVisualRuns) { |
| run.forEachTextBlockInGlyphRange(lineBlocks, [&](DirTextRange dirTextRange) { |
| GlyphRange glyphRange = this->textToGlyphs(unicodeText, positionType, runIndex, dirTextRange); |
| auto diff = line.fTextMetrics.above() - run.fTextMetrics.above(); |
| SkRect boundingRect = |
| SkRect::MakeXYWH(line.fOffset.fX + run.fPositions[glyphRange.fStart].fX, |
| line.fOffset.fY + diff, |
| run.calculateWidth(glyphRange), |
| run.fTextMetrics.height()); |
| visitor->onGlyphRun(run.fFont, |
| dirTextRange, |
| boundingRect, |
| run.trailingSpacesStart(), |
| glyphRange.width(), |
| &run.fGlyphs[glyphRange.fStart], |
| &run.fPositions[glyphRange.fStart], |
| &run.fClusters[glyphRange.fStart]); |
| }); |
| ++runIndex; |
| glyphCount += run.size(); |
| } |
| visitor->onEndLine(lineIndex, line.text(), line.trailingSpaces(), glyphCount); |
| verticalOffset += line.fTextMetrics.height(); |
| ++lineIndex; |
| } |
| } |
| |
| // TODO: Implement more effective search |
| GlyphRange WrappedText::textToGlyphs(UnicodeText* unicodeText, PositionType positionType, RunIndex runIndex, DirTextRange dirTextRange) const { |
| SkASSERT(runIndex < fVisualRuns.size()); |
| auto& run = fVisualRuns[runIndex]; |
| SkASSERT(run.fDirTextRange.contains(dirTextRange)); |
| GlyphRange glyphRange(0, run.size()); |
| for (GlyphIndex glyph = 0; glyph < run.size(); ++glyph) { |
| auto textIndex = run.fClusters[glyph]; |
| if (positionType == PositionType::kGraphemeCluster && |
| unicodeText->hasProperty(textIndex, SkUnicode::CodeUnitFlags::kGraphemeStart)) { |
| if (dirTextRange.after(textIndex)) { |
| glyphRange.fStart = glyph; |
| } else if (dirTextRange.before(textIndex)) { |
| glyphRange.fEnd = glyph; |
| } else { |
| return glyphRange; |
| } |
| } |
| } |
| SkASSERT(false); |
| return glyphRange; |
| } |
| |
| std::unique_ptr<SelectableText> WrappedText::prepareToEdit(UnicodeText* unicodeText) const { |
| auto selectableText = std::make_unique<SelectableText>(); |
| this->visit(selectableText.get()); |
| selectableText->fGlyphUnitProperties.push_back_n(unicodeText->getText16().size() + 1, GlyphUnitFlags::kNoGlyphUnitFlag); |
| for (auto index = 0; index < unicodeText->getText16().size(); ++index) { |
| if (unicodeText->hasProperty(index, SkUnicode::CodeUnitFlags::kHardLineBreakBefore)) { |
| selectableText->fGlyphUnitProperties[index] = GlyphUnitFlags::kGraphemeClusterStart; |
| } |
| } |
| for (const auto& run : fVisualRuns) { |
| for (auto& cluster : run.fClusters) { |
| if (unicodeText->hasProperty(cluster, SkUnicode::CodeUnitFlags::kGraphemeStart)) { |
| selectableText->fGlyphUnitProperties[cluster] = GlyphUnitFlags::kGraphemeClusterStart; |
| } |
| } |
| } |
| return selectableText; |
| } |
| |
| void SelectableText::onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) { |
| SkASSERT(fBoxLines.size() == index); |
| fBoxLines.emplace_back(index, lineText, hardBreak, bounds); |
| } |
| |
| void SelectableText::onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) { |
| auto& line = fBoxLines.back(); |
| line.fTextEnd = trailingSpaces.fStart; |
| line.fTrailingSpacesEnd = trailingSpaces.fEnd; |
| SkASSERT(line.fTextByGlyph.size() == glyphCount); |
| line.fBoxGlyphs.emplace_back(SkRect::MakeXYWH(line.fBounds.fRight, line.fBounds.fTop, 0.0f, line.fBounds.height())); |
| if (line.fTextByGlyph.empty()) { |
| // Let's create an empty fake box to avoid all the checks |
| line.fTextByGlyph.emplace_back(lineText.fEnd); |
| } |
| line.fTextByGlyph.emplace_back(lineText.fEnd); |
| } |
| |
| void SelectableText::onGlyphRun(const SkFont& font, |
| DirTextRange dirTextRange, |
| SkRect bounds, |
| TextIndex trailingSpaces, |
| size_t glyphCount, |
| const uint16_t glyphs[], |
| const SkPoint positions[], |
| const TextIndex clusters[]) { |
| auto& line = fBoxLines.back(); |
| auto start = line.fTextByGlyph.size(); |
| line.fBoxGlyphs.push_back_n(glyphCount); |
| line.fTextByGlyph.push_back_n(glyphCount); |
| for (auto i = 0; i < glyphCount; ++i) { |
| auto pos = positions[i]; |
| auto pos1 = positions[i + 1]; |
| line.fBoxGlyphs[start + i] = SkRect::MakeXYWH(pos.fX, bounds.fTop, pos1.fX - pos.fX, bounds.height()); |
| line.fTextByGlyph[start + i] = clusters[i]; |
| } |
| } |
| |
| // TODO: Do something (logN) that is not a linear search |
| Position SelectableText::findPosition(PositionType positionType, const BoxLine& line, SkScalar x) const { |
| Position position(positionType); |
| position.fGlyphRange = GlyphRange(0, line.fBoxGlyphs.size() - 1); |
| position.fTextRange = line.fTextRange; |
| position.fBoundaries.fTop = line.fBounds.fTop; |
| position.fBoundaries.fBottom = line.fBounds.fBottom; |
| // We look for the narrowest glyph range adjusted to positionType that contains the point. |
| // So far we made sure that one unit of any positionType does not cross the run edges |
| // Therefore it's going to be represented by a single text range only |
| for (; position.fGlyphRange.fStart < position.fGlyphRange.fEnd; ++position.fGlyphRange.fStart) { |
| auto glyphBox = line.fBoxGlyphs[position.fGlyphRange.fStart]; |
| if (glyphBox.fLeft > x) { |
| break; |
| } |
| if (position.fPositionType == PositionType::kGraphemeCluster) { |
| auto textIndex = line.fTextByGlyph[position.fGlyphRange.fStart]; |
| if (this->hasProperty(textIndex, GlyphUnitFlags::kGraphemeClusterStart)) { |
| position.fTextRange.fStart = textIndex; |
| } |
| } else { |
| // TODO: Implement |
| SkASSERT(false); |
| } |
| } |
| for (; position.fGlyphRange.fEnd > position.fGlyphRange.fStart ; --position.fGlyphRange.fEnd) { |
| auto glyphBox = line.fBoxGlyphs[position.fGlyphRange.fStart]; |
| if (glyphBox.fRight <= x) { |
| break; |
| } |
| if (position.fPositionType == PositionType::kGraphemeCluster) { |
| auto textIndex = line.fTextByGlyph[position.fGlyphRange.fEnd]; |
| if (this->hasProperty(textIndex, GlyphUnitFlags::kGraphemeClusterStart)) { |
| position.fTextRange.fEnd = textIndex; |
| break; |
| } |
| } else { |
| // TODO: Implement |
| SkASSERT(false); |
| } |
| } |
| position.fLineIndex = line.fIndex; |
| position.fBoundaries.fLeft = line.fBoxGlyphs[position.fGlyphRange.fStart].fLeft; |
| position.fBoundaries.fRight = line.fBoxGlyphs[position.fGlyphRange.fEnd].fRight; |
| return position; |
| } |
| |
| Position SelectableText::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); |
| for (auto& line : fBoxLines) { |
| if (line.fBounds.fTop > xy.fY) { |
| // We are past the point vertically |
| break; |
| } else if (line.fBounds.fBottom <= xy.fY) { |
| // We haven't reached the point vertically yet |
| continue; |
| } |
| return this->findPosition(positionType, line, xy.fX); |
| } |
| return this->lastPosition(positionType); |
| } |
| |
| Position SelectableText::previousPosition(Position current) const { |
| const BoxLine* currentLine = &fBoxLines[current.fLineIndex]; |
| if (this->isFirstOnTheLine(current)) { |
| // Go to the previous line |
| if (current.fLineIndex == 0) { |
| // We reached the end; there is nowhere to move |
| current.fGlyphRange = GlyphRange(0, 0); |
| return current; |
| } else { |
| current.fLineIndex -= 1; |
| currentLine = &fBoxLines[current.fLineIndex]; |
| current.fGlyphRange.fStart = currentLine->fBoxGlyphs.size(); |
| } |
| } |
| auto position = this->findPosition(current.fPositionType, *currentLine, currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX()); |
| if (current.fPositionType == PositionType::kGraphemeCluster) { |
| // Either way we found us a grapheme cluster (just make sure of it) |
| SkASSERT(this->hasProperty(current.fTextRange.fStart, GlyphUnitFlags::kGraphemeClusterStart)); |
| } |
| return position; |
| } |
| |
| Position SelectableText::nextPosition(Position current) const { |
| const BoxLine* currentLine = &fBoxLines[current.fLineIndex]; |
| if (this->isLastOnTheLine(current)) { |
| // Go to the next line |
| if (current.fLineIndex == this->fBoxLines.size() - 1) { |
| // We reached the end; there is nowhere to move |
| current.fGlyphRange = GlyphRange(currentLine->fBoxGlyphs.size(), currentLine->fBoxGlyphs.size()); |
| return current; |
| } else { |
| current.fLineIndex += 1; |
| currentLine = &fBoxLines[current.fLineIndex]; |
| current.fGlyphRange.fEnd = 0; |
| } |
| } |
| auto position = this->findPosition(current.fPositionType, *currentLine, currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX()); |
| if (current.fPositionType == PositionType::kGraphemeCluster) { |
| // Either way we found us a grapheme cluster (just make sure of it) |
| SkASSERT(this->hasProperty(current.fTextRange.fEnd, GlyphUnitFlags::kGraphemeClusterStart)); |
| } |
| return position; |
| } |
| |
| Position SelectableText::upPosition(Position current) const { |
| |
| if (current.fLineIndex == 0) { |
| // We are on the first line; just move to the first position |
| return this->firstPosition(current.fPositionType); |
| } |
| |
| // Go to the previous line |
| const BoxLine* currentLine = &fBoxLines[current.fLineIndex]; |
| auto position = this->findPosition(current.fPositionType, fBoxLines[current.fLineIndex - 1], currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX()); |
| if (current.fPositionType == PositionType::kGraphemeCluster) { |
| // Either way we found us a grapheme cluster (just make sure of it) |
| SkASSERT(this->hasProperty(current.fTextRange.fEnd, GlyphUnitFlags::kGraphemeClusterStart)); |
| } |
| return position; |
| } |
| |
| Position SelectableText::downPosition(Position current) const { |
| |
| if (current.fLineIndex == this->countLines() - 1) { |
| // We are on the last line; just move to the last position |
| return this->lastPosition(current.fPositionType); |
| } |
| |
| // Go to the next line |
| const BoxLine* currentLine = &fBoxLines[current.fLineIndex]; |
| auto position = this->findPosition(current.fPositionType, fBoxLines[current.fLineIndex + 1], currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX()); |
| if (current.fPositionType == PositionType::kGraphemeCluster) { |
| // Either way we found us a grapheme cluster (just make sure of it) |
| SkASSERT(this->hasProperty(current.fTextRange.fEnd, GlyphUnitFlags::kGraphemeClusterStart)); |
| } |
| return position; |
| } |
| |
| Position SelectableText::firstPosition(PositionType positionType) const { |
| auto firstLine = fBoxLines.front(); |
| auto firstGlyph = firstLine.fBoxGlyphs.front(); |
| Position beginningOfText(positionType); |
| // Set the glyph range after the last glyph |
| beginningOfText.fGlyphRange = GlyphRange { 0, 0}; |
| beginningOfText.fLineIndex = 0; |
| beginningOfText.fBoundaries = SkRect::MakeXYWH(firstGlyph.fLeft, firstGlyph.fTop, 0, firstGlyph.height()); |
| beginningOfText.fTextRange = this->glyphsToText(beginningOfText); |
| beginningOfText.fLineIndex = 0; |
| return beginningOfText; |
| } |
| |
| Position SelectableText::lastPosition(PositionType positionType) const { |
| auto lastLine = fBoxLines.back(); |
| auto lastGlyph = lastLine.fBoxGlyphs.back(); |
| Position endOfText(positionType); |
| endOfText.fLineIndex = lastLine.fIndex; |
| endOfText.fGlyphRange = GlyphRange(lastLine.fBoxGlyphs.size() - 1, lastLine.fBoxGlyphs.size() - 1); |
| endOfText.fBoundaries = SkRect::MakeXYWH(lastGlyph.fRight, lastGlyph.fTop, 0, lastGlyph.height()); |
| endOfText.fTextRange = this->glyphsToText(endOfText); |
| endOfText.fLineIndex = lastLine.fIndex; |
| return endOfText; |
| } |
| |
| Position SelectableText::firstInLinePosition(PositionType positionType, LineIndex lineIndex) const { |
| SkASSERT(lineIndex >= 0 && lineIndex < fBoxLines.size()); |
| auto& line = fBoxLines[lineIndex]; |
| return this->findPosition(positionType, line, line.fBounds.left()); |
| } |
| |
| Position SelectableText::lastInLinePosition(PositionType positionType, LineIndex lineIndex) const { |
| auto& line = fBoxLines[lineIndex]; |
| return this->findPosition(positionType, line, line.fBounds.right()); |
| } |
| |
| |
| TextRange SelectableText::glyphsToText(Position position) const { |
| SkASSERT(position.fPositionType != PositionType::kRandomText); |
| auto line = this->getLine(position.fLineIndex); |
| TextRange textRange = EMPTY_RANGE; |
| for (auto glyph = position.fGlyphRange.fStart; glyph <= position.fGlyphRange.fEnd; ++glyph) { |
| if (textRange.fStart == EMPTY_INDEX) { |
| textRange.fStart = line.fTextByGlyph[glyph]; |
| } |
| textRange.fEnd = line.fTextByGlyph[glyph]; |
| } |
| return textRange; |
| } |
| } // namespace text |
| } // namespace skia |