xcb_imdkit/
lib.rs

1/*!
2Wrapper around [xcb-imdkit](https://github.com/fcitx/xcb-imdkit), providing an IME client.
3
4[xcb-imdkit](https://github.com/fcitx/xcb-imdkit) provides a partial implementation of the [X11
5Input Method Protocol](https://www.x.org/releases/current/doc/libX11/XIM/xim.html) using
6[XCB](https://xcb.freedesktop.org/). This wrapper library provides the most essential functionality
7of said library as simply as possible.
8
9To get started quickly, consult the examples folder.
10*/
11
12#[macro_use]
13extern crate lazy_static;
14
15use std::os::raw::{c_char, c_void};
16use std::pin::Pin;
17use std::sync::{Arc, Mutex};
18
19use bitflags::bitflags;
20
21use clib::*;
22
23mod clib;
24
25type LogFn = dyn for<'a> FnMut(&'a str) + Send;
26
27lazy_static! {
28    static ref LOGGER: Mutex<Option<Box<LogFn>>> = Mutex::default();
29}
30
31extern "C" {
32    fn xcb_log_wrapper(msg: *const c_char, ...);
33}
34
35#[no_mangle]
36fn rust_log(msg: *const c_char) {
37    let msg = unsafe { std::ffi::CStr::from_ptr(msg) }.to_string_lossy();
38    let msg = msg.trim();
39    if let Some(logger) = LOGGER.lock().unwrap().as_mut() {
40        logger(msg);
41    }
42}
43
44extern "C" fn create_ic_callback(im: *mut xcb_xim_t, new_ic: xcb_xic_t, user_data: *mut c_void) {
45    let ime = unsafe { ime_from_user_data(user_data) };
46    ime.ic = Some(new_ic);
47    unsafe {
48        xcb_xim_set_ic_focus(im, new_ic);
49    }
50}
51
52extern "C" fn open_callback(im: *mut xcb_xim_t, user_data: *mut c_void) {
53    let ime = unsafe { ime_from_user_data(user_data) };
54    let input_style = ime.input_style.bits();
55    let spot = xcb_point_t {
56        x: ime.pos_req.x,
57        y: ime.pos_req.y,
58    };
59    let w = &mut ime.pos_req.win as *mut u32;
60    unsafe {
61        let nested = xcb_xim_create_nested_list(
62            im,
63            XCB_XIM_XNSpotLocation,
64            &spot,
65            std::ptr::null_mut::<c_void>(),
66        );
67        xcb_xim_create_ic(
68            im,
69            Some(create_ic_callback),
70            user_data,
71            XCB_XIM_XNInputStyle,
72            &input_style,
73            XCB_XIM_XNClientWindow,
74            w,
75            XCB_XIM_XNFocusWindow,
76            w,
77            XCB_XIM_XNPreeditAttributes,
78            &nested,
79            std::ptr::null_mut::<c_void>(),
80        );
81        free(nested.data as _);
82    }
83    ime.pos_cur = ime.pos_req;
84}
85
86unsafe fn xim_encoding_to_utf8(
87    im: *mut xcb_xim_t,
88    xim_str: *const c_char,
89    length: usize,
90) -> String {
91    let mut buf: Vec<u8> = vec![];
92    if xcb_xim_get_encoding(im) == _xcb_xim_encoding_t_XCB_XIM_UTF8_STRING {
93        buf.extend(std::slice::from_raw_parts(
94            xim_str as *const u8,
95            length as usize,
96        ));
97    } else if xcb_xim_get_encoding(im) == _xcb_xim_encoding_t_XCB_XIM_COMPOUND_TEXT {
98        let mut new_length = 0usize;
99        let utf8 = xcb_compound_text_to_utf8(xim_str, length as usize, &mut new_length);
100        if !utf8.is_null() {
101            buf.extend(std::slice::from_raw_parts(utf8 as _, new_length));
102            free(utf8 as _);
103        }
104    }
105    String::from_utf8_unchecked(buf)
106}
107
108unsafe fn ime_from_user_data(user_data: *mut c_void) -> &'static mut ImeClient {
109    &mut *(user_data as *mut ImeClient)
110}
111
112extern "C" fn commit_string_callback(
113    im: *mut xcb_xim_t,
114    _ic: xcb_xic_t,
115    _flag: u32,
116    input: *mut c_char,
117    length: u32,
118    _keysym: *mut u32,
119    _n_keysym: usize,
120    user_data: *mut c_void,
121) {
122    let input = unsafe { xim_encoding_to_utf8(im, input, length as usize) };
123    let ime = unsafe { ime_from_user_data(user_data) };
124    let win = ime.pos_req.win;
125    ime.callbacks.commit_string.as_mut().map(|f| f(win, &input));
126}
127
128extern "C" fn update_pos_callback(_im: *mut xcb_xim_t, ic: xcb_xic_t, user_data: *mut c_void) {
129    let ime = unsafe { ime_from_user_data(user_data) };
130    if ime.pos_update_queued {
131        ime.pos_update_queued = false;
132        ime.send_pos_update(ic);
133    } else {
134        ime.is_processing_pos_update = false;
135    }
136}
137
138extern "C" fn forward_event_callback(
139    _im: *mut xcb_xim_t,
140    _ic: xcb_xic_t,
141    event: *mut xcb_key_press_event_t,
142    user_data: *mut c_void,
143) {
144    let ptr = event as *const xcb::ffi::xcb_key_press_event_t;
145    let event = xcb::KeyPressEvent { ptr: ptr as _ };
146    let ime = unsafe { ime_from_user_data(user_data) };
147    let win = ime.pos_req.win;
148    ime.callbacks.forward_event.as_mut().map(|f| f(win, &event));
149
150    // xcb::KeyPressEvent has a Drop impl that will free `event`, but since we don't own it, we
151    // have to prevent that from happening
152    std::mem::forget(event);
153}
154
155extern "C" fn preedit_start_callback(_im: *mut xcb_xim_t, _ic: xcb_xic_t, user_data: *mut c_void) {
156    let ime = unsafe { ime_from_user_data(user_data) };
157    let win = ime.pos_req.win;
158    ime.callbacks.preedit_start.as_mut().map(|f| f(win));
159}
160
161extern "C" fn preedit_draw_callback(
162    im: *mut xcb_xim_t,
163    _ic: xcb_xic_t,
164    frame: *mut xcb_im_preedit_draw_fr_t,
165    user_data: *mut c_void,
166) {
167    let frame = unsafe { &*frame };
168    let preedit_info = PreeditInfo { inner: frame, im };
169    let ime = unsafe { ime_from_user_data(user_data) };
170    let win = ime.pos_req.win;
171    ime.callbacks
172        .preedit_draw
173        .as_mut()
174        .map(|f| f(win, preedit_info));
175}
176
177extern "C" fn preedit_done_callback(_im: *mut xcb_xim_t, _ic: xcb_xic_t, user_data: *mut c_void) {
178    let ime = unsafe { ime_from_user_data(user_data) };
179    let win = ime.pos_req.win;
180    ime.callbacks.preedit_done.as_mut().map(|f| f(win));
181}
182
183bitflags! {
184    /// [`InputStyle`] determines how the IME should integrate into the application.
185    pub struct InputStyle: u32 {
186        /// By default let the IME handle all input composition internally and only process the
187        /// final string after composition is finished using [`ImeClient::set_commit_string_cb`].
188        const DEFAULT = 0;
189
190        /// Enable calling of the preedit callbacks like the one set with
191        /// [`ImeClient::set_preedit_draw_cb`]. This enables displaying the currently edited text
192        /// inside the application and not only within the IME. The IME may stop displaying its
193        /// cursor if this flag is set.
194        const PREEDIT_CALLBACKS = _xcb_im_style_t_XCB_IM_PreeditCallbacks;
195    }
196}
197
198type StringCB = dyn for<'a> FnMut(u32, &'a str);
199type KeyPressCB = dyn for<'a> FnMut(u32, &'a xcb::KeyPressEvent);
200type PreeditDrawCB = dyn for<'a> FnMut(u32, PreeditInfo<'a>);
201type NotifyCB = dyn FnMut(u32);
202
203#[derive(Default)]
204struct Callbacks {
205    commit_string: Option<Box<StringCB>>,
206    forward_event: Option<Box<KeyPressCB>>,
207    preedit_start: Option<Box<NotifyCB>>,
208    preedit_draw: Option<Box<PreeditDrawCB>>,
209    preedit_done: Option<Box<NotifyCB>>,
210}
211
212#[derive(Debug, Clone, Copy)]
213struct ImePos {
214    win: u32,
215    x: i16,
216    y: i16,
217}
218
219/// [`PreeditInfo`] provides information about the text that is currently being edited by the IME.
220///
221/// Additionally it provides information about how the text has been changed.
222pub struct PreeditInfo<'a> {
223    im: *mut xcb_xim_t,
224    inner: &'a xcb_im_preedit_draw_fr_t,
225}
226
227impl<'a> PreeditInfo<'a> {
228    /// Status bitmask.
229    ///
230    /// - `0x01`: no string
231    /// - `0x02`: no feedback
232    ///
233    /// If no bits are set, [`text`] contains the current text of the IME.
234    ///
235    /// [`text`]: PreeditInfo::text
236    pub fn status(&self) -> u32 {
237        self.inner.status
238    }
239
240    /// Cursor offset within the currently edited text in characters.
241    pub fn caret(&self) -> u32 {
242        self.inner.caret
243    }
244
245    /// Starting change position.
246    pub fn chg_first(&self) -> u32 {
247        self.inner.chg_first
248    }
249
250    /// Length of the change counting characters.
251    pub fn chg_length(&self) -> u32 {
252        self.inner.chg_length
253    }
254
255    /// Current text in the IME.
256    pub fn text(&self) -> String {
257        unsafe {
258            xim_encoding_to_utf8(
259                self.im,
260                self.inner.preedit_string as _,
261                self.inner.length_of_preedit_string as usize,
262            )
263        }
264    }
265}
266
267impl<'a> std::fmt::Debug for PreeditInfo<'a> {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        f.debug_struct("PreeditInfo")
270            .field("status", &self.status())
271            .field("caret", &self.caret())
272            .field("chg_first", &self.chg_first())
273            .field("chg_length", &self.chg_length())
274            .field("text", &self.text());
275        Ok(())
276    }
277}
278
279/// Input Method Editor (IME) client.
280///
281/// [`ImeClient`] represents one instance of an Input Method Editor client. It provides callbacks for
282/// event handling as well as control over the position of the IME window. There should be only one
283/// IME client per application and it is advised to create at most one instance.
284pub struct ImeClient {
285    conn: Option<Arc<xcb::Connection>>,
286    im: *mut xcb_xim_t,
287    ic: Option<xcb_xic_t>,
288    callbacks: Callbacks,
289    input_style: InputStyle,
290    pos_cur: ImePos,
291    pos_req: ImePos,
292    is_processing_pos_update: bool,
293    pos_update_queued: bool,
294}
295
296impl ImeClient {
297    /// Set the global logger for xcb-imdkit.
298    ///
299    /// The callback will receive debug messages from the [C
300    /// library](https://github.com/fcitx/xcb-imdkit) this crate is wrapping.
301    pub fn set_logger<F>(f: F)
302    where
303        F: for<'a> FnMut(&'a str) + Send + 'static,
304    {
305        LOGGER.lock().unwrap().replace(Box::new(f));
306    }
307
308    /// Create a new [`ImeClient`].
309    ///
310    /// The first two arguments correspond to the result of [`xcb::Connection::connect`] with the
311    /// connection wrapped into an [`Arc`] to ensure that the `Ime` does not outlive its
312    /// connection.
313    /// For documentation on `input_style` refer to [`InputStyle`].
314    /// `im_name` can be used to specify a custom IME server to connect to using the syntax
315    /// `@im=custom_server`.
316    ///
317    /// [`Arc`]: std::sync::Arc
318    pub fn new(
319        conn: Arc<xcb::Connection>,
320        screen_id: i32,
321        input_style: InputStyle,
322        im_name: Option<&str>,
323    ) -> Pin<Box<Self>> {
324        let mut res = unsafe { Self::unsafe_new(&conn, screen_id, input_style, im_name) };
325        res.conn = Some(conn);
326        res
327    }
328
329    /// Create a new [`ImeClient`].
330    ///
331    /// This is the same as [`new`], except that the [`xcb::Connection`] is not wrapped
332    /// into an [`Arc`].
333    ///
334    /// # Safety
335    ///
336    /// The caller is responsible to ensure that the [`ImeClient`] does not outlive the connection.
337    ///
338    /// [`Arc`]: std::sync::Arc
339    /// [`new`]: ImeClient::new
340    pub unsafe fn unsafe_new(
341        conn: &xcb::Connection,
342        screen_id: i32,
343        input_style: InputStyle,
344        im_name: Option<&str>,
345    ) -> Pin<Box<Self>> {
346        xcb_compound_text_init();
347        let im = xcb_xim_create(
348            conn.get_raw_conn() as _,
349            screen_id,
350            im_name.map_or(std::ptr::null(), |name| name.as_ptr() as _),
351        );
352        let mut res = Box::pin(Self {
353            conn: None,
354            im,
355            ic: None,
356            callbacks: Callbacks::default(),
357            input_style,
358            pos_cur: ImePos { win: 0, x: 0, y: 0 },
359            pos_req: ImePos { win: 0, x: 0, y: 0 },
360            is_processing_pos_update: false,
361            pos_update_queued: false,
362        });
363        let callbacks = xcb_xim_im_callback {
364            commit_string: Some(commit_string_callback),
365            forward_event: Some(forward_event_callback),
366            preedit_start: Some(preedit_start_callback),
367            preedit_draw: Some(preedit_draw_callback),
368            preedit_done: Some(preedit_done_callback),
369            ..Default::default()
370        };
371        let data: *mut Self = res.as_mut().get_mut();
372        xcb_xim_set_im_callback(im, &callbacks, data as _);
373        xcb_xim_set_log_handler(im, Some(xcb_log_wrapper));
374        xcb_xim_set_use_compound_text(im, true);
375        xcb_xim_set_use_utf8_string(im, true);
376        res
377    }
378
379    fn try_open_ic(&mut self) {
380        if self.ic.is_some() {
381            return;
382        }
383        let data: *mut ImeClient = self as _;
384        unsafe { xcb_xim_open(self.im, Some(open_callback), true, data as _) };
385    }
386
387    /// Let the IME client process XCB's events.
388    ///
389    /// Return `true` if the IME client is handling the event and `false` if the event is ignored
390    /// by the IME client and has to be handled separately.
391    ///
392    /// This method should be called on **any** event from the event queue and not just
393    /// keypress/keyrelease events as it handles other events as well.
394    ///
395    /// Typically you will want to let the IME client handle all keypress/keyrelease events in your
396    /// main loop. The IME client will then forward all key events that were not used for input
397    /// composition to the callback set by [`set_forward_event_cb`]. Often those events include all
398    /// keyrelease events as well as the events for `ESC`, `Enter` or key combinations such as
399    /// `CTRL+C`.
400    /// To obtain the text currently typed into the IME and the final string consult
401    /// [`set_preedit_draw_cb`] and [`set_commit_string_cb`].
402    ///
403    /// [`set_forward_event_cb`]: ImeClient::set_forward_event_cb
404    /// [`set_commit_string_cb`]: ImeClient::set_commit_string_cb
405    /// [`set_preedit_draw_cb`]: ImeClient::set_preedit_draw_cb
406    pub fn process_event(&mut self, event: &xcb::GenericEvent) -> bool {
407        if !unsafe { xcb_xim_filter_event(self.im, event.ptr as _) } {
408            let mask = event.response_type() & !0x80;
409            if (mask == xcb::ffi::XCB_KEY_PRESS) || (mask == xcb::ffi::XCB_KEY_RELEASE) {
410                match self.ic {
411                    Some(ic) => {
412                        unsafe {
413                            xcb_xim_forward_event(self.im, ic, event.ptr as _);
414                        }
415                        return true;
416                    }
417                    _ => {
418                        self.try_open_ic();
419                    }
420                }
421            }
422        }
423        false
424    }
425
426    /// Set the position at which to place the IME window.
427    ///
428    /// Set the position of the IME window relative to the window specified by `win`. Coordinates
429    /// increase from the top left corner of the window.
430    ///
431    /// Return `true` if an update for the IME window position has been sent to the IME, `false` if
432    /// the update has been queued. If there is still an update request queued and this method is
433    /// called, the previously queued request is discarded in favor of the new one.
434    pub fn update_pos(&mut self, win: u32, x: i16, y: i16) -> bool {
435        self.pos_req = ImePos { win, x, y };
436        match self.ic {
437            Some(ic) => {
438                if self.is_processing_pos_update {
439                    self.pos_update_queued = true;
440                    return false;
441                }
442                self.send_pos_update(ic);
443                true
444            }
445            _ => {
446                self.try_open_ic();
447                false
448            }
449        }
450    }
451
452    fn send_pos_update(&mut self, ic: xcb_xic_t) {
453        self.is_processing_pos_update = true;
454        let spot = xcb_point_t {
455            x: self.pos_req.x,
456            y: self.pos_req.y,
457        };
458        let nested = unsafe {
459            xcb_xim_create_nested_list(
460                self.im,
461                XCB_XIM_XNSpotLocation,
462                &spot,
463                std::ptr::null_mut::<c_void>(),
464            )
465        };
466        if self.pos_req.win != self.pos_cur.win {
467            let w = &mut self.pos_req.win as *mut _;
468            unsafe {
469                xcb_xim_set_ic_values(
470                    self.im,
471                    ic,
472                    Some(update_pos_callback),
473                    self as *mut _ as _,
474                    XCB_XIM_XNClientWindow,
475                    w,
476                    XCB_XIM_XNFocusWindow,
477                    w,
478                    XCB_XIM_XNPreeditAttributes,
479                    &nested,
480                    std::ptr::null_mut::<c_void>(),
481                );
482            }
483        } else {
484            unsafe {
485                xcb_xim_set_ic_values(
486                    self.im,
487                    ic,
488                    Some(update_pos_callback),
489                    self as *mut _ as _,
490                    XCB_XIM_XNPreeditAttributes,
491                    &nested,
492                    std::ptr::null_mut::<c_void>(),
493                );
494            }
495        }
496        unsafe { free(nested.data as _) };
497        self.pos_cur = self.pos_req;
498    }
499
500    /// Set callback to be called once input composition is done.
501    ///
502    /// The window (set by [`update_pos`]) as well as the completed input are passed as arguments.
503    ///
504    /// [`update_pos`]: ImeClient::update_pos
505    pub fn set_commit_string_cb<F>(&mut self, f: F)
506    where
507        F: for<'a> FnMut(u32, &'a str) + 'static,
508    {
509        self.callbacks.commit_string = Some(Box::new(f));
510    }
511
512    // Set callback for keypress/keyrelease events unhandled by the IME.
513    //
514    // The first argument passed is the window (set by [`update_pos`]), the second the key event.
515    /// Often those events include all keyrelease events as well as the events for `ESC`, `Enter`
516    /// or key combinations such as `CTRL+C`. Please note that [`xcb::KeyPressEvent`] ==
517    /// [`xcb::KeyReleaseEvent`] (see [`xcb::ffi::xcb_key_release_event_t`]) and keyrelease events
518    /// are also supplied.
519    ///
520    /// [`update_pos`]: ImeClient::update_pos
521    pub fn set_forward_event_cb<F>(&mut self, f: F)
522    where
523        F: for<'a> FnMut(u32, &'a xcb::KeyPressEvent) + 'static,
524    {
525        self.callbacks.forward_event = Some(Box::new(f));
526    }
527
528    /// Callback called once the IME has been opened.
529    ///
530    /// The current window (set by [`update_pos`]) is supplied as argument.
531    /// Calls callback only if [`InputStyle::PREEDIT_CALLBACKS`] is set.
532    ///
533    /// [`update_pos`]: ImeClient::update_pos
534    pub fn set_preedit_start_cb<F>(&mut self, f: F)
535    where
536        F: FnMut(u32) + 'static,
537    {
538        self.callbacks.preedit_start = Some(Box::new(f));
539    }
540
541    /// Callback called whenever the text whitin the IME has changed.
542    ///
543    /// The current window (set by [`update_pos`]) is supplied as argument as well as
544    /// [`PreeditInfo`], which contains, among other things, the current text of the IME.
545    /// Calls callback only if [`InputStyle::PREEDIT_CALLBACKS`] is set.
546    ///
547    /// [`update_pos`]: ImeClient::update_pos
548    pub fn set_preedit_draw_cb<F>(&mut self, f: F)
549    where
550        F: for<'a> FnMut(u32, PreeditInfo<'a>) + 'static,
551    {
552        self.callbacks.preedit_draw = Some(Box::new(f));
553    }
554
555    /// Callback called once the IME has been closed.
556    ///
557    /// The current window (set by [`update_pos`]) is supplied as argument.
558    /// Calls callback only if [`InputStyle::PREEDIT_CALLBACKS`] is set.
559    ///
560    /// [`update_pos`]: ImeClient::update_pos
561    pub fn set_preedit_done_cb<F>(&mut self, f: F)
562    where
563        F: FnMut(u32) + 'static,
564    {
565        self.callbacks.preedit_done = Some(Box::new(f));
566    }
567}
568
569impl Drop for ImeClient {
570    fn drop(&mut self) {
571        unsafe {
572            if let Some(ic) = self.ic {
573                xcb_xim_destroy_ic(self.im, ic, None, std::ptr::null_mut());
574            }
575            xcb_xim_close(self.im);
576            xcb_xim_destroy(self.im);
577        }
578    }
579}