blob: 054673c47d8f6c71723b9bb318b558cfed0cf3cc [file] [log] [blame]
#include "rive/focus_data.hpp"
#include "rive/artboard.hpp"
#include "rive/artboard_component_list.hpp"
#include "rive/artboard_host.hpp"
#include "rive/constraints/scrolling/scroll_constraint.hpp"
#include "rive/input/focus_input_traversal.hpp"
#include "rive/input/focusable.hpp"
#include "rive/input/focus_listener.hpp"
#include "rive/input/focus_manager.hpp"
#include "rive/layout_component.hpp"
#include "rive/math/mat2d.hpp"
#include "rive/nested_artboard.hpp"
#include "rive/node.hpp"
#include "rive/parent_traversal.hpp"
#include "rive/text/text_input.hpp"
#include "rive/transform_component.hpp"
#include "rive/world_transform_component.hpp"
#include <algorithm>
using namespace rive;
FocusData::~FocusData()
{
if (m_focusNode != nullptr)
{
// Clear the focusable pointer first to prevent callbacks during removal
m_focusNode->clearFocusable();
// Remove from manager if registered
auto* manager = m_focusNode->manager();
if (manager != nullptr)
{
manager->removeChild(m_focusNode);
}
// m_focusNode (rcp) is released automatically when this destructor ends
}
}
rcp<FocusNode> FocusData::focusNode()
{
if (m_focusNode == nullptr)
{
m_focusNode = rcp<FocusNode>(new FocusNode(this));
m_focusNode->canFocus(m_CanFocus);
m_focusNode->canTouch(m_CanTouch);
m_focusNode->canTraverse(m_CanTraverse);
m_focusNode->edgeBehavior(
static_cast<EdgeBehavior>(m_EdgeBehaviorValue));
m_focusNode->name(name());
// Set initial world bounds if available
updateWorldBounds();
}
return m_focusNode;
}
void FocusData::addFocusListener(FocusListener* listener)
{
m_focusListeners.push_back(listener);
}
void FocusData::removeFocusListener(FocusListener* listener)
{
auto it =
std::find(m_focusListeners.begin(), m_focusListeners.end(), listener);
if (it != m_focusListeners.end())
{
m_focusListeners.erase(it);
}
}
void FocusData::focus()
{
// Note: In C++ runtime, focus() needs a FocusManager to set focus.
// The StateMachineInstance will handle this through its own FocusManager.
// This method is provided for API completeness but the actual focus
// setting happens through StateMachineInstance::setFocus(FocusData*).
}
bool FocusData::keyInput(Key value,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat)
{
// Search only the children of this FocusData's owner (parent Node),
// not the entire artboard.
auto* parentNode = parent();
if (parentNode == nullptr || !parentNode->is<Node>())
{
return false;
}
// If the parent is Focusable and immediately handles the input, we're done!
auto* focusable = Focusable::from(parentNode);
if (focusable != nullptr &&
focusable->keyInput(value, modifiers, isPressed, isRepeat))
{
return true;
}
for (auto* child : parentNode->as<Node>()->children())
{
if (sendInputToFocusableChildren(child,
&Focusable::keyInput,
value,
modifiers,
isPressed,
isRepeat))
{
return true;
}
}
return false;
}
bool FocusData::textInput(const std::string& text)
{
// Search only the children of this FocusData's owner (parent Node),
// not the entire artboard.
auto* parentNode = parent();
if (parentNode == nullptr || !parentNode->is<Node>())
{
return false;
}
// If the parent is Focusable and immediately handles the input, we're done!
auto* focusable = Focusable::from(parentNode);
if (focusable != nullptr && focusable->textInput(text))
{
return true;
}
// Look through children for a Focusable to handle the text input.
for (auto* child : parentNode->as<Node>()->children())
{
if (sendInputToFocusableChildren(child, &Focusable::textInput, text))
{
return true;
}
}
return false;
}
void FocusData::scrollIntoView()
{
// Find closest LayoutComponent ancestor
Component* layoutComponent = parent();
while (layoutComponent != nullptr &&
!layoutComponent->is<LayoutComponent>())
{
layoutComponent = layoutComponent->parent();
}
if (layoutComponent == nullptr)
{
return;
}
// Get element bounds in its artboard's world space.
// We'll transform these bounds as we cross artboard boundaries.
AABB elementBounds = layoutComponent->as<LayoutComponent>()->worldBounds();
// Walk up the hierarchy finding all ScrollConstraints.
// This traversal crosses artboard boundaries via the host chain.
ParentTraversal traversal(this);
while (auto* p = traversal.next())
{
// When crossing an artboard boundary, transform element bounds from
// nested artboard space to parent artboard space.
if (traversal.didCrossBoundary())
{
auto* host = traversal.crossingHost();
auto* sourceArtboard = traversal.sourceArtboard();
auto* artboardInstance =
sourceArtboard->is<ArtboardInstance>()
? sourceArtboard->as<ArtboardInstance>()
: nullptr;
Mat2D hostTransform =
host->worldTransformForArtboard(artboardInstance);
Vec2D min =
hostTransform * Vec2D(elementBounds.minX, elementBounds.minY);
Vec2D max =
hostTransform * Vec2D(elementBounds.maxX, elementBounds.maxY);
elementBounds = AABB(min.x, min.y, max.x, max.y);
}
// ScrollConstraint is a Constraint that targets Content, so we check
// TransformComponent::constraints() to find ScrollConstraints.
if (p->is<TransformComponent>())
{
auto* tc = p->as<TransformComponent>();
for (auto* constraint : tc->constraints())
{
if (!constraint->is<ScrollConstraint>())
{
continue;
}
scrollConstraintToShowBounds(constraint->as<ScrollConstraint>(),
elementBounds);
}
}
}
}
void FocusData::scrollConstraintToShowBounds(ScrollConstraint* constraint,
const AABB& elementBounds)
{
auto* content = constraint->content();
auto* viewport = constraint->viewport();
if (content == nullptr || viewport == nullptr)
{
return;
}
// Get viewport position in world space.
const Mat2D& viewportTransform = viewport->worldTransform();
float viewportWorldX = viewportTransform[4];
float viewportWorldY = viewportTransform[5];
float viewportWidth = constraint->viewportWidth();
float viewportHeight = constraint->viewportHeight();
// Element position relative to viewport in world space.
// This directly tells us where the element appears in the viewport.
float viewportLeft = elementBounds.minX - viewportWorldX;
float viewportTop = elementBounds.minY - viewportWorldY;
float viewportRight = elementBounds.maxX - viewportWorldX;
float viewportBottom = elementBounds.maxY - viewportWorldY;
// Use effective scroll offset (target if animating) to handle rapid focus
// changes.
float effectiveScrollX = constraint->effectiveScrollOffsetX();
float effectiveScrollY = constraint->effectiveScrollOffsetY();
float deltaX = 0.0f;
float deltaY = 0.0f;
// Calculate horizontal scroll adjustment.
if (constraint->constrainsHorizontal())
{
if (viewportLeft < 0)
{
// Element is to the left of viewport, scroll right (increase
// offset)
deltaX = -viewportLeft;
}
else if (viewportRight > viewportWidth)
{
// Element is to the right of viewport, scroll left (decrease
// offset)
deltaX = -(viewportRight - viewportWidth);
}
}
// Calculate vertical scroll adjustment
if (constraint->constrainsVertical())
{
if (viewportTop < 0)
{
// Element is above viewport, scroll up (increase offset)
deltaY = -viewportTop;
}
else if (viewportBottom > viewportHeight)
{
// Element is below viewport, scroll down (decrease offset)
deltaY = -(viewportBottom - viewportHeight);
}
}
// Apply scroll if needed (using animated scroll)
if (deltaX != 0 || deltaY != 0)
{
// Add delta to effective scroll offset to get target position.
float targetX = effectiveScrollX + deltaX;
float targetY = effectiveScrollY + deltaY;
constraint->scrollToPosition(targetX, targetY);
}
}
void FocusData::focused()
{
// Scroll this element into view, crossing artboard boundaries as needed
scrollIntoView();
// Notify listeners
for (auto* listener : m_focusListeners)
{
listener->onFocused();
}
}
void FocusData::blurred()
{
for (auto* listener : m_focusListeners)
{
listener->onBlurred();
}
}
void FocusData::canFocusChanged()
{
if (m_focusNode != nullptr)
{
m_focusNode->canFocus(m_CanFocus);
}
}
void FocusData::canTouchChanged()
{
if (m_focusNode != nullptr)
{
m_focusNode->canTouch(m_CanTouch);
}
}
void FocusData::canTraverseChanged()
{
if (m_focusNode != nullptr)
{
m_focusNode->canTraverse(m_CanTraverse);
}
}
void FocusData::edgeBehaviorValueChanged()
{
if (m_focusNode != nullptr)
{
m_focusNode->edgeBehavior(
static_cast<EdgeBehavior>(m_EdgeBehaviorValue));
}
}
FocusData* FocusData::findParentFocusData() const
{
// FocusData's parent is typically a Node. Walk up from the parent
// to find any ancestor Node that has a FocusData child.
auto* current = parent();
while (current != nullptr)
{
if (current->is<Node>())
{
auto* node = current->as<Node>();
for (auto* child : node->children())
{
if (child->is<FocusData>() && child != this)
{
return child->as<FocusData>();
}
}
}
current = current->parent();
}
return nullptr;
}
rcp<FocusNode> FocusData::findClosestFocusNode(Component* component)
{
if (component == nullptr)
{
return nullptr;
}
auto* current = component->parent();
while (current != nullptr)
{
// Check if we've hit an artboard boundary
if (current->is<Artboard>())
{
auto* artboard = current->as<Artboard>();
// Try to cross the artboard boundary via the host component
auto* host = artboard->host();
if (host != nullptr)
{
auto* hostComponent = host->hostComponent();
if (hostComponent != nullptr &&
hostComponent->is<ContainerComponent>())
{
// Continue searching from the host component (e.g.,
// NestedArtboard)
current = hostComponent->as<ContainerComponent>();
continue;
}
}
#ifdef WITH_RIVE_TOOLS
// Fall back to externally-set parent focus node (editor scenario)
auto externalNode = artboard->externalParentFocusNode();
if (externalNode != nullptr)
{
return externalNode;
}
#endif
// No way to traverse up, stop searching
return nullptr;
}
// Check if this node has a FocusData child
if (current->is<Node>())
{
for (auto* child : current->as<Node>()->children())
{
if (child->is<FocusData>())
{
return child->as<FocusData>()->focusNode();
}
}
}
current = current->parent();
}
return nullptr;
}
bool FocusData::worldPosition(Vec2D& outPosition)
{
// Get local position from parent transform component
auto* parentComponent = parent();
if (!parentComponent || !parentComponent->is<WorldTransformComponent>())
{
return false;
}
Vec2D localPos =
parentComponent->as<WorldTransformComponent>()->worldTranslation();
// Transform to root artboard space (handles nested artboards)
auto* ab = artboard();
if (ab)
{
outPosition = ab->rootTransform(localPos);
}
else
{
outPosition = localPos;
}
return true;
}
void FocusData::nameChanged()
{
if (m_focusNode != nullptr)
{
m_focusNode->name(name());
}
}
void FocusData::buildDependencies()
{
Super::buildDependencies();
// Depend on parent's world transform so we update when it moves
auto* parentComponent = parent();
if (parentComponent != nullptr)
{
parentComponent->addDependent(this);
}
}
void FocusData::update(ComponentDirt value)
{
if ((value & ComponentDirt::WorldTransform) ==
ComponentDirt::WorldTransform)
{
updateWorldBounds();
}
}
void FocusData::updateWorldBounds()
{
if (m_focusNode == nullptr)
{
return;
}
auto* parentComponent = parent();
if (parentComponent != nullptr && parentComponent->is<LayoutComponent>())
{
// LayoutComponent has worldBounds based on its layout dimensions
AABB bounds = parentComponent->as<LayoutComponent>()->worldBounds();
// Transform to root artboard space (handles nested artboards)
auto* ab = artboard();
if (ab != nullptr)
{
Vec2D min = ab->rootTransform(Vec2D(bounds.minX, bounds.minY));
Vec2D max = ab->rootTransform(Vec2D(bounds.maxX, bounds.maxY));
bounds = AABB(min.x, min.y, max.x, max.y);
}
m_focusNode->worldBounds(bounds);
}
else
{
// For non-layout parents, clear bounds (will fall back to position)
m_focusNode->clearWorldBounds();
}
}