| #include "rive/text/text.hpp" | |
| #include "rive/text/text_modifier_range.hpp" | |
| #include "rive/text/text_modifier_group.hpp" | |
| #include "rive/text/text_value_run.hpp" | |
| #include "rive/text/glyph_lookup.hpp" | |
| #include "rive/animation/cubic_interpolator_component.hpp" | |
| #include "rive/core_context.hpp" | |
| using namespace rive; | |
| StatusCode TextModifierRange::onAddedDirty(CoreContext* context) | |
| { | |
| StatusCode code = Super::onAddedDirty(context); | |
| if (code != StatusCode::Ok) | |
| { | |
| return code; | |
| } | |
| if (runId() != Core::emptyId) | |
| { | |
| auto coreObject = context->resolve(runId()); | |
| if (coreObject == nullptr || !coreObject->is<TextValueRun>()) | |
| { | |
| return StatusCode::MissingObject; | |
| } | |
| m_run = static_cast<TextValueRun*>(coreObject); | |
| } | |
| if (parent() != nullptr && parent()->is<TextModifierGroup>()) | |
| { | |
| parent()->as<TextModifierGroup>()->addModifierRange(this); | |
| return StatusCode::Ok; | |
| } | |
| return StatusCode::MissingObject; | |
| } | |
| void TextModifierRange::addChild(Component* component) | |
| { | |
| Super::addChild(component); | |
| if (component->is<CubicInterpolatorComponent>()) | |
| { | |
| // mark this as the cubic interpolator. | |
| m_interpolator = component->as<CubicInterpolatorComponent>(); | |
| } | |
| } | |
| void TextModifierRange::clearRangeMap() { m_rangeMapper.clear(); } | |
| void TextModifierRange::computeRange(Span<const Unichar> text, | |
| const SimpleArray<Paragraph>& shape, | |
| const SimpleArray<SimpleArray<GlyphLine>>& lines, | |
| const GlyphLookup& glyphLookup) | |
| { | |
| // Check if the range mapper is still valid. | |
| if (!m_rangeMapper.empty()) | |
| { | |
| return; | |
| } | |
| uint32_t start = 0; | |
| uint32_t end = (uint32_t)text.size(); | |
| if (m_run != nullptr) | |
| { | |
| start = m_run->offset(); | |
| end = start + m_run->length(); | |
| } | |
| switch (units()) | |
| { | |
| case TextRangeUnits::charactersExcludingSpaces: | |
| m_rangeMapper.fromCharacters(text, start, end, glyphLookup, true); | |
| break; | |
| case TextRangeUnits::words: | |
| m_rangeMapper.fromWords(text, start, end); | |
| break; | |
| case TextRangeUnits::lines: | |
| m_rangeMapper.fromLines(text, start, end, shape, lines, glyphLookup); | |
| break; | |
| default: | |
| m_rangeMapper.fromCharacters(text, start, end, glyphLookup); | |
| break; | |
| } | |
| } | |
| float TextModifierRange::coverageAt(float t) | |
| { | |
| float c = 0.0f; | |
| if (m_indexTo >= m_indexFrom) | |
| { | |
| if (t < m_indexFrom || t > m_indexTo) | |
| { | |
| c = 0.0f; | |
| } | |
| else if (t < m_indexFalloffFrom) | |
| { | |
| float range = std::max(0.0f, m_indexFalloffFrom - m_indexFrom); | |
| c = range == 0.0f ? 1.0f : std::max(0.0f, t - m_indexFrom) / range; | |
| if (m_interpolator != nullptr) | |
| { | |
| c = m_interpolator->transform(c); | |
| } | |
| } | |
| else if (t > m_indexFalloffTo) | |
| { | |
| float range = std::max(0.0f, m_indexTo - m_indexFalloffTo); | |
| c = range == 0.0f ? 1.0f : 1.0f - std::min(1.0f, (t - m_indexFalloffTo) / range); | |
| if (m_interpolator != nullptr) | |
| { | |
| c = m_interpolator->transform(c); | |
| } | |
| } | |
| else | |
| { | |
| c = 1.0f; | |
| } | |
| } | |
| return c; | |
| } | |
| void TextModifierRange::computeCoverage(Span<float> coverage) | |
| { | |
| if (m_rangeMapper.empty()) | |
| { | |
| return; | |
| } | |
| // Compute range mapped coverage. | |
| uint32_t count = m_rangeMapper.unitCount(); | |
| switch (type()) | |
| { | |
| case TextRangeType::unitIndex: | |
| m_indexFrom = offsetModifyFrom(); | |
| m_indexTo = offsetModifyTo(); | |
| m_indexFalloffFrom = offsetFalloffFrom(); | |
| m_indexFalloffTo = offsetFalloffTo(); | |
| break; | |
| case TextRangeType::percentage: | |
| m_indexFrom = count * offsetModifyFrom(); | |
| m_indexTo = count * offsetModifyTo(); | |
| m_indexFalloffFrom = count * offsetFalloffFrom(); | |
| m_indexFalloffTo = count * offsetFalloffTo(); | |
| break; | |
| } | |
| TextRangeMode rangeMode = mode(); | |
| for (uint32_t unitIndex = 0; unitIndex < count; unitIndex++) | |
| { | |
| uint32_t unitLength = (uint32_t)m_rangeMapper.unitLength(unitIndex); | |
| uint32_t characterIndex = m_rangeMapper.unitCharacterIndex(unitIndex); | |
| float c = strength() * coverageAt(unitIndex + 0.5f); | |
| for (uint32_t i = 0; i < unitLength; i++) | |
| { | |
| assert((characterIndex + i) < (uint32_t)coverage.size()); | |
| float current = coverage[characterIndex + i]; | |
| switch (rangeMode) | |
| { | |
| case TextRangeMode::add: | |
| current += c; | |
| break; | |
| case TextRangeMode::subtract: | |
| current -= c; | |
| break; | |
| case TextRangeMode::max: | |
| current = std::max(current, c); | |
| break; | |
| case TextRangeMode::min: | |
| current = std::min(current, c); | |
| break; | |
| case TextRangeMode::multiply: | |
| current *= c; | |
| break; | |
| case TextRangeMode::difference: | |
| current = std::abs(current - c); | |
| break; | |
| } | |
| coverage[characterIndex + i] = | |
| clamp() ? std::max(0.0f, std::min(1.0f, current)) : current; | |
| } | |
| // Set space between units coverage to 0. | |
| if (unitIndex + 1 < m_rangeMapper.unitCharacterIndexCount()) | |
| { | |
| uint32_t nextCharacterIndex = m_rangeMapper.unitCharacterIndex(unitIndex + 1); | |
| for (uint32_t i = characterIndex + unitLength; i < nextCharacterIndex; i++) | |
| { | |
| coverage[i] = 0; | |
| } | |
| } | |
| } | |
| } | |
| void TextModifierRange::modifyFromChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::modifyToChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::strengthChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::unitsValueChanged() | |
| { | |
| parent()->as<TextModifierGroup>()->rangeTypeChanged(); | |
| } | |
| void TextModifierRange::typeValueChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::modeValueChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::clampChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::falloffFromChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::falloffToChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| void TextModifierRange::offsetChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); } | |
| bool TextModifierRange::needsShape() const { return units() == TextRangeUnits::lines; } | |
| void RangeMapper::clear() | |
| { | |
| m_unitCharacterIndices.clear(); | |
| m_unitLengths.clear(); | |
| } | |
| float RangeMapper::unitToCharacterRange(float word) const | |
| { | |
| if (m_unitCharacterIndices.empty()) | |
| { | |
| return 0.0f; | |
| } | |
| float clampedUnit = std::min(std::max(word, 0.0f), (float)(m_unitCharacterIndices.size() - 1)); | |
| size_t intUnit = (size_t)clampedUnit; | |
| float characters = (float)m_unitCharacterIndices[intUnit]; | |
| if (intUnit < m_unitLengths.size()) | |
| { | |
| float fraction = clampedUnit - intUnit; | |
| characters += m_unitLengths[intUnit] * fraction; | |
| } | |
| return characters; | |
| } | |
| void RangeMapper::addRange(uint32_t indexFrom, | |
| uint32_t indexTo, | |
| uint32_t startOffset, | |
| uint32_t endOffset) | |
| { | |
| if (indexTo > startOffset && endOffset > indexFrom) | |
| { | |
| uint32_t actualStart = std::max(startOffset, indexFrom); | |
| uint32_t actualEnd = std::min(endOffset, indexTo); | |
| if (actualEnd > actualStart) | |
| { | |
| m_unitCharacterIndices.push_back(actualStart); | |
| m_unitLengths.push_back(actualEnd - actualStart); | |
| } | |
| } | |
| } | |
| void RangeMapper::fromWords(Span<const Unichar> text, uint32_t start, uint32_t end) | |
| { | |
| if (text.empty()) | |
| { | |
| return; | |
| } | |
| bool wantWhiteSpace = false; | |
| uint32_t characterCount = 0; | |
| uint32_t index = 0; | |
| uint32_t indexFrom = 0; | |
| for (Unichar unit : text) | |
| { | |
| if (wantWhiteSpace == isWhiteSpace(unit)) | |
| { | |
| if (!wantWhiteSpace) | |
| { | |
| indexFrom = index; | |
| } | |
| else | |
| { | |
| addRange(indexFrom, indexFrom + characterCount, start, end); | |
| characterCount = 0; | |
| } | |
| wantWhiteSpace = !wantWhiteSpace; | |
| } | |
| if (wantWhiteSpace) | |
| { | |
| characterCount++; | |
| } | |
| index++; | |
| } | |
| if (characterCount > 0) | |
| { | |
| addRange(indexFrom, indexFrom + characterCount, start, end); | |
| } | |
| m_unitCharacterIndices.push_back(end); | |
| } | |
| void RangeMapper::fromCharacters(Span<const Unichar> text, | |
| uint32_t start, | |
| uint32_t end, | |
| const GlyphLookup& glyphLookup, | |
| bool withoutSpaces) | |
| { | |
| if (text.empty()) | |
| { | |
| return; | |
| } | |
| for (uint32_t i = start; i < end;) | |
| { | |
| Unichar unit = text[i]; | |
| if (withoutSpaces && isWhiteSpace(unit)) | |
| { | |
| i++; | |
| continue; | |
| } | |
| // Don't break ligated glyphs. | |
| uint32_t codePoints = glyphLookup.count(i); | |
| m_unitCharacterIndices.push_back(i); | |
| m_unitLengths.push_back(codePoints); | |
| i += codePoints; | |
| } | |
| m_unitCharacterIndices.push_back(end); | |
| } | |
| void RangeMapper::fromLines(Span<const Unichar> text, | |
| uint32_t start, | |
| uint32_t end, | |
| const SimpleArray<Paragraph>& shape, | |
| const SimpleArray<SimpleArray<GlyphLine>>& lines, | |
| const GlyphLookup& glyphLookup) | |
| { | |
| if (text.empty()) | |
| { | |
| return; | |
| } | |
| uint32_t paragraphIndex = 0; | |
| for (const SimpleArray<GlyphLine>& paragraphLines : lines) | |
| { | |
| const Paragraph& paragraph = shape[paragraphIndex++]; | |
| const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs; | |
| for (const GlyphLine& line : paragraphLines) | |
| { | |
| const GlyphRun& rf = glyphRuns[line.startRunIndex]; | |
| uint32_t indexFrom = rf.textIndices[line.startGlyphIndex]; | |
| const GlyphRun& rt = glyphRuns[line.endRunIndex]; | |
| uint32_t endGlyphIndex = line.endGlyphIndex == 0 ? 0 : line.endGlyphIndex - 1; | |
| uint32_t indexTo = rt.textIndices[endGlyphIndex]; | |
| indexTo += glyphLookup.count(indexTo); | |
| addRange(indexFrom, indexTo, start, end); | |
| } | |
| } | |
| m_unitCharacterIndices.push_back(end); | |
| } |