#include #include #include #include #include #include #include #include #include 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{ 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{ 1u, 2u, 3u })); EXPECT_EQ(change.currentPath.elements, (std::vector{ 1u, 4u })); EXPECT_EQ(change.lostPath.elements, (std::vector{ 2u, 3u })); EXPECT_EQ(change.gainedPath.elements, (std::vector{ 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{ 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(KeyCode::Enter); UIInputRouteContext context = {}; context.focusedPath = { 1u, 2u, 3u }; std::vector 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{ "1C", "2C", "3T", "2B", "1B" })); } TEST(XCUIShortcutRegistryTest, MatchingPrefersMostSpecificScopeThenNewestBinding) { UIShortcutRegistry registry = {}; UIShortcutBinding globalBinding = {}; globalBinding.scope = UIShortcutScope::Global; globalBinding.chord.keyCode = static_cast(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(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(KeyCode::S); widgetBinding.chord.modifiers.control = true; widgetBinding.commandId = "save.widget"; registry.RegisterBinding(widgetBinding); UIInputEvent event = {}; event.type = UIInputEventType::KeyDown; event.keyCode = static_cast(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{ 100u, 200u })); EXPECT_EQ(dispatcher.GetFocusController().GetActivePath().elements, (std::vector{ 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{ 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(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(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()); }