Skip to content

Commit 2354265

Browse files
committed
x11: Overhaul XIM code
Fixes rust-windowing#195 Fixes rust-windowing#277 * Read `XMODIFIERS` explicitly/directly instead of calling `XSetLocaleModifiers` with an empty string. This is useful for debugging purposes, and more clear to read and handle. * Fallback to local input method if the one specified in `XMODIFIERS` is later closed on the server end (i.e. if ibus/fcitx is terminated). Previously, that would cause the event loop to freeze and usually also segfault. * If using the fallback input method, respond to the `XMODIFIERS` input method later becoming available. This means that the input method restarting is handled, and that even if the program was started while ibus/fcitx/etc. was unavailable, it will start using it as soon as it becomes available. * Only one input method is opened for the whole event loop, with each window having its own input context. * IME works completely out of the box now, no longer requiring application developers to call `setlocale` or `XSetLocaleModifiers`. * Detailed error messages are provided if no input method could be opened. However, no information is provided to the user if their intended `XMODIFIERS` input method failed to open but the fallbacks (which will ostensibly always succeed) succeeded; in my opinion, this is something that is best filled by adding a logging feature to winit.
1 parent 4005bf1 commit 2354265

File tree

8 files changed

+880
-137
lines changed

8 files changed

+880
-137
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

33
- Overhauled X11 window geometry calculations. `get_position` and `set_position` are more universally accurate across different window managers, and `get_outer_size` actually works now.
4+
- On X11, input methods now work completely out of the box, no longer requiring application developers to manually call `setlocale`. Additionally, when input methods are started, stopped, or restarted on the server end, it's correctly handled.
45

56
# Version 0.12.0 (2018-04-06)
67

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

src/platform/linux/x11/ime/context.rs

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use std::ptr;
2+
use std::sync::Arc;
3+
use std::os::raw::{c_short, c_void};
4+
5+
use super::{ffi, util, XConnection, XError};
6+
7+
#[derive(Debug)]
8+
pub enum ImeContextCreationError {
9+
XError(XError),
10+
Null,
11+
}
12+
13+
unsafe fn create_pre_edit_attr<'a>(
14+
xconn: &'a Arc<XConnection>,
15+
ic_spot: &'a ffi::XPoint,
16+
) -> util::XSmartPointer<'a, c_void> {
17+
util::XSmartPointer::new(
18+
xconn,
19+
(xconn.xlib.XVaCreateNestedList)(
20+
0,
21+
ffi::XNSpotLocation_0.as_ptr() as *const _,
22+
ic_spot,
23+
ptr::null_mut::<()>(),
24+
),
25+
).expect("XVaCreateNestedList returned NULL")
26+
}
27+
28+
// WARNING: this struct doesn't destroy its XIC resource when dropped.
29+
// This is intentional, as it doesn't have enough information to know whether or not the context
30+
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
31+
// through `ImeInner`.
32+
#[derive(Debug)]
33+
pub struct ImeContext {
34+
pub ic: ffi::XIC,
35+
pub ic_spot: ffi::XPoint,
36+
}
37+
38+
impl ImeContext {
39+
pub unsafe fn new(
40+
xconn: &Arc<XConnection>,
41+
im: ffi::XIM,
42+
window: ffi::Window,
43+
ic_spot: Option<ffi::XPoint>,
44+
) -> Result<Self, ImeContextCreationError> {
45+
let ic = if let Some(ic_spot) = ic_spot {
46+
ImeContext::create_ic_with_spot(xconn, im, window, ic_spot)
47+
} else {
48+
ImeContext::create_ic(xconn, im, window)
49+
};
50+
51+
let ic = ic.ok_or(ImeContextCreationError::Null)?;
52+
xconn.check_errors().map_err(ImeContextCreationError::XError)?;
53+
54+
Ok(ImeContext {
55+
ic,
56+
ic_spot: ic_spot.unwrap_or_else(|| ffi::XPoint { x: 0, y: 0 }),
57+
})
58+
}
59+
60+
unsafe fn create_ic(
61+
xconn: &Arc<XConnection>,
62+
im: ffi::XIM,
63+
window: ffi::Window,
64+
) -> Option<ffi::XIC> {
65+
let ic = (xconn.xlib.XCreateIC)(
66+
im,
67+
ffi::XNInputStyle_0.as_ptr() as *const _,
68+
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
69+
ffi::XNClientWindow_0.as_ptr() as *const _,
70+
window,
71+
ptr::null_mut::<()>(),
72+
);
73+
if ic.is_null() {
74+
None
75+
} else {
76+
Some(ic)
77+
}
78+
}
79+
80+
unsafe fn create_ic_with_spot(
81+
xconn: &Arc<XConnection>,
82+
im: ffi::XIM,
83+
window: ffi::Window,
84+
ic_spot: ffi::XPoint,
85+
) -> Option<ffi::XIC> {
86+
let pre_edit_attr = create_pre_edit_attr(xconn, &ic_spot);
87+
let ic = (xconn.xlib.XCreateIC)(
88+
im,
89+
ffi::XNInputStyle_0.as_ptr() as *const _,
90+
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
91+
ffi::XNClientWindow_0.as_ptr() as *const _,
92+
window,
93+
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
94+
pre_edit_attr.ptr,
95+
ptr::null_mut::<()>(),
96+
);
97+
if ic.is_null() {
98+
None
99+
} else {
100+
Some(ic)
101+
}
102+
}
103+
104+
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
105+
unsafe {
106+
(xconn.xlib.XSetICFocus)(self.ic);
107+
}
108+
xconn.check_errors()
109+
}
110+
111+
pub fn unfocus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
112+
unsafe {
113+
(xconn.xlib.XUnsetICFocus)(self.ic);
114+
}
115+
xconn.check_errors()
116+
}
117+
118+
pub fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
119+
if self.ic_spot.x == x && self.ic_spot.y == y {
120+
return;
121+
}
122+
self.ic_spot = ffi::XPoint { x, y };
123+
124+
unsafe {
125+
let pre_edit_attr = create_pre_edit_attr(xconn, &self.ic_spot);
126+
(xconn.xlib.XSetICValues)(
127+
self.ic,
128+
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
129+
pre_edit_attr.ptr,
130+
ptr::null_mut::<()>(),
131+
);
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)