223 lines
7.9 KiB
C++
223 lines
7.9 KiB
C++
|
|
#include <gtest/gtest.h>
|
||
|
|
|
||
|
|
#include <XCEngine/Input/InputTypes.h>
|
||
|
|
#include <XCEngine/UI/Input/UIFocusController.h>
|
||
|
|
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||
|
|
#include <XCEngine/UI/Input/UIInputRouter.h>
|
||
|
|
#include <XCEngine/UI/Input/UIShortcutRegistry.h>
|
||
|
|
|
||
|
|
#include <cstdint>
|
||
|
|
#include <string>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
using XCEngine::Input::KeyCode;
|
||
|
|
using XCEngine::UI::UIElementId;
|
||
|
|
using XCEngine::UI::UIFocusController;
|
||
|
|
using XCEngine::UI::UIInputDispatchDecision;
|
||
|
|
using XCEngine::UI::UIInputDispatcher;
|
||
|
|
using XCEngine::UI::UIInputEvent;
|
||
|
|
using XCEngine::UI::UIInputEventType;
|
||
|
|
using XCEngine::UI::UIInputPath;
|
||
|
|
using XCEngine::UI::UIInputRouteContext;
|
||
|
|
using XCEngine::UI::UIInputRouter;
|
||
|
|
using XCEngine::UI::UIInputRoutingPhase;
|
||
|
|
using XCEngine::UI::UIInputTargetKind;
|
||
|
|
using XCEngine::UI::UIPointerButton;
|
||
|
|
using XCEngine::UI::UIShortcutBinding;
|
||
|
|
using XCEngine::UI::UIShortcutContext;
|
||
|
|
using XCEngine::UI::UIShortcutRegistry;
|
||
|
|
using XCEngine::UI::UIShortcutScope;
|
||
|
|
|
||
|
|
std::string BuildTraceLabel(
|
||
|
|
UIElementId elementId,
|
||
|
|
UIInputRoutingPhase phase) {
|
||
|
|
char phaseChar = 'T';
|
||
|
|
switch (phase) {
|
||
|
|
case UIInputRoutingPhase::Capture:
|
||
|
|
phaseChar = 'C';
|
||
|
|
break;
|
||
|
|
case UIInputRoutingPhase::Bubble:
|
||
|
|
phaseChar = 'B';
|
||
|
|
break;
|
||
|
|
case UIInputRoutingPhase::Target:
|
||
|
|
default:
|
||
|
|
phaseChar = 'T';
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return std::to_string(elementId) + phaseChar;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
TEST(XCUIFocusControllerTest, SetFocusedPathTracksLostAndGainedSuffixes) {
|
||
|
|
UIFocusController controller = {};
|
||
|
|
EXPECT_FALSE(controller.HasFocus());
|
||
|
|
|
||
|
|
const auto initialChange = controller.SetFocusedPath({ 1u, 2u, 3u });
|
||
|
|
EXPECT_TRUE(initialChange.Changed());
|
||
|
|
EXPECT_EQ(initialChange.gainedPath.elements, (std::vector<UIElementId>{ 1u, 2u, 3u }));
|
||
|
|
EXPECT_TRUE(initialChange.lostPath.elements.empty());
|
||
|
|
EXPECT_TRUE(controller.HasFocus());
|
||
|
|
|
||
|
|
const auto change = controller.SetFocusedPath({ 1u, 4u });
|
||
|
|
EXPECT_TRUE(change.Changed());
|
||
|
|
EXPECT_EQ(change.previousPath.elements, (std::vector<UIElementId>{ 1u, 2u, 3u }));
|
||
|
|
EXPECT_EQ(change.currentPath.elements, (std::vector<UIElementId>{ 1u, 4u }));
|
||
|
|
EXPECT_EQ(change.lostPath.elements, (std::vector<UIElementId>{ 2u, 3u }));
|
||
|
|
EXPECT_EQ(change.gainedPath.elements, (std::vector<UIElementId>{ 4u }));
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(XCUIInputRouterTest, PointerCaptureOverridesHoveredPath) {
|
||
|
|
UIInputEvent event = {};
|
||
|
|
event.type = UIInputEventType::PointerMove;
|
||
|
|
|
||
|
|
UIInputRouteContext context = {};
|
||
|
|
context.hoveredPath = { 10u, 20u, 30u };
|
||
|
|
context.capturePath = { 90u, 100u };
|
||
|
|
|
||
|
|
const auto plan = UIInputRouter::BuildRoutingPlan(event, context);
|
||
|
|
|
||
|
|
EXPECT_EQ(plan.targetKind, UIInputTargetKind::Captured);
|
||
|
|
EXPECT_EQ(plan.targetPath.elements, (std::vector<UIElementId>{ 90u, 100u }));
|
||
|
|
ASSERT_EQ(plan.steps.size(), 3u);
|
||
|
|
EXPECT_EQ(plan.steps[0].elementId, 90u);
|
||
|
|
EXPECT_EQ(plan.steps[0].phase, UIInputRoutingPhase::Capture);
|
||
|
|
EXPECT_EQ(plan.steps[1].elementId, 100u);
|
||
|
|
EXPECT_EQ(plan.steps[1].phase, UIInputRoutingPhase::Target);
|
||
|
|
EXPECT_EQ(plan.steps[2].elementId, 90u);
|
||
|
|
EXPECT_EQ(plan.steps[2].phase, UIInputRoutingPhase::Bubble);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(XCUIInputRouterTest, KeyboardEventsRouteThroughFocusedPathInCaptureTargetBubbleOrder) {
|
||
|
|
UIInputEvent event = {};
|
||
|
|
event.type = UIInputEventType::KeyDown;
|
||
|
|
event.keyCode = static_cast<std::int32_t>(KeyCode::Enter);
|
||
|
|
|
||
|
|
UIInputRouteContext context = {};
|
||
|
|
context.focusedPath = { 1u, 2u, 3u };
|
||
|
|
|
||
|
|
std::vector<std::string> trace = {};
|
||
|
|
const auto result = UIInputRouter::Dispatch(
|
||
|
|
event,
|
||
|
|
context,
|
||
|
|
[&trace](const auto& request) {
|
||
|
|
trace.push_back(BuildTraceLabel(request.elementId, request.phase));
|
||
|
|
return UIInputDispatchDecision{};
|
||
|
|
});
|
||
|
|
|
||
|
|
EXPECT_FALSE(result.handled);
|
||
|
|
EXPECT_EQ(trace, (std::vector<std::string>{ "1C", "2C", "3T", "2B", "1B" }));
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(XCUIShortcutRegistryTest, MatchingPrefersMostSpecificScopeThenNewestBinding) {
|
||
|
|
UIShortcutRegistry registry = {};
|
||
|
|
|
||
|
|
UIShortcutBinding globalBinding = {};
|
||
|
|
globalBinding.scope = UIShortcutScope::Global;
|
||
|
|
globalBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::S);
|
||
|
|
globalBinding.chord.modifiers.control = true;
|
||
|
|
globalBinding.commandId = "save.global";
|
||
|
|
registry.RegisterBinding(globalBinding);
|
||
|
|
|
||
|
|
UIShortcutBinding panelBinding = {};
|
||
|
|
panelBinding.scope = UIShortcutScope::Panel;
|
||
|
|
panelBinding.ownerId = 20u;
|
||
|
|
panelBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::S);
|
||
|
|
panelBinding.chord.modifiers.control = true;
|
||
|
|
panelBinding.commandId = "save.panel";
|
||
|
|
registry.RegisterBinding(panelBinding);
|
||
|
|
|
||
|
|
UIShortcutBinding widgetBinding = {};
|
||
|
|
widgetBinding.scope = UIShortcutScope::Widget;
|
||
|
|
widgetBinding.ownerId = 30u;
|
||
|
|
widgetBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::S);
|
||
|
|
widgetBinding.chord.modifiers.control = true;
|
||
|
|
widgetBinding.commandId = "save.widget";
|
||
|
|
registry.RegisterBinding(widgetBinding);
|
||
|
|
|
||
|
|
UIInputEvent event = {};
|
||
|
|
event.type = UIInputEventType::KeyDown;
|
||
|
|
event.keyCode = static_cast<std::int32_t>(KeyCode::S);
|
||
|
|
event.modifiers.control = true;
|
||
|
|
|
||
|
|
UIShortcutContext context = {};
|
||
|
|
context.focusedPath = { 10u, 20u, 30u };
|
||
|
|
EXPECT_EQ(registry.Match(event, context).binding.commandId, "save.widget");
|
||
|
|
|
||
|
|
context.focusedPath = { 10u, 20u };
|
||
|
|
EXPECT_EQ(registry.Match(event, context).binding.commandId, "save.panel");
|
||
|
|
|
||
|
|
context.focusedPath.Clear();
|
||
|
|
EXPECT_EQ(registry.Match(event, context).binding.commandId, "save.global");
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(XCUIInputDispatcherTest, PointerDownTransfersFocusAndMaintainsActivePathUntilPointerUp) {
|
||
|
|
UIInputDispatcher dispatcher;
|
||
|
|
|
||
|
|
UIInputEvent pointerDown = {};
|
||
|
|
pointerDown.type = UIInputEventType::PointerButtonDown;
|
||
|
|
pointerDown.pointerButton = UIPointerButton::Left;
|
||
|
|
|
||
|
|
const auto downSummary = dispatcher.Dispatch(
|
||
|
|
pointerDown,
|
||
|
|
UIInputPath{ 100u, 200u },
|
||
|
|
[](const auto&) {
|
||
|
|
return UIInputDispatchDecision{};
|
||
|
|
});
|
||
|
|
|
||
|
|
EXPECT_TRUE(downSummary.focusChange.Changed());
|
||
|
|
EXPECT_EQ(dispatcher.GetFocusController().GetFocusedPath().elements, (std::vector<UIElementId>{ 100u, 200u }));
|
||
|
|
EXPECT_EQ(dispatcher.GetFocusController().GetActivePath().elements, (std::vector<UIElementId>{ 100u, 200u }));
|
||
|
|
EXPECT_EQ(downSummary.routing.plan.targetKind, UIInputTargetKind::Hovered);
|
||
|
|
|
||
|
|
UIInputEvent pointerUp = {};
|
||
|
|
pointerUp.type = UIInputEventType::PointerButtonUp;
|
||
|
|
pointerUp.pointerButton = UIPointerButton::Left;
|
||
|
|
dispatcher.Dispatch(
|
||
|
|
pointerUp,
|
||
|
|
UIInputPath{ 100u, 200u },
|
||
|
|
[](const auto&) {
|
||
|
|
return UIInputDispatchDecision{};
|
||
|
|
});
|
||
|
|
|
||
|
|
EXPECT_TRUE(dispatcher.GetFocusController().GetActivePath().Empty());
|
||
|
|
EXPECT_EQ(dispatcher.GetFocusController().GetFocusedPath().elements, (std::vector<UIElementId>{ 100u, 200u }));
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(XCUIInputDispatcherTest, ShortcutMatchConsumesKeyboardDispatchBeforeRouting) {
|
||
|
|
UIInputDispatcher dispatcher;
|
||
|
|
dispatcher.GetFocusController().SetFocusedPath({ 1u, 2u, 3u });
|
||
|
|
|
||
|
|
UIShortcutBinding binding = {};
|
||
|
|
binding.scope = UIShortcutScope::Widget;
|
||
|
|
binding.ownerId = 3u;
|
||
|
|
binding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||
|
|
binding.chord.modifiers.control = true;
|
||
|
|
binding.commandId = "palette.open";
|
||
|
|
dispatcher.GetShortcutRegistry().RegisterBinding(binding);
|
||
|
|
|
||
|
|
UIInputEvent event = {};
|
||
|
|
event.type = UIInputEventType::KeyDown;
|
||
|
|
event.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||
|
|
event.modifiers.control = true;
|
||
|
|
|
||
|
|
bool handlerCalled = false;
|
||
|
|
const auto summary = dispatcher.Dispatch(
|
||
|
|
event,
|
||
|
|
{},
|
||
|
|
[&handlerCalled](const auto&) {
|
||
|
|
handlerCalled = true;
|
||
|
|
UIInputDispatchDecision decision = {};
|
||
|
|
decision.handled = true;
|
||
|
|
return decision;
|
||
|
|
});
|
||
|
|
|
||
|
|
EXPECT_TRUE(summary.shortcutHandled);
|
||
|
|
EXPECT_EQ(summary.commandId, "palette.open");
|
||
|
|
EXPECT_FALSE(handlerCalled);
|
||
|
|
EXPECT_FALSE(summary.routing.plan.HasTargetPath());
|
||
|
|
}
|