Skip to content

Commit 0fcb5d8

Browse files
feat(macOS): add UTF-8 keyboard text input (#98)
1 parent 1c7d6bc commit 0fcb5d8

6 files changed

Lines changed: 56 additions & 5 deletions

File tree

docs/platform-support.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,8 @@ Current macOS capabilities:
322322

323323
- Keyboard key press and release using the existing Windows-style portable key
324324
codes.
325+
- UTF-8 keyboard text input, converted to the UTF-16 strings expected by
326+
CoreGraphics keyboard events.
325327
- Mouse relative movement, absolute movement on the main display, left/middle/
326328
right button transitions, and pixel-based vertical/horizontal scroll.
327329
- Shared keyboard modifier state on mouse events, so combinations such as
@@ -331,7 +333,6 @@ Unsupported macOS capabilities currently return `unsupported_profile`:
331333

332334
- Gamepad devices and output reports.
333335
- Touchscreen, trackpad, and pen tablet devices.
334-
- Keyboard text input through `KeyboardTextEvent`.
335336

336337
Native macOS virtual-HID gamepad support is planned. A future backend may use
337338
`IOHIDUserDevice`, DriverKit/HIDDriverKit, or a combination that preserves the

src/platform/macos/macos_backend.cpp

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <memory>
1313
#include <mutex>
1414
#include <optional>
15+
#include <vector>
1516

1617
// platform includes
1718
#include <ApplicationServices/ApplicationServices.h>
@@ -508,7 +509,47 @@ namespace lvh::detail {
508509
return OperationStatus::success();
509510
}
510511

511-
return OperationStatus::failure(unsupported_profile, "macOS keyboard text input is not implemented");
512+
if (!state_->keyboard_source) {
513+
return OperationStatus::failure(backend_failure, "macOS keyboard event source is unavailable");
514+
}
515+
516+
const auto text = CFStringCreateWithBytes(
517+
kCFAllocatorDefault,
518+
reinterpret_cast<const UInt8 *>(event.text.data()),
519+
static_cast<CFIndex>(event.text.size()),
520+
kCFStringEncodingUTF8,
521+
false
522+
);
523+
if (!text) {
524+
return OperationStatus::failure(invalid_argument, "convert UTF-8 text for macOS keyboard input");
525+
}
526+
527+
const auto length = CFStringGetLength(text);
528+
std::vector<UniChar> characters(static_cast<std::size_t>(length));
529+
CFStringGetCharacters(text, CFRangeMake(0, length), characters.data());
530+
CFRelease(text);
531+
532+
const auto key_down = CGEventCreateKeyboardEvent(state_->keyboard_source, 0, true);
533+
if (!key_down) {
534+
return OperationStatus::failure(backend_failure, "create macOS keyboard text key-down event");
535+
}
536+
537+
const auto key_up = CGEventCreateKeyboardEvent(state_->keyboard_source, 0, false);
538+
if (!key_up) {
539+
CFRelease(key_down);
540+
return OperationStatus::failure(backend_failure, "create macOS keyboard text key-up event");
541+
}
542+
543+
std::lock_guard lock {state_->keyboard_mutex};
544+
CGEventKeyboardSetUnicodeString(key_down, characters.size(), characters.data());
545+
CGEventKeyboardSetUnicodeString(key_up, characters.size(), characters.data());
546+
CGEventSetFlags(key_down, state_->keyboard_flags);
547+
CGEventSetFlags(key_up, state_->keyboard_flags);
548+
CGEventPost(kCGSessionEventTap, key_down);
549+
CGEventPost(kCGSessionEventTap, key_up);
550+
CFRelease(key_down);
551+
CFRelease(key_up);
552+
return OperationStatus::success();
512553
}
513554

