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
3 changes: 2 additions & 1 deletion docs/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
43 changes: 42 additions & 1 deletion src/platform/macos/macos_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <memory>
#include <mutex>
#include <optional>
#include <vector>

// platform includes
#include <ApplicationServices/ApplicationServices.h>
Expand Down Expand Up @@ -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<const UInt8 *>(event.text.data()),
static_cast<CFIndex>(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<UniChar> characters(static_cast<std::size_t>(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 {
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion tests/fixtures/macos_backend_test_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>(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;
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/test_macos_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading