| #ifdef WITH_RIVE_TEXT | |
| #include "rive/text/raw_text.hpp" | |
| #include "rive/text_engine.hpp" | |
| #include "rive/factory.hpp" | |
| using namespace rive; | |
| RawText::RawText(Factory* factory) : m_factory(factory) {} | |
| bool RawText::empty() const { return m_styled.empty(); } | |
| void RawText::append(const std::string& text, | |
| rcp<RenderPaint> paint, | |
| rcp<Font> font, | |
| float size, | |
| float lineHeight, | |
| float letterSpacing) | |
| { | |
| int styleIndex = 0; | |
| for (RenderStyle& style : m_styles) | |
| { | |
| if (style.paint == paint) | |
| { | |
| break; | |
| } | |
| styleIndex++; | |
| } | |
| if (styleIndex == m_styles.size()) | |
| { | |
| m_styles.push_back({paint, m_factory->makeEmptyRenderPath(), true}); | |
| } | |
| m_styled.append(font, size, lineHeight, letterSpacing, text, styleIndex); | |
| m_dirty = true; | |
| } | |
| void RawText::clear() | |
| { | |
| m_styled.clear(); | |
| m_dirty = true; | |
| } | |
| TextSizing RawText::sizing() const { return m_sizing; } | |
| TextOverflow RawText::overflow() const { return m_overflow; } | |
| TextAlign RawText::align() const { return m_align; } | |
| float RawText::maxWidth() const { return m_maxWidth; } | |
| float RawText::maxHeight() const { return m_maxHeight; } | |
| float RawText::paragraphSpacing() const { return m_paragraphSpacing; } | |
| void RawText::sizing(TextSizing value) | |
| { | |
| if (m_sizing != value) | |
| { | |
| m_sizing = value; | |
| m_dirty = true; | |
| } | |
| } | |
| void RawText::overflow(TextOverflow value) | |
| { | |
| if (m_overflow != value) | |
| { | |
| m_overflow = value; | |
| m_dirty = true; | |
| } | |
| } | |
| void RawText::align(TextAlign value) | |
| { | |
| if (m_align != value) | |
| { | |
| m_align = value; | |
| m_dirty = true; | |
| } | |
| } | |
| void RawText::paragraphSpacing(float value) | |
| { | |
| if (m_paragraphSpacing != value) | |
| { | |
| m_paragraphSpacing = value; | |
| m_dirty = true; | |
| } | |
| } | |
| void RawText::maxWidth(float value) | |
| { | |
| if (m_maxWidth != value) | |
| { | |
| m_maxWidth = value; | |
| m_dirty = true; | |
| } | |
| } | |
| void RawText::maxHeight(float value) | |
| { | |
| if (m_maxHeight != value) | |
| { | |
| m_maxHeight = value; | |
| m_dirty = true; | |
| } | |
| } | |
| void RawText::update() | |
| { | |
| for (RenderStyle& style : m_styles) | |
| { | |
| style.path->rewind(); | |
| style.isEmpty = true; | |
| } | |
| m_renderStyles.clear(); | |
| if (m_styled.empty()) | |
| { | |
| return; | |
| } | |
| auto runs = m_styled.runs(); | |
| m_shape = runs[0].font->shapeText(m_styled.unichars(), runs); | |
| m_lines = | |
| Text::BreakLines(m_shape, m_sizing == TextSizing::autoWidth ? -1.0f : m_maxWidth, m_align); | |
| m_orderedLines.clear(); | |
| m_ellipsisRun = {}; | |
| // build render styles. | |
| if (m_shape.empty()) | |
| { | |
| m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f); | |
| return; | |
| } | |
| // Build up ordered runs as we go. | |
| int paragraphIndex = 0; | |
| float y = 0.0f; | |
| float minY = 0.0f; | |
| float measuredWidth = 0.0f; | |
| if (m_origin == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty()) | |
| { | |
| y -= m_lines[0][0].baseline; | |
| minY = y; | |
| } | |
| int ellipsisLine = -1; | |
| bool isEllipsisLineLast = false; | |
| // Find the line to put the ellipsis on (line before the one that | |
| // overflows). | |
| bool wantEllipsis = m_overflow == TextOverflow::ellipsis && m_sizing == TextSizing::fixed; | |
| int lastLineIndex = -1; | |
| for (const SimpleArray<GlyphLine>& paragraphLines : m_lines) | |
| { | |
| const Paragraph& paragraph = m_shape[paragraphIndex++]; | |
| for (const GlyphLine& line : paragraphLines) | |
| { | |
| const GlyphRun& endRun = paragraph.runs[line.endRunIndex]; | |
| const GlyphRun& startRun = paragraph.runs[line.startRunIndex]; | |
| float width = endRun.xpos[line.endGlyphIndex] - startRun.xpos[line.startGlyphIndex] - | |
| endRun.letterSpacing; | |
| if (width > measuredWidth) | |
| { | |
| measuredWidth = width; | |
| } | |
| lastLineIndex++; | |
| if (wantEllipsis && y + line.bottom <= m_maxHeight) | |
| { | |
| ellipsisLine++; | |
| } | |
| } | |
| if (!paragraphLines.empty()) | |
| { | |
| y += paragraphLines.back().bottom; | |
| } | |
| y += m_paragraphSpacing; | |
| } | |
| if (wantEllipsis && ellipsisLine == -1) | |
| { | |
| // Nothing fits, just show the first line and ellipse it. | |
| ellipsisLine = 0; | |
| } | |
| isEllipsisLineLast = lastLineIndex == ellipsisLine; | |
| int lineIndex = 0; | |
| paragraphIndex = 0; | |
| switch (m_sizing) | |
| { | |
| case TextSizing::autoWidth: | |
| m_bounds = AABB(0.0f, minY, measuredWidth, std::max(minY, y - m_paragraphSpacing)); | |
| break; | |
| case TextSizing::autoHeight: | |
| m_bounds = AABB(0.0f, minY, m_maxWidth, std::max(minY, y - m_paragraphSpacing)); | |
| break; | |
| case TextSizing::fixed: | |
| m_bounds = AABB(0.0f, minY, m_maxWidth, minY + m_maxHeight); | |
| break; | |
| } | |
| // Build the clip path if we want it. | |
| if (m_overflow == TextOverflow::clipped) | |
| { | |
| if (m_clipRenderPath == nullptr) | |
| { | |
| m_clipRenderPath = m_factory->makeEmptyRenderPath(); | |
| } | |
| else | |
| { | |
| m_clipRenderPath->rewind(); | |
| } | |
| m_clipRenderPath->addRect(m_bounds.minX, | |
| m_bounds.minY, | |
| m_bounds.width(), | |
| m_bounds.height()); | |
| } | |
| else | |
| { | |
| m_clipRenderPath = nullptr; | |
| } | |
| y = 0; | |
| if (m_origin == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty()) | |
| { | |
| y -= m_lines[0][0].baseline; | |
| } | |
| paragraphIndex = 0; | |
| for (const SimpleArray<GlyphLine>& paragraphLines : m_lines) | |
| { | |
| const Paragraph& paragraph = m_shape[paragraphIndex++]; | |
| for (const GlyphLine& line : paragraphLines) | |
| { | |
| switch (m_overflow) | |
| { | |
| case TextOverflow::hidden: | |
| if (m_sizing == TextSizing::fixed && y + line.bottom > m_maxHeight) | |
| { | |
| return; | |
| } | |
| break; | |
| case TextOverflow::clipped: | |
| if (m_sizing == TextSizing::fixed && y + line.top > m_maxHeight) | |
| { | |
| return; | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| if (lineIndex >= m_orderedLines.size()) | |
| { | |
| // We need to still compute this line's ordered runs. | |
| m_orderedLines.emplace_back(OrderedLine(paragraph, | |
| line, | |
| m_maxWidth, | |
| ellipsisLine == lineIndex, | |
| isEllipsisLineLast, | |
| &m_ellipsisRun)); | |
| } | |
| const OrderedLine& orderedLine = m_orderedLines[lineIndex]; | |
| float x = line.startX; | |
| 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(); | |
| const Vec2D& offset = run->offsets[glyphIndex]; | |
| GlyphID glyphId = run->glyphs[glyphIndex]; | |
| float advance = run->advances[glyphIndex]; | |
| RawPath path = font->getPath(glyphId); | |
| path.transformInPlace( | |
| Mat2D(run->size, 0.0f, 0.0f, run->size, x + offset.x, renderY + offset.y)); | |
| x += advance; | |
| assert(run->styleId < m_styles.size()); | |
| RenderStyle* style = &m_styles[run->styleId]; | |
| assert(style != nullptr); | |
| path.addTo(style->path.get()); | |
| if (style->isEmpty) | |
| { | |
| // This was the first path added to the style, so let's mark | |
| // it in our draw list. | |
| style->isEmpty = false; | |
| m_renderStyles.push_back(style); | |
| } | |
| } | |
| if (lineIndex == ellipsisLine) | |
| { | |
| return; | |
| } | |
| lineIndex++; | |
| } | |
| if (!paragraphLines.empty()) | |
| { | |
| y += paragraphLines.back().bottom; | |
| } | |
| y += m_paragraphSpacing; | |
| } | |
| } | |
| AABB RawText::bounds() | |
| { | |
| if (m_dirty) | |
| { | |
| update(); | |
| m_dirty = false; | |
| } | |
| return m_bounds; | |
| } | |
| void RawText::render(Renderer* renderer, rcp<RenderPaint> paint) | |
| { | |
| if (m_dirty) | |
| { | |
| update(); | |
| m_dirty = false; | |
| } | |
| if (m_overflow == TextOverflow::clipped && m_clipRenderPath) | |
| { | |
| renderer->save(); | |
| renderer->clipPath(m_clipRenderPath.get()); | |
| } | |
| for (auto style : m_renderStyles) | |
| { | |
| renderer->drawPath(style->path.get(), paint ? paint.get() : style->paint.get()); | |
| } | |
| if (m_overflow == TextOverflow::clipped && m_clipRenderPath) | |
| { | |
| renderer->restore(); | |
| } | |
| } | |
| #endif |