diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index c7f99e0..becc2bb 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -486,6 +486,17 @@ namespace lvh::detail { pointer_info.ptPixelLocation = pointer_location(viewport, x, y); } + /** + * @brief Identify keys requiring the E0 prefix during Win32 and HID translation. + * + * Preserve the established navigation and modifier handling, and ask Windows + * for the prefix of other keys so media, volume, and browser keys cannot be + * mistaken for ordinary keys sharing the same scan code. The E1 prefix used + * by Pause does not represent KEYEVENTF_EXTENDEDKEY. + * + * @param key_code Windows virtual-key code after any consumer remapping. + * @return Whether the key requires E0 extended-key handling. + */ bool extended_key(KeyboardKeyCode key_code) { switch (key_code) { case VK_LWIN: @@ -506,7 +517,7 @@ namespace lvh::detail { case VK_APPS: return true; default: - return false; + return (::MapVirtualKeyW(key_code, MAPVK_VK_TO_VSC_EX) & 0xFF00U) == 0xE000U; } } diff --git a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp index 85701cd..85bcbf9 100644 --- a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp @@ -7,6 +7,7 @@ // standard includes #include #include +#include #include #include @@ -207,6 +208,16 @@ namespace lvh::detail::test { WindowsSendInputRecord keypad_enter_input; }; + /** + * @brief Captured delivery of a sequence of Windows keyboard events. + */ + struct WindowsKeyboardTranslationResult { + OperationStatus create_status; ///< Keyboard creation result. + std::vector submit_statuses; ///< Result of each submitted event. + std::vector sent_inputs; ///< Intercepted Win32 input events. + std::vector> reports; ///< Intercepted HID keyboard reports. + }; + struct WindowsMouseSendInputResult { OperationStatus relative_status; OperationStatus absolute_status; @@ -278,6 +289,18 @@ namespace lvh::detail::test { WindowsOverlappedIoResult windows_backend_overlapped_device_io(); WindowsBackendSendInputResult windows_backend_send_input_devices(); + /** + * @brief Submit keyboard events while intercepting Win32 input and HID reports. + * + * @param events Keyboard events to translate without delivering input to the host. + * @param use_hid Whether to use the fake HID control channel instead of the Win32 fallback. + * @return Submission results and captured input. + */ + WindowsKeyboardTranslationResult windows_backend_translate_keyboard_events( + std::span events, + bool use_hid + ); + /** * @brief Scale a source coordinate through a target viewport into a virtual desktop axis. * diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp index 892dbf8..7db012a 100644 --- a/tests/fixtures/windows_backend_test_hooks.cpp +++ b/tests/fixtures/windows_backend_test_hooks.cpp @@ -1135,6 +1135,32 @@ namespace lvh::detail { return result; } + WindowsKeyboardTranslationResult windows_backend_translate_keyboard_events( + std::span events, + bool use_hid + ) { + WindowsKeyboardTranslationResult result; + FakeSendInputState send_input_state; + ScopedFakeSendInput fake_send_input {send_input_state}; + auto command_state = std::make_shared(); + auto backend = use_hid ? + make_fake_windows_backend(command_state, std::make_shared()) : + std::make_unique(nullptr, nullptr); + + CreateKeyboardOptions options; + options.profile = profiles::keyboard(); + auto created = backend->create_keyboard(1U, options); + result.create_status = created.status; + if (created) { + for (const auto &event : events) { + result.submit_statuses.push_back(created.keyboard->submit(event)); + } + } + result.sent_inputs = std::move(send_input_state.sent_inputs); + result.reports = command_state->submit_reports(); + return result; + } + WindowsBackendSendInputResult windows_backend_send_input_devices() { using enum MouseEventKind; diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp index 2af5454..1610e4f 100644 --- a/tests/unit/test_windows_backend.cpp +++ b/tests/unit/test_windows_backend.cpp @@ -24,18 +24,22 @@ #endif #endif -// local includes +// test includes #include "fixtures/fixtures.hpp" #include "fixtures/windows_backend_test_hooks.hpp" -#include "lvh_windows_protocol.h" - -// lib includes -#include // standard includes #include +#include +#include #include +// lib includes +#include + +// local includes +#include "lvh_windows_protocol.h" + #ifndef NOMINMAX #define NOMINMAX #endif @@ -330,6 +334,127 @@ TEST_F(WindowsBackendTest, OverlappedDeviceIoUsesAnEventAndWaitsForPendingComple EXPECT_EQ(result.completion_failure_status.code(), lvh::ErrorCode::backend_failure); } +TEST_F(WindowsBackendTest, PreservesExtendedMediaAndBrowserKeysInSendInput) { + // These scan codes overlap letters unless KEYEVENTF_EXTENDEDKEY accompanies them. + const auto keys = std::to_array>({ + {VK_BROWSER_BACK, 0x6AU}, + {VK_BROWSER_FORWARD, 0x69U}, + {VK_BROWSER_REFRESH, 0x67U}, + {VK_BROWSER_STOP, 0x68U}, + {VK_BROWSER_SEARCH, 0x65U}, + {VK_BROWSER_FAVORITES, 0x66U}, + {VK_BROWSER_HOME, 0x32U}, + {VK_VOLUME_MUTE, 0x20U}, + {VK_VOLUME_DOWN, 0x2EU}, + {VK_VOLUME_UP, 0x30U}, + {VK_MEDIA_NEXT_TRACK, 0x19U}, + {VK_MEDIA_PREV_TRACK, 0x10U}, + {VK_MEDIA_STOP, 0x24U}, + {VK_MEDIA_PLAY_PAUSE, 0x22U}, + {VK_LAUNCH_MAIL, 0x6CU}, + {VK_LAUNCH_MEDIA_SELECT, 0x6DU}, + {VK_LAUNCH_APP1, 0x6BU}, + {VK_LAUNCH_APP2, 0x21U}, + }); + for (const auto &[key, scan_code] : keys) { + for (const bool normalized : {false, true}) { + SCOPED_TRACE(key); + SCOPED_TRACE(normalized); + const std::array events { + lvh::KeyboardEvent {.key_code = key, .pressed = true, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true}, + lvh::KeyboardEvent {.key_code = key, .pressed = false, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true}, + }; + const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, false); + expect_ok(result.create_status); + ASSERT_EQ(result.submit_statuses.size(), 2U); + ASSERT_EQ(result.sent_inputs.size(), 2U); + for (std::size_t i = 0; i < events.size(); ++i) { + expect_ok(result.submit_statuses[i]); + const auto &input = result.sent_inputs[i]; + EXPECT_EQ(input.type, INPUT_KEYBOARD); + EXPECT_EQ(input.virtual_key, 0U); + EXPECT_EQ(input.scan_code, scan_code); + EXPECT_EQ(input.key_flags, KEYEVENTF_SCANCODE | KEYEVENTF_EXTENDEDKEY | (i == 0U ? 0U : KEYEVENTF_KEYUP)); + EXPECT_EQ(::MapVirtualKeyW(0xE000U | input.scan_code, MAPVK_VSC_TO_VK_EX), key); + } + } + } +} + +TEST_F(WindowsBackendTest, KeepsOrdinaryKeysAndE1PauseDistinctFromE0Keys) { + for (const auto key : std::to_array({'B', 'C', VK_RETURN, VK_PAUSE, VK_SNAPSHOT, VK_NUMLOCK})) { + SCOPED_TRACE(key); + const std::array events { + lvh::KeyboardEvent {.key_code = static_cast(key), .pressed = true, .uses_normalized_key_code = true}, + lvh::KeyboardEvent {.key_code = static_cast(key), .pressed = false, .prefer_native_scan_code = true}, + }; + const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, false); + expect_ok(result.create_status); + ASSERT_EQ(result.submit_statuses.size(), 2U); + ASSERT_EQ(result.sent_inputs.size(), 2U); + for (std::size_t i = 0; i < events.size(); ++i) { + expect_ok(result.submit_statuses[i]); + EXPECT_EQ(result.sent_inputs[i].key_flags & KEYEVENTF_EXTENDEDKEY, 0U); + } + } +} + +TEST_F(WindowsBackendTest, PreservesVolumeKeysInHidKeyboardReports) { + const auto keys = std::to_array>({ + {VK_VOLUME_MUTE, 0x7FU}, + {VK_VOLUME_DOWN, 0x81U}, + {VK_VOLUME_UP, 0x80U}, + {'C', 0x06U}, + {'B', 0x05U}, + }); + for (const auto &[key, usage] : keys) { + for (const bool normalized : {false, true}) { + SCOPED_TRACE(key); + SCOPED_TRACE(normalized); + const std::array events { + lvh::KeyboardEvent {.key_code = key, .pressed = true, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true}, + lvh::KeyboardEvent {.key_code = key, .pressed = false, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true}, + }; + const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, true); + expect_ok(result.create_status); + ASSERT_EQ(result.submit_statuses.size(), 2U); + for (const auto &status : result.submit_statuses) { + expect_ok(status); + } + EXPECT_TRUE(result.sent_inputs.empty()); + std::vector pressed_report(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 0U); + pressed_report[2U] = usage; + const std::vector released_report(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 0U); + EXPECT_EQ(result.reports, (std::vector> {pressed_report, released_report})); + } + } +} + +TEST_F(WindowsBackendTest, PreservesConsumerKeysWhenHidKeyboardFallsBackToSendInput) { + for (const auto key : {VK_BROWSER_HOME, VK_MEDIA_NEXT_TRACK, VK_LAUNCH_APP2}) { + for (const bool normalized : {false, true}) { + SCOPED_TRACE(key); + SCOPED_TRACE(normalized); + const std::array events { + lvh::KeyboardEvent {.key_code = static_cast(key), .pressed = true, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true}, + lvh::KeyboardEvent {.key_code = static_cast(key), .pressed = false, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true}, + }; + const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, true); + expect_ok(result.create_status); + ASSERT_EQ(result.submit_statuses.size(), 2U); + ASSERT_EQ(result.sent_inputs.size(), 2U); + EXPECT_TRUE(result.reports.empty()); + for (std::size_t i = 0; i < events.size(); ++i) { + expect_ok(result.submit_statuses[i]); + const auto &input = result.sent_inputs[i]; + EXPECT_EQ(input.virtual_key, 0U); + EXPECT_EQ(input.key_flags, KEYEVENTF_SCANCODE | KEYEVENTF_EXTENDEDKEY | (i == 0U ? 0U : KEYEVENTF_KEYUP)); + EXPECT_EQ(::MapVirtualKeyW(0xE000U | input.scan_code, MAPVK_VSC_TO_VK_EX), static_cast(key)); + } + } + } +} + TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsupportedProfiles) { const auto result = lvh::detail::test::windows_backend_send_input_devices();