x11: Overhaul XIM code (#451)

Fixes #195
Fixes #277
Fixes #455

* 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.
This commit is contained in:
Francesca Frangipane 2018-04-10 22:18:30 -04:00 committed by GitHub
parent f08bf44670
commit 09c809003b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 880 additions and 137 deletions

View file

@ -3,6 +3,7 @@
- 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. - 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.
- Fixed SIGSEGV/SIGILL crashes on macOS caused by stabilization of the `!` (never) type. - Fixed SIGSEGV/SIGILL crashes on macOS caused by stabilization of the `!` (never) type.
- Implement `WindowEvent::HiDPIFactorChanged` for macOS - Implement `WindowEvent::HiDPIFactorChanged` for macOS
- 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.
# Version 0.12.0 (2018-04-06) # Version 0.12.0 (2018-04-06)

View file

@ -0,0 +1,185 @@
use std::ptr;
use std::sync::Arc;
use std::collections::HashMap;
use std::os::raw::c_char;
use super::{ffi, XConnection, XError};
use super::inner::{close_im, ImeInner};
use super::input_method::PotentialInputMethods;
use super::context::{ImeContextCreationError, ImeContext};
pub unsafe fn xim_set_callback(
xconn: &Arc<XConnection>,
xim: ffi::XIM,
field: *const c_char,
callback: *mut ffi::XIMCallback,
) -> Result<(), XError> {
// It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize
// access that isn't type-checked.
(xconn.xlib.XSetIMValues)(
xim,
field,
callback,
ptr::null_mut::<()>(),
);
xconn.check_errors()
}
// Set a callback for when an input method matching the current locale modifiers becomes
// available. Note that this has nothing to do with what input methods are open or able to be
// opened, and simply uses the modifiers that are set when the callback is set.
// * This is called per locale modifier, not per input method opened with that locale modifier.
// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt
// input contexts would always silently fail to use the input method.
pub unsafe fn set_instantiate_callback(
xconn: &Arc<XConnection>,
client_data: ffi::XPointer,
) -> Result<(), XError> {
(xconn.xlib.XRegisterIMInstantiateCallback)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
Some(xim_instantiate_callback),
client_data,
);
xconn.check_errors()
}
pub unsafe fn unset_instantiate_callback(
xconn: &Arc<XConnection>,
client_data: ffi::XPointer,
) -> Result<(), XError> {
(xconn.xlib.XUnregisterIMInstantiateCallback)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
Some(xim_instantiate_callback),
client_data,
);
xconn.check_errors()
}
pub unsafe fn set_destroy_callback(
xconn: &Arc<XConnection>,
im: ffi::XIM,
inner: &ImeInner,
) -> Result<(), XError> {
xim_set_callback(
&xconn,
im,
ffi::XNDestroyCallback_0.as_ptr() as *const _,
&inner.destroy_callback as *const _ as *mut _,
)
}
#[derive(Debug)]
enum ReplaceImError {
MethodOpenFailed(PotentialInputMethods),
ContextCreationFailed(ImeContextCreationError),
SetDestroyCallbackFailed(XError),
}
// Attempt to replace current IM (which may or may not be presently valid) with a new one. This
// includes replacing all existing input contexts and free'ing resources as necessary. This only
// modifies existing state if all operations succeed.
unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
let xconn = &(*inner).xconn;
let (new_im, is_fallback) = {
let new_im = (*inner).potential_input_methods.open_im(xconn, None);
let is_fallback = new_im.is_fallback();
(
new_im.ok().ok_or_else(|| {
ReplaceImError::MethodOpenFailed((*inner).potential_input_methods.clone())
})?,
is_fallback,
)
};
// It's important to always set a destroy callback, since there's otherwise potential for us
// to try to use or free a resource that's already been destroyed on the server.
{
let result = set_destroy_callback(xconn, new_im.im, &*inner);
if result.is_err() {
let _ = close_im(xconn, new_im.im);
}
result
}.map_err(ReplaceImError::SetDestroyCallbackFailed)?;
let mut new_contexts = HashMap::new();
for (window, old_context) in (*inner).contexts.iter() {
let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
let new_context = {
let result = ImeContext::new(
xconn,
new_im.im,
*window,
spot,
);
if result.is_err() {
let _ = close_im(xconn, new_im.im);
}
result.map_err(ReplaceImError::ContextCreationFailed)?
};
new_contexts.insert(*window, Some(new_context));
}
// If we've made it this far, everything succeeded.
let _ = (*inner).destroy_all_contexts_if_necessary();
let _ = (*inner).close_im_if_necessary();
(*inner).im = new_im.im;
(*inner).contexts = new_contexts;
(*inner).is_destroyed = false;
(*inner).is_fallback = is_fallback;
Ok(())
}
pub unsafe extern fn xim_instantiate_callback(
_display: *mut ffi::Display,
client_data: ffi::XPointer,
// This field is unsupplied.
_call_data: ffi::XPointer,
) {
let inner: *mut ImeInner = client_data as _;
if !inner.is_null() {
let xconn = &(*inner).xconn;
let result = replace_im(inner);
if result.is_ok() {
let _ = unset_instantiate_callback(xconn, client_data);
(*inner).is_fallback = false;
} else if result.is_err() && (*inner).is_destroyed {
// We have no usable input methods!
result.expect("Failed to reopen input method");
}
}
}
// This callback is triggered when the input method is closed on the server end. When this
// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been
// free'd (attempting to do so causes our connection to freeze).
pub unsafe extern fn xim_destroy_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
// This field is unsupplied.
_call_data: ffi::XPointer,
) {
let inner: *mut ImeInner = client_data as _;
if !inner.is_null() {
(*inner).is_destroyed = true;
let xconn = &(*inner).xconn;
if !(*inner).is_fallback {
let _ = set_instantiate_callback(xconn, client_data);
// Attempt to open fallback input method.
let result = replace_im(inner);
if result.is_ok() {
(*inner).is_fallback = true;
} else {
// We have no usable input methods!
result.expect("Failed to open fallback input method");
}
}
}
}

