diff --git a/src/include/libvirtualhid/types.hpp b/src/include/libvirtualhid/types.hpp index 260fca57..282cf164 100644 --- a/src/include/libvirtualhid/types.hpp +++ b/src/include/libvirtualhid/types.hpp @@ -729,11 +729,21 @@ namespace lvh { */ std::uint16_t scan_code = 0; + /** + * @brief Whether `scan_code` uses the E0/E1 extended prefix from `MapVirtualKeyW`. + */ + bool scan_code_extended = false; + /** * @brief Whether `key_code` is normalized to the Windows US English keyboard layout. */ bool uses_normalized_key_code = false; + /** + * @brief Sunshine keyboard packet flags (`SS_KBE_FLAG_*`) from the streaming client. + */ + std::uint8_t stream_flags = 0; + /** * @brief Whether the backend should prefer a native scan-code translation when `scan_code` is not provided. */ diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index eaffb3e2..40c47eff 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -535,8 +535,29 @@ namespace lvh::detail { return it->second; } - int key_code_to_linux(KeyboardKeyCode key_code) { - static constexpr std::array, 47> special_keys {{ + constexpr std::uint8_t keyboard_stream_flag_lang1 = 0x02; + constexpr std::uint8_t keyboard_stream_flag_lang2 = 0x04; + + int key_code_to_linux(KeyboardKeyCode key_code, bool uses_normalized_key_code, std::uint8_t stream_flags) { + if ((stream_flags & keyboard_stream_flag_lang1) != 0U) { + return KEY_HANGEUL; + } + if ((stream_flags & keyboard_stream_flag_lang2) != 0U) { + return KEY_HANJA; + } + + if (!uses_normalized_key_code) { + switch (key_code) { + case 0xDC: + return KEY_YEN; + case 0xE2: + return KEY_RO; + default: + break; + } + } + + static constexpr std::array, 50> special_keys {{ {0x08, KEY_BACKSPACE}, {0x09, KEY_TAB}, {0x0D, KEY_ENTER}, @@ -547,7 +568,10 @@ namespace lvh::detail { {0x12, KEY_LEFTALT}, {0xA4, KEY_LEFTALT}, {0x14, KEY_CAPSLOCK}, + {0x15, KEY_KATAKANAHIRAGANA}, {0x1B, KEY_ESC}, + {0x1C, KEY_HENKAN}, + {0x1D, KEY_MUHENKAN}, {0x20, KEY_SPACE}, {0x21, KEY_PAGEUP}, {0x22, KEY_PAGEDOWN}, @@ -1442,7 +1466,7 @@ namespace lvh::detail { private: OperationStatus emit_keyboard_event(const KeyboardEvent &event) { - const auto linux_key = key_code_to_linux(event.key_code); + const auto linux_key = key_code_to_linux(event.key_code, event.uses_normalized_key_code, event.stream_flags); if (linux_key < 0) { return OperationStatus::failure(ErrorCode::invalid_argument, "keyboard key code is not supported by the Linux backend"); } @@ -2034,8 +2058,26 @@ namespace lvh::detail { }; #if defined(LIBVIRTUALHID_HAVE_XTEST) - KeySym key_code_to_keysym(KeyboardKeyCode key_code) { - static constexpr std::array, 45> special_keysyms {{ + KeySym key_code_to_keysym(KeyboardKeyCode key_code, bool uses_normalized_key_code, std::uint8_t stream_flags) { + if ((stream_flags & keyboard_stream_flag_lang1) != 0U) { + return XK_Hangul; + } + if ((stream_flags & keyboard_stream_flag_lang2) != 0U) { + return XK_Hangul_Hanja; + } + + if (!uses_normalized_key_code) { + switch (key_code) { + case 0xDC: + return XK_yen; + case 0xE2: + return XK_backslash; + default: + break; + } + } + + static constexpr std::array, 49> special_keysyms {{ {0x08, XK_BackSpace}, {0x09, XK_Tab}, {0x0D, XK_Return}, @@ -2046,7 +2088,10 @@ namespace lvh::detail { {0x12, XK_Alt_L}, {0xA4, XK_Alt_L}, {0x14, XK_Caps_Lock}, + {0x15, XK_Hiragana_Katakana}, {0x1B, XK_Escape}, + {0x1C, XK_Henkan}, + {0x1D, XK_Muhenkan}, {0x20, XK_space}, {0x21, XK_Page_Up}, {0x22, XK_Page_Down}, @@ -2081,6 +2126,7 @@ namespace lvh::detail { {0xDC, XK_backslash}, {0xDD, XK_bracketright}, {0xDE, XK_apostrophe}, + {0xE2, XK_backslash}, }}; if (const auto keysym = mapped_keyboard_code(key_code, special_keysyms); keysym.has_value()) { @@ -2169,7 +2215,7 @@ namespace lvh::detail { return OperationStatus::failure(ErrorCode::device_closed, "XTest keyboard is closed"); } - const auto keysym = key_code_to_keysym(event.key_code); + const auto keysym = key_code_to_keysym(event.key_code, event.uses_normalized_key_code, event.stream_flags); if (keysym == NoSymbol) { return OperationStatus::failure(ErrorCode::invalid_argument, "keyboard key code is not supported by XTest fallback"); } diff --git a/src/platform/windows/keylayout.hpp b/src/platform/windows/keylayout.hpp index 279d46a2..6eaa34fe 100644 --- a/src/platform/windows/keylayout.hpp +++ b/src/platform/windows/keylayout.hpp @@ -37,15 +37,15 @@ namespace lvh::detail { 56, /* 0x12 */ 0, /* 0x13 */ 58, /* 0x14 */ - 0, /* 0x15 */ + 0x70, /* 0x15 VK_KANA */ 0, /* 0x16 */ 0, /* 0x17 */ 0, /* 0x18 */ 0, /* 0x19 */ 0, /* 0x1a */ 1, /* 0x1b */ - 0, /* 0x1c */ - 0, /* 0x1d */ + 0x79, /* 0x1c VK_CONVERT (Henkan) */ + 0x7B, /* 0x1d VK_NONCONVERT (Muhenkan) */ 0, /* 0x1e */ 0, /* 0x1f */ 57, /* 0x20 */ diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index a9c2a923..ffdff88c 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -466,6 +466,30 @@ namespace lvh::detail { } } + /** + * @brief Map a virtual key to a scan code using the active keyboard layout. + * + * @param key_code Windows virtual key code. + * @return Scan code and extended-prefix flag from `MapVirtualKeyExW`. + */ + struct MappedScanCode { + WORD scan_code = 0; + bool extended = false; + }; + + MappedScanCode map_virtual_key_to_scan_code(KeyboardKeyCode key_code) { + const auto mapped = ::MapVirtualKeyExW(key_code, MAPVK_VK_TO_VSC_EX, nullptr); + if (mapped == 0U) { + return {}; + } + + const auto prefix = static_cast((mapped >> 8U) & 0xFFU); + return { + .scan_code = static_cast(mapped & 0xFFU), + .extended = prefix == 0xE0U || prefix == 0xE1U, + }; + } + bool can_map_virtual_key_to_scan_code(KeyboardKeyCode key_code) { return key_code != VK_LWIN && key_code != VK_RWIN && key_code != VK_PAUSE; } @@ -1272,23 +1296,33 @@ namespace lvh::detail { INPUT input {}; input.type = INPUT_KEYBOARD; input.ki.wVk = event.key_code; + auto use_extended_key = false; + if (event.scan_code != 0U) { input.ki.wVk = 0; input.ki.wScan = event.scan_code; input.ki.dwFlags |= KEYEVENTF_SCANCODE; + use_extended_key = event.scan_code_extended; } else { + WORD scan_code = 0; if (event.uses_normalized_key_code) { - input.ki.wScan = windows_us_english_scan_code(event.key_code); - } else if (event.prefer_native_scan_code && can_map_virtual_key_to_scan_code(event.key_code)) { - input.ki.wScan = static_cast(::MapVirtualKeyW(event.key_code, MAPVK_VK_TO_VSC)); + scan_code = windows_us_english_scan_code(event.key_code); + use_extended_key = extended_key(event.key_code); + } else if (can_map_virtual_key_to_scan_code(event.key_code)) { + // Non-normalized keys use the host's active keyboard layout. + const auto mapped = map_virtual_key_to_scan_code(event.key_code); + scan_code = mapped.scan_code; + use_extended_key = mapped.extended; } - if (input.ki.wScan != 0U) { + if (scan_code != 0U) { input.ki.wVk = 0; + input.ki.wScan = scan_code; input.ki.dwFlags |= KEYEVENTF_SCANCODE; } } - if (extended_key(event.key_code)) { + + if (use_extended_key) { input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; } if (!event.pressed) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d5884647..89f204e2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,7 +48,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND LIBVIRTUALHID_TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/linux_backend_test_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_linux_backend.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_linux_consumers.cpp") + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_linux_consumers.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_moonlight_special_key_mappings.cpp") if(LIBVIRTUALHID_ENABLE_XTEST) find_package(X11 QUIET) @@ -68,6 +69,7 @@ elseif(WIN32) "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_broker_client_test_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_broker_service_test_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_backend_test_hooks.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_keyboard_scancodes.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_backend.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_broker_client.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_broker_service.cpp" @@ -103,6 +105,9 @@ if(TARGET virtualhid_control_model) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_include_directories(${TEST_BINARY} + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../../../../moonlight-qt/app") target_link_libraries(${TEST_BINARY} PRIVATE PkgConfig::LIBEVDEV diff --git a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp index 6742901d..8c33326a 100644 --- a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp @@ -433,6 +433,16 @@ namespace lvh::detail::test { */ int linux_key_code(KeyboardKeyCode key_code); + /** + * @brief Translate a portable keyboard key code with normalization and stream flags. + */ + int linux_key_code_with_options(KeyboardKeyCode key_code, bool uses_normalized_key_code, std::uint8_t stream_flags); + + /** + * @brief Translate a portable keyboard key code to an X11 keysym for XTest fallback. + */ + unsigned long linux_xtest_keysym(KeyboardKeyCode key_code, bool uses_normalized_key_code, std::uint8_t stream_flags); + /** * @brief Translate a mouse button to a Linux input button code. * @@ -1220,14 +1230,6 @@ namespace lvh::detail::test { */ OperationStatus linux_xtest_mouse_create_query_failure(); - /** - * @brief Translate a portable key code to an XTest keysym. - * - * @param key_code Portable keyboard key code. - * @return X11 keysym, or `0` when unsupported or XTest is disabled. - */ - unsigned long linux_xtest_keysym(KeyboardKeyCode key_code); - /** * @brief Translate a mouse button to an XTest button code. * diff --git a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp index f5976cf2..c927bf99 100644 --- a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp @@ -178,4 +178,13 @@ namespace lvh::detail::test { WindowsOverlappedIoResult windows_backend_overlapped_device_io(); WindowsBackendSendInputResult windows_backend_send_input_devices(); + struct WindowsVirtualKeyScanMapping { + std::uint16_t scan_code = 0; + bool extended = false; + }; + + WindowsVirtualKeyScanMapping windows_map_active_layout_scan_code(KeyboardKeyCode key_code); + + WindowsSendInputRecord windows_submit_keyboard_event(const KeyboardEvent &event); + } // namespace lvh::detail::test diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index 91345e3d..2a2c7cab 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -962,7 +962,11 @@ namespace lvh::detail::test { } int linux_key_code(KeyboardKeyCode key_code) { - return key_code_to_linux(key_code); + return key_code_to_linux(key_code, true, 0); + } + + int linux_key_code_with_options(KeyboardKeyCode key_code, bool uses_normalized_key_code, std::uint8_t stream_flags) { + return key_code_to_linux(key_code, uses_normalized_key_code, stream_flags); } int linux_mouse_button(MouseButton button) { @@ -2606,11 +2610,13 @@ namespace lvh::detail::test { #endif } - unsigned long linux_xtest_keysym(KeyboardKeyCode key_code) { + unsigned long linux_xtest_keysym(KeyboardKeyCode key_code, bool uses_normalized_key_code, std::uint8_t stream_flags) { #if defined(LIBVIRTUALHID_HAVE_XTEST) - return key_code_to_keysym(key_code); + return key_code_to_keysym(key_code, uses_normalized_key_code, stream_flags); #else static_cast(key_code); + static_cast(uses_normalized_key_code); + static_cast(stream_flags); return 0; #endif } diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp index b3442dcc..ed16e4da 100644 --- a/tests/fixtures/windows_backend_test_hooks.cpp +++ b/tests/fixtures/windows_backend_test_hooks.cpp @@ -990,6 +990,32 @@ namespace lvh::detail { return result; } + WindowsVirtualKeyScanMapping windows_map_active_layout_scan_code(KeyboardKeyCode key_code) { + const auto mapped = map_virtual_key_to_scan_code(key_code); + return { + .scan_code = mapped.scan_code, + .extended = mapped.extended, + }; + } + + WindowsSendInputRecord windows_submit_keyboard_event(const KeyboardEvent &event) { + FakeSendInputState fake_send_input; + ScopedFakeSendInput scoped_send_input {fake_send_input}; + WindowsBackend backend {nullptr, nullptr}; + + CreateKeyboardOptions keyboard_options; + keyboard_options.profile = profiles::keyboard(); + if (const auto keyboard = backend.create_keyboard(99, keyboard_options); keyboard) { + static_cast(keyboard.keyboard->submit(event)); + } + + if (fake_send_input.sent_inputs.empty()) { + return {}; + } + + return fake_send_input.sent_inputs.back(); + } + } // namespace test } // namespace lvh::detail diff --git a/tests/unit/test_linux_backend.cpp b/tests/unit/test_linux_backend.cpp index 70214ebf..54c99311 100644 --- a/tests/unit/test_linux_backend.cpp +++ b/tests/unit/test_linux_backend.cpp @@ -111,6 +111,18 @@ TEST_F(LinuxBackendTest, TranslatesKeyboardKeys) { EXPECT_EQ(lvh::detail::test::linux_key_code(0x88), -1); } +TEST_F(LinuxBackendTest, DistinguishesJapaneseRoFromIso102ndKey) { + EXPECT_EQ(lvh::detail::test::linux_key_code_with_options(0xE2, false, 0), KEY_RO); + EXPECT_EQ(lvh::detail::test::linux_key_code_with_options(0xE2, true, 0), KEY_102ND); + EXPECT_EQ(lvh::detail::test::linux_key_code_with_options(0xDC, false, 0), KEY_YEN); +} + +TEST_F(LinuxBackendTest, KoreanStreamFlagsMapToDistinctLinuxKeys) { + EXPECT_EQ(lvh::detail::test::linux_key_code_with_options(0x15, false, 0x02), KEY_HANGEUL); + EXPECT_EQ(lvh::detail::test::linux_key_code_with_options(0x19, false, 0x04), KEY_HANJA); + EXPECT_EQ(lvh::detail::test::linux_key_code_with_options(0x15, false, 0), KEY_KATAKANAHIRAGANA); +} + TEST_F(LinuxBackendTest, TranslatesMouseButtonsAndBusTypes) { EXPECT_EQ(lvh::detail::test::linux_mouse_button(lvh::MouseButton::left), BTN_LEFT); EXPECT_EQ(lvh::detail::test::linux_mouse_button(lvh::MouseButton::middle), BTN_MIDDLE); @@ -1218,57 +1230,61 @@ TEST_F(LinuxBackendTest, XTestFallbackCoversKeyboardAndMousePaths) { EXPECT_TRUE(mouse_closed_status.code() == lvh::ErrorCode::device_closed || mouse_closed_status.code() == lvh::ErrorCode::backend_unavailable); #if defined(LIBVIRTUALHID_HAVE_XTEST) - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x08), XK_BackSpace); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x09), XK_Tab); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x0D), XK_Return); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x10), XK_Shift_L); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x11), XK_Control_L); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x12), XK_Alt_L); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x14), XK_Caps_Lock); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x1B), XK_Escape); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x20), XK_space); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x21), XK_Page_Up); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x22), XK_Page_Down); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x23), XK_End); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x24), XK_Home); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x25), XK_Left); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x26), XK_Up); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x27), XK_Right); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x28), XK_Down); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x2D), XK_Insert); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x2E), XK_Delete); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5B), XK_Super_L); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5C), XK_Super_R); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x90), XK_Num_Lock); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x91), XK_Scroll_Lock); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA1), XK_Shift_R); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA3), XK_Control_R); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA5), XK_Alt_R); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBA), XK_semicolon); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBB), XK_equal); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBC), XK_comma); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBD), XK_minus); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBE), XK_period); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBF), XK_slash); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xC0), XK_grave); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDB), XK_bracketleft); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDC), XK_backslash); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDD), XK_bracketright); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDE), XK_apostrophe); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x30), XK_0); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x39), XK_9); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x41), XK_a); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5A), XK_z); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x60), XK_KP_0); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x69), XK_KP_9); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6A), XK_KP_Multiply); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6B), XK_KP_Add); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6D), XK_KP_Subtract); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6E), XK_KP_Decimal); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6F), XK_KP_Divide); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x70), XK_F1); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x87), XK_F24); - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x88), 0UL); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x08, true, 0), XK_BackSpace); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x09, true, 0), XK_Tab); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x0D, true, 0), XK_Return); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x10, true, 0), XK_Shift_L); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x11, true, 0), XK_Control_L); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x12, true, 0), XK_Alt_L); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x14, true, 0), XK_Caps_Lock); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x1B, true, 0), XK_Escape); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x20, true, 0), XK_space); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x21, true, 0), XK_Page_Up); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x22, true, 0), XK_Page_Down); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x23, true, 0), XK_End); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x24, true, 0), XK_Home); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x25, true, 0), XK_Left); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x26, true, 0), XK_Up); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x27, true, 0), XK_Right); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x28, true, 0), XK_Down); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x2D, true, 0), XK_Insert); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x2E, true, 0), XK_Delete); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5B, true, 0), XK_Super_L); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5C, true, 0), XK_Super_R); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x90, true, 0), XK_Num_Lock); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x91, true, 0), XK_Scroll_Lock); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA1, true, 0), XK_Shift_R); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA3, true, 0), XK_Control_R); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA5, true, 0), XK_Alt_R); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBA, true, 0), XK_semicolon); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBB, true, 0), XK_equal); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBC, true, 0), XK_comma); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBD, true, 0), XK_minus); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBE, true, 0), XK_period); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBF, true, 0), XK_slash); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xC0, true, 0), XK_grave); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDB, true, 0), XK_bracketleft); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDC, true, 0), XK_backslash); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDD, true, 0), XK_bracketright); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDE, true, 0), XK_apostrophe); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x30, true, 0), XK_0); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x39, true, 0), XK_9); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x41, true, 0), XK_a); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5A, true, 0), XK_z); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x60, true, 0), XK_KP_0); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x69, true, 0), XK_KP_9); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6A, true, 0), XK_KP_Multiply); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6B, true, 0), XK_KP_Add); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6D, true, 0), XK_KP_Subtract); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6E, true, 0), XK_KP_Decimal); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6F, true, 0), XK_KP_Divide); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x70, true, 0), XK_F1); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x87, true, 0), XK_F24); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x88, true, 0), 0UL); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xE2, false, 0), XK_backslash); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDC, false, 0), XK_yen); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x15, false, 0x02), XK_Hangul); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x19, false, 0x04), XK_Hangul_Hanja); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::left), 1); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::middle), 2); @@ -1277,7 +1293,7 @@ TEST_F(LinuxBackendTest, XTestFallbackCoversKeyboardAndMousePaths) { EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::extra), 9); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(static_cast(255)), 1); #else - EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x41), 0UL); + EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x41, true, 0), 0UL); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::left), 1); #endif } diff --git a/tests/unit/test_moonlight_special_key_mappings.cpp b/tests/unit/test_moonlight_special_key_mappings.cpp new file mode 100644 index 00000000..d5686b0c --- /dev/null +++ b/tests/unit/test_moonlight_special_key_mappings.cpp @@ -0,0 +1,58 @@ +/** + * @file tests/unit/test_moonlight_special_key_mappings.cpp + * @brief Regression tests for Moonlight layout-sensitive scancode mapping. + */ + +#include "streaming/input/keyboard_special_keys.h" + +#include + +TEST(MoonlightSpecialKeyMappings, UsQwertyAndUkIsoKeepNonusBackslashNormalized) { + const auto mapping = mapMoonlightSpecialScancode(SDL_SCANCODE_NONUSBACKSLASH, false, false); + ASSERT_TRUE(mapping.handled); + EXPECT_EQ(mapping.key_code, 0xE2); + EXPECT_EQ(mapping.flags, 0); +} + +TEST(MoonlightSpecialKeyMappings, GermanAndFrenchIsoInternational1StaysNormalized) { + const auto mapping = mapMoonlightSpecialScancode(SDL_SCANCODE_INTERNATIONAL1, false, false); + ASSERT_TRUE(mapping.handled); + EXPECT_EQ(mapping.key_code, 0xE2); + EXPECT_EQ(mapping.flags, 0); +} + +TEST(MoonlightSpecialKeyMappings, JapaneseJisRoUsesNonNormalizedIdentity) { + const auto international1 = mapMoonlightSpecialScancode(SDL_SCANCODE_INTERNATIONAL1, true, false); + ASSERT_TRUE(international1.handled); + EXPECT_EQ(international1.key_code, 0xE2); + EXPECT_EQ(international1.flags, SS_KBE_FLAG_NON_NORMALIZED); + + const auto ro_backslash = mapMoonlightSpecialScancode(SDL_SCANCODE_BACKSLASH, true, true); + ASSERT_TRUE(ro_backslash.handled); + EXPECT_EQ(ro_backslash.key_code, 0xE2); + EXPECT_EQ(ro_backslash.flags, SS_KBE_FLAG_NON_NORMALIZED); + + const auto nonus_on_jis = mapMoonlightSpecialScancode(SDL_SCANCODE_NONUSBACKSLASH, true, true); + ASSERT_TRUE(nonus_on_jis.handled); + EXPECT_EQ(nonus_on_jis.key_code, 0xE2); + EXPECT_EQ(nonus_on_jis.flags, 0); +} + +TEST(MoonlightSpecialKeyMappings, UsQwertyBackslashMapsToOem5) { + const auto mapping = mapMoonlightSpecialScancode(SDL_SCANCODE_BACKSLASH, false, false); + ASSERT_TRUE(mapping.handled); + EXPECT_EQ(mapping.key_code, 0xDC); + EXPECT_EQ(mapping.flags, 0); +} + +TEST(MoonlightSpecialKeyMappings, KoreanLangKeysUseDistinctProtocolFlags) { + const auto lang1 = mapMoonlightSpecialScancode(SDL_SCANCODE_LANG1, false, false); + ASSERT_TRUE(lang1.handled); + EXPECT_EQ(lang1.key_code, 0x15); + EXPECT_EQ(lang1.flags, static_cast(SS_KBE_FLAG_NON_NORMALIZED | SS_KBE_FLAG_LANG1)); + + const auto lang2 = mapMoonlightSpecialScancode(SDL_SCANCODE_LANG2, false, false); + ASSERT_TRUE(lang2.handled); + EXPECT_EQ(lang2.key_code, 0x19); + EXPECT_EQ(lang2.flags, static_cast(SS_KBE_FLAG_NON_NORMALIZED | SS_KBE_FLAG_LANG2)); +} diff --git a/tests/unit/test_windows_keyboard_scancodes.cpp b/tests/unit/test_windows_keyboard_scancodes.cpp new file mode 100644 index 00000000..1710ef05 --- /dev/null +++ b/tests/unit/test_windows_keyboard_scancodes.cpp @@ -0,0 +1,151 @@ +/** + * @file tests/unit/test_windows_keyboard_scancodes.cpp + * @brief Regression tests for Windows keyboard scan-code and extended-prefix handling. + */ + +#ifndef DOXYGEN + #if defined(WINVER) && WINVER < 0x0A00 + #undef WINVER + #endif + #ifndef WINVER + #define WINVER 0x0A00 + #endif + #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 + #undef _WIN32_WINNT + #endif + #ifndef _WIN32_WINNT + #define _WIN32_WINNT 0x0A00 + #endif +#endif + +#include "fixtures/fixtures.hpp" +#include "fixtures/windows_backend_test_hooks.hpp" + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +#include + +namespace { + + class WindowsKeyboardScanCodeTest: public WindowsTest {}; + + bool has_extended_flag(const lvh::detail::test::WindowsSendInputRecord &record) { + return (record.key_flags & KEYEVENTF_EXTENDEDKEY) != 0U; + } + + bool uses_scancode(const lvh::detail::test::WindowsSendInputRecord &record) { + return (record.key_flags & KEYEVENTF_SCANCODE) != 0U; + } + +} // namespace + +TEST_F(WindowsKeyboardScanCodeTest, NormalizedKeysDoNotInferExtendedPrefixFromLowByte) { + const auto main_enter = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_RETURN, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(main_enter)); + EXPECT_EQ(main_enter.scan_code, 0x1CU); + EXPECT_FALSE(has_extended_flag(main_enter)); + + const auto slash = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_OEM_2, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(slash)); + EXPECT_EQ(slash.scan_code, 0x53U); + EXPECT_FALSE(has_extended_flag(slash)); + + const auto backslash = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_OEM_5, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(backslash)); + EXPECT_EQ(backslash.scan_code, 0x2BU); + EXPECT_FALSE(has_extended_flag(backslash)); + + const auto left_alt = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_MENU, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(left_alt)); + EXPECT_EQ(left_alt.scan_code, 0x38U); + EXPECT_FALSE(has_extended_flag(left_alt)); +} + +TEST_F(WindowsKeyboardScanCodeTest, NormalizedExtendedKeysKeepActualPrefix) { + const auto keypad_divide = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_DIVIDE, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(keypad_divide)); + EXPECT_EQ(keypad_divide.scan_code, 0x35U); + EXPECT_TRUE(has_extended_flag(keypad_divide)); + + const auto right_alt = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_RMENU, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(right_alt)); + EXPECT_EQ(right_alt.scan_code, 0x38U); + EXPECT_TRUE(has_extended_flag(right_alt)); + + const auto home = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_HOME, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(home)); + EXPECT_EQ(home.scan_code, 0x47U); + EXPECT_FALSE(has_extended_flag(home)); + + const auto keypad_home = lvh::detail::test::windows_submit_keyboard_event({ + .key_code = VK_NUMPAD7, + .pressed = true, + .uses_normalized_key_code = true, + }); + ASSERT_TRUE(uses_scancode(keypad_home)); + EXPECT_EQ(keypad_home.scan_code, 0x47U); + EXPECT_FALSE(has_extended_flag(keypad_home)); +} + +TEST_F(WindowsKeyboardScanCodeTest, ExplicitScanCodeUsesProvidedExtendedPrefix) { + const auto keypad_enter = lvh::detail::test::windows_submit_keyboard_event({ + .scan_code = 0x1C, + .scan_code_extended = true, + .pressed = true, + }); + ASSERT_TRUE(uses_scancode(keypad_enter)); + EXPECT_EQ(keypad_enter.scan_code, 0x1CU); + EXPECT_TRUE(has_extended_flag(keypad_enter)); + + const auto main_enter = lvh::detail::test::windows_submit_keyboard_event({ + .scan_code = 0x1C, + .scan_code_extended = false, + .pressed = true, + }); + ASSERT_TRUE(uses_scancode(main_enter)); + EXPECT_EQ(main_enter.scan_code, 0x1CU); + EXPECT_FALSE(has_extended_flag(main_enter)); +} + +TEST_F(WindowsKeyboardScanCodeTest, MapVirtualKeyExPreservesExtendedPrefix) { + const auto mapped_divide = lvh::detail::test::windows_map_active_layout_scan_code(VK_DIVIDE); + EXPECT_EQ(mapped_divide.scan_code, 0x35U); + EXPECT_TRUE(mapped_divide.extended); + + const auto mapped_return = lvh::detail::test::windows_map_active_layout_scan_code(VK_RETURN); + EXPECT_EQ(mapped_return.scan_code, 0x1CU); + EXPECT_FALSE(mapped_return.extended); +}