| #ifdef WITH_RIVE_TEXT | |
| #include "rive/text/raw_text.hpp" | |
| #include "rive/text_engine.hpp" | |
| #include "rive/factory.hpp" | |
| #include "rive/shapes/paint/color.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, | |
| ColorInt foregroundColor) | |
| { | |
| int styleIndex = 0; | |
| for (RenderStyle& style : m_styles) | |
| { | |
| if (style.paint == paint) | |
| { | |
| break; | |
| } | |
| styleIndex++; | |
| } | |
| if (styleIndex == (int)m_styles.size()) | |
| { | |
| m_styles.emplace_back(); | |
| auto& style = m_styles.back(); | |
| style.paint = paint; | |
| style.isEmpty = true; | |
| style.foregroundColor = foregroundColor; | |
| } | |
| 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(); | |
| m_drawCommands.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_wrap); | |
| 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]; | |
| 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; | |
| 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; | |
| } | |
| float renderY = y + line.baseline; | |
| 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, | |
| renderY)); | |
| } | |
| const OrderedLine& orderedLine = m_orderedLines[lineIndex]; | |
| float x = line.startX; | |
| 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]; | |
| assert(run->styleId < m_styles.size()); | |
| RenderStyle* style = &m_styles[run->styleId]; | |
| assert(style != nullptr); | |
| Mat2D transform(run->size, | |
| 0.0f, | |
| 0.0f, | |
| run->size, | |
| x + offset.x, | |
| renderY + offset.y); | |
| x += advance; | |
| if (font->isColorGlyph(glyphId)) | |
| { | |
| RawTextDrawCommand cmd; | |
| cmd.type = RawTextDrawCommand::kColorGlyph; | |
| cmd.colorGlyph = {run->font, | |
| glyphId, | |
| transform, | |
| style->foregroundColor}; | |
| m_drawCommands.push_back(std::move(cmd)); | |
| } | |
| else | |
| { | |
| RawPath path = font->getPath(glyphId); | |
| style->path.addPathClockwise(path, &transform); | |
| if (style->isEmpty) | |
| { | |
| style->isEmpty = false; | |
| m_renderStyles.push_back(style); | |
| RawTextDrawCommand cmd; | |
| cmd.type = RawTextDrawCommand::kStylePath; | |
| cmd.style = style; | |
| m_drawCommands.push_back(std::move(cmd)); | |
| } | |
| } | |
| } | |
| 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& cmd : m_drawCommands) | |
| { | |
| if (cmd.type == RawTextDrawCommand::kStylePath) | |
| { | |
| auto renderPaint = paint ? paint.get() : cmd.style->paint.get(); | |
| if (renderPaint != nullptr) | |
| { | |
| renderer->drawPath(cmd.style->path.renderPath(m_factory), | |
| renderPaint); | |
| } | |
| } | |
| else | |
| { | |
| // Draw color glyph layers. | |
| std::vector<Font::ColorGlyphLayer> layers; | |
| auto& info = cmd.colorGlyph; | |
| size_t count = info.font->getColorLayers(info.glyphId, | |
| layers, | |
| info.foregroundColor); | |
| if (count > 0) | |
| { | |
| renderer->save(); | |
| renderer->transform(info.transform); | |
| for (auto& layer : layers) | |
| { | |
| auto renderPath = | |
| m_factory->makeRenderPath(layer.path, | |
| FillRule::nonZero); | |
| auto layerPaint = m_factory->makeRenderPaint(); | |
| layerPaint->style(RenderPaintStyle::fill); | |
| layerPaint->color(layer.color); | |
| renderer->drawPath(renderPath.get(), layerPaint.get()); | |
| } | |
| renderer->restore(); | |
| } | |
| } | |
| } | |
| if (m_overflow == TextOverflow::clipped && m_clipRenderPath) | |
| { | |
| renderer->restore(); | |
| } | |
| } | |
| #endif |