denise_keyboard/grid.rs
1//! Where the keys are: a QWERTY-shaped grid of positions.
2//!
3//! Positions, not characters. What each one *types* is the layout's answer and
4//! is read at build time, which is why a layout switch relabels the keys rather
5//! than rebuilding them — `ø` and `;` live on [`KeyCode::Semicolon`] on
6//! Norwegian and US respectively, and the key does not move.
7//!
8//! # The shape, and why this one
9//!
10//! A compact physical keyboard — a Chromebook, near enough: no numpad, no
11//! function row, Backspace at the top right, Enter to the right of the home
12//! row, Shift at the left of the bottom one, and a key of its own where Caps
13//! Lock would be. Chosen over a phone keyboard because of what this is for: a
14//! panel somebody stands in front of, typing addresses into a browser and
15//! filling in a form. Digits belong on screen rather than behind a `123` page,
16//! and Tab is how you get from one field to the next.
17//!
18//! It is also **shorter than what it replaced**. The first version had six
19//! rows, because Backspace, Enter, Shift and the layout key were all crowded
20//! into a modifier row of their own; putting each where a hand reaches for it
21//! empties that row entirely. Five rows instead of six is 276 logical pixels
22//! instead of 330 — 54 back, which on an 800x480 panel is eleven per cent of
23//! the screen returned to the application.
24//!
25//! The keys whose meaning is a *name* — Backspace, Tab, Enter, the cursor keys
26//! — are drawn rather than lettered: see [`icons`](crate::icons). The ones whose
27//! legend carries **state** keep their words, because one glyph cannot say which
28//! of two states it is in.
29
30use denise::KeyCode;
31use denise_render::icon::Icon;
32
33use crate::icons;
34
35use crate::LAYOUT_KEY as LAYOUT;
36
37/// One key in the grid.
38#[derive(Clone, Copy, Debug)]
39pub struct Key {
40 /// The position this key stands for.
41 pub code: KeyCode,
42 /// A fixed legend, for keys whose meaning is not a character the layout
43 /// knows about. `None` means "ask the layout".
44 pub legend: Option<&'static str>,
45 /// A picture to draw instead of the word.
46 ///
47 /// Drawn rather than looked up, so it is the same on every machine. This
48 /// used to be a list of candidate *glyphs* tried against the loaded font,
49 /// because fonts disagree so completely about them: DejaVu has `⌫`, `⇥`,
50 /// `⏎` and the triangles, a Mac's Arial has none of those and no triangle
51 /// either, and the face that ships with `denise-render` has twenty-three
52 /// non-ASCII glyphs of which not one is either. That worked and left a
53 /// ceiling — no font, no symbol — which [`icons`](crate::icons) removes.
54 ///
55 /// The word stays as [`legend`](Self::legend) regardless: it is what the
56 /// key still *reports*, which is what a test reads and what an
57 /// accessibility pass would.
58 pub icon: Option<&'static Icon>,
59 /// Width, in half-widths of an ordinary letter key.
60 ///
61 /// Halves rather than whole keys because the useful sizes are not multiples
62 /// of a letter: Tab and Shift want one and a half, and a row that could only
63 /// count in whole letters would have to round one of them wrong.
64 pub units: i32,
65 /// Whether holding it keeps sending it.
66 ///
67 /// True for Backspace and nothing else, which is what a phone does and what
68 /// stops a slow finger typing `aaaaaa`. Holding a *letter* is a gesture with
69 /// a different meaning — the alternates a layout offers for it — and is not
70 /// this.
71 pub repeats: bool,
72}
73
74/// One letter's worth of width, in the units above.
75const LETTER: i32 = 2;
76
77impl Key {
78 /// A key lettered by the layout, one letter wide.
79 const fn new(code: KeyCode) -> Self {
80 Self {
81 code,
82 legend: None,
83 icon: None,
84 units: LETTER,
85 repeats: false,
86 }
87 }
88
89 /// A key with a legend of its own, `units` half-letters wide.
90 const fn wide(code: KeyCode, legend: &'static str, units: i32) -> Self {
91 Self {
92 code,
93 legend: Some(legend),
94 icon: None,
95 units,
96 repeats: false,
97 }
98 }
99
100 /// The same key, repeating while it is held.
101 const fn repeating(mut self) -> Self {
102 self.repeats = true;
103 self
104 }
105
106 /// The same key, drawn as a picture rather than its word.
107 const fn drawn(mut self, icon: &'static Icon) -> Self {
108 self.icon = Some(icon);
109 self
110 }
111}
112
113/// One row of keys.
114#[derive(Clone, Copy, Debug)]
115pub struct Row {
116 /// Left to right.
117 pub keys: &'static [Key],
118}
119
120// Fourteen columns, which is what both a Chromebook and an iPad settle on for
121// a keyboard of this shape. The width is not decoration: `Minus`, `Equal`,
122// `Backquote`, `BracketLeft`, `BracketRight` and `Backslash` are where a
123// Norwegian layout keeps `+`, `?`, the acute and grave dead keys, `å`, the
124// diaeresis, and `'`. A narrower grid cannot type `å` at all — one of the three
125// letters the layout exists for.
126const DIGITS: [Key; 14] = [
127 Key::new(KeyCode::Backquote),
128 Key::new(KeyCode::Digit1),
129 Key::new(KeyCode::Digit2),
130 Key::new(KeyCode::Digit3),
131 Key::new(KeyCode::Digit4),
132 Key::new(KeyCode::Digit5),
133 Key::new(KeyCode::Digit6),
134 Key::new(KeyCode::Digit7),
135 Key::new(KeyCode::Digit8),
136 Key::new(KeyCode::Digit9),
137 Key::new(KeyCode::Digit0),
138 Key::new(KeyCode::Minus),
139 Key::new(KeyCode::Equal),
140 // Where every keyboard puts it, and where a hand reaching to correct a typo
141 // already goes.
142 Key::wide(KeyCode::Backspace, "back", LETTER * 2)
143 .drawn(&icons::BACKSPACE)
144 .repeating(),
145];
146
147// Tab opens the second row, as it does on a real one. It is here because a form
148// is the main thing this keyboard is for and Tab is how you cross it — the tree
149// already moves focus on Tab, and until now the keyboard had no key to send it
150// with. `BracketLeft` carries å on Norwegian and `BracketRight` the diaeresis
151// that makes ö and ñ reachable.
152const TOP: [Key; 14] = [
153 Key::wide(KeyCode::Tab, "tab", LETTER * 3 / 2).drawn(&icons::TAB),
154 Key::new(KeyCode::Q),
155 Key::new(KeyCode::W),
156 Key::new(KeyCode::E),
157 Key::new(KeyCode::R),
158 Key::new(KeyCode::T),
159 Key::new(KeyCode::Y),
160 Key::new(KeyCode::U),
161 Key::new(KeyCode::I),
162 Key::new(KeyCode::O),
163 Key::new(KeyCode::P),
164 Key::new(KeyCode::BracketLeft),
165 Key::new(KeyCode::BracketRight),
166 Key::new(KeyCode::Backslash),
167];
168
169// Caps Lock where Caps Lock goes. It is a latch of its own rather than a third
170// state of Shift, which is how the keyboards this is shaped after do it and how
171// the composer already modelled it — Shift is then simply a one-shot, and the
172// two together behave the way a hand expects: caps on plus shift gives lower
173// case. `Semicolon` and `Quote` carry ø and æ on Norwegian and `;` and `'` on
174// US — the position is the same, only the legend moves — and Enter closes the
175// row they are on, which is where a hand looks for it.
176const HOME: [Key; 13] = [
177 Key::wide(KeyCode::CapsLock, "caps", LETTER * 2),
178 Key::new(KeyCode::A),
179 Key::new(KeyCode::S),
180 Key::new(KeyCode::D),
181 Key::new(KeyCode::F),
182 Key::new(KeyCode::G),
183 Key::new(KeyCode::H),
184 Key::new(KeyCode::J),
185 Key::new(KeyCode::K),
186 Key::new(KeyCode::L),
187 Key::new(KeyCode::Semicolon),
188 Key::new(KeyCode::Quote),
189 Key::wide(KeyCode::Enter, "enter", LETTER * 2).drawn(&icons::ENTER),
190];
191
192// Shift at both ends, as on both references. They are the same position and do
193// the same thing; having two is what lets either hand reach one.
194const BOTTOM: [Key; 12] = [
195 Key::wide(KeyCode::ShiftLeft, "shift", LETTER * 5 / 2),
196 Key::new(KeyCode::Z),
197 Key::new(KeyCode::X),
198 Key::new(KeyCode::C),
199 Key::new(KeyCode::V),
200 Key::new(KeyCode::B),
201 Key::new(KeyCode::N),
202 Key::new(KeyCode::M),
203 Key::new(KeyCode::Comma),
204 Key::new(KeyCode::Period),
205 Key::new(KeyCode::Slash),
206 Key::wide(KeyCode::ShiftLeft, "shift", LETTER * 5 / 2),
207];
208
209// Ctrl, Alt, the layout key, space, the arrows, and the key that puts the
210// keyboard away — in that order, which is the order the keyboards this is
211// shaped after use.
212//
213// The layout key lives here rather than in the Caps slot for the same reason
214// those keyboards put it here: it is a *setting*, pressed once in the life of a
215// panel, and a setting does not belong under the left hand's home position.
216//
217// Escape ends the row because that is where a soft keyboard puts the key that
218// dismisses it. It is a real `Escape` and not a private dismiss signal, so a
219// field that wants to cancel on Escape still hears one.
220//
221// `<-` and `->` rather than `<` and `>`, which on a Norwegian layout are
222// characters a key can type — and rather than the references' `◀ ▶`, which draw
223// as tofu on a stock Alpine with no fonts installed.
224const SPACE_ROW: [Key; 7] = [
225 // Legends for these two come from the keyboard's state rather than the
226 // layout: the key says what it will do next, not what it types.
227 Key::wide(KeyCode::ControlLeft, "ctrl", LETTER * 3 / 2),
228 Key::wide(KeyCode::AltRight, "alt", LETTER * 3 / 2),
229 Key {
230 code: LAYOUT,
231 legend: None,
232 // A globe says "language" without being in one. The layout's own name
233 // does not go away with it — it moves to the corner, because which
234 // layout is live is the one thing this key has to answer and a globe
235 // alone cannot.
236 icon: Some(&icons::GLOBE),
237 units: LETTER * 2,
238 repeats: false,
239 },
240 Key::wide(KeyCode::Space, " ", LETTER * 6),
241 Key::wide(KeyCode::ArrowLeft, "<-", LETTER).drawn(&icons::ARROW_LEFT),
242 Key::wide(KeyCode::ArrowRight, "->", LETTER).drawn(&icons::ARROW_RIGHT),
243 Key::wide(KeyCode::Escape, "esc", LETTER * 2).drawn(&icons::DISMISS),
244];
245
246/// The grid, top row first.
247pub static ROWS: [Row; 5] = [
248 Row { keys: &DIGITS },
249 Row { keys: &TOP },
250 Row { keys: &HOME },
251 Row { keys: &BOTTOM },
252 Row { keys: &SPACE_ROW },
253];
254
255/// The fixed legend for a position, if it has one.
256///
257/// Keys that type nothing carry their own words; everything else asks the
258/// layout, and the answer changes with the shift level.
259pub(crate) fn legend_of(code: KeyCode) -> Option<&'static str> {
260 entry(code).and_then(|key| key.legend)
261}
262
263/// The picture a position is drawn as, if it has one.
264pub(crate) fn icon_of(code: KeyCode) -> Option<&'static Icon> {
265 entry(code).and_then(|key| key.icon)
266}
267
268/// The grid entry for a position.
269fn entry(code: KeyCode) -> Option<&'static Key> {
270 ROWS.iter()
271 .flat_map(|row| row.keys)
272 .find(|key| key.code == code)
273}