| /* | |
| * Copyright 2024 Rive | |
| */ | |
| #ifndef _RIVE_FOCUS_NODE_HPP_ | |
| #define _RIVE_FOCUS_NODE_HPP_ | |
| #include "rive/input/focusable.hpp" | |
| #include "rive/math/aabb.hpp" | |
| #include "rive/refcnt.hpp" | |
| #include <cstdint> | |
| #include <string> | |
| #include <vector> | |
| namespace rive | |
| { | |
| class FocusManager; // Forward declaration | |
| class ListenerInvocation; | |
| enum class EdgeBehavior : uint8_t | |
| { | |
| parentScope = 0, // Focus exits to parent scope's next focusable | |
| closedLoop = 1, // Focus wraps from last to first within this scope | |
| stop = 2, // Focus stays on boundary element | |
| }; | |
| class FocusNode : public RefCnt<FocusNode> | |
| { | |
| public: | |
| FocusNode(Focusable* focusable = nullptr) : m_focusable(focusable) | |
| { | |
| if (focusable != nullptr) | |
| { | |
| setFlag(Flag::kHadFocusable, true); | |
| } | |
| } | |
| ~FocusNode(); | |
| // Transparent structural scope: unbacked (no Focusable) and never a focus | |
| // target itself. Traversal descends through it to reach focusable | |
| // descendants (see focusNodeTraversable); empty scopes contribute no | |
| // focus stops. Used by data-bound nested-artboard hosts and component | |
| // lists. | |
| static rcp<FocusNode> makeStructuralScope() | |
| { | |
| auto node = make_rcp<FocusNode>(); | |
| node->canFocus(false); | |
| node->canTraverse(false); | |
| node->canTouch(false); | |
| return node; | |
| } | |
| // === Focusable === | |
| Focusable* focusable() const { return m_focusable; } | |
| void setFocusable(Focusable* focusable) | |
| { | |
| const bool backingChanged = | |
| (m_focusable == nullptr) != (focusable == nullptr); | |
| m_focusable = focusable; | |
| if (focusable != nullptr) | |
| { | |
| setFlag(Flag::kHadFocusable, true); | |
| } | |
| if (backingChanged) | |
| { | |
| invalidateFocusableContent(); | |
| } | |
| } | |
| void clearFocusable() { setFocusable(nullptr); } | |
| /// True once this node has been backed by a Focusable. Never cleared, so a | |
| /// node whose Focusable was destroyed (~FocusData clears the backing) can | |
| /// be told apart from a node that legitimately never had one — a | |
| /// structural scope, or a target a host created through this API. The | |
| /// former is defunct and must stop being a focus stop; the latter is | |
| /// still a valid target. See focusNodeEligibleForFocus. | |
| bool hadFocusable() const { return m_flags & Flag::kHadFocusable; } | |
| // === Properties (bitfield-backed) === | |
| // Master switch: if false, node cannot receive focus at all | |
| bool canFocus() const { return m_flags & Flag::kCanFocus; } | |
| void canFocus(bool value) | |
| { | |
| if (canFocus() != value) | |
| { | |
| setFlag(Flag::kCanFocus, value); | |
| invalidateFocusableContent(); | |
| } | |
| } | |
| // Can receive focus via pointer/touch click | |
| bool canTouch() const { return m_flags & Flag::kCanTouch; } | |
| void canTouch(bool value) { setFlag(Flag::kCanTouch, value); } | |
| // Included in tab traversal | |
| bool canTraverse() const { return m_flags & Flag::kCanTraverse; } | |
| void canTraverse(bool value) { setFlag(Flag::kCanTraverse, value); } | |
| #ifdef WITH_RIVE_TOOLS | |
| // Only at edit time, we push down the collapse status of the nodes because | |
| // we can't get the FocusData node from the FocusNode | |
| bool isCollapsed() const { return m_isCollapsed; } | |
| void isCollapsed(bool value) { m_isCollapsed = value; } | |
| #endif | |
| // True if this node or any descendant currently has focus | |
| // (set by FocusManager during focus transitions) | |
| bool hasFocus() const { return m_flags & Flag::kHasFocus; } | |
| // Note: A node with canFocus=true but canTouch=false and canTraverse=false | |
| // can still be focused programmatically (via script) and receives text | |
| // input. | |
| // Scope status is implicit: a node with children is a scope. | |
| // EdgeBehavior only applies to scopes (nodes with children). | |
| EdgeBehavior edgeBehavior() const | |
| { | |
| return static_cast<EdgeBehavior>((m_flags >> edgeBehaviorShift) & | |
| edgeBehaviorMask); | |
| } | |
| void edgeBehavior(EdgeBehavior edge) | |
| { | |
| m_flags = (m_flags & ~(edgeBehaviorMask << edgeBehaviorShift)) | | |
| (static_cast<uint8_t>(edge) << edgeBehaviorShift); | |
| } | |
| int tabIndex() const { return m_tabIndex; } | |
| void tabIndex(int value) { m_tabIndex = static_cast<int16_t>(value); } | |
| // Debug name (not required to be unique) | |
| const std::string& name() const { return m_name; } | |
| void name(const std::string& value) { m_name = value; } | |
| // === World Bounds (for directional navigation) === | |
| // World bounds in root artboard space, used for spatial navigation. | |
| // Set during update cycle by owner (TextInput, Dart, etc). | |
| const AABB& worldBounds() const { return m_worldBounds; } | |
| void worldBounds(const AABB& bounds) { m_worldBounds = bounds; } | |
| // Check if bounds are available (not empty/invalid) | |
| bool hasWorldBounds() const { return !m_worldBounds.isEmptyOrNaN(); } | |
| // Clear bounds (mark as unavailable) | |
| void clearWorldBounds() { m_worldBounds = AABB(); } | |
| // === Hierarchy === | |
| FocusNode* parent() const { return m_parent; } | |
| const std::vector<rcp<FocusNode>>& children() const { return m_children; } | |
| FocusManager* manager() const { return m_manager; } | |
| 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) | |
| void removeFromParent(); | |
| // === Input Handling (delegates to Focusable) === | |
| bool keyInput(Key key, | |
| KeyModifiers modifiers, | |
| bool isPressed, | |
| bool isRepeat) | |
| { | |
| return m_focusable | |
| ? m_focusable->keyInput(key, modifiers, isPressed, isRepeat) | |
| : false; | |
| } | |
| bool textInput(const std::string& text) | |
| { | |
| return m_focusable ? m_focusable->textInput(text) : false; | |
| } | |
| bool gamepadDispatch( | |
| const ListenerInvocation& invocation, | |
| ScriptedDrawable** outDispatchedScriptedDrawable = nullptr) | |
| { | |
| return m_focusable | |
| ? m_focusable->gamepadDispatch(invocation, | |
| outDispatchedScriptedDrawable) | |
| : false; | |
| } | |
| void focused() | |
| { | |
| if (m_focusable) | |
| { | |
| m_focusable->focused(); | |
| } | |
| } | |
| void blurred() | |
| { | |
| if (m_focusable) | |
| { | |
| m_focusable->blurred(); | |
| } | |
| } | |
| private: | |
| friend class FocusManager; | |
| // Used by FocusManager to update focus state | |
| void setHasFocus(bool value) { setFlag(Flag::kHasFocus, value); } | |
| // Tell this node's manager (if any) that its cached | |
| // hasFocusableContent() answer may be stale. Out-of-line because | |
| // FocusManager is only forward-declared here. | |
| void invalidateFocusableContent(); | |
| enum Flag : uint8_t | |
| { | |
| kCanFocus = 1 << 0, // default: true | |
| kCanTouch = 1 << 1, // default: true | |
| kCanTraverse = 1 << 2, // default: true | |
| // bits 3-4: EdgeBehavior (2 bits) | |
| kHasFocus = 1 << 5, // true if this node or descendant has focus | |
| kHadFocusable = 1 << 6, // sticky: was ever backed by a Focusable | |
| }; | |
| static constexpr uint8_t edgeBehaviorShift = 3; | |
| static constexpr uint8_t edgeBehaviorMask = 0x3; | |
| static constexpr uint8_t defaultFlags = | |
| Flag::kCanFocus | Flag::kCanTouch | Flag::kCanTraverse; | |
| void setFlag(Flag flag, bool value) | |
| { | |
| if (value) | |
| { | |
| m_flags |= flag; | |
| } | |
| else | |
| { | |
| m_flags &= ~flag; | |
| } | |
| } | |
| Focusable* m_focusable = nullptr; | |
| FocusNode* m_parent = nullptr; | |
| FocusManager* m_manager = nullptr; | |
| std::vector<rcp<FocusNode>> m_children; | |
| std::string m_name; | |
| AABB m_worldBounds; // Default empty (invalid) - hasWorldBounds() returns | |
| // false | |
| uint8_t m_flags = defaultFlags; | |
| int16_t m_tabIndex = 0; | |
| #ifdef WITH_RIVE_TOOLS | |
| bool m_isCollapsed = false; | |
| #endif | |
| }; | |
| } // namespace rive | |
| #endif |