git.lucas.co / cce-keyboard
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 }