| #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(); |
| } |
| } |