| #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); |
| } |