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 <np@mkv.li>
This commit is contained in:
npmania 2024-05-17 07:13:51 +09:00 committed by GitHub
parent 97691c1def
commit b60254feca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 423 additions and 5 deletions

60
Cargo.lock generated
View File

@ -70,6 +70,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"const-random",
"getrandom 0.2.10", "getrandom 0.2.10",
"once_cell", "once_cell",
"version_check", "version_check",
@ -2560,6 +2561,26 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" 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]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -4752,6 +4773,7 @@ dependencies = [
"windows 0.56.0", "windows 0.56.0",
"windows-core 0.56.0", "windows-core 0.56.0",
"x11rb", "x11rb",
"xim",
"xkbcommon", "xkbcommon",
] ]
@ -10396,6 +10418,15 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]] [[package]]
name = "tiny-skia" name = "tiny-skia"
version = "0.11.4" version = "0.11.4"
@ -12798,6 +12829,35 @@ dependencies = [
"winapi", "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]] [[package]]
name = "xkbcommon" name = "xkbcommon"
version = "0.7.0" version = "0.7.0"

View File

@ -126,6 +126,7 @@ x11rb = { version = "0.13.0", features = [
"resource_manager", "resource_manager",
] } ] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] } 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] [target.'cfg(windows)'.dependencies]
windows.workspace = true windows.workspace = true

View File

@ -2,8 +2,10 @@ mod client;
mod display; mod display;
mod event; mod event;
mod window; mod window;
mod xim_handler;
pub(crate) use client::*; pub(crate) use client::*;
pub(crate) use display::*; pub(crate) use display::*;
pub(crate) use event::*; pub(crate) use event::*;
pub(crate) use window::*; pub(crate) use window::*;
pub(crate) use xim_handler::*;

View File

@ -4,7 +4,8 @@ use std::rc::{Rc, Weak};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use calloop::generic::{FdWrapper, Generic}; use calloop::generic::{FdWrapper, Generic};
use calloop::{EventLoop, LoopHandle, RegistrationToken}; use calloop::{channel, EventLoop, LoopHandle, RegistrationToken};
use collections::HashMap; use collections::HashMap;
use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext}; use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
use copypasta::ClipboardProvider; 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::protocol::{randr, render, xinput, xkb, xproto, Event};
use x11rb::resource_manager::Database; use x11rb::resource_manager::Database;
use x11rb::xcb_ffi::XCBConnection; 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 xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
use xkbcommon::xkb as xkbc; use xkbcommon::xkb as xkbc;
@ -36,6 +38,7 @@ use super::{
X11Display, X11WindowStatePtr, XcbAtoms, X11Display, X11WindowStatePtr, XcbAtoms,
}; };
use super::{button_from_mask, button_of_key, modifiers_from_state}; 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::is_within_click_distance;
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; 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 struct X11ClientState {
pub(crate) loop_handle: LoopHandle<'static, X11Client>, pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'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) windows: HashMap<xproto::Window, WindowRef>,
pub(crate) focused_window: Option<xproto::Window>, pub(crate) focused_window: Option<xproto::Window>,
pub(crate) xkb: xkbc::State, 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_handle: cursor::Handle,
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>, pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
@ -227,12 +262,21 @@ impl X11Client {
let xcb_connection = Rc::new(xcb_connection); 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 // Safety: Safe if xcb::Connection always returns a valid fd
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
handle handle
.insert_source( .insert_source(
Generic::new_with_error::<ConnectionError>( Generic::new_with_error::<EventHandlerError>(
fd, fd,
calloop::Interest::READ, calloop::Interest::READ,
calloop::Mode::Level, calloop::Mode::Level,
@ -241,14 +285,63 @@ impl X11Client {
let xcb_connection = xcb_connection.clone(); let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| { move |_readiness, _, client| {
while let Some(event) = xcb_connection.poll_for_event()? { 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) Ok(calloop::PostAction::Continue)
} }
}, },
) )
.expect("Failed to initialize x11 event source"); .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 { X11Client(Rc::new(RefCell::new(X11ClientState {
event_loop: Some(event_loop), event_loop: Some(event_loop),
loop_handle: handle, loop_handle: handle,
@ -265,6 +358,8 @@ impl X11Client {
windows: HashMap::default(), windows: HashMap::default(),
focused_window: None, focused_window: None,
xkb: xkb_state, xkb: xkb_state,
ximc,
xim_handler,
cursor_handle, cursor_handle,
cursor_styles: HashMap::default(), cursor_styles: HashMap::default(),
@ -365,7 +460,6 @@ impl X11Client {
} }
keystroke keystroke
}; };
drop(state); drop(state);
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke, keystroke,
@ -550,6 +644,79 @@ impl X11Client {
Some(()) 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 { impl LinuxClient for X11Client {

View File

@ -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>) { pub fn configure(&self, bounds: Bounds<i32>) {
let mut resize_args = None; let mut resize_args = None;
let do_move; let do_move;

View File

@ -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(())
}
}