From 154c76278d4ab27a3dce5739a3d4af1a9e42f0e4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 9 May 2026 21:44:32 +0800 Subject: [PATCH] feat(macOS): keyboard type Signed-off-by: fufesou --- Cargo.toml | 1 + examples/simulate_keyboard_type.rs | 165 +++++ src/codes_conv.rs | 2 +- src/lib.rs | 26 + src/macos/keyboard.rs | 188 +++--- src/macos/simulate.rs | 979 ++++++++++++++++++++++++++--- 6 files changed, 1194 insertions(+), 167 deletions(-) create mode 100644 examples/simulate_keyboard_type.rs diff --git a/Cargo.toml b/Cargo.toml index f0c35050..d868813d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ core-foundation = {version = "0.9.3"} core-foundation-sys = {version = "0.8.3"} core-graphics = {version = "0.22.3", features = ["highsierra"]} dispatch = "0.2" +foreign-types = "0.3.2" [target.'cfg(target_os = "linux")'.dependencies] epoll = {version = "4.1.0"} diff --git a/examples/simulate_keyboard_type.rs b/examples/simulate_keyboard_type.rs new file mode 100644 index 00000000..c8835e2a --- /dev/null +++ b/examples/simulate_keyboard_type.rs @@ -0,0 +1,165 @@ +#[cfg(target_os = "macos")] +use rdev::EventType; +#[cfg(target_os = "macos")] +use rdev::MacKeyboardType; +#[cfg(target_os = "macos")] +use std::{env, thread, time}; + +#[cfg(target_os = "macos")] +const EVENT_DELAY_MS: u64 = 20; + +#[cfg(target_os = "macos")] +const FOCUS_DELAY_SECS: u64 = 2; +#[cfg(target_os = "macos")] +const SEND_CONFIRM_ARG: &str = "--send"; + +#[cfg(target_os = "macos")] +struct MacKeyboardSample { + name: &'static str, + keycodes: &'static [rdev::CGKeyCode], +} + +#[cfg(target_os = "macos")] +enum SampleError { + CreateVirtualInput, + SendInput, +} + +#[cfg(target_os = "macos")] +fn mac_key(keycode: rdev::CGKeyCode) -> rdev::Key { + rdev::Key::RawKey(rdev::RawKey::MacVirtualKeycode(keycode)) +} + +#[cfg(target_os = "macos")] +fn send_with_input(input: &rdev::VirtualInput, event_type: &EventType) -> Result<(), SampleError> { + let delay = time::Duration::from_millis(EVENT_DELAY_MS); + input + .simulate(event_type) + .map_err(|_| SampleError::SendInput)?; + thread::sleep(delay); + Ok(()) +} + +#[cfg(target_os = "macos")] +fn send_key_sequence( + input: &rdev::VirtualInput, + keycodes: &[rdev::CGKeyCode], +) -> Result<(), SampleError> { + let mut pressed = Vec::with_capacity(keycodes.len()); + for keycode in keycodes { + if let Err(error) = send_with_input(input, &EventType::KeyPress(mac_key(*keycode))) { + release_pressed_keys(input, &pressed); + return Err(error); + } + pressed.push(*keycode); + } + let mut release_error = None; + for keycode in pressed.iter().rev() { + if let Err(error) = send_with_input(input, &EventType::KeyRelease(mac_key(*keycode))) { + release_error = Some(error); + } + } + if let Some(error) = release_error { + return Err(error); + } + Ok(()) +} + +#[cfg(target_os = "macos")] +fn release_pressed_keys(input: &rdev::VirtualInput, keycodes: &[rdev::CGKeyCode]) { + for keycode in keycodes.iter().rev() { + let _ = send_with_input(input, &EventType::KeyRelease(mac_key(*keycode))); + } +} + +#[cfg(target_os = "macos")] +fn send_keyboard_type_samples( + keyboard_type_name: &str, + keyboard_type: MacKeyboardType, + samples: &[MacKeyboardSample], +) -> Result<(), SampleError> { + let Ok(virtual_input) = rdev::VirtualInput::new( + rdev::CGEventSourceStateID::HIDSystemState, + rdev::CGEventTapLocation::HID, + ) else { + return Err(SampleError::CreateVirtualInput); + }; + let virtual_input = virtual_input.with_keyboard_type(keyboard_type); + + println!("Sending {} keyboard samples:", keyboard_type_name); + for sample in samples { + println!(" {}", sample.name); + send_key_sequence(&virtual_input, sample.keycodes)?; + send_key_sequence(&virtual_input, &[rdev::kVK_Space])?; + } + send_key_sequence(&virtual_input, &[rdev::kVK_Return]) +} + +#[cfg(target_os = "macos")] +fn test_macos_keys() { + if !env::args().any(|arg| arg == SEND_CONFIRM_ARG) { + println!( + "This example sends real key events to the focused application. Run with {} to continue.", + SEND_CONFIRM_ARG + ); + return; + } + + let samples = [ + MacKeyboardSample { + name: "Shift+2", + keycodes: &[rdev::kVK_Shift, rdev::kVK_ANSI_2], + }, + MacKeyboardSample { + name: "ANSI backslash", + keycodes: &[rdev::kVK_ANSI_Backslash], + }, + MacKeyboardSample { + name: "ISO section", + keycodes: &[rdev::kVK_ISO_Section], + }, + MacKeyboardSample { + name: "JIS yen", + keycodes: &[rdev::kVK_JIS_Yen], + }, + ]; + + println!( + "Focus a text field. Sending ANSI, ISO, and JIS samples in {} seconds.", + FOCUS_DELAY_SECS + ); + println!("Each row uses: Shift+2, ANSI backslash, ISO section, JIS yen."); + thread::sleep(time::Duration::from_secs(FOCUS_DELAY_SECS)); + + for (keyboard_type_name, keyboard_type) in [ + ("ANSI", MacKeyboardType::Ansi), + ("ISO", MacKeyboardType::Iso), + ("JIS", MacKeyboardType::Jis), + ] { + if let Err(error) = send_keyboard_type_samples(keyboard_type_name, keyboard_type, &samples) + { + match error { + SampleError::CreateVirtualInput => { + println!( + "Failed to create VirtualInput for {} keyboard samples", + keyboard_type_name + ); + } + SampleError::SendInput => { + println!("Failed to send {} keyboard samples", keyboard_type_name); + } + } + return; + } + } +} + +#[cfg(target_os = "macos")] +fn main() { + test_macos_keys(); +} + +#[cfg(not(target_os = "macos"))] +fn main() { + println!("This example is only implemented for MacOS."); +} diff --git a/src/codes_conv.rs b/src/codes_conv.rs index 8e1367e3..6b03e3ef 100644 --- a/src/codes_conv.rs +++ b/src/codes_conv.rs @@ -155,7 +155,7 @@ mod test { continue; } if let Some(code2) = super::usb_hid_code_to_macos_code(usb_hid) { - assert_eq!(code, code2 as u32) + assert_eq!(u32::from(code), code2 as u32) } else { assert!(false, "We could not convert back code: {:?}", code); } diff --git a/src/lib.rs b/src/lib.rs index 4832560a..60adaf8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -265,6 +265,32 @@ pub use crate::macos::{set_is_main_thread, Keyboard, VirtualInput}; #[cfg(target_os = "macos")] pub use core_graphics::{event::CGEventTapLocation, event_source::CGEventSourceStateID}; +#[cfg(target_os = "macos")] +/// Selects the macOS hardware keyboard type used when translating physical keycodes. +/// +/// This does not switch the current macOS input source, keyboard layout, or IME. +/// Character output is still translated with the active system input source. +/// For example, `Iso` means ISO hardware type, not a specific country layout +/// such as British, German, or French. +#[non_exhaustive] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MacKeyboardType { + /// Use the system keyboard type reported by macOS. + Current, + /// Use ANSI hardware keyboard translation. + Ansi, + /// Use ISO hardware keyboard translation. + Iso, + /// Use JIS hardware keyboard translation. + Jis, + /// Use a raw CoreGraphics keyboard type value. + /// + /// This is an opaque CoreGraphics keyboard type ID, not a stable keyboard + /// layout selector. Prefer the named variants unless the value comes from + /// CoreGraphics or another macOS system API. + Raw(u32), +} + #[cfg(any(target_os = "android", target_os = "linux"))] pub use crate::keycodes::linux::{code_from_key, key_from_code}; #[cfg(target_os = "linux")] diff --git a/src/macos/keyboard.rs b/src/macos/keyboard.rs index ff9cf5b0..c7ad7858 100644 --- a/src/macos/keyboard.rs +++ b/src/macos/keyboard.rs @@ -13,6 +13,15 @@ type TISInputSourceRef = *mut c_void; type ModifierState = u32; type UniCharCount = usize; +// UCKeyTranslate expects Carbon modifierKeyState values, encoded as +// cmdKey/shiftKey/alphaLock/optionKey/controlKey shifted right by 8. +pub(crate) const MODIFIER_STATE_NONE: ModifierState = 0; +pub(crate) const MODIFIER_STATE_COMMAND: ModifierState = 1; +pub(crate) const MODIFIER_STATE_SHIFT: ModifierState = 2; +pub(crate) const MODIFIER_STATE_CAPS_LOCK: ModifierState = 4; +pub(crate) const MODIFIER_STATE_ALT: ModifierState = 8; +pub(crate) const MODIFIER_STATE_CONTROL: ModifierState = 16; + type OptionBits = c_uint; #[allow(non_upper_case_globals)] const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; @@ -83,74 +92,27 @@ extern "C" { static kTISPropertyUnicodeKeyLayoutData: *mut c_void; } -pub struct Keyboard { - is_main_thread: bool, - dead_state: u32, - shift: bool, - alt: bool, // options - caps_lock: bool, -} - -impl Keyboard { - pub fn new() -> Option { - Some(Keyboard { - is_main_thread: true, - dead_state: 0, - shift: false, - alt: false, - caps_lock: false, - }) - } - - pub fn set_is_main_thread(&mut self, b: bool) { - self.is_main_thread = b; - } - - fn modifier_state(&self) -> ModifierState { - if self.alt && (self.shift || self.caps_lock) { - 10 - } else if self.alt && !(self.shift || self.caps_lock) { - 8 - } else if !self.alt && (self.caps_lock || self.shift) { - 2 - } else { - 0 - } - } - - #[allow(dead_code)] - #[inline] - pub(crate) unsafe fn create_unicode_for_key( - &mut self, - code: u32, - flags: CGEventFlags, - ) -> Option { - let flags_bits = flags.bits(); - if flags_bits & NSEventModifierFlagCommand != 0 - || flags_bits & NSEventModifierFlagControl != 0 - { - return None; - } - - let modifier_state = flags_to_state(flags_bits); - - if self.is_main_thread { - self.unicode_from_code(code, modifier_state) - } else { - QUEUE.exec_sync(move || { - // ignore all modifiers for name - self.unicode_from_code(code, modifier_state) - }) - } - } - - #[inline] - unsafe fn unicode_from_code( - &mut self, - code: u32, - modifier_state: ModifierState, - ) -> Option { - // let mut now = std::time::Instant::now(); +/// Translates a macOS virtual keycode through the active Unicode keyboard layout. +/// +/// The `keyboard_type` should be a CoreGraphics keyboard type ID suitable for +/// `UCKeyTranslate`, not a Carbon physical-layout FourCC. +/// +/// `dead_state` is read and updated by `UCKeyTranslate`. Pass `0` to start a +/// fresh translation stream, and reuse the same value for consecutive +/// translations on the same logical keyboard stream. +/// +/// # Safety +/// This must be called on the main thread because it calls Carbon/TIS APIs that +/// read the active keyboard layout from the current application state. Dispatch +/// to the main queue first when translating from another thread. +pub(crate) unsafe fn unicode_from_code_with_keyboard_type( + code: u32, + modifier_state: ModifierState, + keyboard_type: u32, + dead_state: &mut u32, +) -> Option { + let code = code.try_into().ok()?; + unsafe { let mut keyboard = TISCopyCurrentKeyboardInputSource(); let mut layout = std::ptr::null_mut(); if !keyboard.is_null() { @@ -188,19 +150,17 @@ impl Keyboard { } return None; } - // println!("{:?}", now.elapsed()); let mut buff = [0_u16; BUF_LEN]; - let kb_type = super::common::LMGetKbdType(); let mut length = 0; let _retval = UCKeyTranslate( layout_ptr, - code.try_into().ok()?, + code, kUCKeyActionDown, modifier_state, - kb_type as _, + keyboard_type, kUCKeyTranslateDeadKeysBit, - &mut self.dead_state, + dead_state, BUF_LEN, &mut length, &mut buff, @@ -209,7 +169,7 @@ impl Keyboard { CFRelease(keyboard); } if length == 0 { - return if self.is_dead() { + return if *dead_state != 0 { Some(UnicodeInfo { name: None, unicode: Vec::new(), @@ -220,9 +180,8 @@ impl Keyboard { }; } - // C0 controls if length == 1 { - match String::from_utf16(&buff[..length].to_vec()) { + match String::from_utf16(&buff[..length]) { Ok(s) => { if let Some(c) = s.chars().next() { if ('\u{1}'..='\u{1f}').contains(&c) { @@ -241,6 +200,83 @@ impl Keyboard { is_dead: false, }) } +} + +pub struct Keyboard { + is_main_thread: bool, + dead_state: u32, + shift: bool, + alt: bool, // options + caps_lock: bool, +} + +impl Keyboard { + pub fn new() -> Option { + Some(Keyboard { + is_main_thread: true, + dead_state: 0, + shift: false, + alt: false, + caps_lock: false, + }) + } + + pub fn set_is_main_thread(&mut self, b: bool) { + self.is_main_thread = b; + } + + fn modifier_state(&self) -> ModifierState { + let mut modifier_state = MODIFIER_STATE_NONE; + if self.shift { + modifier_state |= MODIFIER_STATE_SHIFT; + } + if self.caps_lock { + modifier_state |= MODIFIER_STATE_CAPS_LOCK; + } + if self.alt { + modifier_state |= MODIFIER_STATE_ALT; + } + modifier_state + } + + #[allow(dead_code)] + #[inline] + pub(crate) unsafe fn create_unicode_for_key( + &mut self, + code: u32, + flags: CGEventFlags, + ) -> Option { + let flags_bits = flags.bits(); + if flags_bits & NSEventModifierFlagCommand != 0 + || flags_bits & NSEventModifierFlagControl != 0 + { + return None; + } + + let modifier_state = flags_to_state(flags_bits); + + if self.is_main_thread { + self.unicode_from_code(code, modifier_state) + } else { + QUEUE.exec_sync(move || self.unicode_from_code(code, modifier_state)) + } + } + + #[inline] + unsafe fn unicode_from_code( + &mut self, + code: u32, + modifier_state: ModifierState, + ) -> Option { + unsafe { + unicode_from_code_with_keyboard_type( + code, + modifier_state, + super::common::LMGetKbdType() as u32, + &mut self.dead_state, + ) + } + } pub fn is_dead(&self) -> bool { self.dead_state != 0 diff --git a/src/macos/simulate.rs b/src/macos/simulate.rs index 12abe326..057c35b4 100644 --- a/src/macos/simulate.rs +++ b/src/macos/simulate.rs @@ -1,6 +1,11 @@ use crate::keycodes::macos::{code_from_key, virtual_keycodes::*}; use crate::macos::common::CGEventSourceKeyState; +use crate::macos::keyboard::{ + unicode_from_code_with_keyboard_type, MODIFIER_STATE_ALT, MODIFIER_STATE_CAPS_LOCK, + MODIFIER_STATE_COMMAND, MODIFIER_STATE_CONTROL, MODIFIER_STATE_NONE, MODIFIER_STATE_SHIFT, +}; use crate::rdev::{Button, EventType, RawKey, SimulateError}; +use crate::MacKeyboardType; use core_graphics::{ event::{ CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, @@ -8,18 +13,80 @@ use core_graphics::{ }, event_source::{CGEventSource, CGEventSourceStateID}, geometry::CGPoint, + sys::CGEventSourceRef, }; +use foreign_types::ForeignType; +use std::cell::Cell; use std::convert::TryInto; +use std::sync::atomic::{AtomicI64, Ordering}; + +static MOUSE_EXTRA_INFO: AtomicI64 = AtomicI64::new(0); +static KEYBOARD_EXTRA_INFO: AtomicI64 = AtomicI64::new(0); +type CGEventSourceKeyboardType = u32; +type ModifierKeyMask = u8; +// Apple defines these compatibility IDs in CarbonCore/Gestalt.h as +// gestaltThirdPartyANSIKbd/ISOKbd/JISKbd. They are the keyboard type IDs used +// by UCKeyTranslate and CGEventSourceSetKeyboardType, not HIToolbox Keyboards.h +// PhysicalKeyboardLayoutType FourCC values such as 'ANSI', 'ISO ', or 'JIS '. +const ANSI_KEYBOARD_TYPE: CGEventSourceKeyboardType = 40; +const ISO_KEYBOARD_TYPE: CGEventSourceKeyboardType = 41; +const JIS_KEYBOARD_TYPE: CGEventSourceKeyboardType = 42; +const MODIFIER_KEY_NONE: ModifierKeyMask = 0; +const MODIFIER_KEY_LEFT: ModifierKeyMask = 1 << 0; +const MODIFIER_KEY_RIGHT: ModifierKeyMask = 1 << 1; + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGEventSourceGetKeyboardType(source: CGEventSourceRef) -> CGEventSourceKeyboardType; + fn CGEventSourceSetKeyboardType( + source: CGEventSourceRef, + keyboard_type: CGEventSourceKeyboardType, + ); +} -static mut MOUSE_EXTRA_INFO: i64 = 0; -static mut KEYBOARD_EXTRA_INFO: i64 = 0; +#[derive(Copy, Clone)] +struct KeyboardEventOptions { + keydown: bool, + keyboard_type: CGEventSourceKeyboardType, +} + +#[derive(Copy, Clone)] +enum ModifierKey { + Shift(ModifierSide), + Alt(ModifierSide), + CapsLock, + Control(ModifierSide), + Command(ModifierSide), +} +#[derive(Copy, Clone)] +enum ModifierSide { + Left, + Right, +} + +fn keyboard_type_value(keyboard_type: MacKeyboardType) -> CGEventSourceKeyboardType { + match keyboard_type { + MacKeyboardType::Current => current_keyboard_type(), + MacKeyboardType::Ansi => ANSI_KEYBOARD_TYPE, + MacKeyboardType::Iso => ISO_KEYBOARD_TYPE, + MacKeyboardType::Jis => JIS_KEYBOARD_TYPE, + MacKeyboardType::Raw(keyboard_type) => keyboard_type, + } +} + +/// Sets the macOS user-data tag used for simulated mouse and wheel events. +/// +/// Keyboard events use `set_keyboard_extra_info()` instead. pub fn set_mouse_extra_info(extra: i64) { - unsafe { MOUSE_EXTRA_INFO = extra } + MOUSE_EXTRA_INFO.store(extra, Ordering::Relaxed); } +/// Sets the macOS user-data tag used for simulated keyboard events. +/// +/// This is separate from `set_mouse_extra_info()`. pub fn set_keyboard_extra_info(extra: i64) { - unsafe { KEYBOARD_EXTRA_INFO = extra } + KEYBOARD_EXTRA_INFO.store(extra, Ordering::Relaxed); } #[allow(non_upper_case_globals)] @@ -62,101 +129,259 @@ fn workaround_fn(event: CGEvent, keycode: CGKeyCode) -> CGEvent { event } -unsafe fn convert_native_with_source( - event_type: &EventType, +fn current_keyboard_type() -> CGEventSourceKeyboardType { + unsafe { CGEventSourceGetKeyboardType(std::ptr::null_mut()) } +} + +unsafe fn set_keyboard_source_type( + source: &CGEventSource, + keyboard_type: CGEventSourceKeyboardType, +) { + // The source keeps this keyboard type for later events created from it. + // Callers must ensure the source is not being mutated concurrently. + CGEventSourceSetKeyboardType(source.as_ptr(), keyboard_type); +} + +fn set_keyboard_event_type(event: &CGEvent, keyboard_type: CGEventSourceKeyboardType) { + event.set_integer_value_field( + EventField::KEYBOARD_EVENT_KEYBOARD_TYPE, + keyboard_type as i64, + ); +} + +fn keycode_from_key(key: crate::Key) -> Option { + match key { + crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode)) => Some(keycode as _), + crate::Key::RawKey(_) => { + // Only macOS virtual keycodes can be converted into CG key events. + None + } + _ => code_from_key(key).map(|keycode| keycode as _), + } +} + +fn modifier_key_from_key(key: crate::Key) -> Option { + match key { + crate::Key::ShiftLeft => Some(ModifierKey::Shift(ModifierSide::Left)), + crate::Key::ShiftRight => Some(ModifierKey::Shift(ModifierSide::Right)), + crate::Key::Alt => Some(ModifierKey::Alt(ModifierSide::Left)), + crate::Key::AltGr => Some(ModifierKey::Alt(ModifierSide::Right)), + crate::Key::CapsLock => Some(ModifierKey::CapsLock), + crate::Key::ControlLeft => Some(ModifierKey::Control(ModifierSide::Left)), + crate::Key::ControlRight => Some(ModifierKey::Control(ModifierSide::Right)), + crate::Key::MetaLeft => Some(ModifierKey::Command(ModifierSide::Left)), + crate::Key::MetaRight => Some(ModifierKey::Command(ModifierSide::Right)), + crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode)) => match keycode { + code if code == kVK_Shift => Some(ModifierKey::Shift(ModifierSide::Left)), + code if code == kVK_RightShift => Some(ModifierKey::Shift(ModifierSide::Right)), + code if code == kVK_Option => Some(ModifierKey::Alt(ModifierSide::Left)), + code if code == kVK_RightOption => Some(ModifierKey::Alt(ModifierSide::Right)), + code if code == kVK_CapsLock => Some(ModifierKey::CapsLock), + code if code == kVK_Control => Some(ModifierKey::Control(ModifierSide::Left)), + code if code == kVK_RightControl => Some(ModifierKey::Control(ModifierSide::Right)), + code if code == kVK_Command => Some(ModifierKey::Command(ModifierSide::Left)), + code if code == kVK_RightCommand => Some(ModifierKey::Command(ModifierSide::Right)), + _ => None, + }, + _ => None, + } +} + +fn modifier_key_mask(side: ModifierSide) -> ModifierKeyMask { + match side { + ModifierSide::Left => MODIFIER_KEY_LEFT, + ModifierSide::Right => MODIFIER_KEY_RIGHT, + } +} + +fn set_modifier_key_state(state: &Cell, side: ModifierSide, keydown: bool) { + let mask = modifier_key_mask(side); + let current = state.get(); + if keydown { + state.set(current | mask); + } else { + state.set(current & !mask); + } +} + +fn modifier_suppresses_unicode(modifier_state: u32) -> bool { + modifier_state & (MODIFIER_STATE_COMMAND | MODIFIER_STATE_CONTROL) != 0 +} + +fn should_translate_key_unicode(key: crate::Key, modifier_state: u32) -> bool { + modifier_key_from_key(key).is_none() && !modifier_suppresses_unicode(modifier_state) +} + +fn new_keyboard_event( source: CGEventSource, + keycode: CGKeyCode, + options: KeyboardEventOptions, +) -> Result { + let event = CGEvent::new_keyboard_event(source, keycode, options.keydown)?; + set_keyboard_event_type(&event, options.keyboard_type); + Ok(event) +} + +fn keyboard_event_from_key( + source: CGEventSource, + key: crate::Key, + options: KeyboardEventOptions, + modifier_state: Option, + dead_state: Option<&Cell>, ) -> Option { - match event_type { - EventType::KeyPress(key) => match key { - crate::Key::RawKey(rawkey) => { - if let RawKey::MacVirtualKeycode(keycode) = rawkey { - CGEvent::new_keyboard_event(source, *keycode as _, true) - // Don't use `workaround_fn()` for `KeyPress`, or `F11` will not work. - // .and_then(|event| Ok(workaround_fn(event, *keycode))) - .ok() - } else { - None - } - } - _ => { - let code = code_from_key(*key)?; - CGEvent::new_keyboard_event(source, code as _, true) - // Don't use `workaround_fn()` for `KeyPress`, or `F11` will not work. - // .and_then(|event| Ok(workaround_fn(event, code as _))) - .ok() - } - }, - EventType::KeyRelease(key) => match key { - crate::Key::RawKey(rawkey) => { - if let RawKey::MacVirtualKeycode(keycode) = rawkey { - CGEvent::new_keyboard_event(source, *keycode as _, false) - .and_then(|event| Ok(workaround_fn(event, *keycode))) - .ok() - } else { - None + let keycode = keycode_from_key(key)?; + let event = new_keyboard_event(source, keycode, options).ok()?; + if options.keydown { + if let (Some(modifier_state), Some(dead_state)) = (modifier_state, dead_state) { + if should_translate_key_unicode(key, modifier_state) { + let mut dead_state_value = dead_state.get(); + let unicode = unsafe { + unicode_from_code_with_keyboard_type( + keycode as u32, + modifier_state, + options.keyboard_type, + &mut dead_state_value, + ) + }; + dead_state.set(dead_state_value); + if let Some(unicode) = unicode { + if !unicode.unicode.is_empty() { + event.set_string_from_utf16_unchecked(&unicode.unicode); + } } + } else if modifier_key_from_key(key).is_none() + && modifier_suppresses_unicode(modifier_state) + { + dead_state.set(0); } - _ => { - let code = code_from_key(*key)?; - CGEvent::new_keyboard_event(source, code as _, false) - .and_then(|event| Ok(workaround_fn(event, code as _))) - .ok() + } + // KeyPress intentionally skips workaround_fn(); applying it here makes F11 fail. + return Some(event); + } + Some(workaround_fn(event, keycode)) +} + +fn mouse_button_event( + source: CGEventSource, + button: Button, + event_type: CGEventType, +) -> Option { + let point = unsafe { get_current_mouse_location()? }; + match button { + Button::Left | Button::Right => CGEvent::new_mouse_event( + source, + event_type, + point, + CGMouseButton::Left, // ignored because we don't use OtherMouse EventType + ) + .ok(), + _ => None, + } +} + +fn mouse_move_event(source: CGEventSource, x: f64, y: f64) -> Option { + let point = CGPoint { x, y }; + CGEvent::new_mouse_event(source, CGEventType::MouseMoved, point, CGMouseButton::Left).ok() +} + +fn wheel_event(source: CGEventSource, delta_x: i64, delta_y: i64) -> Option { + let wheel_count = 2; + CGEvent::new_scroll_event( + source, + ScrollEventUnit::PIXEL, + wheel_count, + delta_y.try_into().ok()?, + delta_x.try_into().ok()?, + 0, + ) + .ok() +} + +fn event_source_user_data(event_type: &EventType) -> i64 { + match event_type { + EventType::KeyPress(_) | EventType::KeyRelease(_) => { + KEYBOARD_EXTRA_INFO.load(Ordering::Relaxed) + } + EventType::ButtonPress(_) + | EventType::ButtonRelease(_) + | EventType::MouseMove { .. } + | EventType::Wheel { .. } => { + // Wheel events are pointer events and intentionally use mouse extra info. + MOUSE_EXTRA_INFO.load(Ordering::Relaxed) + } + } +} + +fn set_event_source_user_data(event: &CGEvent, event_type: &EventType) { + event.set_integer_value_field( + EventField::EVENT_SOURCE_USER_DATA, + event_source_user_data(event_type), + ); +} + +unsafe fn convert_native_with_source( + event_type: &EventType, + source: &CGEventSource, + keyboard_type: CGEventSourceKeyboardType, + modifier_state: Option, + dead_state: Option<&Cell>, +) -> Option { + match event_type { + EventType::KeyPress(_) | EventType::KeyRelease(_) => { + // Set both the source and event field to the requested keyboard type: + // event creation can read the source, while the field is explicit. + unsafe { + set_keyboard_source_type(source, keyboard_type); } - }, - EventType::ButtonPress(button) => { - let point = get_current_mouse_location()?; - let event = match button { - Button::Left => CGEventType::LeftMouseDown, - Button::Right => CGEventType::RightMouseDown, - _ => return None, - }; - CGEvent::new_mouse_event( - source, - event, - point, - CGMouseButton::Left, // ignored because we don't use OtherMouse EventType - ) - .ok() - } - EventType::ButtonRelease(button) => { - let point = get_current_mouse_location()?; - let event = match button { - Button::Left => CGEventType::LeftMouseUp, - Button::Right => CGEventType::RightMouseUp, - _ => return None, - }; - CGEvent::new_mouse_event( - source, - event, - point, - CGMouseButton::Left, // ignored because we don't use OtherMouse EventType - ) - .ok() - } - EventType::MouseMove { x, y } => { - let point = CGPoint { x: (*x), y: (*y) }; - CGEvent::new_mouse_event(source, CGEventType::MouseMoved, point, CGMouseButton::Left) - .ok() - } - EventType::Wheel { delta_x, delta_y } => { - let wheel_count = 2; - CGEvent::new_scroll_event( - source, - ScrollEventUnit::PIXEL, - wheel_count, - (*delta_y).try_into().ok()?, - (*delta_x).try_into().ok()?, - 0, - ) - .ok() } + _ => {} } + + let event = match event_type { + EventType::KeyPress(key) => keyboard_event_from_key( + source.clone(), + *key, + KeyboardEventOptions { + keydown: true, + keyboard_type, + }, + modifier_state, + dead_state, + ), + EventType::KeyRelease(key) => keyboard_event_from_key( + source.clone(), + *key, + KeyboardEventOptions { + keydown: false, + keyboard_type, + }, + modifier_state, + dead_state, + ), + EventType::ButtonPress(Button::Left) => { + mouse_button_event(source.clone(), Button::Left, CGEventType::LeftMouseDown) + } + EventType::ButtonPress(Button::Right) => { + mouse_button_event(source.clone(), Button::Right, CGEventType::RightMouseDown) + } + EventType::ButtonRelease(Button::Left) => { + mouse_button_event(source.clone(), Button::Left, CGEventType::LeftMouseUp) + } + EventType::ButtonRelease(Button::Right) => { + mouse_button_event(source.clone(), Button::Right, CGEventType::RightMouseUp) + } + EventType::ButtonPress(_) | EventType::ButtonRelease(_) => None, + EventType::MouseMove { x, y } => mouse_move_event(source.clone(), *x, *y), + EventType::Wheel { delta_x, delta_y } => wheel_event(source.clone(), *delta_x, *delta_y), + }?; + set_event_source_user_data(&event, event_type); + Some(event) } unsafe fn convert_native(event_type: &EventType) -> Option { // https://developer.apple.com/documentation/coregraphics/cgeventsourcestateid#:~:text=kCGEventSourceStatePrivate let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState).ok()?; - convert_native_with_source(event_type, source) + convert_native_with_source(event_type, &source, current_keyboard_type(), None, None) } unsafe fn get_current_mouse_location() -> Option { @@ -168,7 +393,6 @@ unsafe fn get_current_mouse_location() -> Option { pub fn simulate(event_type: &EventType) -> Result<(), SimulateError> { unsafe { if let Some(cg_event) = convert_native(event_type) { - cg_event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, MOUSE_EXTRA_INFO); cg_event.post(CGEventTapLocation::HID); Ok(()) } else { @@ -180,20 +404,125 @@ pub fn simulate(event_type: &EventType) -> Result<(), SimulateError> { pub struct VirtualInput { source: CGEventSource, tap_loc: CGEventTapLocation, + keyboard_type: MacKeyboardType, + shift: Cell, + alt: Cell, + caps_lock: Cell, + control: Cell, + command: Cell, + dead_state: Cell, } impl VirtualInput { pub fn new(state_id: CGEventSourceStateID, tap_loc: CGEventTapLocation) -> Result { + let source = CGEventSource::new(state_id)?; + Ok(Self { - source: CGEventSource::new(state_id)?, + source, tap_loc, + keyboard_type: MacKeyboardType::Current, + shift: Cell::new(MODIFIER_KEY_NONE), + alt: Cell::new(MODIFIER_KEY_NONE), + caps_lock: Cell::new(false), + control: Cell::new(MODIFIER_KEY_NONE), + command: Cell::new(MODIFIER_KEY_NONE), + dead_state: Cell::new(0), }) } + /// Sets the hardware keyboard type used for physical keycode translation. + /// + /// This keeps using the active macOS input source/layout for character output. + /// For key events, the underlying event source is updated when events are + /// sent through this `VirtualInput`. + /// The selected type is fixed for this `VirtualInput`; create another + /// instance to use a different keyboard type for a separate input stream. + /// `MacKeyboardType::Current` stores the selection, but `simulate()` resolves + /// it through `keyboard_type_value()` for each event. + /// Do not share the same `CGEventSource` with code that expects a different + /// keyboard type. + pub fn with_keyboard_type(mut self, keyboard_type: MacKeyboardType) -> Self { + self.keyboard_type = keyboard_type; + self + } + + fn modifier_state(&self) -> u32 { + let mut modifier_state = MODIFIER_STATE_NONE; + if self.command.get() != MODIFIER_KEY_NONE { + modifier_state |= MODIFIER_STATE_COMMAND; + } + if self.shift.get() != MODIFIER_KEY_NONE { + modifier_state |= MODIFIER_STATE_SHIFT; + } + if self.caps_lock.get() { + modifier_state |= MODIFIER_STATE_CAPS_LOCK; + } + if self.alt.get() != MODIFIER_KEY_NONE { + modifier_state |= MODIFIER_STATE_ALT; + } + if self.control.get() != MODIFIER_KEY_NONE { + modifier_state |= MODIFIER_STATE_CONTROL; + } + modifier_state + } + + fn update_modifier_state(&self, event_type: &EventType) { + match event_type { + EventType::KeyPress(key) => self.update_key_modifier_state(*key, true), + EventType::KeyRelease(key) => self.update_key_modifier_state(*key, false), + _ => {} + } + } + + fn update_key_modifier_state(&self, key: crate::Key, keydown: bool) { + let Some(modifier_key) = modifier_key_from_key(key) else { + return; + }; + match modifier_key { + ModifierKey::Shift(side) => set_modifier_key_state(&self.shift, side, keydown), + ModifierKey::Alt(side) => set_modifier_key_state(&self.alt, side, keydown), + ModifierKey::Control(side) => set_modifier_key_state(&self.control, side, keydown), + ModifierKey::Command(side) => set_modifier_key_state(&self.command, side, keydown), + ModifierKey::CapsLock if keydown => self.caps_lock.set(!self.caps_lock.get()), + ModifierKey::CapsLock => {} + } + } + + /// Sends one event and updates this input stream's modifier/dead-key state. + /// + /// Call this from the main thread for keyboard events. Unicode translation + /// reads the active macOS input source through Carbon TIS APIs. + /// Printable key presses set the event Unicode string from `UCKeyTranslate` + /// output, so they are not keycode-only events. + /// + /// Do not call this concurrently on the same `VirtualInput`; keyboard events + /// update the underlying `CGEventSource` keyboard type before creating the + /// event. + /// + /// Caps Lock is tracked inside this input stream and toggles on key press. + /// Callers that need matching Unicode output must route Caps Lock through + /// the same `VirtualInput`; real system Caps Lock changes are not + /// synchronized. Dead-key state is carried between key presses until + /// `UCKeyTranslate` clears it, and is reset when Unicode translation is + /// suppressed for Command/Control shortcuts. Use a separate `VirtualInput` + /// for an independent keyboard stream. + /// + /// Modifier state is updated only after an event is created and posted + /// successfully. Dead-key state is updated during event creation because + /// `UCKeyTranslate` reads and writes that state. + /// `KeyRelease(CapsLock)` does not change the tracked Caps Lock state. pub fn simulate(&self, event_type: &EventType) -> Result<(), SimulateError> { unsafe { - if let Some(cg_event) = convert_native_with_source(event_type, self.source.clone()) { + let keyboard_type = keyboard_type_value(self.keyboard_type); + if let Some(cg_event) = convert_native_with_source( + event_type, + &self.source, + keyboard_type, + Some(self.modifier_state()), + Some(&self.dead_state), + ) { cg_event.post(self.tap_loc); + self.update_modifier_state(event_type); Ok(()) } else { Err(SimulateError) @@ -206,3 +535,473 @@ impl VirtualInput { unsafe { CGEventSourceKeyState(state_id, keycode) } } } + +#[cfg(test)] +mod tests { + use super::*; + use core_foundation::string::UniChar; + use serial_test::serial; + + #[link(name = "CoreGraphics", kind = "framework")] + extern "C" { + fn CGEventKeyboardGetUnicodeString( + event: core_graphics::sys::CGEventRef, + maxStringLength: usize, + actualStringLength: *mut usize, + unicodeString: *mut UniChar, + ); + } + + // Tests using this guard mutate global extra-info state and must be `#[serial]`. + struct ExtraInfoGuard { + mouse: i64, + keyboard: i64, + } + + impl ExtraInfoGuard { + fn new(mouse: i64, keyboard: i64) -> Self { + let guard = Self { + mouse: MOUSE_EXTRA_INFO.load(Ordering::Relaxed), + keyboard: KEYBOARD_EXTRA_INFO.load(Ordering::Relaxed), + }; + set_mouse_extra_info(mouse); + set_keyboard_extra_info(keyboard); + guard + } + } + + impl Drop for ExtraInfoGuard { + fn drop(&mut self) { + set_mouse_extra_info(self.mouse); + set_keyboard_extra_info(self.keyboard); + } + } + + const TEST_MOUSE_EXTRA_INFO: i64 = 11; + const TEST_KEYBOARD_EXTRA_INFO: i64 = 22; + const TEST_MOUSE_X: f64 = 1.0; + const TEST_MOUSE_Y: f64 = 2.0; + const RAW_KEYBOARD_TYPE: CGEventSourceKeyboardType = 91; + const TEST_UNICODE_BUFFER_LEN: usize = 8; + const TEST_PENDING_DEAD_STATE: u32 = 1; + fn event_unicode_string(event: &CGEvent) -> Vec { + let mut unicode = [0_u16; TEST_UNICODE_BUFFER_LEN]; + let mut length = 0; + unsafe { + CGEventKeyboardGetUnicodeString( + event.as_ptr(), + TEST_UNICODE_BUFFER_LEN, + &mut length, + unicode.as_mut_ptr(), + ); + } + assert!(length <= TEST_UNICODE_BUFFER_LEN); + unicode[..length].to_vec() + } + + fn key_press_event(input: &VirtualInput, keycode: CGKeyCode) -> CGEvent { + let dead_state = Cell::new(0); + unsafe { + convert_native_with_source( + &EventType::KeyPress(crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode))), + &input.source, + keyboard_type_value(input.keyboard_type), + Some(input.modifier_state()), + Some(&dead_state), + ) + .unwrap() + } + } + + fn virtual_input_event(input: &VirtualInput, event_type: &EventType) -> CGEvent { + unsafe { + convert_native_with_source( + event_type, + &input.source, + keyboard_type_value(input.keyboard_type), + Some(input.modifier_state()), + Some(&input.dead_state), + ) + .unwrap() + } + } + + #[test] + #[serial] + fn caps_lock_uses_alpha_lock_modifier_state() { + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + input.update_modifier_state(&EventType::KeyPress(crate::Key::CapsLock)); + + assert_eq!(MODIFIER_STATE_CAPS_LOCK, input.modifier_state()); + } + + #[test] + #[serial] + fn virtual_input_current_keyboard_type_uses_system_keyboard_type() { + let input = VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .unwrap() + .with_keyboard_type(MacKeyboardType::Current); + + let expected_keyboard_type = current_keyboard_type(); + let keyboard_type = keyboard_type_value(input.keyboard_type); + let dead_state = Cell::new(0); + let event = unsafe { + convert_native_with_source( + &EventType::KeyPress(crate::Key::RawKey(RawKey::MacVirtualKeycode(kVK_ANSI_2))), + &input.source, + keyboard_type, + Some(input.modifier_state()), + Some(&dead_state), + ) + .unwrap() + }; + + assert_eq!( + expected_keyboard_type as i64, + event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYBOARD_TYPE) + ); + } + + #[test] + #[serial] + fn keyboard_event_uses_requested_keyboard_type_with_shift() { + let input = VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .unwrap() + .with_keyboard_type(MacKeyboardType::Jis); + input.update_modifier_state(&EventType::KeyPress(crate::Key::ShiftLeft)); + let dead_state = Cell::new(0); + let event = unsafe { + convert_native_with_source( + &EventType::KeyPress(crate::Key::RawKey(RawKey::MacVirtualKeycode(kVK_ANSI_2))), + &input.source, + keyboard_type_value(input.keyboard_type), + Some(input.modifier_state()), + Some(&dead_state), + ) + .unwrap() + }; + + assert_eq!( + JIS_KEYBOARD_TYPE as i64, + event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYBOARD_TYPE) + ); + assert_eq!(MODIFIER_STATE_SHIFT, input.modifier_state()); + } + + #[test] + #[serial] + fn command_modifier_does_not_set_printable_unicode_string() { + let input = VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .unwrap() + .with_keyboard_type(MacKeyboardType::Ansi); + input.update_modifier_state(&EventType::KeyPress(crate::Key::MetaLeft)); + + let event = key_press_event(&input, kVK_ANSI_A); + + assert!(event_unicode_string(&event).is_empty()); + } + + #[test] + #[serial] + fn control_modifier_does_not_set_printable_unicode_string() { + let input = VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .unwrap() + .with_keyboard_type(MacKeyboardType::Ansi); + input.update_modifier_state(&EventType::KeyPress(crate::Key::ControlLeft)); + + let event = key_press_event(&input, kVK_ANSI_A); + + assert!(event_unicode_string(&event).is_empty()); + } + + #[test] + #[serial] + fn raw_shift_key_updates_modifier_state_for_keyboard_type_event() { + let input = VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .unwrap() + .with_keyboard_type(MacKeyboardType::Jis); + input.update_modifier_state(&EventType::KeyPress(crate::Key::RawKey( + RawKey::MacVirtualKeycode(kVK_Shift), + ))); + let dead_state = Cell::new(0); + let event = unsafe { + convert_native_with_source( + &EventType::KeyPress(crate::Key::RawKey(RawKey::MacVirtualKeycode(kVK_ANSI_2))), + &input.source, + keyboard_type_value(input.keyboard_type), + Some(input.modifier_state()), + Some(&dead_state), + ) + .unwrap() + }; + + assert_eq!( + JIS_KEYBOARD_TYPE as i64, + event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYBOARD_TYPE) + ); + assert_eq!(MODIFIER_STATE_SHIFT, input.modifier_state()); + } + + #[test] + #[serial] + fn alt_gr_key_updates_modifier_state() { + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + input.update_modifier_state(&EventType::KeyPress(crate::Key::AltGr)); + assert_eq!(MODIFIER_STATE_ALT, input.modifier_state()); + + input.update_modifier_state(&EventType::KeyRelease(crate::Key::AltGr)); + assert_eq!(MODIFIER_STATE_NONE, input.modifier_state()); + } + + #[test] + #[serial] + fn raw_right_option_key_updates_modifier_state() { + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + input.update_modifier_state(&EventType::KeyPress(crate::Key::RawKey( + RawKey::MacVirtualKeycode(kVK_RightOption), + ))); + assert_eq!(MODIFIER_STATE_ALT, input.modifier_state()); + + input.update_modifier_state(&EventType::KeyRelease(crate::Key::RawKey( + RawKey::MacVirtualKeycode(kVK_RightOption), + ))); + assert_eq!(MODIFIER_STATE_NONE, input.modifier_state()); + } + + fn assert_key_press_dead_state( + input: &VirtualInput, + event_type: EventType, + expected_dead_state: u32, + ) { + input.dead_state.set(TEST_PENDING_DEAD_STATE); + unsafe { + convert_native_with_source( + &event_type, + &input.source, + keyboard_type_value(input.keyboard_type), + Some(input.modifier_state()), + Some(&input.dead_state), + ) + .unwrap(); + } + + assert_eq!(expected_dead_state, input.dead_state.get()); + } + + #[test] + #[serial] + fn command_suppressed_key_press_clears_pending_dead_state() { + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + input.update_modifier_state(&EventType::KeyPress(crate::Key::MetaLeft)); + assert_key_press_dead_state(&input, EventType::KeyPress(crate::Key::KeyA), 0); + } + + #[test] + #[serial] + fn modifier_key_press_preserves_pending_dead_state() { + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + assert_key_press_dead_state( + &input, + EventType::KeyPress(crate::Key::ShiftLeft), + TEST_PENDING_DEAD_STATE, + ); + } + + #[test] + #[serial] + fn command_modified_modifier_key_press_preserves_pending_dead_state() { + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + input.update_modifier_state(&EventType::KeyPress(crate::Key::MetaLeft)); + assert_key_press_dead_state( + &input, + EventType::KeyPress(crate::Key::ShiftLeft), + TEST_PENDING_DEAD_STATE, + ); + } + + fn assert_paired_modifier_state( + input: &VirtualInput, + key_pair: (crate::Key, crate::Key), + modifier_state: u32, + ) { + let (left_key, right_key) = key_pair; + input.update_modifier_state(&EventType::KeyPress(left_key)); + input.update_modifier_state(&EventType::KeyPress(right_key)); + input.update_modifier_state(&EventType::KeyRelease(left_key)); + assert_eq!(modifier_state, input.modifier_state()); + + input.update_modifier_state(&EventType::KeyRelease(right_key)); + assert_eq!(MODIFIER_STATE_NONE, input.modifier_state()); + } + + #[test] + #[serial] + fn paired_modifier_keys_stay_active_until_both_are_released() { + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + assert_paired_modifier_state( + &input, + (crate::Key::ShiftLeft, crate::Key::ShiftRight), + MODIFIER_STATE_SHIFT, + ); + assert_paired_modifier_state( + &input, + (crate::Key::Alt, crate::Key::AltGr), + MODIFIER_STATE_ALT, + ); + assert_paired_modifier_state( + &input, + ( + crate::Key::RawKey(RawKey::MacVirtualKeycode(kVK_Option)), + crate::Key::RawKey(RawKey::MacVirtualKeycode(kVK_RightOption)), + ), + MODIFIER_STATE_ALT, + ); + assert_paired_modifier_state( + &input, + (crate::Key::ControlLeft, crate::Key::ControlRight), + MODIFIER_STATE_CONTROL, + ); + assert_paired_modifier_state( + &input, + (crate::Key::MetaLeft, crate::Key::MetaRight), + MODIFIER_STATE_COMMAND, + ); + } + + #[test] + fn modifier_keys_do_not_request_unicode_translation() { + assert!(!should_translate_key_unicode( + crate::Key::ShiftLeft, + MODIFIER_STATE_NONE + )); + assert!(!should_translate_key_unicode( + crate::Key::RawKey(RawKey::MacVirtualKeycode(kVK_RightOption)), + MODIFIER_STATE_NONE + )); + assert!(!should_translate_key_unicode( + crate::Key::KeyA, + MODIFIER_STATE_COMMAND + )); + assert!(should_translate_key_unicode( + crate::Key::KeyA, + MODIFIER_STATE_SHIFT + )); + } + + #[test] + #[serial] + fn keyboard_event_conversion_matches_raw_keyboard_type() { + let input = VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .unwrap() + .with_keyboard_type(MacKeyboardType::Raw(RAW_KEYBOARD_TYPE)); + + let event = key_press_event(&input, kVK_ANSI_2); + + assert_eq!( + RAW_KEYBOARD_TYPE as i64, + event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYBOARD_TYPE) + ); + } + + #[test] + #[serial] + fn event_source_user_data_matches_event_kind() { + let _guard = ExtraInfoGuard::new(TEST_MOUSE_EXTRA_INFO, TEST_KEYBOARD_EXTRA_INFO); + + assert_eq!( + TEST_KEYBOARD_EXTRA_INFO, + event_source_user_data(&EventType::KeyPress(crate::Key::KeyA)) + ); + assert_eq!( + TEST_MOUSE_EXTRA_INFO, + event_source_user_data(&EventType::MouseMove { + x: TEST_MOUSE_X, + y: TEST_MOUSE_Y + }) + ); + } + + #[test] + #[serial] + fn virtual_input_event_source_user_data_matches_event_kind() { + let _guard = ExtraInfoGuard::new(TEST_MOUSE_EXTRA_INFO, TEST_KEYBOARD_EXTRA_INFO); + let input = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).unwrap(); + + let keyboard_event = virtual_input_event(&input, &EventType::KeyPress(crate::Key::KeyA)); + assert_eq!( + TEST_KEYBOARD_EXTRA_INFO, + keyboard_event.get_integer_value_field(EventField::EVENT_SOURCE_USER_DATA) + ); + + let mouse_event = virtual_input_event( + &input, + &EventType::MouseMove { + x: TEST_MOUSE_X, + y: TEST_MOUSE_Y, + }, + ); + assert_eq!( + TEST_MOUSE_EXTRA_INFO, + mouse_event.get_integer_value_field(EventField::EVENT_SOURCE_USER_DATA) + ); + } + + #[test] + #[serial] + fn virtual_input_keyboard_type_is_used_for_key_events() { + let input = VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .unwrap() + .with_keyboard_type(MacKeyboardType::Jis); + + let dead_state = Cell::new(0); + let event = unsafe { + convert_native_with_source( + &EventType::KeyPress(crate::Key::RawKey(RawKey::MacVirtualKeycode(kVK_ANSI_2))), + &input.source, + keyboard_type_value(input.keyboard_type), + Some(input.modifier_state()), + Some(&dead_state), + ) + .unwrap() + }; + + assert_eq!( + JIS_KEYBOARD_TYPE as i64, + event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYBOARD_TYPE) + ); + } + + #[test] + #[serial] + fn mac_keyboard_type_maps_to_raw_keyboard_type() { + assert_eq!( + current_keyboard_type(), + keyboard_type_value(MacKeyboardType::Current) + ); + assert_eq!( + ANSI_KEYBOARD_TYPE, + keyboard_type_value(MacKeyboardType::Ansi) + ); + assert_eq!(ISO_KEYBOARD_TYPE, keyboard_type_value(MacKeyboardType::Iso)); + assert_eq!(JIS_KEYBOARD_TYPE, keyboard_type_value(MacKeyboardType::Jis)); + assert_eq!( + RAW_KEYBOARD_TYPE, + keyboard_type_value(MacKeyboardType::Raw(RAW_KEYBOARD_TYPE)) + ); + } +}