View file

@ -0,0 +1,134 @@
use std::ptr;
use std::sync::Arc;
use std::os::raw::{c_short, c_void};
use super::{ffi, util, XConnection, XError};
#[derive(Debug)]
pub enum ImeContextCreationError {
XError(XError),
Null,
}
unsafe fn create_pre_edit_attr<'a>(
xconn: &'a Arc<XConnection>,
ic_spot: &'a ffi::XPoint,
) -> util::XSmartPointer<'a, c_void> {
util::XSmartPointer::new(
xconn,
(xconn.xlib.XVaCreateNestedList)(
0,
ffi::XNSpotLocation_0.as_ptr() as *const _,
ic_spot,
ptr::null_mut::<()>(),
),
).expect("XVaCreateNestedList returned NULL")
}
// WARNING: this struct doesn't destroy its XIC resource when dropped.
// This is intentional, as it doesn't have enough information to know whether or not the context
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
// through `ImeInner`.
#[derive(Debug)]
pub struct ImeContext {
pub ic: ffi::XIC,
pub ic_spot: ffi::XPoint,
}
impl ImeContext {
pub unsafe fn new(
xconn: &Arc<XConnection>,
im: ffi::XIM,
window: ffi::Window,
ic_spot: Option<ffi::XPoint>,
) -> Result<Self, ImeContextCreationError> {
let ic = if let Some(ic_spot) = ic_spot {
ImeContext::create_ic_with_spot(xconn, im, window, ic_spot)
} else {
ImeContext::create_ic(xconn, im, window)
};
let ic = ic.ok_or(ImeContextCreationError::Null)?;
xconn.check_errors().map_err(ImeContextCreationError::XError)?;
Ok(ImeContext {
ic,
ic_spot: ic_spot.unwrap_or_else(|| ffi::XPoint { x: 0, y: 0 }),
})
}
unsafe fn create_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
window: ffi::Window,
) -> Option<ffi::XIC> {
let ic = (xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ptr::null_mut::<()>(),
);
if ic.is_null() {
None
} else {
Some(ic)
}
}
unsafe fn create_ic_with_spot(
xconn: &Arc<XConnection>,
im: ffi::XIM,
window: ffi::Window,
ic_spot: ffi::XPoint,
) -> Option<ffi::XIC> {
let pre_edit_attr = create_pre_edit_attr(xconn, &ic_spot);
let ic = (xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
pre_edit_attr.ptr,
ptr::null_mut::<()>(),
);
if ic.is_null() {
None
} else {
Some(ic)
}
}
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
unsafe {
(xconn.xlib.XSetICFocus)(self.ic);
}
xconn.check_errors()
}
pub fn unfocus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
unsafe {
(xconn.xlib.XUnsetICFocus)(self.ic);
}
xconn.check_errors()
}
pub fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
if self.ic_spot.x == x && self.ic_spot.y == y {
return;
}
self.ic_spot = ffi::XPoint { x, y };
unsafe {
let pre_edit_attr = create_pre_edit_attr(xconn, &self.ic_spot);
(xconn.xlib.XSetICValues)(
self.ic,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
pre_edit_attr.ptr,
ptr::null_mut::<()>(),
);
}
}
}

