| #include "rive/text_engine.hpp" | |
| #include "rive/text/utf.hpp" | |
| #include "rive/text/glyph_lookup.hpp" | |
| #include "rive/text/line_break.hpp" | |
| using namespace rive; | |
| bool rive::isWhiteSpace(Unichar c) | |
| { | |
| // Zs spaces, line and paragraph separators, NEL and zero width space. | |
| // The no-break spaces U+00A0, U+2007 and U+202F are deliberately not. | |
| return c <= ' ' || c == 0x0085 || c == 0x1680 || | |
| (c >= 0x2000 && c <= 0x200B && c != 0x2007) || c == 0x2028 || | |
| c == 0x2029 || c == 0x205F || c == 0x3000; | |
| } | |
| #ifdef WITH_RIVE_TEXT | |
| static void appendUnicode(std::vector<rive::Unichar>& unichars, | |
| const char text[]) | |
| { | |
| const uint8_t* ptr = (const uint8_t*)text; | |
| while (*ptr) | |
| { | |
| unichars.push_back(rive::UTF::NextUTF8(&ptr)); | |
| } | |
| } | |
| static void reverseRuns(const GlyphRun** runs, int count) | |
| { | |
| int halfCount = count / 2; | |
| int finalIndex = count - 1; | |
| for (int index = 0; index < halfCount; index++) | |
| { | |
| int tieIndex = finalIndex - index; | |
| const GlyphRun* tempRun = runs[index]; | |
| runs[index] = runs[tieIndex]; | |
| runs[tieIndex] = tempRun; | |
| } | |
| } | |
| OrderedLine::OrderedLine(const Paragraph& paragraph, | |
| const GlyphLine& line, | |
| float lineWidth, | |
| bool wantEllipsis, | |
| bool isEllipsisLineLast, | |
| GlyphRun* ellipsisRun, | |
| float y) : | |
| m_startGlyphIndex(line.startGlyphIndex), | |
| m_endGlyphIndex(line.endGlyphIndex), | |
| m_glyphLine(&line), | |
| m_y(y) | |
| { | |
| std::vector<const GlyphRun*> logicalRuns; | |
| const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs; | |
| if (!wantEllipsis || !buildEllipsisRuns(logicalRuns, | |
| paragraph, | |
| line, | |
| lineWidth, | |
| isEllipsisLineLast, | |
| ellipsisRun)) | |
| { | |
| for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++) | |
| { | |
| logicalRuns.push_back(&glyphRuns[i]); | |
| } | |
| if (!logicalRuns.empty()) | |
| { | |
| m_startLogical = logicalRuns.front(); | |
| m_endLogical = logicalRuns.back(); | |
| } | |
| } | |
| uint8_t maxLevel = 0; | |
| for (auto run : logicalRuns) | |
| { | |
| if (run->level > maxLevel) | |
| { | |
| maxLevel = run->level; | |
| } | |
| } | |
| for (uint8_t newLevel = maxLevel; newLevel > 0; newLevel--) | |
| { | |
| for (int start = (int)(logicalRuns.size()) - 1; start >= 0; start--) | |
| { | |
| if (logicalRuns[start]->level >= newLevel) | |
| { | |
| int count = 1; | |
| for (; start > 0 && logicalRuns[start - 1]->level >= newLevel; | |
| start--) | |
| { | |
| count++; | |
| } | |
| reverseRuns(logicalRuns.data() + start, count); | |
| } | |
| } | |
| } | |
| m_runs = std::move(logicalRuns); | |
| } | |
| uint32_t OrderedLine::firstCodePointIndex(const GlyphLookup& glyphLookup) const | |
| { | |
| GlyphItr index = begin(); | |
| auto glyphIndex = index.glyphIndex(); | |
| auto run = index.run(); | |
| uint32_t firstCodePointIndex = run->textIndices[glyphIndex]; | |
| if (run->dir() == TextDirection::rtl) | |
| { | |
| // If the final run is RTL we want to add the length of the final glyph | |
| // to get the actual last available codepoint in the line. | |
| firstCodePointIndex += glyphLookup.count(run->textIndices[glyphIndex]); | |
| } | |
| // Clamp ignoring last zero width space for editing. | |
| return std::min(firstCodePointIndex, glyphLookup.lastCodePointIndex() - 1); | |
| } | |
| uint32_t OrderedLine::lastCodePointIndex(const GlyphLookup& glyphLookup) const | |
| { | |
| GlyphItr index = begin(); | |
| GlyphItr lastIndex = index; | |
| while (index != end()) | |
| { | |
| lastIndex = index; | |
| ++index; | |
| } | |
| auto glyphIndex = lastIndex.glyphIndex(); | |
| auto run = lastIndex.run(); | |
| uint32_t lastCodePointIndex = run->textIndices[glyphIndex]; | |
| if (run->dir() == TextDirection::ltr) | |
| { | |
| lastCodePointIndex += glyphLookup.count(run->textIndices[glyphIndex]); | |
| } | |
| // Clamp ignoring last zero width space for editing. | |
| return std::min(lastCodePointIndex, glyphLookup.lastCodePointIndex() - 1); | |
| } | |
| bool OrderedLine::containsCodePointIndex(const GlyphLookup& glyphLookup, | |
| uint32_t codePointIndex) const | |
| { | |
| return codePointIndex >= firstCodePointIndex(glyphLookup) && | |
| codePointIndex <= lastCodePointIndex(glyphLookup); | |
| } | |
| void GlyphItr::tryAdvanceRun() | |
| { | |
| while (true) | |
| { | |
| auto run = *m_run; | |
| if (m_glyphIndex == m_line->endGlyphIndex(run) && | |
| run != m_line->lastRun()) | |
| { | |
| m_run++; | |
| m_glyphIndex = m_line->startGlyphIndex(*m_run); | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| GlyphItr& GlyphItr::operator++() | |
| { | |
| auto run = *m_run; | |
| m_glyphIndex += run->dir() == TextDirection::ltr ? 1 : -1; | |
| tryAdvanceRun(); | |
| return *this; | |
| } | |
| bool OrderedLine::buildEllipsisRuns(std::vector<const GlyphRun*>& logicalRuns, | |
| const Paragraph& paragraph, | |
| const GlyphLine& line, | |
| float lineWidth, | |
| bool isEllipsisLineLast, | |
| GlyphRun* storedEllipsisRun) | |
| { | |
| float x = 0.0f; | |
| const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs; | |
| uint32_t startGIndex = line.startGlyphIndex; | |
| // If it's the last line we can actually early out if the whole things fits, | |
| // so check that first with no extra shaping. | |
| if (isEllipsisLineLast) | |
| { | |
| bool fits = true; | |
| for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++) | |
| { | |
| const GlyphRun& run = glyphRuns[i]; | |
| uint32_t endGIndex = i == line.endRunIndex | |
| ? line.endGlyphIndex | |
| : (uint32_t)run.glyphs.size(); | |
| for (uint32_t j = startGIndex; j != endGIndex; j++) | |
| { | |
| x += run.advances[j]; | |
| if (x > lineWidth) | |
| { | |
| fits = false; | |
| goto measured; | |
| } | |
| } | |
| startGIndex = 0; | |
| } | |
| measured: | |
| if (fits) | |
| { | |
| // It fits, just get the regular glyphs. | |
| return false; | |
| } | |
| } | |
| std::vector<Unichar> ellipsisCodePoints; | |
| appendUnicode(ellipsisCodePoints, "..."); | |
| rcp<Font> ellipsisFont = nullptr; | |
| float ellipsisFontSize = 0.0f; | |
| GlyphRun ellipsisRun = {}; | |
| float ellipsisWidth = 0.0f; | |
| bool ellipsisOverflowed = false; | |
| startGIndex = line.startGlyphIndex; | |
| x = 0.0f; | |
| for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++) | |
| { | |
| const GlyphRun& run = glyphRuns[i]; | |
| if (run.font != ellipsisFont && run.size != ellipsisFontSize) | |
| { | |
| // Track the latest we've checked (even if we discard it so we don't | |
| // try to do this again for this ellipsis). | |
| ellipsisFont = run.font; | |
| ellipsisFontSize = run.size; | |
| // Get the next shape so we can check if it fits, otherwise keep | |
| // using the last one. | |
| TextRun ellipsisRuns[] = {{ | |
| ellipsisFont, | |
| ellipsisFontSize, | |
| run.lineHeight, | |
| run.letterSpacing, | |
| (uint32_t)ellipsisCodePoints.size(), | |
| 0, // default, TextRun.script | |
| run.styleId, | |
| }}; | |
| auto nextEllipsisShape = | |
| ellipsisFont->shapeText(ellipsisCodePoints, | |
| Span<TextRun>(ellipsisRuns, 1)); | |
| // Hard assumption one run and para | |
| const Paragraph& para = nextEllipsisShape[0]; | |
| const GlyphRun& nextEllipsisRun = para.runs.front(); | |
| float nextEllipsisWidth = 0; | |
| for (size_t j = 0; j < nextEllipsisRun.glyphs.size(); j++) | |
| { | |
| nextEllipsisWidth += nextEllipsisRun.advances[j]; | |
| } | |
| if (ellipsisRun.font == nullptr || | |
| x + nextEllipsisWidth <= lineWidth) | |
| { | |
| // This ellipsis still fits, go ahead and use it. Otherwise | |
| // stick with the old one. | |
| ellipsisWidth = nextEllipsisWidth; | |
| ellipsisRun = std::move(para.runs.front()); | |
| } | |
| } | |
| uint32_t endGIndex = i == line.endRunIndex | |
| ? line.endGlyphIndex | |
| : (uint32_t)run.glyphs.size(); | |
| for (uint32_t j = startGIndex; j != endGIndex; j++) | |
| { | |
| float advance = run.advances[j]; | |
| if (x + advance + ellipsisWidth > lineWidth) | |
| { | |
| m_endGlyphIndex = j; | |
| ellipsisOverflowed = true; | |
| break; | |
| } | |
| x += advance; | |
| } | |
| startGIndex = 0; | |
| logicalRuns.push_back(&run); | |
| m_endLogical = &run; | |
| if (ellipsisOverflowed && ellipsisRun.font != nullptr) | |
| { | |
| *storedEllipsisRun = std::move(ellipsisRun); | |
| logicalRuns.push_back(storedEllipsisRun); | |
| break; | |
| } | |
| } | |
| // There was enough space for it, so let's add the ellipsis (if we didn't | |
| // already). Note that we already checked if this is the last line and found | |
| // that the whole text didn't fit. | |
| if (!ellipsisOverflowed && ellipsisRun.font != nullptr) | |
| { | |
| *storedEllipsisRun = std::move(ellipsisRun); | |
| logicalRuns.push_back(storedEllipsisRun); | |
| } | |
| m_startLogical = storedEllipsisRun == logicalRuns.front() | |
| ? nullptr | |
| : logicalRuns.front(); | |
| return true; | |
| } | |
| float OrderedLine::bottom() const | |
| { | |
| return m_y - m_glyphLine->baseline + m_glyphLine->bottom; | |
| } | |
| SimpleArray<Paragraph> Font::shapeText(Span<const Unichar> text, | |
| Span<const TextRun> runs, | |
| int textDirectionFlag) const | |
| { | |
| #ifdef DEBUG | |
| size_t count = 0; | |
| for (const TextRun& tr : runs) | |
| { | |
| assert(tr.unicharCount > 0); | |
| count += tr.unicharCount; | |
| } | |
| assert(count <= text.size()); | |
| #endif | |
| SimpleArray<Paragraph> paragraphs = | |
| onShapeText(text, runs, textDirectionFlag); | |
| // Boundaries live on the stack for typical UI strings. | |
| constexpr size_t kInlineBreaks = 257; | |
| LineBreak inlineBreaks[kInlineBreaks]; | |
| std::vector<LineBreak> heapBreaks; | |
| size_t breakCount = text.size() + 1; | |
| if (breakCount > kInlineBreaks) | |
| { | |
| heapBreaks.resize(breakCount); | |
| } | |
| Span<LineBreak> lineBreaks(heapBreaks.empty() ? inlineBreaks | |
| : heapBreaks.data(), | |
| breakCount); | |
| computeLineBreaks(text, lineBreaks); | |
| bool inWord = false; | |
| GlyphRun* lastRun = nullptr; | |
| size_t reserveSize = text.size() / 4; | |
| SimpleArrayBuilder<uint32_t> breakBuilder(reserveSize); | |
| SimpleArrayBuilder<uint32_t> joinerBuilder(reserveSize); | |
| for (const Paragraph& para : paragraphs) | |
| { | |
| for (GlyphRun& gr : para.runs) | |
| { | |
| if (lastRun != nullptr) | |
| { | |
| lastRun->breaks = std::move(breakBuilder); | |
| lastRun->joiners = std::move(joinerBuilder); | |
| // Reset the builder. | |
| breakBuilder = SimpleArrayBuilder<uint32_t>(reserveSize); | |
| joinerBuilder = SimpleArrayBuilder<uint32_t>(reserveSize); | |
| } | |
| uint32_t glyphIndex = 0; | |
| uint32_t lastOffset = (uint32_t)-1; | |
| for (uint32_t offset : gr.textIndices) | |
| { | |
| // Only the first glyph of a cluster can start or end a word. | |
| if (offset == lastOffset) | |
| { | |
| glyphIndex++; | |
| continue; | |
| } | |
| lastOffset = offset; | |
| Unichar unicode = text[offset]; | |
| if (lineBreaks[offset + 1] == LineBreak::mandatory) | |
| { | |
| breakBuilder.add(glyphIndex); | |
| breakBuilder.add(glyphIndex); | |
| } | |
| // Only U+2060 and U+FEFF are word joiners, skip the lookup | |
| // for everything below them. | |
| if (unicode >= 0x2060 && | |
| lineBreakProps(unicode).cls == LineBreakClass::WJ) | |
| { | |
| joinerBuilder.add(offset); | |
| } | |
| if (inWord) | |
| { | |
| if (isWhiteSpace(unicode)) | |
| { | |
| breakBuilder.add(glyphIndex); | |
| inWord = false; | |
| } | |
| // Soft hyphens stay unbreakable until we can draw the | |
| // hyphen at the line end. | |
| else if (lineBreaks[offset] == LineBreak::allowed && | |
| text[offset - 1] != 0x00AD) | |
| { | |
| breakBuilder.add(glyphIndex); | |
| breakBuilder.add(glyphIndex); | |
| } | |
| } | |
| else if (!isWhiteSpace(unicode)) | |
| { | |
| breakBuilder.add(glyphIndex); | |
| inWord = true; | |
| } | |
| glyphIndex++; | |
| } | |
| lastRun = &gr; | |
| } | |
| } | |
| if (lastRun != nullptr) | |
| { | |
| if (inWord) | |
| { | |
| breakBuilder.add((uint32_t)lastRun->glyphs.size()); | |
| } | |
| else | |
| { | |
| // Consume the rest of the run. | |
| breakBuilder.add(breakBuilder.empty() ? 0 : breakBuilder.back()); | |
| breakBuilder.add((uint32_t)lastRun->glyphs.size()); | |
| } | |
| lastRun->breaks = std::move(breakBuilder); | |
| lastRun->joiners = std::move(joinerBuilder); | |
| } | |
| #ifdef DEBUG | |
| for (const Paragraph& para : paragraphs) | |
| { | |
| for (const GlyphRun& gr : para.runs) | |
| { | |
| assert(gr.glyphs.size() > 0); | |
| assert(gr.glyphs.size() == gr.textIndices.size()); | |
| assert(gr.glyphs.size() + 1 == gr.xpos.size()); | |
| } | |
| } | |
| #endif | |
| return paragraphs; | |
| } | |
| #endif |