From ec80f1d14f85b41ea27343429a4020632c1e494a Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Wed, 1 Oct 2025 16:09:34 +0200 Subject: [PATCH] Use the user's keyboard layout from the x11 system This change uses xkbcommon to decode the key utf8 value based on the user's current layout. The implementation is adapted from the x11rb's xkbcommon-example. --- Cargo.toml | 1 + src/wrappers.rs | 4 ++ src/wrappers/xkbcommon.rs | 73 +++++++++++++++++++++ src/x11/event_loop.rs | 12 +++- src/x11/keyboard.rs | 131 ++++++++++++++++++++++---------------- src/x11/window.rs | 9 ++- 6 files changed, 169 insertions(+), 61 deletions(-) create mode 100644 src/wrappers/xkbcommon.rs diff --git a/Cargo.toml b/Cargo.toml index 534fb563..54cbd5d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ raw-window-handle = "0.5" [target.'cfg(target_os="linux")'.dependencies] x11rb = { version = "0.13.2", features = ["cursor", "resource_manager", "allow-unsafe-code", "dl-libxcb"], default-features = false } +xkbcommon-dl = { version = "0.4", features = ["x11"] } x11-dl = { version = "2.21" } polling = "3.11.0" percent-encoding = "2.3.1" diff --git a/src/wrappers.rs b/src/wrappers.rs index d06a6d8b..d5d30536 100644 --- a/src/wrappers.rs +++ b/src/wrappers.rs @@ -15,6 +15,10 @@ #[cfg(target_os = "linux")] pub mod xlib; +/// Wrappers and utilities around xkbcommon. (provided by xkbcommon_dl) +#[cfg(target_os = "linux")] +pub mod xkbcommon; + /// Wrappers and utilities around GLX #[cfg(all(target_os = "linux", feature = "opengl"))] pub mod glx; diff --git a/src/wrappers/xkbcommon.rs b/src/wrappers/xkbcommon.rs new file mode 100644 index 00000000..041426bf --- /dev/null +++ b/src/wrappers/xkbcommon.rs @@ -0,0 +1,73 @@ +use xkbcommon_dl as xkbc; + +pub(crate) type Keycode = xkbcommon_dl::xkb_keycode_t; +/// A xkbcommon state object +pub struct XkbcommonState { + state: *mut xkbc::xkb_state, + xkb_common: &'static xkbc::XkbCommon, +} + +impl XkbcommonState { + pub fn new(xcb_connection: &crate::x11::XcbConnection) -> Self { + let xkb_common = xkbc::xkbcommon_handle(); + let context = + unsafe { (xkb_common.xkb_context_new)(xkbc::xkb_context_flags::XKB_CONTEXT_NO_FLAGS) }; + + let conn: *mut xkbc::x11::xcb_connection_t = + xcb_connection.conn.xcb_connection().get_raw_xcb_connection(); + + let xkb_x11 = xkbc::x11::xkbcommon_x11_handle(); + + let state = unsafe { + let device_id = (xkb_x11.xkb_x11_get_core_keyboard_device_id)(conn); + assert!(device_id >= 0); + let keymap = (xkb_x11.xkb_x11_keymap_new_from_device)( + context, + conn, + device_id, + xkbc::xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + (xkb_x11.xkb_x11_state_new_from_device)(keymap, conn, device_id) + }; + XkbcommonState { state, xkb_common } + } + + pub fn key_get_utf8(&self, code: Keycode) -> String { + // A buffer to store the cstr + let buffer_size = 32; + let mut buffer = vec![0; buffer_size]; + let result = unsafe { + (self.xkb_common.xkb_state_key_get_utf8)( + self.state, + code, + buffer.as_mut_ptr(), + buffer_size, + ) + }; + + // Convert back to String + if result < 0 { + "".to_string() + } else { + let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) }; + match c_str.to_str() { + Ok(s) => s.to_string(), + Err(_) => "".to_string(), + } + } + } + + pub fn update_key(&mut self, code: Keycode, dir: xkbc::xkb_key_direction) { + unsafe { + (self.xkb_common.xkb_state_update_key)(self.state, code, dir); + } + } + + pub fn update_key_down(&mut self, code: Keycode) { + self.update_key(code, xkbc::xkb_key_direction::XKB_KEY_DOWN) + } + + pub fn update_key_up(&mut self, code: Keycode) { + self.update_key(code, xkbc::xkb_key_direction::XKB_KEY_UP) + } +} diff --git a/src/x11/event_loop.rs b/src/x11/event_loop.rs index 21b50b1d..7bbb4a0e 100644 --- a/src/x11/event_loop.rs +++ b/src/x11/event_loop.rs @@ -1,4 +1,5 @@ use crate::wrappers::connection_poller::{ConnectionPoller, PollStatus}; +use crate::wrappers::xkbcommon::XkbcommonState; use crate::x11::drag_n_drop::DragNDropState; use crate::x11::keyboard::{convert_key_press_event, convert_key_release_event, key_mods}; use crate::x11::{ParentHandle, Window, WindowInner}; @@ -22,12 +23,14 @@ pub(super) struct EventLoop { event_loop_running: bool, drag_n_drop: DragNDropState, + + xkb_state: XkbcommonState, } impl EventLoop { pub fn new( window: WindowInner, handler: impl WindowHandler + 'static, - parent_handle: Option, + parent_handle: Option, xkb_state: XkbcommonState, ) -> Self { Self { window, @@ -37,6 +40,7 @@ impl EventLoop { event_loop_running: false, new_physical_size: None, drag_n_drop: DragNDropState::NoCurrentSession, + xkb_state, } } @@ -264,11 +268,13 @@ impl EventLoop { // keys //// XEvent::KeyPress(event) => { - self.handle_event(Event::Keyboard(convert_key_press_event(&event))); + let ev = Event::Keyboard(convert_key_press_event(&event, &mut self.xkb_state)); + self.handle_event(ev); } XEvent::KeyRelease(event) => { - self.handle_event(Event::Keyboard(convert_key_release_event(&event))); + let ev = Event::Keyboard(convert_key_release_event(&event, &mut self.xkb_state)); + self.handle_event(ev); } XEvent::FocusIn(_) => { diff --git a/src/x11/keyboard.rs b/src/x11/keyboard.rs index 634a205a..44c5de5d 100644 --- a/src/x11/keyboard.rs +++ b/src/x11/keyboard.rs @@ -23,12 +23,10 @@ use x11rb::protocol::xproto::{KeyButMask, KeyPressEvent, KeyReleaseEvent}; use keyboard_types::*; use crate::keyboard::code_to_location; +use crate::wrappers::xkbcommon::{Keycode, XkbcommonState}; /// Convert a hardware scan code to a key. -/// -/// Note: this is a hardcoded layout. We need to detect the user's -/// layout from the system and apply it. -fn code_to_key(code: Code, m: Modifiers) -> Key { +fn code_to_key(code: Code, m: Modifiers, hw_code: Keycode, xkb_state: &XkbcommonState) -> Key { fn a(s: &str) -> Key { Key::Character(s.into()) } @@ -39,6 +37,15 @@ fn code_to_key(code: Code, m: Modifiers) -> Key { Key::Character(base.into()) } } + fn k(mods: Modifiers, base: &str, code: Keycode, state: &XkbcommonState) -> Key { + if mods.contains(Modifiers::CONTROL) { + // When ctrl is set, then state.key_get_utf8 return control sequence like \x1e. + // TODO: handle this better? + Key::Character(base.into()) + } else { + Key::Character(state.key_get_utf8(code)) + } + } fn n(mods: Modifiers, base: Key, num: &str) -> Key { if mods.contains(Modifiers::NUM_LOCK) != mods.contains(Modifiers::SHIFT) { Key::Character(num.into()) @@ -47,55 +54,55 @@ fn code_to_key(code: Code, m: Modifiers) -> Key { } } match code { - Code::KeyA => s(m, "a", "A"), - Code::KeyB => s(m, "b", "B"), - Code::KeyC => s(m, "c", "C"), - Code::KeyD => s(m, "d", "D"), - Code::KeyE => s(m, "e", "E"), - Code::KeyF => s(m, "f", "F"), - Code::KeyG => s(m, "g", "G"), - Code::KeyH => s(m, "h", "H"), - Code::KeyI => s(m, "i", "I"), - Code::KeyJ => s(m, "j", "J"), - Code::KeyK => s(m, "k", "K"), - Code::KeyL => s(m, "l", "L"), - Code::KeyM => s(m, "m", "M"), - Code::KeyN => s(m, "n", "N"), - Code::KeyO => s(m, "o", "O"), - Code::KeyP => s(m, "p", "P"), - Code::KeyQ => s(m, "q", "Q"), - Code::KeyR => s(m, "r", "R"), - Code::KeyS => s(m, "s", "S"), - Code::KeyT => s(m, "t", "T"), - Code::KeyU => s(m, "u", "U"), - Code::KeyV => s(m, "v", "V"), - Code::KeyW => s(m, "w", "W"), - Code::KeyX => s(m, "x", "X"), - Code::KeyY => s(m, "y", "Y"), - Code::KeyZ => s(m, "z", "Z"), + Code::KeyA => k(m, "a", hw_code, xkb_state), + Code::KeyB => k(m, "b", hw_code, xkb_state), + Code::KeyC => k(m, "c", hw_code, xkb_state), + Code::KeyD => k(m, "d", hw_code, xkb_state), + Code::KeyE => k(m, "e", hw_code, xkb_state), + Code::KeyF => k(m, "f", hw_code, xkb_state), + Code::KeyG => k(m, "g", hw_code, xkb_state), + Code::KeyH => k(m, "h", hw_code, xkb_state), + Code::KeyI => k(m, "i", hw_code, xkb_state), + Code::KeyJ => k(m, "j", hw_code, xkb_state), + Code::KeyK => k(m, "k", hw_code, xkb_state), + Code::KeyL => k(m, "l", hw_code, xkb_state), + Code::KeyM => k(m, "m", hw_code, xkb_state), + Code::KeyN => k(m, "n", hw_code, xkb_state), + Code::KeyO => k(m, "o", hw_code, xkb_state), + Code::KeyP => k(m, "p", hw_code, xkb_state), + Code::KeyQ => k(m, "q", hw_code, xkb_state), + Code::KeyR => k(m, "r", hw_code, xkb_state), + Code::KeyS => k(m, "s", hw_code, xkb_state), + Code::KeyT => k(m, "t", hw_code, xkb_state), + Code::KeyU => k(m, "u", hw_code, xkb_state), + Code::KeyV => k(m, "v", hw_code, xkb_state), + Code::KeyW => k(m, "w", hw_code, xkb_state), + Code::KeyX => k(m, "x", hw_code, xkb_state), + Code::KeyY => k(m, "y", hw_code, xkb_state), + Code::KeyZ => k(m, "z", hw_code, xkb_state), - Code::Digit0 => s(m, "0", ")"), - Code::Digit1 => s(m, "1", "!"), - Code::Digit2 => s(m, "2", "@"), - Code::Digit3 => s(m, "3", "#"), - Code::Digit4 => s(m, "4", "$"), - Code::Digit5 => s(m, "5", "%"), - Code::Digit6 => s(m, "6", "^"), - Code::Digit7 => s(m, "7", "&"), - Code::Digit8 => s(m, "8", "*"), - Code::Digit9 => s(m, "9", "("), + Code::Digit0 => k(m, "0", hw_code, xkb_state), + Code::Digit1 => k(m, "1", hw_code, xkb_state), + Code::Digit2 => k(m, "2", hw_code, xkb_state), + Code::Digit3 => k(m, "3", hw_code, xkb_state), + Code::Digit4 => k(m, "4", hw_code, xkb_state), + Code::Digit5 => k(m, "5", hw_code, xkb_state), + Code::Digit6 => k(m, "6", hw_code, xkb_state), + Code::Digit7 => k(m, "7", hw_code, xkb_state), + Code::Digit8 => k(m, "8", hw_code, xkb_state), + Code::Digit9 => k(m, "9", hw_code, xkb_state), - Code::Backquote => s(m, "`", "~"), - Code::Minus => s(m, "-", "_"), - Code::Equal => s(m, "=", "+"), - Code::BracketLeft => s(m, "[", "{"), - Code::BracketRight => s(m, "]", "}"), - Code::Backslash => s(m, "\\", "|"), - Code::Semicolon => s(m, ";", ":"), - Code::Quote => s(m, "'", "\""), - Code::Comma => s(m, ",", "<"), - Code::Period => s(m, ".", ">"), - Code::Slash => s(m, "/", "?"), + Code::Backquote => k(m, "`", hw_code, xkb_state), + Code::Minus => k(m, "-", hw_code, xkb_state), + Code::Equal => k(m, "=", hw_code, xkb_state), + Code::BracketLeft => k(m, "[", hw_code, xkb_state), + Code::BracketRight => k(m, "]", hw_code, xkb_state), + Code::Backslash => k(m, "\\", hw_code, xkb_state), + Code::Semicolon => k(m, ";", hw_code, xkb_state), + Code::Quote => k(m, "'", hw_code, xkb_state), + Code::Comma => k(m, ",", hw_code, xkb_state), + Code::Period => k(m, ".", hw_code, xkb_state), + Code::Slash => k(m, "/", hw_code, xkb_state), Code::Space => a(" "), @@ -383,22 +390,36 @@ pub(super) fn key_mods(mods: KeyButMask) -> Modifiers { ret } -pub(super) fn convert_key_press_event(key_press: &KeyPressEvent) -> KeyboardEvent { +pub(super) fn convert_key_press_event( + key_press: &KeyPressEvent, state: &mut XkbcommonState, +) -> KeyboardEvent { let hw_keycode = key_press.detail; + + // Update the xkbc state + let hw_code = hw_keycode.into(); + state.update_key_down(hw_code); + let code = hardware_keycode_to_code(hw_keycode.into()); let modifiers = key_mods(key_press.state); - let key = code_to_key(code, modifiers); + let key = code_to_key(code, modifiers, hw_code, state); let location = code_to_location(code); let state = KeyState::Down; KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false } } -pub(super) fn convert_key_release_event(key_release: &KeyReleaseEvent) -> KeyboardEvent { +pub(super) fn convert_key_release_event( + key_release: &KeyReleaseEvent, state: &mut XkbcommonState, +) -> KeyboardEvent { let hw_keycode = key_release.detail; + + // Update the xkbc state + let hw_code = hw_keycode.into(); + state.update_key_up(hw_code); + let code = hardware_keycode_to_code(hw_keycode.into()); let modifiers = key_mods(key_release.state); - let key = code_to_key(code, modifiers); + let key = code_to_key(code, modifiers, hw_code, state); let location = code_to_location(code); let state = KeyState::Up; diff --git a/src/x11/window.rs b/src/x11/window.rs index 448e648e..1b0b2c25 100644 --- a/src/x11/window.rs +++ b/src/x11/window.rs @@ -2,9 +2,9 @@ use std::cell::Cell; use std::error::Error; use std::ffi::c_void; use std::rc::Rc; +use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; -use std::sync::Arc; use std::thread::{self, JoinHandle}; use raw_window_handle::{ @@ -26,7 +26,7 @@ use crate::{ }; #[cfg(feature = "opengl")] -use crate::gl::{platform, GlContext}; +use crate::gl::{GlContext, platform}; use crate::x11::event_loop::EventLoop; use crate::x11::visual_info::WindowVisualConfig; @@ -177,6 +177,9 @@ impl<'a> Window<'a> { // FIXME: baseview error type instead of unwrap() let xcb_connection = XcbConnection::new()?; + // Setup xkbcommon + let xkb_state = crate::wrappers::xkbcommon::XkbcommonState::new(&xcb_connection); + // Get screen information let screen = xcb_connection.screen(); let parent_id = parent.unwrap_or(screen.root); @@ -303,7 +306,7 @@ impl<'a> Window<'a> { let _ = tx.send(Ok(SendableRwh(window.raw_window_handle()))); - EventLoop::new(inner, handler, parent_handle).run()?; + EventLoop::new(inner, handler, parent_handle, xkb_state).run()?; Ok(()) }