1use std::{collections::HashMap, os::raw::c_char, ptr, sync::Arc};
2
3use super::{ffi, XConnection, XError};
4
5use super::{
6 context::{ImeContext, ImeContextCreationError},
7 inner::{close_im, ImeInner},
8 input_method::PotentialInputMethods,
9};
10
11pub(crate) unsafe fn xim_set_callback(
12 xconn: &Arc<XConnection>,
13 xim: ffi::XIM,
14 field: *const c_char,
15 callback: *mut ffi::XIMCallback,
16) -> Result<(), XError> {
17 // It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize
18 // access that isn't type-checked.
19 unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) };
20 xconn.check_errors()
21}
22
23// Set a callback for when an input method matching the current locale modifiers becomes
24// available. Note that this has nothing to do with what input methods are open or able to be
25// opened, and simply uses the modifiers that are set when the callback is set.
26// * This is called per locale modifier, not per input method opened with that locale modifier.
27// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt
28// input contexts would always silently fail to use the input method.
29pub(crate) unsafe fn set_instantiate_callback(
30 xconn: &Arc<XConnection>,
31 client_data: ffi::XPointer,
32) -> Result<(), XError> {
33 unsafe {
34 (xconn.xlib.XRegisterIMInstantiateCallback)(
35 xconn.display,
36 ptr::null_mut(),
37 ptr::null_mut(),
38 ptr::null_mut(),
39 Some(xim_instantiate_callback),
40 client_data,
41 )
42 };
43 xconn.check_errors()
44}
45
46pub(crate) unsafe fn unset_instantiate_callback(
47 xconn: &Arc<XConnection>,
48 client_data: ffi::XPointer,
49) -> Result<(), XError> {
50 unsafe {
51 (xconn.xlib.XUnregisterIMInstantiateCallback)(
52 xconn.display,
53 ptr::null_mut(),
54 ptr::null_mut(),
55 ptr::null_mut(),
56 Some(xim_instantiate_callback),
57 client_data,
58 )
59 };
60 xconn.check_errors()
61}
62
63pub(crate) unsafe fn set_destroy_callback(
64 xconn: &Arc<XConnection>,
65 im: ffi::XIM,
66 inner: &ImeInner,
67) -> Result<(), XError> {
68 unsafe {
69 xim_set_callback(
70 xconn,
71 xim:im,
72 field:ffi::XNDestroyCallback_0.as_ptr() as *const _,
73 &inner.destroy_callback as *const _ as *mut _,
74 )
75 }
76}
77
78#[derive(Debug)]
79#[allow(clippy::enum_variant_names)]
80enum ReplaceImError {
81 // Boxed to prevent large error type
82 MethodOpenFailed(#[allow(dead_code)] Box<PotentialInputMethods>),
83 ContextCreationFailed(#[allow(dead_code)] ImeContextCreationError),
84 SetDestroyCallbackFailed(#[allow(dead_code)] XError),
85}
86
87// Attempt to replace current IM (which may or may not be presently valid) with a new one. This
88// includes replacing all existing input contexts and free'ing resources as necessary. This only
89// modifies existing state if all operations succeed.
90unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
91 let xconn = unsafe { &(*inner).xconn };
92
93 let (new_im, is_fallback) = {
94 let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) };
95 let is_fallback = new_im.is_fallback();
96 (
97 new_im.ok().ok_or_else(|| {
98 ReplaceImError::MethodOpenFailed(Box::new(unsafe {
99 (*inner).potential_input_methods.clone()
100 }))
101 })?,
102 is_fallback,
103 )
104 };
105
106 // It's important to always set a destroy callback, since there's otherwise potential for us
107 // to try to use or free a resource that's already been destroyed on the server.
108 {
109 let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) };
110 if result.is_err() {
111 let _ = unsafe { close_im(xconn, new_im.im) };
112 }
113 result
114 }
115 .map_err(ReplaceImError::SetDestroyCallbackFailed)?;
116
117 let mut new_contexts = HashMap::new();
118 for (window, old_context) in unsafe { (*inner).contexts.iter() } {
119 let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
120
121 // Check if the IME was allowed on that context.
122 let is_allowed = old_context
123 .as_ref()
124 .map(|old_context| old_context.is_allowed())
125 .unwrap_or_default();
126
127 // We can't use the style from the old context here, since it may change on reload, so
128 // pick style from the new XIM based on the old state.
129 let style = if is_allowed {
130 new_im.preedit_style
131 } else {
132 new_im.none_style
133 };
134
135 let new_context = {
136 let result = unsafe {
137 ImeContext::new(
138 xconn,
139 new_im.im,
140 style,
141 *window,
142 spot,
143 (*inner).event_sender.clone(),
144 )
145 };
146 if result.is_err() {
147 let _ = unsafe { close_im(xconn, new_im.im) };
148 }
149 result.map_err(ReplaceImError::ContextCreationFailed)?
150 };
151 new_contexts.insert(*window, Some(new_context));
152 }
153
154 // If we've made it this far, everything succeeded.
155 unsafe {
156 let _ = (*inner).destroy_all_contexts_if_necessary();
157 let _ = (*inner).close_im_if_necessary();
158 (*inner).im = Some(new_im);
159 (*inner).contexts = new_contexts;
160 (*inner).is_destroyed = false;
161 (*inner).is_fallback = is_fallback;
162 }
163 Ok(())
164}
165
166pub unsafe extern "C" fn xim_instantiate_callback(
167 _display: *mut ffi::Display,
168 client_data: ffi::XPointer,
169 // This field is unsupplied.
170 _call_data: ffi::XPointer,
171) {
172 let inner: *mut ImeInner = client_data as _;
173 if !inner.is_null() {
174 let xconn: &Arc = unsafe { &(*inner).xconn };
175 match unsafe { replace_im(inner) } {
176 Ok(()) => unsafe {
177 let _ = unset_instantiate_callback(xconn, client_data);
178 (*inner).is_fallback = false;
179 },
180 Err(err: ReplaceImError) => unsafe {
181 if (*inner).is_destroyed {
182 // We have no usable input methods!
183 panic!("Failed to reopen input method: {err:?}");
184 }
185 },
186 }
187 }
188}
189
190// This callback is triggered when the input method is closed on the server end. When this
191// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been
192// free'd (attempting to do so causes our connection to freeze).
193pub unsafe extern "C" fn xim_destroy_callback(
194 _xim: ffi::XIM,
195 client_data: ffi::XPointer,
196 // This field is unsupplied.
197 _call_data: ffi::XPointer,
198) {
199 let inner: *mut ImeInner = client_data as _;
200 if !inner.is_null() {
201 unsafe { (*inner).is_destroyed = true };
202 let xconn: &Arc = unsafe { &(*inner).xconn };
203 if unsafe { !(*inner).is_fallback } {
204 let _ = unsafe { set_instantiate_callback(xconn, client_data) };
205 // Attempt to open fallback input method.
206 match unsafe { replace_im(inner) } {
207 Ok(()) => unsafe { (*inner).is_fallback = true },
208 Err(err: ReplaceImError) => {
209 // We have no usable input methods!
210 panic!("Failed to open fallback input method: {err:?}");
211 }
212 }
213 }
214 }
215}
216