View file

@ -0,0 +1,75 @@
use std::mem;
use std::ptr;
use std::sync::Arc;
use std::collections::HashMap;
use super::{ffi, XConnection, XError};
use super::input_method::PotentialInputMethods;
use super::context::ImeContext;
pub unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
(xconn.xlib.XCloseIM)(im);
xconn.check_errors()
}
pub unsafe fn destroy_ic(xconn: &Arc<XConnection>, ic: ffi::XIC) -> Result<(), XError> {
(xconn.xlib.XDestroyIC)(ic);
xconn.check_errors()
}
pub struct ImeInner {
pub xconn: Arc<XConnection>,
// WARNING: this is initially null!
pub im: ffi::XIM,
pub potential_input_methods: PotentialInputMethods,
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
// WARNING: this is initially zeroed!
pub destroy_callback: ffi::XIMCallback,
// Indicates whether or not the the input method was destroyed on the server end
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
pub is_destroyed: bool,
pub is_fallback: bool,
}
impl ImeInner {
pub fn new(
xconn: Arc<XConnection>,
potential_input_methods: PotentialInputMethods,
) -> Self {
ImeInner {
xconn,
im: ptr::null_mut(),
potential_input_methods,
contexts: HashMap::new(),
destroy_callback: unsafe { mem::zeroed() },
is_destroyed: false,
is_fallback: false,
}
}
pub unsafe fn close_im_if_necessary(&self) -> Result<bool, XError> {
if !self.is_destroyed {
close_im(&self.xconn, self.im).map(|_| true)
} else {
Ok(false)
}
}
pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result<bool, XError> {
if !self.is_destroyed {
destroy_ic(&self.xconn, ic).map(|_| true)
} else {
Ok(false)
}
}
pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result<bool, XError> {
for context in self.contexts.values() {
if let &Some(ref context) = context {
self.destroy_ic_if_necessary(context.ic)?;
}
}
Ok(!self.is_destroyed)
}
}

View file

