Compare commits

...
Sign in to create a new pull request.

48 commits

Author SHA1 Message Date
Junkui Zhang
8276554222 fix merge conflicts 2025-07-10 16:28:10 +08:00
Junkui Zhang
2400ed34e0 fix merge conflicts 2025-07-10 14:06:42 +08:00
Junkui Zhang
ce23637dc1 fix test 2025-07-10 13:46:56 +08:00
Junkui Zhang
7bcd6d839f use Keymap 2025-07-10 13:46:56 +08:00
Junkui Zhang
20b9989b79 remove unused 2025-07-10 13:46:56 +08:00
Junkui Zhang
3269029a3e clippy 2025-07-10 13:46:56 +08:00
Junkui Zhang
ea0e908714 fix merge conflicts 2025-07-10 13:46:56 +08:00
Junkui Zhang
8b69ccb488 actually fix x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
2d6d53219b try fix x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
de615870cc fix ci 2025-07-10 13:46:24 +08:00
Junkui Zhang
f1f9470a14 simplify 2025-07-10 13:46:24 +08:00
Junkui Zhang
9433c381eb remove debug print 2025-07-10 13:46:24 +08:00
Junkui Zhang
6ede6a1573 test 2025-07-10 13:46:24 +08:00
Junkui Zhang
7e0d74db8a simplify 2025-07-10 13:46:24 +08:00
Junkui Zhang
e019ce8260 clippy 2025-07-10 13:46:24 +08:00
Junkui Zhang
7ab7eab54e fix headless 2025-07-10 13:46:24 +08:00
Junkui Zhang
7da2e2af50 use cache for x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
92a986cffd use cache for wayland 2025-07-10 13:46:24 +08:00
Junkui Zhang
8b6658c650 refactor keyboard layout 2025-07-10 13:46:24 +08:00
Junkui Zhang
9bc4c0f7f5 try fix x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
f829a178f6 fix group 2025-07-10 13:46:24 +08:00
Junkui Zhang
2df80de880 fix 2025-07-10 13:46:24 +08:00
Junkui Zhang
2976b99b14 update group 2025-07-10 13:46:24 +08:00
Junkui Zhang
ad3d44119a fix all 2025-07-10 13:46:24 +08:00
Junkui Zhang
0b5a264f88 revert Instroduce KeyboardState 2025-07-10 13:46:07 +08:00
Junkui Zhang
f22d06cf6e Revert "test"
This reverts commit 8e773a8fcdffbaa754098da3f93c6ede28bf75a3.
2025-07-10 13:46:00 +08:00
Junkui Zhang
26718da9bb test 2025-07-10 13:46:00 +08:00
Junkui Zhang
671a91e69d remvoe deps of state 2025-07-10 13:46:00 +08:00
Junkui Zhang
bbda5d4f78 test 2025-07-10 13:46:00 +08:00
Junkui Zhang
2fb674088f remove unused 2025-07-10 13:46:00 +08:00
Junkui Zhang
5c4ea49793 fix dvoka 2025-07-10 13:46:00 +08:00
Junkui Zhang
21e7cc3fed add letters 2025-07-10 13:46:00 +08:00
Junkui Zhang
f53168c56c test 2025-07-10 13:46:00 +08:00
Junkui Zhang
3182f14972 add shift test 2025-07-10 13:46:00 +08:00
Junkui Zhang
eca211e0f1 add helper 2025-07-10 13:46:00 +08:00
Junkui Zhang
d6add799dc try add a test 2025-07-10 13:46:00 +08:00
Junkui Zhang
e9649dc25c fix 2025-07-10 13:46:00 +08:00
Junkui Zhang
71daf47ad5 fix deadkey 2025-07-10 13:45:28 +08:00
Junkui Zhang
66c6f5066e fix russian layout 2025-07-10 13:45:28 +08:00
Junkui Zhang
be79ccde07 fix all 2025-07-10 13:45:28 +08:00
Junkui Zhang
061ba9b6d2 use it 2025-07-10 13:45:28 +08:00
Junkui Zhang
1ba3c2f589 fix wayland 2025-07-10 13:45:28 +08:00
Junkui Zhang
bbee877ca8 fix x11 2025-07-10 13:45:28 +08:00
Junkui Zhang
c6e697fc7f add KeyboardState 2025-07-10 13:44:12 +08:00
Junkui Zhang
b7b83105fc add mapper 2025-07-10 13:44:12 +08:00
Junkui Zhang
7d901c5e47 parse immutable 2025-07-10 13:44:12 +08:00
Junkui Zhang
1a8d1944b0 test 2025-07-10 13:44:12 +08:00
Junkui Zhang
35db644beb init 2025-07-10 13:44:12 +08:00
6 changed files with 633 additions and 168 deletions

View file

@ -1,6 +1,12 @@
name: "Run tests"
description: "Runs the tests"
inputs:
use-xvfb:
description: "Whether to run tests with xvfb"
required: false
default: "false"
runs:
using: "composite"
steps:
@ -20,4 +26,9 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast
run: |
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
else
cargo nextest run --workspace --no-fail-fast
fi

