From 5c9f1b2e9a605be7465c13378b34c116271d91e3 Mon Sep 17 00:00:00 2001 From: John Nunley Date: Fri, 29 Sep 2023 19:25:20 -0700 Subject: [PATCH] Switch to using xim for IME This is broken until we start using x11rb for event lookups. Signed-off-by: John Nunley --- Cargo.toml | 3 +- src/platform_impl/linux/x11/atoms.rs | 1 - .../linux/x11/event_processor.rs | 170 ++-- src/platform_impl/linux/x11/ime.rs | 816 ++++++++++++++++++ src/platform_impl/linux/x11/ime/callbacks.rs | 215 ----- src/platform_impl/linux/x11/ime/context.rs | 385 --------- src/platform_impl/linux/x11/ime/inner.rs | 75 -- .../linux/x11/ime/input_method.rs | 370 -------- src/platform_impl/linux/x11/ime/mod.rs | 249 ------ src/platform_impl/linux/x11/mod.rs | 82 +- src/platform_impl/linux/x11/util/input.rs | 54 -- src/platform_impl/linux/x11/util/memory.rs | 12 - src/platform_impl/linux/x11/window.rs | 27 +- 13 files changed, 949 insertions(+), 1510 deletions(-) create mode 100644 src/platform_impl/linux/x11/ime.rs delete mode 100644 src/platform_impl/linux/x11/ime/callbacks.rs delete mode 100644 src/platform_impl/linux/x11/ime/context.rs delete mode 100644 src/platform_impl/linux/x11/ime/inner.rs delete mode 100644 src/platform_impl/linux/x11/ime/input_method.rs delete mode 100644 src/platform_impl/linux/x11/ime/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 6997b34fb..ce370e7c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["rwh_06", "x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"] +x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb", "xim"] wayland = ["wayland-client", "wayland-backend", "wayland-protocols", "wayland-protocols-plasma", "sctk", "ahash", "memmap2"] wayland-dlopen = ["wayland-backend/dlopen"] wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] @@ -167,6 +167,7 @@ wayland-protocols = { version = "0.31.0", features = [ "staging"], optional = tr wayland-protocols-plasma = { version = "0.2.0", features = [ "client" ], optional = true } x11-dl = { version = "2.18.5", optional = true } x11rb = { version = "0.13.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "randr", "resource_manager", "xinput", "xkb"], optional = true } +xim = { version = "0.3.0", features = ["x11rb-client", "client"], optional = true } xkbcommon-dl = "0.4.0" [target.'cfg(target_os = "redox")'.dependencies] diff --git a/src/platform_impl/linux/x11/atoms.rs b/src/platform_impl/linux/x11/atoms.rs index bfdef98ef..f6c2d32dc 100644 --- a/src/platform_impl/linux/x11/atoms.rs +++ b/src/platform_impl/linux/x11/atoms.rs @@ -40,7 +40,6 @@ atom_manager! { WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, - XIM_SERVERS, // Assorted ICCCM Atoms _NET_WM_ICON, diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 6395d3582..981da8647 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -4,7 +4,7 @@ use std::{ os::raw::{c_char, c_int, c_long, c_ulong}, rc::Rc, slice, - sync::{Arc, Mutex}, + sync::{mpsc, Arc, Mutex}, }; use x11rb::x11_utils::Serialize; @@ -17,10 +17,11 @@ use x11rb::{ }; use super::{ - atoms::*, ffi, get_xtarget, mkdid, mkwid, util, CookieResultExt, Device, DeviceId, DeviceInfo, - Dnd, DndState, GenericEventCookie, ImeReceiver, ScrollOrientation, UnownedWindow, WindowId, + atoms::*, ffi, get_xtarget, ime, mkdid, mkwid, util, CookieResultExt, Device, DeviceId, + DeviceInfo, Dnd, DndState, GenericEventCookie, ScrollOrientation, UnownedWindow, WindowId, }; +use crate::event::InnerSizeWriter; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, event::{DeviceEvent, ElementState, Event, Ime, RawKeyEvent, TouchPhase, WindowEvent}, @@ -28,18 +29,14 @@ use crate::{ keyboard::ModifiersState, platform_impl::platform::common::{keymap, xkb_state::KbdState}, }; -use crate::{ - event::InnerSizeWriter, - platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest}, -}; /// The X11 documentation states: "Keycodes lie in the inclusive range `[8, 255]`". const KEYCODE_OFFSET: u8 = 8; pub(super) struct EventProcessor { pub(super) dnd: Dnd, - pub(super) ime_receiver: ImeReceiver, - pub(super) ime_event_receiver: ImeEventReceiver, + /// Requests from other threads for IME. + pub(super) ime_requests: mpsc::Receiver, pub(super) randr_event_offset: u8, pub(super) devices: RefCell>, pub(super) xi2ext: ExtensionInformation, @@ -560,10 +557,11 @@ impl EventProcessor { // Since all XIM stuff needs to happen from the same thread, we destroy the input // context here instead of when dropping the window. - wt.ime - .borrow_mut() - .remove_context(window as ffi::Window) - .expect("Failed to destroy input context"); + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .remove_context(window) + .expect("Failed to destroy input context"); + } callback(Event::WindowEvent { window_id, @@ -669,23 +667,6 @@ impl EventProcessor { is_synthetic: false, }, }); - } else if let Some(ic) = wt.ime.borrow().get_context(window as ffi::Window) { - let written = wt.xconn.lookup_utf8(ic, xkev); - if !written.is_empty() { - let event = Event::WindowEvent { - window_id, - event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), - }; - callback(event); - - let event = Event::WindowEvent { - window_id, - event: WindowEvent::Ime(Ime::Commit(written)), - }; - - self.is_composing = false; - callback(event); - } } } @@ -970,10 +951,11 @@ impl EventProcessor { // Set the timestamp. wt.xconn.set_timestamp(xev.time as xproto::Timestamp); - wt.ime - .borrow_mut() - .focus(xev.event) - .expect("Failed to focus input context"); + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .focus_window(window) + .expect("Failed to focus input context"); + } if self.active_window != Some(window) { self.active_window = Some(window); @@ -1039,10 +1021,11 @@ impl EventProcessor { return; } - wt.ime - .borrow_mut() - .unfocus(xev.event) - .expect("Failed to unfocus input context"); + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .unfocus_window(window) + .expect("Failed to focus input context"); + } if self.active_window.take() == Some(window) { let window_id = mkwid(window); @@ -1319,60 +1302,79 @@ impl EventProcessor { } } - // Handle IME requests. - if let Ok(request) = self.ime_receiver.try_recv() { - let mut ime = wt.ime.borrow_mut(); - match request { - ImeRequest::Position(window_id, x, y) => { - ime.send_xim_spot(window_id, x, y); - } - ImeRequest::Allow(window_id, allowed) => { - ime.set_ime_allowed(window_id, allowed); + if let Some(ime) = wt.ime.as_ref() { + let mut ime = ime.borrow_mut(); + + // Handle IME requests. + if let Ok(request) = self.ime_requests.try_recv() { + match request { + ime::ImeRequest::Position(window_id, x, y) => { + ime.set_spot(window_id, x, y) + .expect("Failed to set IME spot"); + } + ime::ImeRequest::Allow(window_id, allowed) => { + ime.set_ime_allowed(window_id, allowed) + .expect("Failed to set IME allowed"); + } } } - } - let (window, event) = match self.ime_event_receiver.try_recv() { - Ok((window, event)) => (window as xproto::Window, event), - Err(_) => return, - }; + let (window, event) = match ime.next_ime_event() { + Some((window, event)) => (window, event), + None => return, + }; - match event { - ImeEvent::Enabled => { - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Enabled), - }); - } - ImeEvent::Start => { - self.is_composing = true; - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Preedit("".to_owned(), None)), - }); - } - ImeEvent::Update(text, position) => { - if self.is_composing { + match event { + ime::ImeEvent::Enabled => { callback(Event::WindowEvent { window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))), + event: WindowEvent::Ime(Ime::Enabled), + }); + } + ime::ImeEvent::Start => { + self.is_composing = true; + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Preedit("".to_owned(), None)), + }); + } + ime::ImeEvent::Update(text, position) => { + if self.is_composing { + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Preedit( + text, + position.map(|position| (position, position)), + )), + }); + } + } + ime::ImeEvent::Commit(text) => { + self.is_composing = false; + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + ime::ImeEvent::End => { + self.is_composing = false; + // Issue empty preedit on `Done`. + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + } + ime::ImeEvent::Disabled => { + self.is_composing = false; + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Disabled), }); } - } - ImeEvent::End => { - self.is_composing = false; - // Issue empty preedit on `Done`. - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), - }); - } - ImeEvent::Disabled => { - self.is_composing = false; - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Disabled), - }); } } } diff --git a/src/platform_impl/linux/x11/ime.rs b/src/platform_impl/linux/x11/ime.rs new file mode 100644 index 000000000..a59f8c267 --- /dev/null +++ b/src/platform_impl/linux/x11/ime.rs @@ -0,0 +1,816 @@ +//! IME handler, using the xim-rs crate. + +use super::{X11Error, X11rbConnection, XConnection}; + +use x11rb::connection::Connection; +use x11rb::protocol::xproto::Window; +use x11rb::protocol::Event; + +use xim::x11rb::{HasConnection, X11rbClient}; +use xim::{AttributeName, Client as _, ClientError, ClientHandler, InputStyle, Point}; + +use std::collections::{HashMap, VecDeque}; +use std::fmt; +use std::sync::Arc; + +impl HasConnection for XConnection { + type Connection = X11rbConnection; + + fn conn(&self) -> &Self::Connection { + self.xcb_connection() + } +} + +/// A collection of the IME events that can occur. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ImeEvent { + Enabled, + Start, + Update(String, Option), + Commit(String), + End, + Disabled, +} + +/// Invalid states that an IME client can enter. +#[derive(Debug, Clone)] +pub enum InvalidImeState { + /// The IME has no style information. + NoStyle, + + /// No windows in the pending window queue. + NoWindows, + + /// Invalid input context. + InvalidIc(u16), +} + +impl fmt::Display for InvalidImeState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InvalidImeState::NoStyle => write!(f, "IME has no style information"), + InvalidImeState::NoWindows => { + write!(f, "IME has no windows in the pending window queue") + } + InvalidImeState::InvalidIc(ic) => write!(f, "IME has invalid input context {}", ic), + } + } +} + +/// Request to control XIM handler from the window. +pub enum ImeRequest { + /// Set IME spot position for given `window_id`. + Position(Window, i16, i16), + + /// Allow IME input for the given `window_id`. + Allow(Window, bool), +} + +/// The IME data for winit. +pub(super) struct ImeData { + /// The XIM client manager. + client: X11rbClient>, + + /// Relevant IME data. + handler: ImeHandler, +} + +/// Inner IME handler. +struct ImeHandler { + /// Whether IME is currently disconnected. + disconnected: bool, + + /// IME events waiting to be read. + ime_events: VecDeque<(Window, ImeEvent)>, + + /// Windows waiting to be assigned an input context. + pending_windows: VecDeque, + + /// Currently registered input styles. + styles: Option<(Style, Style)>, + + /// The input method for the display, if there is one. + input_method: Option, + + /// Hash map between input contexts and their associated data. + input_contexts: HashMap, + + /// Map between window IDs and their associated input contexts. + window_contexts: HashMap, +} + +/// Data relevant for each input context. +struct IcData { + /// Data associated with the window. + window: WindowData, + + /// Newly set point for the context. + new_spot: Option, + + /// The current preedit string. + /// + /// We use a `Vec` here instead of a string because the IME indices operate on chars, + /// not bytes. + text: Vec, + + /// The current cursor position in the preedit string. + cursor: usize, +} + +/// Windows waiting for IME events. +struct WindowData { + /// The window ID. + id: Window, + + /// The style of the window. + style: Style, + + /// Current "spot" for the context. + spot: Point, +} + +#[derive(Copy, Clone)] +enum Style { + Preedit, + Nothing, + None, +} + +impl ImeData { + /// Creates the IME data for the display. + pub(super) fn new(conn: &Arc, screen: usize) -> Result { + // IM servers to try, in order: + // - None, which defaults to the environment variable `XMODIFIERS` in xim's impl. + // - "local", which is the default for most IMEs. + // - empty string, which may work in some cases. + let input_methods = [None, Some("local"), Some("")]; + let mut last_error = X11Error::Ime(ClientError::NoXimServer); + + for im in input_methods { + // Try to initialize a client here. + match X11rbClient::init(conn.clone(), screen, im) { + Ok(client) => { + return Ok(Self { + client, + handler: ImeHandler { + disconnected: true, + ime_events: VecDeque::new(), + pending_windows: VecDeque::new(), + styles: None, + input_method: None, + input_contexts: HashMap::new(), + window_contexts: HashMap::new(), + }, + }) + } + + Err(err) => { + log::warn!("Failed to create XIM client for {:?}: {err}", ImData(im)); + last_error = X11Error::Ime(err); + } + } + } + + Err(last_error) + } + + /// Filter an event. + pub(super) fn filter_event(&mut self, event: &Event) -> Result { + self.client + .filter_event(event, &mut self.handler) + .map_err(X11Error::Ime) + } + + /// Connection to the X server. + fn conn(&self) -> &X11rbConnection { + self.client.conn() + } + + /// Get an IME event. + pub(super) fn next_ime_event(&mut self) -> Option<(Window, ImeEvent)> { + self.handler.ime_events.pop_front() + } + + /// Create a new IME context for the provided window. + pub(super) fn create_context( + &mut self, + window: Window, + with_preedit: bool, + spot: Option, + ) -> Result { + // If we aren't connected, nothing can be done. + if self.handler.disconnected { + return Ok(false); + } + let method = match self.handler.input_method { + Some(im) => im, + None => return Ok(false), + }; + + // Get the current style. + let style = match (self.handler.styles, with_preedit) { + (None, _) => return Err(X11Error::InvalidImeState(InvalidImeState::NoStyle)), + (Some((preedit_style, _)), true) => preedit_style, + (Some((_, none_style)), false) => none_style, + }; + + // Setup IC attributes. + let ic_attributes = { + let mut ic_attributes = self + .client + .build_ic_attributes() + .push(AttributeName::ClientWindow, window); + + let ic_style = match style { + Style::Preedit => InputStyle::PREEDIT_POSITION | InputStyle::STATUS_NOTHING, + Style::Nothing => InputStyle::PREEDIT_NOTHING | InputStyle::STATUS_NOTHING, + Style::None => InputStyle::PREEDIT_NONE | InputStyle::STATUS_NONE, + }; + + if let Some(spot) = spot.clone() { + ic_attributes = ic_attributes.push(AttributeName::SpotLocation, spot); + } + + ic_attributes + .push(AttributeName::InputStyle, ic_style) + .build() + }; + + // Create the IC. + self.client.create_ic(method, ic_attributes)?; + + // Add to the waiting window list. + self.handler.pending_windows.push_back(WindowData { + id: window, + style, + spot: spot.unwrap_or(Point { x: 0, y: 0 }), + }); + + Ok(true) + } + + /// Remove an IME context for a window. + pub(super) fn remove_context(&mut self, window: Window) -> Result { + if self.handler.disconnected { + return Ok(false); + } + let method = match self.handler.input_method { + Some(im) => im, + None => return Ok(false), + }; + + // Remove the pending window if it's still pending. + let mut removed = false; + self.handler.pending_windows.retain(|pending| { + if pending.id == window { + removed = true; + false + } else { + true + } + }); + + if removed { + return Ok(true); + } + + // Remove the IC if it's already created. + if let Some(ic) = self.handler.window_contexts.remove(&window) { + self.handler.input_contexts.remove(&ic); + + // Destroy the IC. + self.client.destroy_ic(method, ic)?; + } + + Ok(false) + } + + /// Focus an IME context. + pub(super) fn focus_window(&mut self, window: Window) -> Result { + if self.handler.disconnected { + return Ok(false); + } + + let method = self.wait_for_method()?; + let ic = self.wait_for_context(window)?; + + if let Some(ic) = ic { + self.client.set_focus(method, ic)?; + return Ok(true); + } + + Ok(false) + } + + /// Unfocus an IME context. + pub(super) fn unfocus_window(&mut self, window: Window) -> Result { + if self.handler.disconnected { + return Ok(false); + } + + let method = self.wait_for_method()?; + let ic = self.wait_for_context(window)?; + + if let Some(ic) = ic { + self.client.unset_focus(method, ic)?; + return Ok(true); + } + + Ok(false) + } + + /// Set the spot for an IME context. + pub(super) fn set_spot(&mut self, window: Window, x: i16, y: i16) -> Result<(), X11Error> { + if self.handler.disconnected { + return Ok(()); + } + + let method = self.wait_for_method()?; + let ic = self.wait_for_context(window)?; + + if let Some(ic) = ic { + // If the IC is not available, or if the spot is the same, then we don't need to update. + let ic_data = match self.handler.input_contexts.get_mut(&ic) { + Some(ic_data) => ic_data, + None => return Ok(()), + }; + + let new_point = Point { x, y }; + if !matches!(ic_data.window.style, Style::None) || ic_data.window.spot == new_point { + return Ok(()); + } + + let new_attrs = self + .client + .build_ic_attributes() + .push(AttributeName::SpotLocation, new_point.clone()) + .build(); + self.client.set_ic_values(method, ic, new_attrs)?; + + // Indicate that we have a new spot. + debug_assert!(ic_data.new_spot.is_none()); + ic_data.new_spot = Some(new_point); + } + + Ok(()) + } + + pub(super) fn set_ime_allowed( + &mut self, + window: Window, + allowed: bool, + ) -> Result<(), X11Error> { + if self.handler.disconnected { + return Ok(()); + } + + // Get the client info. + let _ = self.wait_for_method()?; + let ic = self.wait_for_context(window)?; + + if let Some(ic) = ic { + let mut spot = None; + + // See if we need to update the allowed state. + if let Some(ic_data) = self.handler.input_contexts.get(&ic) { + spot = Some(ic_data.window.spot.clone()); + if matches!(ic_data.window.style, Style::None) != allowed { + return Ok(()); + } + } + + // Delete and re-install the IC. + self.remove_context(window)?; + self.create_context(window, allowed, spot)?; + } + + Ok(()) + } + + /// Wait for the input method to be set. + fn wait_for_method(&mut self) -> Result { + loop { + if let Some(im) = self.handler.input_method { + return Ok(im); + } + + // Wait and hope the input method is set. + self.block_for_ime()?; + } + } + + /// Wait for an input context to be set. + fn wait_for_context(&mut self, window: Window) -> Result, X11Error> { + if let Some(cid) = self.handler.window_contexts.get(&window) { + return Ok(Some(*cid)); + } + + // If the window isn't in our pending windows queue, there's no way for it to get an IC. + if !self + .handler + .pending_windows + .iter() + .any(|WindowData { id, .. }| *id == window) + { + return Ok(None); + } + + loop { + self.block_for_ime()?; + if let Some(cid) = self.handler.window_contexts.get(&window) { + return Ok(Some(*cid)); + } + } + } + + /// Wait until we've acted on an IME event. + fn block_for_ime(&mut self) -> Result<(), X11Error> { + let mut last_event = self.conn().poll_for_event()?; + + loop { + if let Some(last_event) = last_event { + if self.filter_event(&last_event)? { + return Ok(()); + } + } + + // TODO: have an queue queue. + log::info!("Waiting for IME event"); + last_event = Some(self.conn().wait_for_event()?); + } + } +} + +impl ClientHandler for ImeHandler { + fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> { + // We have been connected, now request a new input method for our current locale. + self.disconnected = false; + client.open(&locale()) + } + + fn handle_disconnect(&mut self) { + // We are now disconnected. + self.disconnected = true; + } + + fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> { + // We now have an input method. + debug_assert!(self.input_method.is_none()); + self.input_method = Some(input_method_id); + + // Ask for the IM's attributes. + client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle]) + } + + fn handle_close(&mut self, _client: &mut C, input_method_id: u16) -> Result<(), ClientError> { + // No more input method. + debug_assert_eq!(self.input_method, Some(input_method_id)); + self.input_method = None; + + Ok(()) + } + + fn handle_get_im_values( + &mut self, + _client: &mut C, + input_method_id: u16, + mut attributes: xim::AHashMap>, + ) -> Result<(), ClientError> { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + // Get the input styles. + let mut preedit_style = None; + let mut none_style = None; + + let styles = { + let style = attributes + .remove(&AttributeName::QueryInputStyle) + .expect("No query input style"); + let mut result = vec![0u32; style.len() / 4]; + + bytemuck::cast_slice_mut::(&mut result).copy_from_slice(&style); + + result + }; + + { + // The styles that we're looking for. + let lu_preedit_style = InputStyle::PREEDIT_CALLBACKS | InputStyle::STATUS_NOTHING; + let lu_nothing_style = InputStyle::PREEDIT_NOTHING | InputStyle::STATUS_NOTHING; + let lu_none_style = InputStyle::PREEDIT_NONE | InputStyle::STATUS_NONE; + + for style in styles { + let style = InputStyle::from_bits_truncate(style); + + if style == lu_preedit_style { + preedit_style = Some(Style::Preedit); + } else if style == lu_nothing_style { + preedit_style = Some(Style::Nothing); + } else if style == lu_none_style { + none_style = Some(Style::None); + } + } + } + + let (preedit_style, none_style) = match (preedit_style, none_style) { + (None, None) => { + log::error!("No supported input styles found"); + return Ok(()); + } + + (Some(style), None) | (None, Some(style)) => (style, style), + + (Some(preedit_style), Some(none_style)) => (preedit_style, none_style), + }; + + self.styles = Some((preedit_style, none_style)); + + Ok(()) + } + + fn handle_create_ic( + &mut self, + _client: &mut C, + input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + // Get the window that wanted the IC context. + let window = self + .pending_windows + .pop_front() + .ok_or_else(|| invalid_state(InvalidImeState::NoWindows))?; + + // Create the IC data. + let ic_data = IcData { + window, + new_spot: None, + text: Vec::new(), + cursor: 0, + }; + + // Store the context. + let (window, style) = (ic_data.window.id, ic_data.window.style); + self.input_contexts.insert(input_context_id, ic_data); + self.window_contexts.insert(window, input_context_id); + + // Indicate our status. + let event = if matches!(style, Style::Nothing) { + ImeEvent::Disabled + } else { + ImeEvent::Enabled + }; + self.ime_events.push_back((window, event)); + + Ok(()) + } + + fn handle_destroy_ic( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + ) -> Result<(), ClientError> { + // This is already handled by the higher-level function. + Ok(()) + } + + fn handle_set_ic_values( + &mut self, + _client: &mut C, + input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + // Get the IC data. + let ic_data = self + .input_contexts + .get_mut(&input_context_id) + .ok_or_else(|| invalid_state(InvalidImeState::InvalidIc(input_context_id)))?; + + // Move up the new spot + if let Some(spot) = ic_data.new_spot.take() { + ic_data.window.spot = spot; + } + + Ok(()) + } + + fn handle_preedit_start( + &mut self, + _client: &mut C, + input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) { + // Start a pre-edit. + ic_data.text.clear(); + ic_data.cursor = 0; + + // Indicate the start. + self.ime_events + .push_back((ic_data.window.id, ImeEvent::Start)); + } + + Ok(()) + } + + fn handle_preedit_draw( + &mut self, + _client: &mut C, + input_method_id: u16, + input_context_id: u16, + caret: i32, + chg_first: i32, + chg_len: i32, + _status: xim::PreeditDrawStatus, + preedit_string: &str, + _feedbacks: Vec, + ) -> Result<(), ClientError> { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) { + // Set the cursor. + ic_data.cursor = caret as usize; + + // Figure out the range of text to change. + let change_range = chg_first as usize..(chg_first + chg_len) as usize; + + // If the range doesn't fit our current text, warn and return. + if change_range.start > ic_data.text.len() || change_range.end > ic_data.text.len() { + warn!( + "Preedit draw range {}..{} doesn't fit text of length {}", + change_range.start, + change_range.end, + ic_data.text.len() + ); + return Ok(()); + } + + // Update the text in the changed range. + { + let text = &mut ic_data.text; + let mut old_text_tail = text.split_off(change_range.end); + + text.truncate(change_range.start); + text.extend(preedit_string.chars()); + text.append(&mut old_text_tail); + } + + // Send the event. + let cursor_byte_pos = calc_byte_position(&ic_data.text, ic_data.cursor); + let event = ImeEvent::Update(ic_data.text.iter().collect(), Some(cursor_byte_pos)); + + self.ime_events.push_back((ic_data.window.id, event)); + } + + Ok(()) + } + + fn handle_preedit_caret( + &mut self, + _client: &mut C, + input_method_id: u16, + input_context_id: u16, + position: &mut i32, + direction: xim::CaretDirection, + _style: xim::CaretStyle, + ) -> Result<(), ClientError> { + // We only care about absolute position. + if matches!(direction, xim::CaretDirection::AbsolutePosition) { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) { + ic_data.cursor = *position as usize; + + // Send the event + let event = + ImeEvent::Update(ic_data.text.iter().collect(), Some(*position as usize)); + self.ime_events.push_back((ic_data.window.id, event)); + } + } + + Ok(()) + } + + fn handle_preedit_done( + &mut self, + _client: &mut C, + input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + // Get the client data. + if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) { + // We're done with a preedit. + ic_data.text.clear(); + ic_data.cursor = 0; + + // Send a message to the window. + let window = ic_data.window.id; + self.ime_events.push_back((window, ImeEvent::End)); + } + + Ok(()) + } + + fn handle_commit( + &mut self, + _client: &mut C, + input_method_id: u16, + input_context_id: u16, + text: &str, + ) -> Result<(), ClientError> { + debug_assert_eq!(self.input_method, Some(input_method_id)); + + // Get the client data. + if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) { + // Send a message to the window. + let window = ic_data.window.id; + self.ime_events + .push_back((window, ImeEvent::Commit(text.to_owned()))); + } + + Ok(()) + } + + fn handle_query_extension( + &mut self, + _client: &mut C, + _extensions: &[xim::Extension], + ) -> Result<(), ClientError> { + // Don't care. + Ok(()) + } + + fn handle_forward_event( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _flag: xim::ForwardEventFlag, + _xev: C::XEvent, + ) -> Result<(), ClientError> { + // Don't care. + Ok(()) + } + + fn handle_set_event_mask( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _forward_event_mask: u32, + _synchronous_event_mask: u32, + ) -> Result<(), ClientError> { + // Don't care. + Ok(()) + } +} + +#[inline(always)] +fn invalid_state(state: InvalidImeState) -> ClientError { + ClientError::Other(Box::new(X11Error::InvalidImeState(state))) +} + +struct ImData(Option<&'static str>); + +impl fmt::Debug for ImData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(name) => write!(f, "\"{}\"", name), + None => write!(f, "default input method"), + } + } +} + +/// Get the current locale. +fn locale() -> String { + use std::ffi::CStr; + + const EN_US: &str = "en_US.UTF-8"; + + // Get the pointer to the current locale. + let locale_ptr = unsafe { libc::setlocale(libc::LC_CTYPE, std::ptr::null()) }; + + // If locale_ptr is null, just default to en_US.UTF-8. + if locale_ptr.is_null() { + return EN_US.to_owned(); + } + + // Convert the pointer to a CStr. + let locale_cstr = unsafe { CStr::from_ptr(locale_ptr) }; + + // Convert the CStr to a String to prevent the result from getting clobbered. + locale_cstr.to_str().unwrap_or(EN_US).to_owned() +} + +fn calc_byte_position(text: &[char], pos: usize) -> usize { + text.iter().take(pos).map(|c| c.len_utf8()).sum() +} diff --git a/src/platform_impl/linux/x11/ime/callbacks.rs b/src/platform_impl/linux/x11/ime/callbacks.rs deleted file mode 100644 index b5a010714..000000000 --- a/src/platform_impl/linux/x11/ime/callbacks.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::{collections::HashMap, os::raw::c_char, ptr, sync::Arc}; - -use super::{ffi, XConnection, XError}; - -use super::{ - context::{ImeContext, ImeContextCreationError}, - inner::{close_im, ImeInner}, - input_method::PotentialInputMethods, -}; - -pub(crate) 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. - unsafe { (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(crate) unsafe fn set_instantiate_callback( - xconn: &Arc, - client_data: ffi::XPointer, -) -> Result<(), XError> { - unsafe { - (xconn.xlib.XRegisterIMInstantiateCallback)( - xconn.display, - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - Some(xim_instantiate_callback), - client_data, - ) - }; - xconn.check_errors() -} - -pub(crate) unsafe fn unset_instantiate_callback( - xconn: &Arc, - client_data: ffi::XPointer, -) -> Result<(), XError> { - unsafe { - (xconn.xlib.XUnregisterIMInstantiateCallback)( - xconn.display, - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - Some(xim_instantiate_callback), - client_data, - ) - }; - xconn.check_errors() -} - -pub(crate) unsafe fn set_destroy_callback( - xconn: &Arc, - im: ffi::XIM, - inner: &ImeInner, -) -> Result<(), XError> { - unsafe { - xim_set_callback( - xconn, - im, - ffi::XNDestroyCallback_0.as_ptr() as *const _, - &inner.destroy_callback as *const _ as *mut _, - ) - } -} - -#[derive(Debug)] -#[allow(clippy::enum_variant_names)] -enum ReplaceImError { - // Boxed to prevent large error type - MethodOpenFailed(Box), - 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 = unsafe { &(*inner).xconn }; - - let (new_im, is_fallback) = { - let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) }; - let is_fallback = new_im.is_fallback(); - ( - new_im.ok().ok_or_else(|| { - ReplaceImError::MethodOpenFailed(Box::new(unsafe { - (*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 = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) }; - if result.is_err() { - let _ = unsafe { close_im(xconn, new_im.im) }; - } - result - } - .map_err(ReplaceImError::SetDestroyCallbackFailed)?; - - let mut new_contexts = HashMap::new(); - for (window, old_context) in unsafe { (*inner).contexts.iter() } { - let spot = old_context.as_ref().map(|old_context| old_context.ic_spot); - - // Check if the IME was allowed on that context. - let is_allowed = old_context - .as_ref() - .map(|old_context| old_context.is_allowed()) - .unwrap_or_default(); - - // We can't use the style from the old context here, since it may change on reload, so - // pick style from the new XIM based on the old state. - let style = if is_allowed { - new_im.preedit_style - } else { - new_im.none_style - }; - - let new_context = { - let result = unsafe { - ImeContext::new( - xconn, - new_im.im, - style, - *window, - spot, - (*inner).event_sender.clone(), - ) - }; - if result.is_err() { - let _ = unsafe { 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. - unsafe { - let _ = (*inner).destroy_all_contexts_if_necessary(); - let _ = (*inner).close_im_if_necessary(); - (*inner).im = Some(new_im); - (*inner).contexts = new_contexts; - (*inner).is_destroyed = false; - (*inner).is_fallback = is_fallback; - } - Ok(()) -} - -pub unsafe extern "C" 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 = unsafe { &(*inner).xconn }; - match unsafe { replace_im(inner) } { - Ok(()) => unsafe { - let _ = unset_instantiate_callback(xconn, client_data); - (*inner).is_fallback = false; - }, - Err(err) => unsafe { - if (*inner).is_destroyed { - // We have no usable input methods! - panic!("Failed to reopen input method: {err:?}"); - } - }, - } - } -} - -// 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 "C" 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() { - unsafe { (*inner).is_destroyed = true }; - let xconn = unsafe { &(*inner).xconn }; - if unsafe { !(*inner).is_fallback } { - let _ = unsafe { set_instantiate_callback(xconn, client_data) }; - // Attempt to open fallback input method. - match unsafe { replace_im(inner) } { - Ok(()) => unsafe { (*inner).is_fallback = true }, - Err(err) => { - // We have no usable input methods! - panic!("Failed to open fallback input method: {err:?}"); - } - } - } - } -} diff --git a/src/platform_impl/linux/x11/ime/context.rs b/src/platform_impl/linux/x11/ime/context.rs deleted file mode 100644 index dab2c6bd3..000000000 --- a/src/platform_impl/linux/x11/ime/context.rs +++ /dev/null @@ -1,385 +0,0 @@ -use std::ffi::CStr; -use std::os::raw::c_short; -use std::sync::Arc; -use std::{mem, ptr}; - -use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct}; - -use crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle}; -use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; - -use super::{ffi, util, XConnection, XError}; - -/// IME creation error. -#[derive(Debug)] -pub enum ImeContextCreationError { - /// Got the error from Xlib. - XError(XError), - - /// Got null pointer from Xlib but without exact reason. - Null, -} - -/// The callback used by XIM preedit functions. -type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer); - -/// Wrapper for creating XIM callbacks. -#[inline] -fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback { - XIMCallback { - client_data, - callback: Some(callback), - } -} - -/// The server started preedit. -extern "C" fn preedit_start_callback( - _xim: ffi::XIM, - client_data: ffi::XPointer, - _call_data: ffi::XPointer, -) -> i32 { - let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; - - client_data.text.clear(); - client_data.cursor_pos = 0; - client_data - .event_sender - .send((client_data.window, ImeEvent::Start)) - .expect("failed to send preedit start event"); - -1 -} - -/// Done callback is used when the preedit should be hidden. -extern "C" fn preedit_done_callback( - _xim: ffi::XIM, - client_data: ffi::XPointer, - _call_data: ffi::XPointer, -) { - let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; - - // Drop text buffer and reset cursor position on done. - client_data.text = Vec::new(); - client_data.cursor_pos = 0; - - client_data - .event_sender - .send((client_data.window, ImeEvent::End)) - .expect("failed to send preedit end event"); -} - -fn calc_byte_position(text: &[char], pos: usize) -> usize { - text.iter() - .take(pos) - .fold(0, |byte_pos, text| byte_pos + text.len_utf8()) -} - -/// Preedit text information to be drawn inline by the client. -extern "C" fn preedit_draw_callback( - _xim: ffi::XIM, - client_data: ffi::XPointer, - call_data: ffi::XPointer, -) { - let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; - let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) }; - client_data.cursor_pos = call_data.caret as usize; - - let chg_range = - call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize; - if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() { - warn!( - "invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}", - client_data.text.len(), - call_data.chg_first, - call_data.chg_length - ); - return; - } - - // NULL indicate text deletion - let mut new_chars = if call_data.text.is_null() { - Vec::new() - } else { - let xim_text = unsafe { &mut *(call_data.text) }; - if xim_text.encoding_is_wchar > 0 { - return; - } - - let new_text = unsafe { xim_text.string.multi_byte }; - - if new_text.is_null() { - return; - } - - let new_text = unsafe { CStr::from_ptr(new_text) }; - - String::from(new_text.to_str().expect("Invalid UTF-8 String from IME")) - .chars() - .collect() - }; - let mut old_text_tail = client_data.text.split_off(chg_range.end); - client_data.text.truncate(chg_range.start); - client_data.text.append(&mut new_chars); - client_data.text.append(&mut old_text_tail); - let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); - - client_data - .event_sender - .send(( - client_data.window, - ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), - )) - .expect("failed to send preedit update event"); -} - -/// Handling of cursor movements in preedit text. -extern "C" fn preedit_caret_callback( - _xim: ffi::XIM, - client_data: ffi::XPointer, - call_data: ffi::XPointer, -) { - let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; - let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) }; - - if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition { - client_data.cursor_pos = call_data.position as usize; - let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); - - client_data - .event_sender - .send(( - client_data.window, - ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), - )) - .expect("failed to send preedit update event"); - } -} - -/// Struct to simplify callback creation and latter passing into Xlib XIM. -struct PreeditCallbacks { - start_callback: ffi::XIMCallback, - done_callback: ffi::XIMCallback, - draw_callback: ffi::XIMCallback, - caret_callback: ffi::XIMCallback, -} - -impl PreeditCallbacks { - pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks { - let start_callback = create_xim_callback(client_data, unsafe { - mem::transmute(preedit_start_callback as usize) - }); - let done_callback = create_xim_callback(client_data, preedit_done_callback); - let caret_callback = create_xim_callback(client_data, preedit_caret_callback); - let draw_callback = create_xim_callback(client_data, preedit_draw_callback); - - PreeditCallbacks { - start_callback, - done_callback, - caret_callback, - draw_callback, - } - } -} - -struct ImeContextClientData { - window: ffi::Window, - event_sender: ImeEventSender, - text: Vec, - cursor_pos: usize, -} - -// XXX: 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`. -pub struct ImeContext { - pub(crate) ic: ffi::XIC, - pub(crate) ic_spot: ffi::XPoint, - pub(crate) style: Style, - // Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from - // there we keep the pointer to automatically deallocate it. - _client_data: Box, -} - -impl ImeContext { - pub(crate) unsafe fn new( - xconn: &Arc, - im: ffi::XIM, - style: Style, - window: ffi::Window, - ic_spot: Option, - event_sender: ImeEventSender, - ) -> Result { - let client_data = Box::into_raw(Box::new(ImeContextClientData { - window, - event_sender, - text: Vec::new(), - cursor_pos: 0, - })); - - let ic = match style as _ { - Style::Preedit(style) => unsafe { - ImeContext::create_preedit_ic( - xconn, - im, - style, - window, - client_data as ffi::XPointer, - ) - }, - Style::Nothing(style) => unsafe { - ImeContext::create_nothing_ic(xconn, im, style, window) - }, - Style::None(style) => unsafe { ImeContext::create_none_ic(xconn, im, style, window) }, - } - .ok_or(ImeContextCreationError::Null)?; - - xconn - .check_errors() - .map_err(ImeContextCreationError::XError)?; - - let mut context = ImeContext { - ic, - ic_spot: ffi::XPoint { x: 0, y: 0 }, - style, - _client_data: unsafe { Box::from_raw(client_data) }, - }; - - // Set the spot location, if it's present. - if let Some(ic_spot) = ic_spot { - context.set_spot(xconn, ic_spot.x, ic_spot.y) - } - - Ok(context) - } - - unsafe fn create_none_ic( - xconn: &Arc, - im: ffi::XIM, - style: XIMStyle, - window: ffi::Window, - ) -> Option { - let ic = unsafe { - (xconn.xlib.XCreateIC)( - im, - ffi::XNInputStyle_0.as_ptr() as *const _, - style, - ffi::XNClientWindow_0.as_ptr() as *const _, - window, - ptr::null_mut::<()>(), - ) - }; - - (!ic.is_null()).then_some(ic) - } - - unsafe fn create_preedit_ic( - xconn: &Arc, - im: ffi::XIM, - style: XIMStyle, - window: ffi::Window, - client_data: ffi::XPointer, - ) -> Option { - let preedit_callbacks = PreeditCallbacks::new(client_data); - let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe { - (xconn.xlib.XVaCreateNestedList)( - 0, - ffi::XNPreeditStartCallback_0.as_ptr() as *const _, - &(preedit_callbacks.start_callback) as *const _, - ffi::XNPreeditDoneCallback_0.as_ptr() as *const _, - &(preedit_callbacks.done_callback) as *const _, - ffi::XNPreeditCaretCallback_0.as_ptr() as *const _, - &(preedit_callbacks.caret_callback) as *const _, - ffi::XNPreeditDrawCallback_0.as_ptr() as *const _, - &(preedit_callbacks.draw_callback) as *const _, - ptr::null_mut::<()>(), - ) - }) - .expect("XVaCreateNestedList returned NULL"); - - let ic = unsafe { - (xconn.xlib.XCreateIC)( - im, - ffi::XNInputStyle_0.as_ptr() as *const _, - style, - ffi::XNClientWindow_0.as_ptr() as *const _, - window, - ffi::XNPreeditAttributes_0.as_ptr() as *const _, - preedit_attr.ptr, - ptr::null_mut::<()>(), - ) - }; - - (!ic.is_null()).then_some(ic) - } - - unsafe fn create_nothing_ic( - xconn: &Arc, - im: ffi::XIM, - style: XIMStyle, - window: ffi::Window, - ) -> Option { - let ic = unsafe { - (xconn.xlib.XCreateIC)( - im, - ffi::XNInputStyle_0.as_ptr() as *const _, - style, - ffi::XNClientWindow_0.as_ptr() as *const _, - window, - ptr::null_mut::<()>(), - ) - }; - - (!ic.is_null()).then_some(ic) - } - - pub(crate) fn focus(&self, xconn: &Arc) -> Result<(), XError> { - unsafe { - (xconn.xlib.XSetICFocus)(self.ic); - } - xconn.check_errors() - } - - pub(crate) fn unfocus(&self, xconn: &Arc) -> Result<(), XError> { - unsafe { - (xconn.xlib.XUnsetICFocus)(self.ic); - } - xconn.check_errors() - } - - pub fn is_allowed(&self) -> bool { - !matches!(self.style, Style::None(_)) - } - - // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks - // are being used. Certain IMEs do show selection window, but it's placed in bottom left of the - // window and couldn't be changed. - // - // For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580. - pub(crate) fn set_spot(&mut self, xconn: &Arc, x: c_short, y: c_short) { - if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y { - return; - } - - self.ic_spot = ffi::XPoint { x, y }; - - unsafe { - let preedit_attr = util::memory::XSmartPointer::new( - xconn, - (xconn.xlib.XVaCreateNestedList)( - 0, - ffi::XNSpotLocation_0.as_ptr(), - &self.ic_spot, - ptr::null_mut::<()>(), - ), - ) - .expect("XVaCreateNestedList returned NULL"); - - (xconn.xlib.XSetICValues)( - self.ic, - ffi::XNPreeditAttributes_0.as_ptr() as *const _, - preedit_attr.ptr, - ptr::null_mut::<()>(), - ); - } - } -} diff --git a/src/platform_impl/linux/x11/ime/inner.rs b/src/platform_impl/linux/x11/ime/inner.rs deleted file mode 100644 index ddff12fb3..000000000 --- a/src/platform_impl/linux/x11/ime/inner.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::{collections::HashMap, mem, sync::Arc}; - -use super::{ffi, XConnection, XError}; - -use super::{ - context::ImeContext, - input_method::{InputMethod, PotentialInputMethods}, -}; -use crate::platform_impl::platform::x11::ime::ImeEventSender; - -pub(crate) unsafe fn close_im(xconn: &Arc, im: ffi::XIM) -> Result<(), XError> { - unsafe { (xconn.xlib.XCloseIM)(im) }; - xconn.check_errors() -} - -pub(crate) unsafe fn destroy_ic(xconn: &Arc, ic: ffi::XIC) -> Result<(), XError> { - unsafe { (xconn.xlib.XDestroyIC)(ic) }; - xconn.check_errors() -} - -pub(crate) struct ImeInner { - pub xconn: Arc, - pub im: Option, - pub potential_input_methods: PotentialInputMethods, - pub contexts: HashMap>, - // WARNING: this is initially zeroed! - pub destroy_callback: ffi::XIMCallback, - pub event_sender: ImeEventSender, - // 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(crate) fn new( - xconn: Arc, - potential_input_methods: PotentialInputMethods, - event_sender: ImeEventSender, - ) -> Self { - ImeInner { - xconn, - im: None, - potential_input_methods, - contexts: HashMap::new(), - destroy_callback: unsafe { mem::zeroed() }, - event_sender, - is_destroyed: false, - is_fallback: false, - } - } - - pub unsafe fn close_im_if_necessary(&self) -> Result { - if !self.is_destroyed && self.im.is_some() { - unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true) - } else { - Ok(false) - } - } - - pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result { - if !self.is_destroyed { - unsafe { 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().flatten() { - unsafe { self.destroy_ic_if_necessary(context.ic)? }; - } - Ok(!self.is_destroyed) - } -} diff --git a/src/platform_impl/linux/x11/ime/input_method.rs b/src/platform_impl/linux/x11/ime/input_method.rs deleted file mode 100644 index 30a4a3eb4..000000000 --- a/src/platform_impl/linux/x11/ime/input_method.rs +++ /dev/null @@ -1,370 +0,0 @@ -use std::{ - env, - ffi::{CStr, CString, IntoStringError}, - fmt, - os::raw::{c_char, c_ulong, c_ushort}, - ptr, - sync::{Arc, Mutex}, -}; - -use super::{super::atoms::*, ffi, util, XConnection, XError}; -use once_cell::sync::Lazy; -use x11rb::protocol::xproto; - -static GLOBAL_LOCK: Lazy> = Lazy::new(Default::default); - -unsafe fn open_im(xconn: &Arc, locale_modifiers: &CStr) -> Option { - let _lock = GLOBAL_LOCK.lock(); - - // 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 or if the - // current locale is not supported by Xlib. - unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) }; - - let im = unsafe { - (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, - pub preedit_style: Style, - pub none_style: Style, - _name: String, -} - -impl InputMethod { - fn new(xconn: &Arc, im: ffi::XIM, name: String) -> Option { - let mut styles: *mut XIMStyles = std::ptr::null_mut(); - - // Query the styles supported by the XIM. - unsafe { - if !(xconn.xlib.XGetIMValues)( - im, - ffi::XNQueryInputStyle_0.as_ptr() as *const _, - (&mut styles) as *mut _, - std::ptr::null_mut::<()>(), - ) - .is_null() - { - return None; - } - } - - let mut preedit_style = None; - let mut none_style = None; - - unsafe { - std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _) - .iter() - .for_each(|style| match *style { - XIM_PREEDIT_STYLE => { - preedit_style = Some(Style::Preedit(*style)); - } - XIM_NOTHING_STYLE if preedit_style.is_none() => { - preedit_style = Some(Style::Nothing(*style)) - } - XIM_NONE_STYLE => none_style = Some(Style::None(*style)), - _ => (), - }); - - (xconn.xlib.XFree)(styles.cast()); - }; - - if preedit_style.is_none() && none_style.is_none() { - return None; - } - - let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap()); - let none_style = none_style.unwrap_or(preedit_style); - - Some(InputMethod { - im, - _name: name, - preedit_style, - none_style, - }) - } -} - -const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle; -const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle; -const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle; - -/// Style of the IME context. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Style { - /// Preedit callbacks. - Preedit(XIMStyle), - - /// Nothing. - Nothing(XIMStyle), - - /// No IME. - None(XIMStyle), -} - -impl Default for Style { - fn default() -> Self { - Style::None(XIM_NONE_STYLE) - } -} - -#[repr(C)] -#[derive(Debug)] -struct XIMStyles { - count_styles: c_ushort, - supported_styles: *const XIMStyle, -} - -pub(crate) type XIMStyle = c_ulong; - -#[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 { - matches!(self, InputMethodResult::Fallback(_)) - } - - 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), -} - -impl From for GetXimServersError { - fn from(error: util::GetPropertyError) -> Self { - GetXimServersError::GetPropertyError(error) - } -} - -// 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 atoms = xconn.atoms(); - let servers_atom = atoms[XIM_SERVERS]; - - let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) }; - - let mut atoms: Vec = xconn - .get_property::( - root as xproto::Window, - servers_atom, - xproto::Atom::from(xproto::AtomEnum::ATOM), - ) - .map_err(GetXimServersError::GetPropertyError)? - .into_iter() - .map(ffi::Atom::from) - .collect::>(); - - let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len()); - unsafe { - (xconn.xlib.XGetAtomNames)( - xconn.display, - atoms.as_mut_ptr(), - atoms.len() as _, - names.as_mut_ptr() as _, - ) - }; - unsafe { names.set_len(atoms.len()) }; - - let mut formatted_names = Vec::with_capacity(names.len()); - for name in names { - let string = unsafe { CStr::from_ptr(name) } - .to_owned() - .into_string() - .map_err(GetXimServersError::InvalidUtf8)?; - unsafe { (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.and_then(|im| InputMethod::new(xconn, 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 succeeded. -#[derive(Debug, Clone)] -pub(crate) struct PotentialInputMethods { - // On correctly configured systems, the XMODIFIERS environment 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 sequences, 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<&dyn 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_impl/linux/x11/ime/mod.rs b/src/platform_impl/linux/x11/ime/mod.rs deleted file mode 100644 index e0979681d..000000000 --- a/src/platform_impl/linux/x11/ime/mod.rs +++ /dev/null @@ -1,249 +0,0 @@ -// Important: all XIM calls need to happen from the same thread! - -mod callbacks; -mod context; -mod inner; -mod input_method; - -use std::sync::{ - mpsc::{Receiver, Sender}, - Arc, -}; - -use super::{ffi, util, XConnection, XError}; - -pub use self::context::ImeContextCreationError; -use self::{ - callbacks::*, - context::ImeContext, - inner::{close_im, ImeInner}, - input_method::{PotentialInputMethods, Style}, -}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum ImeEvent { - Enabled, - Start, - Update(String, usize), - End, - Disabled, -} - -pub type ImeReceiver = Receiver; -pub type ImeSender = Sender; -pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>; -pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>; - -/// Request to control XIM handler from the window. -pub enum ImeRequest { - /// Set IME spot position for given `window_id`. - Position(ffi::Window, i16, i16), - - /// Allow IME input for the given `window_id`. - Allow(ffi::Window, bool), -} - -#[derive(Debug)] -pub(crate) enum ImeCreationError { - // Boxed to prevent large error type - OpenFailure(Box), - SetDestroyCallbackFailed(XError), -} - -pub(crate) 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, - event_sender: ImeEventSender, - ) -> Result { - let potential_input_methods = PotentialInputMethods::new(&xconn); - - let (mut inner, client_data) = { - let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender)); - 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.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?; - } - inner.im = Some(input_method); - Ok(Ime { xconn, inner }) - } else { - Err(ImeCreationError::OpenFailure(Box::new( - 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, - with_preedit: bool, - ) -> 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 { - let im = self.inner.im.as_ref().unwrap(); - let style = if with_preedit { - im.preedit_style - } else { - im.none_style - }; - - let context = unsafe { - ImeContext::new( - &self.inner.xconn, - im.im, - style, - window, - None, - self.inner.event_sender.clone(), - )? - }; - - // Check the state on the context, since it could fail to enable or disable preedit. - let event = if matches!(style, Style::None(_)) { - if with_preedit { - debug!("failed to create IME context with preedit support.") - } - ImeEvent::Disabled - } else { - if !with_preedit { - debug!("failed to create IME context without preedit support.") - } - ImeEvent::Enabled - }; - - self.inner - .event_sender - .send((window, event)) - .expect("Failed to send enabled event"); - - Some(context) - }; - - 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(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 _); - } - } - - pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) { - if self.is_destroyed() { - return; - } - - if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { - if allowed == context.is_allowed() { - return; - } - } - - // Remove context for that window. - let _ = self.remove_context(window); - - // Create new context supporting IME input. - let _ = self.create_context(window, allowed); - } -} - -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_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index 6d8d9fd30..52c78eeed 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -34,7 +34,6 @@ use std::{ raw::*, unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}, }, - ptr, rc::Rc, slice, str, sync::mpsc::{Receiver, Sender, TryRecvError}, @@ -42,8 +41,6 @@ use std::{ time::{Duration, Instant}, }; -use libc::{self, setlocale, LC_CTYPE}; - use atoms::*; use x11rb::x11_utils::X11Error as LogicalError; @@ -63,7 +60,6 @@ use x11rb::{ use self::{ dnd::{Dnd, DndState}, event_processor::EventProcessor, - ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender}, }; use super::{common::xkb_state::KbdState, ControlFlow, OsError}; use crate::{ @@ -147,15 +143,18 @@ pub struct EventLoopWindowTarget { xconn: Arc, wm_delete_window: xproto::Atom, net_wm_ping: xproto::Atom, - ime_sender: ImeSender, + ime_sender: mpsc::Sender, control_flow: Cell, exit: Cell>, root: xproto::Window, - ime: RefCell, windows: RefCell>>, redraw_sender: WakeSender, activation_sender: WakeSender, device_events: Cell, + + /// State of IME. + ime: Option>, + _marker: ::std::marker::PhantomData, } @@ -205,37 +204,14 @@ impl EventLoop { .expect("Failed to call XInternAtoms when initializing drag and drop"); let (ime_sender, ime_receiver) = mpsc::channel(); - let (ime_event_sender, ime_event_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 { - // Remember default locale to restore it if target locale is unsupported - // by Xlib - let default_locale = setlocale(LC_CTYPE, ptr::null()); - setlocale(LC_CTYPE, b"\0".as_ptr() as *const _); - // Check if set locale is supported by Xlib. - // If not, calls to some Xlib functions like `XSetLocaleModifiers` - // will fail. - let locale_supported = (xconn.xlib.XSupportsLocale)() == 1; - if !locale_supported { - let unsupported_locale = setlocale(LC_CTYPE, ptr::null()); - warn!( - "Unsupported locale \"{}\". Restoring default locale \"{}\".", - CStr::from_ptr(unsupported_locale).to_string_lossy(), - CStr::from_ptr(default_locale).to_string_lossy() - ); - // Restore default locale - setlocale(LC_CTYPE, default_locale); + let ime = match ime::ImeData::new(&xconn, xconn.default_screen_index()) { + Ok(ime) => Some(ime), + Err(e) => { + log::error!("Failed to open IME: {e}"); + None } - } - let ime = RefCell::new({ - let result = Ime::new(Arc::clone(&xconn), ime_event_sender); - if let Err(ImeCreationError::OpenFailure(ref state)) = result { - panic!("Failed to open input method: {state:#?}"); - } - result.expect("Failed to set input method destruction callback") - }); + }; let randr_event_offset = xconn .select_xrandr_input(root) @@ -303,12 +279,12 @@ impl EventLoop { KbdState::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()).unwrap(); let window_target = EventLoopWindowTarget { - ime, root, control_flow: Cell::new(ControlFlow::default()), exit: Cell::new(None), windows: Default::default(), _marker: ::std::marker::PhantomData, + ime: ime.map(RefCell::new), ime_sender, xconn, wm_delete_window, @@ -337,8 +313,7 @@ impl EventLoop { dnd, devices: Default::default(), randr_event_offset, - ime_receiver, - ime_event_receiver, + ime_requests: ime_receiver, xi2ext, xkbext, kb_state, @@ -880,8 +855,11 @@ pub enum X11Error { /// The XID range has been exhausted. XidsExhausted(IdsExhausted), - /// Got `null` from an Xlib function without a reason. - UnexpectedNull(&'static str), + /// An IME client error occurred. + Ime(xim::ClientError), + + /// The IME client has entered an invalid state. + InvalidImeState(ime::InvalidImeState), /// Got an invalid activation token. InvalidActivationToken(Vec), @@ -900,8 +878,9 @@ impl fmt::Display for X11Error { X11Error::Connect(e) => write!(f, "X11 connection error: {}", e), X11Error::Connection(e) => write!(f, "X11 connection error: {}", e), X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e), + X11Error::Ime(e) => write!(f, "An IME error occurred: {}", e), + X11Error::InvalidImeState(e) => write!(f, "Invalid IME state: {}", e), X11Error::X11(e) => write!(f, "X11 error: {:?}", e), - X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {}", s), X11Error::InvalidActivationToken(s) => write!( f, "Invalid activation token: {}", @@ -964,15 +943,6 @@ impl From for X11Error { } } -impl From for X11Error { - fn from(value: ime::ImeContextCreationError) -> Self { - match value { - ime::ImeContextCreationError::XError(e) => e.into(), - ime::ImeContextCreationError::Null => Self::UnexpectedNull("XOpenIM"), - } - } -} - impl From for X11Error { fn from(value: ReplyOrIdError) -> Self { match value { @@ -983,6 +953,18 @@ impl From for X11Error { } } +impl From for X11Error { + fn from(e: xim::ClientError) -> Self { + match e { + xim::ClientError::Other(other) => match other.downcast::() { + Ok(x11) => *x11, + Err(other) => X11Error::Ime(xim::ClientError::Other(other)), + }, + e => X11Error::Ime(e), + } + } +} + /// The underlying x11rb connection that we are using. type X11rbConnection = x11rb::xcb_ffi::XCBConnection; diff --git a/src/platform_impl/linux/x11/util/input.rs b/src/platform_impl/linux/x11/util/input.rs index c515229b6..ae582ca7e 100644 --- a/src/platform_impl/linux/x11/util/input.rs +++ b/src/platform_impl/linux/x11/util/input.rs @@ -1,4 +1,3 @@ -use std::{slice, str}; use x11rb::protocol::{ xinput::{self, ConnectionExt as _}, xkb, @@ -9,11 +8,6 @@ use super::*; pub const VIRTUAL_CORE_POINTER: u16 = 2; pub const VIRTUAL_CORE_KEYBOARD: u16 = 3; -// A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to -// re-allocate (and make another round-trip) in the *vast* majority of cases. -// To test if `lookup_utf8` works correctly, set this to 1. -const TEXT_BUFFER_SIZE: usize = 1024; - impl XConnection { pub fn select_xinput_events( &self, @@ -60,52 +54,4 @@ impl XConnection { .reply() .map_err(Into::into) } - - fn lookup_utf8_inner( - &self, - ic: ffi::XIC, - key_event: &mut ffi::XKeyEvent, - buffer: *mut u8, - size: usize, - ) -> (ffi::KeySym, ffi::Status, c_int) { - let mut keysym: ffi::KeySym = 0; - let mut status: ffi::Status = 0; - let count = unsafe { - (self.xlib.Xutf8LookupString)( - ic, - key_event, - buffer as *mut c_char, - size as c_int, - &mut keysym, - &mut status, - ) - }; - (keysym, status, count) - } - - pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String { - // `assume_init` is safe here because the array consists of `MaybeUninit` values, - // which do not require initialization. - let mut buffer: [MaybeUninit; TEXT_BUFFER_SIZE] = - unsafe { MaybeUninit::uninit().assume_init() }; - // If the buffer overflows, we'll make a new one on the heap. - let mut vec; - - let (_, status, count) = - self.lookup_utf8_inner(ic, key_event, buffer.as_mut_ptr() as *mut u8, buffer.len()); - - let bytes = if status == ffi::XBufferOverflow { - vec = Vec::with_capacity(count as usize); - let (_, _, new_count) = - self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity()); - debug_assert_eq!(count, new_count); - - unsafe { vec.set_len(count as usize) }; - &vec[..count as usize] - } else { - unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) } - }; - - str::from_utf8(bytes).unwrap_or("").to_string() - } } diff --git a/src/platform_impl/linux/x11/util/memory.rs b/src/platform_impl/linux/x11/util/memory.rs index d32eb8ceb..6588c414e 100644 --- a/src/platform_impl/linux/x11/util/memory.rs +++ b/src/platform_impl/linux/x11/util/memory.rs @@ -7,18 +7,6 @@ pub(crate) struct XSmartPointer<'a, T> { pub ptr: *mut T, } -impl<'a, T> XSmartPointer<'a, T> { - // You're responsible for only passing things to this that should be XFree'd. - // Returns None if ptr is null. - pub fn new(xconn: &'a XConnection, ptr: *mut T) -> Option { - if !ptr.is_null() { - Some(XSmartPointer { xconn, ptr }) - } else { - None - } - } -} - impl<'a, T> Deref for XSmartPointer<'a, T> { type Target = T; diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 2d4314c4f..acd732255 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -4,7 +4,7 @@ use std::{ mem::replace, os::raw::*, path::Path, - sync::{Arc, Mutex, MutexGuard}, + sync::{mpsc, Arc, Mutex, MutexGuard}, }; use crate::cursor::CustomCursor as RootCustomCursor; @@ -44,7 +44,7 @@ use crate::{ use super::{ ffi, util::{self, CustomCursor, SelectedCursor}, - CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId, + CookieResultExt, EventLoopWindowTarget, ime::ImeRequest, VoidCookie, WindowId, XConnection, }; @@ -135,7 +135,7 @@ pub(crate) struct UnownedWindow { cursor_grabbed_mode: Mutex, #[allow(clippy::mutex_atomic)] cursor_visible: Mutex, - ime_sender: Mutex, + ime_sender: Mutex>, pub shared_state: Mutex, redraw_sender: WakeSender, activation_sender: WakeSender, @@ -547,11 +547,10 @@ impl UnownedWindow { .ignore_error(); { - let result = event_loop - .ime - .borrow_mut() - .create_context(window.xwindow as ffi::Window, false); - leap!(result); + if let Some(ime) = event_loop.ime.as_ref() { + let result = ime.borrow_mut().create_context(window.xwindow, false, None); + leap!(result); + } } // These properties must be set after mapping @@ -1792,11 +1791,11 @@ impl UnownedWindow { #[inline] pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) { let (x, y) = spot.to_physical::(self.scale_factor()).into(); - let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position( - self.xwindow as ffi::Window, - x, - y, - )); + let _ = self + .ime_sender + .lock() + .unwrap() + .send(ImeRequest::Position(self.xwindow, x, y)); } #[inline] @@ -1805,7 +1804,7 @@ impl UnownedWindow { .ime_sender .lock() .unwrap() - .send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed)); + .send(ImeRequest::Allow(self.xwindow, allowed)); } #[inline]