keyboard.rs

  1#[cfg(any(feature = "wayland", feature = "x11"))]
  2use collections::{HashMap, HashSet};
  3#[cfg(any(feature = "wayland", feature = "x11"))]
  4use strum::{EnumIter, IntoEnumIterator as _};
  5#[cfg(any(feature = "wayland", feature = "x11"))]
  6use xkbcommon::xkb::{Keycode, Keysym};
  7
  8use crate::{PlatformKeyboardLayout, SharedString};
  9
 10#[derive(Clone)]
 11pub(crate) struct LinuxKeyboardLayout {
 12    name: SharedString,
 13}
 14
 15impl PlatformKeyboardLayout for LinuxKeyboardLayout {
 16    fn id(&self) -> &str {
 17        &self.name
 18    }
 19
 20    fn name(&self) -> &str {
 21        &self.name
 22    }
 23}
 24
 25impl LinuxKeyboardLayout {
 26    pub(crate) fn new(name: SharedString) -> Self {
 27        Self { name }
 28    }
 29}
 30
 31#[cfg(any(feature = "wayland", feature = "x11"))]
 32pub(crate) struct LinuxKeyboardMapper {
 33    code_to_key: HashMap<Keycode, String>,
 34    code_to_shifted_key: HashMap<Keycode, String>,
 35}
 36
 37#[cfg(any(feature = "wayland", feature = "x11"))]
 38impl LinuxKeyboardMapper {
 39    pub(crate) fn new(xkb_state: &xkbcommon::xkb::State) -> Self {
 40        let mut code_to_key = HashMap::default();
 41        let mut code_to_shifted_key = HashMap::default();
 42        let mut inserted = HashSet::default();
 43
 44        let keymap = xkb_state.get_keymap();
 45        let mut shifted_state = xkbcommon::xkb::State::new(&keymap);
 46        let shift_mod = keymap.mod_get_index(xkbcommon::xkb::MOD_NAME_SHIFT);
 47        let shift_mask = 1 << shift_mod;
 48        shifted_state.update_mask(shift_mask, 0, 0, 0, 0, 0);
 49
 50        for scan_code in LinuxScanCodes::iter() {
 51            let keycode = Keycode::new(scan_code as u32);
 52
 53            let key = xkb_state.key_get_utf8(keycode);
 54            if !key.is_empty() {
 55                code_to_key.insert(keycode, key.clone());
 56                inserted.insert(key);
 57            } else {
 58                // keycode might be a dead key
 59                let keysym = xkb_state.key_get_one_sym(keycode);
 60                if let Some(key) = underlying_dead_key(keysym) {
 61                    code_to_key.insert(keycode, key.clone());
 62                    inserted.insert(key);
 63                }
 64            }
 65
 66            let shifted_key = shifted_state.key_get_utf8(keycode);
 67            if !shifted_key.is_empty() {
 68                code_to_shifted_key.insert(keycode, shifted_key);
 69            } else {
 70                // keycode might be a dead key
 71                let shifted_keysym = shifted_state.key_get_one_sym(keycode);
 72                if let Some(shifted_key) = underlying_dead_key(shifted_keysym) {
 73                    code_to_shifted_key.insert(keycode, shifted_key);
 74                }
 75            }
 76        }
 77        insert_letters_if_missing(&inserted, &mut code_to_key);
 78
 79        Self {
 80            code_to_key,
 81            code_to_shifted_key,
 82        }
 83    }
 84
 85    pub(crate) fn get_key(
 86        &self,
 87        keycode: Keycode,
 88        modifiers: &mut crate::Modifiers,
 89    ) -> Option<String> {
 90        if is_alphabetic_key(keycode) || !modifiers.shift {
 91            self.code_to_key.get(&keycode).cloned()
 92        } else {
 93            modifiers.shift = false;
 94            self.code_to_shifted_key.get(&keycode).cloned()
 95        }
 96    }
 97}
 98
 99#[cfg(any(feature = "wayland", feature = "x11"))]
