Skip to content
Open
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
139 changes: 119 additions & 20 deletions src/platform/macos/macos_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,86 @@ namespace lvh::detail {
}
}

/// Keys a physical keyboard reports with the secondary-Fn flag: arrows, navigation keys, and F1 to F20.
constexpr std::array<CGKeyCode, 30> secondary_fn_keys {
kVK_LeftArrow,
kVK_RightArrow,
kVK_UpArrow,
kVK_DownArrow,
kVK_Home,
kVK_End,
kVK_PageUp,
kVK_PageDown,
kVK_ForwardDelete,
kVK_Help,
kVK_F1,
kVK_F2,
kVK_F3,
kVK_F4,
kVK_F5,
kVK_F6,
kVK_F7,
kVK_F8,
kVK_F9,
kVK_F10,
kVK_F11,
kVK_F12,
kVK_F13,
kVK_F14,
kVK_F15,
kVK_F16,
kVK_F17,
kVK_F18,
kVK_F19,
kVK_F20,
};

/// Keys a physical keyboard reports with the numeric-pad flag: arrows and the keypad.
constexpr std::array<CGKeyCode, 22> numeric_pad_keys {
kVK_LeftArrow,
kVK_RightArrow,
kVK_UpArrow,
kVK_DownArrow,
kVK_ANSI_Keypad0,
kVK_ANSI_Keypad1,
kVK_ANSI_Keypad2,
kVK_ANSI_Keypad3,
kVK_ANSI_Keypad4,
kVK_ANSI_Keypad5,
kVK_ANSI_Keypad6,
kVK_ANSI_Keypad7,
kVK_ANSI_Keypad8,
kVK_ANSI_Keypad9,
kVK_ANSI_KeypadDecimal,
kVK_ANSI_KeypadMultiply,
kVK_ANSI_KeypadPlus,
kVK_ANSI_KeypadClear,
kVK_ANSI_KeypadDivide,
kVK_ANSI_KeypadEnter,
kVK_ANSI_KeypadMinus,
kVK_ANSI_KeypadEquals,
};

/**
* @brief Resolve the flags macOS attaches to a non-modifier key by itself.
*
* The system hotkey layer matches on these flags, so a synthetic Control+Up posted without
* them never reaches Mission Control even though the same event reaches ordinary applications.
*
* @param key macOS virtual key code.
* @return Flags to add to the posted event for this key only.
*/
inline CGEventFlags implicit_key_flags(CGKeyCode key) {
CGEventFlags flags {};
if (std::ranges::find(secondary_fn_keys, key) != secondary_fn_keys.end()) {
flags |= kCGEventFlagMaskSecondaryFn;
}
if (std::ranges::find(numeric_pad_keys, key) != numeric_pad_keys.end()) {
flags |= kCGEventFlagMaskNumericPad;
}
return flags;
}

/**
* @brief Convert a scroll-wheel slider value to logical lines per detent.
*
Expand Down Expand Up @@ -442,6 +522,43 @@ namespace lvh::detail {
std::mutex keyboard_mutex; ///< Guards shared keyboard modifier state.
};

/**
* @brief Build the CoreGraphics event for one key transition and update the shared modifier state.
*
* The caller must hold `state.keyboard_mutex`.
*
* @param state Shared backend state whose modifier flags are read and updated.
* @param key macOS virtual key code.
* @param pressed Whether the key is going down.
* @return Event ready to post, or `nullptr` when CoreGraphics cannot create one. The caller releases it.
*/
inline CGEventRef create_keyboard_event(MacosInputState &state, CGKeyCode key, bool pressed) {
const auto keyboard_event = CGEventCreateKeyboardEvent(state.keyboard_source, key, pressed);
if (!keyboard_event) {
return nullptr;
}

CGEventSetIntegerValueField(keyboard_event, kCGKeyboardEventKeycode, key);

ModifierFlags modifier_flags;
if (modifier_flags_for_key(key, modifier_flags)) {
if (pressed) {
state.keyboard_flags |= modifier_flags.generic | modifier_flags.device;
} else {
state.keyboard_flags &= ~modifier_flags.device;
if ((state.keyboard_flags & modifier_flags.all_devices) == 0) {
state.keyboard_flags &= ~modifier_flags.generic;
}
}
CGEventSetType(keyboard_event, kCGEventFlagsChanged);
} else {
CGEventSetType(keyboard_event, pressed ? kCGEventKeyDown : kCGEventKeyUp);
}

CGEventSetFlags(keyboard_event, state.keyboard_flags | implicit_key_flags(key));
return keyboard_event;
}

