blob: 5ff0f78b3dbdb407678d1ca6dc6057214900b2f8 [file] [log] [blame]
/*
* Copyright 2024 Rive
*/
#include "rive/input/focus_manager.hpp"
#include "rive/artboard.hpp"
#include "rive/artboard_host.hpp"
#include "rive/math/aabb.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <unordered_set>
namespace rive
{
Artboard* FocusManager::primaryFocusArtboard() const
{
if (m_primaryFocus == nullptr || m_primaryFocus->focusable() == nullptr)
{
return nullptr;
}
// Get the immediate artboard containing the focused element
Artboard* artboard = m_primaryFocus->focusable()->focusableArtboard();
if (artboard == nullptr)
{
return nullptr;
}
// Walk up the nested artboard chain to find the root (one with no host)
// The root artboard is the one mounted by Dart, which has no host.
while (artboard->host() != nullptr &&
artboard->host()->parentArtboard() != nullptr)
{
artboard = artboard->host()->parentArtboard();
}
return artboard;
}
FocusManager::~FocusManager()
{
// Don't call clearFocus() here - it would invoke callbacks on FocusNodes,
// but during destruction (especially Dart finalizers) callbacks may be
// invalid. Just clear the reference directly.
m_primaryFocus = nullptr;
}
void FocusManager::setFocus(rcp<FocusNode> node)
{
if (node == m_primaryFocus)
{
return;
}
if (node && !node->canFocus())
{
return;
}
FocusNode* oldFocus = m_primaryFocus.get();
m_primaryFocus = std::move(node);
notifyFocusChange(oldFocus, m_primaryFocus.get());
}
void FocusManager::clearFocus()
{
if (m_primaryFocus)
{
// Move to local variable to keep node alive during notification.
// Setting m_primaryFocus to nullptr first ensures hasFocus() returns
// false during the blurred() callback, but the node stays alive until
// notification completes.
auto oldFocus = std::move(m_primaryFocus);
notifyFocusChange(oldFocus.get(), nullptr);
// oldFocus is released here after notification is complete
}
}
bool FocusManager::hasFocus(rcp<FocusNode> node) const
{
// The hasFocus flag on the node is maintained by notifyFocusChange
return node && node->hasFocus();
}
bool FocusManager::hasPrimaryFocus(rcp<FocusNode> node) const
{
return m_primaryFocus == node;
}
void FocusManager::addChild(rcp<FocusNode> parent, rcp<FocusNode> child)
{
if (!child)
{
return;
}
// Remove from old location first
if (child->parent())
{
child->removeFromParent();
}
else
{
// Remove from root nodes if it was a root
auto it = std::find(m_rootNodes.begin(), m_rootNodes.end(), child);
if (it != m_rootNodes.end())
{
m_rootNodes.erase(it);
}
}
// Set the manager reference on the child
child->m_manager = this;
if (parent)
{
parent->addChild(std::move(child));
}
else
{
// Add to root nodes
m_rootNodes.push_back(std::move(child));
}
}
void FocusManager::removeChild(rcp<FocusNode> child)
{
if (!child)
{
return;
}
// Clear focus if this node or descendant has focus
if (hasFocus(child))
{
clearFocus();
}
// Clear the manager reference
child->m_manager = nullptr;
if (child->parent())
{
child->removeFromParent();
}
else
{
// Remove from root nodes
auto it = std::find(m_rootNodes.begin(), m_rootNodes.end(), child);
if (it != m_rootNodes.end())
{
m_rootNodes.erase(it);
}
}
}
bool FocusManager::focusNext()
{
return findNextFocusable(m_primaryFocus.get(), true) != nullptr;
}
bool FocusManager::focusPrevious()
{
return findNextFocusable(m_primaryFocus.get(), false) != nullptr;
}
// Helper to get root-space world position from FocusNode
// Computes center from bounds if available, falls back to Focusable
static bool getRootPosition(FocusNode* node, Vec2D& outPosition)
{
if (!node)
{
return false;
}
// First try bounds stored directly on FocusNode (set during update cycle)
if (node->hasWorldBounds())
{
outPosition = node->worldBounds().center();
return true;
}
// Fall back to Focusable for legacy implementations
if (node->focusable())
{
return node->focusable()->worldPosition(outPosition);
}
return false;
}
// Helper to get root-space world bounds from FocusNode
// First checks bounds stored on FocusNode, falls back to Focusable
static bool getRootBounds(FocusNode* node, AABB& outBounds)
{
if (!node)
{
return false;
}
// First try bounds stored directly on FocusNode (set during update cycle)
if (node->hasWorldBounds())
{
outBounds = node->worldBounds();
return true;
}
// Fall back to Focusable for legacy implementations
if (node->focusable())
{
return node->focusable()->worldBounds(outBounds);
}
return false;
}
// Helper to check if a node is a leaf (no traversable children)
static bool isLeaf(FocusNode* node)
{
for (const auto& child : node->children())
{
if (child->canFocus() && child->canTraverse())
{
return false;
}
}
return true;
}
// Helper to collect all traversable leaf focus nodes recursively
// Only collects leaves (nodes with no traversable children) to match
// next/prev behavior
static void collectAllTraversableNodes(const std::vector<rcp<FocusNode>>& nodes,
std::vector<FocusNode*>& result)
{
for (const auto& node : nodes)
{
if (node->canFocus() && node->canTraverse() && isLeaf(node.get()))
{
result.push_back(node.get());
}
// Recurse into children
collectAllTraversableNodes(node->children(), result);
}
}
// Calculate overlap on the orthogonal axis (perpendicular to navigation)
// Returns the length of overlap, or 0 if no overlap
static float calculateOverlap(float aMin, float aMax, float bMin, float bMax)
{
float overlapMin = std::max(aMin, bMin);
float overlapMax = std::min(aMax, bMax);
return std::max(0.0f, overlapMax - overlapMin);
}
// CSS Spatial Navigation-inspired scoring for bounds-aware navigation
// Formula: distance = displacement + orthogonalWeight * orthogonalDistance -
// sqrt(overlap)
struct ScoreBreakdown
{
float displacement;
float orthogonalDistance;
float overlap;
float orthogonalWeight;
float total;
bool rejected;
};
static ScoreBreakdown scoreCandidateBoundsDetailed(const AABB& current,
const AABB& candidate,
Direction direction)
{
// CSS Spatial Navigation weights
const float horizontalWeight = 30.0f;
const float verticalWeight = 2.0f;
ScoreBreakdown result = {};
switch (direction)
{
case Direction::left:
// Displacement: distance from current's left edge to candidate's
// right edge
result.displacement = current.left() - candidate.right();
if (result.displacement < 0)
{
result.rejected = true;
result.total = std::numeric_limits<float>::max();
return result;
}
// Orthogonal: vertical distance between closest edges
result.orthogonalDistance =
std::max(0.0f,
std::max(candidate.top() - current.bottom(),
current.top() - candidate.bottom()));
result.overlap = calculateOverlap(current.top(),
current.bottom(),
candidate.top(),
candidate.bottom());
result.orthogonalWeight = horizontalWeight;
break;
case Direction::right:
result.displacement = candidate.left() - current.right();
if (result.displacement < 0)
{
result.rejected = true;
result.total = std::numeric_limits<float>::max();
return result;
}
result.orthogonalDistance =
std::max(0.0f,
std::max(candidate.top() - current.bottom(),
current.top() - candidate.bottom()));
result.overlap = calculateOverlap(current.top(),
current.bottom(),
candidate.top(),
candidate.bottom());
result.orthogonalWeight = horizontalWeight;
break;
case Direction::up:
result.displacement = current.top() - candidate.bottom();
if (result.displacement < 0)
{
result.rejected = true;
result.total = std::numeric_limits<float>::max();
return result;
}
result.orthogonalDistance =
std::max(0.0f,
std::max(candidate.left() - current.right(),
current.left() - candidate.right()));
result.overlap = calculateOverlap(current.left(),
current.right(),
candidate.left(),
candidate.right());
result.orthogonalWeight = verticalWeight;
break;
case Direction::down:
result.displacement = candidate.top() - current.bottom();
if (result.displacement < 0)
{
result.rejected = true;
result.total = std::numeric_limits<float>::max();
return result;
}
result.orthogonalDistance =
std::max(0.0f,
std::max(candidate.left() - current.right(),
current.left() - candidate.right()));
result.overlap = calculateOverlap(current.left(),
current.right(),
candidate.left(),
candidate.right());
result.orthogonalWeight = verticalWeight;
break;
}
// CSS-inspired formula: displacement + weighted orthogonal - sqrt(overlap)
// The sqrt(overlap) bonus favors candidates that are "in line" with current
result.total = result.displacement +
result.orthogonalWeight * result.orthogonalDistance -
std::sqrt(result.overlap);
return result;
}
static float scoreCandidateBounds(const AABB& current,
const AABB& candidate,
Direction direction)
{
return scoreCandidateBoundsDetailed(current, candidate, direction).total;
}
// Point-based scoring fallback for nodes without bounds
static float scoreCandidatePoint(const Vec2D& currentPos,
const Vec2D& candidatePos,
Direction direction)
{
const float horizontalWeight = 30.0f;
const float verticalWeight = 2.0f;
Vec2D delta = candidatePos - currentPos;
float primary, orthogonal, orthogonalWeight;
switch (direction)
{
case Direction::left:
primary = -delta.x;
orthogonal = std::abs(delta.y);
orthogonalWeight = horizontalWeight;
break;
case Direction::right:
primary = delta.x;
orthogonal = std::abs(delta.y);
orthogonalWeight = horizontalWeight;
break;
case Direction::up:
primary = -delta.y;
orthogonal = std::abs(delta.x);
orthogonalWeight = verticalWeight;
break;
case Direction::down:
primary = delta.y;
orthogonal = std::abs(delta.x);
orthogonalWeight = verticalWeight;
break;
}
if (primary <= 0)
{
return std::numeric_limits<float>::max();
}
return primary + orthogonalWeight * orthogonal;
}
FocusNode* FocusManager::findNodeInDirection(FocusNode* current,
Direction direction) const
{
if (!current)
{
return nullptr;
}
std::vector<FocusNode*> candidates;
collectAllTraversableNodes(m_rootNodes, candidates);
FocusNode* best = nullptr;
float bestScore = std::numeric_limits<float>::max();
// Try to get bounds for current node
AABB currentBounds;
bool currentHasBounds = getRootBounds(current, currentBounds);
// Fallback to position if no bounds
Vec2D currentPos;
if (!currentHasBounds && !getRootPosition(current, currentPos))
{
return nullptr;
}
for (FocusNode* candidate : candidates)
{
if (candidate == current)
{
continue;
}
float score;
// Try bounds-based scoring first
AABB candidateBounds;
if (currentHasBounds && getRootBounds(candidate, candidateBounds))
{
score =
scoreCandidateBounds(currentBounds, candidateBounds, direction);
}
else
{
// Fall back to point-based scoring
Vec2D candidatePos;
if (!getRootPosition(candidate, candidatePos))
{
continue;
}
if (currentHasBounds)
{
// Use center of current bounds
score = scoreCandidatePoint(currentBounds.center(),
candidatePos,
direction);
}
else
{
score =
scoreCandidatePoint(currentPos, candidatePos, direction);
}
}
if (score < bestScore)
{
bestScore = score;
best = candidate;
}
}
return best;
}
bool FocusManager::focusLeft()
{
FocusNode* next =
findNodeInDirection(m_primaryFocus.get(), Direction::left);
if (next)
{
setFocus(ref_rcp(next));
return true;
}
return false;
}
bool FocusManager::focusRight()
{
FocusNode* next =
findNodeInDirection(m_primaryFocus.get(), Direction::right);
if (next)
{
setFocus(ref_rcp(next));
return true;
}
return false;
}
bool FocusManager::focusUp()
{
FocusNode* next = findNodeInDirection(m_primaryFocus.get(), Direction::up);
if (next)
{
setFocus(ref_rcp(next));
return true;
}
return false;
}
bool FocusManager::focusDown()
{
FocusNode* next =
findNodeInDirection(m_primaryFocus.get(), Direction::down);
if (next)
{
setFocus(ref_rcp(next));
return true;
}
return false;
}
bool FocusManager::keyInput(Key key,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat)
{
// Bubble up through focus tree until someone handles the input
FocusNode* node = m_primaryFocus.get();
while (node != nullptr)
{
if (node->keyInput(key, modifiers, isPressed, isRepeat))
{
return true;
}
node = node->parent();
}
return false;
}
bool FocusManager::textInput(const std::string& text)
{
// Bubble up through focus tree until someone handles the input
FocusNode* node = m_primaryFocus.get();
while (node != nullptr)
{
if (node->textInput(text))
{
return true;
}
node = node->parent();
}
return false;
}
void FocusManager::notifyFocusChange(FocusNode* oldFocus, FocusNode* newFocus)
{
// Find the common ancestor to avoid unnecessary blur/focus notifications
// on shared ancestors
FocusNode* commonAncestor = nullptr;
if (oldFocus != nullptr && newFocus != nullptr)
{
// Build a set of ancestors from oldFocus
std::unordered_set<FocusNode*> oldAncestors;
for (FocusNode* node = oldFocus; node != nullptr; node = node->parent())
{
oldAncestors.insert(node);
}
// Find the first ancestor of newFocus that's also an ancestor of
// oldFocus
for (FocusNode* node = newFocus; node != nullptr; node = node->parent())
{
if (oldAncestors.count(node) > 0)
{
commonAncestor = node;
break;
}
}
}
// Walk up from oldFocus, clear hasFocus flag and notify blurred
// Stop at common ancestor (don't blur it or its ancestors)
FocusNode* current = oldFocus;
while (current != nullptr && current != commonAncestor &&
current->hasFocus())
{
current->setHasFocus(false);
current->blurred();
current = current->parent();
}
// Walk up from newFocus, set hasFocus flag and notify focused
// Stop at common ancestor (don't re-focus it or its ancestors)
current = newFocus;
while (current != nullptr && current != commonAncestor &&
!current->hasFocus())
{
current->setHasFocus(true);
current->focused();
current = current->parent();
}
#ifdef WITH_RIVE_TOOLS
if (m_focusChangedCallback)
{
m_focusChangedCallback();
}
// Check if we should fire scroll-into-view callback for Dart-mounted
// artboards. This happens when the focused element is in an artboard
// whose root has no host (mounted by Dart).
if (m_scrollIntoViewCallback && newFocus != nullptr)
{
AABB bounds;
if (getRootBounds(newFocus, bounds))
{
// Get the immediate artboard containing the focused element
Artboard* artboard =
newFocus->focusable() != nullptr
? newFocus->focusable()->focusableArtboard()
: nullptr;
if (artboard != nullptr)
{
// Walk up to find the highest artboard (host == nullptr).
// This is the artboard that Dart is hosting.
while (artboard->host() != nullptr &&
artboard->host()->parentArtboard() != nullptr)
{
artboard = artboard->host()->parentArtboard();
}
// If this artboard has no host, it's Dart-mounted
// Pass it to the callback so Dart can find and scroll it
if (artboard->host() == nullptr)
{
m_scrollIntoViewCallback(bounds, artboard);
}
}
}
}
#endif
}
std::vector<FocusNode*> FocusManager::getTraversableNodes(
FocusNode* scope) const
{
std::vector<FocusNode*> result;
// Get children from scope or root nodes
const std::vector<rcp<FocusNode>>* childList =
scope ? &scope->children() : &m_rootNodes;
for (const auto& child : *childList)
{
if (child->canFocus() && child->canTraverse())
{
result.push_back(child.get());
}
}
// Sort by tabIndex, then by tree order (which is insertion order)
std::stable_sort(result.begin(),
result.end(),
[](FocusNode* a, FocusNode* b) {
return a->tabIndex() < b->tabIndex();
});
return result;
}
// Helper to get the first focusable leaf (deepest first child)
static FocusNode* getFirstLeaf(FocusNode* node, const FocusManager* manager)
{
if (!node)
return nullptr;
// If node has traversable children, descend to first child
auto children = manager->getTraversableNodes(node);
if (!children.empty())
{
return getFirstLeaf(children.front(), manager);
}
// No traversable children - this is a leaf
return node;
}
// Helper to get the last focusable leaf (deepest last child)
static FocusNode* getLastLeaf(FocusNode* node, const FocusManager* manager)
{
if (!node)
return nullptr;
// If node has traversable children, descend to last child
auto children = manager->getTraversableNodes(node);
if (!children.empty())
{
return getLastLeaf(children.back(), manager);
}
// No traversable children - this is a leaf
return node;
}
FocusNode* FocusManager::findNextFocusable(FocusNode* current,
bool forward) const
{
FocusNode* scope = current ? current->parent() : nullptr;
auto traversable = getTraversableNodes(scope);
if (traversable.empty())
{
// No traversable nodes at this level, try parent scope
if (scope)
{
return findNextFocusable(scope, forward);
}
return nullptr;
}
// Find current position
auto it = std::find(traversable.begin(), traversable.end(), current);
FocusNode* next = nullptr;
if (it == traversable.end())
{
// No current focus or current not in this list, pick first or last leaf
FocusNode* candidate =
forward ? traversable.front() : traversable.back();
next = forward ? getFirstLeaf(candidate, this)
: getLastLeaf(candidate, this);
}
else
{
if (forward)
{
++it;
if (it == traversable.end())
{
// At end, check edge behavior
EdgeBehavior edge =
scope ? scope->edgeBehavior() : EdgeBehavior::parentScope;
switch (edge)
{
case EdgeBehavior::closedLoop:
{
FocusNode* candidate = traversable.front();
next = getFirstLeaf(candidate, this);
break;
}
case EdgeBehavior::stop:
next = current; // Stay on current
break;
case EdgeBehavior::parentScope:
// Exit to parent scope - find scope's next sibling
if (scope)
{
return findNextFocusable(scope, forward);
}
// No parent, wrap to first leaf
next = getFirstLeaf(traversable.front(), this);
break;
}
}
else
{
// Move to next sibling, descend to its first leaf
next = getFirstLeaf(*it, this);
}
}
else
{
// Going backward
if (it == traversable.begin())
{
// At beginning of scope, check edge behavior
EdgeBehavior edge =
scope ? scope->edgeBehavior() : EdgeBehavior::parentScope;
switch (edge)
{
case EdgeBehavior::closedLoop:
{
FocusNode* candidate = traversable.back();
next = getLastLeaf(candidate, this);
break;
}
case EdgeBehavior::stop:
next = current;
break;
case EdgeBehavior::parentScope:
// Exit to parent scope - find scope's previous sibling
if (scope)
{
return findNextFocusable(scope, forward);
}
// No parent, wrap to last leaf
next = getLastLeaf(traversable.back(), this);
break;
}
}
else
{
// Move to previous sibling, descend to its last leaf
--it;
next = getLastLeaf(*it, this);
}
}
}
if (next && next != current)
{
const_cast<FocusManager*>(this)->setFocus(ref_rcp(next));
return next;
}
return nullptr;
}
} // namespace rive