blob: 7153d91076ecb8a66dd42dbac71548649144c9eb [file]
#include "rive/text/text_style_share_cache.hpp"
#include "rive/animation/any_state.hpp"
#include "rive/animation/entry_state.hpp"
#include "rive/animation/exit_state.hpp"
#include "rive/animation/state_machine.hpp"
#include "rive/animation/state_machine_layer.hpp"
#include "rive/animation/state_machine_listener.hpp"
#include "rive/artboard.hpp"
#include "rive/data_bind/converters/data_converter.hpp"
#include "rive/data_bind/converters/data_converter_group.hpp"
#include "rive/data_bind/converters/data_converter_group_item.hpp"
#include "rive/data_bind/data_bind.hpp"
#include "rive/data_bind/data_bind_context.hpp"
#include "rive/file.hpp"
#include "rive/generated/data_bind/converters/data_converter_formula_base.hpp"
#include "rive/generated/data_bind/converters/data_converter_operation_viewmodel_base.hpp"
#include "rive/generated/scripted/scripted_data_converter_base.hpp"
#include "rive/layout/layout_component_style.hpp"
#include "rive/shapes/paint/feather.hpp"
#include "rive/shapes/paint/fill.hpp"
#include "rive/shapes/paint/gradient_stop.hpp"
#include "rive/shapes/paint/linear_gradient.hpp"
#include "rive/shapes/paint/radial_gradient.hpp"
#include "rive/shapes/paint/solid_color.hpp"
#include "rive/shapes/paint/stroke.hpp"
#include "rive/text/text_interface.hpp"
#include "rive/text/text_style_axis.hpp"
#include "rive/text/text_style_feature.hpp"
#include "rive/text/text_style_global.hpp"
#include "rive/text/text_style_paint.hpp"
#include "rive/viewmodel/viewmodel.hpp"
#include "rive/viewmodel/viewmodel_instance.hpp"
#include <algorithm>
using namespace rive;
namespace
{
// Only plain paints can be drawn by several Texts: the glyph outlines each
// Text draws are its own (see TextStyleGeometry), but backgrounds keep their
// Text's glyph rects, path effects and inner feathers are built from the
// paint's own glyph path, and an image in a paint fits that Text's bounds.
// Anything else (scripted objects, nested artboards...) may animate. Unknown
// types are refused, so a new type can never silently break sharing.
bool isPlainStyleObject(const Core* object)
{
switch (object->coreType())
{
case ArtboardBase::typeKey:
case LayoutComponentStyleBase::typeKey:
case TextStylePaintBase::typeKey:
case TextStyleAxisBase::typeKey:
case TextStyleFeatureBase::typeKey:
case FillBase::typeKey:
case StrokeBase::typeKey:
case SolidColorBase::typeKey:
case LinearGradientBase::typeKey:
case RadialGradientBase::typeKey:
case GradientStopBase::typeKey:
// Binds are checked on their own (see analyze).
case DataBindBase::typeKey:
case DataBindContextBase::typeKey:
return true;
case FeatherBase::typeKey:
return !object->as<Feather>()->inner();
default:
return false;
}
}
bool isInertState(const LayerState* state)
{
return state->is<EntryState>() || state->is<AnyState>() ||
state->is<ExitState>();
}
// Only the default state machine ever runs on a style. It does nothing when
// it has no listeners, no binds, and no state beyond Entry/Any/Exit.
bool hasStateMachineLogic(const Artboard* source)
{
int index = source->defaultStateMachineIndex();
if (index < 0)
{
return false;
}
const StateMachine* machine = source->stateMachine((size_t)index);
if (machine == nullptr)
{
return false;
}
if (machine->listenerCount() != 0 || machine->dataBindCount() != 0)
{
return true;
}
for (size_t i = 0; i < machine->layerCount(); i++)
{
const StateMachineLayer* layer = machine->layer(i);
for (size_t j = 0; j < layer->stateCount(); j++)
{
if (!isInertState(layer->state(j)))
{
return true;
}
}
}
return false;
}
// Pointer listeners with no target listen to the run the style is drawn for.
bool listensToRuns(const Artboard* source)
{
int index = source->defaultStateMachineIndex();
if (index < 0)
{
return false;
}
const StateMachine* machine = source->stateMachine((size_t)index);
if (machine == nullptr)
{
return false;
}
for (size_t i = 0; i < machine->listenerCount(); i++)
{
const StateMachineListener* listener = machine->listener(i);
if (listener != nullptr && listener->hasPointerListeners() &&
source->resolve(listener->targetId()) == nullptr)
{
return true;
}
}
return false;
}
// A converter that never changes on its own and reads nothing but its input.
bool isStaticConverter(DataConverter* converter)
{
if (converter == nullptr)
{
return true;
}
switch (converter->coreType())
{
case DataConverterFormulaBase::typeKey:
case DataConverterOperationViewModelBase::typeKey:
case ScriptedDataConverterBase::typeKey:
return false;
default:
break;
}
if (converter->mayAdvance() || !converter->dataBinds().empty())
{
return false;
}
if (converter->is<DataConverterGroup>())
{
for (auto item : converter->as<DataConverterGroup>()->items())
{
if (!isStaticConverter(item->converter()))
{
return false;
}
}
}
return true;
}
} // namespace
TextStyleShareInfo TextStyleShareCache::analyze(
const Artboard* source,
const File* file)
{
TextStyleShareInfo info;
if (source == nullptr)
{
return info;
}
uint32_t ownViewModelId = source->viewModelId();
// Whether [dataBind] reads nothing but the style's own view model.
auto readsOwnViewModel = [ownViewModelId](const DataBind* dataBind) {
if (!dataBind->is<DataBindContext>() || dataBind->isNameBased())
{
return false;
}
auto& path = dataBind->as<DataBindContext>()->sourcePathIds();
return !path.empty() && path[0] == ownViewModelId;
};
bool onlyOwnViewModel = true;
bool staticBinds = true;
for (auto dataBind : source->dataBinds())
{
if (!readsOwnViewModel(dataBind))
{
onlyOwnViewModel = false;
}
if (dataBind->is<DataBindContext>() && !dataBind->isNameBased() &&
!isStaticConverter(dataBind->converter()))
{
staticBinds = false;
}
}
// The state machine's binds (its conditions, listener actions) read the
// same data context.
int machineIndex = source->defaultStateMachineIndex();
const StateMachine* machine = machineIndex >= 0
? source->stateMachine((size_t)machineIndex)
: nullptr;
if (machine != nullptr)
{
for (size_t i = 0; i < machine->dataBindCount(); i++)
{
auto dataBind = machine->dataBind(i);
if (dataBind != nullptr && !readsOwnViewModel(dataBind))
{
onlyOwnViewModel = false;
}
}
}
info.readsOutsideOwnViewModel = !onlyOwnViewModel;
info.listensToRuns = listensToRuns(source);
info.hasStateMachine = hasStateMachineLogic(source);
#ifdef WITH_RIVE_EDITOR
// The editor edits source artboards in place; never freeze one.
return info;
#else
if (!onlyOwnViewModel || !staticBinds || info.hasStateMachine)
{
return info;
}
for (auto object : source->objects())
{
if (object != nullptr && !isPlainStyleObject(object))
{
return info;
}
}
// Exposed properties take per-text values.
ViewModel* viewModel = file != nullptr ? file->viewModel(ownViewModelId)
: nullptr;
if (viewModel != nullptr)
{
for (auto property : viewModel->properties())
{
if (property->isInput() || property->isOutput())
{
return info;
}
}
}
info.shareable = true;
return info;
#endif
}
TextStyleShareInfo TextStyleShareCache::info(const Artboard* source)
{
std::lock_guard<std::mutex> lock(m_mutex);
auto itr = m_info.find(source);
if (itr != m_info.end())
{
return itr->second;
}
auto info = analyze(source, m_file);
m_info[source] = info;
return info;
}
bool TextStyleShareCache::anyListensToRuns()
{
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_anyListensToRuns != -1)
{
return m_anyListensToRuns == 1;
}
}
bool any = false;
if (m_file != nullptr)
{
for (auto artboard : m_file->artboards())
{
if (TextStyleGlobal::firstTextStylePaint(artboard) != nullptr &&
info(artboard).listensToRuns)
{
any = true;
break;
}
}
}
std::lock_guard<std::mutex> lock(m_mutex);
m_anyListensToRuns = any ? 1 : 0;
return any;
}
void TextStyleShareCache::forget(const Artboard* source)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_info.erase(source);
m_anyListensToRuns = -1;
}
SharedTextStyleLease TextStyleShareCache::acquire(
const Artboard* source,
Factory* factory,
TextInterface* consumer)
{
// The consumers are the lease count.
if (consumer == nullptr || !info(source).shareable)
{
return SharedTextStyleLease();
}
std::lock_guard<std::mutex> lock(m_mutex);
auto key = std::make_pair(source, factory);
auto itr = m_shared.find(key);
SharedTextStyle* style = nullptr;
if (itr != m_shared.end())
{
style = itr->second.get();
}
else
{
auto created = std::make_unique<SharedTextStyle>(source, m_file, factory);
if (created->paint() == nullptr)
{
return SharedTextStyleLease();
}
style = created.get();
m_shared[key] = std::move(created);
}
style->m_consumers.push_back(consumer);
return SharedTextStyleLease(m_link, style, consumer);
}
void TextStyleShareCache::release(
SharedTextStyle* style,
TextInterface* consumer)
{
std::unique_ptr<SharedTextStyle> dead;
{
std::lock_guard<std::mutex> lock(m_mutex);
auto& consumers = style->m_consumers;
auto found = std::find(consumers.begin(), consumers.end(), consumer);
if (found != consumers.end())
{
consumers.erase(found);
}
if (!consumers.empty())
{
return;
}
for (auto itr = m_shared.begin(); itr != m_shared.end(); ++itr)
{
if (itr->second.get() == style)
{
dead = std::move(itr->second);
m_shared.erase(itr);
break;
}
}
}
// Destroyed outside the lock.
}
TextStyleShareCache::~TextStyleShareCache()
{
// Leases can outlive the cache (see TextStyleShareLink): cut them loose,
// and free the shared instances now, while the File still has the style
// artboards they were made from.
m_link->cache = nullptr;
m_shared.clear();
}
SharedTextStyle::SharedTextStyle(
const Artboard* source,
File* file,
Factory* factory)
{
m_instance = source->instance<ArtboardInstance>(factory);
m_paint = TextStyleGlobal::firstTextStylePaint(m_instance.get());
ViewModel* viewModel = file != nullptr
? file->viewModel(source->viewModelId())
: nullptr;
if (viewModel != nullptr)
{
m_viewModelInstance = file->createDefaultViewModelInstance(viewModel);
if (m_viewModelInstance != nullptr)
{
file->completeViewModelProperties(m_viewModelInstance.get());
m_instance->bindViewModelInstance(m_viewModelInstance, nullptr);
}
}
// Its values never change after this.
m_instance->updateDataBinds();
m_instance->updatePass(false);
if (m_paint != nullptr)
{
m_paint->owner(this);
m_paint->isShared(true);
}
}
SharedTextStyle::~SharedTextStyle()
{
// The instance's data binds reference the view model instance.
m_instance = nullptr;
m_viewModelInstance = nullptr;
}
void SharedTextStyle::styleShapeChanged()
{
m_instance->updatePass(false);
for (auto consumer : m_consumers)
{
consumer->markShapeDirty();
}
}
SharedTextStyleLease::SharedTextStyleLease(
SharedTextStyleLease&& other) noexcept
: m_link(std::move(other.m_link))
, m_style(other.m_style)
, m_consumer(other.m_consumer)
{
other.m_style = nullptr;
other.m_consumer = nullptr;
}
SharedTextStyleLease& SharedTextStyleLease::operator=(
SharedTextStyleLease&& other) noexcept
{
if (this != &other)
{
release();
m_link = std::move(other.m_link);
m_style = other.m_style;
m_consumer = other.m_consumer;
other.m_style = nullptr;
other.m_consumer = nullptr;
}
return *this;
}
void SharedTextStyleLease::release()
{
if (get() != nullptr)
{
m_link->cache->release(m_style, m_consumer);
}
m_link = nullptr;
m_style = nullptr;
m_consumer = nullptr;
}