| #include "rive/data_bind/data_bind.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/layout_component.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_asset_font.hpp" | |
| #include "rive/data_bind/context/context_value_asset_blob.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 ViewModelInstanceAssetFontBase::typeKey: | |
| return DataType::assetFont; | |
| case ViewModelInstanceAssetBlobBase::typeKey: | |
| return DataType::assetBlob; | |
| 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::assetFont: | |
| m_ContextValue = new DataBindContextValueAssetFont(this); | |
| break; | |
| case DataType::assetBlob: | |
| m_ContextValue = new DataBindContextValueAssetBlob(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); | |
| } | |
| // (Re)bind is a reconcile, not a one-sided change: mark every direction the | |
| // bind supports so both sides sync in favor order (as before origin gating | |
| // existed). A one-sided change instead marks only its own direction bit. | |
| addDirt(reconcileDirt(), true); | |
| } | |
| #ifndef WITH_RIVE_EDITOR | |
| 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; | |
| // `clip` is bindable; a layout bound on clip needs its DrawableProxy up | |
| // front so it exists before the artboard's one-time proxy injection, even | |
| // while clip is currently false. Stamped here so it also covers instance | |
| // clones, whose targets are re-assigned through this setter. | |
| if (m_target != nullptr && | |
| propertyKey() == LayoutComponentBase::clipPropertyKey && | |
| m_target->is<LayoutComponent>()) | |
| { | |
| m_target->as<LayoutComponent>()->markClipMayBeDynamic(); | |
| } | |
| if (toSource() && m_target != nullptr && targetSupportsPush()) | |
| { | |
| m_target->addPropertyObserver(this); | |
| setFlag(Flag::Observing, true); | |
| } | |
| } | |
| #endif | |
| 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 || | |
| key == ScrollConstraintBase::computedContentWidthPropertyKey || | |
| key == ScrollConstraintBase::computedContentHeightPropertyKey || | |
| key == ColorChannelsBase::colorAlphaPropertyKey || | |
| key == ColorChannelsBase::colorRedPropertyKey || | |
| key == ColorChannelsBase::colorBluePropertyKey || | |
| key == ColorChannelsBase::colorGreenPropertyKey) | |
| { | |
| return false; | |
| } | |
| auto t = m_target->coreType(); | |
| if (t == BindablePropertyAssetBase::typeKey || | |
| t == BindablePropertyViewModelBase::typeKey || | |
| t == ViewModelInstanceViewModelBase::typeKey) | |
| { | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool DataBind::canSkip() | |
| { | |
| auto* t = target(); | |
| return t && t->is<Component>() && t->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(target(), | |
| propertyKey(), | |
| (flagsValue & DataBindFlags::Direction) == | |
| DataBindFlags::ToTarget, | |
| this); | |
| // We just wrote the target; sync the cached target value so it | |
| // reflects what we wrote instead of what was there before. | |
| m_ContextValue->refreshTargetValue(this); | |
| suppressDirt(false); | |
| } | |
| } | |
| } | |
| } | |
| void DataBind::updateDependents() | |
| { | |
| if (m_dataConverter) | |
| { | |
| m_dataConverter->update(); | |
| } | |
| } | |
| void DataBind::updateSourceBinding(bool invalidate) | |
| { | |
| if (toSource() && target() && m_ContextValue != nullptr) | |
| { | |
| if (invalidate) | |
| { | |
| m_ContextValue->invalidate(); | |
| } | |
| m_ContextValue->applyToSource(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; | |
| } | |
| ComponentDirt DataBind::reconcileDirt() | |
| { | |
| return (toTarget() ? ComponentDirt::Bindings : ComponentDirt::None) | | |
| (toSource() ? ComponentDirt::BindingsTarget : ComponentDirt::None); | |
| } | |
| void DataBind::addDirt(ComponentDirt value, bool recurse) | |
| { | |
| if (hasFlag(Flag::SuppressDirt) || (m_Dirt & value) == value) | |
| { | |
| // Already marked. | |
| return; | |
| } | |
| // Latch the direction this change came from so a converter that re-dirties | |
| // us over multiple frames (interpolators) re-asserts the same direction — | |
| // per-frame dirt gets cleared and can't carry the origin forward. Placed | |
| // after the guards above so a suppressed self-notify (from applying either | |
| // direction) can't flip the origin. Record a single origin, never the OR: | |
| // a reconcile marks both bits, in which case the favored direction wins. | |
| bool hasSource = enums::is_flag_set(value, ComponentDirt::Bindings); | |
| bool hasTarget = enums::is_flag_set(value, ComponentDirt::BindingsTarget); | |
| if (hasSource && hasTarget) | |
| { | |
| setFlag(Flag::TargetOrigin, !sourceToTargetRunsFirst()); | |
| } | |
| else if (hasTarget) | |
| { | |
| setFlag(Flag::TargetOrigin, true); | |
| } | |
| else if (hasSource) | |
| { | |
| setFlag(Flag::TargetOrigin, false); | |
| } | |
| 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); | |
| } | |
| } |