blob: d8ffcf45c94f76f98acddf8873fbc71da8156703 [file] [edit]
#include "rive/scripted/scripted_transition.hpp"
#include "rive/nested_artboard.hpp"
#include "rive/artboard.hpp"
#include "rive/file.hpp"
#include "rive/component.hpp"
#include "rive/component_dirt.hpp"
#include "rive/drawable_flag.hpp"
#include "rive/enums.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/viewmodel/viewmodel_instance.hpp"
#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
#ifdef WITH_RIVE_LAYOUT
#include "rive/layout_component.hpp"
#endif
using namespace rive;
// v1: only NestedArtboard children participate in the design-time solo set.
static bool isTransitionChild(Component* child)
{
return child->is<NestedArtboard>();
}
// Toggle the loop-exclusion (Hidden drawable flag) on a design child. Hidden
// removes the child from the artboard draw walk (Drawable::isHidden()) without
// collapsing it, so NestedArtboard::advanceComponent keeps advancing its
// content while the container composites it in draw().
static void setChildHidden(Drawable* drawable, bool hidden)
{
uint32_t flags = drawable->drawableFlags();
if (hidden)
{
flags |= static_cast<uint32_t>(DrawableFlag::Hidden);
}
else
{
flags &= ~static_cast<uint32_t>(DrawableFlag::Hidden);
}
drawable->drawableFlags(flags);
}
ScriptedTransition::~ScriptedTransition() { clearListInstances(); }
// ---------------------------------------------------------------------------
// Design-time solo set
// ---------------------------------------------------------------------------
int ScriptedTransition::designChildCount()
{
int count = 0;
for (Component* child : children())
{
if (isTransitionChild(child))
{
count++;
}
}
return count;
}
NestedArtboard* ScriptedTransition::designChildAt(int index)
{
int i = 0;
for (Component* child : children())
{
if (!isTransitionChild(child))
{
continue;
}
if (i == index)
{
return child->as<NestedArtboard>();
}
i++;
}
return nullptr;
}
int ScriptedTransition::sourceIndex(const TransitionSource& source)
{
if (source.instance != nullptr)
{
for (size_t i = 0; i < m_listInstancesByIndex.size(); i++)
{
if (m_listInstancesByIndex[i] == source.instance)
{
return designChildCount() + (int)i;
}
}
return -1;
}
if (source.design != nullptr)
{
int i = 0;
for (Component* child : children())
{
if (!isTransitionChild(child))
{
continue;
}
if (child == source.design)
{
return i;
}
i++;
}
}
return -1;
}
Component* ScriptedTransition::activeComponent()
{
auto* ab = artboard();
Core* active = ab != nullptr ? ab->resolve(activeComponentId()) : nullptr;
for (Component* child : children())
{
if (child == active && isTransitionChild(child))
{
return child;
}
}
return nullptr;
}
bool ScriptedTransition::scriptReady() const
{
#ifdef WITH_RIVE_SCRIPTING
return m_vm != nullptr && m_self != 0;
#else
return false;
#endif
}
bool ScriptedTransition::scriptManagesTo() const
{
#ifdef WITH_RIVE_SCRIPTING
if (m_vm == nullptr || !m_vm->valid() || m_self == 0)
{
return true;
}
return m_vm->transitionManagesTo(m_self);
#else
return true;
#endif
}
void ScriptedTransition::propagateCollapse(bool collapse)
{
Core* active = collapse || artboard() == nullptr
? nullptr
: artboard()->resolve(activeComponentId());
for (Component* child : children())
{
if (!isTransitionChild(child))
{
child->collapse(collapse);
continue;
}
if (!collapse && m_transitioning &&
(child == m_from.design || child == m_to.design))
{
// Both transitioning design children stay live; drawn by draw().
child->collapse(false);
}
else
{
child->collapse(child != active);
}
}
}
bool ScriptedTransition::collapse(bool value)
{
if (!Component::collapse(value))
{
return false;
}
propagateCollapse(value);
return true;
}
// ---------------------------------------------------------------------------
// List instancing (lazy) — modeled on ArtboardComponentList
// ---------------------------------------------------------------------------
Artboard* ScriptedTransition::findListArtboard(
const rcp<ViewModelInstanceListItem>& item)
{
if (m_file == nullptr || item == nullptr)
{
return nullptr;
}
auto vmi = item->viewModelInstance();
if (vmi == nullptr)
{
return nullptr;
}
uint32_t viewModelId = vmi->viewModelId();
auto cached = m_artboardsMap.find(viewModelId);
if (cached != m_artboardsMap.end())
{
return cached->second;
}
for (auto ab : m_file->artboards())
{
if (ab->viewModelId() == viewModelId)
{
m_artboardsMap[viewModelId] = ab;
return ab;
}
}
return nullptr;
}
ArtboardInstance* ScriptedTransition::ensureListInstance(int index)
{
if (index < 0 || index >= (int)m_listItems.size())
{
return nullptr;
}
if (index < (int)m_listInstancesByIndex.size() &&
m_listInstancesByIndex[index] != nullptr)
{
return m_listInstancesByIndex[index];
}
auto& item = m_listItems[index];
auto existing = m_listInstances.find(item);
if (existing != m_listInstances.end())
{
return existing->second.get();
}
Artboard* source = findListArtboard(item);
if (source == nullptr)
{
return nullptr;
}
std::unique_ptr<ArtboardInstance> inst = source->instance();
ArtboardInstance* raw = inst.get();
if (raw == nullptr)
{
return nullptr;
}
if (artboard() != nullptr)
{
raw->bindViewModelInstance(item->viewModelInstance(),
artboard()->dataContext());
raw->updateDataBinds();
}
raw->frameOrigin(false);
// Default state machine so the instanced artboard animates.
int smIndex = raw->defaultStateMachineIndex();
std::unique_ptr<StateMachineInstance> sm =
raw->stateMachineAt(smIndex >= 0 ? (size_t)smIndex : 0u);
if (sm != nullptr)
{
sm->dataContext(raw->dataContext());
sm->updateDataBinds(false);
m_listStateMachines[raw] = std::move(sm);
}
m_listInstances[item] = std::move(inst);
if ((int)m_listInstancesByIndex.size() <= index)
{
m_listInstancesByIndex.resize(m_listItems.size(), nullptr);
}
m_listInstancesByIndex[index] = raw;
return raw;
}
void ScriptedTransition::forgetInstance(ArtboardInstance* instance)
{
if (instance == nullptr)
{
return;
}
if (m_transitioning &&
(m_from.instance == instance || m_to.instance == instance))
{
// The transition can't run against a source that's about to be freed:
// settle it now, which also un-hides any design child it had hidden.
completeTransition();
}
if (m_current.instance == instance)
{
// completeTransition() may have just seated it as current.
m_current = TransitionSource();
}
}
void ScriptedTransition::disposeListItem(
const rcp<ViewModelInstanceListItem>& item)
{
auto instIt = m_listInstances.find(item);
if (instIt != m_listInstances.end())
{
forgetInstance(instIt->second.get());
// State machine before artboard (avoids use-after-free in teardown).
m_listStateMachines.erase(instIt->second.get());
m_listInstances.erase(instIt);
}
}
void ScriptedTransition::clearListInstances()
{
if (m_transitioning &&
(m_from.instance != nullptr || m_to.instance != nullptr))
{
completeTransition();
}
if (m_current.instance != nullptr)
{
m_current = TransitionSource();
}
m_listStateMachines.clear(); // state machines before artboards
m_listInstances.clear();
m_listInstancesByIndex.clear();
m_listItems.clear();
m_artboardsMap.clear();
m_activeListIndex = -1;
}
static bool listsAreEqual(
const std::vector<rcp<ViewModelInstanceListItem>>& current,
std::vector<rcp<ViewModelInstanceListItem>>* incoming)
{
if (incoming == nullptr)
{
return current.empty();
}
if (current.size() != incoming->size())
{
return false;
}
for (size_t i = 0; i < current.size(); i++)
{
if (current[i] != (*incoming)[i])
{
return false;
}
}
return true;
}
void ScriptedTransition::updateList(
std::vector<rcp<ViewModelInstanceListItem>>* list)
{
if (listsAreEqual(m_listItems, list))
{
return;
}
std::vector<rcp<ViewModelInstanceListItem>> old = m_listItems;
if (list != nullptr)
{
m_listItems = *list;
}
else
{
m_listItems.clear();
}
// Dispose cached instances for items no longer present.
for (auto& item : old)
{
bool present = false;
for (auto& current : m_listItems)
{
if (current == item)
{
present = true;
break;
}
}
if (!present)
{
disposeListItem(item);
}
}
// Assign each item its `index` symbol (if any) + rebuild the lazy index
// cache, keeping instances that survived.
m_listInstancesByIndex.assign(m_listItems.size(), nullptr);
for (size_t i = 0; i < m_listItems.size(); i++)
{
m_listItems[i]->assignListIndex((uint32_t)i);
auto it = m_listInstances.find(m_listItems[i]);
if (it != m_listInstances.end())
{
m_listInstancesByIndex[i] = it->second.get();
}
}
addDirt(ComponentDirt::Components);
applySelection(); // clamps m_activeListIndex and (re)selects
wakeAdvance();
}
void ScriptedTransition::listSourceChanged()
{
// Unbound: clear the instanced children.
if (listSource() == (uint32_t)-1)
{
std::vector<rcp<ViewModelInstanceListItem>> empty;
updateList(&empty);
}
}
// ---------------------------------------------------------------------------
// Unified selection over the combined solo set
// ---------------------------------------------------------------------------
ScriptedTransition::TransitionSource ScriptedTransition::intendedSource()
{
TransitionSource source;
if (m_activeListIndex >= 0 && m_activeListIndex < (int)m_listItems.size())
{
source.instance = ensureListInstance(m_activeListIndex);
if (source.instance != nullptr)
{
return source;
}
}
Component* design = activeComponent();
if (design != nullptr && design->is<NestedArtboard>())
{
source.design = design->as<NestedArtboard>();
}
return source;
}
void ScriptedTransition::applySelection()
{
selectActiveSource(intendedSource());
}
void ScriptedTransition::selectActiveSource(const TransitionSource& next)
{
if (next == m_current)
{
return;
}
TransitionSource prev = m_current;
m_current = next;
if (scriptReady() && !prev.empty() && !next.empty())
{
startTransition(prev, next);
return;
}
// Instant: clear any in-flight transition and settle collapse.
if (m_transitioning)
{
if (m_from.design != nullptr)
{
setChildHidden(m_from.design, false);
}
if (m_to.design != nullptr)
{
setChildHidden(m_to.design, false);
}
m_transitioning = false;
m_from = TransitionSource();
m_to = TransitionSource();
m_manageTo = true;
}
propagateCollapse(isCollapsed());
addScriptedDirt(ComponentDirt::Paint);
}
void ScriptedTransition::startTransition(const TransitionSource& from,
const TransitionSource& to)
{
TransitionSource previousFrom =
m_transitioning ? m_from : TransitionSource();
m_from = from;
m_to = to;
m_transitioning = true;
// Interruption: retire a previously-outgoing design child not in the new
// pair (list instances need no un-hide — they are never in the draw loop).
if (!previousFrom.empty() && previousFrom != m_from &&
previousFrom != m_to && previousFrom.design != nullptr)
{
setChildHidden(previousFrom.design, false);
previousFrom.design->collapse(true);
}
// Keep design sources live; list instances are host-drawn regardless.
if (m_from.design != nullptr)
{
m_from.design->collapse(false);
}
if (m_to.design != nullptr)
{
m_to.design->collapse(false);
}
propagateCollapse(isCollapsed());
callChanged(m_from, m_to);
// A list `to` has no draw-loop entry to fall back to, so it must always be
// script-composited; a design `to` may opt out via `managesTo`.
m_manageTo = m_to.instance != nullptr ? true : scriptManagesTo();
if (m_from.design != nullptr)
{
setChildHidden(m_from.design, true);
}
if (m_to.design != nullptr)
{
setChildHidden(m_to.design, m_manageTo);
}
wakeAdvance();
addScriptedDirt(ComponentDirt::Components);
}
void ScriptedTransition::completeTransition()
{
if (!m_transitioning)
{
return;
}
TransitionSource from = m_from;
TransitionSource to = m_to;
m_transitioning = false;
m_from = TransitionSource();
m_to = TransitionSource();
m_manageTo = true;
if (from.design != nullptr)
{
setChildHidden(from.design, false);
}
if (to.design != nullptr)
{
setChildHidden(to.design, false);
}
m_current = to;
propagateCollapse(isCollapsed());
addScriptedDirt(ComponentDirt::Paint);
}
void ScriptedTransition::activeComponentIdChanged()
{
// A genuine design child was chosen -> leave list mode.
if (activeComponent() != nullptr)
{
m_activeListIndex = -1;
}
applySelection();
#ifdef WITH_RIVE_LAYOUT
recollectOwningLayout();
#endif
}
StatusCode ScriptedTransition::onAddedClean(CoreContext* context)
{
StatusCode code = Super::onAddedClean(context);
if (code != StatusCode::Ok)
{
return code;
}
m_current = intendedSource();
propagateCollapse(isCollapsed());
#ifdef WITH_RIVE_LAYOUT
recollectOwningLayout();
#endif
return StatusCode::Ok;
}
// ---------------------------------------------------------------------------
// Advance / draw / update
// ---------------------------------------------------------------------------
void ScriptedTransition::advanceLiveInstance(const TransitionSource& source,
float elapsedSeconds,
AdvanceFlags flags,
bool& keepGoing)
{
if (source.instance == nullptr)
{
return;
}
auto smIt = m_listStateMachines.find(source.instance);
StateMachineInstance* sm =
smIt != m_listStateMachines.end() ? smIt->second.get() : nullptr;
if (sm != nullptr && enums::is_flag_set(flags, AdvanceFlags::AdvanceNested))
{
if (sm->advanceAndApply(elapsedSeconds, false))
{
keepGoing = true;
}
}
else if (source.instance->advanceInternal(elapsedSeconds, flags))
{
keepGoing = true;
}
}
bool ScriptedTransition::advanceComponent(float elapsedSeconds,
AdvanceFlags flags)
{
bool keepGoing = false;
if (m_transitioning)
{
// In design mode (no nested/real time), settle immediately rather than
// freezing mid-blend.
if (!enums::is_flag_set(flags, AdvanceFlags::AdvanceNested))
{
completeTransition();
}
else if (elapsedSeconds == 0)
{
// Waiting for real elapsed time; keep requesting frames.
keepGoing = true;
}
else
{
bool scriptGoing = scriptAdvance(elapsedSeconds);
addScriptedDirt(ComponentDirt::Paint);
if (!scriptGoing)
{
completeTransition();
}
else
{
keepGoing = true;
}
}
}
// Advance the live list instance(s). Design sources advance via the
// artboard's own advancing-component loop, so only list instances here.
if (m_transitioning)
{
advanceLiveInstance(m_from, elapsedSeconds, flags, keepGoing);
advanceLiveInstance(m_to, elapsedSeconds, flags, keepGoing);
}
else
{
advanceLiveInstance(m_current, elapsedSeconds, flags, keepGoing);
}
return keepGoing;
}
void ScriptedTransition::draw(Renderer* renderer)
{
#ifdef WITH_RIVE_SCRIPTING
if (m_transitioning && m_vm != nullptr && m_vm->valid() && draws())
{
ScriptBackend::TransitionChildRef from = childRef(m_from);
ScriptBackend::TransitionChildRef to = childRef(m_to);
// When the script opts out of managing the to child (design only) it
// renders through the normal loop, so hand the script nil instead.
if (!m_manageTo)
{
to.artboard = nullptr;
}
renderer->save();
m_vm->callTransitionDraw(this, m_self, renderer, from, to);
renderer->restore();
return;
}
#endif
// Idle: an active LIST instance has no draw-loop entry, so draw it here
// (design children draw through the normal loop, gated by collapse).
if (m_current.instance != nullptr)
{
renderer->save();
renderer->transform(worldTransform());
artboard()->drawHosted(m_current.instance, renderer);
renderer->restore();
}
}
bool ScriptedTransition::willDraw()
{
return Super::willDraw() &&
(m_transitioning || m_current.instance != nullptr);
}
void ScriptedTransition::update(ComponentDirt value)
{
Super::update(value);
auto forward = [this, value](const TransitionSource& source) {
ArtboardInstance* inst = source.instance;
if (inst == nullptr)
{
return;
}
if ((value & ComponentDirt::RenderOpacity) ==
ComponentDirt::RenderOpacity)
{
inst->opacity(renderOpacity());
}
if ((value & ComponentDirt::Components) == ComponentDirt::Components)
{
inst->updatePass(false);
}
};
if (m_transitioning)
{
forward(m_from);
forward(m_to);
}
else
{
forward(m_current);
}
}
void ScriptedTransition::callChanged(const TransitionSource& from,
const TransitionSource& to)
{
#ifdef WITH_RIVE_SCRIPTING
if (m_vm == nullptr || !m_vm->valid())
{
return;
}
// 1 when the incoming child is at a higher combined index than the
// outgoing one, -1 when lower, 0 when unknown (e.g. first show).
int fromIndex = sourceIndex(from);
int toIndex = sourceIndex(to);
int direction =
(fromIndex >= 0 && toIndex >= 0)
? (toIndex > fromIndex ? 1 : (toIndex < fromIndex ? -1 : 0))
: 0;
m_vm->callTransitionChanged(this,
m_self,
childRef(from),
childRef(to),
direction);
#endif
}
#ifdef WITH_RIVE_SCRIPTING
ScriptBackend::TransitionChildRef ScriptedTransition::childRef(
const TransitionSource& source) const
{
ScriptBackend::TransitionChildRef ref;
if (source.design != nullptr)
{
ref.artboard = static_cast<Artboard*>(source.design->sourceArtboard());
ref.transform = source.design->worldTransform();
}
else
{
ref.artboard = static_cast<Artboard*>(source.instance);
ref.transform = worldTransform();
}
return ref;
}
#endif
Core* ScriptedTransition::clone() const
{
ScriptedTransition* twin =
ScriptedTransitionBase::clone()->as<ScriptedTransition>();
if (m_fileAsset != nullptr)
{
twin->setAsset(m_fileAsset);
}
twin->file(m_file);
return twin;
}
#ifdef WITH_RIVE_LAYOUT
void ScriptedTransition::recollectOwningLayout()
{
for (Component* p = parent(); p != nullptr; p = p->parent())
{
if (p->is<LayoutComponent>())
{
p->as<LayoutComponent>()->syncLayoutChildren();
return;
}
}
}
#endif
// ---------------------------------------------------------------------------
// Combined-index selectors (design children then list instances)
// ---------------------------------------------------------------------------
void ScriptedTransition::updateByIndex(size_t index)
{
int designCount = designChildCount();
// The data-binding path casts rounded floats to size_t without clamping,
// so a negative binding arrives as a huge index. Reject it before the
// narrowing below turns it back into a negative int, which would pass the
// design-child test and clear an active list selection (mirrors
// Solo::updateByIndex).
if (index >= (size_t)designCount + m_listItems.size())
{
return;
}
if ((int)index < designCount)
{
m_activeListIndex = -1;
NestedArtboard* child = designChildAt((int)index);
if (child != nullptr && artboard() != nullptr)
{
activeComponentId(artboard()->idOf(child));
}
applySelection();
return;
}
size_t listIndex = index - (size_t)designCount;
if (listIndex >= m_listItems.size())
{
return;
}
m_activeListIndex = (int)listIndex;
// Neutralize the design selection (0 == runtime "missing"); resolves to no
// child, so activeComponentIdChanged leaves m_activeListIndex intact.
if (activeComponentId() != 0)
{
activeComponentId(0);
}
applySelection();
}
void ScriptedTransition::updateByName(const std::string& name)
{
// v1: name selection covers design children only (list items have no
// stable names).
if (!artboard())
{
return;
}
for (auto& child : children())
{
if (!isTransitionChild(child))
{
continue;
}
if (child->name() == name)
{
m_activeListIndex = -1;
activeComponentId(artboard()->idOf(child));
break;
}
}
}
int ScriptedTransition::getActiveChildIndex()
{
if (m_activeListIndex >= 0)
{
return designChildCount() + m_activeListIndex;
}
if (!artboard())
{
return -1;
}
Core* active = artboard()->resolve(activeComponentId());
if (active)
{
int index = 0;
for (auto& child : children())
{
if (!isTransitionChild(child))
{
continue;
}
if (child == active)
{
return index;
}
index++;
}
}
return -1;
}
std::string ScriptedTransition::getActiveChildName()
{
if (!artboard())
{
return "";
}
Core* active = artboard()->resolve(activeComponentId());
if (active && active->is<Component>())
{
return active->as<Component>()->name();
}
return "";
}