@ -0,0 +1,277 @@
use std::env;
use std::fmt;
use std::ptr;
use std::sync::Arc;
use std::os::raw::c_char;
use std::ffi::{CStr, CString, IntoStringError};
use super::{ffi, util, XConnection, XError};
unsafe fn open_im(
xconn: &Arc<XConnection>,
locale_modifiers: &CStr,
) -> Option<ffi::XIM> {
// XSetLocaleModifiers returns...
// * The current locale modifiers if it's given a NULL pointer.
// * The new locale modifiers if we succeeded in setting them.
// * NULL if the locale modifiers string is malformed.
(xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr());
let im = (xconn.xlib.XOpenIM)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
);
if im.is_null() {
None
} else {
Some(im)
}
}
#[derive(Debug)]
pub struct InputMethod {
pub im: ffi::XIM,
name: String,
}
impl InputMethod {
fn new(im: ffi::XIM, name: String) -> Self {
InputMethod { im, name }
}
}
#[derive(Debug)]
pub enum InputMethodResult {
/// Input method used locale modifier from `XMODIFIERS` environment variable.
XModifiers(InputMethod),
/// Input method used internal fallback locale modifier.
Fallback(InputMethod),
/// Input method could not be opened using any locale modifier tried.
Failure,
}
impl InputMethodResult {
pub fn is_fallback(&self) -> bool {
if let &InputMethodResult::Fallback(_) = self {
true
} else {
false
}
}
pub fn ok(self) -> Option<InputMethod> {
use self::InputMethodResult::*;
match self {
XModifiers(im) | Fallback(im) => Some(im),
Failure => None,
}
}
}
#[derive(Debug, Clone)]
enum GetXimServersError {
XError(XError),
GetPropertyError(util::GetPropertyError),
InvalidUtf8(IntoStringError),
}
// The root window has a property named XIM_SERVERS, which contains a list of atoms represeting
// the availabile XIM servers. For instance, if you're using ibus, it would contain an atom named
// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably
// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale
// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set
// XMODIFIERS to `@server=ibus`?!?"
unsafe fn get_xim_servers(xconn: &Arc<XConnection>) -> Result<Vec<String>, GetXimServersError> {
let servers_atom = util::get_atom(&xconn, b"XIM_SERVERS\0")
.map_err(GetXimServersError::XError)?;
let root = (xconn.xlib.XDefaultRootWindow)(xconn.display);
let mut atoms: Vec<ffi::Atom> = util::get_property(
&xconn,
root,
servers_atom,
ffi::XA_ATOM,
).map_err(GetXimServersError::GetPropertyError)?;
let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len());
(xconn.xlib.XGetAtomNames)(
xconn.display,
atoms.as_mut_ptr(),
atoms.len() as _,
names.as_mut_ptr() as _,
);
names.set_len(atoms.len());
let mut formatted_names = Vec::with_capacity(names.len());
for name in names {
let string = CStr::from_ptr(name)
.to_owned()
.into_string()
.map_err(GetXimServersError::InvalidUtf8)?;
(xconn.xlib.XFree)(name as _);
formatted_names.push(string.replace("@server=", "@im="));
}
xconn.check_errors().map_err(GetXimServersError::XError)?;
Ok(formatted_names)
}
#[derive(Clone)]
struct InputMethodName {
c_string: CString,
string: String,
}
impl InputMethodName {
pub fn from_string(string: String) -> Self {
let c_string = CString::new(string.clone())
.expect("String used to construct CString contained null byte");
InputMethodName {
c_string,
string,
}
}
pub fn from_str(string: &str) -> Self {
let c_string = CString::new(string)
.expect("String used to construct CString contained null byte");
InputMethodName {
c_string,
string: string.to_owned(),
}
}
}
impl fmt::Debug for InputMethodName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.string.fmt(f)
}
}
#[derive(Debug, Clone)]
struct PotentialInputMethod {
name: InputMethodName,
successful: Option<bool>,
}
impl PotentialInputMethod {
pub fn from_string(string: String) -> Self {
PotentialInputMethod {
name: InputMethodName::from_string(string),
successful: None,
}
}
pub fn from_str(string: &str) -> Self {
PotentialInputMethod {
name: InputMethodName::from_str(string),
successful: None,
}
}
pub fn reset(&mut self) {
self.successful = None;
}
pub fn open_im(&mut self, xconn: &Arc<XConnection>) -> Option<InputMethod> {
let im = unsafe { open_im(xconn, &self.name.c_string) };
self.successful = Some(im.is_some());
im.map(|im| InputMethod::new(im, self.name.string.clone()))
}
}
// By logging this struct, you get a sequential listing of every locale modifier tried, where it
// came from, and if it succceeded.
#[derive(Debug, Clone)]
pub struct PotentialInputMethods {
// On correctly configured systems, the XMODIFIERS environemnt variable tells us everything we
// need to know.
xmodifiers: Option<PotentialInputMethod>,
// We have some standard options at our disposal that should ostensibly always work. For users
// who only need compose sequences, this ensures that the program launches without a hitch
// For users who need more sophisticated IME features, this is more or less a silent failure.
// Logging features should be added in the future to allow both audiences to be effectively
// served.
fallbacks: [PotentialInputMethod; 2],
// For diagnostic purposes, we include the list of XIM servers that the server reports as
// being available.
_xim_servers: Result<Vec<String>, GetXimServersError>,
}
impl PotentialInputMethods {
pub fn new(xconn: &Arc<XConnection>) -> Self {
let xmodifiers = env::var("XMODIFIERS")
.ok()
.map(PotentialInputMethod::from_string);
PotentialInputMethods {
// Since passing "" to XSetLocaleModifiers results in it defaulting to the value of
// XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply
// running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is
// defined in the profile (or parent environment) then that parent XMODIFIERS is used.
// If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then
// XSetLocaleModifiers uses the default local input method. Note that defining
// XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in
// that case, we get `None` and end up skipping ahead to the next method.
xmodifiers,
fallbacks: [
// This is a standard input method that supports compose equences, which should
// always be available. `@im=none` appears to mean the same thing.
PotentialInputMethod::from_str("@im=local"),
// This explicitly specifies to use the implementation-dependent default, though
// that seems to be equivalent to just using the local input method.
PotentialInputMethod::from_str("@im="),
],
// The XIM_SERVERS property can have surprising values. For instance, when I exited
// ibus to run fcitx, it retained the value denoting ibus. Even more surprising is
// that the fcitx input method could only be successfully opened using "@im=ibus".
// Presumably due to this quirk, it's actually possible to alternate between ibus and
// fcitx in a running application.
_xim_servers: unsafe { get_xim_servers(xconn) },
}
}
// This resets the `successful` field of every potential input method, ensuring we have
// accurate information when this struct is re-used by the destruction/instantiation callbacks.
fn reset(&mut self) {
if let Some(ref mut input_method) = self.xmodifiers {
input_method.reset();
}
for input_method in &mut self.fallbacks {
input_method.reset();
}
}
pub fn open_im(
&mut self,
xconn: &Arc<XConnection>,
callback: Option<&Fn() -> ()>,
) -> InputMethodResult {
use self::InputMethodResult::*;
self.reset();
if let Some(ref mut input_method) = self.xmodifiers {
let im = input_method.open_im(xconn);
if let Some(im) = im {
return XModifiers(im);
} else {
if let Some(ref callback) = callback {
callback();
}
}
}
for input_method in &mut self.fallbacks {
let im = input_method.open_im(xconn);
if let Some(im) = im {
return Fallback(im);
}
}
Failure
}
}

