blob: 44351811d918871be2832baafe9341ed3a62f7d2 [file] [log] [blame]
#include <catch.hpp>
#include "rive/input/focus_node.hpp"
#include "rive/input/focus_manager.hpp"
namespace rive
{
// Mock Focusable for testing
class MockFocusable : public Focusable
{
public:
int keyInputCount = 0;
int textInputCount = 0;
int focusedCount = 0;
int blurredCount = 0;
std::string lastText;
Key lastKey = Key::a;
bool returnValue = false;
bool keyInput(Key key,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat) override
{
keyInputCount++;
lastKey = key;
return returnValue;
}
bool textInput(const std::string& text) override
{
textInputCount++;
lastText = text;
return returnValue;
}
void focused() override { focusedCount++; }
void blurred() override { blurredCount++; }
};
// =============================================================================
// FocusNode Tests
// =============================================================================
TEST_CASE("FocusNode default properties", "[FocusNode]")
{
auto node = make_rcp<FocusNode>();
CHECK(node->canFocus() == true);
CHECK(node->canTouch() == true);
CHECK(node->canTraverse() == true);
CHECK(node->tabIndex() == 0);
CHECK(node->edgeBehavior() == EdgeBehavior::parentScope);
CHECK(node->focusable() == nullptr);
CHECK(node->parent() == nullptr);
CHECK(node->children().empty());
CHECK(node->isScope() == false);
CHECK(node->hasFocus() == false);
CHECK(node->manager() == nullptr);
}
TEST_CASE("FocusNode property setters", "[FocusNode]")
{
auto node = make_rcp<FocusNode>();
node->canFocus(false);
CHECK(node->canFocus() == false);
node->canTouch(false);
CHECK(node->canTouch() == false);
node->canTraverse(false);
CHECK(node->canTraverse() == false);
node->tabIndex(42);
CHECK(node->tabIndex() == 42);
node->edgeBehavior(EdgeBehavior::closedLoop);
CHECK(node->edgeBehavior() == EdgeBehavior::closedLoop);
node->edgeBehavior(EdgeBehavior::stop);
CHECK(node->edgeBehavior() == EdgeBehavior::stop);
}
TEST_CASE("FocusNode with Focusable", "[FocusNode]")
{
MockFocusable focusable;
auto node = make_rcp<FocusNode>(&focusable);
CHECK(node->focusable() == &focusable);
// Test input delegation
node->keyInput(Key::a, KeyModifiers::none, true, false);
CHECK(focusable.keyInputCount == 1);
CHECK(focusable.lastKey == Key::a);
node->textInput("hello");
CHECK(focusable.textInputCount == 1);
CHECK(focusable.lastText == "hello");
// Test lifecycle delegation
node->focused();
CHECK(focusable.focusedCount == 1);
node->blurred();
CHECK(focusable.blurredCount == 1);
}
TEST_CASE("FocusNode without Focusable doesn't crash", "[FocusNode]")
{
auto node = make_rcp<FocusNode>();
// These should not crash
CHECK(node->keyInput(Key::a, KeyModifiers::none, true, false) == false);
CHECK(node->textInput("hello") == false);
node->focused();
node->blurred();
}
TEST_CASE("FocusNode setFocusable/clearFocusable", "[FocusNode]")
{
MockFocusable focusable;
auto node = make_rcp<FocusNode>();
CHECK(node->focusable() == nullptr);
node->setFocusable(&focusable);
CHECK(node->focusable() == &focusable);
node->clearFocusable();
CHECK(node->focusable() == nullptr);
}
TEST_CASE("FocusNode hierarchy", "[FocusNode]")
{
auto parent = make_rcp<FocusNode>();
auto child1 = make_rcp<FocusNode>();
auto child2 = make_rcp<FocusNode>();
parent->addChild(child1);
parent->addChild(child2);
CHECK(child1->parent() == parent.get());
CHECK(child2->parent() == parent.get());
CHECK(parent->children().size() == 2);
CHECK(parent->isScope() == true);
parent->removeChild(child1);
CHECK(child1->parent() == nullptr);
CHECK(parent->children().size() == 1);
}
// =============================================================================
// FocusManager Tests
// =============================================================================
TEST_CASE("FocusManager basic focus operations", "[FocusManager]")
{
FocusManager manager;
MockFocusable focusable;
auto node = make_rcp<FocusNode>(&focusable);
CHECK(manager.primaryFocus() == nullptr);
manager.addChild(nullptr, node);
manager.setFocus(node);
CHECK(manager.primaryFocus() == node);
CHECK(manager.hasFocus(node) == true);
CHECK(manager.hasPrimaryFocus(node) == true);
CHECK(focusable.focusedCount == 1);
manager.clearFocus();
CHECK(manager.primaryFocus() == nullptr);
CHECK(focusable.blurredCount == 1);
}
TEST_CASE("FocusManager focus change notifications", "[FocusManager]")
{
FocusManager manager;
MockFocusable focusable1, focusable2;
auto node1 = make_rcp<FocusNode>(&focusable1);
auto node2 = make_rcp<FocusNode>(&focusable2);
manager.addChild(nullptr, node1);
manager.addChild(nullptr, node2);
manager.setFocus(node1);
CHECK(focusable1.focusedCount == 1);
CHECK(focusable1.blurredCount == 0);
manager.setFocus(node2);
CHECK(focusable1.blurredCount == 1);
CHECK(focusable2.focusedCount == 1);
}
TEST_CASE("FocusManager respects canFocus", "[FocusManager]")
{
FocusManager manager;
auto node = make_rcp<FocusNode>();
node->canFocus(false);
manager.addChild(nullptr, node);
manager.setFocus(node);
CHECK(manager.primaryFocus() == nullptr);
}
TEST_CASE("FocusManager hierarchy", "[FocusManager]")
{
FocusManager manager;
auto parent = make_rcp<FocusNode>();
auto child1 = make_rcp<FocusNode>();
auto child2 = make_rcp<FocusNode>();
manager.addChild(nullptr, parent);
manager.addChild(parent, child1);
manager.addChild(parent, child2);
CHECK(parent->parent() == nullptr);
CHECK(child1->parent() == parent.get());
CHECK(child2->parent() == parent.get());
CHECK(parent->isScope() == true);
CHECK(child1->isScope() == false);
const auto& children = parent->children();
CHECK(children.size() == 2);
// Manager reference is set on all nodes
CHECK(parent->manager() == &manager);
CHECK(child1->manager() == &manager);
CHECK(child2->manager() == &manager);
}
TEST_CASE("FocusManager hasFocus with descendants", "[FocusManager]")
{
FocusManager manager;
auto parent = make_rcp<FocusNode>();
auto child = make_rcp<FocusNode>();
manager.addChild(nullptr, parent);
manager.addChild(parent, child);
manager.setFocus(child);
// Manager queries should work
CHECK(manager.hasFocus(parent) == true);
CHECK(manager.hasPrimaryFocus(parent) == false);
CHECK(manager.hasFocus(child) == true);
CHECK(manager.hasPrimaryFocus(child) == true);
// Node's hasFocus flag should be set for focused node and ancestors
CHECK(parent->hasFocus() == true);
CHECK(child->hasFocus() == true);
}
TEST_CASE("FocusManager removeChild clears focus", "[FocusManager]")
{
FocusManager manager;
MockFocusable focusable;
auto node = make_rcp<FocusNode>(&focusable);
manager.addChild(nullptr, node);
manager.setFocus(node);
CHECK(manager.primaryFocus() == node);
manager.removeChild(node);
CHECK(manager.primaryFocus() == nullptr);
CHECK(focusable.blurredCount == 1);
}
TEST_CASE("FocusManager input routing", "[FocusManager]")
{
FocusManager manager;
MockFocusable focusable;
focusable.returnValue = true;
auto node = make_rcp<FocusNode>(&focusable);
manager.addChild(nullptr, node);
// No focus, input not handled
CHECK(manager.keyInput(Key::a, KeyModifiers::none, true, false) == false);
CHECK(manager.textInput("hello") == false);
manager.setFocus(node);
// With focus, input is routed
CHECK(manager.keyInput(Key::b, KeyModifiers::none, true, false) == true);
CHECK(focusable.keyInputCount == 1);
CHECK(focusable.lastKey == Key::b);
CHECK(manager.textInput("world") == true);
CHECK(focusable.textInputCount == 1);
CHECK(focusable.lastText == "world");
}
TEST_CASE("FocusManager traversal basic", "[FocusManager]")
{
FocusManager manager;
MockFocusable f1, f2, f3;
auto node1 = make_rcp<FocusNode>(&f1);
auto node2 = make_rcp<FocusNode>(&f2);
auto node3 = make_rcp<FocusNode>(&f3);
manager.addChild(nullptr, node1);
manager.addChild(nullptr, node2);
manager.addChild(nullptr, node3);
// Focus first node
manager.setFocus(node1);
CHECK(manager.primaryFocus() == node1);
// Navigate forward
manager.focusNext();
CHECK(manager.primaryFocus() == node2);
manager.focusNext();
CHECK(manager.primaryFocus() == node3);
// Navigate backward
manager.focusPrevious();
CHECK(manager.primaryFocus() == node2);
}
TEST_CASE("FocusManager traversal with tabIndex", "[FocusManager]")
{
FocusManager manager;
auto node1 = make_rcp<FocusNode>();
auto node2 = make_rcp<FocusNode>();
auto node3 = make_rcp<FocusNode>();
node1->tabIndex(3);
node2->tabIndex(1);
node3->tabIndex(2);
manager.addChild(nullptr, node1);
manager.addChild(nullptr, node2);
manager.addChild(nullptr, node3);
// Start with no focus, focusNext should pick first by tabIndex
manager.focusNext();
CHECK(manager.primaryFocus() == node2); // tabIndex 1
manager.focusNext();
CHECK(manager.primaryFocus() == node3); // tabIndex 2
manager.focusNext();
CHECK(manager.primaryFocus() == node1); // tabIndex 3
}
TEST_CASE("FocusManager traversal skips non-traversable", "[FocusManager]")
{
FocusManager manager;
auto node1 = make_rcp<FocusNode>();
auto node2 = make_rcp<FocusNode>();
auto node3 = make_rcp<FocusNode>();
node2->canTraverse(false);
manager.addChild(nullptr, node1);
manager.addChild(nullptr, node2);
manager.addChild(nullptr, node3);
manager.setFocus(node1);
manager.focusNext();
// Should skip node2 and go to node3
CHECK(manager.primaryFocus() == node3);
}
TEST_CASE("FocusManager edge behavior closedLoop", "[FocusManager]")
{
FocusManager manager;
auto scope = make_rcp<FocusNode>();
auto node1 = make_rcp<FocusNode>();
auto node2 = make_rcp<FocusNode>();
scope->edgeBehavior(EdgeBehavior::closedLoop);
manager.addChild(nullptr, scope);
manager.addChild(scope, node1);
manager.addChild(scope, node2);
manager.setFocus(node2);
manager.focusNext();
// Should wrap to first
CHECK(manager.primaryFocus() == node1);
}
TEST_CASE("FocusManager edge behavior stop", "[FocusManager]")
{
FocusManager manager;
auto scope = make_rcp<FocusNode>();
auto node1 = make_rcp<FocusNode>();
auto node2 = make_rcp<FocusNode>();
scope->edgeBehavior(EdgeBehavior::stop);
manager.addChild(nullptr, scope);
manager.addChild(scope, node1);
manager.addChild(scope, node2);
manager.setFocus(node2);
manager.focusNext();
// Should stay on node2
CHECK(manager.primaryFocus() == node2);
}
TEST_CASE("FocusManager ancestor notification on focus", "[FocusManager]")
{
FocusManager manager;
MockFocusable grandparentFocusable, parentFocusable, childFocusable;
auto grandparent = make_rcp<FocusNode>(&grandparentFocusable);
auto parent = make_rcp<FocusNode>(&parentFocusable);
auto child = make_rcp<FocusNode>(&childFocusable);
manager.addChild(nullptr, grandparent);
manager.addChild(grandparent, parent);
manager.addChild(parent, child);
// Focus the leaf node
manager.setFocus(child);
// All ancestors should have received focused() callback
CHECK(childFocusable.focusedCount == 1);
CHECK(parentFocusable.focusedCount == 1);
CHECK(grandparentFocusable.focusedCount == 1);
// All nodes in the chain should have hasFocus flag
CHECK(child->hasFocus() == true);
CHECK(parent->hasFocus() == true);
CHECK(grandparent->hasFocus() == true);
}
TEST_CASE("FocusManager common ancestor optimization", "[FocusManager]")
{
FocusManager manager;
MockFocusable parentFocusable, child1Focusable, child2Focusable;
auto parent = make_rcp<FocusNode>(&parentFocusable);
auto child1 = make_rcp<FocusNode>(&child1Focusable);
auto child2 = make_rcp<FocusNode>(&child2Focusable);
manager.addChild(nullptr, parent);
manager.addChild(parent, child1);
manager.addChild(parent, child2);
// Focus first child
manager.setFocus(child1);
CHECK(parentFocusable.focusedCount == 1);
CHECK(child1Focusable.focusedCount == 1);
// Move focus to sibling - parent should NOT get re-notified
manager.setFocus(child2);
CHECK(child1Focusable.blurredCount == 1);
CHECK(child2Focusable.focusedCount == 1);
// Parent should not be blurred or re-focused
CHECK(parentFocusable.focusedCount == 1); // Still 1, not 2
CHECK(parentFocusable.blurredCount == 0);
// Parent still has focus (descendant focused)
CHECK(parent->hasFocus() == true);
}
TEST_CASE("FocusManager traversal focuses leaves only", "[FocusManager]")
{
FocusManager manager;
MockFocusable scopeFocusable, leaf1Focusable, leaf2Focusable;
auto scope = make_rcp<FocusNode>(&scopeFocusable);
auto leaf1 = make_rcp<FocusNode>(&leaf1Focusable);
auto leaf2 = make_rcp<FocusNode>(&leaf2Focusable);
manager.addChild(nullptr, scope);
manager.addChild(scope, leaf1);
manager.addChild(scope, leaf2);
// Start with no focus, focusNext should focus first leaf, not scope
manager.focusNext();
CHECK(manager.primaryFocus() == leaf1);
CHECK(manager.hasPrimaryFocus(scope) == false);
CHECK(scope->hasFocus() == true); // But scope has descendant focus
manager.focusNext();
CHECK(manager.primaryFocus() == leaf2);
}
TEST_CASE("FocusManager nested scopes focus deepest leaf", "[FocusManager]")
{
FocusManager manager;
auto scope1 = make_rcp<FocusNode>();
auto scope2 = make_rcp<FocusNode>();
auto leaf = make_rcp<FocusNode>();
manager.addChild(nullptr, scope1);
manager.addChild(scope1, scope2);
manager.addChild(scope2, leaf);
// Navigate should go directly to the deepest leaf
manager.focusNext();
CHECK(manager.primaryFocus() == leaf);
CHECK(scope1->hasFocus() == true);
CHECK(scope2->hasFocus() == true);
}
TEST_CASE("FocusManager edge behavior parentScope exits to parent",
"[FocusManager]")
{
FocusManager manager;
auto root = make_rcp<FocusNode>();
auto scope = make_rcp<FocusNode>();
auto inner1 = make_rcp<FocusNode>();
auto inner2 = make_rcp<FocusNode>();
auto outer = make_rcp<FocusNode>();
scope->edgeBehavior(EdgeBehavior::parentScope);
manager.addChild(nullptr, root);
manager.addChild(root, scope);
manager.addChild(scope, inner1);
manager.addChild(scope, inner2);
manager.addChild(root, outer);
// Focus last node in scope
manager.setFocus(inner2);
CHECK(manager.primaryFocus() == inner2);
// Navigate forward should exit scope and go to outer
manager.focusNext();
CHECK(manager.primaryFocus() == outer);
}
TEST_CASE("FocusManager clearFocus clears hasFocus flag chain",
"[FocusManager]")
{
FocusManager manager;
MockFocusable parentFocusable, childFocusable;
auto parent = make_rcp<FocusNode>(&parentFocusable);
auto child = make_rcp<FocusNode>(&childFocusable);
manager.addChild(nullptr, parent);
manager.addChild(parent, child);
manager.setFocus(child);
CHECK(parent->hasFocus() == true);
CHECK(child->hasFocus() == true);
manager.clearFocus();
// Both should be cleared
CHECK(parent->hasFocus() == false);
CHECK(child->hasFocus() == false);
// Both should have received blurred callback
CHECK(parentFocusable.blurredCount == 1);
CHECK(childFocusable.blurredCount == 1);
}
TEST_CASE("FocusManager removeChild clears manager reference", "[FocusManager]")
{
FocusManager manager;
auto node = make_rcp<FocusNode>();
manager.addChild(nullptr, node);
CHECK(node->manager() == &manager);
manager.removeChild(node);
CHECK(node->manager() == nullptr);
}
TEST_CASE("FocusManager traversal backward from first leaf exits scope",
"[FocusManager]")
{
FocusManager manager;
auto root = make_rcp<FocusNode>();
auto before = make_rcp<FocusNode>();
auto scope = make_rcp<FocusNode>();
auto inner = make_rcp<FocusNode>();
scope->edgeBehavior(EdgeBehavior::parentScope);
manager.addChild(nullptr, root);
manager.addChild(root, before);
manager.addChild(root, scope);
manager.addChild(scope, inner);
// Focus the inner node
manager.setFocus(inner);
// Navigate backward should exit scope and go to before
manager.focusPrevious();
CHECK(manager.primaryFocus() == before);
}
TEST_CASE("FocusManager closedLoop wraps backward", "[FocusManager]")
{
FocusManager manager;
auto scope = make_rcp<FocusNode>();
auto node1 = make_rcp<FocusNode>();
auto node2 = make_rcp<FocusNode>();
scope->edgeBehavior(EdgeBehavior::closedLoop);
manager.addChild(nullptr, scope);
manager.addChild(scope, node1);
manager.addChild(scope, node2);
manager.setFocus(node1);
manager.focusPrevious();
// Should wrap to last
CHECK(manager.primaryFocus() == node2);
}
TEST_CASE("FocusManager stop prevents backward traversal", "[FocusManager]")
{
FocusManager manager;
auto scope = make_rcp<FocusNode>();
auto node1 = make_rcp<FocusNode>();
auto node2 = make_rcp<FocusNode>();
scope->edgeBehavior(EdgeBehavior::stop);
manager.addChild(nullptr, scope);
manager.addChild(scope, node1);
manager.addChild(scope, node2);
manager.setFocus(node1);
manager.focusPrevious();
// Should stay on node1
CHECK(manager.primaryFocus() == node1);
}
} // namespace rive