View file

@ -334,6 +334,8 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
with:
use-xvfb: true
- name: Build other binaries and features
run: |

View file

@ -1,6 +1,13 @@
#[cfg(any(feature = "wayland", feature = "x11"))]
use collections::{HashMap, HashSet};
#[cfg(any(feature = "wayland", feature = "x11"))]
use strum::{EnumIter, IntoEnumIterator as _};
#[cfg(any(feature = "wayland", feature = "x11"))]
use xkbcommon::xkb::{Keycode, Keymap, Keysym, MOD_NAME_SHIFT, State};
use crate::{PlatformKeyboardLayout, SharedString};
#[derive(Clone)]
#[derive(Debug, Clone)]
pub(crate) struct LinuxKeyboardLayout {
name: SharedString,
}
@ -20,3 +27,449 @@ impl LinuxKeyboardLayout {
Self { name }
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) struct LinuxKeyboardMapper {
letters: HashMap<Keycode, String>,
code_to_key: HashMap<Keycode, String>,
code_to_shifted_key: HashMap<Keycode, String>,
}
#[cfg(any(feature = "wayland", feature = "x11"))]
impl LinuxKeyboardMapper {
pub(crate) fn new(
keymap: &Keymap,
base_group: u32,
latched_group: u32,
locked_group: u32,
) -> Self {
let mut xkb_state = State::new(keymap);
xkb_state.update_mask(0, 0, 0, base_group, latched_group, locked_group);
let mut shifted_state = State::new(&keymap);
let shift_mod = keymap.mod_get_index(MOD_NAME_SHIFT);
let shift_mask = 1 << shift_mod;
shifted_state.update_mask(shift_mask, 0, 0, base_group, latched_group, locked_group);
let mut letters = HashMap::default();
let mut code_to_key = HashMap::default();
let mut code_to_shifted_key = HashMap::default();
let mut inserted_letters = HashSet::default();
for scan_code in LinuxScanCodes::iter() {
let keycode = Keycode::new(scan_code as u32);
let key = xkb_state.key_get_utf8(keycode);
if !key.is_empty() {
if key_is_a_letter(&key) {
letters.insert(keycode, key.clone());
inserted_letters.insert(key);
} else {
code_to_key.insert(keycode, key.clone());
}
} else {
// keycode might be a dead key
let keysym = xkb_state.key_get_one_sym(keycode);
if let Some(key) = underlying_dead_key(keysym) {
code_to_key.insert(keycode, key.clone());
}
}
let shifted_key = shifted_state.key_get_utf8(keycode);
if !shifted_key.is_empty() {
code_to_shifted_key.insert(keycode, shifted_key);
} else {
// keycode might be a dead key
let shifted_keysym = shifted_state.key_get_one_sym(keycode);
if let Some(shifted_key) = underlying_dead_key(shifted_keysym) {
code_to_shifted_key.insert(keycode, shifted_key);
}
}
}
insert_letters_if_missing(&inserted_letters, &mut letters);
Self {
letters,
code_to_key,
code_to_shifted_key,
}
}
pub(crate) fn get_key(
&self,
keycode: Keycode,
modifiers: &mut crate::Modifiers,
) -> Option<String> {
if let Some(key) = self.letters.get(&keycode) {
return Some(key.clone());
}
if modifiers.shift {
modifiers.shift = false;
self.code_to_shifted_key.get(&keycode).cloned()
} else {
self.code_to_key.get(&keycode).cloned()
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
fn key_is_a_letter(key: &str) -> bool {
matches!(
key,
"a" | "b"
| "c"
| "d"
| "e"
| "f"
| "g"
| "h"
| "i"
| "j"
| "k"
| "l"
| "m"
| "n"
| "o"
| "p"
| "q"
| "r"
| "s"
| "t"
| "u"
| "v"
| "w"
| "x"
| "y"
| "z"
)
}
/**
* Returns which symbol the dead key represents
* <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
*/
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) fn underlying_dead_key(keysym: Keysym) -> Option<String> {
match keysym {
Keysym::dead_grave => Some("`".to_owned()),
Keysym::dead_acute => Some("´".to_owned()),
Keysym::dead_circumflex => Some("^".to_owned()),
Keysym::dead_tilde => Some("~".to_owned()),
Keysym::dead_macron => Some("¯".to_owned()),
Keysym::dead_breve => Some("˘".to_owned()),
Keysym::dead_abovedot => Some("˙".to_owned()),
Keysym::dead_diaeresis => Some("¨".to_owned()),
Keysym::dead_abovering => Some("˚".to_owned()),
Keysym::dead_doubleacute => Some("˝".to_owned()),
Keysym::dead_caron => Some("ˇ".to_owned()),
Keysym::dead_cedilla => Some("¸".to_owned()),
Keysym::dead_ogonek => Some("˛".to_owned()),
Keysym::dead_iota => Some("ͅ".to_owned()),
Keysym::dead_voiced_sound => Some("".to_owned()),
Keysym::dead_semivoiced_sound => Some("".to_owned()),
Keysym::dead_belowdot => Some("̣̣".to_owned()),
Keysym::dead_hook => Some("̡".to_owned()),
Keysym::dead_horn => Some("̛".to_owned()),
Keysym::dead_stroke => Some("̶̶".to_owned()),
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
Keysym::dead_doublegrave => Some("̏".to_owned()),
Keysym::dead_belowring => Some("˳".to_owned()),
Keysym::dead_belowmacron => Some("̱".to_owned()),
Keysym::dead_belowcircumflex => Some("".to_owned()),
Keysym::dead_belowtilde => Some("̰".to_owned()),
Keysym::dead_belowbreve => Some("̮".to_owned()),
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
Keysym::dead_invertedbreve => Some("̯".to_owned()),
Keysym::dead_belowcomma => Some("̦".to_owned()),
Keysym::dead_currency => None,
Keysym::dead_lowline => None,
Keysym::dead_aboveverticalline => None,
Keysym::dead_belowverticalline => None,
Keysym::dead_longsolidusoverlay => None,
Keysym::dead_a => None,
Keysym::dead_A => None,
Keysym::dead_e => None,
Keysym::dead_E => None,
Keysym::dead_i => None,
Keysym::dead_I => None,
Keysym::dead_o => None,
Keysym::dead_O => None,
Keysym::dead_u => None,
Keysym::dead_U => None,
Keysym::dead_small_schwa => Some("ə".to_owned()),
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
Keysym::dead_greek => None,
_ => None,
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
fn insert_letters_if_missing(inserted: &HashSet<String>, letters: &mut HashMap<Keycode, String>) {
for scan_code in LinuxScanCodes::LETTERS.iter() {
let keycode = Keycode::new(*scan_code as u32);
let key = scan_code.to_str();
if !inserted.contains(key) {
letters.insert(keycode, key.to_owned());
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
enum LinuxScanCodes {
A = 0x0026,
B = 0x0038,
C = 0x0036,
D = 0x0028,
E = 0x001a,
F = 0x0029,
G = 0x002a,
H = 0x002b,
I = 0x001f,
J = 0x002c,
K = 0x002d,
L = 0x002e,
M = 0x003a,
N = 0x0039,
O = 0x0020,
P = 0x0021,
Q = 0x0018,
R = 0x001b,
S = 0x0027,
T = 0x001c,
U = 0x001e,
V = 0x0037,
W = 0x0019,
X = 0x0035,
Y = 0x001d,
Z = 0x0034,
Digit0 = 0x0013,
Digit1 = 0x000a,
Digit2 = 0x000b,
Digit3 = 0x000c,
Digit4 = 0x000d,
Digit5 = 0x000e,
Digit6 = 0x000f,
Digit7 = 0x0010,
Digit8 = 0x0011,
Digit9 = 0x0012,
Backquote = 0x0031,
Minus = 0x0014,
Equal = 0x0015,
LeftBracket = 0x0022,
RightBracket = 0x0023,
Backslash = 0x0033,
Semicolon = 0x002f,
Quote = 0x0030,
Comma = 0x003b,
Period = 0x003c,
Slash = 0x003d,
// This key is typically located near LeftShift key, varies on international keyboards: Dan: <> Dutch: ][ Ger: <> UK: \|
IntlBackslash = 0x005e,
// Used for Brazilian /? and Japanese _ 'ro'.
IntlRo = 0x0061,
}
#[cfg(any(feature = "wayland", feature = "x11"))]
impl LinuxScanCodes {
const LETTERS: &'static [LinuxScanCodes] = &[
LinuxScanCodes::A,
LinuxScanCodes::B,
LinuxScanCodes::C,
LinuxScanCodes::D,
LinuxScanCodes::E,
LinuxScanCodes::F,
LinuxScanCodes::G,
LinuxScanCodes::H,
LinuxScanCodes::I,
LinuxScanCodes::J,
LinuxScanCodes::K,
LinuxScanCodes::L,
LinuxScanCodes::M,
LinuxScanCodes::N,
LinuxScanCodes::O,
LinuxScanCodes::P,
LinuxScanCodes::Q,
LinuxScanCodes::R,
LinuxScanCodes::S,
LinuxScanCodes::T,
LinuxScanCodes::U,
LinuxScanCodes::V,
LinuxScanCodes::W,
LinuxScanCodes::X,
LinuxScanCodes::Y,
LinuxScanCodes::Z,
];
fn to_str(&self) -> &str {
match self {
LinuxScanCodes::A => "a",
LinuxScanCodes::B => "b",
LinuxScanCodes::C => "c",
LinuxScanCodes::D => "d",
LinuxScanCodes::E => "e",
LinuxScanCodes::F => "f",
LinuxScanCodes::G => "g",
LinuxScanCodes::H => "h",
LinuxScanCodes::I => "i",
LinuxScanCodes::J => "j",
LinuxScanCodes::K => "k",
LinuxScanCodes::L => "l",
LinuxScanCodes::M => "m",
LinuxScanCodes::N => "n",
LinuxScanCodes::O => "o",
LinuxScanCodes::P => "p",
LinuxScanCodes::Q => "q",
LinuxScanCodes::R => "r",
LinuxScanCodes::S => "s",
LinuxScanCodes::T => "t",
LinuxScanCodes::U => "u",
LinuxScanCodes::V => "v",
LinuxScanCodes::W => "w",
LinuxScanCodes::X => "x",
LinuxScanCodes::Y => "y",
LinuxScanCodes::Z => "z",
LinuxScanCodes::Digit0 => "0",
LinuxScanCodes::Digit1 => "1",
LinuxScanCodes::Digit2 => "2",
LinuxScanCodes::Digit3 => "3",
LinuxScanCodes::Digit4 => "4",
LinuxScanCodes::Digit5 => "5",
LinuxScanCodes::Digit6 => "6",
LinuxScanCodes::Digit7 => "7",
LinuxScanCodes::Digit8 => "8",
LinuxScanCodes::Digit9 => "9",
LinuxScanCodes::Backquote => "`",
LinuxScanCodes::Minus => "-",
LinuxScanCodes::Equal => "=",
LinuxScanCodes::LeftBracket => "[",
LinuxScanCodes::RightBracket => "]",
LinuxScanCodes::Backslash => "\\",
LinuxScanCodes::Semicolon => ";",
LinuxScanCodes::Quote => "'",
LinuxScanCodes::Comma => ",",
LinuxScanCodes::Period => ".",
LinuxScanCodes::Slash => "/",
LinuxScanCodes::IntlBackslash => "unknown",
LinuxScanCodes::IntlRo => "unknown",
}
}
#[cfg(test)]
fn to_shifted(&self) -> &str {
match self {
LinuxScanCodes::A => "a",
LinuxScanCodes::B => "b",
LinuxScanCodes::C => "c",
LinuxScanCodes::D => "d",
LinuxScanCodes::E => "e",
LinuxScanCodes::F => "f",
LinuxScanCodes::G => "g",
LinuxScanCodes::H => "h",
LinuxScanCodes::I => "i",
LinuxScanCodes::J => "j",
LinuxScanCodes::K => "k",
LinuxScanCodes::L => "l",
LinuxScanCodes::M => "m",
LinuxScanCodes::N => "n",
LinuxScanCodes::O => "o",
LinuxScanCodes::P => "p",
LinuxScanCodes::Q => "q",
LinuxScanCodes::R => "r",
LinuxScanCodes::S => "s",
LinuxScanCodes::T => "t",
LinuxScanCodes::U => "u",
LinuxScanCodes::V => "v",
LinuxScanCodes::W => "w",
LinuxScanCodes::X => "x",
LinuxScanCodes::Y => "y",
LinuxScanCodes::Z => "z",
LinuxScanCodes::Digit0 => ")",
LinuxScanCodes::Digit1 => "!",
LinuxScanCodes::Digit2 => "@",
LinuxScanCodes::Digit3 => "#",
LinuxScanCodes::Digit4 => "$",
LinuxScanCodes::Digit5 => "%",
LinuxScanCodes::Digit6 => "^",
LinuxScanCodes::Digit7 => "&",
LinuxScanCodes::Digit8 => "*",
LinuxScanCodes::Digit9 => "(",
LinuxScanCodes::Backquote => "~",
LinuxScanCodes::Minus => "_",
LinuxScanCodes::Equal => "+",
LinuxScanCodes::LeftBracket => "{",
LinuxScanCodes::RightBracket => "}",
LinuxScanCodes::Backslash => "|",
LinuxScanCodes::Semicolon => ":",
LinuxScanCodes::Quote => "\"",
LinuxScanCodes::Comma => "<",
LinuxScanCodes::Period => ">",
LinuxScanCodes::Slash => "?",
LinuxScanCodes::IntlBackslash => "unknown",
LinuxScanCodes::IntlRo => "unknown",
}
}
}
#[cfg(all(test, any(feature = "wayland", feature = "x11")))]
mod tests {
use std::sync::LazyLock;
use strum::IntoEnumIterator;
use x11rb::{protocol::xkb::ConnectionExt, xcb_ffi::XCBConnection};
use xkbcommon::xkb::{
CONTEXT_NO_FLAGS, KEYMAP_COMPILE_NO_FLAGS, Keymap,
x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION},
};
use crate::platform::linux::keyboard::LinuxScanCodes;
use super::LinuxKeyboardMapper;
fn get_keymap() -> Keymap {
static XCB_CONNECTION: LazyLock<XCBConnection> =
LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
let _ = XCB_CONNECTION
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
.unwrap()
.reply()
.unwrap();
let xkb_context = xkbcommon::xkb::Context::new(CONTEXT_NO_FLAGS);
let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
xkbcommon::xkb::x11::keymap_new_from_device(
&xkb_context,
&*XCB_CONNECTION,
xkb_device_id,
KEYMAP_COMPILE_NO_FLAGS,
)
}
#[test]
fn test_us_layout_mapper() {
let keymap = get_keymap();
let mapper = LinuxKeyboardMapper::new(&keymap, 0, 0, 0);
for scan_code in super::LinuxScanCodes::iter() {
if scan_code == LinuxScanCodes::IntlBackslash || scan_code == LinuxScanCodes::IntlRo {
continue;
}
let keycode = xkbcommon::xkb::Keycode::new(scan_code as u32);
let key = mapper
.get_key(keycode, &mut crate::Modifiers::default())
.unwrap();
assert_eq!(key.as_str(), scan_code.to_str());
let shifted_key = mapper
.get_key(
keycode,
&mut crate::Modifiers {
shift: true,
..Default::default()
},
)
.unwrap();
assert_eq!(shifted_key.as_str(), scan_code.to_shifted());
}
}
}

View file

@ -29,6 +29,9 @@ use crate::{
Point, Result, Task, WindowAppearance, WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
use super::LinuxKeyboardMapper;
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) const SCROLL_LINES: f32 = 3.0;
@ -710,6 +713,7 @@ pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
impl crate::Keystroke {
pub(super) fn from_xkb(
state: &State,
keyboard_mapper: &LinuxKeyboardMapper,
mut modifiers: crate::Modifiers,
keycode: Keycode,
) -> Self {
@ -718,76 +722,67 @@ impl crate::Keystroke {
let key_sym = state.key_get_one_sym(keycode);
let key = match key_sym {
Keysym::space => "space".to_owned(),
Keysym::BackSpace => "backspace".to_owned(),
Keysym::Return => "enter".to_owned(),
Keysym::Prior => "pageup".to_owned(),
Keysym::Next => "pagedown".to_owned(),
// Keysym::Tab => "tab".to_owned(),
Keysym::ISO_Left_Tab => "tab".to_owned(),
Keysym::KP_Prior => "pageup".to_owned(),
Keysym::KP_Next => "pagedown".to_owned(),
Keysym::uparrow => "up".to_owned(),
Keysym::downarrow => "down".to_owned(),
Keysym::leftarrow => "left".to_owned(),
Keysym::rightarrow => "right".to_owned(),
Keysym::Home | Keysym::KP_Home => "home".to_owned(),
Keysym::End | Keysym::KP_End => "end".to_owned(),
Keysym::Prior | Keysym::KP_Prior => "pageup".to_owned(),
Keysym::Next | Keysym::KP_Next => "pagedown".to_owned(),
Keysym::XF86_Back => "back".to_owned(),
Keysym::XF86_Forward => "forward".to_owned(),
Keysym::Escape => "escape".to_owned(),
Keysym::Insert | Keysym::KP_Insert => "insert".to_owned(),
Keysym::Delete | Keysym::KP_Delete => "delete".to_owned(),
Keysym::Menu => "menu".to_owned(),
Keysym::XF86_Cut => "cut".to_owned(),
Keysym::XF86_Copy => "copy".to_owned(),
Keysym::XF86_Paste => "paste".to_owned(),
Keysym::XF86_New => "new".to_owned(),
Keysym::XF86_Open => "open".to_owned(),
Keysym::XF86_Save => "save".to_owned(),
Keysym::comma => ",".to_owned(),
Keysym::period => ".".to_owned(),
Keysym::less => "<".to_owned(),
Keysym::greater => ">".to_owned(),
Keysym::slash => "/".to_owned(),
Keysym::question => "?".to_owned(),
Keysym::semicolon => ";".to_owned(),
Keysym::colon => ":".to_owned(),
Keysym::apostrophe => "'".to_owned(),
Keysym::quotedbl => "\"".to_owned(),
Keysym::bracketleft => "[".to_owned(),
Keysym::braceleft => "{".to_owned(),
Keysym::bracketright => "]".to_owned(),
Keysym::braceright => "}".to_owned(),
Keysym::backslash => "\\".to_owned(),
Keysym::bar => "|".to_owned(),
Keysym::grave => "`".to_owned(),
Keysym::asciitilde => "~".to_owned(),
Keysym::exclam => "!".to_owned(),
Keysym::at => "@".to_owned(),
Keysym::numbersign => "#".to_owned(),
Keysym::dollar => "$".to_owned(),
Keysym::percent => "%".to_owned(),
Keysym::asciicircum => "^".to_owned(),
Keysym::ampersand => "&".to_owned(),
Keysym::asterisk => "*".to_owned(),
Keysym::parenleft => "(".to_owned(),
Keysym::parenright => ")".to_owned(),
Keysym::minus => "-".to_owned(),
Keysym::underscore => "_".to_owned(),
Keysym::equal => "=".to_owned(),
Keysym::plus => "+".to_owned(),
_ => {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {
name.replace("kp_", "")
} else {
name
}
}
Keysym::F1 => "f1".to_owned(),
Keysym::F2 => "f2".to_owned(),
Keysym::F3 => "f3".to_owned(),
Keysym::F4 => "f4".to_owned(),
Keysym::F5 => "f5".to_owned(),
Keysym::F6 => "f6".to_owned(),
Keysym::F7 => "f7".to_owned(),
Keysym::F8 => "f8".to_owned(),
Keysym::F9 => "f9".to_owned(),
Keysym::F10 => "f10".to_owned(),
Keysym::F11 => "f11".to_owned(),
Keysym::F12 => "f12".to_owned(),
Keysym::F13 => "f13".to_owned(),
Keysym::F14 => "f14".to_owned(),
Keysym::F15 => "f15".to_owned(),
Keysym::F16 => "f16".to_owned(),
Keysym::F17 => "f17".to_owned(),
Keysym::F18 => "f18".to_owned(),
Keysym::F19 => "f19".to_owned(),
Keysym::F20 => "f20".to_owned(),
Keysym::F21 => "f21".to_owned(),
Keysym::F22 => "f22".to_owned(),
Keysym::F23 => "f23".to_owned(),
Keysym::F24 => "f24".to_owned(),
_ => keyboard_mapper
.get_key(keycode, &mut modifiers)
.unwrap_or_else(|| {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {
name.replace("kp_", "")
} else {
name
}
}),
};
if modifiers.shift {
// we only include the shift for upper-case letters by convention,
// so don't include for numbers and symbols, but do include for
// tab/enter, etc.
if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
modifiers.shift = false;
}
}
// Ignore control characters (and DEL) for the purposes of key_char
let key_char =
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
@ -798,65 +793,6 @@ impl crate::Keystroke {
key_char,
}
}
/**
* Returns which symbol the dead key represents
* <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
*/
pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
match keysym {
Keysym::dead_grave => Some("`".to_owned()),
Keysym::dead_acute => Some("´".to_owned()),
Keysym::dead_circumflex => Some("^".to_owned()),
Keysym::dead_tilde => Some("~".to_owned()),
Keysym::dead_macron => Some("¯".to_owned()),
Keysym::dead_breve => Some("˘".to_owned()),
Keysym::dead_abovedot => Some("˙".to_owned()),
Keysym::dead_diaeresis => Some("¨".to_owned()),
Keysym::dead_abovering => Some("˚".to_owned()),
Keysym::dead_doubleacute => Some("˝".to_owned()),
Keysym::dead_caron => Some("ˇ".to_owned()),
Keysym::dead_cedilla => Some("¸".to_owned()),
Keysym::dead_ogonek => Some("˛".to_owned()),
Keysym::dead_iota => Some("ͅ".to_owned()),
Keysym::dead_voiced_sound => Some("".to_owned()),
Keysym::dead_semivoiced_sound => Some("".to_owned()),
Keysym::dead_belowdot => Some("̣̣".to_owned()),
Keysym::dead_hook => Some("̡".to_owned()),
Keysym::dead_horn => Some("̛".to_owned()),
Keysym::dead_stroke => Some("̶̶".to_owned()),
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
Keysym::dead_doublegrave => Some("̏".to_owned()),
Keysym::dead_belowring => Some("˳".to_owned()),
Keysym::dead_belowmacron => Some("̱".to_owned()),
Keysym::dead_belowcircumflex => Some("".to_owned()),
Keysym::dead_belowtilde => Some("̰".to_owned()),
Keysym::dead_belowbreve => Some("̮".to_owned()),
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
Keysym::dead_invertedbreve => Some("̯".to_owned()),
Keysym::dead_belowcomma => Some("̦".to_owned()),
Keysym::dead_currency => None,
Keysym::dead_lowline => None,
Keysym::dead_aboveverticalline => None,
Keysym::dead_belowverticalline => None,
Keysym::dead_longsolidusoverlay => None,
Keysym::dead_a => None,
Keysym::dead_A => None,
Keysym::dead_e => None,
Keysym::dead_E => None,
Keysym::dead_i => None,
Keysym::dead_I => None,
Keysym::dead_o => None,
Keysym::dead_O => None,
Keysym::dead_u => None,
Keysym::dead_U => None,
Keysym::dead_small_schwa => Some("ə".to_owned()),
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
Keysym::dead_greek => None,
_ => None,
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]

View file

@ -61,22 +61,20 @@ use wayland_protocols::xdg::decoration::zv1::client::{
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, ffi::XKB_KEYMAP_FORMAT_TEXT_V1};
use super::{
display::WaylandDisplay,
window::{ImeInput, WaylandWindowStatePtr},
};
use crate::platform::{PlatformWindow, blade::BladeContext};
use crate::{
AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta,
ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
LinuxKeyboardLayout, LinuxKeyboardMapper, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels,
ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
};
use crate::{
SharedString,
@ -92,6 +90,10 @@ use crate::{
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
},
};
use crate::{
platform::{PlatformWindow, blade::BladeContext},
underlying_dead_key,
};
/// Used to convert evdev scancode to xkb scancode
const MIN_KEYCODE: u32 = 8;
@ -208,9 +210,11 @@ pub(crate) struct WaylandClientState {
// Output to scale mapping
outputs: HashMap<ObjectId, Output>,
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
keyboard_layout: LinuxKeyboardLayout,
keymap_state: Option<xkb::State>,
compose_state: Option<xkb::compose::State>,
keyboard_layout: LinuxKeyboardLayout,
keyboard_mapper: Option<Rc<LinuxKeyboardMapper>>,
keyboard_mapper_cache: HashMap<String, Rc<LinuxKeyboardMapper>>,
drag: DragState,
click: ClickState,
repeat: KeyRepeat,
@ -340,7 +344,7 @@ impl WaylandClientStatePtr {
text_input.commit();
}
pub fn handle_keyboard_layout_change(&self) {
pub fn handle_keyboard_layout_change(&self, locked_group: u32) {
let client = self.get_client();
let mut state = client.borrow_mut();
let changed = if let Some(keymap_state) = &state.keymap_state {
@ -350,6 +354,17 @@ impl WaylandClientStatePtr {
let changed = layout_name != state.keyboard_layout.name();
if changed {
state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
let mapper = state
.keyboard_mapper_cache
.entry(layout_name.to_string())
.or_insert(Rc::new(LinuxKeyboardMapper::new(
&keymap,
0,
0,
locked_group,
)))
.clone();
state.keyboard_mapper = Some(mapper);
}
changed
} else {
@ -447,6 +462,7 @@ impl WaylandClient {
pub(crate) fn new() -> Self {
let conn = Connection::connect_to_env().unwrap();
let keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME);
let (globals, mut event_queue) =
registry_queue_init::<WaylandClientStatePtr>(&conn).unwrap();
let qh = event_queue.handle();
@ -567,9 +583,11 @@ impl WaylandClient {
in_progress_outputs,
windows: HashMap::default(),
common,
keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME),
keymap_state: None,
compose_state: None,
keyboard_layout,
keyboard_mapper: None,
keyboard_mapper_cache: HashMap::default(),
drag: DragState {
data_offer: None,
window: None,
@ -1214,7 +1232,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
state.compose_state = get_xkb_compose_state(&xkb_context);
drop(state);
this.handle_keyboard_layout_change();
this.handle_keyboard_layout_change(0);
}
wl_keyboard::Event::Enter { surface, .. } => {
state.keyboard_focused_window = get_window(&mut state, &surface.id());
@ -1270,7 +1288,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
}
if group != old_layout {
this.handle_keyboard_layout_change();
this.handle_keyboard_layout_change(group);
}
}
wl_keyboard::Event::Key {
@ -1288,20 +1306,25 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
let focused_window = focused_window.clone();
let keymap_state = state.keymap_state.as_ref().unwrap();
let keycode = Keycode::from(key + MIN_KEYCODE);
let keyboard_mapper = state.keyboard_mapper.as_ref().unwrap();
let keycode = xkb::Keycode::from(key + MIN_KEYCODE);
let keysym = keymap_state.key_get_one_sym(keycode);
match key_state {
wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
let mut keystroke =
Keystroke::from_xkb(&keymap_state, state.modifiers, keycode);
let mut keystroke = Keystroke::from_xkb(
keymap_state,
keyboard_mapper,
state.modifiers,
keycode,
);
if let Some(mut compose) = state.compose_state.take() {
compose.feed(keysym);
match compose.status() {
xkb::Status::Composing => {
keystroke.key_char = None;
state.pre_edit_text =
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
compose.utf8().or(underlying_dead_key(keysym));
let pre_edit =
state.pre_edit_text.clone().unwrap_or(String::default());
drop(state);
@ -1318,7 +1341,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
}
xkb::Status::Cancelled => {
let pre_edit = state.pre_edit_text.take();
let new_pre_edit = Keystroke::underlying_dead_key(keysym);
let new_pre_edit = underlying_dead_key(keysym);
state.pre_edit_text = new_pre_edit.clone();
drop(state);
if let Some(pre_edit) = pre_edit {
@ -1379,7 +1402,12 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
}
wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => {
let input = PlatformInput::KeyUp(KeyUpEvent {
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
keystroke: Keystroke::from_xkb(
keymap_state,
keyboard_mapper,
state.modifiers,
keycode,
),
});
if state.repeat.current_keycode == Some(keycode) {

View file

@ -1,4 +1,3 @@
use crate::{Capslock, xcb_flush};
use core::str;
use std::{
cell::RefCell,
@ -49,7 +48,7 @@ use super::{
};
use crate::platform::{
LinuxCommon, PlatformWindow,
Capslock, LinuxCommon, PlatformWindow,
blade::BladeContext,
linux::{
DEFAULT_CURSOR_ICON_NAME, LinuxClient, get_xkb_compose_state, is_within_click_distance,
@ -58,13 +57,14 @@ use crate::platform::{
reveal_path_internal,
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
},
xcb_flush,
};
use crate::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform,
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions,
ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
modifiers_from_xinput_info, point, px,
LinuxKeyboardLayout, LinuxKeyboardMapper, Modifiers, ModifiersChangedEvent, MouseButton,
Pixels, Platform, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point,
RequestFrameOptions, ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
modifiers_from_xinput_info, point, px, underlying_dead_key,
};
/// Value for DeviceId parameters which selects all devices.
@ -200,6 +200,8 @@ pub struct X11ClientState {
pub(crate) xkb: xkbc::State,
previous_xkb_state: XKBStateNotiy,
keyboard_layout: LinuxKeyboardLayout,
keyboard_mapper: Rc<LinuxKeyboardMapper>,
keyboard_mapper_cache: HashMap<String, Rc<LinuxKeyboardMapper>>,
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
@ -403,22 +405,24 @@ impl X11Client {
let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS);
let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection);
let xkb_state = {
let xkb_keymap = xkbc::x11::keymap_new_from_device(
&xkb_context,
&xcb_connection,
xkb_device_id,
xkbc::KEYMAP_COMPILE_NO_FLAGS,
);
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
};
let xkb_keymap = xkbc::x11::keymap_new_from_device(
&xkb_context,
&xcb_connection,
xkb_device_id,
xkbc::KEYMAP_COMPILE_NO_FLAGS,
);
let xkb_state =
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id);
let compose_state = get_xkb_compose_state(&xkb_context);
let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE);
let layout_name = xkb_state
.get_keymap()
.layout_get_name(layout_idx)
.to_string();
let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into());
let keyboard_layout = LinuxKeyboardLayout::new(layout_name.clone().into());
let keyboard_mapper = Rc::new(LinuxKeyboardMapper::new(&xkb_keymap, 0, 0, 0));
let mut keyboard_mapper_cache = HashMap::default();
keyboard_mapper_cache.insert(layout_name, keyboard_mapper.clone());
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
@ -512,6 +516,8 @@ impl X11Client {
xkb: xkb_state,
previous_xkb_state: XKBStateNotiy::default(),
keyboard_layout,
keyboard_mapper,
keyboard_mapper_cache,
ximc,
xim_handler,
@ -972,24 +978,27 @@ impl X11Client {
};
state.xkb = xkb_state;
drop(state);
self.handle_keyboard_layout_change();
self.handle_keyboard_layout_change(depressed_layout, latched_layout, locked_layout);
}
Event::XkbStateNotify(event) => {
let mut state = self.0.borrow_mut();
let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
let new_layout = u32::from(event.group);
let base_group = event.base_group as u32;
let latched_group = event.latched_group as u32;
let locked_group = event.locked_group.into();
state.xkb.update_mask(
event.base_mods.into(),
event.latched_mods.into(),
event.locked_mods.into(),
event.base_group as u32,
event.latched_group as u32,
event.locked_group.into(),
base_group,
latched_group,
locked_group,
);
state.previous_xkb_state = XKBStateNotiy {
depressed_layout: event.base_group as u32,
latched_layout: event.latched_group as u32,
locked_layout: event.locked_group.into(),
depressed_layout: base_group,
latched_layout: latched_group,
locked_layout: locked_group,
};
let modifiers = Modifiers::from_xkb(&state.xkb);
@ -1016,7 +1025,7 @@ impl X11Client {
}
if new_layout != old_layout {
self.handle_keyboard_layout_change();
self.handle_keyboard_layout_change(base_group, latched_group, locked_group);
}
}
Event::KeyPress(event) => {
@ -1037,7 +1046,12 @@ impl X11Client {
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let mut keystroke = crate::Keystroke::from_xkb(
&state.xkb,
&state.keyboard_mapper,
modifiers,
code,
);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@ -1054,9 +1068,8 @@ impl X11Client {
}
xkbc::Status::Composing => {
keystroke.key_char = None;
state.pre_edit_text = compose_state
.utf8()
.or(crate::Keystroke::underlying_dead_key(keysym));
state.pre_edit_text =
compose_state.utf8().or(underlying_dead_key(keysym));
let pre_edit =
state.pre_edit_text.clone().unwrap_or(String::default());
drop(state);
@ -1069,7 +1082,7 @@ impl X11Client {
if let Some(pre_edit) = pre_edit {
window.handle_ime_commit(pre_edit);
}
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
if let Some(current_key) = underlying_dead_key(keysym) {
window.handle_ime_preedit(current_key);
}
state = self.0.borrow_mut();
@ -1105,7 +1118,12 @@ impl X11Client {
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let keystroke = crate::Keystroke::from_xkb(
&state.xkb,
&state.keyboard_mapper,
modifiers,
code,
);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@ -1327,6 +1345,7 @@ impl X11Client {
let mut state = self.0.borrow_mut();
state.pre_key_char_down = Some(Keystroke::from_xkb(
&state.xkb,
&state.keyboard_mapper,
state.modifiers,
event.detail.into(),
));
@ -1412,13 +1431,29 @@ impl X11Client {
Some(())
}
fn handle_keyboard_layout_change(&self) {
fn handle_keyboard_layout_change(
&self,
base_group: u32,
latched_group: u32,
locked_group: u32,
) {
let mut state = self.0.borrow_mut();
let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
let keymap = state.xkb.get_keymap();
let layout_name = keymap.layout_get_name(layout_idx);
if layout_name != state.keyboard_layout.name() {
state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
let mapper = state
.keyboard_mapper_cache
.entry(layout_name.to_string())
.or_insert(Rc::new(LinuxKeyboardMapper::new(
&keymap,
base_group,
latched_group,
locked_group,
)))
.clone();
state.keyboard_mapper = mapper;
if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
drop(state);
callback();