From b60254fecac461507ea7ca76751756b1a689574b Mon Sep 17 00:00:00 2001 From: npmania Date: Fri, 17 May 2024 07:13:51 +0900 Subject: [PATCH] x11: Add XIM support (#11657) This pull request adds XIM (X Input Method) support to x11 platform. The implementation utilizes [xim-rs](https://crates.io/crates/xim), a XIM library written entirely in Rust, to provide asynchronous XIM communication. Preedit and candidate positioning are fully supported in the editor interface, yet notably absent in the terminal environment. This work is sponsored by [Rainlab Inc.](https://rainlab.co.jp/en/) Release Notes: - N/A --------- Signed-off-by: npmania --- Cargo.lock | 60 ++++++ crates/gpui/Cargo.toml | 1 + crates/gpui/src/platform/linux/x11.rs | 2 + crates/gpui/src/platform/linux/x11/client.rs | 177 +++++++++++++++++- crates/gpui/src/platform/linux/x11/window.rs | 34 ++++ .../src/platform/linux/x11/xim_handler.rs | 154 +++++++++++++++ 6 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 crates/gpui/src/platform/linux/x11/xim_handler.rs diff --git a/Cargo.lock b/Cargo.lock index fc83b03dd922ed18ce4e9db013ff1d23cfa0378c..404a428598ba16ff1e48e84b45f6e79fc0c2d840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" dependencies = [ "cfg-if", + "const-random", "getrandom 0.2.10", "once_cell", "version_check", @@ -2560,6 +2561,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -4752,6 +4773,7 @@ dependencies = [ "windows 0.56.0", "windows-core 0.56.0", "x11rb", + "xim", "xkbcommon", ] @@ -10396,6 +10418,15 @@ dependencies = [ "time", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -12798,6 +12829,35 @@ dependencies = [ "winapi", ] +[[package]] +name = "xim" +version = "0.4.0" +source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af" +dependencies = [ + "ahash 0.8.8", + "hashbrown 0.14.0", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.1" +source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af" +dependencies = [ + "bitflags 2.4.2", +] + [[package]] name = "xkbcommon" version = "0.7.0" diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 70254df74c0a454264d64d67e1bc6fa4691d14d7..bc2297e0538523c4a70be6c08c9878ee718f70d5 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -126,6 +126,7 @@ x11rb = { version = "0.13.0", features = [ "resource_manager", ] } xkbcommon = { version = "0.7", features = ["wayland", "x11"] } +xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = ["x11rb-xcb", "x11rb-client"] } [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/gpui/src/platform/linux/x11.rs b/crates/gpui/src/platform/linux/x11.rs index 958da047d649b5dbbcc72fc3d28f6dff3fa1f7e9..6df8e9a3d6397bd862b25b2a650a8ef3be7115d7 100644 --- a/crates/gpui/src/platform/linux/x11.rs +++ b/crates/gpui/src/platform/linux/x11.rs @@ -2,8 +2,10 @@ mod client; mod display; mod event; mod window; +mod xim_handler; pub(crate) use client::*; pub(crate) use display::*; pub(crate) use event::*; pub(crate) use window::*; +pub(crate) use xim_handler::*; diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 1c3a21c4319acdd0cf732c4d300f108fef49a6d5..df1fc4a34c0f37e620ca18bcb3c539627afdeff8 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -4,7 +4,8 @@ use std::rc::{Rc, Weak}; use std::time::{Duration, Instant}; use calloop::generic::{FdWrapper, Generic}; -use calloop::{EventLoop, LoopHandle, RegistrationToken}; +use calloop::{channel, EventLoop, LoopHandle, RegistrationToken}; + use collections::HashMap; use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext}; use copypasta::ClipboardProvider; @@ -20,6 +21,7 @@ use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _}; use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event}; use x11rb::resource_manager::Database; use x11rb::xcb_ffi::XCBConnection; +use xim::{x11rb::X11rbClient, Client}; use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; use xkbcommon::xkb as xkbc; @@ -36,6 +38,7 @@ use super::{ X11Display, X11WindowStatePtr, XcbAtoms, }; use super::{button_from_mask, button_of_key, modifiers_from_state}; +use super::{XimCallbackEvent, XimHandler}; use crate::platform::linux::is_within_click_distance; use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; @@ -52,6 +55,36 @@ impl Deref for WindowRef { } } +#[derive(Debug)] +#[non_exhaustive] +pub enum EventHandlerError { + XCBConnectionError(ConnectionError), + XIMClientError(xim::ClientError), +} + +impl std::error::Error for EventHandlerError {} + +impl std::fmt::Display for EventHandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EventHandlerError::XCBConnectionError(err) => err.fmt(f), + EventHandlerError::XIMClientError(err) => err.fmt(f), + } + } +} + +impl From for EventHandlerError { + fn from(err: ConnectionError) -> Self { + EventHandlerError::XCBConnectionError(err) + } +} + +impl From for EventHandlerError { + fn from(err: xim::ClientError) -> Self { + EventHandlerError::XIMClientError(err) + } +} + pub struct X11ClientState { pub(crate) loop_handle: LoopHandle<'static, X11Client>, pub(crate) event_loop: Option>, @@ -69,6 +102,8 @@ pub struct X11ClientState { pub(crate) windows: HashMap, pub(crate) focused_window: Option, pub(crate) xkb: xkbc::State, + pub(crate) ximc: Option>>, + pub(crate) xim_handler: Option, pub(crate) cursor_handle: cursor::Handle, pub(crate) cursor_styles: HashMap, @@ -227,12 +262,21 @@ impl X11Client { let xcb_connection = Rc::new(xcb_connection); + let (xim_tx, xim_rx) = channel::channel::(); + + let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok(); + let xim_handler = if ximc.is_some() { + Some(XimHandler::new(xim_tx)) + } else { + None + }; + // Safety: Safe if xcb::Connection always returns a valid fd let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; handle .insert_source( - Generic::new_with_error::( + Generic::new_with_error::( fd, calloop::Interest::READ, calloop::Mode::Level, @@ -241,14 +285,63 @@ impl X11Client { let xcb_connection = xcb_connection.clone(); move |_readiness, _, client| { while let Some(event) = xcb_connection.poll_for_event()? { - client.handle_event(event); + let mut state = client.0.borrow_mut(); + if state.ximc.is_none() || state.xim_handler.is_none() { + drop(state); + client.handle_event(event); + continue; + } + let mut ximc = state.ximc.take().unwrap(); + let mut xim_handler = state.xim_handler.take().unwrap(); + let xim_connected = xim_handler.connected; + drop(state); + let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { + Ok(handled) => handled, + Err(err) => { + log::error!("XIMClientError: {}", err); + false + } + }; + let mut state = client.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + drop(state); + if xim_filtered { + continue; + } + if xim_connected { + client.xim_handle_event(event); + } else { + client.handle_event(event); + } } Ok(calloop::PostAction::Continue) } }, ) .expect("Failed to initialize x11 event source"); - + handle + .insert_source(xim_rx, { + move |chan_event, _, client| match chan_event { + channel::Event::Msg(xim_event) => { + match (xim_event) { + XimCallbackEvent::XimXEvent(event) => { + client.handle_event(event); + } + XimCallbackEvent::XimCommitEvent(window, text) => { + client.xim_handle_commit(window, text); + } + XimCallbackEvent::XimPreeditEvent(window, text) => { + client.xim_handle_preedit(window, text); + } + }; + } + channel::Event::Closed => { + log::error!("XIM Event Sender dropped") + } + } + }) + .expect("Failed to initialize XIM event source"); X11Client(Rc::new(RefCell::new(X11ClientState { event_loop: Some(event_loop), loop_handle: handle, @@ -265,6 +358,8 @@ impl X11Client { windows: HashMap::default(), focused_window: None, xkb: xkb_state, + ximc, + xim_handler, cursor_handle, cursor_styles: HashMap::default(), @@ -365,7 +460,6 @@ impl X11Client { } keystroke }; - drop(state); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, @@ -550,6 +644,79 @@ impl X11Client { Some(()) } + + fn xim_handle_event(&self, event: Event) -> Option<()> { + match event { + Event::KeyPress(event) | Event::KeyRelease(event) => { + let mut state = self.0.borrow_mut(); + let mut ximc = state.ximc.take().unwrap(); + let mut xim_handler = state.xim_handler.take().unwrap(); + drop(state); + xim_handler.window = event.event; + ximc.forward_event( + xim_handler.im_id, + xim_handler.ic_id, + xim::ForwardEventFlag::empty(), + &event, + ) + .unwrap(); + let mut state = self.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + drop(state); + } + event => { + self.handle_event(event); + } + } + Some(()) + } + + fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { + let window = self.get_window(window).unwrap(); + + window.handle_ime_commit(text); + Some(()) + } + + fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> { + let window = self.get_window(window).unwrap(); + window.handle_ime_preedit(text); + + let mut state = self.0.borrow_mut(); + let mut ximc = state.ximc.take().unwrap(); + let mut xim_handler = state.xim_handler.take().unwrap(); + drop(state); + + if let Some(area) = window.get_ime_area() { + let ic_attributes = ximc + .build_ic_attributes() + .push( + xim::AttributeName::InputStyle, + xim::InputStyle::PREEDIT_CALLBACKS + | xim::InputStyle::STATUS_NOTHING + | xim::InputStyle::PREEDIT_POSITION, + ) + .push(xim::AttributeName::ClientWindow, xim_handler.window) + .push(xim::AttributeName::FocusWindow, xim_handler.window) + .nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(area.origin.x + area.size.width) as i16, + y: u32::from(area.origin.y + area.size.height) as i16, + }, + ); + }) + .build(); + ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes); + } + let mut state = self.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + drop(state); + Some(()) + } } impl LinuxClient for X11Client { diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 783af95d2ac5ab8860f53e43b1ca8d2604f4f3d1..7f500b4ed459bb7b934d8f4bbc1b79760b4a5d80 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -478,6 +478,40 @@ impl X11WindowStatePtr { } } + pub fn handle_ime_commit(&self, text: String) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, &text); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_preedit(&self, text: String) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_and_mark_text_in_range(None, &text, None); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn get_ime_area(&self) -> Option> { + let mut state = self.state.borrow_mut(); + let mut bounds: Option> = None; + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(range) = input_handler.selected_text_range() { + bounds = input_handler.bounds_for_range(range); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + }; + bounds + } + pub fn configure(&self, bounds: Bounds) { let mut resize_args = None; let do_move; diff --git a/crates/gpui/src/platform/linux/x11/xim_handler.rs b/crates/gpui/src/platform/linux/x11/xim_handler.rs new file mode 100644 index 0000000000000000000000000000000000000000..b05cf5b0c3d0ddb7de2d4971cda2803b76007381 --- /dev/null +++ b/crates/gpui/src/platform/linux/x11/xim_handler.rs @@ -0,0 +1,154 @@ +use std::cell::RefCell; +use std::default::Default; +use std::rc::Rc; + +use calloop::channel; + +use x11rb::protocol::{xproto, Event}; +use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle, Point}; + +use crate::{Keystroke, PlatformInput, X11ClientState}; + +pub enum XimCallbackEvent { + XimXEvent(x11rb::protocol::Event), + XimPreeditEvent(xproto::Window, String), + XimCommitEvent(xproto::Window, String), +} + +pub struct XimHandler { + pub im_id: u16, + pub ic_id: u16, + pub xim_tx: channel::Sender, + pub connected: bool, + pub window: xproto::Window, +} + +impl XimHandler { + pub fn new(xim_tx: channel::Sender) -> Self { + Self { + im_id: Default::default(), + ic_id: Default::default(), + xim_tx, + connected: false, + window: Default::default(), + } + } +} + +impl> ClientHandler for XimHandler { + fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> { + client.open("C") + } + + fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> { + self.im_id = input_method_id; + + client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle]) + } + + fn handle_get_im_values( + &mut self, + client: &mut C, + input_method_id: u16, + _attributes: AHashMap>, + ) -> Result<(), ClientError> { + let ic_attributes = client + .build_ic_attributes() + .push( + AttributeName::InputStyle, + InputStyle::PREEDIT_CALLBACKS + | InputStyle::STATUS_NOTHING + | InputStyle::PREEDIT_NONE, + ) + .push(AttributeName::ClientWindow, self.window) + .push(AttributeName::FocusWindow, self.window) + .build(); + client.create_ic(input_method_id, ic_attributes) + } + + fn handle_create_ic( + &mut self, + _client: &mut C, + _input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + self.connected = true; + self.ic_id = input_context_id; + Ok(()) + } + + fn handle_commit( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + text: &str, + ) -> Result<(), ClientError> { + self.xim_tx.send(XimCallbackEvent::XimCommitEvent( + self.window, + String::from(text), + )); + 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> { + match (xev.response_type) { + x11rb::protocol::xproto::KEY_PRESS_EVENT => { + self.xim_tx + .send(XimCallbackEvent::XimXEvent(Event::KeyPress(xev))); + } + x11rb::protocol::xproto::KEY_RELEASE_EVENT => { + self.xim_tx + .send(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev))); + } + _ => {} + } + Ok(()) + } + + fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> { + client.disconnect() + } + + fn handle_destroy_ic( + &mut self, + client: &mut C, + input_method_id: u16, + _input_context_id: u16, + ) -> Result<(), ClientError> { + client.close(input_method_id) + } + + 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> { + // XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text + // XIMUnderline: 2, XIMSecondary: 16: underlined text + // XIMHighlight: 4: normal text + // XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position + // XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified, + // but interchangeable as above + // Currently there's no way to support these. + let mark_range = self.xim_tx.send(XimCallbackEvent::XimPreeditEvent( + self.window, + String::from(preedit_string), + )); + Ok(()) + } +}