| #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/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" |
| |
| 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; |
| } |
| |
| 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(); |
| } |
| } |
| |
| // Now sort the runs visually. |
| if (paragraph.baseDirection == TextDirection::ltr || logicalRuns.empty()) |
| { |
| 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); |
| } |
| } |
| |
| 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; |
| |
| 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()}}; |
| 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; |
| } |
| |
| void Text::buildRenderStyles() |
| { |
| for (TextStyle* style : m_renderStyles) |
| { |
| style->rewindPath(); |
| } |
| m_renderStyles.clear(); |
| if (m_shape.size() == 0) |
| { |
| m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f); |
| return; |
| } |
| 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 && 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 > maxWidth) |
| { |
| maxWidth = width; |
| } |
| lastLineIndex++; |
| if (wantEllipsis && y + line.bottom <= height()) |
| { |
| ellipsisLine++; |
| } |
| } |
| |
| if (!paragraphLines.empty()) |
| { |
| y += paragraphLines.back().bottom; |
| } |
| y += paragraphSpace; |
| } |
| 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 (sizing()) |
| { |
| 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, width(), std::max(minY, y - paragraphSpace)); |
| break; |
| case TextSizing::fixed: |
| m_bounds = AABB(0.0f, minY, width(), minY + height()); |
| 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.minY, bounds.width(), bounds.height()); |
| } |
| else |
| { |
| m_clipRenderPath = nullptr; |
| } |
| |
| y = -m_bounds.height() * originY(); |
| 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); |
| } |
| } |
| for (const SimpleArray<GlyphLine>& paragraphLines : m_lines) |
| { |
| const Paragraph& paragraph = m_shape[paragraphIndex++]; |
| for (const GlyphLine& line : paragraphLines) |
| { |
| switch (overflow()) |
| { |
| case TextOverflow::hidden: |
| if (sizing() == TextSizing::fixed && y + line.bottom > height()) |
| { |
| return; |
| } |
| break; |
| case TextOverflow::clipped: |
| if (sizing() == TextSizing::fixed && y + line.top > height()) |
| { |
| 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, |
| width(), |
| ellipsisLine == lineIndex, |
| isEllipsisLineLast, |
| &m_ellipsisRun)); |
| } |
| |
| const OrderedLine& orderedLine = m_orderedLines[lineIndex]; |
| float x = -m_bounds.width() * originX() + 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); |
| |
| 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)); |
| } |
| |
| x += advance; |
| |
| 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); |
| // 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 (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()); |
| } |
| } |
| if (lineIndex == ellipsisLine) |
| { |
| return; |
| } |
| lineIndex++; |
| } |
| if (!paragraphLines.empty()) |
| { |
| y += paragraphLines.back().bottom; |
| } |
| y += paragraphSpace; |
| } |
| } |
| |
| 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 = clip(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); |
| 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() |
| { |
| addDirt(ComponentDirt::Path); |
| for (TextModifierGroup* group : m_modifierGroups) |
| { |
| group->clearRangeMaps(); |
| } |
| markWorldTransformDirty(); |
| } |
| |
| void Text::modifierShapeDirty() { addDirt(ComponentDirt::Path); } |
| |
| void Text::markPaintDirty() { addDirt(ComponentDirt::Paint); } |
| |
| 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::paragraphSpacingChanged() { markPaintDirty(); } |
| |
| void Text::heightChanged() |
| { |
| if (sizing() == 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(); |
| } |
| |
| 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(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(); |
| 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, |
| sizing() == TextSizing::autoWidth ? -1.0f : width(), |
| (TextAlign)alignValue()); |
| 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, |
| sizing() == TextSizing::autoWidth ? -1.0f : width(), |
| (TextAlign)alignValue()); |
| 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()); |
| } |
| } |
| } |
| |
| 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() {} |
| 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() {} |
| #endif |