From 09c809003b4465717ba6055c552be2ab1189864b Mon Sep 17 00:00:00 2001 From: Francesca Frangipane Date: Tue, 10 Apr 2018 22:18:30 -0400 Subject: [PATCH] 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. --- CHANGELOG.md | 1 + src/platform/linux/x11/ime/callbacks.rs | 185 ++++++++++++++ src/platform/linux/x11/ime/context.rs | 134 ++++++++++ src/platform/linux/x11/ime/inner.rs | 75 ++++++ src/platform/linux/x11/ime/input_method.rs | 277 +++++++++++++++++++++ src/platform/linux/x11/ime/mod.rs | 165 ++++++++++++ src/platform/linux/x11/mod.rs | 66 +++-- src/platform/linux/x11/util.rs | 114 +-------- 8 files changed, 880 insertions(+), 137 deletions(-) create mode 100644 src/platform/linux/x11/ime/callbacks.rs create mode 100644 src/platform/linux/x11/ime/context.rs create mode 100644 src/platform/linux/x11/ime/inner.rs create mode 100644 src/platform/linux/x11/ime/input_method.rs create mode 100644 src/platform/linux/x11/ime/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a6311072..37ee791b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - Fixed SIGSEGV/SIGILL crashes on macOS caused by stabilization of the `!` (never) type. - 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) diff --git a/src/platform/linux/x11/ime/callbacks.rs b/src/platform/linux/x11/ime/callbacks.rs new file mode 100644 index 00000000..0618b233 --- /dev/null +++ b/src/platform/linux/x11/ime/callbacks.rs @@ -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, + 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, + 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, + 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, + 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"); + } + } + } +} diff --git a/src/platform/linux/x11/ime/context.rs b/src/platform/linux/x11/ime/context.rs new file mode 100644 index 00000000..598cd486 --- /dev/null +++ b/src/platform/linux/x11/ime/context.rs @@ -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, + 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, + im: ffi::XIM, + window: ffi::Window, + ic_spot: Option, + ) -> Result { + 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, + im: ffi::XIM, + window: ffi::Window, + ) -> Option { + 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, + im: ffi::XIM, + window: ffi::Window, + ic_spot: ffi::XPoint, + ) -> Option { + 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) -> Result<(), XError> { + unsafe { + (xconn.xlib.XSetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub fn unfocus(&self, xconn: &Arc) -> Result<(), XError> { + unsafe { + (xconn.xlib.XUnsetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub fn set_spot(&mut self, xconn: &Arc, 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::<()>(), + ); + } + } +} diff --git a/src/platform/linux/x11/ime/inner.rs b/src/platform/linux/x11/ime/inner.rs new file mode 100644 index 00000000..34bfbe7b --- /dev/null +++ b/src/platform/linux/x11/ime/inner.rs @@ -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, im: ffi::XIM) -> Result<(), XError> { + (xconn.xlib.XCloseIM)(im); + xconn.check_errors() +} + +pub unsafe fn destroy_ic(xconn: &Arc, ic: ffi::XIC) -> Result<(), XError> { + (xconn.xlib.XDestroyIC)(ic); + xconn.check_errors() +} + +pub struct ImeInner { + pub xconn: Arc, + // WARNING: this is initially null! + pub im: ffi::XIM, + pub potential_input_methods: PotentialInputMethods, + pub contexts: HashMap>, + // 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, + 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 { + 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 { + if !self.is_destroyed { + destroy_ic(&self.xconn, ic).map(|_| true) + } else { + Ok(false) + } + } + + pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result { + for context in self.contexts.values() { + if let &Some(ref context) = context { + self.destroy_ic_if_necessary(context.ic)?; + } + } + Ok(!self.is_destroyed) + } +} diff --git a/src/platform/linux/x11/ime/input_method.rs b/src/platform/linux/x11/ime/input_method.rs new file mode 100644 index 00000000..185a9d20 --- /dev/null +++ b/src/platform/linux/x11/ime/input_method.rs @@ -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, + locale_modifiers: &CStr, +) -> Option { + // 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 { + 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) -> Result, 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 = 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, +} + +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) -> Option { + 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, + // 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, GetXimServersError>, +} + +impl PotentialInputMethods { + pub fn new(xconn: &Arc) -> 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, + 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 + } +} diff --git a/src/platform/linux/x11/ime/mod.rs b/src/platform/linux/x11/ime/mod.rs new file mode 100644 index 00000000..2298050c --- /dev/null +++ b/src/platform/linux/x11/ime/mod.rs @@ -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, + // 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, +} + +impl Ime { + pub fn new(xconn: Arc) -> Result { + 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 + { + 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 { + 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 { + 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 { + 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 { + 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(); + } + } +} diff --git a/src/platform/linux/x11/mod.rs b/src/platform/linux/x11/mod.rs index d7e227cb..f0e0fa01 100644 --- a/src/platform/linux/x11/mod.rs +++ b/src/platform/linux/x11/mod.rs @@ -14,22 +14,24 @@ use events::ModifiersState; use std::{mem, ptr, slice}; use std::sync::{Arc, Mutex, Weak}; use std::sync::atomic::{self, AtomicBool}; -use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::mpsc; use std::cell::RefCell; use std::collections::HashMap; use std::ffi::CStr; 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 monitor; mod window; mod xdisplay; mod dnd; +mod ime; mod util; use self::dnd::{Dnd, DndState}; +use self::ime::{ImeReceiver, ImeSender, ImeCreationError, Ime}; // API TRANSITION // @@ -42,9 +44,9 @@ pub struct EventsLoop { display: Arc, wm_delete_window: ffi::Atom, dnd: Dnd, - ime_receiver: Receiver<(WindowId, i16, i16)>, - ime_sender: Sender<(WindowId, i16, i16)>, - ime: RefCell>, + ime_receiver: ImeReceiver, + ime_sender: ImeSender, + ime: RefCell, windows: Arc>>, devices: Mutex>, xi2ext: XExtension, @@ -70,7 +72,17 @@ impl EventsLoop { let dnd = Dnd::new(Arc::clone(&display)) .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 mut result = XExtension { @@ -115,7 +127,7 @@ impl EventsLoop { dnd, ime_receiver, ime_sender, - ime: RefCell::new(HashMap::new()), + ime, windows: Arc::new(Mutex::new(HashMap::new())), devices: Mutex::new(HashMap::new()), xi2ext, @@ -419,7 +431,10 @@ impl EventsLoop { 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 => { @@ -485,8 +500,8 @@ impl EventsLoop { } if state == Pressed { - let written = if let Some(ime) = self.ime.borrow().get(&WindowId(window)) { - unsafe { util::lookup_utf8(&self.display, ime.ic, xkev) } + let written = if let Some(ic) = self.ime.borrow().get_context(window) { + unsafe { util::lookup_utf8(&self.display, ic, xkev) } } else { return; }; @@ -736,11 +751,13 @@ impl EventsLoop { let window_id = mkwid(xev.event); - if let Some(ime) = self.ime.borrow().get(&WindowId(xev.event)) { - ime.focus().expect("Failed to focus input context"); - } else { + if let None = self.windows.lock().unwrap().get(&WindowId(xev.event)) { return; } + self.ime + .borrow_mut() + .focus(xev.event) + .expect("Failed to focus input context"); callback(Event::WindowEvent { window_id, event: Focused(true) }); @@ -764,11 +781,13 @@ impl EventsLoop { ffi::XI_FocusOut => { let xev: &ffi::XIFocusOutEvent = unsafe { &*(xev.data as *const _) }; - if let Some(ime) = self.ime.borrow().get(&WindowId(xev.event)) { - ime.unfocus().expect("Failed to unfocus input context"); - } else { + if let None = self.windows.lock().unwrap().get(&WindowId(xev.event)) { return; } + self.ime + .borrow_mut() + .unfocus(xev.event) + .expect("Failed to unfocus input context"); callback(Event::WindowEvent { window_id: mkwid(xev.event), @@ -888,9 +907,7 @@ impl EventsLoop { match self.ime_receiver.try_recv() { Ok((window_id, x, y)) => { - if let Some(ime) = self.ime.borrow_mut().get_mut(&window_id) { - ime.send_xim_spot(x, y); - } + self.ime.borrow_mut().send_xim_spot(window_id, x, y); } Err(_) => () } @@ -987,7 +1004,7 @@ pub struct Window { pub window: Arc, display: Weak, windows: Weak>>, - ime_sender: Sender<(WindowId, i16, i16)>, + ime_sender: ImeSender, } impl ::std::ops::Deref for Window { @@ -1006,9 +1023,10 @@ impl Window { ) -> Result { 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) - .expect("Failed to initialize IME"); - x_events_loop.ime.borrow_mut().insert(win.id(), ime); + x_events_loop.ime + .borrow_mut() + .create_context(win.id().0) + .expect("Failed to create input context"); x_events_loop.windows.lock().unwrap().insert(win.id(), WindowData { config: None, @@ -1031,7 +1049,7 @@ impl Window { #[inline] 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)); } } diff --git a/src/platform/linux/x11/util.rs b/src/platform/linux/x11/util.rs index e3297dd0..5f185ac9 100644 --- a/src/platform/linux/x11/util.rs +++ b/src/platform/linux/x11/util.rs @@ -90,7 +90,7 @@ pub unsafe fn send_client_msg( xconn.check_errors().map(|_| ()) } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum GetPropertyError { XError(XError), TypeMismatch(ffi::Atom), @@ -309,7 +309,6 @@ pub unsafe fn lookup_utf8( str::from_utf8(&buffer[..count as usize]).unwrap_or("").to_string() } - #[derive(Debug)] pub struct FrameExtents { 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, - pub im: ffi::XIM, - pub ic: ffi::XIC, - ic_spot: ffi::XPoint, -} - -impl Ime { - pub fn new(xconn: Arc, window: ffi::Window) -> Option { - 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"); - } -}