tx2_web/io/
keyboard.rs

1//! This module implements the keyboard of the Lincoln Writer.
2//!
3//! A diagram of the keyboard layout appears in the Lincoln Lab
4//! Division 6 Quarterly Progress Report, 15 June 1958 (Fig. 63-7, on
5//! PDF page 38, page numbered 24).  This diagram is presumably to
6//! scale but dimensions are not given on it.  The Lincoln Writer
7//! actually has two keyboards.  The keyboard nearest the typist
8//! contains decimal digts, majuscule letters (ABC, etc.), the space
9//! bar and a few others.  The keyboard further from the typist
10//! contains some minuscule latin letters, some Greek letters
11//! (minuscule and majuscule) and some symbols (hand, *, arrow, #, ?).
12//!
13//! The near keyboard produces the symbols shown in the left-hand
14//! column of the character set shown in table 7-6 of the TX-2 Lincoln
15//! Technical manual, and the far keyboard contains the symbols shown
16//! in the right-hand column.
17//!
18//! Carriage return sets the Lincoln Writer to lower case (and normal
19//! script).  See pages 4-37 to 4-42 of the User Handbook.  Since this
20//! is presumably for convenience in the common case, and since there
21//! is not a full set of minuscule ("abc...") letters on the Lincoln
22//! Writer, I infer this to mean that the keyboard farthest from the
23//! typist, containing the keys "hijk<>wxyz.." for example, is the in
24//! fact the "upper-case" keyboard.  That is, those keys send the
25//! "UPPER CASE" (075) code if pressed at (for example) the beginning
26//! of a line.
27//!
28//! NOTE: In "The Lincoln Writer" (Lincoln Laboratory Group report
29//! 51-8), the authors state that "The lower case keyboard was almost
30//! standard (our capital letters were put on the lower case)" (p8).
31//!
32//! Also on p. 8 of the same document: "The keyboard is actually two
33//! separate Soroban coding keyboards mounted on the same block.  The
34//! lower keyboard contains the buttons for all the lower case
35//! characters and the typewriter functions.  Thew upper board
36//! contains the buttons for upper case character and a few special;
37//! codes."
38//!
39//! Lincoln Writer codes are not unique without knowing the
40//! upper/lower case state.  For example, code 26 is sent for both "G"
41//! and "w".
42//!
43//! To make the code in this module slightly less confusing, we refer
44//! to the two keyboards as "Far" and "Near".  Assuming a case change
45//! is needed, "G" would be sent as 074 026 (074 signifying LOWER
46//! CASE) and "w" would be sent as 075 026 (075 signifying UPPER
47//! CASE).  The documentation describes a hardware interlock which
48//! keeps a key pressed down while a shift code is sent, so I assume
49//! that shift code are only sent when necessary (e.g. "wGG" would be
50//! sent as 075 026 075 026 026).  This is consistent with the
51//! description in "The Lincoln Writer" (Group Repport 51-8).
52//!
53//! Some changes to the keyboard happened between Lincoln Lab Division
54//! 6 Quarterly Progress Report, 15 June 1958 ("PR") and Group Report
55//! 51-8 ("GR").  Specifically, CONTINUE and HALT were removed and
56//! LINE FEED UP and LINE FEED DOWN were added.  The order of the
57//! function keys was changed from:
58//!
59//! - DELETE
60//! - STOP
61//! - YES
62//! - NO
63//! - WORD EXAM
64//! - CONTINUE
65//! - HALT
66//! - BEGIN
67//! - READ IN
68//!
69//! to:
70//!
71//! - DELETE
72//! - STOP
73//! - LINE FEED UP
74//! - LINE FEED DOWN
75//! - WORD EXAM
76//! - YES
77//! - NO
78//! - BEGIN
79//! - READ IN
80//!
81//! Accordingly, CONTINUE is also not listed in Table 7-6 of the
82//! Technical Manual.
83//!
84//! The documentation is inconsistent about the codes of some of the
85//! keys.  These keys are:
86//!
87//! YES: 73 according to the PR, 17 according to the TM.
88//!
89//! NO: 72 according to the PR, 16 according to the TM.
90//!
91//! LINE FEED UP: not listed in the PR, 73 according to the TM.
92//!
93//! LINE FEED DOWN: not listed in the PR, 72 according to the TM.
94//!
95//! The DELETE key in the PR is listed as NULLIFY in the TM.
96//!
97//! The top-row keys READ IN (14) BEGIN (15), STOP (76), WORD EXAM
98//! (71) are coded consistently between the PR and the TM.
99//!
100//! Using the size of a square key cap as 1x1, dimensions of the other
101//! parts of the keyboard are:
102//!
103//! Total height of keyboard (between top of YES and bottom of space bar): 14.45
104//! Total width of keyboard (between LHS of TAB and RHS of NORMAL): 19.7 (but see below).
105//!
106//! On the RHS of the keyboard unit are three indicator lamps not
107//! included in the "width" figure above.  These are circular and have
108//! diameter 0.5.  Each is labeled on its LHS.
109//!
110//! Dimensions (h*w):
111//! Regular key: 1*1
112//! Gaps between keys: 0.5*0.43
113//! Space bar: 1*8.5
114//! Wide key (for example RETURN): 1.7*1
115//! Tall key (for example HALT): 1.7*1
116//! Gap b/w upper and lower keyboards: 1.9
117//! Whole unit (incl indicators): 23.8*14.5
118//!
119//! An additional reference on the Lincoln Writer is
120//! [The Lincoln Writer](https://apps.dtic.mil/sti/trecms/pdf/AD0235247.pdf).
121//! J. T. Glmore, Jr., R. E. Sewell.  Lincoln Laboratory Group report
122//! 51-8.  October 6, 1959.
123//!
124//! This states a number of things that are perhaps not yet reflected in this code:
125//!
126//! 1. The box character just fits inside of the printing rectangle
127//!    and can contain any other character (p7)
128//! 1. The capital letters are larger than the numerals and small characters (p7)
129//! 1. The numerals are slanted (p7).
130//! 1. With the exception of the capital letters and a few punctuation characters,
131//!    all characters can be circled (p7).   (I interpret this as meaning that all
132//!    characters can be combined with a circle, but the circle is not large enough
133//!    to contain some of the characters.
134
135use core::fmt::{Debug, Display};
136#[cfg(test)]
137use std::collections::{HashMap, HashSet};
138
139use conv::*;
140use tracing::{Level, event};
141use wasm_bindgen::prelude::*;
142use web_sys::{CanvasRenderingContext2d, TextMetrics};
143
144#[cfg(test)]
145use base::Unsigned6Bit;
146#[cfg(test)]
147use base::charset::{
148    Colour, DescribedChar, LincolnState, LwKeyboardCase, Script, lincoln_char_to_described_char,
149};
150
151// Horizontal gap between LHS of unit and the first key of each row:
152const HPOS_DELETE: f32 = 0.7;
153const HPOS_ARROW: f32 = 0.0;
154const HPOS_TILDE: f32 = 0.34;
155const HPOS_LOGICAL_AND: f32 = 1.0;
156const HPOS_TAB: f32 = 0.0;
157const HPOS_BLACK: f32 = 0.0;
158const HPOS_BACKSPACE: f32 = 0.34;
159const HPOS_RETURN: f32 = 0.43;
160const HPOS_Z: f32 = 3.6;
161const HPOS_SPACE_BAR: f32 = 6.3;
162
163// Vertical gap between top of unit and the first key of each row:
164const VPOS_DELETE: f32 = 0.7;
165const VPOS_ARROW: f32 = 2.17;
166const VPOS_TILDE: f32 = 3.6;
167const VPOS_LOGICAL_AND: f32 = 5.0;
168
169const VPOS_TAB: f32 = VPOS_LOGICAL_AND + KEY_HEIGHT + 1.2;
170const VPOS_RED: f32 = VPOS_LOGICAL_AND + KEY_HEIGHT + 1.9;
171const VPOS_BLACK: f32 = VPOS_RED + KEY_HEIGHT + GAP_HEIGHT;
172const VPOS_BACKSPACE: f32 = VPOS_BLACK + KEY_HEIGHT + GAP_HEIGHT;
173const VPOS_RETURN: f32 = VPOS_BACKSPACE + KEY_HEIGHT + GAP_HEIGHT;
174const VPOS_SPACE_BAR: f32 = VPOS_RETURN + KEY_HEIGHT + GAP_HEIGHT;
175
176// Key dimensions:
177const KEY_WIDTH: f32 = 1.0;
178const KEY_HEIGHT: f32 = KEY_WIDTH;
179const WIDE_KEY_WIDTH: f32 = 1.7;
180const TALL_KEY_WIDTH: f32 = KEY_WIDTH;
181const GAP_WIDTH: f32 = 0.5;
182const GAP_HEIGHT: f32 = 0.43;
183const KEY_AND_GAP_WIDTH: f32 = KEY_WIDTH + GAP_WIDTH;
184const TALL_KEY_AND_GAP_WIDTH: f32 = TALL_KEY_WIDTH + GAP_WIDTH;
185const WIDE_KEY_AND_GAP_WIDTH: f32 = WIDE_KEY_WIDTH + GAP_WIDTH;
186
187const HIT_DETECTION_BACKGROUND: &str = "#ffffff";
188
189/// When the ascent or descent font metrics for a text string are NaN,
190/// we fall back on the actual metrics (instead of font metrics).  But
191/// the actual metrics don't include the inter-line spacing, so we use
192/// a fudge factor to guess at something appropriate.
193const FONT_METRIC_FUDGE_FACTOR: f64 = 1.5_f64;
194
195#[derive(Debug)]
196enum KeyColour {
197    Orange,
198    Red,
199    Yellow,
200    Green,
201    Brown,
202    Grey,
203    Black,
204}
205
206impl KeyColour {
207    fn key_css_colour(&self) -> &'static str {
208        match self {
209            KeyColour::Orange => "darkorange",
210            KeyColour::Red => "crimson",
211            KeyColour::Yellow => "gold",
212            KeyColour::Green => "limegreen",
213            KeyColour::Brown => "#ba8759", // "Deer"
214            KeyColour::Grey => "lightslategrey",
215            KeyColour::Black => "black",
216        }
217    }
218
219    fn label_css_colour(&self) -> &'static str {
220        match self {
221            KeyColour::Orange
222            | KeyColour::Red
223            | KeyColour::Yellow
224            | KeyColour::Grey
225            | KeyColour::Green
226            | KeyColour::Brown => "black",
227            KeyColour::Black => "white",
228        }
229    }
230}
231
232#[derive(Debug)]
233enum KeyShape {
234    Square,
235    Wide,
236    Tall,
237    Spacebar,
238}
239
240impl KeyShape {
241    fn width(&self) -> f32 {
242        match self {
243            KeyShape::Square | KeyShape::Tall => 1.0,
244            KeyShape::Wide => 1.7,
245            KeyShape::Spacebar => 8.5,
246        }
247    }
248
249    fn height(&self) -> f32 {
250        match self {
251            KeyShape::Square | KeyShape::Wide | KeyShape::Spacebar => 1.0,
252            KeyShape::Tall => 1.7,
253        }
254    }
255}
256
257#[derive(Copy, Clone, Hash, PartialEq, Eq)]
258pub(crate) enum Code {
259    /// Represents a single key on the Lincoln Writer.  Lincoln Writer
260    /// key codes are actually 6 bits but we convert elsewhere.
261    Far(u8),
262    Near(u8),
263    Unknown,
264}
265
266impl Debug for Code {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        let code = match self {
269            Code::Far(code) => {
270                f.write_str("Far(")?;
271                code
272            }
273            Code::Near(code) => {
274                f.write_str("Near(")?;
275                code
276            }
277            Code::Unknown => {
278                return f.write_str("Unknown");
279            }
280        };
281        write!(f, "{code:#o})")
282    }
283}
284
285pub const SWITCH_TO_FAR: u8 = 0o74; // LOWER CASE
286pub const SWITCH_TO_NEAR: u8 = 0o75; // UPPER CASE
287
288impl Code {
289    fn hit_detection_rgb(&self) -> [u8; 3] {
290        match self {
291            Code::Unknown => [0, 0, 0xFF],
292            Code::Near(n) => [n * 4, 0, 1],
293            Code::Far(n) => [n * 4, 0, 0x80],
294        }
295    }
296
297    fn hit_detection_colour(&self) -> String {
298        let rgb = self.hit_detection_rgb();
299        format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])
300    }
301
302    pub(crate) fn hit_detection_rgb_to_code(r: u8, g: u8, b: u8) -> Result<Option<Code>, String> {
303        match (r, g, b) {
304            (0xff, 0xff, 0xff) => Ok(None), // not a key at all (i.e. background)
305            (0, 0, 0xff) => Ok(Some(Code::Unknown)),
306            (n, 0, 0x01) => Ok(Some(Code::Near(n / 4))),
307            (n, 0, 0x80) => Ok(Some(Code::Far(n / 4))),
308            _ => Err(format!(
309                "failed to decode colour (components={:?})",
310                (r, g, b)
311            )),
312        }
313    }
314
315    // This function is temporarily test-only because while it has
316    // tests, it has no caller yet.
317    #[cfg(test)]
318    fn hit_detection_colour_to_code(colour: &str) -> Result<Option<Code>, String> {
319        let mut components: [u8; 3] = [0, 0, 0];
320        for (i, digit) in colour.chars().enumerate() {
321            if i == 0 {
322                if digit == '#' {
323                    continue;
324                }
325                return Err(format!(
326                    "colour should begin with #, but began with {digit}"
327                ));
328            } else if i > 6 {
329                return Err(format!(
330                    "colour should have only 6 digits but it has {}",
331                    colour.len() - 1
332                ));
333            }
334            if let Ok(n) = hex_digit_value(digit) {
335                let pos: usize = (i - 1) / 2;
336                components[pos] = components[pos] * 16 + n;
337            } else {
338                return Err(format!("{digit} is not a valid hex digit"));
339            }
340        }
341        Code::hit_detection_rgb_to_code(components[0], components[1], components[2])
342    }
343}
344
345#[test]
346fn hit_detection_colour_does_not_map_to_a_key_code() {
347    let expected: Result<Option<Code>, String> = Ok(None);
348    let got = Code::hit_detection_colour_to_code(HIT_DETECTION_BACKGROUND);
349    if got == Ok(None) {
350        return;
351    }
352    panic!(
353        "Colour {HIT_DETECTION_BACKGROUND} should map to 'no key' (that is, {expected:?}) but it actually mapped to {got:?}",
354    );
355}
356
357// This function is temporarily test-only because its sole caller is
358// not used yet.
359#[cfg(test)]
360fn hex_digit_value(ch: char) -> Result<u8, ()> {
361    match ch {
362        '0' => Ok(0),
363        '1' => Ok(1),
364        '2' => Ok(2),
365        '3' => Ok(3),
366        '4' => Ok(4),
367        '5' => Ok(5),
368        '6' => Ok(6),
369        '7' => Ok(7),
370        '8' => Ok(8),
371        '9' => Ok(9),
372        'a' | 'A' => Ok(0xA),
373        'b' | 'B' => Ok(0xB),
374        'c' | 'C' => Ok(0xC),
375        'd' | 'D' => Ok(0xD),
376        'e' | 'E' => Ok(0xE),
377        'f' | 'F' => Ok(0xF),
378        _ => Err(()),
379    }
380}
381
382#[derive(Debug)]
383struct KeyLabel {
384    text: &'static [&'static str],
385}
386
387#[derive(Debug)]
388pub enum KeyPaintError {
389    Failed(String),
390    TextTooBig {
391        msg: String,
392        lines: &'static [&'static str],
393    },
394    InvalidFontMetrics(String),
395}
396
397impl Display for KeyPaintError {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        match self {
400            KeyPaintError::Failed(s) | KeyPaintError::InvalidFontMetrics(s) => {
401                std::fmt::Display::fmt(&s, f)
402            }
403            KeyPaintError::TextTooBig { msg, lines } => {
404                write!(f, "key label text {lines:?} is too big: {msg}")
405            }
406        }
407    }
408}
409
410#[derive(Debug)]
411struct Point {
412    x: f32,
413    y: f32,
414}
415
416impl Display for Point {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        write!(f, "({},{})", self.x, self.y)
419    }
420}
421
422#[derive(Debug)]
423struct BoundingBox {
424    nw: Point,
425    se: Point,
426}
427
428impl BoundingBox {
429    fn height(&self) -> f32 {
430        self.bottom() - self.top()
431    }
432
433    fn width(&self) -> f32 {
434        self.right() - self.left()
435    }
436
437    fn left(&self) -> f32 {
438        self.nw.x
439    }
440
441    fn right(&self) -> f32 {
442        self.se.x
443    }
444
445    fn top(&self) -> f32 {
446        self.nw.y
447    }
448
449    fn bottom(&self) -> f32 {
450        self.se.y
451    }
452}
453
454trait KeyPainter {
455    fn width(&self) -> f32;
456    fn height(&self) -> f32;
457    fn draw_background(&mut self) -> Result<(), KeyPaintError>;
458    fn draw_key(
459        &mut self,
460        keybox: &BoundingBox,
461        colour: &KeyColour,
462        label: &KeyLabel,
463        keycode: Code,
464    ) -> Result<(), KeyPaintError>;
465}
466
467#[wasm_bindgen]
468pub struct HtmlCanvas2DPainter {
469    height: f32,
470    width: f32,
471    context: CanvasRenderingContext2d,
472    hits_only: bool,
473}
474
475fn keep_greatest<T: PartialOrd, E>(
476    accumulator: Option<Result<T, E>>,
477    item: Result<T, E>,
478) -> Option<Result<T, E>> {
479    match (item, accumulator) {
480        (Err(e), _) | (_, Some(Err(e))) => Some(Err(e)), // preserve errors
481        (Ok(curr), None) => Some(Ok(curr)),
482        (Ok(curr), Some(Ok(acc_val))) => {
483            if curr > acc_val {
484                Some(Ok(curr))
485            } else {
486                Some(Ok(acc_val))
487            }
488        }
489    }
490}
491
492enum Room {
493    Sufficient,
494    Insufficient(Vec<String>),
495}
496
497fn room_for_key_padding(w: f64, h: f64, bbox: &BoundingBox) -> Room {
498    const MAX_SIZE_FRACTION: f64 = 0.85;
499    let avail_width = f64::from(bbox.width()) * MAX_SIZE_FRACTION;
500    let hfit = w <= avail_width;
501    let avail_height = f64::from(bbox.height()) * MAX_SIZE_FRACTION;
502    let vfit = h <= avail_height;
503    if vfit && hfit {
504        Room::Sufficient
505    } else {
506        let mut problems = Vec::with_capacity(2);
507        if !hfit {
508            problems.push(format!("max text width is {w:.1} but this will not comfortably fit (available width is {avail_width:.1}"));
509        }
510        if !vfit {
511            problems.push(format!("total text height is {h:.1} but this will not comfortably fit (available height is {avail_height:.1}"));
512        }
513        Room::Insufficient(problems)
514    }
515}
516
517fn check_font_metric(
518    name: &str,
519    value: Option<Result<f64, KeyPaintError>>,
520) -> Result<f64, KeyPaintError> {
521    match value {
522        Some(Ok(x)) => {
523            if x.is_nan() {
524                Err(KeyPaintError::InvalidFontMetrics(format!(
525                    "font metric {name} is NaN"
526                )))
527            } else {
528                Ok(x)
529            }
530        }
531        Some(Err(e)) => Err(e),
532        None => Ok(0.0),
533    }
534}
535
536/// Return the distance between the baseline of some measured text and
537/// the bottom of whatever would be avove it.  We use font ascent
538/// instead of actual ascent, so that we get an appropriate amount of
539/// space between lines.  This also gives us some consistency of
540/// appearance.  However, if the font ascent metric is not available,
541/// we use the actual ascent with a "fudge factor".
542fn font_or_actual(
543    font_value: f64,
544    font_metric_name: &str,
545    actual_value: f64,
546    actual_metric_name: &str,
547) -> Result<f64, KeyPaintError> {
548    match (font_value.is_nan(), actual_value.is_nan()) {
549        (false, _) => Ok(font_value),
550        (true, false) => Ok(actual_value * FONT_METRIC_FUDGE_FACTOR),
551        (true, true) => Err(KeyPaintError::InvalidFontMetrics(format!(
552            "text metric {font_metric_name} is NaN, but fallback metric {actual_metric_name} is also NaN"
553        ))),
554    }
555}
556
557fn ascent(metrics: &TextMetrics) -> Result<f64, KeyPaintError> {
558    font_or_actual(
559        metrics.font_bounding_box_ascent(),
560        "font_bounding_box_ascent",
561        metrics.actual_bounding_box_ascent(),
562        "actual_bounding_box_ascent",
563    )
564}
565
566fn descent(metrics: &TextMetrics) -> Result<f64, KeyPaintError> {
567    font_or_actual(
568        metrics.font_bounding_box_descent(),
569        "font_bounding_box_descent",
570        metrics.actual_bounding_box_descent(),
571        "actual_bounding_box_descent",
572    )
573}
574
575fn bounding_box_width(metrics: &TextMetrics) -> Result<f64, KeyPaintError> {
576    match (
577        metrics.actual_bounding_box_left(),
578        metrics.actual_bounding_box_right(),
579    ) {
580        (left, _) if left.is_nan() => Err(KeyPaintError::InvalidFontMetrics(
581            "actual_bounding_box_left is NaN".to_string(),
582        )),
583        (_, right) if right.is_nan() => Err(KeyPaintError::InvalidFontMetrics(
584            "actual_bounding_box_right is NaN".to_string(),
585        )),
586        (left, right) => Ok((right - left).abs()),
587    }
588}
589
590impl HtmlCanvas2DPainter {
591    pub fn new(
592        context: web_sys::CanvasRenderingContext2d,
593        hits_only: bool,
594    ) -> Result<HtmlCanvas2DPainter, KeyPaintError> {
595        match context.canvas() {
596            Some(canvas) => {
597                HtmlCanvas2DPainter::set_up_context_defaults(&context);
598                Ok(HtmlCanvas2DPainter {
599                    height: canvas.height() as f32,
600                    width: canvas.width() as f32,
601                    context,
602                    hits_only,
603                })
604            }
605            _ => Err(KeyPaintError::Failed(
606                "CanvasRenderingContext2d has no associated canvas".to_string(),
607            )),
608        }
609    }
610
611    fn set_up_context_defaults(context: &CanvasRenderingContext2d) {
612        context.set_line_cap("round");
613        context.set_stroke_style_str("black");
614    }
615
616    fn fill_multiline_text(
617        &mut self,
618        bbox: &BoundingBox,
619        lines: &'static [&'static str],
620        colour: &str,
621    ) -> Result<(), KeyPaintError> {
622        self.context.set_text_baseline("middle");
623        let metrics: Vec<TextMetrics> =
624            match lines.iter().map(|s| self.context.measure_text(s)).collect() {
625                Ok(v) => v,
626                Err(e) => {
627                    return Err(KeyPaintError::Failed(format!("measure_text failed: {e:?}")));
628                }
629            };
630        let max_ascent = check_font_metric(
631            "max ascent",
632            metrics.iter().map(ascent).fold(None, keep_greatest),
633        )?;
634        let max_descent = check_font_metric(
635            "max descent",
636            metrics.iter().map(descent).fold(None, keep_greatest),
637        )?;
638
639        let mw: f64 = match metrics
640            .iter()
641            .map(bounding_box_width)
642            .fold(None, keep_greatest)
643        {
644            Some(Ok(x)) => x,
645            None => 0.0,
646            Some(Err(e)) => {
647                return Err(e);
648            }
649        };
650
651        //for (metric, s) in metrics.iter().zip(lines.iter()) {
652        //    if metric.actual_bounding_box_left() != 0.0_f64 {
653        //        event!(
654        //            Level::TRACE,
655        //            "left bounding box for {s} is {}",
656        //            metric.actual_bounding_box_left()
657        //        );
658        //    }
659        //}
660        let (a, d, max_width) = match (max_ascent, max_descent, mw) {
661            (a, d, m) if (a == 0.0 && d == 0.0) || m == 0.0 => {
662                return Ok(()); // There is no text to draw.
663            }
664            (a, d, m) => (a, d, m),
665        };
666        let n = lines.len();
667        let line_height: f64 = a + d;
668        let total_text_height = line_height * (n as f64);
669        if let Room::Insufficient(problems) =
670            room_for_key_padding(max_width, total_text_height, bbox)
671        {
672            return Err(KeyPaintError::TextTooBig {
673                msg: format!("no room for key padding: {}", problems.join("\n")),
674                lines,
675            });
676        }
677        let x_midline = (bbox.left() + bbox.right()) / 2.0;
678        let y_midline = (bbox.top() + bbox.bottom()) / 2.0;
679        fn yi(i: usize, n: usize, y_midline: f64, a: f64, d: f64) -> f64 {
680            let ashift: f64 = if n.is_multiple_of(2) { a } else { 0.0 };
681            fn f(x: usize) -> f64 {
682                f64::value_from(x).unwrap_or(f64::MAX)
683            }
684            let i_minus_floor_half_n = f(i) - f(n / 2).floor();
685            y_midline + ashift + i_minus_floor_half_n * (a + d)
686        }
687
688        for (i, (s, metrics)) in lines.iter().zip(metrics.iter()).enumerate() {
689            let text_y = yi(i, n, y_midline.into(), a, d);
690
691            // This calculation for x assumes Left-to-Right text.
692            let text_x: f64 = f64::from(x_midline) - metrics.actual_bounding_box_right() / 2.0;
693            self.context.set_fill_style_str(colour);
694            if let Err(e) = self.context.fill_text(s, text_x, text_y) {
695                return Err(KeyPaintError::Failed(format!("fill_text failed: {e:?}")));
696            }
697        }
698        Ok(())
699    }
700
701    fn draw_filled_stroked_rect(&self, x: f64, y: f64, w: f64, h: f64) {
702        self.context.fill_rect(x, y, w, h);
703        self.context.rect(x, y, w, h);
704    }
705}
706
707impl KeyPainter for HtmlCanvas2DPainter {
708    fn width(&self) -> f32 {
709        self.width
710    }
711
712    fn height(&self) -> f32 {
713        self.height
714    }
715
716    fn draw_background(&mut self) -> Result<(), KeyPaintError> {
717        if self.hits_only {
718            // We fill the background with a colour which never maps back
719            // to a key code.
720            self.context.set_fill_style_str(HIT_DETECTION_BACKGROUND);
721            self.context
722                .fill_rect(0.0_f64, 0.0_f64, self.width().into(), self.height().into());
723        }
724        Ok(())
725    }
726
727    fn draw_key(
728        &mut self,
729        keybox: &BoundingBox,
730        colour: &KeyColour,
731        label: &KeyLabel,
732        keycode: Code,
733    ) -> Result<(), KeyPaintError> {
734        let hit_detection_colour: String = keycode.hit_detection_colour();
735
736        if self.hits_only {
737            self.context
738                .set_fill_style_str(hit_detection_colour.as_str());
739            self.context.fill_rect(
740                keybox.left().into(),
741                keybox.top().into(),
742                keybox.width().into(),
743                keybox.height().into(),
744            );
745            self.context.stroke();
746            return Ok(());
747        }
748
749        self.context.set_fill_style_str(colour.key_css_colour());
750        self.context.set_stroke_style_str("black");
751        let label_css_color = colour.label_css_colour();
752
753        self.draw_filled_stroked_rect(
754            keybox.left().into(),
755            keybox.top().into(),
756            keybox.width().into(),
757            keybox.height().into(),
758        );
759        self.context.stroke();
760
761        let mut current_error: Option<KeyPaintError> = None;
762        if !label.text.is_empty() {
763            for font_size in (1..=28).rev() {
764                let font = format!("{font_size}px sans-serif");
765                self.context.set_font(&font);
766                match self.fill_multiline_text(keybox, label.text, label_css_color) {
767                    Ok(()) => {
768                        current_error = None;
769                    }
770                    Err(KeyPaintError::InvalidFontMetrics(msg)) => {
771                        current_error = Some(KeyPaintError::InvalidFontMetrics(format!(
772                            "font metrics for {:?} in font '{}' are invalid: {}",
773                            label.text, font, msg
774                        )));
775                    }
776                    Err(e) => {
777                        current_error = Some(e);
778                    }
779                }
780                match &current_error {
781                    Some(KeyPaintError::InvalidFontMetrics(msg)) => {
782                        event!(
783                            Level::WARN,
784                            "Invalid font metrics for '{}' ({}); trying a smaller font size",
785                            font,
786                            msg
787                        );
788                        continue; // Try a smaller font size.
789                    }
790                    Some(KeyPaintError::TextTooBig { msg: _, lines: _ }) => {
791                        continue; // Try a smaller font size.
792                    }
793                    Some(KeyPaintError::Failed(why)) => {
794                        return Err(KeyPaintError::Failed(format!(
795                            "failed to draw multiline text in font {font_size}: {why}"
796                        )));
797                    }
798                    None => {
799                        break;
800                    }
801                }
802            }
803        }
804        match current_error {
805            None => Ok(()),
806            Some(error) => Err(error),
807        }
808    }
809}
810
811#[derive(Debug)]
812struct Key {
813    left: f32,
814    top: f32,
815    shape: KeyShape,
816    colour: KeyColour,
817    label: KeyLabel,
818    code: Code,
819}
820
821fn row0() -> &'static [Key] {
822    // This is the top row of the keyboard furthest from the typist.
823    const HPOS_YES: f32 = HPOS_DELETE + WIDE_KEY_AND_GAP_WIDTH * 2.0;
824    &[
825        Key /* DELETE */ {
826            left: HPOS_DELETE,
827            top: VPOS_DELETE,
828            shape: KeyShape::Wide,
829            colour: KeyColour::Brown,
830            label: KeyLabel {
831                text: &["DELETE"],
832            },
833            code: Code::Far(0o77),
834        },
835        Key /* STOP */ {
836            left: HPOS_DELETE + WIDE_KEY_WIDTH + GAP_WIDTH,
837            top: VPOS_DELETE,
838            shape: KeyShape::Wide,
839            colour: KeyColour::Grey,
840            label: KeyLabel {
841                text: &["STOP"],
842            },
843            code: Code::Far(0o76),
844        },
845        Key /* LINE FEED UP */ {
846            left: HPOS_YES,
847            top: 0.0,
848            shape: KeyShape::Tall,
849            colour: KeyColour::Black,
850            label: KeyLabel {
851                text: &["LINE", "FEED", "UP"],
852            },
853            code: Code::Far(0o73),
854        },
855        Key /* LINE FEED DOWN */ {
856            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH,
857            top: 0.0,
858            shape: KeyShape::Tall,
859            colour: KeyColour::Black,
860            label: KeyLabel {
861                text: &["LINE", "FEED", "DOWN"],
862            },
863            code: Code::Far(0o72),
864        },
865        Key /* WORD EXAM */ {
866            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 2.0,
867            top: 0.0,
868            shape: KeyShape::Tall,
869            colour: KeyColour::Black,
870            label: KeyLabel {
871                text: &["WORD", "EXAM"],
872            },
873            code: Code::Far(0o71),
874        },
875        Key /* YES */ {
876            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 3.0,
877            top: 0.0,
878            shape: KeyShape::Tall,
879            colour: KeyColour::Black,
880            label: KeyLabel {
881                text: &["YES"],
882            },
883            code: Code::Far(0o17),
884        },
885        Key /* NO */ {
886            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 4.0,
887            top: 0.0,
888            shape: KeyShape::Tall,
889            colour: KeyColour::Black,
890            label: KeyLabel {
891                text: &["NO"],
892            },
893            code: Code::Far(0o16),
894        },
895        Key /* BEGIN */ {
896            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 5.0,
897            top: 0.0,
898            shape: KeyShape::Tall,
899            colour: KeyColour::Black,
900            label: KeyLabel {
901                text: &["B", "E", "G", "I", "N"],
902            },
903            code: Code::Far(0o15),
904        },
905        Key /* READ IN */ {
906            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 6.0,
907            top: 0.0,
908            shape: KeyShape::Tall,
909            colour: KeyColour::Black,
910            label: KeyLabel {
911                text: &["READ", "IN"],
912            },
913            code: Code::Far(0o14),
914        },
915        Key /* apostrophe */ {
916            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 6.0 + KEY_AND_GAP_WIDTH,
917            top: VPOS_DELETE,
918            shape: KeyShape::Square,
919            colour: KeyColour::Red,
920            label: KeyLabel {
921                text: &["'"],
922            },
923            code: Code::Far(0o56),
924        },
925        Key /* asterisk */ {
926            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 6.0 + KEY_AND_GAP_WIDTH * 2.0,
927            top: VPOS_DELETE,
928            shape: KeyShape::Square,
929            colour: KeyColour::Red,
930            label: KeyLabel {
931                text: &["*"],
932            },
933            code: Code::Far(0o57),
934        },
935        Key /* meta hand */ {
936            left: HPOS_YES + TALL_KEY_AND_GAP_WIDTH * 6.0 + KEY_AND_GAP_WIDTH * 3.0,
937            top: VPOS_DELETE,
938            shape: KeyShape::Wide,
939            colour: KeyColour::Brown,
940            label: KeyLabel {
941                text: &["\u{261E}"],
942                // Symbol choices for the "meta hand" were:
943                //
944                // U+261B, black hand pointing right or
945                // U+261E, white hand pointing right
946                //
947                // The latter is an outlined while the former is a
948                // filled symbol.  The outlined symbol is more
949                // consistent with the appearance of the key as whown
950                // in the illustration in the monthly status report.
951                // Unfortunately neither symbol shows a cuff, which
952                // does appear in the illustration.
953            },
954            code: Code::Far(0o00),
955        },
956    ]
957}
958
959fn row1() -> &'static [Key] {
960    &[
961        Key /* -> */ {
962            left: HPOS_ARROW,
963            top: VPOS_ARROW,
964            shape: KeyShape::Square,
965            colour: KeyColour::Brown,
966            label: KeyLabel {
967                text: &["\u{2192}"], // rightwards arrow
968            },
969            code: Code::Far(0o07),
970        },
971        Key /* ∪ */ {
972            left: HPOS_ARROW + 1.0 * KEY_AND_GAP_WIDTH,
973            top: VPOS_ARROW,
974            shape: KeyShape::Square,
975            colour: KeyColour::Brown,
976            label: KeyLabel {
977                text: &["\u{222A}"], // Union
978            },
979            code: Code::Far(0o34),
980        },
981        Key /* ∩ */ {
982            left: HPOS_ARROW + 2.0 * KEY_AND_GAP_WIDTH,
983            top: VPOS_ARROW,
984            shape: KeyShape::Square,
985            colour: KeyColour::Brown,
986            label: KeyLabel {
987                text: &["\u{2229}"], // Intersection
988            },
989            code: Code::Far(0o35),
990        },
991        Key /* Σ */ {
992            left: HPOS_ARROW + 3.0 * KEY_AND_GAP_WIDTH,
993            top: VPOS_ARROW,
994            shape: KeyShape::Square,
995            colour: KeyColour::Yellow,
996            label: KeyLabel {
997                text: &["\u{03A3}"], // Greek capital letter Sigma
998            },
999            code: Code::Far(0o01),
1000        },
1001        Key /* × (multiply) */ {
1002            left: HPOS_ARROW + 4.0 * KEY_AND_GAP_WIDTH,
1003            top: VPOS_ARROW,
1004            shape: KeyShape::Square,
1005            colour: KeyColour::Yellow,
1006            label: KeyLabel {
1007                text: &["\u{00D7}"],
1008            },
1009            code: Code::Far(0o05),
1010        },
1011        Key /* h */ {
1012            left: HPOS_ARROW + 5.0 * KEY_AND_GAP_WIDTH,
1013            top: VPOS_ARROW,
1014            shape: KeyShape::Square,
1015            colour: KeyColour::Green,
1016            label: KeyLabel {
1017                text: &["h"],
1018            },
1019            code: Code::Far(0o44),
1020        },
1021        Key /* i */ {
1022            left: HPOS_ARROW + 6.0 * KEY_AND_GAP_WIDTH,
1023            top: VPOS_ARROW,
1024            shape: KeyShape::Square,
1025            colour: KeyColour::Green,
1026            label: KeyLabel {
1027                text: &["i"],
1028            },
1029            code: Code::Far(0o30),
1030        },
1031        Key /* j */ {
1032            left: HPOS_ARROW + 7.0 * KEY_AND_GAP_WIDTH,
1033            top: VPOS_ARROW,
1034            shape: KeyShape::Square,
1035            colour: KeyColour::Green,
1036            label: KeyLabel {
1037                text: &["j"],
1038            },
1039            code: Code::Far(0o36),
1040        },
1041        Key /* k */ {
1042            left: HPOS_ARROW + 8.0 * KEY_AND_GAP_WIDTH,
1043            top: VPOS_ARROW,
1044            shape: KeyShape::Square,
1045            colour: KeyColour::Green,
1046            label: KeyLabel {
1047                text: &["k"],
1048            },
1049            code: Code::Far(0o37),
1050        },
1051        Key /* ε */ {
1052            left: HPOS_ARROW + 9.0 * KEY_AND_GAP_WIDTH,
1053            top: VPOS_ARROW,
1054            shape: KeyShape::Square,
1055            colour: KeyColour::Orange,
1056            label: KeyLabel {
1057                text: &["\u{03B5}"], // Epsilon (not ∈, Element of)
1058            },
1059            code: Code::Far(0o43),
1060        },
1061        Key /* λ */ {
1062            left: HPOS_ARROW + 10.0 * KEY_AND_GAP_WIDTH,
1063            top: VPOS_ARROW,
1064            shape: KeyShape::Square,
1065            colour: KeyColour::Orange,
1066            label: KeyLabel {
1067                text: &["\u{03BB}"], // Greek small letter lambda, U+03BB
1068            },
1069            code: Code::Far(0o50),
1070        },
1071        Key /* # */ {
1072            left: HPOS_ARROW + 11.0 * KEY_AND_GAP_WIDTH,
1073            top: VPOS_ARROW,
1074            shape: KeyShape::Square,
1075            colour: KeyColour::Red,
1076            label: KeyLabel {
1077                text: &["#"],
1078            },
1079            code: Code::Far(0o06),
1080        },
1081        Key /* ‖ */ {
1082            left: HPOS_ARROW + 12.0 * KEY_AND_GAP_WIDTH,
1083            top: VPOS_ARROW,
1084            shape: KeyShape::Square,
1085            colour: KeyColour::Red,
1086            label: KeyLabel {
1087                text: &["\u{2016}"], // U+2016, double vertical line
1088            },
1089            code: Code::Far(0o03),
1090        },
1091        Key /* ? */ {
1092            left: HPOS_ARROW + 13.0 * KEY_AND_GAP_WIDTH,
1093            top: VPOS_ARROW,
1094            shape: KeyShape::Square,
1095            colour: KeyColour::Red,
1096            label: KeyLabel {
1097                text: &["?"],
1098            },
1099            code: Code::Far(0o33),
1100        },
1101    ]
1102}
1103
1104// missing tests: key codes are unique, key labels consistent with base charset definition
1105
1106fn row2() -> &'static [Key] {
1107    &[
1108        Key /* ~ */ {
1109            left: HPOS_TILDE,
1110            top: VPOS_TILDE,
1111            shape: KeyShape::Square,
1112            colour: KeyColour::Brown,
1113            label: KeyLabel {
1114                text: &["~"], // tilde
1115            },
1116            code: Code::Far(0o51),
1117        },
1118        Key /* ⊃ */ {
1119            left: HPOS_TILDE + 1.0 * KEY_AND_GAP_WIDTH,
1120            top: VPOS_TILDE,
1121            shape: KeyShape::Square,
1122            colour: KeyColour::Brown,
1123            label: KeyLabel {
1124                text: &["\u{2283}"], // Superset of
1125            },
1126            code: Code::Far(0o45),
1127        },
1128        Key /* ⊂ */ {
1129            left: HPOS_TILDE + 2.0 * KEY_AND_GAP_WIDTH,
1130            top: VPOS_TILDE,
1131            shape: KeyShape::Square,
1132            colour: KeyColour::Brown,
1133            label: KeyLabel {
1134                text: &["\u{2282}"], // Subset of
1135            },
1136            code: Code::Far(0o21),
1137        },
1138        Key /* < */ {
1139            left: HPOS_TILDE + 3.0 * KEY_AND_GAP_WIDTH,
1140            top: VPOS_TILDE,
1141            shape: KeyShape::Square,
1142            colour: KeyColour::Yellow,
1143            label: KeyLabel {
1144                text: &["<"],
1145            },
1146            code: Code::Far(0o10),
1147        },
1148        Key /* > */ {
1149            left: HPOS_TILDE + 4.0 * KEY_AND_GAP_WIDTH,
1150            top: VPOS_TILDE,
1151            shape: KeyShape::Square,
1152            colour: KeyColour::Yellow,
1153            label: KeyLabel {
1154                text: &[">"],
1155            },
1156            code: Code::Far(0o11),
1157        },
1158        Key /* n */ {
1159            // On the diagram in the progress report (see module
1160            // header for details) this looks very clearly like a
1161            // lower-case Greek eta (η, Unicode U+03B7).  However two
1162            // other considerations make me believe this is in fact n
1163            // (lower-case N):
1164            //
1165            // 1. It's next to "p" on the keyboard
1166            // 2. The symbol in in the Users Handbook, although hard
1167            //    to make out, really doesn't look like an eta because
1168            //    it doesn't have a long tail on the right-hand side.
1169            left: HPOS_TILDE + 5.0 * KEY_AND_GAP_WIDTH,
1170            top: VPOS_TILDE,
1171            shape: KeyShape::Square,
1172            colour: KeyColour::Green,
1173            label: KeyLabel {
1174                text: &["n"],
1175            },
1176            code: Code::Far(0o20),
1177        },
1178        Key /* p */ {
1179            left: HPOS_TILDE + 6.0 * KEY_AND_GAP_WIDTH,
1180            top: VPOS_TILDE,
1181            shape: KeyShape::Square,
1182            colour: KeyColour::Green,
1183            label: KeyLabel {
1184                text: &["p"],
1185            },
1186            code: Code::Far(0o42),
1187        },
1188        Key /* q */ {
1189            left: HPOS_TILDE + 7.0 * KEY_AND_GAP_WIDTH,
1190            top: VPOS_TILDE,
1191            shape: KeyShape::Square,
1192            colour: KeyColour::Green,
1193            label: KeyLabel {
1194                text: &["q"],
1195            },
1196            code: Code::Far(0o23),
1197        },
1198        Key /* t */ {
1199            left: HPOS_TILDE + 8.0 * KEY_AND_GAP_WIDTH,
1200            top: VPOS_TILDE,
1201            shape: KeyShape::Square,
1202            colour: KeyColour::Green,
1203            label: KeyLabel {
1204                text: &["t"],
1205            },
1206            code: Code::Far(0o25),
1207        },
1208        Key /* Δ */ {
1209            left: HPOS_TILDE + 9.0 * KEY_AND_GAP_WIDTH,
1210            top: VPOS_TILDE,
1211            shape: KeyShape::Square,
1212            colour: KeyColour::Orange,
1213            label: KeyLabel {
1214                text: &["\u{0394}"], // Greek capital delta, U+0394
1215            },
1216            code: Code::Far(0o41),
1217        },
1218        Key /* γ */ {
1219            left: HPOS_TILDE + 10.0 * KEY_AND_GAP_WIDTH,
1220            top: VPOS_TILDE,
1221            shape: KeyShape::Square,
1222            colour: KeyColour::Orange,
1223            label: KeyLabel {
1224                text: &["\u{03B3}"], // Greek small letter gamma (U+03B3)
1225            },
1226            code: Code::Far(0o24),
1227        },
1228        Key /* { */ {
1229            left: HPOS_TILDE + 11.0 * KEY_AND_GAP_WIDTH,
1230            top: VPOS_TILDE,
1231            shape: KeyShape::Square,
1232            colour: KeyColour::Red,
1233            label: KeyLabel {
1234                text: &["{"],
1235            },
1236            code: Code::Far(0o52),
1237        },
1238        Key /* } */ {
1239            left: HPOS_TILDE + 12.0 * KEY_AND_GAP_WIDTH,
1240            top: VPOS_TILDE,
1241            shape: KeyShape::Square,
1242            colour: KeyColour::Red,
1243            label: KeyLabel {
1244                text: &["}"],
1245            },
1246            code: Code::Far(0o53),
1247        },
1248        Key /* |, LW code 02 */ {
1249            left: HPOS_TILDE + 13.0 * KEY_AND_GAP_WIDTH,
1250            top: VPOS_TILDE,
1251            shape: KeyShape::Square,
1252            colour: KeyColour::Red,
1253            label: KeyLabel {
1254                text: &["|"],
1255            },
1256            code: Code::Far(0o02),
1257        },
1258    ]
1259}
1260
1261fn row3() -> &'static [Key] {
1262    &[
1263        Key /* ∧ */ {
1264            left: HPOS_LOGICAL_AND,
1265            top: VPOS_LOGICAL_AND,
1266            shape: KeyShape::Square,
1267            colour: KeyColour::Brown,
1268            label: KeyLabel {
1269                text: &["\u{2227}"], // U+2227, Logical And
1270            },
1271            code: Code::Far(0o47),
1272        },
1273        Key /* ∨ */ {
1274            left: HPOS_LOGICAL_AND + 1.0 * KEY_AND_GAP_WIDTH,
1275            top: VPOS_LOGICAL_AND,
1276            shape: KeyShape::Square,
1277            colour: KeyColour::Brown,
1278            label: KeyLabel {
1279                text: &["\u{2228}"], // U+2228, Logical Or
1280            },
1281            code: Code::Far(0o22),
1282        },
1283        Key /* ≡ */ {
1284            left: HPOS_LOGICAL_AND + 2.0 * KEY_AND_GAP_WIDTH,
1285            top: VPOS_LOGICAL_AND,
1286            shape: KeyShape::Square,
1287            colour: KeyColour::Brown,
1288            label: KeyLabel {
1289                text: &["\u{2261}"], // U+2261, Identical to
1290            },
1291            code: Code::Far(0o54),
1292        },
1293        Key /* / */ {
1294            left: HPOS_LOGICAL_AND + 3.0 * KEY_AND_GAP_WIDTH,
1295            top: VPOS_LOGICAL_AND,
1296            shape: KeyShape::Square,
1297            colour: KeyColour::Yellow,
1298            label: KeyLabel {
1299                text: &["/"],
1300            },
1301            code: Code::Far(0o04),
1302        },
1303        Key /* = */ {
1304            left: HPOS_LOGICAL_AND + 4.0 * KEY_AND_GAP_WIDTH,
1305            top: VPOS_LOGICAL_AND,
1306            shape: KeyShape::Square,
1307            colour: KeyColour::Yellow,
1308            label: KeyLabel {
1309                text: &["="],
1310            },
1311            code: Code::Far(0o55),
1312        },
1313        Key /* w */ {
1314            left: HPOS_LOGICAL_AND + 5.0 * KEY_AND_GAP_WIDTH,
1315            top: VPOS_LOGICAL_AND,
1316            shape: KeyShape::Square,
1317            colour: KeyColour::Green,
1318            label: KeyLabel {
1319                text: &["w"],
1320            },
1321            code: Code::Far(0o26),
1322        },
1323        Key /* x */ {
1324            left: HPOS_LOGICAL_AND + 6.0 * KEY_AND_GAP_WIDTH,
1325            top: VPOS_LOGICAL_AND,
1326            shape: KeyShape::Square,
1327            colour: KeyColour::Green,
1328            label: KeyLabel {
1329                text: &["x"],
1330            },
1331            code: Code::Far(0o27),
1332        },
1333        Key /* y */ {
1334            left: HPOS_LOGICAL_AND + 7.0 * KEY_AND_GAP_WIDTH,
1335            top: VPOS_LOGICAL_AND,
1336            shape: KeyShape::Square,
1337            colour: KeyColour::Green,
1338            label: KeyLabel {
1339                text: &["y"],
1340            },
1341            code: Code::Far(0o31),
1342        },
1343        Key /* z */ {
1344            left: HPOS_LOGICAL_AND + 8.0 * KEY_AND_GAP_WIDTH,
1345            top: VPOS_LOGICAL_AND,
1346            shape: KeyShape::Square,
1347            colour: KeyColour::Green,
1348            label: KeyLabel {
1349                text: &["z"],
1350            },
1351            code: Code::Far(0o32),
1352        },
1353        Key /* α */ {
1354            left: HPOS_LOGICAL_AND + 9.0 * KEY_AND_GAP_WIDTH,
1355            top: VPOS_LOGICAL_AND,
1356            shape: KeyShape::Square,
1357            colour: KeyColour::Orange,
1358            label: KeyLabel {
1359                text: &["\u{03B1}"],
1360            },
1361            code: Code::Far(0o40),
1362        },
1363        Key /* β */ {
1364            left: HPOS_LOGICAL_AND + 10.0 * KEY_AND_GAP_WIDTH,
1365            top: VPOS_LOGICAL_AND,
1366            shape: KeyShape::Square,
1367            colour: KeyColour::Orange,
1368            label: KeyLabel {
1369                text: &["\u{03B2}"], // Greek beta symbol, U+03B2
1370            },
1371            code: Code::Far(0o46),
1372        },
1373        Key {
1374            // overline
1375            left: HPOS_LOGICAL_AND + 11.0 * KEY_AND_GAP_WIDTH,
1376            top: VPOS_LOGICAL_AND,
1377            shape: KeyShape::Square,
1378            colour: KeyColour::Red,
1379            label: KeyLabel {
1380                text: &["\u{0305} "], // Combining overline U+0305
1381            },
1382            code: Code::Far(0o12),
1383        },
1384        Key {
1385            // square
1386            left: HPOS_LOGICAL_AND + 12.0 * KEY_AND_GAP_WIDTH,
1387            top: VPOS_LOGICAL_AND,
1388            shape: KeyShape::Square,
1389            colour: KeyColour::Red,
1390            label: KeyLabel {
1391                text: &["\u{20DE} "], // combining enclosing square
1392            },
1393            code: Code::Far(0o13),
1394        },
1395    ]
1396}
1397
1398fn row4() -> &'static [Key] {
1399    // This is the top row of the keyboard nearest the typist.
1400    &[
1401        Key /* TAB */ {
1402            left: HPOS_TAB,
1403            top: VPOS_TAB,
1404            shape: KeyShape::Tall,
1405            colour: KeyColour::Grey,
1406            label: KeyLabel {
1407                text: &["T", "A", "B"],
1408            },
1409            code: Code::Near(0o61),
1410        },
1411        Key /* RED */ {
1412            left: HPOS_TAB + KEY_AND_GAP_WIDTH,
1413            top: VPOS_RED,
1414            shape: KeyShape::Square,
1415            colour: KeyColour::Red,
1416            label: KeyLabel {
1417                text: &["RED"],
1418            },
1419            code: Code::Near(0o67),
1420        },
1421        Key /* 0 */ {
1422            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 2.0,
1423            top: VPOS_RED,
1424            shape: KeyShape::Square,
1425            colour: KeyColour::Yellow,
1426            label: KeyLabel {
1427                text: &["0"],
1428            },
1429            code: Code::Near(0o00),
1430        },
1431        Key /* 1 */ {
1432            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 3.0,
1433            top: VPOS_RED,
1434            shape: KeyShape::Square,
1435            colour: KeyColour::Yellow,
1436            label: KeyLabel {
1437                text: &["1"],
1438            },
1439            code: Code::Near(0o01),
1440        },
1441        Key /* 2 */ {
1442            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 4.0,
1443            top: VPOS_RED,
1444            shape: KeyShape::Square,
1445            colour: KeyColour::Yellow,
1446            label: KeyLabel {
1447                text: &["2"],
1448            },
1449            code: Code::Near(0o02),
1450        },
1451        Key /* 3 */ {
1452            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 5.0,
1453            top: VPOS_RED,
1454            shape: KeyShape::Square,
1455            colour: KeyColour::Yellow,
1456            label: KeyLabel {
1457                text: &["3"],
1458            },
1459            code: Code::Near(0o03),
1460        },
1461        Key /* 4 */ {
1462            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 6.0,
1463            top: VPOS_RED,
1464            shape: KeyShape::Square,
1465            colour: KeyColour::Yellow,
1466            label: KeyLabel {
1467                text: &["4"],
1468            },
1469            code: Code::Near(0o04),
1470        },
1471        Key /* 5 */ {
1472            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 7.0,
1473            top: VPOS_RED,
1474            shape: KeyShape::Square,
1475            colour: KeyColour::Yellow,
1476            label: KeyLabel {
1477                text: &["5"],
1478            },
1479            code: Code::Near(0o05),
1480        },
1481        Key /* 6 */ {
1482            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 8.0,
1483            top: VPOS_RED,
1484            shape: KeyShape::Square,
1485            colour: KeyColour::Yellow,
1486            label: KeyLabel {
1487                text: &["6"],
1488            },
1489            code: Code::Near(0o06),
1490        },
1491        Key /* 7 */ {
1492            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 9.0,
1493            top: VPOS_RED,
1494            shape: KeyShape::Square,
1495            colour: KeyColour::Yellow,
1496            label: KeyLabel {
1497                text: &["7"],
1498            },
1499            code: Code::Near(0o07),
1500        },
1501        Key /* 8 */ {
1502            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 10.0,
1503            top: VPOS_RED,
1504            shape: KeyShape::Square,
1505            colour: KeyColour::Yellow,
1506            label: KeyLabel {
1507                text: &["8"],
1508            },
1509            code: Code::Near(0o10),
1510        },
1511        Key /* 9 */ {
1512            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 11.0,
1513            top: VPOS_RED,
1514            shape: KeyShape::Square,
1515            colour: KeyColour::Yellow,
1516            label: KeyLabel {
1517                text: &["9"],
1518            },
1519            code: Code::Near(0o11),
1520        },
1521        Key /* underbar */ {
1522            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 12.0,
1523            top: VPOS_RED,
1524            shape: KeyShape::Square,
1525            colour: KeyColour::Red,
1526            label: KeyLabel {
1527                text: &["\u{0332} "], // combining low line
1528            },
1529            code: Code::Near(0o12),
1530        },
1531        Key /* circle */ {
1532            left: HPOS_TAB + KEY_AND_GAP_WIDTH * 13.0,
1533            top: VPOS_RED,
1534            shape: KeyShape::Square,
1535            colour: KeyColour::Red,
1536            label: KeyLabel {
1537                text: &[
1538                    "\u{20DD} ", // combining enclosing circle
1539                ]
1540            },
1541            code: Code::Near(0o13),
1542        },
1543    ]
1544}
1545
1546fn row5() -> &'static [Key] {
1547    const HPOS_Q: f32 = HPOS_BLACK + WIDE_KEY_WIDTH + GAP_WIDTH;
1548    &[
1549        Key /* BLACK */ {
1550            left: HPOS_BLACK,
1551            top: VPOS_BLACK,
1552            shape: KeyShape::Wide,
1553            colour: KeyColour::Black,
1554            label: KeyLabel {
1555                text: &["BLACK"]
1556            },
1557            code: Code::Near(0o63),
1558        },
1559        Key {
1560            left: HPOS_Q,
1561            top: VPOS_BLACK,
1562            shape: KeyShape::Square,
1563            colour: KeyColour::Green,
1564            label: KeyLabel { text: &["Q"] },
1565            code: Code::Near(0o40),
1566        },
1567        Key {
1568            left: HPOS_Q + KEY_AND_GAP_WIDTH * 1.0,
1569            top: VPOS_BLACK,
1570            shape: KeyShape::Square,
1571            colour: KeyColour::Green,
1572            label: KeyLabel { text: &["W"] },
1573            code: Code::Near(0o46),
1574        },
1575        Key {
1576            left: HPOS_Q + KEY_AND_GAP_WIDTH * 2.0,
1577            top: VPOS_BLACK,
1578            shape: KeyShape::Square,
1579            colour: KeyColour::Green,
1580            label: KeyLabel { text: &["E"] },
1581            code: Code::Near(0o24),
1582        },
1583        Key {
1584            left: HPOS_Q + KEY_AND_GAP_WIDTH * 3.0,
1585            top: VPOS_BLACK,
1586            shape: KeyShape::Square,
1587            colour: KeyColour::Green,
1588            label: KeyLabel { text: &["R"] },
1589            code: Code::Near(0o41),
1590        },
1591        Key {
1592            left: HPOS_Q + KEY_AND_GAP_WIDTH * 4.0,
1593            top: VPOS_BLACK,
1594            shape: KeyShape::Square,
1595            colour: KeyColour::Green,
1596            label: KeyLabel { text: &["T"] },
1597            code: Code::Near(0o43),
1598        },
1599        Key {
1600            left: HPOS_Q + KEY_AND_GAP_WIDTH * 5.0,
1601            top: VPOS_BLACK,
1602            shape: KeyShape::Square,
1603            colour: KeyColour::Green,
1604            label: KeyLabel { text: &["Y"] },
1605            code: Code::Near(0o50),
1606        },
1607        Key {
1608            left: HPOS_Q + KEY_AND_GAP_WIDTH * 6.0,
1609            top: VPOS_BLACK,
1610            shape: KeyShape::Square,
1611            colour: KeyColour::Green,
1612            label: KeyLabel { text: &["U"] },
1613            code: Code::Near(0o44),
1614        },
1615        Key {
1616            left: HPOS_Q + KEY_AND_GAP_WIDTH * 7.0,
1617            top: VPOS_BLACK,
1618            shape: KeyShape::Square,
1619            colour: KeyColour::Green,
1620            label: KeyLabel { text: &["I"] },
1621            code: Code::Near(0o30),
1622        },
1623        Key {
1624            left: HPOS_Q + KEY_AND_GAP_WIDTH * 8.0,
1625            top: VPOS_BLACK,
1626            shape: KeyShape::Square,
1627            colour: KeyColour::Green,
1628            label: KeyLabel { text: &["O"] },
1629            code: Code::Near(0o36),
1630        },
1631        Key {
1632            left: HPOS_Q + KEY_AND_GAP_WIDTH * 9.,
1633            top: VPOS_BLACK,
1634            shape: KeyShape::Square,
1635            colour: KeyColour::Green,
1636            label: KeyLabel { text: &["P"] },
1637            code: Code::Near(0o37),
1638        },
1639        Key {
1640            left: HPOS_Q + KEY_AND_GAP_WIDTH * 10.0,
1641            top: VPOS_BLACK,
1642            shape: KeyShape::Square,
1643            colour: KeyColour::Yellow,
1644            label: KeyLabel { text: &["."] },
1645            code: Code::Near(0o57),
1646        },
1647        Key {
1648            left: HPOS_Q + KEY_AND_GAP_WIDTH * 11.0,
1649            top: VPOS_BLACK,
1650            shape: KeyShape::Wide,
1651            colour: KeyColour::Black,
1652            label: KeyLabel { text: &["SUPER"] },
1653            code: Code::Near(0o64),
1654        },
1655    ]
1656}
1657
1658fn row6() -> &'static [Key] {
1659    const HPOS_A: f32 = HPOS_BACKSPACE + WIDE_KEY_WIDTH + GAP_WIDTH;
1660    &[
1661        Key /* BACKSPACE */ {
1662            left: HPOS_BACKSPACE,
1663            top: VPOS_BACKSPACE,
1664            shape: KeyShape::Wide,
1665            colour: KeyColour::Grey,
1666            label: KeyLabel {
1667                text: &["BACK", "SPACE"]
1668            },
1669            code: Code::Near(0o62),
1670        },
1671        Key {
1672            left: HPOS_A,
1673            top: VPOS_BACKSPACE,
1674            shape: KeyShape::Square,
1675            colour: KeyColour::Green,
1676            label: KeyLabel { text: &["A"] },
1677            code: Code::Near(0o20),
1678        },
1679        Key {
1680            left: HPOS_A + KEY_AND_GAP_WIDTH,
1681            top: VPOS_BACKSPACE,
1682            shape: KeyShape::Square,
1683            colour: KeyColour::Green,
1684            label: KeyLabel { text: &["S"] },
1685            code: Code::Near(0o42),
1686        },
1687        Key {
1688            left: HPOS_A + KEY_AND_GAP_WIDTH * 2.0,
1689            top: VPOS_BACKSPACE,
1690            shape: KeyShape::Square,
1691            colour: KeyColour::Green,
1692            label: KeyLabel { text: &["D"] },
1693            code: Code::Near(0o23),
1694        },
1695        Key {
1696            left: HPOS_A + KEY_AND_GAP_WIDTH * 3.0,
1697            top: VPOS_BACKSPACE,
1698            shape: KeyShape::Square,
1699            colour: KeyColour::Green,
1700            label: KeyLabel { text: &["F"] },
1701            code: Code::Near(0o25),
1702        },
1703        Key {
1704            left: HPOS_A + KEY_AND_GAP_WIDTH * 4.0,
1705            top: VPOS_BACKSPACE,
1706            shape: KeyShape::Square,
1707            colour: KeyColour::Green,
1708            label: KeyLabel { text: &["G"] },
1709            code: Code::Near(0o26),
1710        },
1711        Key {
1712            left: HPOS_A + KEY_AND_GAP_WIDTH * 5.0,
1713            top: VPOS_BACKSPACE,
1714            shape: KeyShape::Square,
1715            colour: KeyColour::Green,
1716            label: KeyLabel { text: &["H"] },
1717            code: Code::Near(0o27),
1718        },
1719        Key {
1720            left: HPOS_A + KEY_AND_GAP_WIDTH * 6.0,
1721            top: VPOS_BACKSPACE,
1722            shape: KeyShape::Square,
1723            colour: KeyColour::Green,
1724            label: KeyLabel { text: &["J"] },
1725            code: Code::Near(0o31),
1726        },
1727        Key {
1728            left: HPOS_A + KEY_AND_GAP_WIDTH * 7.0,
1729            top: VPOS_BACKSPACE,
1730            shape: KeyShape::Square,
1731            colour: KeyColour::Green,
1732            label: KeyLabel { text: &["K"] },
1733            code: Code::Near(0o32),
1734        },
1735        Key {
1736            left: HPOS_A + KEY_AND_GAP_WIDTH * 8.0,
1737            top: VPOS_BACKSPACE,
1738            shape: KeyShape::Square,
1739            colour: KeyColour::Green,
1740            label: KeyLabel { text: &["L"] },
1741            code: Code::Near(0o33),
1742        },
1743        Key {
1744            left: HPOS_A + KEY_AND_GAP_WIDTH * 9.0,
1745            top: VPOS_BACKSPACE,
1746            shape: KeyShape::Square,
1747            colour: KeyColour::Yellow,
1748            label: KeyLabel { text: &["+"] },
1749            code: Code::Near(0o54),
1750        },
1751        Key {
1752            left: HPOS_A + KEY_AND_GAP_WIDTH * 10.0,
1753            top: VPOS_BACKSPACE,
1754            shape: KeyShape::Square,
1755            colour: KeyColour::Yellow,
1756            label: KeyLabel { text: &["-"] },
1757            code: Code::Near(0o55),
1758        },
1759        Key {
1760            left: HPOS_A + KEY_AND_GAP_WIDTH * 11.0,
1761            top: VPOS_BACKSPACE,
1762            shape: KeyShape::Wide,
1763            colour: KeyColour::Grey,
1764            label: KeyLabel { text: &["NORMAL"] },
1765            code: Code::Near(0o65),
1766        },
1767    ]
1768}
1769
1770fn row7() -> &'static [Key] {
1771    &[
1772        Key /* RETURN */ {
1773            left: HPOS_RETURN,
1774            top: VPOS_RETURN,
1775            shape: KeyShape::Wide,
1776            colour: KeyColour::Black,
1777            label: KeyLabel {
1778                text: &["RETURN"]
1779            },
1780            code: Code::Near(0o60),
1781        },
1782        Key {
1783            left: HPOS_Z,
1784            top: VPOS_RETURN,
1785            shape: KeyShape::Square,
1786            colour: KeyColour::Green,
1787            label: KeyLabel { text: &["Z"] },
1788            code: Code::Near(0o51),
1789        },
1790        Key {
1791            left: HPOS_Z + KEY_AND_GAP_WIDTH,
1792            top: VPOS_RETURN,
1793            shape: KeyShape::Square,
1794            colour: KeyColour::Green,
1795            label: KeyLabel { text: &["X"] },
1796            code: Code::Near(0o47),
1797        },
1798        Key {
1799            left: HPOS_Z + KEY_AND_GAP_WIDTH * 2.0,
1800            top: VPOS_RETURN,
1801            shape: KeyShape::Square,
1802            colour: KeyColour::Green,
1803            label: KeyLabel { text: &["C"] },
1804            code: Code::Near(0o22),
1805        },
1806        Key {
1807            left: HPOS_Z + KEY_AND_GAP_WIDTH * 3.0,
1808            top: VPOS_RETURN,
1809            shape: KeyShape::Square,
1810            colour: KeyColour::Green,
1811            label: KeyLabel { text: &["V"] },
1812            code: Code::Near(0o45),
1813        },
1814        Key {
1815            left: HPOS_Z + KEY_AND_GAP_WIDTH * 4.0,
1816            top: VPOS_RETURN,
1817            shape: KeyShape::Square,
1818            colour: KeyColour::Green,
1819            label: KeyLabel { text: &["B"] },
1820            code: Code::Near(0o21),
1821        },
1822        Key {
1823            left: HPOS_Z + KEY_AND_GAP_WIDTH * 5.0,
1824            top: VPOS_RETURN,
1825            shape: KeyShape::Square,
1826            colour: KeyColour::Green,
1827            label: KeyLabel { text: &["N"] },
1828            code: Code::Near(0o35),
1829        },
1830        Key {
1831            left: HPOS_Z + KEY_AND_GAP_WIDTH * 6.0,
1832            top: VPOS_RETURN,
1833            shape: KeyShape::Square,
1834            colour: KeyColour::Green,
1835            label: KeyLabel { text: &["M"] },
1836            code: Code::Near(0o34),
1837        },
1838        Key {
1839            left: HPOS_Z + KEY_AND_GAP_WIDTH * 7.0,
1840            top: VPOS_RETURN,
1841            shape: KeyShape::Square,
1842            colour: KeyColour::Red,
1843            label: KeyLabel { text: &["("] },
1844            code: Code::Near(0o52),
1845        },
1846        Key {
1847            left: HPOS_Z + KEY_AND_GAP_WIDTH * 8.0,
1848            top: VPOS_RETURN,
1849            shape: KeyShape::Square,
1850            colour: KeyColour::Red,
1851            label: KeyLabel { text: &[")"] },
1852            code: Code::Near(0o53),
1853        },
1854        Key {
1855            left: HPOS_Z + KEY_AND_GAP_WIDTH * 9.0,
1856            top: VPOS_RETURN,
1857            shape: KeyShape::Square,
1858            colour: KeyColour::Red,
1859            label: KeyLabel { text: &[","] },
1860            code: Code::Near(0o56),
1861        },
1862        Key {
1863            left: HPOS_Z + KEY_AND_GAP_WIDTH * 10.0,
1864            top: VPOS_RETURN,
1865            shape: KeyShape::Wide,
1866            colour: KeyColour::Black,
1867            label: KeyLabel { text: &["SUB"] },
1868            code: Code::Near(0o66),
1869        },
1870    ]
1871}
1872
1873fn row8() -> &'static [Key] {
1874    &[Key /* space bar */ {
1875            left: HPOS_SPACE_BAR,
1876            top: VPOS_SPACE_BAR,
1877            shape: KeyShape::Spacebar,
1878            colour: KeyColour::Grey,
1879            label: KeyLabel {
1880                text: &[]
1881            },
1882            code: Code::Near(0o70),
1883        }]
1884}
1885
1886fn all_keys() -> impl Iterator<Item = &'static Key> {
1887    row0()
1888        .iter()
1889        .chain(row1().iter())
1890        .chain(row2().iter())
1891        .chain(row3().iter())
1892        .chain(row4().iter())
1893        .chain(row5().iter())
1894        .chain(row6().iter())
1895        .chain(row7().iter())
1896        .chain(row8().iter())
1897}
1898
1899#[test]
1900fn known_keys_have_unique_codes() {
1901    let mut codes_seen: HashMap<Code, &'static Key> = HashMap::new();
1902    for key in all_keys() {
1903        if key.code == Code::Unknown {
1904            // This is a key for which the code mapping is not known.
1905            // Since there is more than one such key, we do not apply
1906            // the uniqueness constraint to them.
1907            continue;
1908        }
1909        if let Some(previous) = codes_seen.get(&key.code) {
1910            panic!(
1911                "duplicate key code {:?} was used for both {:?} and {:?}",
1912                key.code, key, *previous
1913            );
1914        }
1915        codes_seen.insert(key.code, key);
1916    }
1917}
1918
1919#[cfg(test)]
1920fn key_code_frequencies() -> HashMap<u8, usize> {
1921    let mut result: HashMap<u8, usize> = HashMap::new();
1922    for key in all_keys() {
1923        match key.code {
1924            Code::Near(n) | Code::Far(n) => {
1925                *result.entry(n).or_insert(0) += 1;
1926            }
1927            Code::Unknown => (),
1928        }
1929    }
1930    result
1931}
1932
1933#[test]
1934fn codes_used_in_both_near_and_far_keyboards() {
1935    // Verify that the expected codes appear once, and other codes
1936    // appear twice (upper and lower case).
1937    let once_keys: HashSet<u8> = (
1938        // READ IN, BEGIN, NO, YES
1939        0o14_u8..=0o17_u8
1940    )
1941        .chain(
1942            // CARRIAGE RETURN, TAB, BACK SPACE, COLOR BLACK, SUPER,
1943            // NORMAL, SUB, COLOR RED, SPACE, WORD EXAM, LINE FEED DOWN,
1944            // LINE FEED UP, LOWER CASE, UPPER CASE, STOP, NULLIFY
1945            0o60..=0o77,
1946        )
1947        .collect();
1948    let key_code_counts = key_code_frequencies();
1949    for (code, actual_count) in key_code_counts.iter() {
1950        let expected_count: usize = if once_keys.contains(code) { 1 } else { 2 };
1951        if *actual_count != expected_count {
1952            panic!(
1953                "expected to see code code {code:o} with frequency of {expected_count} but got frequency of {actual_count}",
1954            );
1955        }
1956    }
1957}
1958
1959#[test]
1960fn all_input_codes_used() {
1961    let key_code_counts = key_code_frequencies();
1962    for code in 0..=0o77 {
1963        match code {
1964            0o72 | 0o73 => {
1965                // LINE FEED DOWN and LINE FEED UP don't appear to be
1966                // generated by keys.  I assume these are used only in
1967                // Lincoln Writer output, not input.
1968            }
1969            0o74 | 0o75 => {
1970                // LOWER CASE and UPPER CASE are generated by the
1971                // keyboard, but the keyboard generates them to
1972                // indicate which keyboard the currently-pressed key
1973                // belongs to.
1974            }
1975            n => {
1976                if !key_code_counts.contains_key(&n) {
1977                    panic!("No key generates code {n:o}");
1978                }
1979            }
1980        }
1981    }
1982}
1983
1984#[test]
1985fn code_round_trips_as_pixel_colour() {
1986    for key in all_keys() {
1987        let colour = key.code.hit_detection_colour();
1988        match Code::hit_detection_colour_to_code(colour.as_str()) {
1989            Ok(Some(code)) => {
1990                assert!(
1991                    code == key.code,
1992                    "expected code {:?}, got code {:?}",
1993                    key.code,
1994                    code
1995                );
1996            }
1997            Ok(None) => {
1998                panic!(
1999                    "Key code {:?} round-tripped as if it were the keyboard's background (colour is {})",
2000                    key.code, colour
2001                );
2002            }
2003            Err(msg) => {
2004                panic!(
2005                    "Key code {:?} mapped to hit detection colour {} but this could not be round-tripped: {}",
2006                    key.code, colour, msg
2007                );
2008            }
2009        }
2010    }
2011}
2012
2013#[test]
2014fn known_keys_consistent_with_base_charset() {
2015    // This test ensures that key code assignments in the keycode
2016    // implementation are consistent with the code concersion logic in
2017    // lincoln_char_to_described_char().
2018    for key in all_keys() {
2019        let (case, code) = match key.code {
2020            Code::Near(code) => (LwKeyboardCase::Lower, code),
2021            Code::Far(code) => (LwKeyboardCase::Upper, code),
2022            Code::Unknown => {
2023                event!(
2024                    Level::WARN,
2025                    "Skipping consistency check for key {key:?} because it has an unknown code",
2026                );
2027                continue;
2028            }
2029        };
2030        let mut state = LincolnState {
2031            script: Script::Normal,
2032            colour: Colour::Black,
2033            case,
2034        };
2035        let code: Unsigned6Bit = match <Unsigned6Bit as std::convert::TryFrom<u8>>::try_from(code) {
2036            Ok(x) => x,
2037            Err(e) => {
2038                panic!("key code for key {key:?} does not fit into 6 bits: {e}");
2039            }
2040        };
2041        match lincoln_char_to_described_char(code, &mut state) {
2042            Some(DescribedChar {
2043                label_matches_unicode: false,
2044                ..
2045            }) => {
2046                // The label on this key isn't supposed to match its
2047                // Unicode representation.  For example LINE FEED is
2048                // Unicode '\n'.
2049            }
2050            Some(described) => {
2051                if let Some(unicode) = described.unicode_representation {
2052                    let unicode_as_string = format!("{unicode}");
2053                    let one_line_text =
2054                        key.label.text.iter().fold(String::new(), |mut acc, line| {
2055                            if !acc.is_empty() {
2056                                acc.push(' ');
2057                            }
2058                            acc.push_str(line);
2059                            acc
2060                        });
2061                    if unicode_as_string != one_line_text {
2062                        panic!(
2063                            "inconsistency for key {key:?}: label is {:?} (on one line: '{one_line_text}') but lincoln_char_to_described_char calls it '{unicode_as_string}'",
2064                            key.label.text
2065                        );
2066                    }
2067                }
2068            }
2069            None => {
2070                // Some codes are not expected to have a mapping in
2071                // lincoln_char_to_described_char.  Some codes we
2072                // classify here as expect_match=true are handled
2073                // above by the label_matches_unicode=false case
2074                // above.  That is, they do have a mapping in
2075                // lincoln_char_to_described_char but they don't have
2076                // a unicode representation.
2077                let expect_match = match u8::from(code) {
2078                    0o00..=0o13 => true,
2079                    0o14..=0o17 => false,
2080                    0o20..=0o57 => true,
2081                    0o60..=0o77 => false,
2082                    0o100..=u8::MAX => unreachable!(),
2083                };
2084                if expect_match {
2085                    panic!("key {key:?} has no support in lincoln_char_to_described_char");
2086                }
2087            }
2088        }
2089    }
2090}
2091
2092fn draw_kb<P: KeyPainter>(painter: &mut P) -> Result<(), KeyPaintError> {
2093    painter.draw_background()?;
2094
2095    let unit_width = painter.width() / 23.8_f32;
2096    let unit_height = painter.height() / 14.5_f32;
2097
2098    for key in all_keys() {
2099        let left = key.left * unit_width;
2100        let top = key.top * unit_width;
2101        let width = key.shape.width() * unit_width;
2102        let height = key.shape.height() * unit_height;
2103        let bounds = BoundingBox {
2104            nw: Point { x: left, y: top },
2105            se: Point {
2106                x: left + width,
2107                y: top + height,
2108            },
2109        };
2110        painter.draw_key(&bounds, &key.colour, &key.label, key.code)?;
2111    }
2112    Ok(())
2113}
2114
2115#[wasm_bindgen]
2116pub fn draw_keyboard(painter: &mut HtmlCanvas2DPainter) -> Result<(), JsValue> {
2117    draw_kb(painter).map_err(|e| e.to_string().into())
2118}