keymaps/
inverse_keymap.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::collections::HashMap;
6
7use crate as keymaps;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum Shift {
11    No,
12    Yes,
13    DontCare,
14}
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct KeyStroke {
18    pub usage: u32,
19    pub shift: Shift,
20}
21
22/// Lightweight utility for basic keymap conversion of chars to keystrokes.
23///
24/// This is intended for end-to-end and input testing only; for production use cases and general
25/// testing, IME injection should be used instead. Generally a mapping exists only for printable
26/// ASCII characters; in particular neither `'\t'` nor `'\n'` is mapped in either of the standard
27/// zircon keymaps. Furthermore, IME implementations may themselves override the keymap in a way
28/// that invalidates this translation.
29///
30/// This is an inverse of [hid/hid.h:hid_map_key].
31///
32/// [hid/hid.h:hid_map_key]: https://fuchsia.googlesource.com/fuchsia/+/ef9c451ba83a3ece22cad66b9dcfb446be291966/src/ui/input/lib/hid/include/hid/hid.h#30
33#[derive(Debug)]
34pub struct InverseKeymap {
35    map: HashMap<char, KeyStroke>,
36}
37
38impl InverseKeymap {
39    /// Creates an inverse keymap from a specialized `keymap` array. The value of this array at
40    /// index `u`, where `u` is the usage, can be:
41    ///
42    ///  * `None` if the key maps to no `char` (Esc key)
43    ///  * `Some((c, None))` if the key maps to `c`, but does not map to any `char` when shift is pressed
44    ///  * `Some((c, Some(cs)))` if the key maps to `c` when shift is not pressed and to `cs` when it is
45    ///    pressed
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// # use crate::inverse_keymap::InverseKeymap;
51    /// # use crate::US_QWERTY;
52    ///
53    /// let _keymap = InverseKeymap::new(&US_QWERTY);
54    /// ```
55    pub fn new(keymap: &keymaps::Keymap<'_>) -> Self {
56        let mut map = HashMap::new();
57
58        // A real keymap is not invertible, so multiple keys can produce the same effect.  For
59        // example, a key `1` and a numeric keypad `1` will end up inserting the same element in
60        // the inverse keymap.  By iterating over the forward keymap in reverse, we give priority
61        // to more "conventional" keys, i.e. a key `1` will always be used instead of numeric
62        // keypad `1`.  A similar idea is applied in all match arms below.
63        for (usage, key_levels) in keymap.as_ref().iter().enumerate().rev() {
64            let entry = key_levels.as_ref().map(|kl| (kl.ch, kl.shift_ch));
65            match entry {
66                Some((ch, Some(shift_ch))) if ch == shift_ch => {
67                    map.insert(ch, KeyStroke { usage: usage as u32, shift: Shift::DontCare });
68                }
69                Some((ch, Some(shift_ch))) => {
70                    map.insert(ch, KeyStroke { usage: usage as u32, shift: Shift::No });
71                    map.insert(shift_ch, KeyStroke { usage: usage as u32, shift: Shift::Yes });
72                }
73                Some((ch, None)) => {
74                    map.insert(ch, KeyStroke { usage: usage as u32, shift: Shift::No });
75                }
76                _ => (),
77            }
78        }
79
80        // Additionally, insert some convenience keystrokes.  This is not intended to be
81        // complete, but to allow simple keyboard navigation such as confirming text input
82        // or focus change to the next element.
83
84        // Based on //sdk/fidl/fuchsia.input/key.fidl.
85        // Assumes the usage page is 0x7.
86        map.insert('\n', KeyStroke { usage: 0x28, shift: Shift::No });
87        map.insert('\t', KeyStroke { usage: 0x2b, shift: Shift::No });
88
89        Self { map }
90    }
91
92    pub fn get(&self, c: &char) -> Option<&KeyStroke> {
93        self.map.get(c)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use assert_matches::assert_matches;
101
102    #[test]
103    fn returns_correct_shift_level() {
104        let keymap = InverseKeymap::new(&keymaps::US_QWERTY);
105        assert_matches!(keymap.get(&'a'), Some(KeyStroke { shift: Shift::No, .. }));
106        assert_matches!(keymap.get(&'A'), Some(KeyStroke { shift: Shift::Yes, .. }));
107    }
108
109    #[test]
110    fn returns_expected_usage() {
111        let keymap = InverseKeymap::new(&keymaps::US_QWERTY);
112        assert_matches!(keymap.get(&'a'), Some(KeyStroke { usage: 0x04, .. }));
113        // Numeric character: maps to main keyboard, not keypad.
114        assert_matches!(keymap.get(&'1'), Some(KeyStroke { usage: 0x1e, .. }));
115    }
116
117    #[test]
118    fn returns_special_keys() {
119        let keymap = InverseKeymap::new(&keymaps::US_QWERTY);
120        assert_matches!(keymap.get(&'\n'), Some(KeyStroke { usage: 0x28, shift: Shift::No }));
121        assert_matches!(keymap.get(&'\t'), Some(KeyStroke { usage: 0x2b, shift: Shift::No }));
122    }
123}