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}