From 6404099d255012b50b846caa90babf08ddbc26dd Mon Sep 17 00:00:00 2001 From: HMH Date: Fri, 20 Aug 2021 05:51:56 +0200 Subject: [PATCH] IME support on X11 (#1043) * WIP: IME support for X11 * Handle text generated by IME. * Set IME position according to the cursor position. * Improve IME position handling. Geometry as well as window focus changes are now handled. * Dispatch IME strings like it's done on windows. * Make sure not to silently drop IME errors. * Respect `use_ime` configuration. * Add xcb-util as dependency. * Only update IME position if necessary. * Formatting. * Update xcb-imdkit-rs. * Set IME position under the start of the cursor. This seems to be the way it is commonly done among gui frameworks. (Tested with Firefox for GTK and Konsole for QT). * Update xcb-imdkit-rs. * Handle systems only providing libxcb-util0-dev. * Add libxcb to freebsd dependencies. Required by xcb-imdkit-rs. * Update xcb-imdkit-rs. * Try to use more recent gcc on centos7. * More recent C++ compiler on centos7 as well. * Also setup correct env on centos7 for tests. --- .github/workflows/gen_centos7.yml | 17 +++++---- Cargo.lock | 13 +++++++ get-deps | 7 ++++ window/Cargo.toml | 1 + window/src/os/x11/connection.rs | 62 ++++++++++++++++++++++++++++--- window/src/os/x11/window.rs | 54 ++++++++++++++++++++++++++- window/src/os/x_and_wayland.rs | 10 ++++- 7 files changed, 147 insertions(+), 17 deletions(-) diff --git a/.github/workflows/gen_centos7.yml b/.github/workflows/gen_centos7.yml index 7e351a348..8c2e8ec87 100644 --- a/.github/workflows/gen_centos7.yml +++ b/.github/workflows/gen_centos7.yml @@ -31,9 +31,10 @@ jobs: - name: "Install Git from source" shell: bash run: | - - yum install -y wget curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker make - + + yum install -y wget curl-devel expat-devel gettext-devel openssl-devel zlib-devel centos-release-scl-rh perl-ExtUtils-MakeMaker make + yum install -y devtoolset-9-gcc devtoolset-9-gcc-c++ + if test ! -x /usr/local/git/bin/git ; then cd /tmp wget https://github.com/git/git/archive/v2.26.2.tar.gz @@ -41,9 +42,9 @@ jobs: cd git-2.26.2 make prefix=/usr/local/git install fi - + ln -s /usr/local/git/bin/git /usr/local/bin/git - + - name: "Install curl" shell: bash @@ -84,10 +85,10 @@ jobs: run: "cargo fmt --all -- --check" - name: "Build (Release mode)" shell: bash - run: "cargo build --all --release" + run: "source /opt/rh/devtoolset-9/enable && cargo build --all --release" - name: "Test (Release mode)" shell: bash - run: "cargo test --all --release" + run: "source /opt/rh/devtoolset-9/enable && cargo test --all --release" - name: "Package" shell: bash run: "bash ci/deploy.sh" @@ -96,7 +97,7 @@ jobs: run: | mkdir pkg_ mv ~/rpmbuild/RPMS/*/*.rpm pkg_ - + - name: "Upload artifact" uses: actions/upload-artifact@master diff --git a/Cargo.lock b/Cargo.lock index 38df28923..ded58d6d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5339,6 +5339,7 @@ dependencies = [ "winreg 0.6.2", "x11", "xcb 0.9.0", + "xcb-imdkit", "xcb-util", "xkbcommon", ] @@ -5453,6 +5454,18 @@ dependencies = [ "x11", ] +[[package]] +name = "xcb-imdkit" +version = "0.1.0" +source = "git+https://github.com/H-M-H/xcb-imdkit-rs#3cb940863f64d13a9cdbc09eea67d2a69e0a2737" +dependencies = [ + "bitflags", + "cc", + "lazy_static", + "pkg-config", + "xcb 0.9.0", +] + [[package]] name = "xcb-util" version = "0.3.0" diff --git a/get-deps b/get-deps index 1626ef84d..af81411c2 100755 --- a/get-deps +++ b/get-deps @@ -77,6 +77,7 @@ if test -e /etc/centos-release || test -e /etc/fedora-release; then libxkbcommon-x11-devel \ wayland-devel \ mesa-libEGL-devel \ + xcb-util-devel \ xcb-util-keysyms-devel \ xcb-util-image-devel \ xcb-util-wm-devel \ @@ -101,6 +102,7 @@ if test -x /usr/bin/lsb_release && test `lsb_release -si` = "openSUSE"; then libxkbcommon-x11-devel \ wayland-devel \ Mesa-libEGL-devel \ + xcb-util-devel \ xcb-util-keysyms-devel \ xcb-util-image-devel \ xcb-util-wm-devel \ @@ -110,6 +112,7 @@ fi if test -e /etc/debian_version ; then APT="$SUDO apt-get" + apt-cache show libxcb-util-dev > /dev/null 2>&1 && XCBUTIL="libxcb-util-dev" || XCBUTIL="libxcb-util0-dev" $APT install -y \ bsdutils \ cmake \ @@ -130,6 +133,7 @@ if test -e /etc/debian_version ; then libxcb-xkb-dev \ libxkbcommon-dev \ libxkbcommon-x11-dev \ + "$XCBUTIL" \ lsb-release \ python3 \ xdg-utils \ @@ -151,6 +155,7 @@ if test -e /etc/arch-release ; then 'python3' \ 'rust' \ 'wayland' \ + 'xcb-util' \ 'xcb-util-image' \ 'xcb-util-keysyms' \ 'xcb-util-wm' @@ -183,6 +188,8 @@ case $OSTYPE in python3 \ rust \ wayland \ + libxcb \ + xcb-util \ xcb-util-image \ xcb-util-keysyms \ xcb-util-wm \ diff --git a/window/Cargo.toml b/window/Cargo.toml index 8fdfe53c2..5342354fb 100644 --- a/window/Cargo.toml +++ b/window/Cargo.toml @@ -73,6 +73,7 @@ smithay-client-toolkit = {version = "0.14", default-features=false, optional=tru wayland-protocols = {version="0.28", optional=true} wayland-client = {version="0.28", optional=true} wayland-egl = {version="0.28", optional=true} +xcb-imdkit = { version="0.1", git = "https://github.com/H-M-H/xcb-imdkit-rs" } [target.'cfg(target_os="macos")'.dependencies] cocoa = "0.20" diff --git a/window/src/os/x11/connection.rs b/window/src/os/x11/connection.rs index c621df9ef..81e6d4e78 100644 --- a/window/src/os/x11/connection.rs +++ b/window/src/os/x11/connection.rs @@ -46,6 +46,8 @@ pub struct XConnection { pub(crate) visual: xcb::xproto::Visualtype, pub(crate) depth: u8, pub(crate) gl_connection: RefCell>>, + pub(crate) ime: RefCell>>, + pub(crate) ime_process_event_result: RefCell>, } impl std::ops::Deref for XConnection { @@ -275,7 +277,7 @@ impl XConnection { } }, Some(event) => { - if let Err(err) = self.process_xcb_event(&event) { + if let Err(err) = self.process_xcb_event_ime(&event) { return Err(err); } } @@ -285,12 +287,23 @@ impl XConnection { loop { match self.conn.poll_for_queued_event() { None => return Ok(()), - Some(event) => self.process_xcb_event(&event)?, + Some(event) => self.process_xcb_event_ime(&event)?, } self.conn.flush(); } } + fn process_xcb_event_ime(&self, event: &xcb::GenericEvent) -> anyhow::Result<()> { + // check for previous errors produced by the IME forward_event callback + self.ime_process_event_result.replace(Ok(()))?; + + if config::configuration().use_ime && self.ime.borrow_mut().process_event(event) { + self.ime_process_event_result.replace(Ok(())) + } else { + self.process_xcb_event(event) + } + } + fn process_xcb_event(&self, event: &xcb::GenericEvent) -> anyhow::Result<()> { if let Some(window_id) = window_id_from_event(event) { self.process_window_event(window_id, event)?; @@ -326,7 +339,7 @@ impl XConnection { Ok(()) } - pub(crate) fn create_new() -> anyhow::Result { + pub(crate) fn create_new() -> anyhow::Result> { let (conn, screen_num) = connect_with_xlib_display()?; let conn = xcb_util::ewmh::Connection::connect(conn) .map_err(|_| anyhow!("failed to init ewmh"))?; @@ -441,7 +454,17 @@ impl XConnection { let default_dpi = RefCell::new(compute_default_dpi(&xrm, &xsettings)); - let conn = XConnection { + xcb_imdkit::ImeClient::set_logger(|msg| log::debug!("Ime: {}", msg)); + let ime = unsafe { + xcb_imdkit::ImeClient::unsafe_new( + &conn, + screen_num, + xcb_imdkit::InputStyle::DEFAULT, + None, + ) + }; + + let conn = Rc::new(XConnection { conn, default_dpi, xsettings: RefCell::new(xsettings), @@ -472,7 +495,36 @@ impl XConnection { depth, visual, gl_connection: RefCell::new(None), - }; + ime: RefCell::new(ime), + ime_process_event_result: RefCell::new(Ok(())), + }); + + { + let conn = conn.clone(); + conn.clone() + .ime + .borrow_mut() + .set_commit_string_cb(move |window_id, input| { + if let Some(window) = conn.window_by_id(window_id) { + let mut inner = window.lock().unwrap(); + inner.dispatch_ime_text(input); + } + }); + } + { + let conn = conn.clone(); + conn.clone() + .ime + .borrow_mut() + .set_forward_event_cb(move |_win, e| { + if let err @ Err(_) = conn.process_xcb_event(unsafe { std::mem::transmute(e) }) + { + if let Err(err) = conn.ime_process_event_result.replace(err) { + log::warn!("IME process event error dropped: {}", err); + } + } + }); + } Ok(conn) } diff --git a/window/src/os/x11/window.rs b/window/src/os/x11/window.rs index 3e62d7e51..d7a6497c1 100644 --- a/window/src/os/x11/window.rs +++ b/window/src/os/x11/window.rs @@ -5,8 +5,8 @@ use crate::os::xkeysyms; use crate::os::{Connection, Window}; use crate::{ Appearance, Clipboard, Dimensions, MouseButtons, MouseCursor, MouseEvent, MouseEventKind, - MousePress, Point, ScreenPoint, WindowDecorations, WindowEvent, WindowEventSender, WindowOps, - WindowState, + MousePress, Point, Rect, ScreenPoint, WindowDecorations, WindowEvent, WindowEventSender, + WindowOps, WindowState, }; use anyhow::{anyhow, Context as _}; use async_trait::async_trait; @@ -19,6 +19,7 @@ use std::convert::TryInto; use std::rc::{Rc, Weak}; use std::sync::{Arc, Mutex}; use wezterm_font::FontConfiguration; +use wezterm_input_types::{KeyCode, KeyEvent, Modifiers}; #[derive(Default)] struct CopyAndPaste { @@ -64,6 +65,8 @@ pub(crate) struct XWindowInner { config: ConfigHandle, appearance: Appearance, title: String, + has_focus: bool, + last_cursor_position: Rect, } impl Drop for XWindowInner { @@ -167,6 +170,8 @@ impl XWindowInner { self.expose(expose.x(), expose.y(), expose.width(), expose.height()); } xcb::CONFIGURE_NOTIFY => { + self.update_ime_position(); + let cfg: &xcb::ConfigureNotifyEvent = unsafe { xcb::cast_event(event) }; let width = cfg.width(); let height = cfg.height(); @@ -331,10 +336,13 @@ impl XWindowInner { } } xcb::FOCUS_IN => { + self.has_focus = true; + self.update_ime_position(); log::trace!("Calling focus_change(true)"); self.events.dispatch(WindowEvent::FocusChanged(true)); } xcb::FOCUS_OUT => { + self.has_focus = false; log::trace!("Calling focus_change(false)"); self.events.dispatch(WindowEvent::FocusChanged(false)); } @@ -346,6 +354,20 @@ impl XWindowInner { Ok(()) } + pub fn dispatch_ime_text(&mut self, text: &str) { + let key_event = KeyEvent { + key: KeyCode::Composed(text.into()), + raw_key: None, + raw_modifiers: Modifiers::NONE, + raw_code: None, + modifiers: Modifiers::NONE, + repeat_count: 1, + key_is_down: true, + } + .normalize_shift(); + self.events.dispatch(WindowEvent::KeyEvent(key_event)); + } + /// If we own the selection, make sure that the X server reflects /// that and vice versa. fn update_selection_owner(&mut self, clipboard: Clipboard) { @@ -758,6 +780,8 @@ impl XWindow { copy_and_paste: CopyAndPaste::default(), cursors: CursorInfo::new(&conn), config: config.clone(), + has_focus: false, + last_cursor_position: Rect::default(), })) }; @@ -872,6 +896,25 @@ impl XWindowInner { } } + fn set_text_cursor_position(&mut self, cursor: Rect) { + if self.last_cursor_position == cursor { + return; + } + self.last_cursor_position = cursor; + self.update_ime_position(); + } + + fn update_ime_position(&mut self) { + if !self.has_focus { + return; + } + self.conn().ime.borrow_mut().update_pos( + self.window_id, + self.last_cursor_position.min_x() as i16, + self.last_cursor_position.max_y() as i16, + ); + } + fn set_icon(&mut self, image: &dyn BitmapImage) { let (width, height) = image.image_dimensions(); @@ -1012,6 +1055,13 @@ impl WindowOps for XWindow { }); } + fn set_text_cursor_position(&self, cursor: Rect) { + XConnection::with_window_inner(self.0, move |inner| { + inner.set_text_cursor_position(cursor); + Ok(()) + }); + } + fn set_icon(&self, image: Image) { XConnection::with_window_inner(self.0, move |inner| { inner.set_icon(&image); diff --git a/window/src/os/x_and_wayland.rs b/window/src/os/x_and_wayland.rs index c6ff60c3c..bbb405a0f 100644 --- a/window/src/os/x_and_wayland.rs +++ b/window/src/os/x_and_wayland.rs @@ -7,7 +7,7 @@ use crate::os::wayland::connection::WaylandConnection; use crate::os::wayland::window::WaylandWindow; use crate::os::x11::connection::XConnection; use crate::os::x11::window::XWindow; -use crate::{Clipboard, MouseCursor, ScreenPoint, WindowEvent, WindowOps}; +use crate::{Clipboard, MouseCursor, Rect, ScreenPoint, WindowEvent, WindowOps}; use async_trait::async_trait; use config::ConfigHandle; use promise::*; @@ -43,7 +43,7 @@ impl Connection { } } } - Ok(Connection::X11(Rc::new(XConnection::create_new()?))) + Ok(Connection::X11(XConnection::create_new()?)) } pub async fn new_window( @@ -277,6 +277,12 @@ impl WindowOps for Window { } } + fn set_text_cursor_position(&self, cursor: Rect) { + if let Self::X11(x) = self { + x.set_text_cursor_position(cursor); + } + } + fn get_clipboard(&self, clipboard: Clipboard) -> Future { match self { Self::X11(x) => x.get_clipboard(clipboard),