View file

@ -0,0 +1,165 @@
// Important: all XIM calls need to happen from the same thread!
mod inner;
mod input_method;
mod context;
mod callbacks;
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender};
use super::{ffi, util, XConnection, XError};
use self::inner::{close_im, ImeInner};
use self::input_method::PotentialInputMethods;
use self::context::{ImeContextCreationError, ImeContext};
use self::callbacks::*;
pub type ImeReceiver = Receiver<(ffi::Window, i16, i16)>;
pub type ImeSender = Sender<(ffi::Window, i16, i16)>;
#[derive(Debug)]
pub enum ImeCreationError {
OpenFailure(PotentialInputMethods),
SetDestroyCallbackFailed(XError),
}
pub struct Ime {
xconn: Arc<XConnection>,
// The actual meat of this struct is boxed away, since it needs to have a fixed location in
// memory so we can pass a pointer to it around.
inner: Box<ImeInner>,
}
impl Ime {
pub fn new(xconn: Arc<XConnection>) -> Result<Self, ImeCreationError> {
let potential_input_methods = PotentialInputMethods::new(&xconn);
let (mut inner, client_data) = {
let mut inner = Box::new(ImeInner::new(
xconn,
potential_input_methods,
));
let inner_ptr = Box::into_raw(inner);
let client_data = inner_ptr as _;
let destroy_callback = ffi::XIMCallback {
client_data,
callback: Some(xim_destroy_callback),
};
inner = unsafe { Box::from_raw(inner_ptr) };
inner.destroy_callback = destroy_callback;
(inner, client_data)
};
let xconn = Arc::clone(&inner.xconn);
let input_method = inner.potential_input_methods.open_im(&xconn, Some(&|| {
let _ = unsafe { set_instantiate_callback(&xconn, client_data) };
}));
let is_fallback = input_method.is_fallback();
if let Some(input_method) = input_method.ok() {
inner.im = input_method.im;
inner.is_fallback = is_fallback;
unsafe {
let result = set_destroy_callback(&xconn, input_method.im, &*inner)
.map_err(ImeCreationError::SetDestroyCallbackFailed);
if result.is_err() {
let _ = close_im(&xconn, input_method.im);
}
result?;
}
Ok(Ime { xconn, inner })
} else {
Err(ImeCreationError::OpenFailure(inner.potential_input_methods))
}
}
pub fn is_destroyed(&self) -> bool {
self.inner.is_destroyed
}
// This pattern is used for various methods here:
// Ok(_) indicates that nothing went wrong internally
// Ok(true) indicates that the action was actually performed
// Ok(false) indicates that the action is not presently applicable
pub fn create_context(&mut self, window: ffi::Window)
-> Result<bool, ImeContextCreationError>
{
let context = if self.is_destroyed() {
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
None
} else {
Some(unsafe { ImeContext::new(
&self.inner.xconn,
self.inner.im,
window,
None,
) }?)
};
self.inner.contexts.insert(window, context);
Ok(!self.is_destroyed())
}
pub fn get_context(&self, window: ffi::Window) -> Option<ffi::XIC> {
if self.is_destroyed() {
return None;
}
if let Some(&Some(ref context)) = self.inner.contexts.get(&window) {
Some(context.ic)
} else {
None
}
}
pub fn remove_context(&mut self, window: ffi::Window) -> Result<bool, XError> {
if let Some(Some(context)) = self.inner.contexts.remove(&window) {
unsafe {
self.inner.destroy_ic_if_necessary(context.ic)?;
}
Ok(true)
} else {
Ok(false)
}
}
pub fn focus(&mut self, window: ffi::Window) -> Result<bool, XError> {
if self.is_destroyed() {
return Ok(false);
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.focus(&self.xconn).map(|_| true)
} else {
Ok(false)
}
}
pub fn unfocus(&mut self, window: ffi::Window) -> Result<bool, XError> {
if self.is_destroyed() {
return Ok(false);
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.unfocus(&self.xconn).map(|_| true)
} else {
Ok(false)
}
}
pub fn send_xim_spot(&mut self, window: ffi::Window, x: i16, y: i16) {
if self.is_destroyed() {
return;
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.set_spot(&self.xconn, x as _, y as _);
}
}
}
impl Drop for Ime {
fn drop(&mut self) {
unsafe {
let _ = self.inner.destroy_all_contexts_if_necessary();
let _ = self.inner.close_im_if_necessary();
}
}
}

