1 | use std::ffi::CStr; |
2 | use std::os::raw::c_short; |
3 | use std::sync::Arc; |
4 | use std::{mem, ptr}; |
5 | |
6 | use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct}; |
7 | |
8 | use super::{ffi, util, XConnection, XError}; |
9 | use crate::platform_impl::platform::x11::ime::input_method::{InputMethod, Style, XIMStyle}; |
10 | use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; |
11 | |
12 | /// IME creation error. |
13 | #[derive (Debug)] |
14 | pub 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. |
23 | type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer); |
24 | |
25 | /// Wrapper for creating XIM callbacks. |
26 | #[inline ] |
27 | fn 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. |
32 | extern "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: &mut ImeContextClientData = 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(msg:"failed to send preedit start event" ); |
45 | -1 |
46 | } |
47 | |
48 | /// Done callback is used when the preedit should be hidden. |
49 | extern "C" fn preedit_done_callback( |
50 | _xim: ffi::XIM, |
51 | client_data: ffi::XPointer, |
52 | _call_data: ffi::XPointer, |
53 | ) { |
54 | let client_data: &mut ImeContextClientData = 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(msg:"failed to send preedit end event" ); |
64 | } |
65 | |
66 | fn calc_byte_position(text: &[char], pos: usize) -> usize { |
67 | text.iter().take(pos).fold(init:0, |byte_pos: usize, text: &char| byte_pos + text.len_utf8()) |
68 | } |
69 | |
70 | /// Preedit text information to be drawn inline by the client. |
71 | extern "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. |
127 | extern "C" fn preedit_caret_callback( |
128 | _xim: ffi::XIM, |
129 | client_data: ffi::XPointer, |
130 | call_data: ffi::XPointer, |
131 | ) { |
132 | let client_data: &mut ImeContextClientData = unsafe { &mut *(client_data as *mut ImeContextClientData) }; |
133 | let call_data: &mut XIMPreeditCaretCallbackStruct = 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: usize = 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(msg:"failed to send preedit update event" ); |
146 | } |
147 | } |
148 | |
149 | /// Struct to simplify callback creation and latter passing into Xlib XIM. |
150 | struct PreeditCallbacks { |
151 | start_callback: ffi::XIMCallback, |
152 | done_callback: ffi::XIMCallback, |
153 | draw_callback: ffi::XIMCallback, |
154 | caret_callback: ffi::XIMCallback, |
155 | } |
156 | |
157 | impl PreeditCallbacks { |
158 | pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks { |
159 | let start_callback: XIMCallback = create_xim_callback(client_data, callback:unsafe { |
160 | mem::transmute::<usize, unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer)>( |
161 | src:preedit_start_callback as usize, |
162 | ) |
163 | }); |
164 | let done_callback: XIMCallback = create_xim_callback(client_data, preedit_done_callback); |
165 | let caret_callback: XIMCallback = create_xim_callback(client_data, preedit_caret_callback); |
166 | let draw_callback: XIMCallback = create_xim_callback(client_data, preedit_draw_callback); |
167 | |
168 | PreeditCallbacks { start_callback, done_callback, caret_callback, draw_callback } |
169 | } |
170 | } |
171 | |
172 | struct 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`. |
183 | pub 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 | |
192 | impl 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 | } |
377 | |