blob: 9d8c2dda99275725e589045060f7822507a2ece3 [file]
#include "rive/text/text_style.hpp"
#include "rive/text/text_style_axis.hpp"
#include "rive/text/text_style_feature.hpp"
#include "rive/text/text_variation_helper.hpp"
#include "rive/renderer.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/backboard.hpp"
#include "rive/importers/backboard_importer.hpp"
#include "rive/text/text.hpp"
#include "rive/artboard.hpp"
#include "rive/file.hpp"
using namespace rive;
// satisfy unique_ptr
TextStyle::TextStyle() = default;
void TextStyle::addVariation(TextStyleAxis* axis)
{
m_variations.push_back(axis);
}
void TextStyle::addFeature(TextStyleFeature* feature)
{
m_styleFeatures.push_back(feature);
}
void TextStyle::onDirty(ComponentDirt dirt)
{
// This can happen if the Text is in a Solo which is propagating its
// collapsed state during its onAddedClean (which could be called before
// ours, so m_text might still be null).
if (m_text != nullptr)
{
if ((dirt & ComponentDirt::TextShape) == ComponentDirt::TextShape)
{
m_text->markShapeDirty();
if (m_variationHelper != nullptr)
{
m_variationHelper->addDirt(ComponentDirt::TextShape);
}
}
}
else if (
m_owner != nullptr &&
(dirt & ComponentDirt::TextShape) == ComponentDirt::TextShape)
{
// A global style's paint: rebuild its variable font, and let the Texts
// drawing it re-shape.
if (m_variationHelper != nullptr)
{
m_variationHelper->addDirt(ComponentDirt::TextShape);
}
m_owner->styleShapeChanged();
}
}
StatusCode TextStyle::onAddedClean(CoreContext* context)
{
// We know this is good because we validated it during validate().
m_text = TextInterface::from(parent());
auto code = Super::onAddedClean(context);
if (code != StatusCode::Ok)
{
return code;
}
// This ensures context propagates to variation helper too.
if (!m_variations.empty() || !m_styleFeatures.empty())
{
m_variationHelper = std::make_unique<TextVariationHelper>(this);
}
if (m_variationHelper != nullptr)
{
if ((code = m_variationHelper->onAddedDirty(context)) != StatusCode::Ok)
{
return code;
}
if ((code = m_variationHelper->onAddedClean(context)) != StatusCode::Ok)
{
return code;
}
}
return StatusCode::Ok;
}
const rcp<Font> TextStyle::font() const
{
if (m_variableFont != nullptr)
{
return m_variableFont;
}
// Lazily build the variable font if variations/features are present but
// the variable font hasn't been created yet. This ensures the very first
// shape/measure uses the intended axes (e.g. wght) rather than the font's
// default axis values.
if ((!m_variations.empty() || !m_styleFeatures.empty()))
{
updateVariableFont();
if (m_variableFont != nullptr)
{
return m_variableFont;
}
}
auto asset = fontAsset();
return asset == nullptr ? nullptr : asset->font();
}
void TextStyle::updateVariableFont() const
{
auto asset = fontAsset();
rcp<Font> baseFont = asset == nullptr ? nullptr : asset->font();
if (baseFont == nullptr)
{
// Not ready yet, or the font was cleared. Drop any cached variable
// font so font() can't keep returning one built from the old base.
m_variableFont = nullptr;
m_variableFontBase = nullptr;
return;
}
if (!m_variations.empty() || !m_styleFeatures.empty())
{
// The variation helper calls this whenever it updates, which is far
// more often than the axes change. Building a new font each time cost
// a font object per call, and it gave the text a different font every
// frame, so the text could never tell that its layout had already
// measured the shape (Text::measureKey) and re-solved it every time.
if (m_variableFont != nullptr && m_variableFontBase == baseFont &&
sameOptions())
{
return;
}
m_coords.clear();
for (TextStyleAxis* axis : m_variations)
{
m_coords.push_back({axis->tag(), axis->axisValue()});
}
m_features.clear();
for (TextStyleFeature* styleFeature : m_styleFeatures)
{
m_features.push_back({styleFeature->tag(), styleFeature->featureValue()});
}
m_variableFont = baseFont->withOptions(m_coords, m_features);
m_variableFontBase = baseFont;
}
else
{
m_variableFont = nullptr;
m_variableFontBase = nullptr;
}
}
// Whether the axes and features still hold the values m_variableFont was
// built with, in the same order.
bool TextStyle::sameOptions() const
{
if (m_coords.size() != m_variations.size() ||
m_features.size() != m_styleFeatures.size())
{
return false;
}
for (size_t i = 0; i < m_variations.size(); i++)
{
if (m_coords[i].axis != m_variations[i]->tag() ||
m_coords[i].value != m_variations[i]->axisValue())
{
return false;
}
}
for (size_t i = 0; i < m_styleFeatures.size(); i++)
{
if (m_features[i].tag != m_styleFeatures[i]->tag() ||
m_features[i].value != m_styleFeatures[i]->featureValue())
{
return false;
}
}
return true;
}
void TextStyle::buildDependencies()
{
if (m_variationHelper != nullptr)
{
m_variationHelper->buildDependencies();
}
parent()->addDependent(this);
Super::buildDependencies();
}
uint32_t TextStyle::assetId() { return this->fontAssetId(); }
void TextStyle::setAsset(rcp<FileAsset> asset)
{
if (asset != nullptr && asset->is<FontAsset>())
{
FileAssetReferencer::setAsset(asset);
// Changing the referenced font at runtime (e.g. via data binding) must
// reshape the text. Mirror FontAsset::font()'s dirtying so onDirty
// marks the text shape dirty and rebuilds any variable font. Guard for
// the clone/import case where the style isn't wired to its Text yet.
// A global style's paint has no Text; onDirty tells its owner instead.
if (m_text != nullptr || m_owner != nullptr)
{
addDirt(ComponentDirt::TextShape);
}
}
}
// A keyframe moved the style to another font. Data binding swaps the asset
// itself (see DataBindContextValueAssetFont); a key only sets the id.
void TextStyle::fontAssetIdChanged()
{
rcp<FileAsset> asset;
#ifdef WITH_RIVE_EDITOR
if (m_file != nullptr)
{
// Loaded from a .riv: the id is an index into the file's assets.
asset = m_file->asset(fontAssetId().object);
}
else if (auto* ab = artboard())
{
// Coop-delivered: the full editor Id of the FontAsset. Ask the resolver
// directly, Artboard::resolve would find a component at a small index.
if (auto* resolver = ab->editorResolver())
{
auto* core = resolver->resolve(fontAssetId());
if (core != nullptr && core->is<FileAsset>())
{
asset = ref_rcp(core->as<FileAsset>());
}
}
}
#else
if (m_file != nullptr)
{
asset = m_file->asset(fontAssetId());
}
#endif
// A font the export dropped (out of range) keeps the current one.
if (asset != nullptr && asset->is<FontAsset>())
{
setAsset(asset);
}
}
StatusCode TextStyle::import(ImportStack& importStack)
{
auto result = registerReferencer(importStack);
if (result != StatusCode::Ok)
{
return result;
}
auto backboardImporter = importStack.latest<BackboardImporter>(
Backboard::typeKey);
if (backboardImporter != nullptr)
{
m_file = backboardImporter->file();
}
return Super::import(importStack);
}
// A metric change re-shapes the Text the style belongs to. A global text
// style's paint has none (it's parented to its style Artboard): its owner
// re-shapes the Texts drawing it. Nothing else dirties the style artboard when
// a keyframe or bind sets one of these, so this is their only notice.
void TextStyle::metricsChanged()
{
if (m_text != nullptr)
{
m_text->markShapeDirty();
}
else if (m_owner != nullptr)
{
m_owner->styleShapeChanged();
}
}
void TextStyle::fontSizeChanged() { metricsChanged(); }
void TextStyle::lineHeightChanged() { metricsChanged(); }
void TextStyle::letterSpacingChanged() { metricsChanged(); }
Core* TextStyle::clone() const
{
TextStyle* twin = TextStyleBase::clone()->as<TextStyle>();
cloneFontInto(twin);
return twin;
}
void TextStyle::cloneFontInto(TextStyle* twin) const
{
twin->m_file = m_file;
if (m_fileAsset != nullptr)
{
twin->setAsset(m_fileAsset);
}
}
bool TextStyle::validate(CoreContext* context)
{
auto parent = context->resolve(parentId());
// A style normally lives under a Text (TextInterface). A global text
// style's TextStylePaint instead lives directly under the style Artboard,
// so accept an Artboard parent too.
return TextInterface::from(parent) != nullptr ||
(parent != nullptr && parent->is<Artboard>());
}