1use 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
151const 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
163const 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
176const 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
189const 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", 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 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; pub const SWITCH_TO_NEAR: u8 = 0o75; impl 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), (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 #[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#[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)), (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
536fn 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 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(()); }
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 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 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 ¤t_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; }
790 Some(KeyPaintError::TextTooBig { msg: _, lines: _ }) => {
791 continue; }
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 const HPOS_YES: f32 = HPOS_DELETE + WIDE_KEY_AND_GAP_WIDTH * 2.0;
824 &[
825 Key {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 },
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}"], },
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}"], },
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}"], },
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}"], },
999 code: Code::Far(0o01),
1000 },
1001 Key {
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 {
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 {
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 {
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 {
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}"], },
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}"], },
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}"], },
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
1104fn 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: &["~"], },
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}"], },
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}"], },
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 {
1159 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 {
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 {
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 {
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}"], },
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}"], },
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 {
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}"], },
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}"], },
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}"], },
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 {
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 {
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 {
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 {
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}"], },
1371 code: Code::Far(0o46),
1372 },
1373 Key {
1374 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} "], },
1382 code: Code::Far(0o12),
1383 },
1384 Key {
1385 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} "], },
1393 code: Code::Far(0o13),
1394 },
1395 ]
1396}
1397
1398fn row4() -> &'static [Key] {
1399 &[
1401 Key {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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} "], },
1529 code: Code::Near(0o12),
1530 },
1531 Key {
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} ", ]
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 {
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 {
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 {
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 {
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 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 let once_keys: HashSet<u8> = (
1938 0o14_u8..=0o17_u8
1940 )
1941 .chain(
1942 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 }
1969 0o74 | 0o75 => {
1970 }
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 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 }
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 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}