| #include "rive/text_engine.hpp" | |
| #include "rive/text/utf.hpp" | |
| #include "rive/text/glyph_lookup.hpp" | |
| #ifdef WITH_RIVE_TEXT | |
| using namespace rive; | |
| 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; | |
| } | |
| #endif |