Nnnnn focus management fixes (#12303) b2438309fa * sort focus nodes hierarchically * chore(focus): correctly handle focus with lists * fix virtualizableChanged lifecycle Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head index f2ece2f..aeb8937 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -fcbebb25e41ff16477bb48325bd5cd66d7f28038 +b2438309fa61b3d52d4d1a0e6e97f3ef5ce71bd6
diff --git a/include/rive/artboard_component_list.hpp b/include/rive/artboard_component_list.hpp index e676e4e..98da956 100644 --- a/include/rive/artboard_component_list.hpp +++ b/include/rive/artboard_component_list.hpp
@@ -10,11 +10,13 @@ #include "rive/property_recorder.hpp" #include "rive/file.hpp" #include "rive/artboard_host.hpp" +#include "rive/input/focus_node.hpp" #include "rive/data_bind/data_bind_list_item_consumer.hpp" #include "rive/layout/layout_node_provider.hpp" #include "rive/viewmodel/viewmodel_instance_list_item.hpp" #include "rive/viewmodel/symbol_type.hpp" #include "rive/virtualizing_component.hpp" +#include "rive/refcnt.hpp" #include <memory> #include <stdio.h> #include <unordered_map> @@ -25,6 +27,7 @@ class ScrollConstraint; class ArtboardListMapRule; class ArtboardListDrawIndexDependent; +class FocusManager; class ArtboardComponentList : public ArtboardComponentListBase, public ArtboardHost, @@ -106,6 +109,7 @@ Artboard* findArtboard( const rcp<ViewModelInstanceListItem>& listItem) const; void addVirtualizable(int index) override; + void virtualizableChanged() override; void removeVirtualizable(int index) override; void setVisibleIndices(int start, int end) override { @@ -137,6 +141,13 @@ void addMapRule(ArtboardListMapRule*); int type() const override { return coreType(); } + /// Create/parent a synthetic list scope FocusNode (structural, no + /// Focusable) so list item focus trees group under it. Idempotent. + void ensureListScopeFocusNode(FocusManager* focusManager, + rcp<FocusNode> hostParent); + rcp<FocusNode> listScopeFocusNode() const { return m_listScopeFocusNode; } + void removeListScopeFocusNode(); + /// Rebuilds the ordered-list cache when invalid (list, visibility, or /// drawIndex sort inputs changed). void ensureOrderedListIndices(); @@ -196,6 +207,22 @@ m_artboardOverridesMap; std::unordered_map<int, int> m_artboardMapRules; + // Synthetic scope that parents all list item focus subtrees; no Focusable. + rcp<FocusNode> m_listScopeFocusNode = nullptr; + // One structural row per list item index, direct child of + // m_listScopeFocusNode. + std::vector<rcp<FocusNode>> m_listRowFocusNodes; + + void syncListRowNodesWithList(FocusManager* fm); + void syncListRowNodesWithList( + FocusManager* fm, + const std::vector<rcp<ViewModelInstanceListItem>>& previousListItems, + const std::vector<rcp<FocusNode>>& previousRowNodes); + rcp<FocusNode> makeListRowFocusNode() const; + void reparentListRowsInScope(FocusManager* fm); + bool listItemNeedsBuildUnderRow(FocusManager* parentFM, + ArtboardInstance* inst, + rcp<FocusNode> row) const; void attachArtboardOverride(ArtboardInstance*, rcp<ViewModelInstanceListItem>); void clearArtboardOverride(ArtboardInstance*);
diff --git a/include/rive/input/focus_manager.hpp b/include/rive/input/focus_manager.hpp index da57b2d..41f9962 100644 --- a/include/rive/input/focus_manager.hpp +++ b/include/rive/input/focus_manager.hpp
@@ -81,6 +81,9 @@ // Add child to parent (or to root nodes if parent is null) void addChild(rcp<FocusNode> parent, rcp<FocusNode> child); + // Insert as index-th child of parent (0 = first). Same re-parenting as + // addChild. + void addChild(rcp<FocusNode> parent, rcp<FocusNode> child, size_t index); // Remove child from its current parent (clears focus if needed) void removeChild(rcp<FocusNode> child);
diff --git a/include/rive/input/focus_node.hpp b/include/rive/input/focus_node.hpp index 51c8885..3b771c3 100644 --- a/include/rive/input/focus_node.hpp +++ b/include/rive/input/focus_node.hpp
@@ -105,6 +105,7 @@ bool isScope() const { return !m_children.empty(); } void addChild(rcp<FocusNode> child); + void insertChild(size_t index, rcp<FocusNode> child); void removeChild(rcp<FocusNode> child); // Remove this node from its current parent (used internally)
diff --git a/include/rive/virtualizing_component.hpp b/include/rive/virtualizing_component.hpp index 70b57cb..6a9c680 100644 --- a/include/rive/virtualizing_component.hpp +++ b/include/rive/virtualizing_component.hpp
@@ -28,6 +28,7 @@ virtual Vec2D itemSize(int index) = 0; virtual void setItemSize(Vec2D size, int index) = 0; virtual void addVirtualizable(int index) = 0; + virtual void virtualizableChanged() = 0; virtual void removeVirtualizable(int index) = 0; virtual void setVisibleIndices(int start, int end) = 0; virtual void setVirtualizablePosition(int index, Vec2D position) = 0;
diff --git a/src/artboard.cpp b/src/artboard.cpp index d92c7d4..5ed982f 100644 --- a/src/artboard.cpp +++ b/src/artboard.cpp
@@ -3,6 +3,8 @@ #include "rive/animation/keyframe_interpolator.hpp" #include "rive/artboard_component_list.hpp" #include "rive/backboard.hpp" +#include "rive/component.hpp" +#include "rive/container_component.hpp" #include "rive/focus_data.hpp" #include "rive/input/focus_manager.hpp" #include "rive/semantic/semantic_data.hpp" @@ -26,6 +28,7 @@ #include "rive/importers/import_stack.hpp" #include "rive/importers/backboard_importer.hpp" #include "rive/layout_component.hpp" +#include "rive/node.hpp" #include "rive/foreground_layout_drawable.hpp" #include "rive/nested_artboard.hpp" #include "rive/nested_artboard_leaf.hpp" @@ -1870,6 +1873,82 @@ return nullptr; } +namespace +{ +// Depth-first in scene (Container child) order so FocusManager child order +// (tab) matches the hierarchy, not the flat m_Objects / grouped passes. +// At most one FocusData is allowed as a direct child of a container; it is +// registered in a first pass, then child traversal skips that FocusData. +void buildFocusTreeVisit(FocusManager* focusManager, + Component* component, + rcp<FocusNode> focusNode) +{ + if (component == nullptr) + { + return; + } + if (component->is<NestedArtboard>()) + { + auto* nestedHost = component->as<NestedArtboard>(); + auto* nestedArtboard = nestedHost->artboardInstance(0); + if (nestedArtboard != nullptr && + nestedArtboard->focusManager() != focusManager) + { + nestedArtboard->cleanupFocusTree(); + nestedArtboard->buildFocusTree(focusManager, focusNode); + } + for (auto* animation : nestedHost->nestedAnimations()) + { + if (animation->is<NestedStateMachine>()) + { + auto* nsm = animation->as<NestedStateMachine>(); + auto* smi = nsm->stateMachineInstance(); + if (smi != nullptr && smi->focusManager() != focusManager) + { + smi->setExternalFocusManager(focusManager); + } + } + } + } + else if (component->is<ArtboardComponentList>()) + { + auto* list = component->as<ArtboardComponentList>(); + list->ensureListScopeFocusNode(focusManager, focusNode); + } + if (component->is<ContainerComponent>()) + { + auto* cc = component->as<ContainerComponent>(); + FocusData* directFd = nullptr; + for (Component* ch : cc->children()) + { + if (ch != nullptr && ch->is<FocusData>()) + { + directFd = ch->as<FocusData>(); + break; + } + } + rcp<FocusNode> recurseWith; + if (directFd != nullptr) + { + focusManager->addChild(focusNode, directFd->focusNode()); + recurseWith = directFd->focusNode(); + } + else + { + recurseWith = focusNode; + } + for (Component* ch : cc->children()) + { + if (ch == nullptr || ch->is<FocusData>()) + { + continue; + } + buildFocusTreeVisit(focusManager, ch, recurseWith); + } + } +} +} // namespace + void Artboard::buildFocusTree(FocusManager* focusManager, rcp<FocusNode> parentFocusNode) { @@ -1895,101 +1974,9 @@ rcp<FocusNode> effectiveParent = parentFocusNode; #endif - // Register all FocusData in this artboard - for (auto* obj : m_Objects) + if (as<ContainerComponent>() != nullptr) { - if (obj != nullptr && obj->is<FocusData>()) - { - auto* fd = obj->as<FocusData>(); - auto* localParent = fd->findParentFocusData(); - - rcp<FocusNode> parentNode = localParent != nullptr - ? localParent->focusNode() - : effectiveParent; - - focusManager->addChild(parentNode, fd->focusNode()); - } - } - // Propagate focus registration to nested artboards that might have been - // created before this artboard's focusManager was available. This handles - // ArtboardComponentList and NestedArtboard items that were initialized - // before the parent StateMachineInstance was created. - // - // We check if the nested artboard's focusManager is DIFFERENT from ours. - // If it is, that means it created its own internal focusManager when it - // should be sharing the parent's. We rebuild its focus tree with the - // correct shared focusManager. - - // Handle NestedArtboard instances - for (auto* nestedArtboardHost : m_NestedArtboards) - { - // Find closest focus node (handles artboard boundaries) - auto hostParentNode = - FocusData::findClosestFocusNode(nestedArtboardHost); - if (hostParentNode == nullptr) - { - hostParentNode = effectiveParent; - } - - auto* nestedArtboard = nestedArtboardHost->artboardInstance(0); - if (nestedArtboard != nullptr && - nestedArtboard->focusManager() != focusManager) - { - // Clean up old focus tree if it exists (with wrong focusManager) - nestedArtboard->cleanupFocusTree(); - nestedArtboard->buildFocusTree(focusManager, hostParentNode); - } - - // Also update the external focus manager on any nested state machines. - // This handles the case where initializeAnimation was called before - // the parent artboard had a focus manager. - for (auto* animation : nestedArtboardHost->nestedAnimations()) - { - if (animation->is<NestedStateMachine>()) - { - auto* nsm = animation->as<NestedStateMachine>(); - auto* smi = nsm->stateMachineInstance(); - if (smi != nullptr && smi->focusManager() != focusManager) - { - smi->setExternalFocusManager(focusManager); - } - } - } - } - - // Handle ArtboardComponentList instances - for (auto* componentList : m_ComponentLists) - { - // Find closest focus node (handles artboard boundaries) - auto hostParentNode = FocusData::findClosestFocusNode(componentList); - if (hostParentNode == nullptr) - { - hostParentNode = effectiveParent; - } - - for (size_t i = 0; i < componentList->artboardCount(); i++) - { - auto* nestedArtboard = - componentList->artboardInstance(static_cast<int>(i)); - if (nestedArtboard != nullptr && - nestedArtboard->focusManager() != focusManager) - { - // Clean up old focus tree if it exists (with wrong - // focusManager) - nestedArtboard->cleanupFocusTree(); - nestedArtboard->buildFocusTree(focusManager, hostParentNode); - } - - // Also update the state machine's external focus manager. - // This handles the case where linkStateMachine was called before - // the parent artboard had a focus manager. - auto* smi = - componentList->stateMachineInstance(static_cast<int>(i)); - if (smi != nullptr && smi->focusManager() != focusManager) - { - smi->setExternalFocusManager(focusManager); - } - } + buildFocusTreeVisit(focusManager, this, std::move(effectiveParent)); } } @@ -2057,6 +2044,11 @@ } } + for (auto* componentList : m_ComponentLists) + { + componentList->removeListScopeFocusNode(); + } + // Clear the active focus manager reference m_activeFocusManager = nullptr; }
diff --git a/src/artboard_component_list.cpp b/src/artboard_component_list.cpp index f80b773..6b7c179 100644 --- a/src/artboard_component_list.cpp +++ b/src/artboard_component_list.cpp
@@ -120,6 +120,8 @@ artboard.second->cleanupFocusTree(); } } + removeListScopeFocusNode(); + m_listRowFocusNodes.clear(); // Destroy state machines BEFORE artboards. // StateMachineInstance owns FocusListenerGroup objects that hold raw @@ -371,45 +373,265 @@ return nullptr; } +void ArtboardComponentList::ensureListScopeFocusNode(FocusManager* focusManager, + rcp<FocusNode> hostParent) +{ + if (focusManager == nullptr) + { + return; + } + if (m_listScopeFocusNode == nullptr) + { + m_listScopeFocusNode = rcp<FocusNode>(new FocusNode(nullptr)); + m_listScopeFocusNode->canFocus(true); + m_listScopeFocusNode->canTraverse(true); + m_listScopeFocusNode->name("ArtboardComponentListScope"); + } + if (m_listScopeFocusNode->manager() == focusManager) + { + if (m_listScopeFocusNode->parent() == hostParent.get() || + (m_listScopeFocusNode->parent() == nullptr && + hostParent == nullptr)) + { + syncListRowNodesWithList(focusManager); + return; + } + } + 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 +{ + auto node = rcp<FocusNode>(new FocusNode(nullptr)); + node->canFocus(true); + node->canTraverse(true); + 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); + } +} + +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 focusables but the row is empty, focus is still + // attached under the list scope (legacy) and must be rebuilt on the row. + if (row->children().empty() && inst->rootFocusDataCount() > 0) + { + 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; + } + 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); + 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; + } + if (listItemNeedsBuildUnderRow(fm, inst, row)) + { + if (inst->focusManager() != nullptr) + { + inst->cleanupFocusTree(); + } + inst->buildFocusTree(fm, row); + } + auto* smi = stateMachineInstance(i); + if (smi != nullptr && smi->focusManager() != fm) + { + smi->setExternalFocusManager(fm); + } + } +} + void ArtboardComponentList::linkStateMachineToArtboard( StateMachineInstance* stateMachineInstance, ArtboardInstance* artboardInstance) { - if (artboardInstance != nullptr && stateMachineInstance != nullptr) + if (artboardInstance == nullptr || stateMachineInstance == nullptr) { - auto dataContext = artboardInstance->dataContext(); - stateMachineInstance->dataContext(dataContext); - // 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. - stateMachineInstance->updateDataBinds(false); + return; + } + auto dataContext = artboardInstance->dataContext(); + stateMachineInstance->dataContext(dataContext); + stateMachineInstance->updateDataBinds(false); - // Share parent artboard's focus manager and build focus tree for list - // item. - auto* parentArtboard = this->artboard(); - if (parentArtboard != nullptr && - parentArtboard->focusManager() != nullptr) - { - auto* parentFM = parentArtboard->focusManager(); - stateMachineInstance->setExternalFocusManager(parentFM); + auto* parentArtboard = this->artboard(); + if (parentArtboard != nullptr && parentArtboard->focusManager() != nullptr) + { + auto* parentFM = parentArtboard->focusManager(); + stateMachineInstance->setExternalFocusManager(parentFM); + } - // Find closest focus node (handles artboard boundaries) - auto parentNode = FocusData::findClosestFocusNode(this); - - // Build list item's focus tree under parent - artboardInstance->buildFocusTree(parentFM, parentNode); - } - - // 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); - } + // 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); } } @@ -444,6 +666,9 @@ { return; } + const std::vector<rcp<ViewModelInstanceListItem>> preListItems = + m_listItems; + const std::vector<rcp<FocusNode>> preRowNodes = m_listRowFocusNodes; m_oldItems.clear(); m_oldItems.assign(m_listItems.begin(), m_listItems.end()); m_listItems.clear(); @@ -525,6 +750,14 @@ addDirt(ComponentDirt::Components); recomputeListUsesDrawIndexSort(); syncDrawIndexListeners(); + auto* parentAb = artboard(); + if (parentAb != nullptr && parentAb->focusManager() != nullptr && + m_listScopeFocusNode != nullptr) + { + syncListRowNodesWithList(parentAb->focusManager(), + preListItems, + preRowNodes); + } } void ArtboardComponentList::syncLayoutChildren() @@ -1404,6 +1637,19 @@ } } +void ArtboardComponentList::virtualizableChanged() +{ + 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);
diff --git a/src/constraints/scrolling/scroll_virtualizer.cpp b/src/constraints/scrolling/scroll_virtualizer.cpp index 3ffd2a3..a35aa40 100644 --- a/src/constraints/scrolling/scroll_virtualizer.cpp +++ b/src/constraints/scrolling/scroll_virtualizer.cpp
@@ -1,6 +1,7 @@ #include "rive/constraints/scrolling/scroll_constraint.hpp" #include "rive/layout/layout_node_provider.hpp" #include "rive/constraints/scrolling/scroll_virtualizer.hpp" +#include <set> using namespace rive; @@ -79,6 +80,7 @@ int currentChildIndex = 0; bool isHorz = m_direction == VirtualizedDirection::horizontal; float gap = isHorz ? scroll->gap().x : scroll->gap().y; + std::set<VirtualizingComponent*> changedVirtualizingComponents; for (int i = 0; i < children.size(); i++) { @@ -269,6 +271,7 @@ if (item == nullptr) { virt->addVirtualizable(childIndex); + changedVirtualizingComponents.emplace(virt); } auto size = getItemSize(child, childIndex, isHorz); @@ -322,6 +325,10 @@ } } } + for (auto& virtualizingComponent : changedVirtualizingComponents) + { + virtualizingComponent->virtualizableChanged(); + } } void ScrollVirtualizer::recycleItems(std::vector<int> indices,
diff --git a/src/input/focus_manager.cpp b/src/input/focus_manager.cpp index f31c241..23399f0 100644 --- a/src/input/focus_manager.cpp +++ b/src/input/focus_manager.cpp
@@ -9,6 +9,7 @@ #include "rive/focus_data.hpp" #include "rive/math/aabb.hpp" #include <algorithm> +#include <cstddef> #include <cmath> #include <limits> #include <unordered_set> @@ -185,6 +186,42 @@ } } +void FocusManager::addChild(rcp<FocusNode> parent, + rcp<FocusNode> child, + size_t index) +{ + if (!child) + { + return; + } + if (child->parent()) + { + child->removeFromParent(); + } + else + { + auto it = std::find(m_rootNodes.begin(), m_rootNodes.end(), child); + if (it != m_rootNodes.end()) + { + m_rootNodes.erase(it); + } + } + child->m_manager = this; + if (parent) + { + parent->insertChild(index, std::move(child)); + } + else + { + if (index > m_rootNodes.size()) + { + index = m_rootNodes.size(); + } + m_rootNodes.insert(m_rootNodes.begin() + static_cast<ptrdiff_t>(index), + std::move(child)); + } +} + void FocusManager::removeChild(rcp<FocusNode> child) { if (!child)
diff --git a/src/input/focus_node.cpp b/src/input/focus_node.cpp index 8e30df6..a97eb85 100644 --- a/src/input/focus_node.cpp +++ b/src/input/focus_node.cpp
@@ -4,6 +4,7 @@ #include "rive/input/focus_node.hpp" #include <algorithm> +#include <cstddef> namespace rive { @@ -22,6 +23,22 @@ m_children.push_back(std::move(child)); } +void FocusNode::insertChild(size_t index, rcp<FocusNode> child) +{ + if (!child) + { + return; + } + child->removeFromParent(); + child->m_parent = this; + if (index > m_children.size()) + { + index = m_children.size(); + } + m_children.insert(m_children.begin() + static_cast<ptrdiff_t>(index), + std::move(child)); +} + void FocusNode::removeChild(rcp<FocusNode> child) { if (!child || child->m_parent != this)
diff --git a/tests/unit_tests/assets/list_focus_order.riv b/tests/unit_tests/assets/list_focus_order.riv new file mode 100644 index 0000000..bbb376d --- /dev/null +++ b/tests/unit_tests/assets/list_focus_order.riv Binary files differ
diff --git a/tests/unit_tests/runtime/focus_test.cpp b/tests/unit_tests/runtime/focus_test.cpp index 7e8c50a..5be91cb 100644 --- a/tests/unit_tests/runtime/focus_test.cpp +++ b/tests/unit_tests/runtime/focus_test.cpp
@@ -4,6 +4,9 @@ #include "rive/animation/state_machine.hpp" #include "rive/animation/state_machine_instance.hpp" #include "rive/artboard.hpp" +#include "rive/artboard_component_list.hpp" +#include "rive/focus_data.hpp" +#include "rive/node.hpp" #include "rive/input/focus_node.hpp" #include "rive/input/focus_manager.hpp" #include "utils/no_op_factory.hpp" @@ -291,6 +294,34 @@ CHECK(focusable.blurredCount == 1); } +TEST_CASE( + "List row reparent: FocusNode removeFromParent preserves primary focus", + "[FocusManager][list]") +{ + FocusManager manager; + MockFocusable fLeaf; + auto scope = make_rcp<FocusNode>(nullptr); + scope->canFocus(true); + scope->canTraverse(true); + auto row = make_rcp<FocusNode>(nullptr); + row->canFocus(true); + row->canTraverse(true); + auto leaf = make_rcp<FocusNode>(&fLeaf); + + manager.addChild(nullptr, scope); + manager.addChild(scope, row); + manager.addChild(row, leaf); + manager.setFocus(leaf); + CHECK(manager.primaryFocus() == leaf); + + row->removeFromParent(); + CHECK(manager.primaryFocus() == leaf); + + manager.addChild(scope, row, 0); + CHECK(manager.primaryFocus() == leaf); + CHECK(fLeaf.blurredCount == 0); +} + TEST_CASE("FocusManager input routing", "[FocusManager]") { FocusManager manager; @@ -970,18 +1001,21 @@ ->as<rive::ViewModelInstanceBoolean>(); stateMachine->bindViewModelInstance(vmi); + // ===> Frame 0 auto renderer = silver.makeRenderer(); stateMachine->advanceAndApply(0.016f); + // ===> Frame 1 artboard->draw(renderer.get()); silver.addFrame(); focusManager->focusNext(); + focusManager->focusNext(); // Focus on an element REQUIRE(focusManager->primaryFocus() != nullptr); stateMachine->advanceAndApply(0.016f); artboard->draw(renderer.get()); + // ===> Frame 2 silver.addFrame(); - // Hide the element, ensure the focus has been dropped opacityProp->propertyValue(0); // First advance sets the opacity to 0 @@ -990,33 +1024,40 @@ stateMachine->advanceAndApply(0.016f); REQUIRE(focusManager->primaryFocus() == nullptr); artboard->draw(renderer.get()); + // ===> Frame 3 silver.addFrame(); opacityProp->propertyValue(1); stateMachine->advanceAndApply(0.016f); artboard->draw(renderer.get()); + // ===> Frame 4 silver.addFrame(); focusManager->focusNext(); + focusManager->focusNext(); stateMachine->advanceAndApply(0.016f); REQUIRE(focusManager->primaryFocus() != nullptr); artboard->draw(renderer.get()); + // ===> Frame 5 silver.addFrame(); isMainLayout2VisibleProp->propertyValue(false); stateMachine->advanceAndApply(0.016f); focusManager->focusNext(); stateMachine->advanceAndApply(0.016f); artboard->draw(renderer.get()); + // ===> Frame 6 silver.addFrame(); // Toggles only between visible focused elements focusManager->focusNext(); stateMachine->advanceAndApply(0.016f); artboard->draw(renderer.get()); + // ===> Frame 7 silver.addFrame(); focusManager->focusNext(); focusManager->focusNext(); stateMachine->advanceAndApply(0.016f); artboard->draw(renderer.get()); + // ===> Frame 8 silver.addFrame(); // Fully rotates over all nodes @@ -1024,22 +1065,27 @@ stateMachine->advanceAndApply(0.016f); focusManager->focusNext(); artboard->draw(renderer.get()); + // ===> Frame 9 silver.addFrame(); stateMachine->advanceAndApply(0.016f); focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + // ===> Frame 10 + silver.addFrame(); + focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + focusManager->focusNext(); artboard->draw(renderer.get()); + // ===> Frame 11 silver.addFrame(); + focusManager->focusNext(); stateMachine->advanceAndApply(0.016f); - focusManager->focusNext(); - focusManager->focusNext(); artboard->draw(renderer.get()); + // ===> Frame 12 silver.addFrame(); - stateMachine->advanceAndApply(0.016f); focusManager->focusNext(); - artboard->draw(renderer.get()); - silver.addFrame(); stateMachine->advanceAndApply(0.016f); - focusManager->focusNext(); artboard->draw(renderer.get()); CHECK(silver.matches("focus_collapsing")); @@ -1420,4 +1466,153 @@ artboard->draw(renderer.get()); CHECK(silver.matches("focusable_element")); +} + +TEST_CASE("ArtboardComponentList list scope is registered on shared " + "FocusManager", + "[FocusManager][list]") +{ + auto file = ReadRiveFile("assets/component_list_1.riv"); + auto artboard = file->artboard("Main")->instance(); + REQUIRE(artboard != nullptr); + auto vmi = file->createDefaultViewModelInstance(artboard.get()); + REQUIRE(vmi != nullptr); + artboard->bindViewModelInstance(vmi); + auto sm = artboard->stateMachineAt(0); + REQUIRE(sm != nullptr); + artboard->advance(0.0f); + + auto* list = artboard->find<rive::ArtboardComponentList>("List"); + REQUIRE(list != nullptr); + auto* fm = artboard->focusManager(); + REQUIRE(fm != nullptr); + + artboard->buildFocusTree(artboard->focusManager(), nullptr); + auto scope = list->listScopeFocusNode(); + REQUIRE(scope != nullptr); + CHECK(scope->manager() == fm); + CHECK(scope->name() == "ArtboardComponentListScope"); + CHECK(scope->canFocus() == true); + CHECK(scope->canTraverse() == true); + CHECK(scope->focusable() == nullptr); +} + +TEST_CASE("List under Node: when parent has a direct FocusData, " + "findClosestFocusNode from list matches that node", + "[FocusManager][list]") +{ + // buildFocusTreeVisit pass-1: at most one direct child FocusData per + // container; if present, its focusNode is the scope for siblings (e.g. the + // list host). The walk-based fallback from the old findClosest for the + // no-direct-FocusData case is not used by the focus build anymore. + auto file = ReadRiveFile("assets/component_list_1.riv"); + auto artboard = file->artboard("Main")->instance(); + REQUIRE(artboard != nullptr); + auto vmi = file->createDefaultViewModelInstance(artboard.get()); + REQUIRE(vmi != nullptr); + artboard->bindViewModelInstance(vmi); + auto sm = artboard->stateMachineAt(0); + REQUIRE(sm != nullptr); + artboard->advance(0.0f); + + auto* list = artboard->find<rive::ArtboardComponentList>("List"); + REQUIRE(list != nullptr); + auto* p = list->parent(); + REQUIRE(p != nullptr); + REQUIRE(p->is<rive::Node>()); + + rive::rcp<rive::FocusNode> fromFirstDirectFd; + for (auto* ch : p->as<rive::Node>()->children()) + { + if (ch != nullptr && ch->is<rive::FocusData>()) + { + fromFirstDirectFd = ch->as<rive::FocusData>()->focusNode(); + break; + } + } + if (fromFirstDirectFd != nullptr) + { + CHECK(rive::FocusData::findClosestFocusNode(list) == fromFirstDirectFd); + } +} + +TEST_CASE("Focus is correctly built and updated for lists", "[silver]") +{ + rive::SerializingFactory silver; + auto file = ReadRiveFile("assets/list_focus_order.riv", &silver); + + auto artboard = file->artboardDefault(); + REQUIRE(artboard != nullptr); + + silver.frameSize(artboard->width(), artboard->height()); + + auto stateMachine = artboard->stateMachineAt(0); + auto focusManager = stateMachine->focusManager(); + + auto vmi = file->createDefaultViewModelInstance(artboard.get()); + auto stageProcessedProp = vmi->propertyValue("stageProcessed") + ->as<rive::ViewModelInstanceBoolean>(); + auto stageCountProp = + vmi->propertyValue("stageCount")->as<rive::ViewModelInstanceNumber>(); + + auto renderer = silver.makeRenderer(); + stateMachine->bindViewModelInstance(vmi); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Focuses on first element of tree + focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Focuses on last element of list + focusManager->focusNext(); + focusManager->focusNext(); + focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Inserts one element at end of list + stageProcessedProp->propertyValue(false); + stageCountProp->propertyValue(1); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Focus is on that new element + focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Focused elements is moved in the list and keeps focus + stageProcessedProp->propertyValue(false); + stageCountProp->propertyValue(2); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Focusing on the next element correctly focuses on the next element on the + // list + focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Removing the focused element from the list, clears the focus + stageProcessedProp->propertyValue(false); + stageCountProp->propertyValue(3); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Focuses back on first element of tree + focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + + CHECK(silver.matches("list_focus_order")); } \ No newline at end of file
diff --git a/tests/unit_tests/silvers/focus_collapsing.sriv b/tests/unit_tests/silvers/focus_collapsing.sriv index a3ee9b7..04e33a7 100644 --- a/tests/unit_tests/silvers/focus_collapsing.sriv +++ b/tests/unit_tests/silvers/focus_collapsing.sriv Binary files differ
diff --git a/tests/unit_tests/silvers/list_focus_order.sriv b/tests/unit_tests/silvers/list_focus_order.sriv new file mode 100644 index 0000000..046ebf0 --- /dev/null +++ b/tests/unit_tests/silvers/list_focus_order.sriv Binary files differ