From f814c4377f1dc1f4aabc91eedd468f9a869ee7df Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:48:31 -0400 Subject: [PATCH] feat(macOS): add UTF-8 keyboard text input Implement KeyboardTextEvent handling in the macOS backend by converting UTF-8 text to UTF-16 and posting Unicode key-down/key-up CoreGraphics events with current modifier flags. Update platform support docs to mark keyboard text input as supported, and expand macOS backend tests/fixtures to cover successful text input, empty input, invalid UTF-8 rejection, and behavior after device close. --- docs/platform-support.md | 3 +- src/platform/macos/macos_backend.cpp | 43 ++++++++++++++++++- .../fixtures/macos_backend_test_hooks.hpp | 3 ++ tests/fixtures/macos_backend_test_hooks.cpp | 5 ++- tests/unit/test_macos_backend.cpp | 5 ++- tests/unit/test_runtime.cpp | 2 +- 6 files changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/platform-support.md b/docs/platform-support.md index d12c7eb..e9d6ed4 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -314,6 +314,8 @@ Current macOS capabilities: - Keyboard key press and release using the existing Windows-style portable key codes. +- UTF-8 keyboard text input, converted to the UTF-16 strings expected by + CoreGraphics keyboard events. - Mouse relative movement, absolute movement on the main display, left/middle/ right button transitions, and pixel-based vertical/horizontal scroll. - Shared keyboard modifier state on mouse events, so combinations such as @@ -323,7 +325,6 @@ Unsupported macOS capabilities currently return `unsupported_profile`: - Gamepad devices and output reports. - Touchscreen, trackpad, and pen tablet devices. -- Keyboard text input through `KeyboardTextEvent`. Native macOS virtual-HID gamepad support is planned. A future backend may use `IOHIDUserDevice`, DriverKit/HIDDriverKit, or a combination that preserves the diff --git a/src/platform/macos/macos_backend.cpp b/src/platform/macos/macos_backend.cpp index b393852..c67ca89 100644 --- a/src/platform/macos/macos_backend.cpp +++ b/src/platform/macos/macos_backend.cpp @@ -12,6 +12,7 @@ #include #include #include +#include // platform includes #include @@ -508,7 +509,47 @@ namespace lvh::detail { return OperationStatus::success(); } - return OperationStatus::failure(unsupported_profile, "macOS keyboard text input is not implemented"); + if (!state_->keyboard_source) { + return OperationStatus::failure(backend_failure, "macOS keyboard event source is unavailable"); + } + + const auto text = CFStringCreateWithBytes( + kCFAllocatorDefault, + reinterpret_cast(event.text.data()), + static_cast(event.text.size()), + kCFStringEncodingUTF8, + false + ); + if (!text) { + return OperationStatus::failure(invalid_argument, "convert UTF-8 text for macOS keyboard input"); + } + + const auto length = CFStringGetLength(text); + std::vector characters(static_cast(length)); + CFStringGetCharacters(text, CFRangeMake(0, length), characters.data()); + CFRelease(text); + + const auto key_down = CGEventCreateKeyboardEvent(state_->keyboard_source, 0, true); + if (!key_down) { + return OperationStatus::failure(backend_failure, "create macOS keyboard text key-down event"); + } + + const auto key_up = CGEventCreateKeyboardEvent(state_->keyboard_source, 0, false); + if (!key_up) { + CFRelease(key_down); + return OperationStatus::failure(backend_failure, "create macOS keyboard text key-up event"); + } + + std::lock_guard lock {state_->keyboard_mutex}; + CGEventKeyboardSetUnicodeString(key_down, characters.size(), characters.data()); + CGEventKeyboardSetUnicodeString(key_up, characters.size(), characters.data()); + CGEventSetFlags(key_down, state_->keyboard_flags); + CGEventSetFlags(key_up, state_->keyboard_flags); + CGEventPost(kCGSessionEventTap, key_down); + CGEventPost(kCGSessionEventTap, key_up); + CFRelease(key_down); + CFRelease(key_up); + return OperationStatus::success(); } OperationStatus close() override { diff --git a/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp index ed1dc06..a67966b 100644 --- a/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp @@ -36,8 +36,11 @@ namespace lvh::detail::test { BackendCapabilities capabilities; ///< Backend capabilities reported by the macOS backend. OperationStatus keyboard_create_status; ///< Keyboard creation status. OperationStatus keyboard_text_status; ///< Non-empty keyboard text submit status. + OperationStatus keyboard_empty_text_status; ///< Empty keyboard text submit status. + OperationStatus keyboard_invalid_text_status; ///< Invalid UTF-8 keyboard text submit status. OperationStatus keyboard_close_status; ///< Keyboard close status. OperationStatus keyboard_submit_after_close_status; ///< Keyboard submit status after close. + OperationStatus keyboard_text_after_close_status; ///< Keyboard text submit status after close. OperationStatus keyboard_invalid_profile_status; ///< Keyboard creation status for a non-keyboard profile. OperationStatus mouse_create_status; ///< Mouse creation status. OperationStatus mouse_close_status; ///< Mouse close status. diff --git a/tests/fixtures/macos_backend_test_hooks.cpp b/tests/fixtures/macos_backend_test_hooks.cpp index 58dbd73..709e346 100644 --- a/tests/fixtures/macos_backend_test_hooks.cpp +++ b/tests/fixtures/macos_backend_test_hooks.cpp @@ -75,9 +75,12 @@ namespace lvh::detail::test { auto keyboard = backend->create_keyboard(1, keyboard_options); result.keyboard_create_status = keyboard.status; if (keyboard) { - result.keyboard_text_status = keyboard.keyboard->type_text({.text = "A"}); + result.keyboard_text_status = keyboard.keyboard->type_text({.text = "Text \x{E2}\x{98}\x{80} \x{F0}\x{9F}\x{98}\x{80}"}); + result.keyboard_empty_text_status = keyboard.keyboard->type_text({.text = ""}); + result.keyboard_invalid_text_status = keyboard.keyboard->type_text({.text = std::string(1U, static_cast(0xFF))}); result.keyboard_close_status = keyboard.keyboard->close(); result.keyboard_submit_after_close_status = keyboard.keyboard->submit({.key_code = 0x41, .pressed = true}); + result.keyboard_text_after_close_status = keyboard.keyboard->type_text({.text = "A"}); } CreateKeyboardOptions invalid_keyboard_options; diff --git a/tests/unit/test_macos_backend.cpp b/tests/unit/test_macos_backend.cpp index 986e81d..19a37ac 100644 --- a/tests/unit/test_macos_backend.cpp +++ b/tests/unit/test_macos_backend.cpp @@ -139,9 +139,12 @@ TEST_F(MacosBackendTest, ReportsCapabilitiesAndUnsupportedDevices) { EXPECT_FALSE(result.capabilities.requires_installed_driver); ASSERT_TRUE(result.keyboard_create_status.ok()) << result.keyboard_create_status.message(); - EXPECT_EQ(result.keyboard_text_status.code(), lvh::ErrorCode::unsupported_profile); + EXPECT_TRUE(result.keyboard_text_status.ok()) << result.keyboard_text_status.message(); + EXPECT_TRUE(result.keyboard_empty_text_status.ok()) << result.keyboard_empty_text_status.message(); + EXPECT_EQ(result.keyboard_invalid_text_status.code(), lvh::ErrorCode::invalid_argument); ASSERT_TRUE(result.keyboard_close_status.ok()) << result.keyboard_close_status.message(); EXPECT_EQ(result.keyboard_submit_after_close_status.code(), lvh::ErrorCode::device_closed); + EXPECT_EQ(result.keyboard_text_after_close_status.code(), lvh::ErrorCode::device_closed); EXPECT_EQ(result.keyboard_invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile); ASSERT_TRUE(result.mouse_create_status.ok()) << result.mouse_create_status.message(); diff --git a/tests/unit/test_runtime.cpp b/tests/unit/test_runtime.cpp index 2476133..38aa924 100644 --- a/tests/unit/test_runtime.cpp +++ b/tests/unit/test_runtime.cpp @@ -130,7 +130,7 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) { auto keyboard = runtime->create_keyboard(); ASSERT_TRUE(keyboard) << keyboard.status.message(); - EXPECT_EQ(keyboard.keyboard->type_text({.text = "A"}).code(), lvh::ErrorCode::unsupported_profile); + EXPECT_TRUE(keyboard.keyboard->type_text({.text = "A"}).ok()); EXPECT_TRUE(keyboard.keyboard->close().ok()); auto mouse = runtime->create_mouse();