100fn is_alphabetic_key(keycode: Keycode) -> bool {
101    matches!(
102        keycode.raw(),
103        0x0026 // a
104        | 0x0038 // b
105        | 0x0036 // c
106        | 0x0028 // d
107        | 0x001a // e
108        | 0x0029 // f
109        | 0x002a // g
110        | 0x002b // h
111        | 0x001f // i
112        | 0x002c // j
113        | 0x002d // k
114        | 0x002e // l
115        | 0x003a // m
116        | 0x0039 // n
117        | 0x0020 // o
118        | 0x0021 // p
119        | 0x0018 // q
120        | 0x001b // r
121        | 0x0027 // s
122        | 0x001c // t
123        | 0x001e // u
124        | 0x0037 // v
125        | 0x0019 // w
126        | 0x0035 // x
127        | 0x001d // y
128        | 0x0034 // z
129    )
130}
131
132/**
133 * Returns which symbol the dead key represents
134 * <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
135 */
136#[cfg(any(feature = "wayland", feature = "x11"))]
137pub(crate) fn underlying_dead_key(keysym: Keysym) -> Option<String> {
138    match keysym {
139        Keysym::dead_grave => Some("`".to_owned()),
140        Keysym::dead_acute => Some("´".to_owned()),
141        Keysym::dead_circumflex => Some("^".to_owned()),
142        Keysym::dead_tilde => Some("~".to_owned()),
143        Keysym::dead_macron => Some("¯".to_owned()),
144        Keysym::dead_breve => Some("˘".to_owned()),
145        Keysym::dead_abovedot => Some("˙".to_owned()),
146        Keysym::dead_diaeresis => Some("¨".to_owned()),
147        Keysym::dead_abovering => Some("˚".to_owned()),
148        Keysym::dead_doubleacute => Some("˝".to_owned()),
149        Keysym::dead_caron => Some("ˇ".to_owned()),
150        Keysym::dead_cedilla => Some("¸".to_owned()),
151        Keysym::dead_ogonek => Some("˛".to_owned()),
152        Keysym::dead_iota => Some("ͅ".to_owned()),
153        Keysym::dead_voiced_sound => Some("".to_owned()),
154        Keysym::dead_semivoiced_sound => Some("".to_owned()),
155        Keysym::dead_belowdot => Some("̣̣".to_owned()),
156        Keysym::dead_hook => Some("̡".to_owned()),
157        Keysym::dead_horn => Some("̛".to_owned()),
158        Keysym::dead_stroke => Some("̶̶".to_owned()),
159        Keysym::dead_abovecomma => Some("̓̓".to_owned()),
160        Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
161        Keysym::dead_doublegrave => Some("̏".to_owned()),
162        Keysym::dead_belowring => Some("˳".to_owned()),
163        Keysym::dead_belowmacron => Some("̱".to_owned()),
164        Keysym::dead_belowcircumflex => Some("".to_owned()),
165        Keysym::dead_belowtilde => Some("̰".to_owned()),
166        Keysym::dead_belowbreve => Some("̮".to_owned()),
167        Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
168        Keysym::dead_invertedbreve => Some("̯".to_owned()),
169        Keysym::dead_belowcomma => Some("̦".to_owned()),
170        Keysym::dead_currency => None,
171        Keysym::dead_lowline => None,
172        Keysym::dead_aboveverticalline => None,
173        Keysym::dead_belowverticalline => None,
174        Keysym::dead_longsolidusoverlay => None,
175        Keysym::dead_a => None,
176        Keysym::dead_A => None,
177        Keysym::dead_e => None,
178        Keysym::dead_E => None,
179        Keysym::dead_i => None,
180        Keysym::dead_I => None,
181        Keysym::dead_o => None,
182        Keysym::dead_O => None,
183        Keysym::dead_u => None,
184        Keysym::dead_U => None,
185        Keysym::dead_small_schwa => Some("ə".to_owned()),
186        Keysym::dead_capital_schwa => Some("Ə".to_owned()),
187        Keysym::dead_greek => None,
188        _ => None,
189    }
190}
191
192#[cfg(any(feature = "wayland", feature = "x11"))]
193fn insert_letters_if_missing(
194    inserted: &HashSet<String>,
195    code_to_key: &mut HashMap<Keycode, String>,
196) {
197    for scan_code in LinuxScanCodes::LETTERS.iter() {
198        let keycode = Keycode::new(*scan_code as u32);
199        let key = scan_code.to_str();
200        if !inserted.contains(key) {
201            code_to_key.insert(keycode, key.to_string());
202        }
203    }
204}
205
206#[cfg(any(feature = "wayland", feature = "x11"))]
207#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
208enum LinuxScanCodes {
209    A = 0x0026,
210    B = 0x0038,
211    C = 0x0036,
212    D = 0x0028,
213    E = 0x001a,
214    F = 0x0029,
215    G = 0x002a,
216    H = 0x002b,
217    I = 0x001f,
218    J = 0x002c,
219    K = 0x002d,
220    L = 0x002e,
221    M = 0x003a,
222    N = 0x0039,
223    O = 0x0020,
224    P = 0x0021,
225    Q = 0x0018,
226    R = 0x001b,
227    S = 0x0027,
228    T = 0x001c,
229    U = 0x001e,
230    V = 0x0037,
231    W = 0x0019,
232    X = 0x0035,
233    Y = 0x001d,
234    Z = 0x0034,
235    Digit0 = 0x0013,
236    Digit1 = 0x000a,
237    Digit2 = 0x000b,
238    Digit3 = 0x000c,
239    Digit4 = 0x000d,
240    Digit5 = 0x000e,
241    Digit6 = 0x000f,
242    Digit7 = 0x0010,
243    Digit8 = 0x0011,
244    Digit9 = 0x0012,
245    Backquote = 0x0031,
246    Minus = 0x0014,
247    Equal = 0x0015,
248    LeftBracket = 0x0022,
249    RightBracket = 0x0023,
250    Backslash = 0x0033,
251    Semicolon = 0x002f,
252    Quote = 0x0030,
253    Comma = 0x003b,
254    Period = 0x003c,
255    Slash = 0x003d,
256    // This key is typically located near LeftShift key, varies on international keyboards: Dan: <> Dutch: ][ Ger: <> UK: \|
257    IntlBackslash = 0x005e,
258    // Used for Brazilian /? and Japanese _ 'ro'.
259    IntlRo = 0x0061,
260}
261
262#[cfg(any(feature = "wayland", feature = "x11"))]
263impl LinuxScanCodes {
264    const LETTERS: &'static [LinuxScanCodes] = &[
265        LinuxScanCodes::A,
266        LinuxScanCodes::B,
267        LinuxScanCodes::C,
268        LinuxScanCodes::D,
269        LinuxScanCodes::E,
270        LinuxScanCodes::F,
271        LinuxScanCodes::G,
272        LinuxScanCodes::H,
273        LinuxScanCodes::I,
274        LinuxScanCodes::J,
275        LinuxScanCodes::K,
276        LinuxScanCodes::L,
277        LinuxScanCodes::M,
278        LinuxScanCodes::N,
279        LinuxScanCodes::O,
280        LinuxScanCodes::P,
281        LinuxScanCodes::Q,
282        LinuxScanCodes::R,
283        LinuxScanCodes::S,
284        LinuxScanCodes::T,
285        LinuxScanCodes::U,
286        LinuxScanCodes::V,
287        LinuxScanCodes::W,
288        LinuxScanCodes::X,
289        LinuxScanCodes::Y,
290        LinuxScanCodes::Z,
291    ];
292
293    fn to_str(&self) -> &str {
294        match self {
295            LinuxScanCodes::A => "a",
296            LinuxScanCodes::B => "b",
297            LinuxScanCodes::C => "c",
298            LinuxScanCodes::D => "d",
299            LinuxScanCodes::E => "e",
300            LinuxScanCodes::F => "f",
301            LinuxScanCodes::G => "g",
302            LinuxScanCodes::H => "h",
303            LinuxScanCodes::I => "i",
304            LinuxScanCodes::J => "j",
305            LinuxScanCodes::K => "k",
306            LinuxScanCodes::L => "l",
307            LinuxScanCodes::M => "m",
308            LinuxScanCodes::N => "n",
309            LinuxScanCodes::O => "o",
310            LinuxScanCodes::P => "p",
311            LinuxScanCodes::Q => "q",
312            LinuxScanCodes::R => "r",
313            LinuxScanCodes::S => "s",
314            LinuxScanCodes::T => "t",
315            LinuxScanCodes::U => "u",
316            LinuxScanCodes::V => "v",
317            LinuxScanCodes::W => "w",
318            LinuxScanCodes::X => "x",
319            LinuxScanCodes::Y => "y",
320            LinuxScanCodes::Z => "z",
321            LinuxScanCodes::Digit0 => "0",
322            LinuxScanCodes::Digit1 => "1",
323            LinuxScanCodes::Digit2 => "2",
324            LinuxScanCodes::Digit3 => "3",
325            LinuxScanCodes::Digit4 => "4",
326            LinuxScanCodes::Digit5 => "5",
327            LinuxScanCodes::Digit6 => "6",
328            LinuxScanCodes::Digit7 => "7",
329            LinuxScanCodes::Digit8 => "8",
330            LinuxScanCodes::Digit9 => "9",
331            LinuxScanCodes::Backquote => "`",
332            LinuxScanCodes::Minus => "-",
333            LinuxScanCodes::Equal => "=",
334            LinuxScanCodes::LeftBracket => "[",
335            LinuxScanCodes::RightBracket => "]",
336            LinuxScanCodes::Backslash => "\\",
337            LinuxScanCodes::Semicolon => ";",
338            LinuxScanCodes::Quote => "'",
339            LinuxScanCodes::Comma => ",",
340            LinuxScanCodes::Period => ".",
341            LinuxScanCodes::Slash => "/",
342            LinuxScanCodes::IntlBackslash => "unknown",
343            LinuxScanCodes::IntlRo => "unknown",
344        }
345    }
346}
347
348#[cfg(all(test, any(feature = "wayland", feature = "x11")))]
349mod tests {
350    use std::sync::LazyLock;
351
352    use strum::IntoEnumIterator;
353    use x11rb::{protocol::xkb::ConnectionExt as _, xcb_ffi::XCBConnection};
354    use xkbcommon::xkb::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
355
356    use crate::platform::linux::keyboard::LinuxScanCodes;
357
358    use super::LinuxKeyboardMapper;
359
360    static XCB_CONNECTION: LazyLock<XCBConnection> =
361        LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
362
363    fn create_test_mapper() -> LinuxKeyboardMapper {
364        let _ = XCB_CONNECTION
365            .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
366            .unwrap()
367            .reply()
368            .unwrap();
369        let xkb_context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
370        let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
371        let xkb_state = {
372            let xkb_keymap = xkbcommon::xkb::x11::keymap_new_from_device(
373                &xkb_context,
374                &*XCB_CONNECTION,
375                xkb_device_id,
376                xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
377            );
378            xkbcommon::xkb::x11::state_new_from_device(&xkb_keymap, &*XCB_CONNECTION, xkb_device_id)
379        };
380        LinuxKeyboardMapper::new(&xkb_state)
381    }
382
383    #[test]
384    fn test_us_layout_mapper() {
385        let mapper = create_test_mapper();
386        for scan_code in super::LinuxScanCodes::iter() {
387            if scan_code == LinuxScanCodes::IntlBackslash || scan_code == LinuxScanCodes::IntlRo {
388                continue;
389            }
390            let keycode = xkbcommon::xkb::Keycode::new(scan_code as u32);
391            let key = mapper.get_key(keycode, &mut crate::Modifiers::default());
392            assert_eq!(key, Some(scan_code.to_str().to_string()));
393        }
394    }
395}