blob: 50e34bbde97be20397061052641930a1554b3320 [file]
#include "rive/component.hpp"
#include "rive/file.hpp"
#include <algorithm>
#include <atomic>
#include <cmath>
#include "rive/animation/keyframe_interpolator.hpp"
#include "rive/artboard_component_list.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/constraints/layout_constraint.hpp"
#include "rive/constraints/list_constraint.hpp"
#include "rive/constraints/scrolling/scroll_constraint.hpp"
#include "rive/data_bind/data_bind.hpp"
#include "rive/data_bind/data_context.hpp"
#include "rive/math/bitwise.hpp"
#include "rive/data_bind_flags.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_number_base.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_string_base.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_color_base.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_boolean_base.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_enum_base.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_list_base.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_trigger_base.hpp"
#include "rive/generated/viewmodel/viewmodel_instance_viewmodel_base.hpp"
#include "rive/focus_data.hpp"
#include "rive/input/focus_manager.hpp"
#include "rive/semantic/semantic_manager.hpp"
#include "rive/layout_component.hpp"
#include "rive/viewmodel/viewmodel.hpp"
#include "rive/viewmodel/viewmodel_instance.hpp"
#include "rive/viewmodel/viewmodel_instance_number.hpp"
#include "rive/viewmodel/viewmodel_instance_symbol_list_index.hpp"
#include "rive/viewmodel/viewmodel_property.hpp"
#include "rive/viewmodel/symbol_type.hpp"
#include "rive/viewmodel/viewmodel_value_dependent.hpp"
#include "rive/world_transform_component.hpp"
#include "rive/layout/layout_data.hpp"
#include "rive/artboard_list_map_rule.hpp"
#include "rive/semantic/semantic_data.hpp"
using namespace rive;
namespace rive
{
/// Marks the hosting list dirty when a list item's drawIndex VM value changes.
class ArtboardListDrawIndexDependent final : public ViewModelValueDependent
{
public:
ArtboardListDrawIndexDependent(
ArtboardComponentList* list,
ViewModelInstanceValue* value)
: m_list(list)
, m_value(ref_rcp(value))
{
value->addDependent(this);
}
~ArtboardListDrawIndexDependent() { clear(); }
void addDirt(ComponentDirt value, bool recurse) override
{
if (m_list != nullptr)
{
m_list->invalidateOrderedListIndicesCache();
m_list->addDirt(ComponentDirt::Components, false);
}
}
void relinkDataBind() override {}
void clear()
{
if (m_value != nullptr)
{
m_value->removeDependent(this);
m_value = nullptr;
}
}
private:
ArtboardComponentList* m_list;
rcp<ViewModelInstanceValue> m_value;
};
} // namespace rive
static std::vector<const ViewModelInstanceListItem*> sortedItems(
const std::vector<rcp<ViewModelInstanceListItem>>& items)
{
std::vector<const ViewModelInstanceListItem*> sorted;
sorted.reserve(items.size());
for (const auto& item : items)
{
sorted.push_back(item.get());
}
std::sort(sorted.begin(), sorted.end());
return sorted;
}
// Shared by every list, so a scroll can't mistake one list's version for
// another's.
static std::atomic<uint32_t> s_itemsVersion{0};
ArtboardComponentList::ArtboardComponentList() {}
ArtboardComponentList::~ArtboardComponentList() { clear(); }
bool ArtboardComponentList::collapse(bool value)
{
if (!Super::collapse(value))
{
return false;
}
// The hosted artboard's semantics are hidden with its host; its own
// components are not collapsed.
forEachRealized([&](int, ArtboardInstance* nestedArtboard) {
nestedArtboard->syncSemanticBoundary();
});
return true;
}
void ArtboardComponentList::clear()
{
// Rows being destroyed must not call back into quiet-row bookkeeping.
resetQuietRows(0);
// Clean up semantic trees before destroying artboards/state machines.
for (auto& artboard : m_artboardInstancesMap)
{
if (artboard.second != nullptr)
{
artboard.second->cleanupSemanticTree();
}
}
clearDrawIndexListeners();
invalidateOrderedListIndicesCache();
// Clean up focus trees FIRST to prevent use-after-free when the
// FocusManager still holds references to FocusNodes.
for (auto& artboard : m_artboardInstancesMap)
{
if (artboard.second != nullptr)
{
artboard.second->cleanupFocusTree();
}
}
removeListScopeFocusNode();
m_listRowFocusNodes.clear();
// Destroy state machines BEFORE artboards.
// StateMachineInstance owns FocusListenerGroup objects that hold raw
// pointers to FocusData (owned by artboards). Destroying artboards first
// would cause use-after-free when FocusListenerGroup destructor tries to
// unregister from the already-deleted FocusData.
for (auto& sm : m_stateMachinesMap)
{
sm.second.reset();
}
for (auto& artboard : m_artboardInstancesMap)
{
artboard.second.reset();
}
m_stateMachinesMap.clear();
m_artboardInstancesByIndex.clear();
m_realizedIndices.clear();
m_stateMachinesByIndex.clear();
m_artboardInstancesMap.clear();
m_listItems.clear();
m_listHasDuplicateItems = false;
m_artboardsMap.clear();
// Pooled state machines go before the pooled artboards too, for the same
// reason as above.
m_stateMachinesPool.clear();
m_resourcePool.clear();
m_artboardOverridesMap.clear();
}
rcp<ViewModelInstanceListItem> ArtboardComponentList::listItem(int index)
{
if (index < m_listItems.size())
{
return m_listItems[index];
}
return nullptr;
}
ArtboardInstance* ArtboardComponentList::artboardInstance(int index)
{
if (index >= 0 && index < m_artboardInstancesByIndex.size())
{
return m_artboardInstancesByIndex[index];
}
return nullptr;
}
int ArtboardComponentList::indexOfArtboardInstance(
ArtboardInstance* instance) const
{
if (instance == nullptr)
{
return -1;
}
for (size_t i = 0; i < m_listItems.size(); ++i)
{
auto itr = m_artboardInstancesMap.find(m_listItems[i]);
if (itr != m_artboardInstancesMap.end() && itr->second.get() == instance)
{
return static_cast<int>(i);
}
}
return -1;
}
StateMachineInstance* ArtboardComponentList::stateMachineInstance(int index)
{
if (index >= 0 && index < m_stateMachinesByIndex.size())
{
return m_stateMachinesByIndex[index];
}
return nullptr;
}
void ArtboardComponentList::setRowsForItem(
int index,
const rcp<ViewModelInstanceListItem>& item,
ArtboardInstance* artboard,
StateMachineInstance* stateMachine)
{
if (!m_listHasDuplicateItems)
{
if (index >= 0 && static_cast<size_t>(index) < m_listItems.size() &&
m_listItems[index] == item)
{
resetQuietRow(index);
m_artboardInstancesByIndex[index] = artboard;
m_stateMachinesByIndex[index] = stateMachine;
markRealized(index, artboard != nullptr);
}
return;
}
for (size_t i = 0; i < m_listItems.size(); i++)
{
if (m_listItems[i] == item)
{
resetQuietRow(i);
m_artboardInstancesByIndex[i] = artboard;
m_stateMachinesByIndex[i] = stateMachine;
markRealized(i, artboard != nullptr);
}
}
}
#ifdef WITH_RIVE_LAYOUT
void* ArtboardComponentList::layoutNode(int index)
{
auto artboard = artboardInstance(index);
if (artboard != nullptr)
{
return static_cast<void*>(&artboard->takeLayoutData()->node);
}
return nullptr;
}
void ArtboardComponentList::collectLayoutNodes(std::vector<void*>& out)
{
for (int index : m_realizedIndices)
{
if (auto node = layoutNode(index))
{
out.push_back(node);
}
}
}
#endif
void ArtboardComponentList::markLayoutNodeDirty(
bool shouldForceUpdateLayoutBounds)
{
bool parentIsRow = mainAxisIsRow();
bool parentIsStack = isStack();
forEachRealized([&](int, ArtboardInstance* artboard) {
artboard->parentIsRow(parentIsRow);
artboard->parentIsStack(parentIsStack);
});
}
enum class WindowState : uint8_t
{
none,
realized,
visible
};
void ArtboardComponentList::clearVirtualWindow()
{
for (int index : m_windowOrder)
{
if (index < (int)m_windowState.size())
{
m_windowState[index] = (uint8_t)WindowState::none;
}
}
m_windowOrder.clear();
m_windowVisibleCount = 0;
invalidateOrderedListIndicesCache();
}
void ArtboardComponentList::addToVirtualWindow(int index, bool visible)
{
if (index >= (int)m_windowState.size())
{
m_windowState.resize(
std::max((size_t)index + 1, m_listItems.size()),
(uint8_t)WindowState::none);
}
m_windowState[index] = (uint8_t)(visible ? WindowState::visible
: WindowState::realized);
m_windowOrder.push_back(index);
if (visible)
{
m_windowVisibleCount++;
}
invalidateOrderedListIndicesCache();
}
bool ArtboardComponentList::isWithinVisibleWindow(int index) const
{
return index >= 0 && index < (int)m_windowState.size() &&
m_windowState[index] == (uint8_t)WindowState::visible;
}
void ArtboardComponentList::updateLayoutBounds(bool animate)
{
#ifdef WITH_RIVE_LAYOUT
// Buffered items are realized but off screen. Writing their measured size
// into m_artboardSizes would feed back into the virtualizer, which sums
// that same table to pick the visible window - so only visible items
// report their size.
const bool hasVirtualWindow = virtualizationEnabled() &&
m_windowVisibleCount > 0;
forEachRealized([&](int i, ArtboardInstance* artboard) {
artboard->updateLayoutBounds(animate);
if (!hasVirtualWindow || isWithinVisibleWindow(i))
{
auto bounds = artboard->layoutBounds();
setItemSize(Vec2D(bounds.width(), bounds.height()), i);
}
});
#endif
computeLayoutBounds();
}
#ifdef WITH_RIVE_LAYOUT
bool ArtboardComponentList::cascadeLayoutStyle(
LayoutStyleInterpolation inheritedInterpolation,
KeyFrameInterpolator* inheritedInterpolator,
float inheritedInterpolationTime,
LayoutDirection direction)
{
forEachRealized([&](int, ArtboardInstance* artboard) {
artboard->cascadeLayoutStyle(
inheritedInterpolation,
inheritedInterpolator,
inheritedInterpolationTime,
direction);
});
return false;
}
#endif
bool ArtboardComponentList::syncStyleChanges()
{
bool changed = false;
forEachRealized([&](int, ArtboardInstance* artboard) {
bool artboardChanged = artboard->syncStyleChanges();
if (artboardChanged)
{
changed = true;
}
});
return changed;
}
Artboard* ArtboardComponentList::findArtboard(
const rcp<ViewModelInstanceListItem>& listItem) const
{
auto viewModelInstance = listItem->viewModelInstance();
if (viewModelInstance == nullptr)
{
return nullptr;
}
auto viewModelId = viewModelInstance->viewModelId();
// Find artboard in the cached mappings
auto artboard = m_artboardsMap.find(viewModelId);
if (artboard != m_artboardsMap.end())
{
return artboard->second;
}
auto artboards = m_file->artboards();
// Check if there is a special rule that maps the view model to a specific
// artboard
auto listRule = m_artboardMapRules.find(viewModelId);
if (listRule != m_artboardMapRules.end())
{
auto artboardIndex = listRule->second;
if (artboardIndex < artboards.size())
{
auto artboard = artboards[artboardIndex];
m_artboardsMap[viewModelId] = artboard;
return artboard;
}
}
// Search for the first artboard that is bound to this view model
for (auto& artboard : artboards)
{
if (artboard->viewModelId() == viewModelId)
{
m_artboardsMap[viewModelId] = artboard;
return artboard;
}
}
return nullptr;
}
void ArtboardComponentList::disposeListItem(
const rcp<ViewModelInstanceListItem>& listItem)
{
removeArtboard(listItem);
}
void ArtboardComponentList::poolListItem(
const rcp<ViewModelInstanceListItem>& listItem)
{
auto artboard = findArtboard(listItem);
auto itr = m_artboardInstancesMap.find(listItem);
// A row that hosts artboards (nested artboards, lists, artboard paints)
// carries their state machines' progress, and a row that runs scripts
// carries their state; reuse resets neither. Such a row is destroyed
// rather than handed to another item.
if (artboard == nullptr || artboard->hostsArtboards() ||
artboard->usesScripts() || itr == m_artboardInstancesMap.end() ||
itr->second == nullptr)
{
removeArtboard(listItem);
return;
}
// What removeArtboard does for a row it destroys, minus the destroy: the
// row leaves the semantic and focus trees and its override, then the
// instances move to the pools (state machine paired with its artboard,
// since reuse pops both from the back).
auto smItr = m_stateMachinesMap.find(listItem);
StateMachineInstance* stateMachine = smItr != m_stateMachinesMap.end()
? smItr->second.get()
: nullptr;
if (stateMachine != nullptr)
{
// Through the state machine, which also forgets the list's manager:
// reuse hands the same manager back, and an unchanged manager would not
// build the new item's tree.
stateMachine->setExternalSemanticManager(nullptr, nullptr);
}
itr->second->cleanupSemanticTree();
itr->second->cleanupFocusTree();
clearArtboardOverride(itr->second.get());
m_resourcePool[artboard].push_back(std::move(itr->second));
if (stateMachine != nullptr)
{
m_stateMachinesPool[artboard].push_back(std::move(smItr->second));
}
removeArtboard(listItem);
}
void ArtboardComponentList::createOrReuseArtboardAt(int index)
{
auto item = listItem(index);
auto artboard = item != nullptr ? findArtboard(item) : nullptr;
if (artboard == nullptr)
{
createArtboardAt(index, false);
return;
}
auto poolItr = m_resourcePool.find(artboard);
if (poolItr == m_resourcePool.end() || poolItr->second.empty())
{
createArtboardAt(index, false);
return;
}
createArtboardRecorders(artboard);
auto& pool = poolItr->second;
auto pooledArtboard = std::move(pool.back());
pool.pop_back();
applyRecorders(pooledArtboard.get(), artboard);
attachArtboardOverride(pooledArtboard.get(), item);
addArtboardAt(std::move(pooledArtboard), index, false);
}
void ArtboardComponentList::releasePools()
{
// State machines before the artboards they drive (see clear()).
m_stateMachinesPool.clear();
m_resourcePool.clear();
}
std::unique_ptr<ArtboardInstance> ArtboardComponentList::createArtboard(
Component* target,
rcp<ViewModelInstanceListItem> listItem) const
{
auto artboard = findArtboard(listItem);
if (artboard != nullptr)
{
auto inst = artboard->instance();
return inst;
}
return nullptr;
}
std::unique_ptr<StateMachineInstance> ArtboardComponentList::
createStateMachineInstance(Component* target, ArtboardInstance* artboard)
{
if (artboard != nullptr)
{
const int defaultIndex = artboard->defaultStateMachineIndex();
const size_t stateMachineIndex = defaultIndex >= 0
? static_cast<size_t>(defaultIndex)
: 0u;
auto stateMachineInstance = artboard->stateMachineAt(stateMachineIndex);
linkStateMachineToArtboard(stateMachineInstance.get(), artboard);
return stateMachineInstance;
}
return nullptr;
}
void ArtboardComponentList::ensureListScopeFocusNode(
FocusManager* focusManager,
rcp<FocusNode> hostParent)
{
if (focusManager == nullptr)
{
return;
}
if (m_listScopeFocusNode == nullptr)
{
m_listScopeFocusNode = FocusNode::makeStructuralScope();
m_listScopeFocusNode->name("ArtboardComponentListScope");
}
// Only called from the full build pass (buildFocusTreeVisit), which is
// the ordering authority: always re-append so the scope sits at the
// walk's current position
focusManager->addChild(std::move(hostParent), m_listScopeFocusNode);
syncListRowNodesWithList(focusManager);
}
void ArtboardComponentList::removeListScopeFocusNode()
{
for (size_t r = 0; r < m_listRowFocusNodes.size(); r++)
{
auto& row = m_listRowFocusNodes[r];
if (row == nullptr)
{
continue;
}
if (row->manager() != nullptr)
{
row->manager()->removeChild(row);
}
row.reset();
}
m_listRowFocusNodes.clear();
if (m_listScopeFocusNode == nullptr)
{
return;
}
if (m_listScopeFocusNode->manager() != nullptr)
{
m_listScopeFocusNode->manager()->removeChild(m_listScopeFocusNode);
}
m_listScopeFocusNode.reset();
}
rcp<FocusNode> ArtboardComponentList::makeListRowFocusNode() const
{
// Structural rows: rows with focusable content are descended through;
// rows for items with no focusables are skipped entirely instead of
// becoming invisible focus stops.
auto node = FocusNode::makeStructuralScope();
node->name("ArtboardComponentListRow");
return node;
}
void ArtboardComponentList::reparentListRowsInScope(FocusManager* fm)
{
if (fm == nullptr || m_listScopeFocusNode == nullptr)
{
return;
}
for (size_t i = 0; i < m_listRowFocusNodes.size(); i++)
{
rcp<FocusNode> row = m_listRowFocusNodes[i];
if (row == nullptr)
{
continue;
}
if (row->parent() != nullptr)
{
row->removeFromParent();
}
}
for (size_t i = 0; i < m_listRowFocusNodes.size(); i++)
{
rcp<FocusNode> row = m_listRowFocusNodes[i];
if (row == nullptr)
{
continue;
}
fm->addChild(m_listScopeFocusNode, row, i);
}
}
namespace
{
// True if the artboard contains any focus content at any depth: authored
// FocusData, or a data-bound nested artboard host (whose persistent scope
// counts as focus content even while its current artboard has no focusables —
// a later swap can materialize focus nodes under it). Recurses through both
// host kinds — single nested artboards and component lists
bool artboardHasFocusContent(Artboard* artboard)
{
if (artboard == nullptr)
{
return false;
}
if (artboard->rootFocusDataCount() > 0)
{
return true;
}
for (auto* host : artboard->nestedArtboards())
{
if (host == nullptr)
{
continue;
}
if (host->isArtboardDataBound())
{
return true;
}
if (artboardHasFocusContent(host->artboardInstance(0)))
{
return true;
}
}
for (auto* list : artboard->artboardComponentLists())
{
if (list == nullptr)
{
continue;
}
// A component list is bound to a VM list property and always owns a
// structural scope (ensureListScopeFocusNode); its items — and their
// focusables — can populate at runtime, exactly like a data-bound
// nested-artboard host swap. Count the list itself as latent focus
// content (symmetric with the isArtboardDataBound() host check above)
// so the enclosing item is rebuilt under its row while the inner list
// is still empty, keeping tab order correct once it fills.
return true;
}
return false;
}
} // namespace
bool ArtboardComponentList::listItemNeedsBuildUnderRow(
FocusManager* parentFM,
ArtboardInstance* inst,
rcp<FocusNode> row) const
{
if (parentFM == nullptr || inst == nullptr || row == nullptr)
{
return false;
}
if (inst->focusManager() != parentFM)
{
return true;
}
// If the artboard has focus content but the row is empty, that content is
// attached outside the row (setExternalFocusManager builds at the manager
// root) and must be rebuilt under the row. Focus content includes
// data-bound host scopes at any depth, not just authored FocusData, so a
// later swap materializes focus nodes at the row's position.
if (row->children().empty() && artboardHasFocusContent(inst))
{
return true;
}
return false;
}
void ArtboardComponentList::syncListRowNodesWithList(FocusManager* fm)
{
if (m_listItems.empty())
{
while (!m_listRowFocusNodes.empty())
{
auto row = m_listRowFocusNodes.back();
m_listRowFocusNodes.pop_back();
if (row != nullptr && row->manager() != nullptr)
{
row->manager()->removeChild(row);
}
}
return;
}
if (fm != nullptr && listRowNodesInPlace())
{
// The list hasn't changed and its rows already sit under the scope in
// order, so matching them to the list (O(n^2)) and re-parenting them
// would rebuild the same tree. This runs whenever a virtualized list
// realizes rows, so only the realized rows' focus trees get work.
buildListRowFocusTrees(fm);
return;
}
std::vector<rcp<ViewModelInstanceListItem>> listCopy = m_listItems;
std::vector<rcp<FocusNode>> rowCopy = m_listRowFocusNodes;
syncListRowNodesWithList(fm, listCopy, rowCopy);
}
void ArtboardComponentList::syncListRowNodesWithList(
FocusManager* fm,
const std::vector<rcp<ViewModelInstanceListItem>>& previousListItems,
const std::vector<rcp<FocusNode>>& previousRowNodes)
{
if (fm == nullptr || m_listScopeFocusNode == nullptr)
{
return;
}
const int n = static_cast<int>(m_listItems.size());
if (n == 0)
{
// The rows leave the scope with their items, as they do below for the
// items a non-empty list drops. Clearing only our references left them
// under the scope, so every list emptied and refilled grew it.
for (const auto& row : previousRowNodes)
{
if (row != nullptr && row->manager() != nullptr)
{
row->manager()->removeChild(row);
}
}
m_listRowFocusNodes.clear();
return;
}
std::vector<rcp<FocusNode>> newRows(static_cast<size_t>(n), nullptr);
std::vector<rcp<FocusNode>> pr = previousRowNodes;
for (int j = 0; j < n; j++)
{
for (int k = 0; k < static_cast<int>(previousListItems.size()) &&
k < static_cast<int>(pr.size());
k++)
{
if (m_listItems[static_cast<size_t>(j)] ==
previousListItems[static_cast<size_t>(k)])
{
newRows[static_cast<size_t>(j)] = pr[static_cast<size_t>(k)];
pr[static_cast<size_t>(k)] = nullptr;
break;
}
}
}
for (int k = 0; k < static_cast<int>(pr.size()) &&
k < static_cast<int>(previousListItems.size());
k++)
{
rcp<FocusNode> unmapped = pr[static_cast<size_t>(k)];
if (unmapped == nullptr)
{
continue;
}
bool inNew = false;
for (int j = 0; j < n; j++)
{
if (m_listItems[static_cast<size_t>(j)] ==
previousListItems[static_cast<size_t>(k)])
{
inNew = true;
break;
}
}
if (!inNew)
{
if (unmapped->manager() != nullptr)
{
unmapped->manager()->removeChild(unmapped);
}
}
}
m_listRowFocusNodes = std::move(newRows);
for (int i = 0; i < n; i++)
{
rcp<FocusNode>& row = m_listRowFocusNodes[static_cast<size_t>(i)];
if (row == nullptr)
{
row = makeListRowFocusNode();
}
}
reparentListRowsInScope(fm);
buildListRowFocusTrees(fm);
}
bool ArtboardComponentList::listRowNodesInPlace() const
{
if (m_listScopeFocusNode == nullptr ||
m_listRowFocusNodes.size() != m_listItems.size())
{
return false;
}
const auto& children = m_listScopeFocusNode->children();
if (children.size() != m_listRowFocusNodes.size())
{
return false;
}
for (size_t i = 0; i < children.size(); i++)
{
if (m_listRowFocusNodes[i] == nullptr ||
children[i] != m_listRowFocusNodes[i])
{
return false;
}
}
return true;
}
void ArtboardComponentList::buildListRowFocusTrees(FocusManager* fm)
{
const int n = static_cast<int>(m_listItems.size());
for (int i = 0; i < n; i++)
{
ArtboardInstance* inst = artboardInstance(i);
if (inst == nullptr)
{
continue;
}
rcp<FocusNode> row = m_listRowFocusNodes[static_cast<size_t>(i)];
if (row == nullptr)
{
continue;
}
// Wire the item's state machine to the shared manager first:
// setExternalFocusManager rebuilds the item's focus tree at the manager
// root, so it must run before building under the row. Same "wire first,
// place last" ordering as buildFocusTreeVisit and updateArtboard.
auto* smi = stateMachineInstance(i);
if (smi != nullptr && smi->focusManager() != fm)
{
smi->setExternalFocusManager(fm);
}
if (listItemNeedsBuildUnderRow(fm, inst, row))
{
if (inst->focusManager() != nullptr)
{
inst->cleanupFocusTree();
}
inst->buildFocusTree(fm, row);
}
}
}
void ArtboardComponentList::linkStateMachineToArtboard(
StateMachineInstance* stateMachineInstance,
ArtboardInstance* artboardInstance)
{
if (artboardInstance == nullptr || stateMachineInstance == nullptr)
{
return;
}
auto dataContext = artboardInstance->dataContext();
stateMachineInstance->dataContext(dataContext);
stateMachineInstance->updateDataBinds(false);
auto* parentArtboard = this->artboard();
if (parentArtboard != nullptr && parentArtboard->focusManager() != nullptr)
{
auto* parentFM = parentArtboard->focusManager();
stateMachineInstance->setExternalFocusManager(parentFM);
}
// Share parent artboard's semantic manager and build semantic tree
// reparented under the closest SemanticData in the parent artboard, or
// its boundary.
if (parentArtboard != nullptr && parentArtboard->semanticManager() != nullptr)
{
auto* parentSM = parentArtboard->semanticManager();
auto parentNode = SemanticData::findClosestSemanticNode(this);
stateMachineInstance->setExternalSemanticManager(parentSM, parentNode);
}
}
bool ArtboardComponentList::listsAreEqual(
std::vector<rcp<ViewModelInstanceListItem>>* list,
std::vector<rcp<ViewModelInstanceListItem>>* compared)
{
if (!list || !compared)
{
return false;
}
if (list->size() != compared->size())
{
return false;
}
size_t index = 0;
for (auto& item : *list)
{
if (item != (*compared)[index])
{
return false;
}
++index;
}
return true;
}
void ArtboardComponentList::updateList(
std::vector<rcp<ViewModelInstanceListItem>>* list)
{
if (listsAreEqual(&m_listItems, list))
{
return;
}
const std::vector<rcp<ViewModelInstanceListItem>> preListItems = m_listItems;
const std::vector<rcp<FocusNode>> preRowNodes = m_listRowFocusNodes;
// Laying out a virtualized list below realizes rows, which would sync the
// focus rows against the new list with rows still matching the old one;
// the sync at the end, against the rows the update started from, covers
// those rows too.
const bool wasUpdatingList = m_updatingList;
m_updatingList = true;
m_listItems.clear();
m_listItems.assign(list->begin(), list->end());
const auto sorted = sortedItems(m_listItems);
// Rows that show the same item share its instances, and keeping them in
// step takes a scan of every row. Most lists show each item once.
m_listHasDuplicateItems = std::adjacent_find(sorted.begin(), sorted.end()) !=
sorted.end();
invalidateOrderedListIndicesCache();
m_artboardSizes.clear();
// Clear the index vectors - they'll be rebuilt as artboards are created
resetQuietRows(m_listItems.size());
m_artboardInstancesByIndex.assign(m_listItems.size(), nullptr);
m_realizedIndices.clear();
m_stateMachinesByIndex.assign(m_listItems.size(), nullptr);
quietUnrealizedRows();
auto p = layoutParent();
if (p != nullptr)
{
#ifdef WITH_RIVE_LAYOUT
p->clearLayoutChildren();
#endif
}
// We need to dispose old items after the layout children of the parent have
// updated to ensure no bad YGNodes are being hosted from the old data
// during clearLayoutChildren.
const bool virtualized = virtualizationEnabled();
for (const auto& item : preListItems)
{
if (!std::binary_search(sorted.begin(), sorted.end(), item.get()))
{
if (virtualized)
{
disposeListItem(item);
}
else
{
poolListItem(item);
}
}
}
uint32_t index = 0;
for (auto& item : m_listItems)
{
item->assignListIndex(index);
auto artboard = findArtboard(item);
if (artboard != nullptr)
{
m_artboardSizes.push_back(Vec2D(artboard->width(), artboard->height()));
}
auto itr = m_artboardInstancesMap.find(item);
if (itr != m_artboardInstancesMap.end())
{
// Existing artboard - update index vectors
resetQuietRow(index);
m_artboardInstancesByIndex[index] = itr->second.get();
markRealized(index, itr->second != nullptr);
auto smItr = m_stateMachinesMap.find(item);
if (smItr != m_stateMachinesMap.end())
{
m_stateMachinesByIndex[index] = smItr->second.get();
}
}
else if (!virtualized)
{
createOrReuseArtboardAt(index);
// Enter the new row's initial state now, the way the settle pass would
// a step later, so the first layout solve and text shape already see
// what that state keys (font sizes, visibility). Otherwise the row is
// laid out and shaped twice in its first frame. Only when the host
// advances the rows' state machines at all: one that doesn't (an
// editor in design mode) shows rows as authored.
auto stateMachine = m_stateMachinesByIndex[index];
if (m_advancesNested && stateMachine != nullptr &&
stateMachine->tryChangeState())
{
stateMachine->advance(0.0f, false);
}
}
index++;
}
if (!virtualized)
{
// Rows nothing took are the ones this update removed for good.
releasePools();
}
m_itemsVersion = ++s_itemsVersion;
computeLayoutBounds();
syncLayoutChildren();
markLayoutNodeDirty();
markWorldTransformDirty();
addDirt(ComponentDirt::Components);
recomputeListUsesDrawIndexSort();
syncDrawIndexListeners();
m_updatingList = wasUpdatingList;
auto* parentAb = artboard();
if (parentAb != nullptr && parentAb->focusManager() != nullptr &&
m_listScopeFocusNode != nullptr)
{
syncListRowNodesWithList(
parentAb->focusManager(),
preListItems,
preRowNodes);
}
}
void ArtboardComponentList::syncLayoutChildren()
{
auto p = layoutParent();
if (p != nullptr)
{
#ifdef WITH_RIVE_LAYOUT
p->syncLayoutChildren();
#endif
}
}
#ifdef TESTING
uint64_t ArtboardComponentList::sm_quietRowSkips = 0;
bool ArtboardComponentList::sm_quietRowsEnabled = true;
bool ArtboardComponentList::sm_verifyQuietRows = true;
#endif
static size_t lowestSetBit(uint64_t bits)
{
return static_cast<size_t>(63 - math::clz64(bits & (~bits + 1)));
}
size_t ArtboardComponentList::nextAwakeRow(size_t row) const
{
const size_t count = m_listItems.size();
while (row < count)
{
const size_t word = row >> 6;
const uint64_t quiet = word < m_quietRows.size() ? m_quietRows[word] : 0;
const uint64_t awake = ~quiet & (~uint64_t(0) << (row & 63));
if (awake != 0)
{
return std::min(count, (word << 6) + lowestSetBit(awake));
}
row = (word + 1) << 6;
}
return count;
}
bool ArtboardComponentList::isRowQuiet(size_t row) const
{
const size_t word = row >> 6;
return word < m_quietRows.size() &&
((m_quietRows[word] >> (row & 63)) & 1) != 0;
}
AdvancingComponent::QuietState ArtboardComponentList::rowQuietState(size_t row)
{
using QuietState = AdvancingComponent::QuietState;
auto artboard = m_artboardInstancesByIndex[row];
auto stateMachine = m_stateMachinesByIndex[row];
if (artboard == nullptr)
{
// An unrealized row has no work at all until it is realized, which
// resets it (setRowsForItem).
return stateMachine == nullptr ? QuietState::quiet : QuietState::busy;
}
// Pooled machines pair up with pooled artboards by pool order only.
if (stateMachine != nullptr && stateMachine->artboard() != artboard)
{
return QuietState::busy;
}
auto state = artboard->rowQuietState();
if (state != QuietState::quiet || stateMachine == nullptr)
{
return state;
}
return stateMachine->rowQuietState();
}
bool ArtboardComponentList::tryQuietRow(size_t row)
{
const size_t word = row >> 6;
const uint64_t bit = uint64_t(1) << (row & 63);
if (!canQuietRows() || word >= m_quietRows.size() ||
(m_neverQuietRows[word] & bit) != 0)
{
return false;
}
switch (rowQuietState(row))
{
case AdvancingComponent::QuietState::never:
m_neverQuietRows[word] |= bit;
return false;
case AdvancingComponent::QuietState::busy:
return false;
case AdvancingComponent::QuietState::quiet:
break;
}
m_quietRows[word] |= bit;
if (auto artboard = m_artboardInstancesByIndex[row])
{
artboard->quietHostRow(static_cast<uint32_t>(row));
}
return true;
}
bool ArtboardComponentList::canQuietRows() const
{
#ifdef TESTING
if (!sm_quietRowsEnabled)
{
return false;
}
#endif
// A row sharing its item's instances with other rows can't be judged on
// its own.
return !m_listHasDuplicateItems;
}
void ArtboardComponentList::quietUnrealizedRows()
{
if (!canQuietRows() || m_quietRows.empty())
{
return;
}
std::fill(m_quietRows.begin(), m_quietRows.end(), ~uint64_t(0));
if (size_t tail = m_listItems.size() & 63)
{
m_quietRows.back() = (uint64_t(1) << tail) - 1;
}
}
void ArtboardComponentList::resetQuietRow(size_t row)
{
const size_t word = row >> 6;
if (word >= m_quietRows.size())
{
return;
}
const uint64_t bit = uint64_t(1) << (row & 63);
if ((m_quietRows[word] & bit) != 0 &&
row < m_artboardInstancesByIndex.size() &&
m_artboardInstancesByIndex[row] != nullptr)
{
m_artboardInstancesByIndex[row]->quietHostRow(Artboard::kNoQuietRow);
}
m_quietRows[word] &= ~bit;
m_neverQuietRows[word] &= ~bit;
}
void ArtboardComponentList::resetQuietRows(size_t rowCount)
{
for (size_t word = 0; word < m_quietRows.size(); word++)
{
for (uint64_t quiet = m_quietRows[word]; quiet != 0; quiet &= quiet - 1)
{
const size_t row = (word << 6) + lowestSetBit(quiet);
if (row < m_artboardInstancesByIndex.size() &&
m_artboardInstancesByIndex[row] != nullptr)
{
m_artboardInstancesByIndex[row]->quietHostRow(Artboard::kNoQuietRow);
}
}
}
const size_t words = (rowCount + 63) >> 6;
m_quietRows.assign(words, 0);
m_neverQuietRows.assign(words, 0);
}
void ArtboardComponentList::hostedRowWoke(Artboard* artboard, uint32_t row)
{
// Rows are reset before their instances change, so the artboard is still
// the row's; checked anyway, since waking a row by mistake only costs a
// frame of its work.
if (row < m_artboardInstancesByIndex.size() &&
m_artboardInstancesByIndex[row] == artboard)
{
const size_t word = row >> 6;
if (word < m_quietRows.size())
{
m_quietRows[word] &= ~(uint64_t(1) << (row & 63));
}
}
}
#ifdef TESTING
void ArtboardComponentList::verifyQuietRows(
RowPass pass,
float elapsedSeconds,
AdvanceFlags flags,
bool advanceNested)
{
if (!sm_verifyQuietRows)
{
return;
}
for (size_t word = 0; word < m_quietRows.size(); word++)
{
for (uint64_t quiet = m_quietRows[word]; quiet != 0; quiet &= quiet - 1)
{
const size_t row = (word << 6) + lowestSetBit(quiet);
sm_quietRowSkips++;
auto artboard = m_artboardInstancesByIndex[row];
auto stateMachine = m_stateMachinesByIndex[row];
const char* problem = nullptr;
if (rowQuietState(row) != AdvancingComponent::QuietState::quiet)
{
problem = "stopped being quiet but nothing woke it";
}
else if (artboard == nullptr)
{
// Unrealized, so there is no work to check.
}
else if (artboard->quietHostRow() != row)
{
problem = "lost track of its artboard";
}
else
{
auto dirt = Artboard::sm_dirtNotifications;
auto updates = DataBindContainer::sm_dataBindUpdates;
bool didWork = false;
switch (pass)
{
case RowPass::advance:
if (advanceNested && stateMachine != nullptr)
{
didWork = stateMachine->advance(elapsedSeconds, true);
}
didWork = artboard->advanceInternal(elapsedSeconds, flags) ||
didWork;
break;
case RowPass::settle:
if (advanceNested && stateMachine != nullptr)
{
didWork = stateMachine->tryChangeState();
}
didWork = artboard->advanceInternal(elapsedSeconds, flags) ||
didWork;
break;
case RowPass::updateDataBinds:
if (stateMachine != nullptr)
{
stateMachine->updateDataBinds(false);
}
artboard->updateDataBinds();
break;
case RowPass::update:
didWork = artboard->updatePass(false);
break;
}
if (didWork || dirt != Artboard::sm_dirtNotifications ||
updates != DataBindContainer::sm_dataBindUpdates ||
artboard->hasDirt(ComponentDirt::Components) ||
artboard->quietHostRow() != row ||
(stateMachine != nullptr &&
(stateMachine->needsAdvance() ||
stateMachine->stateChangedCount() != 0)))
{
problem = "did work while quiet";
}
}
if (problem != nullptr)
{
fprintf(
stderr,
"Quiet row %zu of list '%s' %s\n",
row,
name().c_str(),
problem);
abort();
}
}
}
}
#endif
bool ArtboardComponentList::advanceComponent(
float elapsedSeconds,
AdvanceFlags flags)
{
m_advancesNested = (flags & AdvanceFlags::AdvanceNested) ==
AdvanceFlags::AdvanceNested;
if (artboardCount() == 0)
{
return false;
}
if (isCollapsed())
{
m_inactiveChangeSequence = ViewModelInstanceValue::latestChangeSequence();
return false;
}
bool keepGoing = false;
bool advanceNested = (flags & AdvanceFlags::AdvanceNested) ==
AdvanceFlags::AdvanceNested;
bool newFrame = (flags & AdvanceFlags::NewFrame) == AdvanceFlags::NewFrame;
auto advancingFlags = flags & ~AdvanceFlags::IsRoot;
const size_t count = m_listItems.size();
for (size_t row = nextAwakeRow(0); row < count; row = nextAwakeRow(row + 1))
{
// A row whose work this frame would do nothing sleeps until something
// wakes it.
if (newFrame && tryQuietRow(row))
{
continue;
}
int i = static_cast<int>(row);
if (advanceNested)
{
auto stateMachine = stateMachineInstance(i);
if (stateMachine != nullptr)
{
// If it is not a new frame, we
// first validate whether their state has changed. Then and only
// then we advance the state machine. This avoids triggering
// dirt from advances that make intermediate value changes but
// finally settle in the same value
if (!newFrame)
{
if (stateMachine->tryChangeState())
{
if (stateMachine->advance(elapsedSeconds, newFrame))
{
keepGoing = true;
}
}
}
else
{
if (stateMachine->advance(elapsedSeconds, newFrame))
{
keepGoing = true;
}
}
}
}
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
if (artboard->advanceInternal(elapsedSeconds, advancingFlags))
{
keepGoing = true;
}
if (artboard->hasDirt(ComponentDirt::Components))
{
// The animation(s) caused the artboard to need an update.
addDirt(ComponentDirt::Components);
}
}
}
#ifdef TESTING
verifyQuietRows(
newFrame ? RowPass::advance : RowPass::settle,
elapsedSeconds,
advancingFlags,
advanceNested);
#endif
return keepGoing;
}
void ArtboardComponentList::reset() {}
AABB ArtboardComponentList::layoutBounds()
{
return AABB(0, 0, m_layoutSize.x, m_layoutSize.y);
}
AABB ArtboardComponentList::layoutBoundsForNode(int index)
{
if (virtualizationEnabled())
{
int count = (int)m_artboardSizes.size();
if (count == 0)
{
return AABB();
}
int realIndex = index % count;
// The scroll's layout places items in virtualized content space,
// after any scroll children ahead of this list.
auto position = scrollConstraint()->virtualItemPosition(this, realIndex);
auto itemSize = m_artboardSizes[realIndex];
return AABB(
position.x,
position.y,
position.x + itemSize.x,
position.y + itemSize.y);
}
else
{
// A row can have no artboard (e.g. its view model resolves to none).
auto artboard = artboardInstance(index);
if (artboard != nullptr)
{
return artboard->layoutBounds();
}
}
return AABB();
}
void ArtboardComponentList::markHostingLayoutDirty(
ArtboardInstance* artboardInstance)
{
// Only an artboard showing in a row joins the parent's layout. Artboards
// in the pools get here too, whenever reusing one resets its properties.
if (artboardInstance != nullptr &&
std::any_of(
m_realizedIndices.begin(),
m_realizedIndices.end(),
[&](int i) {
return m_artboardInstancesByIndex[i] == artboardInstance;
}))
{
this->artboard()->markLayoutDirty(artboardInstance);
}
markWorldTransformDirty();
}
bool ArtboardComponentList::willDraw()
{
return Super::willDraw() && m_listItems.size() > 0;
}
void ArtboardComponentList::invalidateOrderedListIndicesCache()
{
m_orderedListIndicesCacheValid = false;
}
void ArtboardComponentList::recomputeListUsesDrawIndexSort()
{
const bool prevUsesDrawIndexSort = m_listUsesDrawIndexSort;
m_listUsesDrawIndexSort = false;
for (const auto& item : m_listItems)
{
auto vmi = item->viewModelInstance().get();
if (vmi == nullptr)
{
continue;
}
auto* vm = vmi->viewModel();
if (vm != nullptr && vm->property(SymbolType::drawIndex) != nullptr)
{
m_listUsesDrawIndexSort = true;
if (prevUsesDrawIndexSort != m_listUsesDrawIndexSort)
{
invalidateOrderedListIndicesCache();
}
return;
}
}
if (prevUsesDrawIndexSort != m_listUsesDrawIndexSort)
{
invalidateOrderedListIndicesCache();
}
}
float ArtboardComponentList::listItemDrawIndex(int index) const
{
if (index < 0 || static_cast<size_t>(index) >= m_listItems.size())
{
return 0.0f;
}
auto*
vmi = m_listItems[static_cast<size_t>(index)]->viewModelInstance().get();
if (vmi == nullptr)
{
return 0.0f;
}
auto* vm = vmi->viewModel();
if (vm == nullptr || vm->property(SymbolType::drawIndex) == nullptr)
{
return 0.0f;
}
auto* prop = vmi->propertyValue(SymbolType::drawIndex);
if (prop != nullptr && prop->is<ViewModelInstanceNumber>())
{
float v = prop->as<ViewModelInstanceNumber>()->propertyValue();
if (!std::isfinite(v))
{
return 0.0f;
}
return v;
}
return 0.0f;
}
void ArtboardComponentList::clearDrawIndexListeners()
{
m_drawIndexDependents.clear();
}
void ArtboardComponentList::removeDrawIndexListenerForItem(
const rcp<ViewModelInstanceListItem>& listItem)
{
m_drawIndexDependents.erase(listItem);
}
void ArtboardComponentList::syncDrawIndexListeners()
{
clearDrawIndexListeners();
if (!m_listUsesDrawIndexSort)
{
return;
}
for (const auto& item : m_listItems)
{
auto vmi = item->viewModelInstance().get();
if (vmi == nullptr)
{
continue;
}
auto* prop = vmi->propertyValue(SymbolType::drawIndex);
if (prop == nullptr)
{
continue;
}
m_drawIndexDependents[item] = std::make_unique<
ArtboardListDrawIndexDependent>(this, prop);
}
}
void ArtboardComponentList::ensureOrderedListIndices()
{
const int count = static_cast<int>(m_listItems.size());
if (count == 0)
{
m_orderedListIndicesCacheValid = false;
m_cachedOrderedListIndices.clear();
return;
}
if (m_orderedListIndicesCacheValid)
{
return;
}
std::vector<int>& cache = m_cachedOrderedListIndices;
cache.clear();
const bool useVirtualWindow = virtualizationEnabled() &&
!m_windowOrder.empty();
if (useVirtualWindow)
{
for (int index : m_windowOrder)
{
if (index < count)
{
cache.push_back(index);
}
}
}
else
{
cache.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; i++)
{
cache.push_back(i);
}
}
if (m_listUsesDrawIndexSort)
{
std::sort(cache.begin(), cache.end(), [this](int a, int b) {
const float da = listItemDrawIndex(a);
const float db = listItemDrawIndex(b);
if (da != db)
{
return da < db;
}
return a < b;
});
}
m_orderedListIndicesCacheValid = true;
}
const std::vector<int>& ArtboardComponentList::orderedListIndices()
{
ensureOrderedListIndices();
return m_cachedOrderedListIndices;
}
void ArtboardComponentList::draw(Renderer* renderer)
{
if (needsSaveOperation())
{
renderer->save();
}
if (virtualizationEnabled())
{
renderer->transform(
parent()->as<WorldTransformComponent>()->worldTransform());
if (!m_windowOrder.empty())
{
// We need to render in the correct order so we get the correct
// z-index for items in cases where there is overlap
for (int i : orderedListIndices())
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
renderer->save();
auto transform = m_artboardTransforms[artboard];
renderer->transform(transform);
Component::artboard()->drawHosted(artboard, renderer);
renderer->restore();
}
}
}
}
else
{
renderer->transform(worldTransform());
for (int i : orderedListIndices())
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
renderer->save();
auto transform = m_artboardTransforms[artboard];
renderer->transform(transform);
Component::artboard()->drawHosted(artboard, renderer);
renderer->restore();
}
}
}
if (needsSaveOperation())
{
renderer->restore();
}
}
Core* ArtboardComponentList::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
bool ArtboardComponentList::hitTestHost(
const Vec2D& position,
bool skipOnUnclipped,
ArtboardInstance* artboard)
{
if (artboard == nullptr)
{
return false;
}
auto bounds = artboardPosition(artboard);
Vec2D offset(bounds.x + position.x, bounds.y + position.y);
auto transform = virtualizationEnabled()
? parent()->as<LayoutComponent>()->worldTransform()
: worldTransform();
return parent()->hitTestPoint(transform * offset, skipOnUnclipped, false);
}
Vec2D ArtboardComponentList::hostTransformPoint(
const Vec2D& vec,
ArtboardInstance* artboardInstance)
{
auto bounds = artboardPosition(artboardInstance);
Vec2D offset(bounds.x + vec.x, bounds.y + vec.y);
auto transform = virtualizationEnabled()
? parent()->as<LayoutComponent>()->worldTransform()
: worldTransform();
auto localVec = transform * offset;
auto ab = artboard();
return ab ? ab->rootTransform(localVec) : localVec;
}
Mat2D ArtboardComponentList::worldTransformForArtboard(
ArtboardInstance* artboardInstance)
{
auto offset = artboardPosition(artboardInstance);
// For scroll-into-view calculations, we need the position in content-local
// space (without scroll applied). Use the list's layout position combined
// with parent's world transform, rather than worldTransform() which may
// include scroll constraints.
auto* parentLayout = parent() != nullptr && parent()->is<LayoutComponent>()
? parent()->as<LayoutComponent>()
: nullptr;
if (parentLayout != nullptr)
{
AABB listBounds = layoutBounds();
Mat2D transform = parentLayout->worldTransform() *
Mat2D::fromTranslate(listBounds.minX, listBounds.minY);
return transform * Mat2D::fromTranslate(offset.x, offset.y);
}
auto transform = virtualizationEnabled()
? parent()->as<LayoutComponent>()->worldTransform()
: worldTransform();
return transform * Mat2D::fromTranslate(offset.x, offset.y);
}
void ArtboardComponentList::update(ComponentDirt value)
{
Super::update(value);
if (artboardCount() == 0)
{
return;
}
if (hasDirt(value, ComponentDirt::WorldTransform))
{
// Mark semantic bounds dirty for nodes inside each list item
// artboard. Their root-space bounds depend on the host's
// world transform.
forEachRealized([&](int, ArtboardInstance* artboard) {
artboard->markSemanticBoundaryTransformDirty();
});
}
if (hasDirt(value, ComponentDirt::RenderOpacity))
{
forEachRealized([&](int, ArtboardInstance* artboard) {
artboard->opacity(renderOpacity());
});
}
if (hasDirt(value, ComponentDirt::Components))
{
const size_t count = m_listItems.size();
for (size_t row = nextAwakeRow(0); row < count; row = nextAwakeRow(row + 1))
{
auto artboard = m_artboardInstancesByIndex[row];
if (artboard != nullptr)
{
artboard->updatePass(false);
}
}
#ifdef TESTING
verifyQuietRows(RowPass::update, 0.0f, AdvanceFlags::None, false);
#endif
}
}
void ArtboardComponentList::updateWorldTransform()
{
updateArtboardsWorldTransform();
Super::updateWorldTransform();
}
void ArtboardComponentList::updateArtboardsWorldTransform()
{
auto count = m_listItems.size();
if (count == 0)
{
return;
}
// We only update non layout transforms here
if (!virtualizationEnabled())
{
auto useLayout = layoutParent() != nullptr;
for (int i = 0; i < count; i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
auto bounds = useLayout ? artboard->layoutBounds()
: artboard->worldBounds();
auto origin = useLayout ? artboard->origin() : Vec2D();
m_artboardTransforms[artboard] = Mat2D::fromTranslate(
bounds.left() - origin.x,
bounds.top() - origin.y);
}
}
}
}
void ArtboardComponentList::updateConstraints()
{
if (layoutConstraints().size() > 0)
{
for (auto parentConstraint : layoutConstraints())
{
parentConstraint->constrainChild(this);
}
}
if (m_listConstraints.size() > 0 && !virtualizationEnabled())
{
for (auto listConstraint : m_listConstraints)
{
listConstraint->constrainList(this);
}
}
for (auto constraint : constraints())
{
auto listConstraint = ListConstraint::from(constraint);
if (listConstraint != nullptr)
{
continue;
}
constraint->constrain(this);
}
}
void ArtboardComponentList::internalDataContext(rcp<DataContext> value)
{
// Reconcile the existing data contexts with the new parent. A row can have
// no artboard (nothing bound to its view model) or no state machine (its
// artboard has none), and both maps keep those rows as null entries.
for (auto& artboard : m_artboardInstancesMap)
{
if (artboard.second == nullptr)
{
continue;
}
auto dataContext = artboard.second->dataContext();
if (dataContext != nullptr)
{
dataContext->parent(value);
artboard.second->internalDataContext(dataContext);
}
}
for (auto& sm : m_stateMachinesMap)
{
if (sm.second == nullptr)
{
continue;
}
auto dataContext = sm.second->dataContext();
if (dataContext != nullptr)
{
dataContext->parent(value);
sm.second->internalDataContext(dataContext);
}
}
}
void ArtboardComponentList::bindViewModelInstance(
rcp<ViewModelInstance> viewModelInstance,
rcp<DataContext> parent)
{
// At the time this is called, artboards will not yet have been instanced
// so its essentially a no-op and the call to bindViewModelInstance on
// each artboard is made at the time of artboard instancing
}
void ArtboardComponentList::clearDataContext() {}
void ArtboardComponentList::unbind() { clear(); }
void ArtboardComponentList::updateDataBinds()
{
// A collapsed list does no bind work at all: not the rows' own binds, not
// their state machines, and nothing mounted inside them. collapse() here
// is semantic-only, exactly as in NestedArtboard, so nothing in a row is
// ever flagged and DataBind::canSkip cannot see that the list is hidden --
// without this, a toSource bind in a row publishes an uncomputed value
// into the shared view model and every reader picks it up.
if (isCollapsed())
{
return;
}
const size_t count = m_listItems.size();
for (size_t row = nextAwakeRow(0); row < count; row = nextAwakeRow(row + 1))
{
auto stateMachine = m_stateMachinesByIndex[row];
if (stateMachine != nullptr)
{
stateMachine->updateDataBinds(false);
}
auto artboard = m_artboardInstancesByIndex[row];
if (artboard != nullptr)
{
artboard->updateDataBinds();
}
}
#ifdef TESTING
verifyQuietRows(RowPass::updateDataBinds, 0.0f, AdvanceFlags::None, false);
#endif
}
#ifdef WITH_RIVE_TOOLS
Vec2D ArtboardComponentList::itemPosition(int index)
{
auto artboard = artboardInstance(index);
return artboard == nullptr ? Vec2D() : artboardPosition(artboard);
}
#endif
Vec2D ArtboardComponentList::artboardPosition(ArtboardInstance* artboard)
{
auto mat = m_artboardTransforms[artboard];
return Vec2D(mat[4], mat[5]);
}
bool ArtboardComponentList::worldToLocal(Vec2D world, Vec2D* local, int index)
{
assert(local != nullptr);
auto artboard = artboardInstance(index);
if (artboard == nullptr)
{
return false;
}
Vec2D offset = artboardPosition(artboard);
auto transform = virtualizationEnabled()
? parent()->as<LayoutComponent>()->worldTransform()
: worldTransform();
auto artboardTransform = transform * Mat2D::fromTranslate(offset.x, offset.y);
Mat2D toMountedArtboard;
if (!artboardTransform.invert(&toMountedArtboard))
{
return false;
}
*local = toMountedArtboard * world;
return true;
}
void ArtboardComponentList::file(File* value) { m_file = value; }
File* ArtboardComponentList::file() const { return m_file; }
Core* ArtboardComponentList::clone() const
{
auto clone = static_cast<ArtboardComponentList*>(
ArtboardComponentListBase::clone());
clone->file(file());
return clone;
}
void ArtboardComponentList::createArtboardAt(int index, bool forceLayoutSync)
{
auto item = listItem(index);
if (item != nullptr)
{
auto artboardCopy = createArtboard(this, item);
if (artboardCopy != nullptr)
{
attachArtboardOverride(artboardCopy.get(), item);
addArtboardAt(std::move(artboardCopy), index, forceLayoutSync);
}
}
}
void ArtboardComponentList::addArtboardAt(
std::unique_ptr<ArtboardInstance> artboard,
int index,
bool forceLayoutSync)
{
auto item = listItem(index);
if (item != nullptr)
{
auto artboardInstance = artboard.get();
m_artboardInstancesMap[item] = std::move(artboard);
// In a virtualized list, the rows showing the item get each new
// instance right away: hosting it and syncing the layout below look it
// up by row. A list can show one item on several rows, and those rows
// share its instances.
const bool virtualized = virtualizationEnabled();
if (virtualized)
{
setRowsForItem(index, item, artboardInstance, nullptr);
}
bindArtboard(artboardInstance, item);
if (artboardInstance != nullptr)
{
artboardInstance->host(this);
artboardInstance->frameOrigin(false);
artboardInstance->parentIsRow(mainAxisIsRow());
artboardInstance->parentIsStack(isStack());
#ifdef WITH_RIVE_LAYOUT
// Cascade into the new row what its layout parent cascades into the
// rows it already has: its layout animation and direction. The parent
// only cascades when its own style changes, so a row added later would
// otherwise never inherit them. Doing it now, before the row's first
// layout solve, also means a row with a right-to-left layout doesn't
// flip its children after that solve and have the whole host solved
// again. Without a layout parent the row cascades its own style, as
// its update would a step later.
LayoutComponent* from = layoutParent();
if (from == nullptr)
{
from = artboardInstance;
}
artboardInstance->cascadeLayoutStyle(
from->interpolation(),
from->interpolator(),
from->interpolationTime(),
from->actualDirection());
#endif
}
StateMachineInstance* stateMachineInstance = nullptr;
auto artboard = findArtboard(item);
if (artboard != nullptr)
{
auto& smPool = m_stateMachinesPool[artboard];
if (!smPool.empty())
{
auto sm = smPool.back().get();
sm->resetState();
applyRecorders(sm, artboard);
m_stateMachinesMap[item] = std::move(smPool.back());
if (virtualized)
{
setRowsForItem(index, item, artboardInstance, sm);
}
linkStateMachineToArtboard(sm, artboardInstance);
stateMachineInstance = sm;
smPool.pop_back();
// Back in the tree without being rebuilt, so a root that
// cached its tree as capture-free has to look again.
if (sm->hasCaptureListeners())
{
StateMachineInstance::captureTopologyChanged();
}
}
}
if (stateMachineInstance == nullptr)
{
auto stateMachineCopy = createStateMachineInstance(
this,
artboardInstance);
stateMachineInstance = stateMachineCopy.get();
m_stateMachinesMap[item] = std::move(stateMachineCopy);
if (virtualized)
{
setRowsForItem(index, item, artboardInstance, stateMachineInstance);
}
}
if (!virtualized)
{
// updateList builds a non-virtualized list's rows in order and
// gives an item's later rows the instances made for its first.
resetQuietRow(index);
m_artboardInstancesByIndex[index] = artboardInstance;
m_stateMachinesByIndex[index] = stateMachineInstance;
markRealized(index, artboardInstance != nullptr);
}
// After the instance is findable by index, which the sync reads.
if (forceLayoutSync)
{
syncLayoutChildren();
}
}
}
void ArtboardComponentList::bindArtboard(
ArtboardInstance* artboardInstance,
rcp<ViewModelInstanceListItem> listItem)
{
if (artboardInstance != nullptr)
{
auto mainArtboard = this->artboard();
auto dataContext = mainArtboard->dataContext();
rcp<ViewModelInstance> viewModelInstance = listItem->viewModelInstance();
// TODO: @hernan added this to make sure data binds are procesed in the
// current frame instead of waiting for the next run. But might not be
// necessary. Needs more testing.
artboardInstance->bindViewModelInstance(viewModelInstance, dataContext);
artboardInstance->updateDataBinds();
invalidateOrderedListIndicesCache();
}
}
void ArtboardComponentList::removeArtboardAt(int index)
{
auto item = listItem(index);
// The rows let go of the item's instances before they are destroyed.
setRowsForItem(index, item, nullptr, nullptr);
removeArtboard(item);
}
void ArtboardComponentList::removeArtboard(rcp<ViewModelInstanceListItem> item)
{
invalidateOrderedListIndicesCache();
removeDrawIndexListenerForItem(item);
auto itr = m_artboardInstancesMap.find(item);
if (itr != m_artboardInstancesMap.end())
{
// Clean up semantic tree before destroying the artboard.
if (itr->second != nullptr)
{
itr->second->cleanupSemanticTree();
}
// Clean up focus tree before destroying the artboard to prevent
// use-after-free when the FocusManager still holds references
// to FocusNodes pointing to FocusData in this artboard.
if (itr->second != nullptr)
{
itr->second->cleanupFocusTree();
}
clearArtboardOverride(itr->second.get());
}
// Destroy state machines BEFORE artboards to ensure FocusListenerGroup
// can unregister from FocusData before the artboard (and its FocusData)
// is destroyed. Otherwise we get use-after-free in ~FocusListenerGroup.
m_stateMachinesMap.erase(item);
m_artboardInstancesMap.erase(item);
}
void ArtboardComponentList::createArtboardRecorders(const Artboard* artboard)
{
if (artboard == nullptr)
{
return;
}
auto recorderIt = m_propertyRecordersMap.find(artboard);
if (recorderIt == m_propertyRecordersMap.end())
{
auto propertyRecorder = std::make_unique<PropertyRecorder>();
propertyRecorder->recordArtboard(artboard);
m_propertyRecordersMap[artboard] = std::move(propertyRecorder);
for (auto& nestedArtboard : artboard->nestedArtboards())
{
if (nestedArtboard == nullptr)
{
continue;
}
auto sourceArtboard = nestedArtboard->sourceArtboard();
createArtboardRecorders(sourceArtboard);
}
}
}
void ArtboardComponentList::applyRecorders(
Artboard* artboard,
const Artboard* sourceArtboard)
{
if (artboard == nullptr)
{
return;
}
auto sourcedArtboardIt = m_propertyRecordersMap.find(sourceArtboard);
if (sourcedArtboardIt != m_propertyRecordersMap.end())
{
auto propertyRecorder = sourcedArtboardIt->second.get();
if (propertyRecorder != nullptr)
{
propertyRecorder->apply(artboard);
}
}
for (auto& nestedArtboard : artboard->nestedArtboards())
{
if (nestedArtboard == nullptr)
{
continue;
}
auto nestedInstance = nestedArtboard->sourceArtboard();
if (nestedInstance == nullptr)
{
continue;
}
applyRecorders(nestedInstance, nestedInstance->artboardSource());
}
}
void ArtboardComponentList::applyRecorders(
StateMachineInstance* stateMachineInstance,
const Artboard* sourceArtboard)
{
auto it = m_propertyRecordersMap.find(sourceArtboard);
if (it == m_propertyRecordersMap.end())
{
return;
}
auto propertyRecorder = it->second.get();
if (propertyRecorder != nullptr)
{
propertyRecorder->apply(stateMachineInstance);
}
}
void ArtboardComponentList::addVirtualizable(int index)
{
auto listItem = this->listItem(index);
if (listItem != nullptr)
{
auto artboard = findArtboard(listItem);
if (artboard != nullptr)
{
createArtboardRecorders(artboard);
auto& pool = m_resourcePool[artboard];
if (pool.empty())
{
createArtboardAt(index);
}
else
{
auto pooledArtboard = pool.back().get();
applyRecorders(pooledArtboard, artboard);
addArtboardAt(std::move(pool.back()), index);
pool.pop_back();
}
// Add components dirt so we process layout updates in the same
// frame that a mounted artboard is added
addDirt(ComponentDirt::Components);
auto p = layoutParent();
if (p != nullptr)
{
p->markLayoutStyleDirty();
}
}
}
}
void ArtboardComponentList::virtualizableChanged()
{
if (m_updatingList)
{
return;
}
auto* parentArtboard = this->artboard();
if (parentArtboard != nullptr && parentArtboard->focusManager() != nullptr)
{
auto* parentFM = parentArtboard->focusManager();
if (m_listScopeFocusNode != nullptr)
{
syncListRowNodesWithList(parentFM);
}
}
}
void ArtboardComponentList::removeVirtualizable(int index)
{
auto listItem = this->listItem(index);
if (listItem != nullptr)
{
// The rows let go of the item's instances before they move to the
// pools.
setRowsForItem(index, listItem, nullptr, nullptr);
auto artboard = findArtboard(listItem);
auto artboardInstance = std::move(m_artboardInstancesMap[listItem]);
if (artboard != nullptr && artboardInstance != nullptr)
{
// A pooled instance may be reused anywhere, so it keeps no cell.
artboardInstance->virtualGridCell(-1, -1);
auto& pool = m_resourcePool[artboard];
pool.push_back(std::move(artboardInstance));
}
auto smInstanceIterator = m_stateMachinesMap.find(listItem);
// A row without a state machine has nothing to pool, and a null in
// the pool would be reused as one.
if (smInstanceIterator != m_stateMachinesMap.end() &&
smInstanceIterator->second != nullptr)
{
auto& smPool = m_stateMachinesPool[artboard];
smPool.push_back(std::move(smInstanceIterator->second));
}
}
removeArtboardAt(index);
}
void ArtboardComponentList::realizedIndices(std::vector<int>& out)
{
out.insert(out.end(), m_realizedIndices.begin(), m_realizedIndices.end());
}
void ArtboardComponentList::markRealized(size_t index, bool realized)
{
auto it = std::lower_bound(
m_realizedIndices.begin(),
m_realizedIndices.end(),
(int)index);
bool present = it != m_realizedIndices.end() && *it == (int)index;
if (realized && !present)
{
m_realizedIndices.insert(it, (int)index);
}
else if (!realized && present)
{
m_realizedIndices.erase(it);
}
}
void ArtboardComponentList::setVirtualizableCell(int index, int column, int row)
{
auto artboard = artboardInstance(index);
if (artboard != nullptr)
{
artboard->virtualGridCell(column, row);
}
}
void ArtboardComponentList::setVirtualizablePosition(int index, Vec2D position)
{
auto artboard = this->artboardInstance(index);
if (artboard != nullptr)
{
auto useLayout = layoutParent() != nullptr;
auto origin = useLayout ? artboard->origin() : Vec2D();
m_artboardTransforms[artboard] = Mat2D::fromTranslate(
position.x - origin.x,
position.y - origin.y);
}
}
bool ArtboardComponentList::virtualizationEnabled()
{
auto virtualizer = scrollConstraint();
return virtualizer != nullptr && virtualizer->virtualize();
}
ScrollConstraint* ArtboardComponentList::scrollConstraint()
{
for (auto parentConstraint : layoutConstraints())
{
if (parentConstraint->constraint()->is<ScrollConstraint>())
{
return parentConstraint->constraint()->as<ScrollConstraint>();
}
}
return nullptr;
}
void ArtboardComponentList::computeLayoutBounds()
{
if (virtualizationEnabled())
{
auto gap = this->gap();
bool isHorz = mainAxisIsRow();
if (m_layoutSizeVersion != m_itemsVersion || m_layoutSizeGap != gap ||
m_layoutSizeIsHorz != isHorz)
{
m_layoutSizeVersion = m_itemsVersion;
m_layoutSizeGap = gap;
m_layoutSizeIsHorz = isHorz;
auto runningWidth = 0.0f;
auto runningHeight = 0.0f;
for (int i = 0; i < m_artboardSizes.size(); i++)
{
auto size = m_artboardSizes[i];
auto realGap = i == m_artboardSizes.size() - 1 ? 0 : gap;
if (isHorz)
{
runningWidth += size.x + realGap;
runningHeight = std::max(runningHeight, size.y);
}
else
{
runningWidth = std::max(runningWidth, size.x);
runningHeight += size.y + realGap;
}
}
m_layoutSize = Vec2D(runningWidth, runningHeight);
}
auto scroll = scrollConstraint();
if (scroll != nullptr)
{
scroll->constrainVirtualized(true);
}
}
}
Vec2D ArtboardComponentList::size() { return m_layoutSize; }
Vec2D ArtboardComponentList::itemSize(int index)
{
return index < m_artboardSizes.size() ? m_artboardSizes[index] : Vec2D();
}
void ArtboardComponentList::setItemSize(Vec2D size, int index)
{
if (index < m_artboardSizes.size() && m_artboardSizes[index] != size)
{
m_artboardSizes[index] = size;
m_itemsVersion = ++s_itemsVersion;
}
}
float ArtboardComponentList::gap()
{
auto p = layoutParent();
if (p != nullptr)
{
return mainAxisIsRow() ? p->gapHorizontal() : p->gapVertical();
}
return 0.0f;
}
void ArtboardComponentList::attachArtboardOverride(
ArtboardInstance* instance,
rcp<ViewModelInstanceListItem> listItem)
{
auto viewModelInstance = listItem->viewModelInstance();
if (viewModelInstance == nullptr)
{
return;
}
auto artboards = m_file->artboards();
int artboardIndex = -1;
for (auto& artboard : artboards)
{
artboardIndex++;
if (artboard->viewModelId() == viewModelInstance->viewModelId())
{
break;
}
}
if (artboardIndex < 0 && artboardIndex >= artboards.size())
{
return;
}
ArtboardComponentListOverride* artboardOverride = nullptr;
for (auto& child : children())
{
if (child->is<ArtboardComponentListOverride>())
{
if (child->as<ArtboardComponentListOverride>()->artboardId() == -1)
{
artboardOverride = child->as<ArtboardComponentListOverride>();
}
else if (
child->as<ArtboardComponentListOverride>()->artboardId() ==
artboardIndex)
{
artboardOverride = child->as<ArtboardComponentListOverride>();
break;
}
}
}
if (artboardOverride != nullptr)
{
artboardOverride->addArtboard(instance);
}
}
void ArtboardComponentList::clearArtboardOverride(
ArtboardInstance* artboardInstance)
{
for (auto& child : children())
{
if (child->is<ArtboardComponentListOverride>())
{
// Passing the artboard to all overrides in case in the future we
// support stacking overrides on top of each other.
// Currently all except one will be a no-op.
child->as<ArtboardComponentListOverride>()->removeArtboard(
artboardInstance);
}
}
}
bool ArtboardComponentList::mainAxisIsRow()
{
auto p = layoutParent();
return p != nullptr ? p->mainAxisIsRow() : true;
}
bool ArtboardComponentList::isStack()
{
auto p = layoutParent();
return p != nullptr && p->isStackContainer();
}
LayoutComponent* ArtboardComponentList::layoutParent()
{
auto* direct = parent();
if (direct == nullptr)
{
return nullptr;
}
if (direct->is<LayoutComponent>())
{
return direct->as<LayoutComponent>();
}
if (!joinsLayoutThroughContainer(this))
{
return nullptr;
}
for (auto* p = direct; p != nullptr; p = p->parent())
{
if (p->is<LayoutComponent>())
{
return p->as<LayoutComponent>();
}
if (!isTransparentLayoutContainer(p))
{
return nullptr;
}
}
return nullptr;
}
const Mat2D& ArtboardComponentList::listTransform() { return worldTransform(); }
void ArtboardComponentList::listItemTransforms(std::vector<Mat2D*>& transforms)
{
auto count = m_listItems.size();
for (int i = 0; i < count; i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
transforms.push_back(&m_artboardTransforms[artboard]);
}
}
}
void ArtboardComponentList::addMapRule(ArtboardListMapRule* rule)
{
m_artboardMapRules[rule->viewModelId()] = rule->artboardId();
}
#ifdef WITH_RIVE_EDITOR
void ArtboardComponentList::editorPrepareForDestruction()
{
clear();
Component::editorPrepareForDestruction();
}
#endif