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 crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle}; |
9 | use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; |
10 | |
11 | use super::{ffi, util, XConnection, XError}; |
12 | |
13 | /// IME creation error. |
14 | #[derive (Debug)] |
15 | pub 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. |
24 | type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer); |
25 | |
26 | /// Wrapper for creating XIM callbacks. |
27 | #[inline ] |
28 | fn 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. |
36 | extern "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. |
53 | extern "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 | |
70 | fn 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. |
77 | extern "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. |
135 | extern "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. |
158 | struct PreeditCallbacks { |
159 | start_callback: ffi::XIMCallback, |
160 | done_callback: ffi::XIMCallback, |
161 | draw_callback: ffi::XIMCallback, |
162 | caret_callback: ffi::XIMCallback, |
163 | } |
164 | |
165 | impl 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 | |
183 | struct 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`. |
194 | pub 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 | |
203 | impl 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 | |