Skip to main content

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}