| /* | |
| * Copyright 2024 Rive | |
| */ | |
| #ifndef _RIVE_FOCUS_MANAGER_HPP_ | |
| #define _RIVE_FOCUS_MANAGER_HPP_ | |
| #include "rive/input/focus_node.hpp" | |
| #include "rive/refcnt.hpp" | |
| #include <vector> | |
| namespace rive | |
| { | |
| class Artboard; | |
| class ListenerInvocation; | |
| class ScriptedDrawable; | |
| /// Direction for directional focus navigation | |
| enum class Direction : uint8_t | |
| { | |
| left, | |
| right, | |
| up, | |
| down | |
| }; | |
| /// FocusManager tracks focus state and provides traversal. | |
| /// Hierarchy is stored on FocusNode itself (parent/children). | |
| class FocusManager | |
| { | |
| public: | |
| FocusManager() = default; | |
| ~FocusManager(); | |
| // === Focus State === | |
| rcp<FocusNode> primaryFocus() const { return m_primaryFocus; } | |
| FocusNode* primaryFocusPtr() const { return m_primaryFocus.get(); } | |
| void setFocus(rcp<FocusNode> node); | |
| void clearFocus(); | |
| /// Clears primary focus if it targets FocusData that is no longer visible | |
| /// in the hierarchy (collapsed, hidden, opacity 0, nested host hidden). | |
| void dropFocusIfFocusTargetHidden(); | |
| bool hasFocus(rcp<FocusNode> node) const; // node or descendant has focus | |
| bool hasPrimaryFocus( | |
| rcp<FocusNode> node) const; // node is the primary focus | |
| /// Get the world bounds of the primary focus node. | |
| /// Returns true if bounds are valid, false if no focus or no bounds. | |
| bool primaryFocusBounds(AABB& outBounds) const | |
| { | |
| if (m_primaryFocus == nullptr || !m_primaryFocus->hasWorldBounds()) | |
| { | |
| return false; | |
| } | |
| outBounds = m_primaryFocus->worldBounds(); | |
| return true; | |
| } | |
| /// Get the root artboard that contains the primary focus node. | |
| /// This walks up the nested artboard chain to find the topmost artboard | |
| /// (the one mounted by Dart/the host). | |
| /// Returns nullptr if there is no focus or the focusable has no artboard. | |
| Artboard* primaryFocusArtboard() const; | |
| /// Get the immediate artboard that contains the primary focus node. | |
| /// Unlike primaryFocusArtboard(), this returns the direct parent artboard | |
| /// without walking up nested artboard chains. | |
| Artboard* primaryFocusImmediateArtboard() const | |
| { | |
| if (m_primaryFocus == nullptr || m_primaryFocus->focusable() == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return m_primaryFocus->focusable()->focusableArtboard(); | |
| } | |
| // === Hierarchy === | |
| // 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); | |
| const std::vector<rcp<FocusNode>>& rootNodes() const { return m_rootNodes; } | |
| // === Traversal === | |
| bool focusNext(); | |
| bool focusPrevious(); | |
| // Directional navigation (gamepad/arrow keys) | |
| bool focusLeft(); | |
| bool focusRight(); | |
| bool focusUp(); | |
| bool focusDown(); | |
| // Get traversable children of a scope (or root nodes if scope is null) | |
| // Sorted by tabIndex, filtered by canFocus && canTraverse | |
| std::vector<FocusNode*> getTraversableNodes(FocusNode* scope) const; | |
| // === Input Routing === | |
| bool keyInput(Key key, | |
| KeyModifiers modifiers, | |
| bool isPressed, | |
| bool isRepeat); | |
| bool textInput(const std::string& text); | |
| /// Bubble gamepad invocations from primary focus up through ancestors. | |
| /// `outDispatchedScriptedDrawable` (when non-null) is filled with the | |
| /// `ScriptedDrawable` that the focus tree forwarded the event to so | |
| /// callers can avoid double-dispatching during a separate broadcast pass. | |
| bool gamepadDispatch( | |
| const ListenerInvocation& invocation, | |
| ScriptedDrawable** outDispatchedScriptedDrawable = nullptr); | |
| #ifdef WITH_RIVE_TOOLS | |
| // === Callbacks (editor/tools only) === | |
| using FocusChangedCallback = void (*)(); | |
| void setFocusChangedCallback(FocusChangedCallback callback) | |
| { | |
| m_focusChangedCallback = callback; | |
| } | |
| /// Callback for scroll-into-view requests from Dart-mounted artboards. | |
| /// Called when focus changes to an element in an artboard whose root | |
| /// has no host (i.e., is mounted by Dart). | |
| /// Parameters: | |
| /// - bounds: world bounds of the focused element to scroll into view | |
| /// - artboard: the artboard directly hosted by the Dart root artboard | |
| using ScrollIntoViewCallback = void (*)(const AABB& bounds, | |
| Artboard* artboard); | |
| void setScrollIntoViewCallback(ScrollIntoViewCallback callback) | |
| { | |
| m_scrollIntoViewCallback = callback; | |
| } | |
| #endif | |
| /// True if any node in the tree is backed by focusable data or can take | |
| /// focus — even while currently ineligible for traversal. Structural | |
| /// scopes (unbacked, canFocus=false) don't count on their own. Gates | |
| /// one-time keyboard setup in high-level runtimes via | |
| /// StateMachineInstance::hasFocusNodes(). | |
| bool hasFocusableContent() const; | |
| private: | |
| rcp<FocusNode> m_primaryFocus; | |
| std::vector<rcp<FocusNode>> m_rootNodes; | |
| void removeManager(rcp<FocusNode>); | |
| // Erase a node from m_rootNodes if present (no-op otherwise). | |
| void eraseRoot(const rcp<FocusNode>& node); | |
| #ifdef WITH_RIVE_TOOLS | |
| FocusChangedCallback m_focusChangedCallback = nullptr; | |
| ScrollIntoViewCallback m_scrollIntoViewCallback = nullptr; | |
| #endif | |
| void notifyFocusChange(FocusNode* oldFocus, FocusNode* newFocus); | |
| FocusNode* findNextFocusable(FocusNode* current, bool forward) const; | |
| FocusNode* findNodeInDirection(FocusNode* current, | |
| Direction direction) const; | |
| }; | |
| } // namespace rive | |
| #endif |