/**
* @brief Backend keyboard backed by CoreGraphics keyboard events.
*/
Expand Down Expand Up @@ -470,30 +587,12 @@ namespace lvh::detail {
return OperationStatus::failure(backend_failure, "macOS keyboard event source is unavailable");
}

const auto keyboard_event = CGEventCreateKeyboardEvent(state_->keyboard_source, *key, event.pressed);
std::lock_guard lock {state_->keyboard_mutex};
const auto keyboard_event = create_keyboard_event(*state_, *key, event.pressed);
if (!keyboard_event) {
return OperationStatus::failure(backend_failure, "create macOS keyboard event");
}

std::lock_guard lock {state_->keyboard_mutex};
CGEventSetIntegerValueField(keyboard_event, kCGKeyboardEventKeycode, *key);

ModifierFlags modifier_flags;
if (modifier_flags_for_key(*key, modifier_flags)) {
if (event.pressed) {
state_->keyboard_flags |= modifier_flags.generic | modifier_flags.device;
} else {
state_->keyboard_flags &= ~modifier_flags.device;
if ((state_->keyboard_flags & modifier_flags.all_devices) == 0) {
state_->keyboard_flags &= ~modifier_flags.generic;
}
}
CGEventSetType(keyboard_event, kCGEventFlagsChanged);
} else {
CGEventSetType(keyboard_event, event.pressed ? kCGEventKeyDown : kCGEventKeyUp);
}

CGEventSetFlags(keyboard_event, state_->keyboard_flags);
CGEventPost(kCGSessionEventTap, keyboard_event);
CFRelease(keyboard_event);
return OperationStatus::success();
Expand Down
28 changes: 28 additions & 0 deletions tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// standard includes
#include <cstdint>
#include <optional>
#include <utility>
#include <vector>

// lib includes
#include <libvirtualhid/types.hpp>
Expand All @@ -29,6 +31,16 @@ namespace lvh::detail::test {
std::uint32_t event_type {}; ///< CoreGraphics mouse event type value.
};

/**
* @brief Portable view of one CoreGraphics keyboard event the macOS backend built.
*/
struct MacosKeyEventResult {
std::uint32_t event_type {}; ///< CoreGraphics event type value.
std::int64_t key_code {}; ///< macOS virtual key code stored on the event.
std::uint64_t flags {}; ///< Flags stored on the event.
std::uint64_t tracked_flags {}; ///< Shared modifier state after the event was built.
};

