Detailed changes
@@ -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"
@@ -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
@@ -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::*;
@@ -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<ConnectionError> for EventHandlerError {
+ fn from(err: ConnectionError) -> Self {
+ EventHandlerError::XCBConnectionError(err)
+ }
+}
+
+impl From<xim::ClientError> 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<calloop::EventLoop<'static, X11Client>>,
@@ -69,6 +102,8 @@ pub struct X11ClientState {
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
pub(crate) focused_window: Option<xproto::Window>,
pub(crate) xkb: xkbc::State,
+ pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
+ pub(crate) xim_handler: Option<XimHandler>,
pub(crate) cursor_handle: cursor::Handle,
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
@@ -227,12 +262,21 @@ impl X11Client {
let xcb_connection = Rc::new(xcb_connection);
+ let (xim_tx, xim_rx) = channel::channel::<XimCallbackEvent>();
+
+ 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::<ConnectionError>(
+ Generic::new_with_error::<EventHandlerError>(
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 {
@@ -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<Bounds<Pixels>> {
+ let mut state = self.state.borrow_mut();
+ let mut bounds: Option<Bounds<Pixels>> = 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<i32>) {
let mut resize_args = None;
let do_move;
@@ -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<XimCallbackEvent>,
+ pub connected: bool,
+ pub window: xproto::Window,
+}
+
+impl XimHandler {
+ pub fn new(xim_tx: channel::Sender<XimCallbackEvent>) -> Self {
+ Self {
+ im_id: Default::default(),
+ ic_id: Default::default(),
+ xim_tx,
+ connected: false,
+ window: Default::default(),
+ }
+ }
+}
+
+impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> 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<AttributeName, Vec<u8>>,
+ ) -> 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<xim::Feedback>,
+ ) -> 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(())
+ }
+}