| #include "rive/text/text.hpp" | |
| using namespace rive; | |
| #ifdef WITH_RIVE_TEXT | |
| #include "rive/text_engine.hpp" | |
| #include "rive/component_dirt.hpp" | |
| #include "rive/text/utf.hpp" | |
| #include "rive/text/text_style.hpp" | |
| #include "rive/text/text_value_run.hpp" | |
| #include "rive/shapes/paint/shape_paint.hpp" | |
| GlyphItr& GlyphItr::operator++() | |
| { | |
| m_glyphIndex += m_run->dir == TextDirection::ltr ? 1 : -1; | |
| // Did we reach the end of the run? | |
| if (m_glyphIndex == m_line->endGlyphIndex(m_run) && m_run != m_line->lastRun()) | |
| { | |
| m_run++; | |
| m_glyphIndex = m_line->startGlyphIndex(m_run); | |
| } | |
| return *this; | |
| } | |
| OrderedLine::OrderedLine(const Paragraph& paragraph, const GlyphLine& line) : m_line(&line) | |
| { | |
| std::vector<const GlyphRun*> logicalRuns; | |
| const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs; | |
| for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++) | |
| { | |
| logicalRuns.push_back(&glyphRuns[i]); | |
| } | |
| if (paragraph.baseDirection == TextDirection::ltr || logicalRuns.empty()) | |
| { | |
| m_startLogical = logicalRuns.front(); | |
| m_endLogical = logicalRuns.back(); | |
| m_runs = std::move(logicalRuns); | |
| } | |
| else | |
| { | |
| std::vector<const GlyphRun*> visualRuns; | |
| visualRuns.reserve(logicalRuns.size()); | |
| auto itr = logicalRuns.rbegin(); | |
| auto end = logicalRuns.rend(); | |
| const GlyphRun* first = *itr; | |
| visualRuns.push_back(first); | |
| size_t ltrIndex = 0; | |
| TextDirection previousDirection = first->dir; | |
| while (++itr != end) | |
| { | |
| const GlyphRun* run = *itr; | |
| if (run->dir == TextDirection::ltr && previousDirection == run->dir) | |
| { | |
| visualRuns.insert(visualRuns.begin() + ltrIndex, run); | |
| } | |
| else | |
| { | |
| if (run->dir == TextDirection::ltr) | |
| { | |
| ltrIndex = visualRuns.size(); | |
| } | |
| visualRuns.push_back(run); | |
| } | |
| previousDirection = run->dir; | |
| } | |
| m_runs = std::move(visualRuns); | |
| } | |
| } | |
| void Text::buildRenderStyles() | |
| { | |
| for (TextStyle* style : m_renderStyles) | |
| { | |
| style->resetPath(); | |
| } | |
| m_renderStyles.clear(); | |
| // Build up ordered runs as we go. | |
| int paragraphIndex = 0; | |
| int lineIndex = 0; | |
| for (const SimpleArray<GlyphLine>& paragraphLines : m_lines) | |
| { | |
| const Paragraph& paragraph = m_shape[paragraphIndex++]; | |
| float y = 0.0f; | |
| for (const GlyphLine& line : paragraphLines) | |
| { | |
| if (lineIndex >= m_orderedLines.size()) | |
| { | |
| // We need to still compute this line's ordered runs. | |
| m_orderedLines.emplace_back(OrderedLine(paragraph, line)); | |
| } | |
| const OrderedLine& orderedLine = m_orderedLines[lineIndex]; | |
| float x = 0.0f; | |
| float renderY = y + line.baseline; | |
| for (auto glyphItr : orderedLine) | |
| { | |
| const GlyphRun* run = std::get<0>(glyphItr); | |
| size_t glyphIndex = std::get<1>(glyphItr); | |
| const Font* font = run->font.get(); | |
| GlyphID glyphId = run->glyphs[glyphIndex]; | |
| // TODO: profile if this should be cached. | |
| RawPath path = font->getPath(glyphId); | |
| // If we do end up caching these, we'll want to not | |
| // transformInPlace and just transform instead. | |
| path.transformInPlace(Mat2D(run->size, 0.0f, 0.0f, run->size, x, renderY)); | |
| x += run->advances[glyphIndex]; | |
| assert(run->styleId < m_runs.size()); | |
| TextStyle* style = m_runs[run->styleId]->style(); | |
| // TextValueRun::onAddedDirty botches loading if it cannot | |
| // resolve a style, so we're confident we have a style here. | |
| assert(style != nullptr); | |
| if (style->addPath(path)) | |
| { | |
| // This was the first path added to the style, so let's mark | |
| // it in our draw list. | |
| m_renderStyles.push_back(style); | |
| } | |
| } | |
| lineIndex++; | |
| } | |
| } | |
| } | |
| void Text::draw(Renderer* renderer) | |
| { | |
| if (!clip(renderer)) | |
| { | |
| // We didn't clip, so make sure to save as we'll be doing some | |
| // transformations. | |
| renderer->save(); | |
| } | |
| renderer->transform(worldTransform()); | |
| for (auto style : m_renderStyles) | |
| { | |
| style->draw(renderer); | |
| } | |
| renderer->restore(); | |
| } | |
| void Text::addRun(TextValueRun* run) { m_runs.push_back(run); } | |
| void Text::markShapeDirty() { addDirt(ComponentDirt::Path); } | |
| void Text::alignValueChanged() { markShapeDirty(); } | |
| void Text::sizingValueChanged() { markShapeDirty(); } | |
| void Text::overflowValueChanged() | |
| { | |
| if (sizing() != TextSizing::autoWidth) | |
| { | |
| markShapeDirty(); | |
| } | |
| } | |
| void Text::widthChanged() | |
| { | |
| if (sizing() != TextSizing::autoWidth) | |
| { | |
| markShapeDirty(); | |
| } | |
| } | |
| void Text::heightChanged() | |
| { | |
| if (sizing() == TextSizing::fixed) | |
| { | |
| markShapeDirty(); | |
| } | |
| } | |
| static rive::TextRun append(std::vector<Unichar>& unichars, | |
| rcp<Font> font, | |
| float size, | |
| const std::string& text, | |
| uint16_t styleId) | |
| { | |
| const uint8_t* ptr = (const uint8_t*)text.c_str(); | |
| uint32_t n = 0; | |
| while (*ptr) | |
| { | |
| unichars.push_back(UTF::NextUTF8(&ptr)); | |
| n += 1; | |
| } | |
| return {std::move(font), size, n, 0, styleId}; | |
| } | |
| static SimpleArray<SimpleArray<GlyphLine>> breakLines(const SimpleArray<Paragraph>& paragraphs, | |
| float width, | |
| TextAlign align) | |
| { | |
| bool autoWidth = width == -1.0f; | |
| float paragraphWidth = width; | |
| SimpleArray<SimpleArray<GlyphLine>> lines(paragraphs.size()); | |
| size_t paragraphIndex = 0; | |
| for (auto& para : paragraphs) | |
| { | |
| lines[paragraphIndex] = GlyphLine::BreakLines(para.runs, autoWidth ? -1.0f : width); | |
| if (autoWidth) | |
| { | |
| paragraphWidth = std::max(paragraphWidth, | |
| GlyphLine::ComputeMaxWidth(lines[paragraphIndex], para.runs)); | |
| } | |
| paragraphIndex++; | |
| } | |
| paragraphIndex = 0; | |
| for (auto& para : paragraphs) | |
| { | |
| GlyphLine::ComputeLineSpacing(lines[paragraphIndex++], para.runs, paragraphWidth, align); | |
| } | |
| return lines; | |
| } | |
| void Text::update(ComponentDirt value) | |
| { | |
| Super::update(value); | |
| if (hasDirt(value, ComponentDirt::Path)) | |
| { | |
| std::vector<Unichar> unichars; | |
| std::vector<TextRun> runs; | |
| uint16_t runIndex = 0; | |
| for (auto valueRun : m_runs) | |
| { | |
| auto style = valueRun->style(); | |
| const std::string& text = valueRun->text(); | |
| if (style == nullptr || style->font() == nullptr || text.empty()) | |
| { | |
| runIndex++; | |
| continue; | |
| } | |
| runs.emplace_back(append(unichars, style->font(), style->fontSize(), text, runIndex++)); | |
| } | |
| if (!runs.empty()) | |
| { | |
| m_shape = runs[0].font->shapeText(unichars, runs); | |
| m_lines = breakLines(m_shape, | |
| sizing() == TextSizing::autoWidth ? -1.0f : width(), | |
| (TextAlign)alignValue()); | |
| m_orderedLines.clear(); | |
| } | |
| // This could later be flagged as dirty and called only when actually | |
| // rendered the first time, for now we do it on update cycle in tandem | |
| // with shaping. | |
| buildRenderStyles(); | |
| } | |
| } | |
| Core* Text::hitTest(HitInfo*, const Mat2D&) | |
| { | |
| if (renderOpacity() == 0.0f) | |
| { | |
| return nullptr; | |
| } | |
| return nullptr; | |
| } | |
| #else | |
| // Text disabled. | |
| void Text::draw(Renderer* renderer) {} | |
| Core* Text::hitTest(HitInfo*, const Mat2D&) { return nullptr; } | |
| void Text::addRun(TextValueRun* run) {} | |
| void Text::markShapeDirty() {} | |
| void Text::update(ComponentDirt value) {} | |
| void Text::alignValueChanged() {} | |
| void Text::sizingValueChanged() {} | |
| void Text::overflowValueChanged() {} | |
| void Text::widthChanged() {} | |
| void Text::heightChanged() {} | |
| #endif |