/**
* @brief Result set for macOS backend lifecycle utility coverage.
*/
Expand Down Expand Up @@ -68,6 +80,22 @@ namespace lvh::detail::test {
*/
bool macos_backend_is_modifier_key(KeyboardKeyCode key_code);

/**
* @brief Resolve the flags the macOS backend adds to a key event for the key itself.
*
* @param key_code Portable key code.
* @return CoreGraphics event flags, or zero when the key is unmapped or carries none.
*/
std::uint64_t macos_backend_implicit_key_flags(KeyboardKeyCode key_code);

/**
* @brief Build, without posting, the events the macOS backend would send for a key sequence.
*
* @param transitions Portable key codes paired with `true` for press and `false` for release.
* @return One result per transition whose key code the backend maps.
*/
std::vector<MacosKeyEventResult> macos_backend_key_events(const std::vector<std::pair<KeyboardKeyCode, bool>> &transitions);

/**
* @brief Convert a macOS scroll-wheel scaling value to lines per detent.
*
Expand Down
37 changes: 37 additions & 0 deletions tests/fixtures/macos_backend_test_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,43 @@ namespace lvh::detail::test {
return macos::modifier_flags_for_key(*mapped, flags);
}

std::uint64_t macos_backend_implicit_key_flags(KeyboardKeyCode key_code) {
const auto mapped = macos::macos_key_code(key_code);
if (!mapped) {
return 0;
}

return static_cast<std::uint64_t>(macos::implicit_key_flags(*mapped));
}

std::vector<MacosKeyEventResult> macos_backend_key_events(const std::vector<std::pair<KeyboardKeyCode, bool>> &transitions) {
macos::MacosInputState state;
std::lock_guard lock {state.keyboard_mutex};

std::vector<MacosKeyEventResult> results;
for (const auto &[key_code, pressed] : transitions) {
const auto mapped = macos::macos_key_code(key_code);
if (!mapped) {
continue;
}

const auto event = macos::create_keyboard_event(state, *mapped, pressed);
if (!event) {
continue;
}

results.push_back({
.event_type = static_cast<std::uint32_t>(CGEventGetType(event)),
.key_code = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode),
.flags = static_cast<std::uint64_t>(CGEventGetFlags(event)),
.tracked_flags = static_cast<std::uint64_t>(state.keyboard_flags),
});
CFRelease(event);
}

return results;
}

int macos_backend_scroll_lines_per_detent(double scale) {
return macos::scroll_lines_per_detent(scale);
}
Expand Down
85 changes: 85 additions & 0 deletions tests/unit/test_macos_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,91 @@ TEST_F(MacosBackendTest, IdentifiesModifierKeys) {
EXPECT_FALSE(lvh::detail::test::macos_backend_is_modifier_key(0x13));
}

TEST_F(MacosBackendTest, AddsImplicitFlagsForFunctionAndKeypadKeys) {
using lvh::detail::test::macos_backend_implicit_key_flags;
constexpr std::uint64_t fn = kCGEventFlagMaskSecondaryFn;
constexpr std::uint64_t numeric_pad = kCGEventFlagMaskNumericPad;

EXPECT_EQ(macos_backend_implicit_key_flags(0x25), fn | numeric_pad); // VKEY_LEFT
EXPECT_EQ(macos_backend_implicit_key_flags(0x26), fn | numeric_pad); // VKEY_UP
EXPECT_EQ(macos_backend_implicit_key_flags(0x27), fn | numeric_pad); // VKEY_RIGHT
EXPECT_EQ(macos_backend_implicit_key_flags(0x28), fn | numeric_pad); // VKEY_DOWN
EXPECT_EQ(macos_backend_implicit_key_flags(0x21), fn); // VKEY_PRIOR
EXPECT_EQ(macos_backend_implicit_key_flags(0x22), fn); // VKEY_NEXT
EXPECT_EQ(macos_backend_implicit_key_flags(0x23), fn); // VKEY_END
EXPECT_EQ(macos_backend_implicit_key_flags(0x24), fn); // VKEY_HOME
EXPECT_EQ(macos_backend_implicit_key_flags(0x2D), fn); // VKEY_INSERT
EXPECT_EQ(macos_backend_implicit_key_flags(0x2E), fn); // VKEY_DELETE
EXPECT_EQ(macos_backend_implicit_key_flags(0x70), fn); // VKEY_F1
EXPECT_EQ(macos_backend_implicit_key_flags(0x83), fn); // VKEY_F20
EXPECT_EQ(macos_backend_implicit_key_flags(0x60), numeric_pad); // VKEY_NUMPAD0
EXPECT_EQ(macos_backend_implicit_key_flags(0x6B), numeric_pad); // VKEY_ADD
EXPECT_EQ(macos_backend_implicit_key_flags(0x6E), numeric_pad); // VKEY_DECIMAL
EXPECT_EQ(macos_backend_implicit_key_flags(0x0D), 0U); // VKEY_RETURN
EXPECT_EQ(macos_backend_implicit_key_flags(0x41), 0U); // VKEY_A
EXPECT_EQ(macos_backend_implicit_key_flags(0xA2), 0U); // VKEY_LCONTROL
EXPECT_EQ(macos_backend_implicit_key_flags(0x13), 0U); // unmapped VKEY_PAUSE
EXPECT_EQ(macos_backend_implicit_key_flags(0xFFFF), 0U);
}

TEST_F(MacosBackendTest, BuildsControlArrowEventsWithFnFlags) {
using lvh::detail::test::macos_backend_key_events;
constexpr std::uint64_t control = kCGEventFlagMaskControl;
constexpr std::uint64_t fn = kCGEventFlagMaskSecondaryFn;
constexpr std::uint64_t numeric_pad = kCGEventFlagMaskNumericPad;
constexpr std::uint64_t checked = kCGEventFlagMaskShift | kCGEventFlagMaskControl | kCGEventFlagMaskAlternate |
kCGEventFlagMaskCommand | fn | numeric_pad;

// Control+Up as a client sends it, then Control+A for contrast.
const auto events = macos_backend_key_events({
{0xA2, true}, // VKEY_LCONTROL down
{0x26, true}, // VKEY_UP down
{0x26, false}, // VKEY_UP up
{0x41, true}, // VKEY_A down
{0x41, false}, // VKEY_A up
{0xA2, false}, // VKEY_LCONTROL up
});
ASSERT_EQ(events.size(), 6U);

EXPECT_EQ(events[0].event_type, kCGEventFlagsChanged);
EXPECT_EQ(events[0].flags & checked, control);

// The arrow event carries the flags a physical arrow key reports, on top of the held Control.
EXPECT_EQ(events[1].event_type, kCGEventKeyDown);
EXPECT_EQ(events[1].key_code, kVK_UpArrow);
EXPECT_EQ(events[1].flags & checked, control | fn | numeric_pad);
EXPECT_EQ(events[2].event_type, kCGEventKeyUp);
EXPECT_EQ(events[2].flags & checked, control | fn | numeric_pad);

// The per-key flags never enter the shared modifier state, so Control+A is unchanged.
EXPECT_EQ(events[1].tracked_flags & checked, control);
EXPECT_EQ(events[2].tracked_flags & checked, control);
EXPECT_EQ(events[3].event_type, kCGEventKeyDown);
EXPECT_EQ(events[3].key_code, kVK_ANSI_A);
EXPECT_EQ(events[3].flags & checked, control);
EXPECT_EQ(events[4].flags & checked, control);

EXPECT_EQ(events[5].event_type, kCGEventFlagsChanged);
EXPECT_EQ(events[5].flags & checked, 0U);
EXPECT_EQ(events[5].tracked_flags & checked, 0U);
}

TEST_F(MacosBackendTest, BuildsKeypadEventsWithNumericPadFlag) {
using lvh::detail::test::macos_backend_key_events;
constexpr std::uint64_t fn = kCGEventFlagMaskSecondaryFn;
constexpr std::uint64_t numeric_pad = kCGEventFlagMaskNumericPad;

const auto events = macos_backend_key_events({
{0x67, true}, // VKEY_NUMPAD7 down
{0x0D, true}, // VKEY_RETURN down
});
ASSERT_EQ(events.size(), 2U);
EXPECT_EQ(events[0].key_code, kVK_ANSI_Keypad7);
EXPECT_EQ(events[0].flags & (fn | numeric_pad), numeric_pad);
EXPECT_EQ(events[1].key_code, kVK_Return);
EXPECT_EQ(events[1].flags & (fn | numeric_pad), 0U);
}

TEST_F(MacosBackendTest, ConvertsScrollSettings) {
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_lines_per_detent(0.0), 1);
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_lines_per_detent(0.3125), 5);
Expand Down