| #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 |