Skip to main content

denise_render/
font.rs

1//! The built-in bitmap font.
2//!
3//! Five pixels wide, seven tall, in an eight-row cell, one row per byte with bit 7
4//! on the left. Monospace, with a six-pixel advance and integer scaling — at 3× on
5//! a 1080p panel that is a comfortable 15×21 px of text, which is what an HMI read
6//! at arm's length actually wants.
7//!
8//! # Why this is here in M3 rather than M4
9//!
10//! M4 owns text properly: `denise-text`, `cosmic-text` behind a feature flag, a
11//! glyph atlas, real shaping and proportional metrics. But a Label, a Button and a
12//! TextInput without glyphs are three rectangles, so the milestone that ships them
13//! needs *some* font. This is the "built-in 8×8 bitmap font" M4 already promised,
14//! brought forward and no more than that. It is deliberately not extensible: there
15//! is no font loading here, and there will not be.
16//!
17//! # Coverage
18//!
19//! Printable ASCII, plus `ÆØÅ æøå ÄÖÜ äöü Éé ß °`. Anything else draws as an empty
20//! box, which is a visible defect rather than a silent gap. Combining marks, RTL
21//! and complex shaping are not supported and cannot be — this is a fixed grid.
22//!
23//! # The art is the source
24//!
25//! Glyphs are written as ASCII art and packed into bits by a `const fn` at compile
26//! time. A hand-maintained table of hex bytes is unreviewable; a picture of a `Ø`
27//! is not.
28
29#[cfg(test)]
30extern crate alloc;
31
32use denise::{Point, Rect, Size};
33
34use crate::blend::Paint;
35use crate::canvas::Canvas;
36
37/// Glyph box width in pixels.
38pub const CELL_WIDTH: i32 = 5;
39/// Glyph cell height in pixels, descender row included.
40pub const CELL_HEIGHT: i32 = 8;
41/// Horizontal distance between glyph origins, at scale 1.
42pub const ADVANCE: i32 = 6;
43/// Vertical distance between baselines, at scale 1.
44pub const LINE_HEIGHT: i32 = 9;
45
46/// One glyph: eight rows of five bits, bit 7 leftmost.
47pub type Glyph = [u8; CELL_HEIGHT as usize];
48
49/// Packs `CELL_WIDTH * CELL_HEIGHT` ASCII bytes into a glyph.
50///
51/// `#` sets a pixel; anything else clears it.
52const fn pack(art: &str) -> Glyph {
53    let bytes = art.as_bytes();
54    assert!(
55        bytes.len() == (CELL_WIDTH * CELL_HEIGHT) as usize,
56        "glyph art must be exactly CELL_WIDTH by CELL_HEIGHT characters"
57    );
58    let mut rows = [0u8; CELL_HEIGHT as usize];
59    let mut y = 0;
60    while y < CELL_HEIGHT as usize {
61        let mut x = 0;
62        while x < CELL_WIDTH as usize {
63            if bytes[y * CELL_WIDTH as usize + x] == b'#' {
64                rows[y] |= 0x80 >> x;
65            }
66            x += 1;
67        }
68        y += 1;
69    }
70    rows
71}
72
73/// A fixed-pitch bitmap font.
74#[derive(Clone, Copy, Debug)]
75pub struct BitmapFont {
76    ascii: &'static [Glyph; 95],
77    /// Sorted by code point.
78    extras: &'static [(char, Glyph)],
79    fallback: Glyph,
80}
81
82/// The one font that ships with Denise.
83pub static BUILT_IN: BitmapFont = BitmapFont {
84    ascii: &ASCII,
85    extras: &EXTRAS,
86    fallback: pack(concat!(
87        "#####", "#...#", "#...#", "#...#", "#...#", "#...#", "#####", ".....",
88    )),
89};
90
91impl BitmapFont {
92    /// The glyph for `ch`, or the missing-character box.
93    pub fn glyph(&self, ch: char) -> &Glyph {
94        let code = ch as u32;
95        if (0x20..0x7F).contains(&code) {
96            return &self.ascii[(code - 0x20) as usize];
97        }
98        match self.extras.binary_search_by_key(&ch, |&(c, _)| c) {
99            Ok(index) => &self.extras[index].1,
100            Err(_) => &self.fallback,
101        }
102    }
103
104    /// Returns `true` if `ch` has a glyph of its own.
105    pub fn contains(&self, ch: char) -> bool {
106        let code = ch as u32;
107        (0x20..0x7F).contains(&code) || self.extras.binary_search_by_key(&ch, |&(c, _)| c).is_ok()
108    }
109
110    /// Width of one line of text, excluding the trailing inter-glyph gap.
111    ///
112    /// The gap is excluded so that centring text in a button actually centres the
113    /// ink rather than leaving it a pixel left of true.
114    pub fn line_width(&self, line: &str, scale: i32) -> i32 {
115        let scale = scale.max(1);
116        let count = line.chars().count() as i32;
117        if count == 0 {
118            0
119        } else {
120            (count * ADVANCE - (ADVANCE - CELL_WIDTH)) * scale
121        }
122    }
123
124    /// Extent of `text`, honouring `\n`.
125    pub fn measure(&self, text: &str, scale: i32) -> Size {
126        let scale = scale.max(1);
127        let mut widest = 0;
128        let mut lines = 0;
129        for line in text.split('\n') {
130            widest = widest.max(self.line_width(line, scale));
131            lines += 1;
132        }
133        Size::new(
134            widest.max(0) as u32,
135            ((lines - 1) * LINE_HEIGHT * scale + CELL_HEIGHT * scale).max(0) as u32,
136        )
137    }
138
139    /// Horizontal offset of the glyph at character index `index`.
140    ///
141    /// Fixed pitch, so this is multiplication rather than a layout pass — which is
142    /// exactly why a text field's caret arithmetic is trivial here and will stop
143    /// being trivial when M4 brings proportional fonts.
144    #[inline]
145    pub const fn caret_offset(&self, index: usize, scale: i32) -> i32 {
146        index as i32 * ADVANCE * if scale > 1 { scale } else { 1 }
147    }
148}
149
150impl Canvas<'_> {
151    /// Draws one glyph with its cell's top-left corner at `at`.
152    pub fn draw_glyph(&mut self, glyph: &Glyph, at: Point, scale: i32, color: impl Into<Paint>) {
153        let scale = scale.max(1);
154        let cell = Rect::new(at.x, at.y, CELL_WIDTH * scale, CELL_HEIGHT * scale);
155        if self.visible(cell).is_none() {
156            return;
157        }
158        let paint = color.into();
159        for (row, bits) in glyph.iter().enumerate() {
160            if *bits == 0 {
161                continue;
162            }
163            let y = at.y + row as i32 * scale;
164            // Runs of set bits blit as one span. The per-pixel path measured
165            // fifteen times slower than the span path on a Pi 3, and glyphs are
166            // where that difference will be paid most often.
167            let mut x = 0;
168            while x < CELL_WIDTH {
169                if bits & (0x80 >> x) == 0 {
170                    x += 1;
171                    continue;
172                }
173                let mut end = x + 1;
174                while end < CELL_WIDTH && bits & (0x80 >> end) != 0 {
175                    end += 1;
176                }
177                self.fill_rect(
178                    Rect::new(at.x + x * scale, y, (end - x) * scale, scale),
179                    paint,
180                );
181                x = end;
182            }
183        }
184    }
185
186    /// Draws `text` with the first cell's top-left corner at `at`, honouring `\n`.
187    ///
188    /// Returns the extent actually laid out, whether or not it was clipped.
189    pub fn draw_text(
190        &mut self,
191        font: &BitmapFont,
192        at: Point,
193        scale: i32,
194        text: &str,
195        color: impl Into<Paint>,
196    ) -> Size {
197        let scale = scale.max(1);
198        let paint = color.into();
199        let mut pen = at;
200        for ch in text.chars() {
201            if ch == '\n' {
202                pen = Point::new(at.x, pen.y + LINE_HEIGHT * scale);
203                continue;
204            }
205            self.draw_glyph(font.glyph(ch), pen, scale, paint);
206            pen.x += ADVANCE * scale;
207        }
208        font.measure(text, scale)
209    }
210}
211
212/// Printable ASCII, `0x20..=0x7E`, in code-point order.
213const ASCII_ART: [&str; 95] = [
214    //
215    concat!(
216        ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....",
217    ),
218    // !
219    concat!(
220        "..#..", "..#..", "..#..", "..#..", "..#..", ".....", "..#..", ".....",
221    ),
222    // double quote
223    concat!(
224        ".#.#.", ".#.#.", ".....", ".....", ".....", ".....", ".....", ".....",
225    ),
226    // #
227    concat!(
228        ".#.#.", ".#.#.", "#####", ".#.#.", "#####", ".#.#.", ".#.#.", ".....",
229    ),
230    // $
231    concat!(
232        "..#..", ".####", "#.#..", ".###.", "..#.#", "####.", "..#..", ".....",
233    ),
234    // %
235    concat!(
236        "##...", "##..#", "...#.", "..#..", ".#...", "#..##", "...##", ".....",
237    ),
238    // &
239    concat!(
240        ".##..", "#..#.", "#.#..", ".#...", "#.#.#", "#..#.", ".##.#", ".....",
241    ),
242    // '
243    concat!(
244        "..#..", "..#..", ".....", ".....", ".....", ".....", ".....", ".....",
245    ),
246    // (
247    concat!(
248        "...#.", "..#..", ".#...", ".#...", ".#...", "..#..", "...#.", ".....",
249    ),
250    // )
251    concat!(
252        ".#...", "..#..", "...#.", "...#.", "...#.", "..#..", ".#...", ".....",
253    ),
254    // *
255    concat!(
256        ".....", "#.#.#", ".###.", "#####", ".###.", "#.#.#", ".....", ".....",
257    ),
258    // +
259    concat!(
260        ".....", "..#..", "..#..", "#####", "..#..", "..#..", ".....", ".....",
261    ),
262    // ,
263    concat!(
264        ".....", ".....", ".....", ".....", ".....", "..##.", "..#..", ".#...",
265    ),
266    // -
267    concat!(
268        ".....", ".....", ".....", ".###.", ".....", ".....", ".....", ".....",
269    ),
270    // .
271    concat!(
272        ".....", ".....", ".....", ".....", ".....", ".##..", ".##..", ".....",
273    ),
274    // /
275    concat!(
276        "....#", "....#", "...#.", "..#..", ".#...", "#....", "#....", ".....",
277    ),
278    // 0
279    concat!(
280        ".###.", "#...#", "#..##", "#.#.#", "##..#", "#...#", ".###.", ".....",
281    ),
282    // 1
283    concat!(
284        "..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###.", ".....",
285    ),
286    // 2
287    concat!(
288        ".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####", ".....",
289    ),
290    // 3
291    concat!(
292        "#####", "...#.", "..#..", "...#.", "....#", "#...#", ".###.", ".....",
293    ),
294    // 4
295    concat!(
296        "...#.", "..##.", ".#.#.", "#..#.", "#####", "...#.", "...#.", ".....",
297    ),
298    // 5
299    concat!(
300        "#####", "#....", "####.", "....#", "....#", "#...#", ".###.", ".....",
301    ),
302    // 6
303    concat!(
304        "..##.", ".#...", "#....", "####.", "#...#", "#...#", ".###.", ".....",
305    ),
306    // 7
307    concat!(
308        "#####", "....#", "...#.", "..#..", ".#...", ".#...", ".#...", ".....",
309    ),
310    // 8
311    concat!(
312        ".###.", "#...#", "#...#", ".###.", "#...#", "#...#", ".###.", ".....",
313    ),
314    // 9
315    concat!(
316        ".###.", "#...#", "#...#", ".####", "....#", "...#.", ".##..", ".....",
317    ),
318    // :
319    concat!(
320        ".....", ".##..", ".##..", ".....", ".##..", ".##..", ".....", ".....",
321    ),
322    // ;
323    concat!(
324        ".....", ".##..", ".##..", ".....", ".##..", "..#..", ".#...", ".....",
325    ),
326    // <
327    concat!(
328        "...#.", "..#..", ".#...", "#....", ".#...", "..#..", "...#.", ".....",
329    ),
330    // =
331    concat!(
332        ".....", ".....", "#####", ".....", "#####", ".....", ".....", ".....",
333    ),
334    // >
335    concat!(
336        ".#...", "..#..", "...#.", "....#", "...#.", "..#..", ".#...", ".....",
337    ),
338    // ?
339    concat!(
340        ".###.", "#...#", "....#", "...#.", "..#..", ".....", "..#..", ".....",
341    ),
342    // @
343    concat!(
344        ".###.", "#...#", "#.###", "#.#.#", "#.###", "#....", ".###.", ".....",
345    ),
346    // A
347    concat!(
348        ".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#", ".....",
349    ),
350    // B
351    concat!(
352        "####.", "#...#", "#...#", "####.", "#...#", "#...#", "####.", ".....",
353    ),
354    // C
355    concat!(
356        ".###.", "#...#", "#....", "#....", "#....", "#...#", ".###.", ".....",
357    ),
358    // D
359    concat!(
360        "####.", "#...#", "#...#", "#...#", "#...#", "#...#", "####.", ".....",
361    ),
362    // E
363    concat!(
364        "#####", "#....", "#....", "####.", "#....", "#....", "#####", ".....",
365    ),
366    // F
367    concat!(
368        "#####", "#....", "#....", "####.", "#....", "#....", "#....", ".....",
369    ),
370    // G
371    concat!(
372        ".###.", "#...#", "#....", "#.###", "#...#", "#...#", ".###.", ".....",
373    ),
374    // H
375    concat!(
376        "#...#", "#...#", "#...#", "#####", "#...#", "#...#", "#...#", ".....",
377    ),
378    // I
379    concat!(
380        ".###.", "..#..", "..#..", "..#..", "..#..", "..#..", ".###.", ".....",
381    ),
382    // J
383    concat!(
384        "..###", "...#.", "...#.", "...#.", "...#.", "#..#.", ".##..", ".....",
385    ),
386    // K
387    concat!(
388        "#...#", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "#...#", ".....",
389    ),
390    // L
391    concat!(
392        "#....", "#....", "#....", "#....", "#....", "#....", "#####", ".....",
393    ),
394    // M
395    concat!(
396        "#...#", "##.##", "#.#.#", "#.#.#", "#...#", "#...#", "#...#", ".....",
397    ),
398    // N
399    concat!(
400        "#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "#...#", ".....",
401    ),
402    // O
403    concat!(
404        ".###.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###.", ".....",
405    ),
406    // P
407    concat!(
408        "####.", "#...#", "#...#", "####.", "#....", "#....", "#....", ".....",
409    ),
410    // Q
411    concat!(
412        ".###.", "#...#", "#...#", "#...#", "#.#.#", "#..#.", ".##.#", ".....",
413    ),
414    // R
415    concat!(
416        "####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#", ".....",
417    ),
418    // S
419    concat!(
420        ".####", "#....", "#....", ".###.", "....#", "....#", "####.", ".....",
421    ),
422    // T
423    concat!(
424        "#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", ".....",
425    ),
426    // U
427    concat!(
428        "#...#", "#...#", "#...#", "#...#", "#...#", "#...#", ".###.", ".....",
429    ),
430    // V
431    concat!(
432        "#...#", "#...#", "#...#", "#...#", "#...#", ".#.#.", "..#..", ".....",
433    ),
434    // W
435    concat!(
436        "#...#", "#...#", "#...#", "#.#.#", "#.#.#", "##.##", "#...#", ".....",
437    ),
438    // X
439    concat!(
440        "#...#", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "#...#", ".....",
441    ),
442    // Y
443    concat!(
444        "#...#", "#...#", ".#.#.", "..#..", "..#..", "..#..", "..#..", ".....",
445    ),
446    // Z
447    concat!(
448        "#####", "....#", "...#.", "..#..", ".#...", "#....", "#####", ".....",
449    ),
450    // [
451    concat!(
452        ".###.", ".#...", ".#...", ".#...", ".#...", ".#...", ".###.", ".....",
453    ),
454    // backslash
455    concat!(
456        "#....", "#....", ".#...", "..#..", "...#.", "....#", "....#", ".....",
457    ),
458    // ]
459    concat!(
460        ".###.", "...#.", "...#.", "...#.", "...#.", "...#.", ".###.", ".....",
461    ),
462    // ^
463    concat!(
464        "..#..", ".#.#.", "#...#", ".....", ".....", ".....", ".....", ".....",
465    ),
466    // _
467    concat!(
468        ".....", ".....", ".....", ".....", ".....", ".....", ".....", "#####",
469    ),
470    // `
471    concat!(
472        "..#..", "...#.", ".....", ".....", ".....", ".....", ".....", ".....",
473    ),
474    // a
475    concat!(
476        ".....", ".....", ".###.", "....#", ".####", "#...#", ".####", ".....",
477    ),
478    // b
479    concat!(
480        "#....", "#....", "####.", "#...#", "#...#", "#...#", "####.", ".....",
481    ),
482    // c
483    concat!(
484        ".....", ".....", ".###.", "#....", "#....", "#....", ".###.", ".....",
485    ),
486    // d
487    concat!(
488        "....#", "....#", ".####", "#...#", "#...#", "#...#", ".####", ".....",
489    ),
490    // e
491    concat!(
492        ".....", ".....", ".###.", "#...#", "#####", "#....", ".###.", ".....",
493    ),
494    // f
495    concat!(
496        "..##.", ".#...", ".#...", "####.", ".#...", ".#...", ".#...", ".....",
497    ),
498    // g
499    concat!(
500        ".....", ".....", ".####", "#...#", "#...#", ".####", "....#", ".###.",
501    ),
502    // h
503    concat!(
504        "#....", "#....", "####.", "#...#", "#...#", "#...#", "#...#", ".....",
505    ),
506    // i
507    concat!(
508        "..#..", ".....", ".##..", "..#..", "..#..", "..#..", ".###.", ".....",
509    ),
510    // j
511    concat!(
512        "...#.", ".....", "..##.", "...#.", "...#.", "...#.", "#..#.", ".##..",
513    ),
514    // k
515    concat!(
516        "#....", "#....", "#..#.", "#.#..", "##...", "#.#..", "#..#.", ".....",
517    ),
518    // l
519    concat!(
520        ".##..", "..#..", "..#..", "..#..", "..#..", "..#..", ".###.", ".....",
521    ),
522    // m
523    concat!(
524        ".....", ".....", "##.#.", "#.#.#", "#.#.#", "#...#", "#...#", ".....",
525    ),
526    // n
527    concat!(
528        ".....", ".....", "####.", "#...#", "#...#", "#...#", "#...#", ".....",
529    ),
530    // o
531    concat!(
532        ".....", ".....", ".###.", "#...#", "#...#", "#...#", ".###.", ".....",
533    ),
534    // p
535    concat!(
536        ".....", ".....", "####.", "#...#", "#...#", "####.", "#....", "#....",
537    ),
538    // q
539    concat!(
540        ".....", ".....", ".####", "#...#", "#...#", ".####", "....#", "....#",
541    ),
542    // r
543    concat!(
544        ".....", ".....", "#.##.", "##..#", "#....", "#....", "#....", ".....",
545    ),
546    // s
547    concat!(
548        ".....", ".....", ".####", "#....", ".###.", "....#", "####.", ".....",
549    ),
550    // t
551    concat!(
552        ".#...", ".#...", "####.", ".#...", ".#...", ".#..#", "..##.", ".....",
553    ),
554    // u
555    concat!(
556        ".....", ".....", "#...#", "#...#", "#...#", "#..##", ".##.#", ".....",
557    ),
558    // v
559    concat!(
560        ".....", ".....", "#...#", "#...#", "#...#", ".#.#.", "..#..", ".....",
561    ),
562    // w
563    concat!(
564        ".....", ".....", "#...#", "#...#", "#.#.#", "#.#.#", ".#.#.", ".....",
565    ),
566    // x
567    concat!(
568        ".....", ".....", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", ".....",
569    ),
570    // y
571    concat!(
572        ".....", ".....", "#...#", "#...#", "#...#", ".####", "....#", ".###.",
573    ),
574    // z
575    concat!(
576        ".....", ".....", "#####", "...#.", "..#..", ".#...", "#####", ".....",
577    ),
578    // {
579    concat!(
580        "...##", "..#..", "..#..", ".#...", "..#..", "..#..", "...##", ".....",
581    ),
582    // |
583    concat!(
584        "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", ".....",
585    ),
586    // }
587    concat!(
588        "##...", "..#..", "..#..", "...#.", "..#..", "..#..", "##...", ".....",
589    ),
590    // ~
591    concat!(
592        ".....", ".....", ".#..#", "#.#.#", "#..#.", ".....", ".....", ".....",
593    ),
594];
595
596/// Glyphs outside ASCII, **sorted by code point** so lookup can bisect.
597const EXTRA_ART: [(char, &str); 23] = [
598    // «
599    (
600        '\u{00ab}',
601        concat!(
602            ".....", ".....", "..#.#", ".#.#.", "#.#..", ".#.#.", "..#.#", ".....",
603        ),
604    ),
605    // °
606    (
607        '\u{00b0}',
608        concat!(
609            ".##..", "#..#.", ".##..", ".....", ".....", ".....", ".....", ".....",
610        ),
611    ),
612    // ±
613    (
614        '\u{00b1}',
615        concat!(
616            "..#..", "..#..", "#####", "..#..", "..#..", ".....", "#####", ".....",
617        ),
618    ),
619    // µ
620    (
621        '\u{00b5}',
622        concat!(
623            ".....", ".....", "#...#", "#...#", "#...#", "#..##", "#.##.", "#....",
624        ),
625    ),
626    // »
627    (
628        '\u{00bb}',
629        concat!(
630            ".....", ".....", "#.#..", ".#.#.", "..#.#", ".#.#.", "#.#..", ".....",
631        ),
632    ),
633    // Ä
634    (
635        '\u{00c4}',
636        concat!(
637            ".#.#.", ".###.", "#...#", "#...#", "#####", "#...#", "#...#", ".....",
638        ),
639    ),
640    // Å
641    (
642        '\u{00c5}',
643        concat!(
644            "..#..", ".###.", "#...#", "#...#", "#####", "#...#", "#...#", ".....",
645        ),
646    ),
647    // Æ
648    (
649        '\u{00c6}',
650        concat!(
651            ".####", "#.#..", "#.#..", "#####", "#.#..", "#.#..", "#.###", ".....",
652        ),
653    ),
654    // É
655    (
656        '\u{00c9}',
657        concat!(
658            "...#.", "#####", "#....", "####.", "#....", "#....", "#####", ".....",
659        ),
660    ),
661    // Ö
662    (
663        '\u{00d6}',
664        concat!(
665            ".#.#.", ".###.", "#...#", "#...#", "#...#", "#...#", ".###.", ".....",
666        ),
667    ),
668    // ×
669    (
670        '\u{00d7}',
671        concat!(
672            ".....", ".....", ".....", ".#.#.", "..#..", ".#.#.", ".....", ".....",
673        ),
674    ),
675    // Ø
676    (
677        '\u{00d8}',
678        concat!(
679            ".####", "#..##", "#..##", "#.#.#", "##..#", "##..#", "####.", ".....",
680        ),
681    ),
682    // Ü
683    (
684        '\u{00dc}',
685        concat!(
686            ".#.#.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###.", ".....",
687        ),
688    ),
689    // ß
690    (
691        '\u{00df}',
692        concat!(
693            ".....", ".##..", "#..#.", "#.#..", "#..#.", "#..#.", "#.##.", ".....",
694        ),
695    ),
696    // ä
697    (
698        '\u{00e4}',
699        concat!(
700            ".#.#.", ".....", ".###.", "....#", ".####", "#...#", ".####", ".....",
701        ),
702    ),
703    // å
704    (
705        '\u{00e5}',
706        concat!(
707            "..#..", ".....", ".###.", "....#", ".####", "#...#", ".####", ".....",
708        ),
709    ),
710    // æ
711    (
712        '\u{00e6}',
713        concat!(
714            ".....", ".....", "##.##", "..#.#", ".####", "#.#..", ".####", ".....",
715        ),
716    ),
717    // é
718    (
719        '\u{00e9}',
720        concat!(
721            "...#.", ".....", ".###.", "#...#", "#####", "#....", ".###.", ".....",
722        ),
723    ),
724    // ö
725    (
726        '\u{00f6}',
727        concat!(
728            ".#.#.", ".....", ".###.", "#...#", "#...#", "#...#", ".###.", ".....",
729        ),
730    ),
731    // ø
732    (
733        '\u{00f8}',
734        concat!(
735            ".....", ".....", ".####", "#..##", "#.#.#", "##..#", "####.", ".....",
736        ),
737    ),
738    // ü
739    (
740        '\u{00fc}',
741        concat!(
742            ".#.#.", ".....", "#...#", "#...#", "#...#", "#..##", ".##.#", ".....",
743        ),
744    ),
745    // en dash
746    (
747        '\u{2013}',
748        concat!(
749            ".....", ".....", ".....", "#####", ".....", ".....", ".....", ".....",
750        ),
751    ),
752    // em dash
753    (
754        '\u{2014}',
755        concat!(
756            ".....", ".....", ".....", "#####", ".....", ".....", ".....", ".....",
757        ),
758    ),
759];
760
761const ASCII: [Glyph; 95] = {
762    let mut packed = [[0u8; CELL_HEIGHT as usize]; 95];
763    let mut i = 0;
764    while i < 95 {
765        packed[i] = pack(ASCII_ART[i]);
766        i += 1;
767    }
768    packed
769};
770
771const EXTRAS: [(char, Glyph); EXTRA_ART.len()] = {
772    let mut packed = [('\0', [0u8; CELL_HEIGHT as usize]); EXTRA_ART.len()];
773    let mut i = 0;
774    while i < EXTRA_ART.len() {
775        packed[i] = (EXTRA_ART[i].0, pack(EXTRA_ART[i].1));
776        // Lookup bisects, so an unsorted table would silently miss glyphs.
777        assert!(
778            i == 0 || (EXTRA_ART[i - 1].0 as u32) < (EXTRA_ART[i].0 as u32),
779            "EXTRA_ART must be sorted by code point"
780        );
781        i += 1;
782    }
783    packed
784};
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789    use crate::testing::TestCanvas;
790    use denise::Color;
791
792    /// Pairs that genuinely cannot be told apart in five columns. Anything not
793    /// listed here being identical is a mistake in the art, not a limit of the
794    /// grid.
795    const TIED: [(char, char); 1] = [('\u{2013}', '\u{2014}')];
796
797    #[test]
798    fn every_glyph_is_distinct() {
799        // Catches the copy-paste that leaves `Q` looking exactly like `O`, which is
800        // the failure mode of hand-authored bitmap art and is otherwise only found
801        // by someone squinting at a panel.
802        let mut seen: alloc::vec::Vec<(char, Glyph)> = alloc::vec::Vec::new();
803        let chars = (0x20u32..0x7F)
804            .filter_map(char::from_u32)
805            .chain(EXTRAS.iter().map(|&(c, _)| c));
806        for ch in chars {
807            let glyph = *BUILT_IN.glyph(ch);
808            if ch == ' ' {
809                assert_eq!(glyph, [0; 8], "space must be blank");
810                continue;
811            }
812            assert_ne!(glyph, [0; 8], "{ch:?} has no ink");
813            if let Some((other, _)) = seen.iter().find(|(_, g)| *g == glyph) {
814                assert!(
815                    TIED.contains(&(*other, ch)),
816                    "{ch:?} and {other:?} are the same picture"
817                );
818            }
819            seen.push((ch, glyph));
820        }
821    }
822
823    #[test]
824    fn nordic_letters_are_present_and_ascii_is_complete() {
825        for ch in "ÆØÅæøåÄÖÜäöüÉéß°".chars() {
826            assert!(BUILT_IN.contains(ch), "{ch:?} is missing");
827        }
828        for code in 0x20u32..0x7F {
829            let ch = char::from_u32(code).expect("ascii");
830            assert!(BUILT_IN.contains(ch), "{ch:?} is missing");
831        }
832    }
833
834    #[test]
835    fn an_unmapped_character_draws_a_visible_box() {
836        assert!(!BUILT_IN.contains('\u{4e2d}'));
837        assert_eq!(*BUILT_IN.glyph('\u{4e2d}'), BUILT_IN.fallback);
838        assert_ne!(BUILT_IN.fallback, [0; 8], "a missing glyph must be visible");
839    }
840
841    #[test]
842    fn extras_are_sorted_so_lookup_can_bisect() {
843        assert!(EXTRAS.windows(2).all(|w| w[0].0 < w[1].0));
844    }
845
846    #[test]
847    fn measurement_matches_what_is_drawn() {
848        let size = BUILT_IN.measure("Hi", 2);
849        // Two glyphs: 5 + gap 1 + 5 = 11 columns at 2×.
850        assert_eq!(size, Size::new(22, 16));
851        assert_eq!(BUILT_IN.measure("", 1), Size::new(0, 8));
852        assert_eq!(BUILT_IN.measure("a\nbb", 1).height, LINE_HEIGHT as u32 + 8);
853        assert_eq!(BUILT_IN.measure("a\nbb", 1).width, 11);
854    }
855
856    #[test]
857    fn scale_is_clamped_to_at_least_one() {
858        assert_eq!(BUILT_IN.line_width("abc", 0), BUILT_IN.line_width("abc", 1));
859        assert_eq!(BUILT_IN.caret_offset(3, -4), BUILT_IN.caret_offset(3, 1));
860    }
861
862    #[test]
863    fn text_stays_inside_the_rectangle_it_measures() {
864        let mut t = TestCanvas::new(80, 40);
865        let text = "Wg|";
866        let scale = 2;
867        let extent = {
868            let mut c = t.canvas();
869            c.draw_text(&BUILT_IN, Point::new(4, 4), scale, text, Color::WHITE)
870        };
871        let bounds = Rect::new(4, 4, extent.width as i32, extent.height as i32);
872        for y in 0..40i32 {
873            for x in 0..80i32 {
874                if !bounds.contains(Point::new(x, y)) {
875                    assert_eq!(
876                        t.pixels()[(y * 80 + x) as usize],
877                        0,
878                        "ink outside the measured extent at {x},{y}"
879                    );
880                }
881            }
882        }
883    }
884
885    #[test]
886    fn drawing_is_clipped_like_everything_else() {
887        let mut t = TestCanvas::new(64, 16);
888        {
889            let mut c = t.canvas();
890            let mut clipped = c.with_clip(Rect::new(0, 0, 12, 16));
891            clipped.draw_text(&BUILT_IN, Point::new(0, 0), 1, "MMMMMMMM", Color::WHITE);
892        }
893        for y in 0..16usize {
894            for x in 12..64usize {
895                assert_eq!(t.pixels()[y * 64 + x], 0, "drew past the clip at {x},{y}");
896            }
897        }
898        assert!(
899            t.pixels().iter().any(|&p| p != 0),
900            "nothing was drawn at all"
901        );
902    }
903
904    #[test]
905    fn a_newline_starts_a_second_line() {
906        let mut t = TestCanvas::new(40, 32);
907        {
908            let mut c = t.canvas();
909            c.draw_text(&BUILT_IN, Point::new(0, 0), 1, "A\nA", Color::WHITE);
910        }
911        let row_of = |y: usize| t.pixels()[y * 40..y * 40 + 5].to_vec();
912        assert_eq!(row_of(1), row_of(1 + LINE_HEIGHT as usize));
913    }
914}