View file

@ -14,22 +14,24 @@ use events::ModifiersState;
use std::{mem, ptr, slice}; use std::{mem, ptr, slice};
use std::sync::{Arc, Mutex, Weak}; use std::sync::{Arc, Mutex, Weak};
use std::sync::atomic::{self, AtomicBool}; use std::sync::atomic::{self, AtomicBool};
use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::mpsc;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::CStr; use std::ffi::CStr;
use std::os::raw::{c_char, c_int, c_long, c_uchar, c_uint, c_ulong}; use std::os::raw::{c_char, c_int, c_long, c_uchar, c_uint, c_ulong};
use libc; use libc::{self, setlocale, LC_CTYPE};
mod events; mod events;
mod monitor; mod monitor;
mod window; mod window;
mod xdisplay; mod xdisplay;
mod dnd; mod dnd;
mod ime;
mod util; mod util;
use self::dnd::{Dnd, DndState}; use self::dnd::{Dnd, DndState};
use self::ime::{ImeReceiver, ImeSender, ImeCreationError, Ime};
// API TRANSITION // API TRANSITION
// //
@ -42,9 +44,9 @@ pub struct EventsLoop {
display: Arc<XConnection>, display: Arc<XConnection>,
wm_delete_window: ffi::Atom, wm_delete_window: ffi::Atom,
dnd: Dnd, dnd: Dnd,
ime_receiver: Receiver<(WindowId, i16, i16)>, ime_receiver: ImeReceiver,
ime_sender: Sender<(WindowId, i16, i16)>, ime_sender: ImeSender,
ime: RefCell<HashMap<WindowId, util::Ime>>, ime: RefCell<Ime>,
windows: Arc<Mutex<HashMap<WindowId, WindowData>>>, windows: Arc<Mutex<HashMap<WindowId, WindowData>>>,
devices: Mutex<HashMap<DeviceId, Device>>, devices: Mutex<HashMap<DeviceId, Device>>,
xi2ext: XExtension, xi2ext: XExtension,
@ -70,7 +72,17 @@ impl EventsLoop {
let dnd = Dnd::new(Arc::clone(&display)) let dnd = Dnd::new(Arc::clone(&display))
.expect("Failed to call XInternAtoms when initializing drag and drop"); .expect("Failed to call XInternAtoms when initializing drag and drop");
let (ime_sender, ime_receiver) = channel(); let (ime_sender, ime_receiver) = mpsc::channel();
// Input methods will open successfully without setting the locale, but it won't be
// possible to actually commit pre-edit sequences.
unsafe { setlocale(LC_CTYPE, b"\0".as_ptr() as *const _); }
let ime = RefCell::new({
let result = Ime::new(Arc::clone(&display));
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
panic!(format!("Failed to open input method: {:#?}", state));
}
result.expect("Failed to set input method destruction callback")
});
let xi2ext = unsafe { let xi2ext = unsafe {
let mut result = XExtension { let mut result = XExtension {
@ -115,7 +127,7 @@ impl EventsLoop {
dnd, dnd,
ime_receiver, ime_receiver,
ime_sender, ime_sender,
ime: RefCell::new(HashMap::new()), ime,
windows: Arc::new(Mutex::new(HashMap::new())), windows: Arc::new(Mutex::new(HashMap::new())),
devices: Mutex::new(HashMap::new()), devices: Mutex::new(HashMap::new()),
xi2ext, xi2ext,
@ -419,7 +431,10 @@ impl EventsLoop {
let window = xev.window; let window = xev.window;
self.ime.borrow_mut().remove(&WindowId(window)); self.ime
.borrow_mut()
.remove_context(window)
.expect("Failed to destroy input context");
} }
ffi::Expose => { ffi::Expose => {
@ -485,8 +500,8 @@ impl EventsLoop {
} }
if state == Pressed { if state == Pressed {
let written = if let Some(ime) = self.ime.borrow().get(&WindowId(window)) { let written = if let Some(ic) = self.ime.borrow().get_context(window) {
unsafe { util::lookup_utf8(&self.display, ime.ic, xkev) } unsafe { util::lookup_utf8(&self.display, ic, xkev) }
} else { } else {
return; return;
}; };
@ -736,11 +751,13 @@ impl EventsLoop {
let window_id = mkwid(xev.event); let window_id = mkwid(xev.event);
if let Some(ime) = self.ime.borrow().get(&WindowId(xev.event)) { if let None = self.windows.lock().unwrap().get(&WindowId(xev.event)) {
ime.focus().expect("Failed to focus input context");
} else {
return; return;
} }
self.ime
.borrow_mut()
.focus(xev.event)
.expect("Failed to focus input context");
callback(Event::WindowEvent { window_id, event: Focused(true) }); callback(Event::WindowEvent { window_id, event: Focused(true) });
@ -764,11 +781,13 @@ impl EventsLoop {
ffi::XI_FocusOut => { ffi::XI_FocusOut => {
let xev: &ffi::XIFocusOutEvent = unsafe { &*(xev.data as *const _) }; let xev: &ffi::XIFocusOutEvent = unsafe { &*(xev.data as *const _) };
if let Some(ime) = self.ime.borrow().get(&WindowId(xev.event)) { if let None = self.windows.lock().unwrap().get(&WindowId(xev.event)) {
ime.unfocus().expect("Failed to unfocus input context");
} else {
return; return;
} }
self.ime
.borrow_mut()
.unfocus(xev.event)
.expect("Failed to unfocus input context");
callback(Event::WindowEvent { callback(Event::WindowEvent {
window_id: mkwid(xev.event), window_id: mkwid(xev.event),
@ -888,9 +907,7 @@ impl EventsLoop {
match self.ime_receiver.try_recv() { match self.ime_receiver.try_recv() {
Ok((window_id, x, y)) => { Ok((window_id, x, y)) => {
if let Some(ime) = self.ime.borrow_mut().get_mut(&window_id) { self.ime.borrow_mut().send_xim_spot(window_id, x, y);
ime.send_xim_spot(x, y);
}
} }
Err(_) => () Err(_) => ()
} }
@ -987,7 +1004,7 @@ pub struct Window {
pub window: Arc<Window2>, pub window: Arc<Window2>,
display: Weak<XConnection>, display: Weak<XConnection>,
windows: Weak<Mutex<HashMap<WindowId, WindowData>>>, windows: Weak<Mutex<HashMap<WindowId, WindowData>>>,
ime_sender: Sender<(WindowId, i16, i16)>, ime_sender: ImeSender,
} }
impl ::std::ops::Deref for Window { impl ::std::ops::Deref for Window {
@ -1006,9 +1023,10 @@ impl Window {
) -> Result<Self, CreationError> { ) -> Result<Self, CreationError> {
let win = Arc::new(try!(Window2::new(&x_events_loop, window, pl_attribs))); let win = Arc::new(try!(Window2::new(&x_events_loop, window, pl_attribs)));
let ime = util::Ime::new(Arc::clone(&x_events_loop.display), win.id().0) x_events_loop.ime
.expect("Failed to initialize IME"); .borrow_mut()
x_events_loop.ime.borrow_mut().insert(win.id(), ime); .create_context(win.id().0)
.expect("Failed to create input context");
x_events_loop.windows.lock().unwrap().insert(win.id(), WindowData { x_events_loop.windows.lock().unwrap().insert(win.id(), WindowData {
config: None, config: None,
@ -1031,7 +1049,7 @@ impl Window {
#[inline] #[inline]
pub fn send_xim_spot(&self, x: i16, y: i16) { pub fn send_xim_spot(&self, x: i16, y: i16) {
let _ = self.ime_sender.send((self.window.id(), x, y)); let _ = self.ime_sender.send((self.window.id().0, x, y));
} }
} }

View file

@ -90,7 +90,7 @@ pub unsafe fn send_client_msg(
xconn.check_errors().map(|_| ()) xconn.check_errors().map(|_| ())
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum GetPropertyError { pub enum GetPropertyError {
XError(XError), XError(XError),
TypeMismatch(ffi::Atom), TypeMismatch(ffi::Atom),
@ -309,7 +309,6 @@ pub unsafe fn lookup_utf8(
str::from_utf8(&buffer[..count as usize]).unwrap_or("").to_string() str::from_utf8(&buffer[..count as usize]).unwrap_or("").to_string()
} }
#[derive(Debug)] #[derive(Debug)]
pub struct FrameExtents { pub struct FrameExtents {
pub left: c_ulong, pub left: c_ulong,
@ -364,114 +363,3 @@ impl WindowGeometry {
) )
} }
} }
// Important: all XIM calls need to happen from the same thread!
pub struct Ime {
xconn: Arc<XConnection>,
pub im: ffi::XIM,
pub ic: ffi::XIC,
ic_spot: ffi::XPoint,
}
impl Ime {
pub fn new(xconn: Arc<XConnection>, window: ffi::Window) -> Option<Self> {
let im = unsafe {
let mut im: ffi::XIM = ptr::null_mut();
// Setting an empty string as the locale modifier results in the user's XMODIFIERS
// environment variable being read, which should result in the user's configured input
// method (ibus, fcitx, etc.) being used. If that fails, we fall back to internal
// input methods which should always be available, though only support compose keys.
for modifiers in &[b"\0" as &[u8], b"@im=local\0", b"@im=\0"] {
if !im.is_null() {
break;
}
(xconn.xlib.XSetLocaleModifiers)(modifiers.as_ptr() as *const _);
im = (xconn.xlib.XOpenIM)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
);
}
if im.is_null() {
return None;
}
im
};
let ic = unsafe {
let ic = (xconn.xlib.XCreateIC)(
im,
b"inputStyle\0".as_ptr() as *const _,
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
b"clientWindow\0".as_ptr() as *const _,
window,
ptr::null::<()>(),
);
if ic.is_null() {
return None;
}
(xconn.xlib.XSetICFocus)(ic);
xconn.check_errors().expect("Failed to call XSetICFocus");
ic
};
Some(Ime {
xconn,
im,
ic,
ic_spot: ffi::XPoint { x: 0, y: 0 },
})
}
pub fn focus(&self) -> Result<(), XError> {
unsafe {
(self.xconn.xlib.XSetICFocus)(self.ic);
}
self.xconn.check_errors()
}
pub fn unfocus(&self) -> Result<(), XError> {
unsafe {
(self.xconn.xlib.XUnsetICFocus)(self.ic);
}
self.xconn.check_errors()
}
pub fn send_xim_spot(&mut self, x: i16, y: i16) {
let nspot = ffi::XPoint { x: x as _, y: y as _ };
if self.ic_spot.x == x && self.ic_spot.y == y {
return;
}
self.ic_spot = nspot;
unsafe {
let preedit_attr = (self.xconn.xlib.XVaCreateNestedList)(
0,
b"spotLocation\0",
&nspot,
ptr::null::<()>(),
);
(self.xconn.xlib.XSetICValues)(
self.ic,
b"preeditAttributes\0",
preedit_attr,
ptr::null::<()>(),
);
(self.xconn.xlib.XFree)(preedit_attr);
}
}
}
impl Drop for Ime {
fn drop(&mut self) {
unsafe {
(self.xconn.xlib.XDestroyIC)(self.ic);
(self.xconn.xlib.XCloseIM)(self.im);
}
self.xconn.check_errors().expect("Failed to close input method");
}
}