| #include "rive/text/text.hpp" | |
| #include "rive/text/text_modifier_group.hpp" | |
| #include "rive/text/text_follow_path_modifier.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_paint.hpp" | |
| #include "rive/artboard.hpp" | |
| #include <limits> | |
| using namespace rive; | |
| Text* TextModifierGroup::textComponent() const | |
| { | |
| if (parent() != nullptr && parent()->is<Text>()) | |
| { | |
| return parent()->as<Text>(); | |
| } | |
| return nullptr; | |
| } | |
| StatusCode TextModifierGroup::onAddedDirty(CoreContext* context) | |
| { | |
| StatusCode code = Super::onAddedDirty(context); | |
| if (code != StatusCode::Ok) | |
| { | |
| return code; | |
| } | |
| if (parent() != nullptr && parent()->is<Text>()) | |
| { | |
| #ifndef WITH_RIVE_EDITOR | |
| // Runtime-only; editor build registers via editorParentChanged. | |
| parent()->as<Text>()->addModifierGroup(this); | |
| #endif | |
| 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>()); | |
| } | |
| if (modifier->is<TextFollowPathModifier>()) | |
| { | |
| m_followPathModifiers.push_back(modifier->as<TextFollowPathModifier>()); | |
| } | |
| } | |
| 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::onTextWorldTransformDirty() | |
| { | |
| bool followsPath = !m_followPathModifiers.empty(); | |
| if (followsPath) | |
| { | |
| textComponent()->addDirt(ComponentDirt::Path); | |
| } | |
| } | |
| void TextModifierGroup::resetTextFollowPath() | |
| { | |
| Text* text = textComponent(); | |
| Mat2D inverseText; | |
| if (text == nullptr || !text->worldTransform().invert(&inverseText)) | |
| { | |
| return; | |
| } | |
| for (TextFollowPathModifier* modifier : m_followPathModifiers) | |
| { | |
| modifier->reset(&inverseText); | |
| } | |
| } | |
| void TextModifierGroup::transform( | |
| float amount, | |
| Mat2D& ctm, | |
| TransformGlyphArg& arg) | |
| { | |
| bool followsPath = !m_followPathModifiers.empty(); | |
| if (amount == 0.0f || (!modifiesTransform() && !followsPath)) | |
| { | |
| return; | |
| } | |
| TransformComponents parts; | |
| if (followsPath) | |
| { | |
| TransformComponents tc; | |
| tc.x(arg.originPosition.x); | |
| tc.y(arg.originPosition.y); | |
| if (modifiesTranslation()) | |
| { | |
| arg.offset = {x(), y()}; | |
| } | |
| for (TextFollowPathModifier* modifier : m_followPathModifiers) | |
| { | |
| tc = modifier->transformGlyph(tc, arg); | |
| } | |
| Vec2D posDiff = tc.translation() - arg.originPosition; | |
| // Commit the effect of the follow path modifiers to parts | |
| parts.rotation(parts.rotation() + tc.rotation() * amount); | |
| parts.x(posDiff.x * amount); | |
| parts.y(posDiff.y * amount); | |
| } | |
| else if (modifiesTranslation()) | |
| { | |
| parts.x(x() * amount); | |
| parts.y(y() * amount); | |
| } | |
| if (modifiesScale()) | |
| { | |
| float iamount = 1.0f - amount; | |
| parts.scaleX(iamount + scaleX() * amount); | |
| parts.scaleY(iamount + scaleY() * amount); | |
| } | |
| if (modifiesRotation()) | |
| { | |
| parts.rotation(parts.rotation() + rotation() * amount); | |
| } | |
| Mat2D transform = Mat2D::compose(parts); | |
| 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.level, | |
| }; | |
| } | |
| TextRun TextModifierGroup::modifyShape( | |
| const Text& text, | |
| TextRun run, | |
| float strength) | |
| { | |
| const TextStylePaint* 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()) | |
| { | |
| std::vector<Font::Coord> coords; | |
| coords.reserve(variations.size()); | |
| for (auto itr = variations.begin(); itr != variations.end(); itr++) | |
| { | |
| coords.push_back({itr->first, itr->second}); | |
| } | |
| // Every styling pass lands here, and the text styles itself both to | |
| // measure and to shape. A new font per pass meant the text could | |
| // never tell that its layout had already measured the shape | |
| // (Text::measureKey), so it re-solved after every one. Keep the font | |
| // while it comes from the same base at the same coordinates. | |
| bool same = m_variableFont != nullptr && m_variableFontBase.get() == font && | |
| coords.size() == m_variationCoords.size(); | |
| for (size_t i = 0; same && i < coords.size(); i++) | |
| { | |
| same = coords[i].axis == m_variationCoords[i].axis && | |
| coords[i].value == m_variationCoords[i].value; | |
| } | |
| if (!same) | |
| { | |
| m_variationCoords = std::move(coords); | |
| m_variableFont = font->makeAtCoords(m_variationCoords); | |
| m_variableFontBase = style->font(); | |
| } | |
| return { | |
| m_variableFont, | |
| run.size, | |
| run.lineHeight, | |
| run.letterSpacing, | |
| run.unicharCount, | |
| run.script, | |
| run.styleId, | |
| run.level, | |
| }; | |
| } | |
| else | |
| { | |
| m_variableFont = nullptr; | |
| m_variableFontBase = 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; | |
| auto coverageSize = m_coverage.size(); | |
| while (index < end && index < coverageSize) | |
| { | |
| 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; | |
| } |