winit/platform_impl/linux/x11/ime/
context.rs

1use std::error::Error;
2use std::ffi::CStr;
3use std::os::raw::c_short;
4use std::sync::Arc;
5use std::{fmt, mem, ptr};
6
7use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
8
9use super::{ffi, util, XConnection, XError};
10use crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle};
11use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
12
13/// IME creation error.
14#[derive(Debug)]
15pub enum ImeContextCreationError {
16    /// Got the error from Xlib.
17    XError(XError),
18
19    /// Got null pointer from Xlib but without exact reason.
20    Null,
21}
22
23impl fmt::Display for ImeContextCreationError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            ImeContextCreationError::XError(err) => err.fmt(f),
27            ImeContextCreationError::Null => {
28                write!(f, "got null pointer from Xlib without exact reason")
29            },
30        }
31    }
32}
33
34impl Error for ImeContextCreationError {}
35
36/// The callback used by XIM preedit functions.
37type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
38
39/// Wrapper for creating XIM callbacks.
40#[inline]
41fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
42    XIMCallback { client_data, callback: Some(callback) }
43}
44
45/// The server started preedit.
46extern "C" fn preedit_start_callback(
47    _xim: ffi::XIM,
48    client_data: ffi::XPointer,
49    _call_data: ffi::XPointer,
50) -> i32 {
51    let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
52
53    client_data.text.clear();
54    client_data.cursor_pos = 0;
55    client_data
56        .event_sender
57        .send((client_data.window, ImeEvent::Start))
58        .expect("failed to send preedit start event");
59    -1
60}
61
62/// Done callback is used when the preedit should be hidden.
63extern "C" fn preedit_done_callback(
64    _xim: ffi::XIM,
65    client_data: ffi::XPointer,
66    _call_data: ffi::XPointer,
67) {
68    let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
69
70    // Drop text buffer and reset cursor position on done.
71    client_data.text = Vec::new();
72    client_data.cursor_pos = 0;
73
74    client_data
75        .event_sender
76        .send((client_data.window, ImeEvent::End))
77        .expect("failed to send preedit end event");
78}
79
80fn calc_byte_position(text: &[char], pos: usize) -> usize {
81    text.iter().take(pos).fold(0, |byte_pos, text| byte_pos + text.len_utf8())
82}
83
84/// Preedit text information to be drawn inline by the client.
85extern "C" fn preedit_draw_callback(
86    _xim: ffi::XIM,
87    client_data: ffi::XPointer,
88    call_data: ffi::XPointer,
89) {
90    let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
91    let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
92    client_data.cursor_pos = call_data.caret as usize;
93
94    let chg_range =
95        call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
96    if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
97        tracing::warn!(
98            "invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
99            client_data.text.len(),
100            call_data.chg_first,
101            call_data.chg_length
102        );
103        return;
104    }
105
106    // NULL indicate text deletion
107    let mut new_chars = if call_data.text.is_null() {
108        Vec::new()
109    } else {
110        let xim_text = unsafe { &mut *(call_data.text) };
111        if xim_text.encoding_is_wchar > 0 {
112            return;
113        }
114
115        let new_text = unsafe { xim_text.string.multi_byte };
116
117        if new_text.is_null() {
118            return;
119        }
120
121        let new_text = unsafe { CStr::from_ptr(new_text) };
122
123        String::from(new_text.to_str().expect("Invalid UTF-8 String from IME")).chars().collect()
124    };
125    let mut old_text_tail = client_data.text.split_off(chg_range.end);
126    client_data.text.truncate(chg_range.start);
127    client_data.text.append(&mut new_chars);
128    client_data.text.append(&mut old_text_tail);
129    let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
130
131    client_data
132        .event_sender
133        .send((
134            client_data.window,
135            ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
136        ))
137        .expect("failed to send preedit update event");
138}
139
140/// Handling of cursor movements in preedit text.
141extern "C" fn preedit_caret_callback(
142    _xim: ffi::XIM,
143    client_data: ffi::XPointer,
144    call_data: ffi::XPointer,
145) {
146    let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
147    let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
148
149    if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition {
150        client_data.cursor_pos = call_data.position as usize;
151        let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
152
153        client_data
154            .event_sender
155            .send((
156                client_data.window,
157                ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
158            ))
159            .expect("failed to send preedit update event");
160    }
161}
162
163/// Struct to simplify callback creation and latter passing into Xlib XIM.
164struct PreeditCallbacks {
165    start_callback: ffi::XIMCallback,
166    done_callback: ffi::XIMCallback,
167    draw_callback: ffi::XIMCallback,
168    caret_callback: ffi::XIMCallback,
169}
170
171impl PreeditCallbacks {
172    pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
173        let start_callback = create_xim_callback(client_data, unsafe {
174            mem::transmute::<usize, unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer)>(
175                preedit_start_callback as usize,
176            )
177        });
178        let done_callback = create_xim_callback(client_data, preedit_done_callback);
179        let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
180        let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
181
182        PreeditCallbacks { start_callback, done_callback, caret_callback, draw_callback }
183    }
184}
185
186struct ImeContextClientData {
187    window: ffi::Window,
188    event_sender: ImeEventSender,
189    text: Vec<char>,
190    cursor_pos: usize,
191}
192
193// XXX: this struct doesn't destroy its XIC resource when dropped.
194// This is intentional, as it doesn't have enough information to know whether or not the context
195// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
196// through `ImeInner`.
197pub struct ImeContext {
198    pub(crate) ic: ffi::XIC,
199    pub(crate) ic_spot: ffi::XPoint,
200    pub(crate) style: Style,
201    // Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free
202    // from there we keep the pointer to automatically deallocate it.
203    _client_data: Box<ImeContextClientData>,
204}
205
206impl ImeContext {
207    pub(crate) unsafe fn new(
208        xconn: &Arc<XConnection>,
209        im: ffi::XIM,
210        style: Style,
211        window: ffi::Window,
212        ic_spot: Option<ffi::XPoint>,
213        event_sender: ImeEventSender,
214    ) -> Result<Self, ImeContextCreationError> {
215        let client_data = Box::into_raw(Box::new(ImeContextClientData {
216            window,
217            event_sender,
218            text: Vec::new(),
219            cursor_pos: 0,
220        }));
221
222        let ic = match style as _ {
223            Style::Preedit(style) => unsafe {
224                ImeContext::create_preedit_ic(
225                    xconn,
226                    im,
227                    style,
228                    window,
229                    client_data as ffi::XPointer,
230                )
231            },
232            Style::Nothing(style) => unsafe {
233                ImeContext::create_nothing_ic(xconn, im, style, window)
234            },
235            Style::None(style) => unsafe { ImeContext::create_none_ic(xconn, im, style, window) },
236        }
237        .ok_or(ImeContextCreationError::Null)?;
238
239        xconn.check_errors().map_err(ImeContextCreationError::XError)?;
240
241        let mut context = ImeContext {
242            ic,
243            ic_spot: ffi::XPoint { x: 0, y: 0 },
244            style,
245            _client_data: unsafe { Box::from_raw(client_data) },
246        };
247
248        // Set the spot location, if it's present.
249        if let Some(ic_spot) = ic_spot {
250            context.set_spot(xconn, ic_spot.x, ic_spot.y)
251        }
252
253        Ok(context)
254    }
255
256    unsafe fn create_none_ic(
257        xconn: &Arc<XConnection>,
258        im: ffi::XIM,
259        style: XIMStyle,
260        window: ffi::Window,
261    ) -> Option<ffi::XIC> {
262        let ic = unsafe {
263            (xconn.xlib.XCreateIC)(
264                im,
265                ffi::XNInputStyle_0.as_ptr() as *const _,
266                style,
267                ffi::XNClientWindow_0.as_ptr() as *const _,
268                window,
269                ptr::null_mut::<()>(),
270            )
271        };
272
273        (!ic.is_null()).then_some(ic)
274    }
275
276    unsafe fn create_preedit_ic(
277        xconn: &Arc<XConnection>,
278        im: ffi::XIM,
279        style: XIMStyle,
280        window: ffi::Window,
281        client_data: ffi::XPointer,
282    ) -> Option<ffi::XIC> {
283        let preedit_callbacks = PreeditCallbacks::new(client_data);
284        let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe {
285            (xconn.xlib.XVaCreateNestedList)(
286                0,
287                ffi::XNPreeditStartCallback_0.as_ptr() as *const _,
288                &(preedit_callbacks.start_callback) as *const _,
289                ffi::XNPreeditDoneCallback_0.as_ptr() as *const _,
290                &(preedit_callbacks.done_callback) as *const _,
291                ffi::XNPreeditCaretCallback_0.as_ptr() as *const _,
292                &(preedit_callbacks.caret_callback) as *const _,
293                ffi::XNPreeditDrawCallback_0.as_ptr() as *const _,
294                &(preedit_callbacks.draw_callback) as *const _,
295                ptr::null_mut::<()>(),
296            )
297        })
298        .expect("XVaCreateNestedList returned NULL");
299
300        let ic = unsafe {
301            (xconn.xlib.XCreateIC)(
302                im,
303                ffi::XNInputStyle_0.as_ptr() as *const _,
304                style,
305                ffi::XNClientWindow_0.as_ptr() as *const _,
306                window,
307                ffi::XNPreeditAttributes_0.as_ptr() as *const _,
308                preedit_attr.ptr,
309                ptr::null_mut::<()>(),
310            )
311        };
312
313        (!ic.is_null()).then_some(ic)
314    }
315
316    unsafe fn create_nothing_ic(
317        xconn: &Arc<XConnection>,
318        im: ffi::XIM,
319        style: XIMStyle,
320        window: ffi::Window,
321    ) -> Option<ffi::XIC> {
322        let ic = unsafe {
323            (xconn.xlib.XCreateIC)(
324                im,
325                ffi::XNInputStyle_0.as_ptr() as *const _,
326                style,
327                ffi::XNClientWindow_0.as_ptr() as *const _,
328                window,
329                ptr::null_mut::<()>(),
330            )
331        };
332
333        (!ic.is_null()).then_some(ic)
334    }
335
336    pub(crate) fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
337        unsafe {
338            (xconn.xlib.XSetICFocus)(self.ic);
339        }
340        xconn.check_errors()
341    }
342
343    pub(crate) fn unfocus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
344        unsafe {
345            (xconn.xlib.XUnsetICFocus)(self.ic);
346        }
347        xconn.check_errors()
348    }
349
350    pub fn is_allowed(&self) -> bool {
351        !matches!(self.style, Style::None(_))
352    }
353
354    // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks
355    // are being used. Certain IMEs do show selection window, but it's placed in bottom left of the
356    // window and couldn't be changed.
357    //
358    // For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580.
359    pub(crate) fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
360        if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y {
361            return;
362        }
363
364        self.ic_spot = ffi::XPoint { x, y };
365
366        unsafe {
367            let preedit_attr = util::memory::XSmartPointer::new(
368                xconn,
369                (xconn.xlib.XVaCreateNestedList)(
370                    0,
371                    ffi::XNSpotLocation_0.as_ptr(),
372                    &self.ic_spot,
373                    ptr::null_mut::<()>(),
374                ),
375            )
376            .expect("XVaCreateNestedList returned NULL");
377
378            (xconn.xlib.XSetICValues)(
379                self.ic,
380                ffi::XNPreeditAttributes_0.as_ptr() as *const _,
381                preedit_attr.ptr,
382                ptr::null_mut::<()>(),
383            );
384        }
385    }
386}