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

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