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