Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/platform/windows/windows_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
}
}

Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// standard includes
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <vector>

Expand Down Expand Up @@ -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<OperationStatus> submit_statuses; ///< Result of each submitted event.
std::vector<WindowsSendInputRecord> sent_inputs; ///< Intercepted Win32 input events.
std::vector<std::vector<std::uint8_t>> reports; ///< Intercepted HID keyboard reports.
};

struct WindowsMouseSendInputResult {
OperationStatus relative_status;
OperationStatus absolute_status;
Expand Down Expand Up @@ -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<const KeyboardEvent> events,
bool use_hid
);

/**
* @brief Scale a source coordinate through a target viewport into a virtual desktop axis.
*
Expand Down
26 changes: 26 additions & 0 deletions tests/fixtures/windows_backend_test_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,32 @@ namespace lvh::detail {
return result;
}

WindowsKeyboardTranslationResult windows_backend_translate_keyboard_events(
std::span<const KeyboardEvent> events,
bool use_hid
) {
WindowsKeyboardTranslationResult result;
FakeSendInputState send_input_state;
ScopedFakeSendInput fake_send_input {send_input_state};
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = use_hid ?
make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>()) :
std::make_unique<WindowsBackend>(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;

Expand Down
135 changes: 130 additions & 5 deletions tests/unit/test_windows_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <libvirtualhid/profiles.hpp>

// standard includes
#include <algorithm>
#include <array>
#include <utility>
#include <vector>

// lib includes
#include <libvirtualhid/profiles.hpp>

// local includes
#include "lvh_windows_protocol.h"

#ifndef NOMINMAX
#define NOMINMAX
#endif
Expand Down Expand Up @@ -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<std::pair<lvh::KeyboardKeyCode, std::uint16_t>>({
{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<lvh::KeyboardKeyCode>({'B', 'C', VK_RETURN, VK_PAUSE, VK_SNAPSHOT, VK_NUMLOCK})) {
SCOPED_TRACE(key);
const std::array events {
lvh::KeyboardEvent {.key_code = static_cast<lvh::KeyboardKeyCode>(key), .pressed = true, .uses_normalized_key_code = true},
lvh::KeyboardEvent {.key_code = static_cast<lvh::KeyboardKeyCode>(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<std::pair<lvh::KeyboardKeyCode, std::uint8_t>>({
{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<std::uint8_t> pressed_report(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 0U);
pressed_report[2U] = usage;
const std::vector<std::uint8_t> released_report(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 0U);
EXPECT_EQ(result.reports, (std::vector<std::vector<std::uint8_t>> {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<lvh::KeyboardKeyCode>(key), .pressed = true, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true},
lvh::KeyboardEvent {.key_code = static_cast<lvh::KeyboardKeyCode>(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<UINT>(key));
}
}
}
}

TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsupportedProfiles) {
const auto result = lvh::detail::test::windows_backend_send_input_devices();

Expand Down
Loading