use std::collections::VecDeque;
use std::ffi::c_void;
use objc2::rc::Retained;
use objc2_app_kit::{NSApplication, NSEvent, NSEventType};
use objc2_foundation::MainThreadMarker;
use crate::{Error, InputMethodEvent, InputMethodState, Result};
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGEventCreateKeyboardEvent(
source: *const c_void,
virtual_key: u16,
key_down: bool,
) -> *mut c_void;
fn CGEventPost(tap: u32, event: *mut c_void);
fn CGEventKeyboardSetUnicodeString(event: *mut c_void, length: u64, chars: *const u16);
fn CFRelease(cf: *mut c_void);
}
const K_VK_DELETE: u16 = 0x33; const K_VK_FORWARD_DELETE: u16 = 0x75;
const K_CG_HID_EVENT_TAP: u32 = 0;
pub struct InputMethod {
active: bool,
serial: u32,
state: InputMethodState,
events: VecDeque<InputMethodEvent>,
#[allow(dead_code)]
mtm: Option<MainThreadMarker>,
}
impl InputMethod {
pub fn new() -> Result<Self> {
let mtm = MainThreadMarker::new();
Ok(Self {
active: true, serial: 0,
state: InputMethodState::new(),
events: VecDeque::new(),
mtm,
})
}
pub fn next_event(&mut self) -> Option<InputMethodEvent> {
if let Some(event) = self.events.pop_front() {
return Some(event);
}
if let Some(mtm) = self.mtm {
let app = NSApplication::sharedApplication(mtm);
loop {
let event: Option<Retained<NSEvent>> = unsafe {
app.nextEventMatchingMask_untilDate_inMode_dequeue(
objc2_app_kit::NSEventMask::Any,
None, objc2_foundation::NSDefaultRunLoopMode,
true,
)
};
match event {
Some(ns_event) => {
if let Some(ime_event) = self.convert_ns_event(&ns_event) {
return Some(ime_event);
}
}
None => break, }
}
}
None
}
fn convert_ns_event(&mut self, ns_event: &NSEvent) -> Option<InputMethodEvent> {
let event_type = ns_event.r#type();
match event_type {
NSEventType::KeyDown => {
if let Some(characters) = ns_event.characters() {
let text = characters.to_string();
if !text.is_empty() {
if !self.state.active {
self.state.active = true;
self.serial += 1;
return Some(InputMethodEvent::Activate {
serial: self.serial,
});
}
}
}
None
}
NSEventType::FlagsChanged => {
None
}
_ => None,
}
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn commit_string(&self, text: &str) -> Result<()> {
let utf16: Vec<u16> = text.encode_utf16().collect();
unsafe {
let event = CGEventCreateKeyboardEvent(std::ptr::null(), 0, true);
if event.is_null() {
return Err(Error::CommitFailed(
"Failed to create keyboard event".to_string(),
));
}
CGEventKeyboardSetUnicodeString(event, utf16.len() as u64, utf16.as_ptr());
CGEventPost(K_CG_HID_EVENT_TAP, event);
CFRelease(event);
let event_up = CGEventCreateKeyboardEvent(std::ptr::null(), 0, false);
if !event_up.is_null() {
CGEventKeyboardSetUnicodeString(event_up, utf16.len() as u64, utf16.as_ptr());
CGEventPost(K_CG_HID_EVENT_TAP, event_up);
CFRelease(event_up);
}
}
Ok(())
}
pub fn set_preedit_string(
&self,
_text: &str,
_cursor_begin: i32,
_cursor_end: i32,
) -> Result<()> {
log_debug!("Preedit not fully supported in macOS CGEvent mode - would need NSTextInputClient integration");
Ok(())
}
pub fn delete_surrounding_text(&self, before: u32, after: u32) -> Result<()> {
unsafe {
for _ in 0..before {
let event = CGEventCreateKeyboardEvent(std::ptr::null(), K_VK_DELETE, true);
if !event.is_null() {
CGEventPost(K_CG_HID_EVENT_TAP, event);
CFRelease(event);
}
let event_up = CGEventCreateKeyboardEvent(std::ptr::null(), K_VK_DELETE, false);
if !event_up.is_null() {
CGEventPost(K_CG_HID_EVENT_TAP, event_up);
CFRelease(event_up);
}
}
for _ in 0..after {
let event = CGEventCreateKeyboardEvent(std::ptr::null(), K_VK_FORWARD_DELETE, true);
if !event.is_null() {
CGEventPost(K_CG_HID_EVENT_TAP, event);
CFRelease(event);
}
let event_up =
CGEventCreateKeyboardEvent(std::ptr::null(), K_VK_FORWARD_DELETE, false);
if !event_up.is_null() {
CGEventPost(K_CG_HID_EVENT_TAP, event_up);
CFRelease(event_up);
}
}
}
Ok(())
}
pub fn commit(&self, _serial: u32) -> Result<()> {
Ok(())
}
pub fn state(&self) -> InputMethodState {
self.state.clone()
}
}