blob: 9f8a32cbe44a91d86656f0952e6b2151bc5321c9 [file]
#include "rive/data_bind/data_bind.hpp"
#include "rive/artboard.hpp"
#include "rive/data_bind_flags.hpp"
#include "rive/generated/core_registry.hpp"
#include "rive/data_bind/bindable_property_artboard.hpp"
#include "rive/data_bind/bindable_property_asset.hpp"
#include "rive/data_bind/bindable_property_number.hpp"
#include "rive/data_bind/bindable_property_string.hpp"
#include "rive/data_bind/bindable_property_color.hpp"
#include "rive/data_bind/bindable_property_enum.hpp"
#include "rive/data_bind/bindable_property_list.hpp"
#include "rive/data_bind/bindable_property_boolean.hpp"
#include "rive/data_bind/bindable_property_trigger.hpp"
#include "rive/data_bind/bindable_property_integer.hpp"
#include "rive/data_bind/bindable_property_viewmodel.hpp"
#include "rive/data_bind/data_bind_container.hpp"
#include "rive/data_bind/context/context_value.hpp"
#include "rive/data_bind/context/context_value_any.hpp"
#include "rive/data_bind/context/context_value_asset_image.hpp"
#include "rive/data_bind/context/context_value_artboard.hpp"
#include "rive/data_bind/context/context_value_boolean.hpp"
#include "rive/data_bind/context/context_value_number.hpp"
#include "rive/data_bind/context/context_value_string.hpp"
#include "rive/data_bind/context/context_value_enum.hpp"
#include "rive/data_bind/context/context_value_list.hpp"
#include "rive/data_bind/context/context_value_color.hpp"
#include "rive/data_bind/context/context_value_trigger.hpp"
#include "rive/data_bind/context/context_value_symbol_list_index.hpp"
#include "rive/data_bind/context/context_value_viewmodel.hpp"
#include "rive/data_bind/data_values/data_type.hpp"
#include "rive/data_bind/converters/data_converter.hpp"
#include "rive/data_bind/converters/formula/formula_token.hpp"
#include "rive/constraints/scrolling/scroll_constraint.hpp"
#include "rive/animation/transition_viewmodel_condition.hpp"
#include "rive/animation/state_machine.hpp"
#include "rive/artboard_component_list.hpp"
#include "rive/generated/node_base.hpp"
#include "rive/importers/artboard_importer.hpp"
#include "rive/importers/state_machine_importer.hpp"
#include "rive/importers/backboard_importer.hpp"
#include "rive/component.hpp"
using namespace rive;
StatusCode DataBind::onAddedDirty(CoreContext* context)
{
StatusCode code = Super::onAddedDirty(context);
if (code != StatusCode::Ok)
{
return code;
}
return StatusCode::Ok;
}
StatusCode DataBind::import(ImportStack& importStack)
{
auto backboardImporter =
importStack.latest<BackboardImporter>(Backboard::typeKey);
if (backboardImporter == nullptr)
{
return StatusCode::MissingObject;
}
file(backboardImporter->file());
backboardImporter->addDataConverterReferencer(this);
if (target())
{
initialize();
auto input = ScriptInput::from(target());
if (input != nullptr)
{
bool ownsDataBind = true;
if (input->scriptedObject() != nullptr)
{
if (input->scriptedObject()->component() != nullptr)
{
auto importer = importStack.latest<ArtboardImporter>(
ArtboardBase::typeKey);
if (importer != nullptr)
{
ownsDataBind = false;
importer->addDataBind(this);
}
}
else if (input->scriptedObject()->addDataBindFromScriptedObject(
this))
{
ownsDataBind = false;
}
}
input->dataBind(this, ownsDataBind);
}
else if (target()->is<DataConverter>())
{
target()->as<DataConverter>()->addDataBind(this);
}
else if (target()->is<FormulaToken>())
{
target()->as<FormulaToken>()->addDataBind(this);
}
else
{
switch (target()->coreType())
{
case BindablePropertyNumberBase::typeKey:
case BindablePropertyStringBase::typeKey:
case BindablePropertyBooleanBase::typeKey:
case BindablePropertyEnumBase::typeKey:
case BindablePropertyArtboardBase::typeKey:
case BindablePropertyColorBase::typeKey:
case BindablePropertyTriggerBase::typeKey:
case BindablePropertyIntegerBase::typeKey:
case BindablePropertyAssetBase::typeKey:
case BindablePropertyViewModelBase::typeKey:
case BindablePropertyListBase::typeKey:
case TransitionPropertyViewModelComparatorBase::typeKey:
case StateTransitionBase::typeKey:
{
auto stateMachineImporter =
importStack.latest<StateMachineImporter>(
StateMachineBase::typeKey);
if (stateMachineImporter != nullptr)
{
stateMachineImporter->addDataBind(
std::unique_ptr<DataBind>(this));
return Super::import(importStack);
}
break;
}
default:
{
// Prefer the artboard that actually owns the target object.
// Relying on latest<ArtboardImporter> can attach binds to
// the wrong source artboard when multiple artboards are
// loaded.
if (target()->is<Component>())
{
auto comp = target()->as<Component>();
auto parentArtboard = comp->artboard();
if (parentArtboard != nullptr)
{
parentArtboard->addDataBind(this);
return Super::import(importStack);
}
}
auto artboardImporter =
importStack.latest<ArtboardImporter>(
ArtboardBase::typeKey);
if (artboardImporter != nullptr)
{
artboardImporter->addDataBind(this);
return Super::import(importStack);
}
break;
}
}
}
}
return Super::import(importStack);
}
DataType DataBind::outputType()
{
if (converter() && converter()->outputType() != DataType::input &&
converter() && converter()->outputType() != DataType::none)
{
return converter()->outputType();
}
return sourceOutputType();
}
DataType DataBind::sourceOutputType()
{
if (m_Source != nullptr)
{
switch (m_Source->coreType())
{
case ViewModelInstanceNumberBase::typeKey:
return DataType::number;
case ViewModelInstanceStringBase::typeKey:
return DataType::string;
case ViewModelInstanceEnumBase::typeKey:
return DataType::enumType;
case ViewModelInstanceColorBase::typeKey:
return DataType::color;
case ViewModelInstanceBooleanBase::typeKey:
return DataType::boolean;
case ViewModelInstanceListBase::typeKey:
return DataType::list;
case ViewModelInstanceTriggerBase::typeKey:
return DataType::trigger;
case ViewModelInstanceSymbolListIndexBase::typeKey:
return DataType::symbolListIndex;
case ViewModelInstanceAssetImageBase::typeKey:
return DataType::assetImage;
case ViewModelInstanceArtboardBase::typeKey:
return DataType::artboard;
case ViewModelInstanceViewModelBase::typeKey:
return DataType::viewModel;
}
}
return DataType::none;
}
void DataBind::source(rcp<ViewModelInstanceValue> value)
{
if (!bindsOnce())
{
value->addDependent(this);
}
m_Source = value;
// We treat this as a special case. If an ArtboardComponentList's list
// property is bound to a number instance, we know that the component
// will be provided with dettached view model instances that need to be
// advanced explicitly
if (m_Source && target() && target()->is<ArtboardComponentList>())
{
target()->as<ArtboardComponentList>()->shouldResetInstances(
m_Source->coreType() == ViewModelInstanceNumberBase::typeKey);
}
}
void DataBind::clearSource()
{
if (m_Source != nullptr)
{
if (!bindsOnce())
{
m_Source->removeDependent(this);
}
m_Source = nullptr;
}
}
DataBind::~DataBind()
{
unbind();
delete m_ContextValue;
m_ContextValue = nullptr;
delete m_dataConverter;
m_dataConverter = nullptr;
}
void DataBind::bind()
{
delete m_ContextValue;
m_ContextValue = nullptr;
switch (outputType())
{
case DataType::number:
m_ContextValue = new DataBindContextValueNumber(this);
break;
case DataType::string:
m_ContextValue = new DataBindContextValueString(this);
break;
case DataType::boolean:
m_ContextValue = new DataBindContextValueBoolean(this);
break;
case DataType::color:
m_ContextValue = new DataBindContextValueColor(this);
break;
case DataType::enumType:
m_ContextValue = new DataBindContextValueEnum(this);
break;
case DataType::list:
m_ContextValue = new DataBindContextValueList(this);
break;
case DataType::trigger:
m_ContextValue = new DataBindContextValueTrigger(this);
break;
case DataType::symbolListIndex:
m_ContextValue = new DataBindContextValueSymbolListIndex(this);
break;
case DataType::assetImage:
m_ContextValue = new DataBindContextValueAssetImage(this);
break;
case DataType::artboard:
m_ContextValue = new DataBindContextValueArtboard(this);
break;
case DataType::viewModel:
m_ContextValue = new DataBindContextValueViewModel(this);
break;
case DataType::any:
m_ContextValue = new DataBindContextValueAny(this);
break;
default:
break;
}
if (m_dataConverter)
{
m_dataConverter->reset();
}
// if this is a toSource bind on a push-capable target,
// subscribe to the target's notifyPropertyChanged stream instead of
// relying on per-frame polling. The container consults targetSupportsPush()
// to decide whether to also enroll the bind in m_persistingDataBinds.
//
// bind() can be called more than once on the same DataBind (e.g. when
// ArtboardComponentList rebinds list items as the viewmodel changes).
// Unsubscribe first if we previously subscribed — otherwise
// addPropertyObserver would prepend the same node twice and form a cycle
// that makes notifyPropertyChanged loop forever. The Observing flag
// gates this so we don't deref m_target when we never subscribed (which
// would be a UAF if the target was freed first).
if (hasFlag(Flag::Observing) && m_target != nullptr)
{
m_target->removePropertyObserver(this);
setFlag(Flag::Observing, false);
}
if (toSource() && m_target != nullptr && targetSupportsPush())
{
m_target->addPropertyObserver(this);
setFlag(Flag::Observing, true);
}
addDirt(ComponentDirt::Bindings, true);
}
void DataBind::target(Core* value)
{
if (m_target == value)
{
return;
}
// Drop the subscription only when we actually have one — otherwise
// dereferencing m_target is a UAF if the old target was freed before us
// (we wouldn't have been notified via onTargetDestroyed because we
// weren't in its observer list).
if (hasFlag(Flag::Observing) && m_target != nullptr)
{
m_target->removePropertyObserver(this);
setFlag(Flag::Observing, false);
}
m_target = value;
if (toSource() && m_target != nullptr && targetSupportsPush())
{
m_target->addPropertyObserver(this);
setFlag(Flag::Observing, true);
}
}
void DataBind::unbind()
{
clearSource();
if (hasFlag(Flag::Observing) && m_target != nullptr)
{
m_target->removePropertyObserver(this);
setFlag(Flag::Observing, false);
}
if (m_dataConverter != nullptr)
{
m_dataConverter->unbind();
}
if (m_ContextValue != nullptr)
{
delete m_ContextValue;
m_ContextValue = nullptr;
}
}
bool DataBind::targetSupportsPush() const
{
if (m_target == nullptr)
{
return false;
}
// Push works for any property whose value is exposed via a generated
// setter that calls notifyPropertyChanged (i.e. anything in *_base.hpp).
// The following targets are computed/derived and don't fit the push
// model — leave them on the polling path:
//
// * Solo's activeComponentId — derived from active child, no setter
// * BindablePropertyAsset's image — loaded asynchronously, no setter
// * BindablePropertyViewModel's value — set via dedicated method
// * ViewModelInstanceViewModel's reference — set via dedicated method
// * Node's computed* properties — written multiple times per frame by
// the transform/layout pipeline; pushing on every intermediate write
// triggers dependent binds mid-computation and shifts per-frame
// ordering. Polling at end of frame captures only the settled value.
auto key = propertyKey();
if (key == SoloBase::activeComponentIdPropertyKey ||
key == NodeBase::computedLocalXPropertyKey ||
key == NodeBase::computedLocalYPropertyKey ||
key == NodeBase::computedWorldXPropertyKey ||
key == NodeBase::computedWorldYPropertyKey ||
key == NodeBase::computedRootXPropertyKey ||
key == NodeBase::computedRootYPropertyKey ||
key == NodeBase::computedWidthPropertyKey ||
key == NodeBase::computedHeightPropertyKey ||
key == ShapeBase::lengthPropertyKey ||
key == ScrollConstraintBase::scrollIndexPropertyKey ||
key == ScrollConstraintBase::scrollPercentXPropertyKey ||
key == ScrollConstraintBase::scrollPercentYPropertyKey ||
key == ScrollConstraintBase::velocityXPropertyKey ||
key == ScrollConstraintBase::velocityYPropertyKey ||
key == ScrollConstraintBase::scrollActivePropertyKey)
{
return false;
}
auto t = m_target->coreType();
if (t == BindablePropertyAssetBase::typeKey ||
t == BindablePropertyViewModelBase::typeKey ||
t == ViewModelInstanceViewModelBase::typeKey)
{
return false;
}
return true;
}
bool DataBind::canSkip()
{
return m_target && m_target->is<Component>() &&
m_target->as<Component>()->isCollapsed() &&
propertyKey() != LayoutComponentStyleBase::displayValuePropertyKey;
}
void DataBind::update(ComponentDirt value)
{
if (m_Source != nullptr && m_ContextValue != nullptr)
{
if ((value & ComponentDirt::Bindings) == ComponentDirt::Bindings)
{
// TODO: @hernan review how dirt and mode work together. If dirt is
// not set for certain modes, we might be able to skip the mode
// validation.
auto flagsValue = static_cast<DataBindFlags>(flags());
if (toTarget())
{
// Alternative A: a TwoWay bind subscribes to its target via
// Core::notifyPropertyChanged. Without suppression, writing
// the target here would push-notify ourselves and add a
// spurious dirt entry, causing an extra updateSourceBinding
// pass in the same frame (silver tests collapse_data_binds-
// test_2 / virtualize_blendmode regress). Mirror the
// suppressDirt pattern used by the source-apply path.
suppressDirt(true);
m_ContextValue->apply(m_target,
propertyKey(),
(flagsValue & DataBindFlags::Direction) ==
DataBindFlags::ToTarget,
this);
suppressDirt(false);
}
}
}
}
void DataBind::updateDependents()
{
if (m_dataConverter)
{
m_dataConverter->update();
}
}
void DataBind::updateSourceBinding(bool invalidate)
{
if (toSource() && m_target && m_ContextValue != nullptr)
{
if (invalidate)
{
m_ContextValue->invalidate();
}
m_ContextValue->applyToSource(m_target,
propertyKey(),
isMainToSource(),
this);
}
}
bool DataBind::isMainToSource()
{
auto flagsValue = static_cast<DataBindFlags>(flags());
return (flagsValue & DataBindFlags::Direction) == DataBindFlags::ToSource;
}
bool DataBind::sourceToTargetRunsFirst()
{
auto flagsValue = static_cast<DataBindFlags>(flags());
return (flagsValue & DataBindFlags::SourceToTargetRunsFirst) ==
DataBindFlags::SourceToTargetRunsFirst;
}
void DataBind::addDirt(ComponentDirt value, bool recurse)
{
if (hasFlag(Flag::SuppressDirt) || (m_Dirt & value) == value)
{
// Already marked.
return;
}
m_Dirt |= value;
#ifdef WITH_RIVE_TOOLS
if (m_changedCallback != nullptr)
{
m_changedCallback();
}
#endif
if (enums::is_flag_set(m_Dirt, ComponentDirt::Dependents) &&
m_ContextValue != nullptr)
{
m_ContextValue->invalidate();
}
if (m_container && !hasFlag(Flag::Collapsed))
{
m_container->addDirtyDataBind(this);
}
}
void DataBind::container(DataBindContainer* container)
{
m_container = container;
}
void DataBind::relinkDataBind() { m_container->rebuildDataBind(this); }
bool DataBind::bindsOnce()
{
auto flagsValue = static_cast<DataBindFlags>(flags());
return enums::is_flag_set(flagsValue, DataBindFlags::Once);
}
bool DataBind::toSource()
{
auto flagsValue = static_cast<DataBindFlags>(flags());
return enums::any_flag_set(flagsValue,
DataBindFlags::TwoWay | DataBindFlags::ToSource);
}
bool DataBind::toTarget()
{
auto flagsValue = static_cast<DataBindFlags>(flags());
return enums::is_flag_set(flagsValue, DataBindFlags::TwoWay) ||
!enums::is_flag_set(flagsValue, DataBindFlags::ToSource);
}
bool DataBind::isNameBased()
{
auto flagsValue = static_cast<DataBindFlags>(flags());
return enums::is_flag_set(flagsValue, DataBindFlags::NameBased);
}
bool DataBind::advance(float elapsedTime)
{
if (converter() && m_Source && !hasFlag(Flag::Collapsed))
{
return converter()->advance(elapsedTime);
}
return false;
}
void DataBind::file(File* value) { m_file = value; };
File* DataBind::file() const { return m_file; };
void DataBind::collapse(bool isCollapsed)
{
if (hasFlag(Flag::Collapsed) == isCollapsed ||
propertyKey() == LayoutComponentStyleBase::displayValuePropertyKey ||
!targetSupportsPush())
{
return;
}
setFlag(Flag::Collapsed, isCollapsed);
if (!isCollapsed && m_Dirt != ComponentDirt::None && m_container)
{
m_container->addDirtyDataBind(this);
}
}
void DataBind::initialize()
{
if (target() && target()->is<Component>())
{
target()->as<Component>()->addCollapsable(this);
}
}