| #include "rive/text/text.hpp" |
| #include "rive/text/text_modifier_group.hpp" |
| #include "rive/text/text_shape_modifier.hpp" |
| #include "rive/text/text_modifier_range.hpp" |
| #include "rive/text/glyph_lookup.hpp" |
| #include "rive/text/text_style.hpp" |
| #include "rive/artboard.hpp" |
| #include <limits> |
| |
| using namespace rive; |
| |
| StatusCode TextModifierGroup::onAddedDirty(CoreContext* context) |
| { |
| StatusCode code = Super::onAddedDirty(context); |
| if (code != StatusCode::Ok) |
| { |
| return code; |
| } |
| |
| if (parent() != nullptr && parent()->is<Text>()) |
| { |
| parent()->as<Text>()->addModifierGroup(this); |
| return StatusCode::Ok; |
| } |
| |
| return StatusCode::MissingObject; |
| } |
| |
| void TextModifierGroup::addModifierRange(TextModifierRange* range) { m_ranges.push_back(range); } |
| |
| void TextModifierGroup::addModifier(TextModifier* modifier) |
| { |
| m_modifiers.push_back(modifier); |
| if (modifier->is<TextShapeModifier>()) |
| { |
| m_shapeModifiers.push_back(modifier->as<TextShapeModifier>()); |
| } |
| } |
| |
| void TextModifierGroup::rangeTypeChanged() |
| { |
| parent()->as<Text>()->modifierShapeDirty(); |
| addDirt(ComponentDirt::TextCoverage); |
| } |
| |
| void TextModifierGroup::shapeModifierChanged() { parent()->as<Text>()->markShapeDirty(); } |
| |
| void TextModifierGroup::rangeChanged() |
| { |
| /// Marking shape dirty should only be done if this modifer group changes |
| /// shaping properties (for now we're just testing and we're hardcoding a |
| /// shaping change). |
| if (!m_shapeModifiers.empty()) |
| { |
| parent()->as<Text>()->modifierShapeDirty(); |
| } |
| else |
| { |
| parent()->as<Text>()->markPaintDirty(); |
| } |
| addDirt(ComponentDirt::TextCoverage); |
| } |
| |
| /// Clear any cached selector range maps so they can be recomputed after next |
| /// shaping. |
| void TextModifierGroup::clearRangeMaps() |
| { |
| for (TextModifierRange* range : m_ranges) |
| { |
| range->clearRangeMap(); |
| } |
| addDirt(ComponentDirt::TextCoverage); |
| } |
| |
| void TextModifierGroup::computeRangeMap(Span<const Unichar> text, |
| const SimpleArray<Paragraph>& shape, |
| const SimpleArray<SimpleArray<GlyphLine>>& lines, |
| const GlyphLookup& glyphLookup) |
| { |
| for (TextModifierRange* range : m_ranges) |
| { |
| range->computeRange(text, shape, lines, glyphLookup); |
| } |
| } |
| |
| void TextModifierGroup::computeCoverage(uint32_t textSize) |
| { |
| if (!hasDirt(ComponentDirt::TextCoverage)) |
| { |
| return; |
| } |
| |
| // Because we're not dependent on anything we need to reset our dirt |
| // ourselves. We're not in the DAG so we'll never get reset. |
| m_Dirt = ComponentDirt::None; |
| |
| // When the text re-shapes, we udpate our coverage values. |
| m_coverage.resize(textSize); |
| std::fill(m_coverage.begin(), m_coverage.end(), 0); |
| for (TextModifierRange* range : m_ranges) |
| { |
| range->computeCoverage(m_coverage); |
| } |
| } |
| |
| float TextModifierGroup::glyphCoverage(uint32_t textIndex, uint32_t codePointCount) |
| { |
| assert(codePointCount >= 1); |
| float c = coverage(textIndex); |
| |
| for (uint32_t i = 1; i < codePointCount; i++) |
| { |
| c += coverage(textIndex + i); |
| } |
| return c /= (float)codePointCount; |
| } |
| |
| void TextModifierGroup::transform(float amount, Mat2D& ctm) |
| { |
| if (amount == 0.0f || !modifiesTransform()) |
| { |
| return; |
| } |
| |
| float actualRotation = modifiesRotation() ? rotation() * amount : 0.0f; |
| Mat2D transform = actualRotation != 0.0f ? Mat2D::fromRotation(actualRotation) : Mat2D(); |
| if (modifiesTranslation()) |
| { |
| transform[4] = x() * amount; |
| transform[5] = y() * amount; |
| } |
| if (modifiesScale()) |
| { |
| float iamount = 1.0f - amount; |
| transform.scaleByValues(iamount + scaleX() * amount, iamount + scaleY() * amount); |
| } |
| bool doesModifyOrigin = modifiesOrigin(); |
| if (doesModifyOrigin) |
| { |
| ctm[4] += originX(); |
| ctm[5] += originY(); |
| } |
| ctm = transform * ctm; |
| if (doesModifyOrigin) |
| { |
| ctm[4] -= originX(); |
| ctm[5] -= originY(); |
| } |
| } |
| |
| float TextModifierGroup::computeOpacity(float current, float t) const |
| { |
| if ((modifierFlags() & (uint32_t)TextModifierFlags::invertOpacity) != 0) |
| { |
| return current * (1.0f - t) + opacity() * t; |
| } |
| else |
| { |
| return current * opacity() * t; |
| } |
| } |
| |
| void TextModifierGroup::modifierFlagsChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::originXChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::originYChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::opacityChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::xChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::yChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::rotationChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::scaleXChanged() { parent()->as<Text>()->markPaintDirty(); } |
| void TextModifierGroup::scaleYChanged() { parent()->as<Text>()->markPaintDirty(); } |
| |
| static TextRun copyRun(const TextRun& source, uint32_t unicharCount) |
| { |
| return { |
| source.font, |
| source.size, |
| source.lineHeight, |
| source.letterSpacing, |
| unicharCount, |
| source.script, |
| source.styleId, |
| source.dir, |
| }; |
| } |
| |
| TextRun TextModifierGroup::modifyShape(const Text& text, TextRun run, float strength) |
| { |
| const TextStyle* style = text.styleFromShaperId(run.styleId); |
| if (style == nullptr || style->font() == nullptr) |
| { |
| return run; |
| } |
| |
| Font* font = style->font().get(); |
| |
| std::unordered_map<uint32_t, float> variations; |
| float fontSize = run.size; |
| for (TextShapeModifier* shapeModifier : m_shapeModifiers) |
| { |
| fontSize = shapeModifier->modify(font, variations, fontSize, strength); |
| } |
| |
| if (!variations.empty()) |
| { |
| m_variationCoords.clear(); |
| for (auto itr = variations.begin(); itr != variations.end(); itr++) |
| { |
| m_variationCoords.push_back({itr->first, itr->second}); |
| } |
| m_variableFont = font->makeAtCoords(m_variationCoords); |
| return { |
| m_variableFont, |
| run.size, |
| run.lineHeight, |
| run.letterSpacing, |
| run.unicharCount, |
| run.script, |
| run.styleId, |
| run.dir, |
| }; |
| } |
| else |
| { |
| m_variableFont = nullptr; |
| } |
| return run; |
| } |
| |
| void TextModifierGroup::applyShapeModifiers(const Text& text, StyledText& styledText) |
| { |
| if (m_shapeModifiers.empty()) |
| { |
| return; |
| } |
| m_nextTextRuns.clear(); |
| m_nextTextRuns.reserve(styledText.runs().size()); |
| |
| uint32_t index = 0; |
| float lastCoverage = std::numeric_limits<float>::max(); |
| uint32_t extractRunIndex = 0; |
| |
| for (const TextRun& run : styledText.runs()) |
| { |
| // Split the run into sub-runs as necessary based on equal coverage |
| // values. |
| uint32_t end = index + run.unicharCount; |
| |
| while (index < end) |
| { |
| float coverage = m_coverage[index]; |
| if (coverage != lastCoverage) |
| { |
| if (index - extractRunIndex != 0) |
| { |
| // Add new run from extractRunStart to index (exclusive) |
| if (lastCoverage == 0.0f) |
| { |
| m_nextTextRuns.push_back(copyRun(run, index - extractRunIndex)); |
| } |
| else |
| { |
| m_nextTextRuns.push_back( |
| modifyShape(text, copyRun(run, index - extractRunIndex), lastCoverage)); |
| } |
| } |
| lastCoverage = coverage; |
| extractRunIndex = index; |
| } |
| index++; |
| } |
| |
| assert(extractRunIndex != end); |
| // Add new run from extractRunStart to index (exclusive) |
| if (lastCoverage == 0.0f) |
| { |
| m_nextTextRuns.push_back(copyRun(run, end - extractRunIndex)); |
| } |
| else |
| { |
| m_nextTextRuns.push_back( |
| modifyShape(text, copyRun(run, end - extractRunIndex), lastCoverage)); |
| } |
| extractRunIndex = end; |
| } |
| styledText.swapRuns(m_nextTextRuns); |
| // return nextTextRuns; |
| } |
| |
| bool TextModifierGroup::needsShape() const |
| { |
| if (!m_shapeModifiers.empty()) |
| { |
| return true; |
| } |
| for (const TextModifierRange* range : m_ranges) |
| { |
| if (range->needsShape()) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |