diff --git a/Cargo.lock b/Cargo.lock index fc83b03dd9..404a428598 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 70254df74c..bc2297e053 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 958da047d6..6df8e9a3d6 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 1c3a21c431..df1fc4a34c 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 783af95d2a..7f500b4ed4 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 0000000000..b05cf5b0c3 --- /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(()) + } +}