514555
OperationStatus close() override {

tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ namespace lvh::detail::test {
3636
BackendCapabilities capabilities; ///< Backend capabilities reported by the macOS backend.
3737
OperationStatus keyboard_create_status; ///< Keyboard creation status.
3838
OperationStatus keyboard_text_status; ///< Non-empty keyboard text submit status.
39+
OperationStatus keyboard_empty_text_status; ///< Empty keyboard text submit status.
40+
OperationStatus keyboard_invalid_text_status; ///< Invalid UTF-8 keyboard text submit status.
3941
OperationStatus keyboard_close_status; ///< Keyboard close status.
4042
OperationStatus keyboard_submit_after_close_status; ///< Keyboard submit status after close.
43+
OperationStatus keyboard_text_after_close_status; ///< Keyboard text submit status after close.
4144
OperationStatus keyboard_invalid_profile_status; ///< Keyboard creation status for a non-keyboard profile.
4245
OperationStatus mouse_create_status; ///< Mouse creation status.
4346
OperationStatus mouse_close_status; ///< Mouse close status.

tests/fixtures/macos_backend_test_hooks.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ namespace lvh::detail::test {
7575
auto keyboard = backend->create_keyboard(1, keyboard_options);
7676
result.keyboard_create_status = keyboard.status;
7777
if (keyboard) {
78-
result.keyboard_text_status = keyboard.keyboard->type_text({.text = "A"});
78+
result.keyboard_text_status = keyboard.keyboard->type_text({.text = "Text \x{E2}\x{98}\x{80} \x{F0}\x{9F}\x{98}\x{80}"});
79+
result.keyboard_empty_text_status = keyboard.keyboard->type_text({.text = ""});
80+
result.keyboard_invalid_text_status = keyboard.keyboard->type_text({.text = std::string(1U, static_cast<char>(0xFF))});
7981
result.keyboard_close_status = keyboard.keyboard->close();
8082
result.keyboard_submit_after_close_status = keyboard.keyboard->submit({.key_code = 0x41, .pressed = true});
83+
result.keyboard_text_after_close_status = keyboard.keyboard->type_text({.text = "A"});
8184
}
8285

8386
CreateKeyboardOptions invalid_keyboard_options;

tests/unit/test_macos_backend.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,12 @@ TEST_F(MacosBackendTest, ReportsCapabilitiesAndUnsupportedDevices) {
139139
EXPECT_FALSE(result.capabilities.requires_installed_driver);
140140

141141
ASSERT_TRUE(result.keyboard_create_status.ok()) << result.keyboard_create_status.message();
142-
EXPECT_EQ(result.keyboard_text_status.code(), lvh::ErrorCode::unsupported_profile);
142+
EXPECT_TRUE(result.keyboard_text_status.ok()) << result.keyboard_text_status.message();
143+
EXPECT_TRUE(result.keyboard_empty_text_status.ok()) << result.keyboard_empty_text_status.message();
144+
EXPECT_EQ(result.keyboard_invalid_text_status.code(), lvh::ErrorCode::invalid_argument);
143145
ASSERT_TRUE(result.keyboard_close_status.ok()) << result.keyboard_close_status.message();
144146
EXPECT_EQ(result.keyboard_submit_after_close_status.code(), lvh::ErrorCode::device_closed);
147+
EXPECT_EQ(result.keyboard_text_after_close_status.code(), lvh::ErrorCode::device_closed);
145148
EXPECT_EQ(result.keyboard_invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile);
146149

147150
ASSERT_TRUE(result.mouse_create_status.ok()) << result.mouse_create_status.message();

tests/unit/test_runtime.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) {
130130

131131
auto keyboard = runtime->create_keyboard();
132132
ASSERT_TRUE(keyboard) << keyboard.status.message();
133-
EXPECT_EQ(keyboard.keyboard->type_text({.text = "A"}).code(), lvh::ErrorCode::unsupported_profile);
133+
EXPECT_TRUE(keyboard.keyboard->type_text({.text = "A"}).ok());
134134
EXPECT_TRUE(keyboard.keyboard->close().ok());
135135

136136
auto mouse = runtime->create_mouse();

0 commit comments

Comments
 (0)