on-screen keyboard
git clone https://git.lucas.co/cce-keyboard.git
src/layout.rs (11.4K)
1 //! The key table and its geometry: pure data and arithmetic, no Wayland.
2 //!
3 //! Five rows of 15 units each, a US ANSI board with the Caps Lock position
4 //! given to Fn. Key codes are evdev (`linux/input-event-codes.h`), which is
5 //! what `zwp_virtual_keyboard_v1.key` takes; the labels of character keys are
6 //! not here at all — they are read from the uploaded keymap (`keymap.rs`), so
7 //! a non-US layout labels itself.
8
9 /// Every row is this many units wide.
10 pub const ROW_UNITS: f32 = 15.0;
11
12 /// evdev key codes the board sends.
13 pub mod code {
14 pub const ESC: u32 = 1;
15 pub const N1: u32 = 2;
16 pub const N0: u32 = 11;
17 pub const MINUS: u32 = 12;
18 pub const EQUAL: u32 = 13;
19 pub const BACKSPACE: u32 = 14;
20 pub const TAB: u32 = 15;
21 pub const Q: u32 = 16;
22 pub const P: u32 = 25;
23 pub const LEFTBRACE: u32 = 26;
24 pub const RIGHTBRACE: u32 = 27;
25 pub const ENTER: u32 = 28;
26 pub const LEFTCTRL: u32 = 29;
27 pub const A: u32 = 30;
28 pub const L: u32 = 38;
29 pub const SEMICOLON: u32 = 39;
30 pub const APOSTROPHE: u32 = 40;
31 pub const GRAVE: u32 = 41;
32 pub const LEFTSHIFT: u32 = 42;
33 pub const BACKSLASH: u32 = 43;
34 pub const Z: u32 = 44;
35 pub const SLASH: u32 = 53;
36 pub const LEFTALT: u32 = 56;
37 pub const SPACE: u32 = 57;
38 pub const F1: u32 = 59;
39 pub const F11: u32 = 87;
40 pub const F12: u32 = 88;
41 pub const HOME: u32 = 102;
42 pub const UP: u32 = 103;
43 pub const PAGEUP: u32 = 104;
44 pub const LEFT: u32 = 105;
45 pub const RIGHT: u32 = 106;
46 pub const END: u32 = 107;
47 pub const DOWN: u32 = 108;
48 pub const PAGEDOWN: u32 = 109;
49 pub const DELETE: u32 = 111;
50 pub const LEFTMETA: u32 = 125;
51 }
52
53 /// A modifier the board latches. Order is press order around a key.
54 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
55 pub enum Modifier {
56 Ctrl,
57 Alt,
58 Super,
59 Shift,
60 }
61
62 impl Modifier {
63 pub const ALL: [Modifier; 4] = [Modifier::Ctrl, Modifier::Alt, Modifier::Super, Modifier::Shift];
64
65 pub fn index(self) -> usize {
66 self as usize
67 }
68
69 /// The key pressed for it (always the left one).
70 pub fn code(self) -> u32 {
71 match self {
72 Modifier::Ctrl => code::LEFTCTRL,
73 Modifier::Alt => code::LEFTALT,
74 Modifier::Super => code::LEFTMETA,
75 Modifier::Shift => code::LEFTSHIFT,
76 }
77 }
78
79 /// The xkb modifier name its mask is looked up by.
80 pub fn xkb_name(self) -> &'static str {
81 match self {
82 Modifier::Ctrl => "Control",
83 Modifier::Alt => "Mod1",
84 Modifier::Super => "Mod4",
85 Modifier::Shift => "Shift",
86 }
87 }
88
89 fn label(self) -> &'static str {
90 match self {
91 Modifier::Ctrl => "Ctrl",
92 Modifier::Alt => "Alt",
93 Modifier::Super => "Super",
94 Modifier::Shift => "Shift",
95 }
96 }
97 }
98
99 /// What a key does.
100 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
101 pub enum Action {
102 /// Send this evdev code, held as long as the pointer holds the key.
103 Code(u32),
104 /// Latch a modifier for the next key; a second tap locks it.
105 Mod(Modifier),
106 /// Latch the Fn layer: the keys with a second action switch to it.
107 Fn,
108 /// Close the keyboard.
109 Hide,
110 }
111
112 /// One key: its action, its action on the Fn layer, and its width in units.
113 #[derive(Debug, Clone, Copy, PartialEq)]
114 pub struct KeyDef {
115 pub action: Action,
116 pub fn_action: Option<Action>,
117 pub units: f32,
118 }
119
120 impl KeyDef {
121 /// The action in force with the Fn layer on or off.
122 pub fn action(&self, fn_layer: bool) -> Action {
123 match (fn_layer, self.fn_action) {
124 (true, Some(a)) => a,
125 _ => self.action,
126 }
127 }
128 }
129
130 const fn key(code: u32) -> KeyDef {
131 KeyDef { action: Action::Code(code), fn_action: None, units: 1.0 }
132 }
133
134 const fn wide(action: Action, units: f32) -> KeyDef {
135 KeyDef { action, fn_action: None, units }
136 }
137
138 const fn layered(code: u32, fn_code: u32, units: f32) -> KeyDef {
139 KeyDef { action: Action::Code(code), fn_action: Some(Action::Code(fn_code)), units }
140 }
141
142 /// The board, top row first.
143 pub fn rows() -> Vec<Vec<KeyDef>> {
144 use code::*;
145 let run = |from: u32, to: u32| (from..=to).map(key).collect::<Vec<_>>();
146 let mut number = vec![layered(ESC, GRAVE, 1.0)];
147 number.extend((N1..=N0).map(|c| layered(c, F1 + (c - N1), 1.0)));
148 number.extend([layered(MINUS, F11, 1.0), layered(EQUAL, F12, 1.0), layered(BACKSPACE, DELETE, 2.0)]);
149
150 let mut top = vec![wide(Action::Code(TAB), 1.5)];
151 top.extend(run(Q, P));
152 top.extend([key(LEFTBRACE), key(RIGHTBRACE), wide(Action::Code(BACKSLASH), 1.5)]);
153
154 let mut home = vec![wide(Action::Fn, 1.75)];
155 home.extend(run(A, L));
156 home.extend([key(SEMICOLON), key(APOSTROPHE), wide(Action::Code(ENTER), 2.25)]);
157
158 let mut bottom = vec![wide(Action::Mod(Modifier::Shift), 2.25)];
159 bottom.extend(run(Z, SLASH));
160 bottom.push(wide(Action::Mod(Modifier::Shift), 2.75));
161
162 let space = vec![
163 wide(Action::Mod(Modifier::Ctrl), 1.5),
164 wide(Action::Mod(Modifier::Super), 1.25),
165 wide(Action::Mod(Modifier::Alt), 1.25),
166 wide(Action::Code(SPACE), 6.0),
167 layered(LEFT, HOME, 1.0),
168 layered(DOWN, PAGEDOWN, 1.0),
169 layered(UP, PAGEUP, 1.0),
170 layered(RIGHT, END, 1.0),
171 wide(Action::Hide, 1.0),
172 ];
173 vec![number, top, home, bottom, space]
174 }
175
176 /// Every code the board can send, for the keymap's label pass.
177 pub fn all_codes(rows: &[Vec<KeyDef>]) -> Vec<u32> {
178 let mut codes: Vec<u32> = rows
179 .iter()
180 .flatten()
181 .flat_map(|k| [Some(k.action), k.fn_action])
182 .flatten()
183 .filter_map(|a| match a {
184 Action::Code(c) => Some(c),
185 _ => None,
186 })
187 .collect();
188 codes.sort_unstable();
189 codes.dedup();
190 codes
191 }
192
193 /// A key that is not a character: the name it wears, or the bundled
194 /// cce-icons glyph standing in for it (with the name as the fallback —
195 /// always a word, never a symbol character, since the fallback only shows
196 /// when the icon set is missing).
197 pub fn fixed_label(action: Action) -> Option<(&'static str, Option<&'static str>)> {
198 use code::*;
199 let named = |name| Some((name, None));
200 match action {
201 Action::Mod(m) => named(m.label()),
202 Action::Fn => named("Fn"),
203 Action::Hide => Some(("Hide", Some("chevron-down"))),
204 Action::Code(c) => match c {
205 ESC => named("Esc"),
206 BACKSPACE => named("Back"),
207 DELETE => named("Del"),
208 TAB => named("Tab"),
209 ENTER => named("Enter"),
210 SPACE => named(""),
211 LEFT => Some(("Left", Some("arrow-left"))),
212 RIGHT => Some(("Right", Some("arrow-right"))),
213 UP => Some(("Up", Some("arrow-up"))),
214 DOWN => Some(("Down", Some("arrow-down"))),
215 HOME => named("Home"),
216 END => named("End"),
217 PAGEUP => named("PgUp"),
218 PAGEDOWN => named("PgDn"),
219 F1..=68 => FN_NAMES.get((c - F1) as usize).map(|n| (*n, None)),
220 F11 => named("F11"),
221 F12 => named("F12"),
222 _ => None,
223 },
224 }
225 }
226
227 const FN_NAMES: [&str; 10] = ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10"];
228
229 /// A key's rectangle, in the surface's logical px.
230 #[derive(Debug, Clone, Copy, PartialEq)]
231 pub struct KeyRect {
232 pub x: f32,
233 pub y: f32,
234 pub w: f32,
235 pub h: f32,
236 }
237
238 /// Where every key sits inside `area` (x, y, w, h), keys `gap` apart.
239 ///
240 /// Units are a pitch, not a width: a key `u` units wide spans `u` pitches
241 /// less one gap, so the columns line up from row to row the way a real
242 /// board's do, whatever the gap.
243 pub fn place(rows: &[Vec<KeyDef>], area: (f32, f32, f32, f32), gap: f32) -> Vec<Vec<KeyRect>> {
244 let (ax, ay, aw, ah) = area;
245 let n = rows.len().max(1) as f32;
246 let pitch_x = (aw + gap) / ROW_UNITS;
247 let pitch_y = (ah + gap) / n;
248 rows.iter()
249 .enumerate()
250 .map(|(r, row)| {
251 let mut at = 0.0;
252 row.iter()
253 .map(|k| {
254 let rect = KeyRect {
255 x: ax + at * pitch_x,
256 y: ay + r as f32 * pitch_y,
257 w: (k.units * pitch_x - gap).max(0.0),
258 h: (pitch_y - gap).max(0.0),
259 };
260 at += k.units;
261 rect
262 })
263 .collect()
264 })
265 .collect()
266 }
267
268 /// The key under (`x`, `y`), as (row, index). The gaps belong to the keys
269 /// on either side of them — half each — so a press between two keys is
270 /// never lost; only a press outside the board misses.
271 pub fn hit(rects: &[Vec<KeyRect>], gap: f32, x: f32, y: f32) -> Option<(usize, usize)> {
272 let half = gap / 2.0;
273 rects.iter().enumerate().find_map(|(r, row)| {
274 row.iter()
275 .position(|k| x >= k.x - half && x < k.x + k.w + half && y >= k.y - half && y < k.y + k.h + half)
276 .map(|i| (r, i))
277 })
278 }
279
280 #[cfg(test)]
281 mod tests {
282 use super::*;
283
284 #[test]
285 fn every_row_is_fifteen_units() {
286 for (r, row) in rows().iter().enumerate() {
287 let units: f32 = row.iter().map(|k| k.units).sum();
288 assert!((units - ROW_UNITS).abs() < 1e-4, "row {r} is {units} units");
289 }
290 }
291
292 #[test]
293 fn rows_fill_the_area_edge_to_edge() {
294 let rows = rows();
295 let rects = place(&rows, (10.0, 20.0, 1000.0, 300.0), 6.0);
296 for row in &rects {
297 let first = row.first().unwrap();
298 let last = row.last().unwrap();
299 assert!((first.x - 10.0).abs() < 1e-3);
300 assert!((last.x + last.w - 1010.0).abs() < 1e-3, "row ends at {}", last.x + last.w);
301 }
302 let last = rects.last().unwrap()[0];
303 assert!((last.y + last.h - 320.0).abs() < 1e-3);
304 }
305
306 #[test]
307 fn neighbours_are_one_gap_apart() {
308 let rows = rows();
309 let rects = place(&rows, (0.0, 0.0, 900.0, 250.0), 8.0);
310 for row in &rects {
311 for pair in row.windows(2) {
312 assert!((pair[1].x - (pair[0].x + pair[0].w) - 8.0).abs() < 1e-3);
313 }
314 }
315 }
316
317 #[test]
318 fn a_press_in_a_gap_lands_on_a_neighbour() {
319 let rows = rows();
320 let gap = 10.0;
321 let rects = place(&rows, (0.0, 0.0, 1500.0, 500.0), gap);
322 let q = rects[1][1];
323 // Just right of Q, inside the gap: still Q.
324 assert_eq!(hit(&rects, gap, q.x + q.w + 4.0, q.y + 5.0), Some((1, 1)));
325 // Just left of W, inside the same gap: W.
326 assert_eq!(hit(&rects, gap, q.x + q.w + 6.0, q.y + 5.0), Some((1, 2)));
327 assert_eq!(hit(&rects, gap, -20.0, 5.0), None);
328 }
329
330 #[test]
331 fn the_fn_layer_swaps_only_layered_keys() {
332 let rows = rows();
333 assert_eq!(rows[0][1].action(true), Action::Code(code::F1));
334 assert_eq!(rows[0][10].action(true), Action::Code(code::F1 + 9));
335 assert_eq!(rows[0][13].action(true), Action::Code(code::DELETE));
336 assert_eq!(rows[1][1].action(true), Action::Code(code::Q));
337 assert_eq!(rows[4][4].action(false), Action::Code(code::LEFT));
338 assert_eq!(rows[4][4].action(true), Action::Code(code::HOME));
339 }
340
341 #[test]
342 fn every_function_key_is_named() {
343 for row in rows() {
344 for k in row {
345 if let Some(Action::Code(c)) = k.fn_action {
346 assert!(fixed_label(Action::Code(c)).is_some() || c == code::GRAVE, "code {c} has no label");
347 }
348 }
349 }
350 }
351 }