| #include "rive/text/text.hpp" | |
| using namespace rive; | |
| #ifdef WITH_RIVE_TEXT | |
| #include "rive/text_engine.hpp" | |
| #include "rive/component_dirt.hpp" | |
| #include "rive/math/rectangles_to_contour.hpp" | |
| #include "rive/text/utf.hpp" | |
| #include "rive/text/text_style.hpp" | |
| #include "rive/text/text_value_run.hpp" | |
| #include "rive/text/text_modifier_group.hpp" | |
| #include "rive/shapes/paint/shape_paint.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/factory.hpp" | |
| #include "rive/clip_result.hpp" | |
| #include <limits> | |
| #include <unordered_map> | |
| 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; | |
| } | |
| 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) : | |
| m_startGlyphIndex(line.startGlyphIndex), m_endGlyphIndex(line.endGlyphIndex) | |
| { | |
| 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); | |
| } | |
| 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)); | |
| } | |
| } | |
| 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; | |
| } | |
| Vec2D Text::measureLayout(float width, | |
| LayoutMeasureMode widthMode, | |
| float height, | |
| LayoutMeasureMode heightMode) | |
| { | |
| return measure(Vec2D(widthMode == LayoutMeasureMode::undefined | |
| ? std::numeric_limits<float>::max() | |
| : width, | |
| heightMode == LayoutMeasureMode::undefined | |
| ? std::numeric_limits<float>::max() | |
| : height)); | |
| } | |
| void Text::controlSize(Vec2D size, | |
| LayoutScaleType widthScaleType, | |
| LayoutScaleType heightScaleType) | |
| { | |
| if (m_layoutWidth != size.x || m_layoutHeight != size.y || | |
| m_layoutWidthScaleType != (uint8_t)widthScaleType || | |
| m_layoutHeightScaleType != (uint8_t)heightScaleType) | |
| { | |
| m_layoutWidth = size.x; | |
| m_layoutHeight = size.y; | |
| m_layoutWidthScaleType = (uint8_t)widthScaleType; | |
| m_layoutHeightScaleType = (uint8_t)heightScaleType; | |
| markShapeDirty(false); | |
| } | |
| } | |
| TextSizing Text::effectiveSizing() const | |
| { | |
| if (m_layoutWidthScaleType == std::numeric_limits<uint8_t>::max() || | |
| m_layoutWidthScaleType == (uint8_t)LayoutScaleType::hug || | |
| m_layoutHeightScaleType == (uint8_t)LayoutScaleType::hug) | |
| { | |
| return sizing(); | |
| } | |
| return TextSizing::fixed; | |
| } | |
| void Text::buildRenderStyles() | |
| { | |
| for (TextStyle* style : m_renderStyles) | |
| { | |
| style->rewindPath(); | |
| } | |
| m_renderStyles.clear(); | |
| if (m_shape.empty()) | |
| { | |
| m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f); | |
| return; | |
| } | |
| std::unordered_map<uint16_t, std::vector<Rect>> textValueRunToRects; | |
| for (uint16_t i = 0; i < m_runs.size(); i++) | |
| { | |
| TextValueRun* textValueRun = m_runs[i]; | |
| textValueRun->resetHitTest(); | |
| if (textValueRun->m_isHitTarget) | |
| { | |
| textValueRunToRects[i] = {}; | |
| } | |
| } | |
| const float paragraphSpace = paragraphSpacing(); | |
| // Build up ordered runs as we go. | |
| int paragraphIndex = 0; | |
| float y = 0.0f; | |
| float minY = 0.0f; | |
| float maxWidth = 0.0f; | |
| if (textOrigin() == 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 = overflow() == TextOverflow::ellipsis && | |
| effectiveSizing() == TextSizing::fixed && | |
| verticalAlign() == VerticalTextAlign::top; | |
| 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 > maxWidth) | |
| { | |
| maxWidth = width; | |
| } | |
| lastLineIndex++; | |
| if (wantEllipsis && y + line.bottom <= effectiveHeight()) | |
| { | |
| ellipsisLine++; | |
| } | |
| } | |
| if (!paragraphLines.empty()) | |
| { | |
| y += paragraphLines.back().bottom; | |
| } | |
| y += paragraphSpace; | |
| } | |
| auto totalHeight = y; | |
| 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 (effectiveSizing()) | |
| { | |
| case TextSizing::autoWidth: | |
| m_bounds = | |
| AABB(0.0f, minY, maxWidth, std::max(minY, y - paragraphSpace)); | |
| break; | |
| case TextSizing::autoHeight: | |
| m_bounds = AABB(0.0f, | |
| minY, | |
| effectiveWidth(), | |
| std::max(minY, y - paragraphSpace)); | |
| break; | |
| case TextSizing::fixed: | |
| m_bounds = | |
| AABB(0.0f, minY, effectiveWidth(), minY + effectiveHeight()); | |
| break; | |
| } | |
| auto verticalAlignOffset = 0.0f; | |
| switch (verticalAlign()) | |
| { | |
| case VerticalTextAlign::middle: | |
| verticalAlignOffset = (totalHeight - m_bounds.height()) / 2; | |
| break; | |
| case VerticalTextAlign::bottom: | |
| verticalAlignOffset = totalHeight - m_bounds.height(); | |
| break; | |
| default: | |
| break; | |
| } | |
| // Build the clip path if we want it. | |
| if (overflow() == TextOverflow::clipped) | |
| { | |
| if (m_clipRenderPath == nullptr) | |
| { | |
| m_clipRenderPath = artboard()->factory()->makeEmptyRenderPath(); | |
| } | |
| else | |
| { | |
| m_clipRenderPath->rewind(); | |
| } | |
| AABB bounds = localBounds(); | |
| m_clipRenderPath->addRect(bounds.minX + bounds.width() * originX(), | |
| bounds.minY + bounds.height() * originY() + | |
| verticalAlignOffset, | |
| bounds.width(), | |
| bounds.height()); | |
| } | |
| else | |
| { | |
| m_clipRenderPath = nullptr; | |
| } | |
| y = 0; | |
| if (textOrigin() == TextOrigin::baseline && !m_lines.empty() && | |
| !m_lines[0].empty()) | |
| { | |
| y -= m_lines[0][0].baseline; | |
| } | |
| paragraphIndex = 0; | |
| bool hasModifiers = haveModifiers(); | |
| if (hasModifiers) | |
| { | |
| uint32_t textSize = (uint32_t)m_styledText.unichars().size(); | |
| for (TextModifierGroup* modifierGroup : m_modifierGroups) | |
| { | |
| modifierGroup->computeCoverage(textSize); | |
| } | |
| } | |
| float minX = std::numeric_limits<float>::max(); | |
| bool drawLine = true; | |
| for (const SimpleArray<GlyphLine>& paragraphLines : m_lines) | |
| { | |
| const Paragraph& paragraph = m_shape[paragraphIndex++]; | |
| for (const GlyphLine& line : paragraphLines) | |
| { | |
| drawLine = true; | |
| switch (overflow()) | |
| { | |
| case TextOverflow::hidden: | |
| if (effectiveSizing() == TextSizing::fixed) | |
| { | |
| switch (verticalAlign()) | |
| { | |
| case VerticalTextAlign::top: | |
| if (y + line.bottom > effectiveHeight()) | |
| { | |
| goto skipLines; | |
| } | |
| break; | |
| case VerticalTextAlign::middle: | |
| if (y + line.top < | |
| totalHeight / 2 - effectiveHeight() / 2) | |
| { | |
| drawLine = false; | |
| } | |
| if (y + line.bottom > | |
| totalHeight / 2 + effectiveHeight() / 2) | |
| { | |
| drawLine = false; | |
| goto skipLines; | |
| } | |
| break; | |
| case VerticalTextAlign::bottom: | |
| if (y + line.top < | |
| totalHeight - effectiveHeight()) | |
| { | |
| continue; | |
| } | |
| break; | |
| } | |
| } | |
| break; | |
| case TextOverflow::clipped: | |
| if (effectiveSizing() == TextSizing::fixed) | |
| { | |
| switch (verticalAlign()) | |
| { | |
| case VerticalTextAlign::top: | |
| if (y + line.top > effectiveHeight()) | |
| { | |
| goto skipLines; | |
| } | |
| break; | |
| case VerticalTextAlign::middle: | |
| if (y + line.bottom < | |
| totalHeight / 2 - effectiveHeight() / 2) | |
| { | |
| drawLine = false; | |
| } | |
| if (y + line.top > | |
| totalHeight / 2 + effectiveHeight() / 2) | |
| { | |
| goto skipLines; | |
| } | |
| break; | |
| case VerticalTextAlign::bottom: | |
| if (y + line.bottom < | |
| totalHeight - effectiveHeight()) | |
| { | |
| drawLine = false; | |
| } | |
| break; | |
| } | |
| } | |
| 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, | |
| effectiveWidth(), | |
| ellipsisLine == lineIndex, | |
| isEllipsisLineLast, | |
| &m_ellipsisRun)); | |
| } | |
| const OrderedLine& orderedLine = m_orderedLines[lineIndex]; | |
| float x = line.startX; | |
| minX = std::min(x, minX); | |
| 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); | |
| uint32_t textIndex = 0; | |
| uint32_t glyphCount = 0; | |
| if (hasModifiers) | |
| { | |
| textIndex = run->textIndices[glyphIndex]; | |
| glyphCount = m_glyphLookup.count(textIndex); | |
| float centerX = advance / 2.0f; | |
| Mat2D transform = Mat2D::fromScaleAndTranslation(run->size, | |
| run->size, | |
| -centerX, | |
| 0.0f); | |
| for (TextModifierGroup* modifierGroup : m_modifierGroups) | |
| { | |
| float coverage = | |
| modifierGroup->glyphCoverage(textIndex, glyphCount); | |
| modifierGroup->transform(coverage, transform); | |
| } | |
| transform = | |
| Mat2D::fromTranslate(centerX + x + offset.x, | |
| y + line.baseline + offset.y) * | |
| transform; | |
| path.transformInPlace(transform); | |
| } | |
| else | |
| { | |
| path.transformInPlace(Mat2D(run->size, | |
| 0.0f, | |
| 0.0f, | |
| run->size, | |
| x + offset.x, | |
| renderY + offset.y)); | |
| } | |
| assert(run->styleId < m_runs.size()); | |
| TextValueRun* textValueRun = m_runs[run->styleId]; | |
| TextStyle* style = textValueRun->style(); | |
| // TextValueRun::onAddedDirty botches loading if it cannot | |
| // resolve a style, so we're confident we have a style here. | |
| assert(style != nullptr); | |
| // Consider this the "local" opacity. | |
| float opacity = 1.0f; | |
| if (hasModifiers) | |
| { | |
| for (TextModifierGroup* modifierGroup : m_modifierGroups) | |
| { | |
| if (modifierGroup->modifiesOpacity()) | |
| { | |
| float coverage = | |
| modifierGroup->glyphCoverage(textIndex, | |
| glyphCount); | |
| opacity = modifierGroup->computeOpacity(opacity, | |
| coverage); | |
| } | |
| } | |
| } | |
| if (drawLine) | |
| { | |
| if (style->addPath(path, opacity)) | |
| { | |
| // This was the first path added to the style, so let's | |
| // mark it in our draw list. | |
| m_renderStyles.push_back(style); | |
| style->propagateOpacity(renderOpacity()); | |
| } | |
| // Bounds of the glyph | |
| auto rectsItr = textValueRunToRects.find(run->styleId); | |
| if (rectsItr != textValueRunToRects.end()) | |
| { | |
| Vec2D topLeft = Vec2D(x, y + line.top); | |
| Vec2D bottomRight = Vec2D(x + advance, y + line.bottom); | |
| AABB::expandTo(textValueRun->m_localBounds, topLeft); | |
| AABB::expandTo(textValueRun->m_localBounds, | |
| bottomRight); | |
| rectsItr->second.emplace_back(topLeft.x, | |
| topLeft.y, | |
| bottomRight.x, | |
| bottomRight.y); | |
| } | |
| } | |
| x += advance; | |
| } | |
| if (lineIndex == ellipsisLine) | |
| { | |
| goto skipLines; | |
| } | |
| lineIndex++; | |
| } | |
| if (!paragraphLines.empty()) | |
| { | |
| y += paragraphLines.back().bottom; | |
| } | |
| y += paragraphSpace; | |
| } | |
| skipLines: | |
| auto scale = 1.0f; | |
| auto xOffset = -m_bounds.width() * originX(); | |
| auto yOffset = -m_bounds.height() * originY(); | |
| if (overflow() == TextOverflow::fit) | |
| { | |
| auto xScale = (effectiveSizing() != TextSizing::autoWidth && | |
| maxWidth > m_bounds.width()) | |
| ? m_bounds.width() / maxWidth | |
| : 1; | |
| auto baseline = fitFromBaseline() ? m_lines[0][0].baseline : 0; | |
| auto yScale = | |
| (effectiveSizing() == TextSizing::fixed && | |
| totalHeight > m_bounds.height()) | |
| ? (m_bounds.height() - baseline) / (totalHeight - baseline) | |
| : 1; | |
| if (xScale != 1 || yScale != 1) | |
| { | |
| scale = std::max(0.0f, xScale > yScale ? yScale : xScale); | |
| yOffset += baseline * (1 - scale); | |
| switch ((TextAlign)alignValue()) | |
| { | |
| case TextAlign::center: | |
| xOffset += (m_bounds.width() - maxWidth * scale) / 2 - | |
| minX * scale; | |
| break; | |
| case TextAlign::right: | |
| xOffset += | |
| m_bounds.width() - maxWidth * scale - minX * scale; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| if (verticalAlign() != VerticalTextAlign::top) | |
| { | |
| if (effectiveSizing() == TextSizing::fixed) | |
| { | |
| yOffset = -m_bounds.height() * originY(); | |
| if (verticalAlign() == VerticalTextAlign::middle) | |
| { | |
| yOffset += (m_bounds.height() - totalHeight * scale) / 2; | |
| } | |
| else if (verticalAlign() == VerticalTextAlign::bottom) | |
| { | |
| yOffset += m_bounds.height() - totalHeight * scale; | |
| } | |
| } | |
| } | |
| m_transform = | |
| Mat2D::fromScaleAndTranslation(scale, scale, xOffset, yOffset); | |
| #ifdef WITH_RIVE_LAYOUT | |
| markLayoutNodeDirty(); | |
| #endif | |
| for (auto it = textValueRunToRects.begin(); it != textValueRunToRects.end(); | |
| ++it) | |
| { | |
| m_runs[it->first]->m_contours = | |
| RectanglesToContour::makeSelectionContours(it->second); | |
| } | |
| } | |
| const TextStyle* Text::styleFromShaperId(uint16_t id) const | |
| { | |
| assert(id < m_runs.size()); | |
| return m_runs[id]->style(); | |
| } | |
| void Text::draw(Renderer* renderer) | |
| { | |
| ClipResult clipResult = applyClip(renderer); | |
| if (clipResult == ClipResult::noClip) | |
| { | |
| // We didn't clip, so make sure to save as we'll be doing some | |
| // transformations. | |
| renderer->save(); | |
| } | |
| if (clipResult != ClipResult::emptyClip) | |
| { | |
| renderer->transform(m_WorldTransform * m_transform); | |
| if (overflow() == TextOverflow::clipped && m_clipRenderPath) | |
| { | |
| renderer->clipPath(m_clipRenderPath.get()); | |
| } | |
| for (auto style : m_renderStyles) | |
| { | |
| style->draw(renderer); | |
| } | |
| } | |
| renderer->restore(); | |
| } | |
| void Text::addRun(TextValueRun* run) { m_runs.push_back(run); } | |
| void Text::addModifierGroup(TextModifierGroup* group) | |
| { | |
| m_modifierGroups.push_back(group); | |
| } | |
| void Text::markShapeDirty(bool sendToLayout) | |
| { | |
| addDirt(ComponentDirt::Path); | |
| for (TextModifierGroup* group : m_modifierGroups) | |
| { | |
| group->clearRangeMaps(); | |
| } | |
| markWorldTransformDirty(); | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (sendToLayout) | |
| { | |
| markLayoutNodeDirty(); | |
| } | |
| #endif | |
| } | |
| #ifdef WITH_RIVE_LAYOUT | |
| void Text::markLayoutNodeDirty() | |
| { | |
| for (ContainerComponent* p = parent(); p != nullptr; p = p->parent()) | |
| { | |
| if (p->is<LayoutComponent>()) | |
| { | |
| p->as<LayoutComponent>()->markLayoutNodeDirty(); | |
| } | |
| } | |
| } | |
| #endif | |
| void Text::modifierShapeDirty() { addDirt(ComponentDirt::Path); } | |
| void Text::markPaintDirty() { addDirt(ComponentDirt::Paint); } | |
| void Text::alignValueChanged() { markShapeDirty(); } | |
| void Text::sizingValueChanged() { markShapeDirty(); } | |
| void Text::overflowValueChanged() | |
| { | |
| if (effectiveSizing() != TextSizing::autoWidth) | |
| { | |
| markShapeDirty(); | |
| } | |
| } | |
| void Text::widthChanged() | |
| { | |
| if (effectiveSizing() != TextSizing::autoWidth) | |
| { | |
| markShapeDirty(); | |
| } | |
| } | |
| void Text::paragraphSpacingChanged() { markPaintDirty(); } | |
| void Text::heightChanged() | |
| { | |
| if (effectiveSizing() == TextSizing::fixed) | |
| { | |
| markShapeDirty(); | |
| } | |
| } | |
| void StyledText::clear() | |
| { | |
| m_value.clear(); | |
| m_runs.clear(); | |
| } | |
| bool StyledText::empty() const { return m_runs.empty(); } | |
| void StyledText::append(rcp<Font> font, | |
| float size, | |
| float lineHeight, | |
| float letterSpacing, | |
| const std::string& text, | |
| uint16_t styleId) | |
| { | |
| const uint8_t* ptr = (const uint8_t*)text.c_str(); | |
| uint32_t n = 0; | |
| while (*ptr) | |
| { | |
| m_value.push_back(UTF::NextUTF8(&ptr)); | |
| n += 1; | |
| } | |
| m_runs.push_back( | |
| {std::move(font), size, lineHeight, letterSpacing, n, 0, styleId}); | |
| } | |
| bool Text::makeStyled(StyledText& styledText, bool withModifiers) const | |
| { | |
| styledText.clear(); | |
| 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; | |
| } | |
| styledText.append(style->font(), | |
| style->fontSize(), | |
| style->lineHeight(), | |
| style->letterSpacing(), | |
| text, | |
| runIndex++); | |
| } | |
| if (withModifiers) | |
| { | |
| for (TextModifierGroup* group : m_modifierGroups) | |
| { | |
| group->applyShapeModifiers(*this, styledText); | |
| } | |
| } | |
| return !styledText.empty(); | |
| } | |
| SimpleArray<SimpleArray<GlyphLine>> Text::BreakLines( | |
| const SimpleArray<Paragraph>& paragraphs, | |
| float width, | |
| TextAlign align, | |
| TextWrap wrap) | |
| { | |
| 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 || wrap == TextWrap::noWrap) ? -1.0f : width); | |
| if (autoWidth) | |
| { | |
| paragraphWidth = std::max( | |
| paragraphWidth, | |
| GlyphLine::ComputeMaxWidth(lines[paragraphIndex], para.runs)); | |
| } | |
| paragraphIndex++; | |
| } | |
| paragraphIndex = 0; | |
| for (auto& para : paragraphs) | |
| { | |
| GlyphLine::ComputeLineSpacing(paragraphIndex == 0, | |
| lines[paragraphIndex], | |
| para.runs, | |
| paragraphWidth, | |
| align); | |
| paragraphIndex++; | |
| } | |
| return lines; | |
| } | |
| bool Text::modifierRangesNeedShape() const | |
| { | |
| for (const TextModifierGroup* modifierGroup : m_modifierGroups) | |
| { | |
| if (modifierGroup->needsShape()) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void Text::update(ComponentDirt value) | |
| { | |
| Super::update(value); | |
| if (hasDirt(value, ComponentDirt::Path)) | |
| { | |
| // We have modifiers that need shaping we'll need to compute the | |
| // coverage right before we build the actual shape. | |
| bool precomputeModifierCoverage = modifierRangesNeedShape(); | |
| bool parentIsLayoutNotArtboard = | |
| parent()->is<LayoutComponent>() && !parent()->is<Artboard>(); | |
| if (precomputeModifierCoverage) | |
| { | |
| makeStyled(m_modifierStyledText, false); | |
| auto runs = m_modifierStyledText.runs(); | |
| m_modifierShape = | |
| runs[0].font->shapeText(m_modifierStyledText.unichars(), runs); | |
| m_modifierLines = | |
| BreakLines(m_modifierShape, | |
| (effectiveSizing() == TextSizing::autoWidth && | |
| !parentIsLayoutNotArtboard) | |
| ? -1.0f | |
| : effectiveWidth(), | |
| (TextAlign)alignValue(), | |
| wrap()); | |
| m_glyphLookup.compute(m_modifierStyledText.unichars(), | |
| m_modifierShape); | |
| uint32_t textSize = | |
| (uint32_t)m_modifierStyledText.unichars().size(); | |
| for (TextModifierGroup* group : m_modifierGroups) | |
| { | |
| group->computeRangeMap(m_modifierStyledText.unichars(), | |
| m_modifierShape, | |
| m_modifierLines, | |
| m_glyphLookup); | |
| group->computeCoverage(textSize); | |
| } | |
| } | |
| if (makeStyled(m_styledText)) | |
| { | |
| auto runs = m_styledText.runs(); | |
| m_shape = runs[0].font->shapeText(m_styledText.unichars(), runs); | |
| m_lines = BreakLines(m_shape, | |
| (effectiveSizing() == TextSizing::autoWidth && | |
| !parentIsLayoutNotArtboard) | |
| ? -1.0f | |
| : effectiveWidth(), | |
| (TextAlign)alignValue(), | |
| wrap()); | |
| if (!precomputeModifierCoverage && haveModifiers()) | |
| { | |
| m_glyphLookup.compute(m_styledText.unichars(), m_shape); | |
| uint32_t textSize = (uint32_t)m_styledText.unichars().size(); | |
| for (TextModifierGroup* group : m_modifierGroups) | |
| { | |
| group->computeRangeMap(m_styledText.unichars(), | |
| m_shape, | |
| m_lines, | |
| m_glyphLookup); | |
| group->computeCoverage(textSize); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| m_shape = SimpleArray<Paragraph>(); | |
| m_lines = SimpleArray<SimpleArray<GlyphLine>>(); | |
| m_glyphLookup.clear(); | |
| } | |
| m_orderedLines.clear(); | |
| m_ellipsisRun = {}; | |
| // Immediately build render styles so dimensions get computed. | |
| buildRenderStyles(); | |
| } | |
| else if (hasDirt(value, ComponentDirt::Paint)) | |
| { | |
| buildRenderStyles(); | |
| } | |
| else if (hasDirt(value, ComponentDirt::RenderOpacity)) | |
| { | |
| // Note that buildRenderStyles does this too, which is why we can get | |
| // away doing this in the else. | |
| for (TextStyle* style : m_renderStyles) | |
| { | |
| style->propagateOpacity(renderOpacity()); | |
| } | |
| } | |
| } | |
| Vec2D Text::measure(Vec2D maxSize) | |
| { | |
| if (makeStyled(m_styledText)) | |
| { | |
| const float paragraphSpace = paragraphSpacing(); | |
| auto runs = m_styledText.runs(); | |
| auto shape = runs[0].font->shapeText(m_styledText.unichars(), runs); | |
| auto measuringWidth = 0.0f; | |
| switch (effectiveSizing()) | |
| { | |
| case TextSizing::autoHeight: | |
| case TextSizing::fixed: | |
| measuringWidth = width(); | |
| break; | |
| default: | |
| measuringWidth = std::numeric_limits<float>::max(); | |
| break; | |
| } | |
| auto measuringWrap = maxSize.x == std::numeric_limits<float>::max() | |
| ? TextWrap::noWrap | |
| : wrap(); | |
| auto lines = BreakLines(shape, | |
| std::min(maxSize.x, measuringWidth), | |
| (TextAlign)alignValue(), | |
| measuringWrap); | |
| float y = 0; | |
| float computedHeight = 0.0f; | |
| float minY = 0; | |
| int paragraphIndex = 0; | |
| float maxWidth = 0; | |
| if (textOrigin() == TextOrigin::baseline && !lines.empty() && | |
| !lines[0].empty()) | |
| { | |
| y -= lines[0][0].baseline; | |
| minY = y; | |
| } | |
| int ellipsisLine = -1; | |
| bool wantEllipsis = overflow() == TextOverflow::ellipsis && | |
| sizing() == TextSizing::fixed && | |
| verticalAlign() == VerticalTextAlign::top; | |
| for (const SimpleArray<GlyphLine>& paragraphLines : lines) | |
| { | |
| const Paragraph& paragraph = 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 > maxWidth) | |
| { | |
| maxWidth = width; | |
| } | |
| if (wantEllipsis && y + line.bottom > maxSize.y) | |
| { | |
| if (ellipsisLine == -1) | |
| { | |
| // Nothing fits, just show the first line and ellipse | |
| // it. | |
| computedHeight = y + line.bottom; | |
| } | |
| goto doneMeasuring; | |
| } | |
| ellipsisLine++; | |
| computedHeight = y + line.bottom; | |
| } | |
| if (!paragraphLines.empty()) | |
| { | |
| y += paragraphLines.back().bottom; | |
| } | |
| y += paragraphSpace; | |
| } | |
| doneMeasuring: | |
| Vec2D bounds; | |
| switch (sizing()) | |
| { | |
| case TextSizing::autoWidth: | |
| bounds = Vec2D(maxWidth, std::max(minY, computedHeight)); | |
| break; | |
| case TextSizing::autoHeight: | |
| bounds = Vec2D(width(), std::max(minY, computedHeight)); | |
| break; | |
| case TextSizing::fixed: | |
| bounds = Vec2D(width(), minY + height()); | |
| break; | |
| } | |
| return Vec2D(std::min(maxSize.x, bounds.x), | |
| std::min(maxSize.y, bounds.y)); | |
| } | |
| return Vec2D(); | |
| } | |
| AABB Text::localBounds() const | |
| { | |
| float width = m_bounds.width(); | |
| float height = m_bounds.height(); | |
| return AABB::fromLTWH(m_bounds.minX - width * originX(), | |
| m_bounds.minY - height * originY(), | |
| width, | |
| height); | |
| } | |
| Core* Text::hitTest(HitInfo*, const Mat2D&) | |
| { | |
| if (renderOpacity() == 0.0f) | |
| { | |
| return nullptr; | |
| } | |
| return nullptr; | |
| } | |
| void Text::originValueChanged() | |
| { | |
| markPaintDirty(); | |
| markWorldTransformDirty(); | |
| } | |
| void Text::originXChanged() | |
| { | |
| markPaintDirty(); | |
| markWorldTransformDirty(); | |
| } | |
| void Text::originYChanged() | |
| { | |
| markPaintDirty(); | |
| markWorldTransformDirty(); | |
| } | |
| #else | |
| // Text disabled. | |
| void Text::draw(Renderer* renderer) {} | |
| Core* Text::hitTest(HitInfo*, const Mat2D&) { return nullptr; } | |
| void Text::addRun(TextValueRun* run) {} | |
| void Text::addModifierGroup(TextModifierGroup* group) {} | |
| void Text::markShapeDirty(bool sendToLayout) {} | |
| void Text::update(ComponentDirt value) {} | |
| void Text::alignValueChanged() {} | |
| void Text::sizingValueChanged() {} | |
| void Text::overflowValueChanged() {} | |
| void Text::widthChanged() {} | |
| void Text::heightChanged() {} | |
| void Text::markPaintDirty() {} | |
| void Text::modifierShapeDirty() {} | |
| bool Text::modifierRangesNeedShape() const { return false; } | |
| const TextStyle* Text::styleFromShaperId(uint16_t id) const { return nullptr; } | |
| void Text::paragraphSpacingChanged() {} | |
| AABB Text::localBounds() const { return AABB(); } | |
| void Text::originValueChanged() {} | |
| void Text::originXChanged() {} | |
| void Text::originYChanged() {} | |
| Vec2D Text::measureLayout(float width, | |
| LayoutMeasureMode widthMode, | |
| float height, | |
| LayoutMeasureMode heightMode) | |
| { | |
| return Vec2D(); | |
| } | |
| void Text::controlSize(Vec2D size, | |
| LayoutScaleType widthScaleType, | |
| LayoutScaleType heightScaleType) | |
| {} | |
| #endif |