Files
XCEngine/tests/Input/test_xcui_input_dispatcher.cpp

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