#[macro_use]
extern crate lazy_static;
use std::os::raw::{c_char, c_void};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use bitflags::bitflags;
use clib::*;
mod clib;
type LogFn = dyn for<'a> FnMut(&'a str) + Send;
lazy_static! {
static ref LOGGER: Mutex<Option<Box<LogFn>>> = Mutex::default();
}
extern "C" {
fn xcb_log_wrapper(msg: *const c_char, ...);
}
#[no_mangle]
fn rust_log(msg: *const c_char) {
let msg = unsafe { std::ffi::CStr::from_ptr(msg) }.to_string_lossy();
let msg = msg.trim();
if let Some(logger) = LOGGER.lock().unwrap().as_mut() {
logger(msg);
}
}
extern "C" fn create_ic_callback(im: *mut xcb_xim_t, new_ic: xcb_xic_t, user_data: *mut c_void) {
let ime = unsafe { ime_from_user_data(user_data) };
ime.ic = Some(new_ic);
unsafe {
xcb_xim_set_ic_focus(im, new_ic);
}
}
extern "C" fn open_callback(im: *mut xcb_xim_t, user_data: *mut c_void) {
let ime = unsafe { ime_from_user_data(user_data) };
let input_style = ime.input_style.bits();
let spot = xcb_point_t {
x: ime.pos_req.x,
y: ime.pos_req.y,
};
let w = &mut ime.pos_req.win as *mut u32;
unsafe {
let nested = xcb_xim_create_nested_list(
im,
XCB_XIM_XNSpotLocation,
&spot,
std::ptr::null_mut::<c_void>(),
);
xcb_xim_create_ic(
im,
Some(create_ic_callback),
user_data,
XCB_XIM_XNInputStyle,
&input_style,
XCB_XIM_XNClientWindow,
w,
XCB_XIM_XNFocusWindow,
w,
XCB_XIM_XNPreeditAttributes,
&nested,
std::ptr::null_mut::<c_void>(),
);
free(nested.data as _);
}
ime.pos_cur = ime.pos_req;
}
unsafe fn xim_encoding_to_utf8(
im: *mut xcb_xim_t,
xim_str: *const c_char,
length: usize,
) -> String {
let mut buf: Vec<u8> = vec![];
if xcb_xim_get_encoding(im) == _xcb_xim_encoding_t_XCB_XIM_UTF8_STRING {
buf.extend(std::slice::from_raw_parts(
xim_str as *const u8,
length as usize,
));
} else if xcb_xim_get_encoding(im) == _xcb_xim_encoding_t_XCB_XIM_COMPOUND_TEXT {
let mut new_length = 0usize;
let utf8 = xcb_compound_text_to_utf8(xim_str, length as usize, &mut new_length);
if !utf8.is_null() {
buf.extend(std::slice::from_raw_parts(utf8 as _, new_length));
free(utf8 as _);
}
}
String::from_utf8_unchecked(buf)
}
unsafe fn ime_from_user_data(user_data: *mut c_void) -> &'static mut ImeClient {
&mut *(user_data as *mut ImeClient)
}
extern "C" fn commit_string_callback(
im: *mut xcb_xim_t,
_ic: xcb_xic_t,
_flag: u32,
input: *mut c_char,
length: u32,
_keysym: *mut u32,
_n_keysym: usize,
user_data: *mut c_void,
) {
let input = unsafe { xim_encoding_to_utf8(im, input, length as usize) };
let ime = unsafe { ime_from_user_data(user_data) };
let win = ime.pos_req.win;
ime.callbacks.commit_string.as_mut().map(|f| f(win, &input));
}
extern "C" fn update_pos_callback(_im: *mut xcb_xim_t, ic: xcb_xic_t, user_data: *mut c_void) {
let ime = unsafe { ime_from_user_data(user_data) };
if ime.pos_update_queued {
ime.pos_update_queued = false;
ime.send_pos_update(ic);
} else {
ime.is_processing_pos_update = false;
}
}
extern "C" fn forward_event_callback(
_im: *mut xcb_xim_t,
_ic: xcb_xic_t,
event: *mut xcb_key_press_event_t,
user_data: *mut c_void,
) {
let ptr = event as *const xcb::ffi::xcb_key_press_event_t;
let event = xcb::KeyPressEvent { ptr: ptr as _ };
let ime = unsafe { ime_from_user_data(user_data) };
let win = ime.pos_req.win;
ime.callbacks.forward_event.as_mut().map(|f| f(win, &event));
std::mem::forget(event);
}
extern "C" fn preedit_start_callback(_im: *mut xcb_xim_t, _ic: xcb_xic_t, user_data: *mut c_void) {
let ime = unsafe { ime_from_user_data(user_data) };
let win = ime.pos_req.win;
ime.callbacks.preedit_start.as_mut().map(|f| f(win));
}
extern "C" fn preedit_draw_callback(
im: *mut xcb_xim_t,
_ic: xcb_xic_t,
frame: *mut xcb_im_preedit_draw_fr_t,
user_data: *mut c_void,
) {
let frame = unsafe { &*frame };
let preedit_info = PreeditInfo { inner: frame, im };
let ime = unsafe { ime_from_user_data(user_data) };
let win = ime.pos_req.win;
ime.callbacks
.preedit_draw
.as_mut()
.map(|f| f(win, preedit_info));
}
extern "C" fn preedit_done_callback(_im: *mut xcb_xim_t, _ic: xcb_xic_t, user_data: *mut c_void) {
let ime = unsafe { ime_from_user_data(user_data) };
let win = ime.pos_req.win;
ime.callbacks.preedit_done.as_mut().map(|f| f(win));
}
bitflags! {
pub struct InputStyle: u32 {
const DEFAULT = 0;
const PREEDIT_CALLBACKS = _xcb_im_style_t_XCB_IM_PreeditCallbacks;
}
}
type StringCB = dyn for<'a> FnMut(u32, &'a str);
type KeyPressCB = dyn for<'a> FnMut(u32, &'a xcb::KeyPressEvent);
type PreeditDrawCB = dyn for<'a> FnMut(u32, PreeditInfo<'a>);
type NotifyCB = dyn FnMut(u32);
#[derive(Default)]
struct Callbacks {
commit_string: Option<Box<StringCB>>,
forward_event: Option<Box<KeyPressCB>>,
preedit_start: Option<Box<NotifyCB>>,
preedit_draw: Option<Box<PreeditDrawCB>>,
preedit_done: Option<Box<NotifyCB>>,
}
#[derive(Debug, Clone, Copy)]
struct ImePos {
win: u32,
x: i16,
y: i16,
}
pub struct PreeditInfo<'a> {
im: *mut xcb_xim_t,
inner: &'a xcb_im_preedit_draw_fr_t,
}
impl<'a> PreeditInfo<'a> {
pub fn status(&self) -> u32 {
self.inner.status
}
pub fn caret(&self) -> u32 {
self.inner.caret
}
pub fn chg_first(&self) -> u32 {
self.inner.chg_first
}
pub fn chg_length(&self) -> u32 {
self.inner.chg_length
}
pub fn text(&self) -> String {
unsafe {
xim_encoding_to_utf8(
self.im,
self.inner.preedit_string as _,
self.inner.length_of_preedit_string as usize,
)
}
}
}
impl<'a> std::fmt::Debug for PreeditInfo<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PreeditInfo")
.field("status", &self.status())
.field("caret", &self.caret())
.field("chg_first", &self.chg_first())
.field("chg_length", &self.chg_length())
.field("text", &self.text());
Ok(())
}
}
pub struct ImeClient {
conn: Option<Arc<xcb::Connection>>,
im: *mut xcb_xim_t,
ic: Option<xcb_xic_t>,
callbacks: Callbacks,
input_style: InputStyle,
pos_cur: ImePos,
pos_req: ImePos,
is_processing_pos_update: bool,
pos_update_queued: bool,
}
impl ImeClient {
pub fn set_logger<F>(f: F)
where
F: for<'a> FnMut(&'a str) + Send + 'static,
{
LOGGER.lock().unwrap().replace(Box::new(f));
}
pub fn new(
conn: Arc<xcb::Connection>,
screen_id: i32,
input_style: InputStyle,
im_name: Option<&str>,
) -> Pin<Box<Self>> {
let mut res = unsafe { Self::unsafe_new(&conn, screen_id, input_style, im_name) };
res.conn = Some(conn);
res
}
pub unsafe fn unsafe_new(
conn: &xcb::Connection,
screen_id: i32,
input_style: InputStyle,
im_name: Option<&str>,
) -> Pin<Box<Self>> {
xcb_compound_text_init();
let im = xcb_xim_create(
conn.get_raw_conn() as _,
screen_id,
im_name.map_or(std::ptr::null(), |name| name.as_ptr() as _),
);
let mut res = Box::pin(Self {
conn: None,
im,
ic: None,
callbacks: Callbacks::default(),
input_style,
pos_cur: ImePos { win: 0, x: 0, y: 0 },
pos_req: ImePos { win: 0, x: 0, y: 0 },
is_processing_pos_update: false,
pos_update_queued: false,
});
let callbacks = xcb_xim_im_callback {
commit_string: Some(commit_string_callback),
forward_event: Some(forward_event_callback),
preedit_start: Some(preedit_start_callback),
preedit_draw: Some(preedit_draw_callback),
preedit_done: Some(preedit_done_callback),
..Default::default()
};
let data: *mut Self = res.as_mut().get_mut();
xcb_xim_set_im_callback(im, &callbacks, data as _);
xcb_xim_set_log_handler(im, Some(xcb_log_wrapper));
xcb_xim_set_use_compound_text(im, true);
xcb_xim_set_use_utf8_string(im, true);
res
}
fn try_open_ic(&mut self) {
if self.ic.is_some() {
return;
}
let data: *mut ImeClient = self as _;
unsafe { xcb_xim_open(self.im, Some(open_callback), true, data as _) };
}
pub fn process_event(&mut self, event: &xcb::GenericEvent) -> bool {
if !unsafe { xcb_xim_filter_event(self.im, event.ptr as _) } {
let mask = event.response_type() & !0x80;
if (mask == xcb::ffi::XCB_KEY_PRESS) || (mask == xcb::ffi::XCB_KEY_RELEASE) {
match self.ic {
Some(ic) => {
unsafe {
xcb_xim_forward_event(self.im, ic, event.ptr as _);
}
return true;
}
_ => {
self.try_open_ic();
}
}
}
}
false
}
pub fn update_pos(&mut self, win: u32, x: i16, y: i16) -> bool {
self.pos_req = ImePos { win, x, y };
match self.ic {
Some(ic) => {
if self.is_processing_pos_update {
self.pos_update_queued = true;
return false;
}
self.send_pos_update(ic);
true
}
_ => {
self.try_open_ic();
false
}
}
}
fn send_pos_update(&mut self, ic: xcb_xic_t) {
self.is_processing_pos_update = true;
let spot = xcb_point_t {
x: self.pos_req.x,
y: self.pos_req.y,
};
let nested = unsafe {
xcb_xim_create_nested_list(
self.im,
XCB_XIM_XNSpotLocation,
&spot,
std::ptr::null_mut::<c_void>(),
)
};
if self.pos_req.win != self.pos_cur.win {
let w = &mut self.pos_req.win as *mut _;
unsafe {
xcb_xim_set_ic_values(
self.im,
ic,
Some(update_pos_callback),
self as *mut _ as _,
XCB_XIM_XNClientWindow,
w,
XCB_XIM_XNFocusWindow,
w,
XCB_XIM_XNPreeditAttributes,
&nested,
std::ptr::null_mut::<c_void>(),
);
}
} else {
unsafe {
xcb_xim_set_ic_values(
self.im,
ic,
Some(update_pos_callback),
self as *mut _ as _,
XCB_XIM_XNPreeditAttributes,
&nested,
std::ptr::null_mut::<c_void>(),
);
}
}
unsafe { free(nested.data as _) };
self.pos_cur = self.pos_req;
}
pub fn set_commit_string_cb<F>(&mut self, f: F)
where
F: for<'a> FnMut(u32, &'a str) + 'static,
{
self.callbacks.commit_string = Some(Box::new(f));
}
pub fn set_forward_event_cb<F>(&mut self, f: F)
where
F: for<'a> FnMut(u32, &'a xcb::KeyPressEvent) + 'static,
{
self.callbacks.forward_event = Some(Box::new(f));
}
pub fn set_preedit_start_cb<F>(&mut self, f: F)
where
F: FnMut(u32) + 'static,
{
self.callbacks.preedit_start = Some(Box::new(f));
}
pub fn set_preedit_draw_cb<F>(&mut self, f: F)
where
F: for<'a> FnMut(u32, PreeditInfo<'a>) + 'static,
{
self.callbacks.preedit_draw = Some(Box::new(f));
}
pub fn set_preedit_done_cb<F>(&mut self, f: F)
where
F: FnMut(u32) + 'static,
{
self.callbacks.preedit_done = Some(Box::new(f));
}
}
impl Drop for ImeClient {
fn drop(&mut self) {
unsafe {
if let Some(ic) = self.ic {
xcb_xim_destroy_ic(self.im, ic, None, std::ptr::null_mut());
}
xcb_xim_close(self.im);
xcb_xim_destroy(self.im);
}
}
}