blob: 32a7fb4b57243f80b9ffa6cd1d8151fee9264012 [file]
#include "rive/component.hpp"
#include "rive/file.hpp"
#include <algorithm>
#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
// 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.
static bool hasDuplicateItems(
const std::vector<rcp<ViewModelInstanceListItem>>& items)
{
if (items.size() < 2)
{
return false;
}
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 std::adjacent_find(sorted.begin(), sorted.end()) != sorted.end();
}
ArtboardComponentList::ArtboardComponentList() {}
ArtboardComponentList::~ArtboardComponentList() { clear(); }
bool ArtboardComponentList::collapse(bool value)
{
if (!Super::collapse(value))
{
return false;
}
// Semantic-only collapse via the artboard boundary node. Only touches
// SemanticData nodes — non-semantic components stay untouched.
for (size_t i = 0; i < artboardCount(); i++)
{
auto* nestedArtboard = artboardInstance(static_cast<int>(i));
if (nestedArtboard != nullptr)
{
nestedArtboard->collapseSemanticBoundary(value);
}
}
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_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;
}
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;
}
}
}
#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;
}
#endif
void ArtboardComponentList::markLayoutNodeDirty(
bool shouldForceUpdateLayoutBounds)
{
bool parentIsRow = mainAxisIsRow();
bool parentIsStack = isStack();
for (int i = 0; i < artboardCount(); i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
artboard->parentIsRow(parentIsRow);
artboard->parentIsStack(parentIsStack);
}
}
}
bool ArtboardComponentList::isWithinVisibleWindow(int index) const
{
const int count = static_cast<int>(m_listItems.size());
if (count == 0 || m_visibleStartIndex < 0 || m_visibleEndIndex < 0)
{
return false;
}
const int start = m_visibleStartIndex % count;
const int end = m_visibleEndIndex % count;
return start <= end ? (index >= start && index <= end)
: (index >= start || index <= end);
}
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_visibleStartIndex >= 0 &&
m_visibleEndIndex >= 0;
for (int i = 0; i < artboardCount(); i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
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)
{
for (int i = 0; i < (int)artboardCount(); i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
artboard->cascadeLayoutStyle(inheritedInterpolation,
inheritedInterpolator,
inheritedInterpolationTime,
direction);
}
}
return false;
}
#endif
bool ArtboardComponentList::syncStyleChanges()
{
bool changed = false;
for (int i = 0; i < artboardCount(); i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
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);
}
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)
{
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 enclosing SemanticData.
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_oldItems.clear();
m_oldItems.assign(m_listItems.begin(), m_listItems.end());
m_listItems.clear();
m_listItems.assign(list->begin(), list->end());
m_listHasDuplicateItems = hasDuplicateItems(m_listItems);
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_stateMachinesByIndex.assign(m_listItems.size(), nullptr);
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.
for (auto item : m_oldItems)
{
auto it = std::find(m_listItems.begin(), m_listItems.end(), item);
if (it == m_listItems.end())
{
disposeListItem(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
m_artboardInstancesByIndex[index] = itr->second.get();
auto smItr = m_stateMachinesMap.find(item);
if (smItr != m_stateMachinesMap.end())
{
m_stateMachinesByIndex[index] = smItr->second.get();
}
}
else if (!virtualizationEnabled())
{
createArtboardAt(index, false);
}
index++;
}
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;
#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;
}
if (m_shouldResetInstances)
{
// reset() also advances a bound main instance that isn't the item's.
auto dataContext = artboard->dataContext();
if (dataContext != nullptr)
{
auto bound = dataContext->mainViewModelInstance();
if (bound != nullptr &&
bound.get() != m_listItems[row]->viewModelInstance().get())
{
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);
// A row sharing its item's instances with other rows can't be judged on
// its own.
if (m_listHasDuplicateItems || word >= m_quietRows.size() ||
(m_neverQuietRows[word] & bit) != 0)
{
return false;
}
#ifdef TESTING
if (!sm_quietRowsEnabled)
{
return false;
}
#endif
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;
}
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));
}
}
}
void ArtboardComponentList::shouldResetInstances(bool value)
{
if (value != m_shouldResetInstances)
{
// Rows were judged quiet with the other setting's reset work.
resetQuietRows(m_listItems.size());
}
m_shouldResetInstances = value;
}
#ifdef TESTING
void ArtboardComponentList::verifyQuietRows(RowPass pass,
float elapsedSeconds,
AdvanceFlags flags,
bool advanceNested)
{
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::reset:
artboard->reset();
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)
{
if (artboardCount() == 0 || isCollapsed())
{
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()
{
const size_t count = m_listItems.size();
// Detached item view models (see shouldResetInstances) are advanced on
// every row, quiet or not; otherwise only awake rows have work here.
for (size_t i = m_shouldResetInstances ? 0 : nextAwakeRow(0); i < count;
i = m_shouldResetInstances ? i + 1 : nextAwakeRow(i + 1))
{
auto artboard = m_artboardInstancesByIndex[i];
if (m_shouldResetInstances)
{
auto& viewModelInstance = m_listItems[i]->viewModelInstance();
if (viewModelInstance != nullptr)
{
viewModelInstance->advanced();
}
if (isRowQuiet(i))
{
continue;
}
if (artboard != nullptr)
{
auto dataContext = artboard->dataContext();
if (dataContext != nullptr)
{
auto boundInstance = dataContext->mainViewModelInstance();
if (boundInstance != nullptr &&
boundInstance != viewModelInstance)
{
boundInstance->advanced();
}
}
}
}
if (artboard != nullptr)
{
artboard->reset();
}
}
#ifdef TESTING
verifyQuietRows(RowPass::reset, 0.0f, AdvanceFlags::None, false);
#endif
}
AABB ArtboardComponentList::layoutBounds()
{
return AABB(0, 0, m_layoutSize.x, m_layoutSize.y);
}
AABB ArtboardComponentList::layoutBoundsForNode(int index)
{
if (virtualizationEnabled())
{
auto realIndex = std::fmod(index, m_listItems.size());
auto gap = this->gap();
float runningSize = 0;
bool isHorizontal = mainAxisIsRow();
for (int i = 0; i < realIndex; i++)
{
auto size = m_artboardSizes[i];
if (isHorizontal)
{
runningSize += size.x + gap;
}
else
{
runningSize += size.y + gap;
}
}
auto itemSize = m_artboardSizes[realIndex];
double left = isHorizontal ? runningSize : 0;
double top = isHorizontal ? 0 : runningSize;
return AABB(left, top, left + itemSize.x, top + 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::find(m_artboardInstancesByIndex.begin(),
m_artboardInstancesByIndex.end(),
artboardInstance) != m_artboardInstancesByIndex.end())
{
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_realizedStartIndex >= 0 &&
m_realizedEndIndex >= 0;
if (useVirtualWindow)
{
auto startIndex = m_realizedStartIndex % count;
auto endIndex = m_realizedEndIndex % count;
int i = startIndex;
while (true)
{
cache.push_back(i);
if (i == endIndex)
{
break;
}
i = (i + 1) % count;
}
}
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_realizedStartIndex != -1 && m_realizedEndIndex != -1)
{
// 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.
for (int i = 0; i < artboardCount(); i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
artboard->markSemanticBoundaryTransformDirty();
}
}
}
if (hasDirt(value, ComponentDirt::RenderOpacity))
{
for (int i = 0; i < artboardCount(); i++)
{
auto artboard = artboardInstance(i);
if (artboard != nullptr)
{
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
for (auto& artboard : m_artboardInstancesMap)
{
auto dataContext = artboard.second->dataContext();
if (dataContext != nullptr)
{
dataContext->parent(value);
artboard.second->internalDataContext(dataContext);
}
}
for (auto& sm : m_stateMachinesMap)
{
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());
}
if (forceLayoutSync)
{
syncLayoutChildren();
}
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();
}
}
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;
}
}
}
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)
{
auto& pool = m_resourcePool[artboard];
pool.push_back(std::move(artboardInstance));
}
auto smInstanceIterator = m_stateMachinesMap.find(listItem);
if (smInstanceIterator != m_stateMachinesMap.end())
{
auto& smPool = m_stateMachinesPool[artboard];
smPool.push_back(std::move(smInstanceIterator->second));
}
}
removeArtboardAt(index);
}
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();
auto runningWidth = 0.0f;
auto runningHeight = 0.0f;
bool isHorz = mainAxisIsRow();
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;
}
}
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();
}