git.lucas.co / cce-keyboard
on-screen keyboard
git clone https://git.lucas.co/cce-keyboard.git

src/keymap.rs (3.4K)

 1 //! The keymap the virtual keyboard uploads, and the key labels read from it.
 2 //!
 3 //! Compiled from the same RMLVO the compositor's default keymap is: empty
 4 //! names, which libxkbcommon fills from `XKB_DEFAULT_*` and then the system
 5 //! defaults (`cce-compositor` `xkb_config.rs` passes NULL names). So the board
 6 //! labels and types whatever layout the session's hardware keyboard has.
 7 
 8 use std::collections::HashMap;
 9 
10 use xkbcommon::xkb;
11 
12 use crate::layout::Modifier;
13 
14 /// What a character key prints, plain and shifted. `None` for a key that
15 /// prints nothing printable (a control character, or nothing at all).
16 #[derive(Debug, Clone, Default, PartialEq)]
17 pub struct Label {
18     pub plain: Option<String>,
19     pub shifted: Option<String>,
20 }
21 
22 pub struct Keymap {
23     /// The keymap in XKB text format, for `zwp_virtual_keyboard_v1.keymap`.
24     pub text: String,
25     /// Each [`Modifier`]'s mask, indexed by [`Modifier::index`].
26     masks: [u32; 4],
27     labels: HashMap<u32, Label>,
28 }
29 
30 impl Keymap {
31     /// Compile the session's default keymap and label `codes` (evdev) from it.
32     pub fn compile(codes: &[u32]) -> Option<Self> {
33         let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
34         let keymap = xkb::Keymap::new_from_names(&context, "", "", "", "", None, xkb::KEYMAP_COMPILE_NO_FLAGS)?;
35         let mut masks = [0; 4];
36         for m in Modifier::ALL {
37             let index = keymap.mod_get_index(m.xkb_name());
38             if index != xkb::MOD_INVALID {
39                 masks[m.index()] = 1 << index;
40             }
41         }
42         let shift = masks[Modifier::Shift.index()];
43         let plain = xkb::State::new(&keymap);
44         let mut shifted = xkb::State::new(&keymap);
45         shifted.update_mask(shift, 0, 0, 0, 0, 0);
46         let printable = |s: String| (!s.is_empty() && !s.chars().any(char::is_control)).then_some(s);
47         let labels = codes
48             .iter()
49             .map(|&code| {
50                 // xkb keycodes are evdev codes offset by 8.
51                 let kc = xkb::Keycode::new(code + 8);
52                 (code, Label { plain: printable(plain.key_get_utf8(kc)), shifted: printable(shifted.key_get_utf8(kc)) })
53             })
54             .collect();
55         Some(Self { text: keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1), masks, labels })
56     }
57 
58     pub fn label(&self, code: u32) -> Option<&Label> {
59         self.labels.get(&code)
60     }
61 
62     /// The depressed-modifier mask for `mods` held.
63     pub fn mask(&self, mods: impl IntoIterator<Item = Modifier>) -> u32 {
64         mods.into_iter().fold(0, |acc, m| acc | self.masks[m.index()])
65     }
66 }
67 
68 impl Label {
69     /// The small legend in a key's corner: the shifted symbol, when it is
70     /// not simply the capital of the plain one (a digit's `!`, not Q's `Q`).
71     pub fn corner(&self) -> Option<&str> {
72         let (plain, shifted) = (self.plain.as_deref()?, self.shifted.as_deref()?);
73         (shifted != plain && shifted != plain.to_uppercase()).then_some(shifted)
74     }
75 }
76 
77 #[cfg(test)]
78 mod tests {
79     use super::*;
80 
81     #[test]
82     fn a_letter_has_no_corner_legend_and_a_digit_does() {
83         let q = Label { plain: Some("q".into()), shifted: Some("Q".into()) };
84         assert_eq!(q.corner(), None);
85         let one = Label { plain: Some("1".into()), shifted: Some("!".into()) };
86         assert_eq!(one.corner(), Some("!"));
87         let dead = Label { plain: Some("1".into()), shifted: None };
88         assert_eq!(dead.corner(), None);
89     }
90 }