Skip to main content

pixel8_runtime/
font.rs

1//! Pixel8's built-in pixel font.
2//!
3//! An original 3-pixel-wide glyph set in a 4x7 cell. Uppercase fills the full
4//! cap height; lowercase are shorter x-height glyphs with true ascenders and
5//! descenders, so case reads correctly in case-sensitive Rust source. Each
6//! glyph is six rows; in every row the leftmost pixel is bit 2 (`0b100`).
7
8/// Advance width of one character cell in pixels.
9pub const GLYPH_W: i32 = 4;
10/// Line height in pixels.
11pub const GLYPH_H: i32 = 7;
12
13/// Glyph used for characters outside the printable ASCII range.
14pub const UNKNOWN: [u8; 6] = [0b111, 0b111, 0b111, 0b111, 0b111, 0b000];
15
16/// Look up the glyph rows for a character.
17pub fn glyph(c: char) -> [u8; 6] {
18    let i = c as u32;
19    if (32..127).contains(&i) {
20        GLYPHS[(i - 32) as usize]
21    } else {
22        UNKNOWN
23    }
24}
25
26/// Pixel width of a string when printed with the built-in font.
27pub fn text_width(s: &str) -> i32 {
28    s.chars().count() as i32 * GLYPH_W
29}
30
31// Uppercase, digits and symbols occupy the top five rows (the cap band) with a
32// blank sixth row. Lowercase letters sit on the baseline with a 4-row x-height;
33// `b d f h k l t` reach the ascender line and `g j p q y` drop into the sixth
34// row as descenders. The leftmost pixel of each row is bit 2 (`0b100`).
35#[rustfmt::skip]
36const GLYPHS: [[u8; 6]; 95] = [
37    [0b000, 0b000, 0b000, 0b000, 0b000, 0b000], // space
38    [0b010, 0b010, 0b010, 0b000, 0b010, 0b000], // !
39    [0b101, 0b101, 0b000, 0b000, 0b000, 0b000], // "
40    [0b101, 0b111, 0b101, 0b111, 0b101, 0b000], // #
41    [0b111, 0b110, 0b111, 0b011, 0b111, 0b000], // $
42    [0b101, 0b001, 0b010, 0b100, 0b101, 0b000], // %
43    [0b010, 0b101, 0b010, 0b101, 0b011, 0b000], // &
44    [0b010, 0b100, 0b000, 0b000, 0b000, 0b000], // '
45    [0b001, 0b010, 0b010, 0b010, 0b001, 0b000], // (
46    [0b100, 0b010, 0b010, 0b010, 0b100, 0b000], // )
47    [0b101, 0b010, 0b111, 0b010, 0b101, 0b000], // *
48    [0b000, 0b010, 0b111, 0b010, 0b000, 0b000], // +
49    [0b000, 0b000, 0b000, 0b010, 0b100, 0b000], // ,
50    [0b000, 0b000, 0b111, 0b000, 0b000, 0b000], // -
51    [0b000, 0b000, 0b000, 0b000, 0b010, 0b000], // .
52    [0b001, 0b001, 0b010, 0b100, 0b100, 0b000], // /
53    [0b111, 0b101, 0b101, 0b101, 0b111, 0b000], // 0
54    [0b110, 0b010, 0b010, 0b010, 0b111, 0b000], // 1
55    [0b111, 0b001, 0b111, 0b100, 0b111, 0b000], // 2
56    [0b111, 0b001, 0b011, 0b001, 0b111, 0b000], // 3
57    [0b101, 0b101, 0b111, 0b001, 0b001, 0b000], // 4
58    [0b111, 0b100, 0b111, 0b001, 0b111, 0b000], // 5
59    [0b011, 0b100, 0b111, 0b101, 0b111, 0b000], // 6
60    [0b111, 0b001, 0b001, 0b001, 0b001, 0b000], // 7
61    [0b111, 0b101, 0b111, 0b101, 0b111, 0b000], // 8
62    [0b111, 0b101, 0b111, 0b001, 0b110, 0b000], // 9
63    [0b000, 0b010, 0b000, 0b010, 0b000, 0b000], // :
64    [0b000, 0b010, 0b000, 0b010, 0b100, 0b000], // ;
65    [0b001, 0b010, 0b100, 0b010, 0b001, 0b000], // <
66    [0b000, 0b111, 0b000, 0b111, 0b000, 0b000], // =
67    [0b100, 0b010, 0b001, 0b010, 0b100, 0b000], // >
68    [0b111, 0b001, 0b011, 0b000, 0b010, 0b000], // ?
69    [0b111, 0b101, 0b111, 0b100, 0b011, 0b000], // @
70    [0b111, 0b101, 0b111, 0b101, 0b101, 0b000], // A
71    [0b111, 0b101, 0b110, 0b101, 0b111, 0b000], // B
72    [0b011, 0b100, 0b100, 0b100, 0b011, 0b000], // C
73    [0b110, 0b101, 0b101, 0b101, 0b110, 0b000], // D
74    [0b111, 0b100, 0b110, 0b100, 0b111, 0b000], // E
75    [0b111, 0b100, 0b110, 0b100, 0b100, 0b000], // F
76    [0b011, 0b100, 0b101, 0b101, 0b011, 0b000], // G
77    [0b101, 0b101, 0b111, 0b101, 0b101, 0b000], // H
78    [0b111, 0b010, 0b010, 0b010, 0b111, 0b000], // I
79    [0b111, 0b010, 0b010, 0b010, 0b110, 0b000], // J
80    [0b101, 0b101, 0b110, 0b101, 0b101, 0b000], // K
81    [0b100, 0b100, 0b100, 0b100, 0b111, 0b000], // L
82    [0b111, 0b111, 0b101, 0b101, 0b101, 0b000], // M
83    [0b110, 0b101, 0b101, 0b101, 0b101, 0b000], // N
84    [0b111, 0b101, 0b101, 0b101, 0b111, 0b000], // O
85    [0b111, 0b101, 0b111, 0b100, 0b100, 0b000], // P
86    [0b111, 0b101, 0b101, 0b111, 0b001, 0b000], // Q
87    [0b111, 0b101, 0b110, 0b101, 0b101, 0b000], // R
88    [0b011, 0b100, 0b111, 0b001, 0b110, 0b000], // S
89    [0b111, 0b010, 0b010, 0b010, 0b010, 0b000], // T
90    [0b101, 0b101, 0b101, 0b101, 0b111, 0b000], // U
91    [0b101, 0b101, 0b101, 0b101, 0b010, 0b000], // V
92    [0b101, 0b101, 0b101, 0b111, 0b111, 0b000], // W
93    [0b101, 0b101, 0b010, 0b101, 0b101, 0b000], // X
94    [0b101, 0b101, 0b010, 0b010, 0b010, 0b000], // Y
95    [0b111, 0b001, 0b010, 0b100, 0b111, 0b000], // Z
96    [0b011, 0b010, 0b010, 0b010, 0b011, 0b000], // [
97    [0b100, 0b100, 0b010, 0b001, 0b001, 0b000], // backslash
98    [0b110, 0b010, 0b010, 0b010, 0b110, 0b000], // ]
99    [0b010, 0b101, 0b000, 0b000, 0b000, 0b000], // ^
100    [0b000, 0b000, 0b000, 0b000, 0b111, 0b000], // _
101    [0b100, 0b010, 0b000, 0b000, 0b000, 0b000], // `
102    [0b000, 0b011, 0b101, 0b111, 0b111, 0b000], // a
103    [0b100, 0b100, 0b110, 0b101, 0b110, 0b000], // b
104    [0b000, 0b011, 0b100, 0b100, 0b011, 0b000], // c
105    [0b001, 0b001, 0b011, 0b101, 0b011, 0b000], // d
106    [0b000, 0b011, 0b111, 0b100, 0b011, 0b000], // e
107    [0b011, 0b010, 0b111, 0b010, 0b010, 0b000], // f
108    [0b000, 0b011, 0b101, 0b011, 0b001, 0b110], // g (descender)
109    [0b100, 0b100, 0b110, 0b101, 0b101, 0b000], // h
110    [0b010, 0b000, 0b010, 0b010, 0b010, 0b000], // i
111    [0b001, 0b000, 0b001, 0b001, 0b001, 0b110], // j (descender)
112    [0b100, 0b100, 0b101, 0b110, 0b101, 0b000], // k
113    [0b110, 0b010, 0b010, 0b010, 0b011, 0b000], // l
114    [0b000, 0b111, 0b111, 0b101, 0b101, 0b000], // m
115    [0b000, 0b110, 0b101, 0b101, 0b101, 0b000], // n
116    [0b000, 0b010, 0b101, 0b101, 0b010, 0b000], // o
117    [0b000, 0b110, 0b101, 0b110, 0b100, 0b100], // p (descender)
118    [0b000, 0b011, 0b101, 0b011, 0b001, 0b001], // q (descender)
119    [0b000, 0b110, 0b100, 0b100, 0b100, 0b000], // r
120    [0b000, 0b011, 0b110, 0b011, 0b110, 0b000], // s
121    [0b010, 0b111, 0b010, 0b010, 0b011, 0b000], // t
122    [0b000, 0b101, 0b101, 0b101, 0b111, 0b000], // u
123    [0b000, 0b101, 0b101, 0b101, 0b010, 0b000], // v
124    [0b000, 0b101, 0b101, 0b111, 0b111, 0b000], // w
125    [0b000, 0b101, 0b010, 0b010, 0b101, 0b000], // x
126    [0b000, 0b101, 0b101, 0b011, 0b001, 0b110], // y (descender)
127    [0b000, 0b111, 0b001, 0b100, 0b111, 0b000], // z
128    [0b011, 0b010, 0b110, 0b010, 0b011, 0b000], // {
129    [0b010, 0b010, 0b010, 0b010, 0b010, 0b000], // |
130    [0b110, 0b010, 0b011, 0b010, 0b110, 0b000], // }
131    [0b000, 0b001, 0b111, 0b100, 0b000, 0b000], // ~
132];
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn case_is_distinct() {
140        // Every letter must render differently in each case; otherwise Rust
141        // source is unreadable (the bug this font shape fixes).
142        for (lo, up) in ('a'..='z').zip('A'..='Z') {
143            assert_ne!(glyph(lo), glyph(up), "{lo} and {up} render identically");
144        }
145    }
146
147    #[test]
148    fn lowercase_is_shorter_than_uppercase() {
149        // Plain x-height letters leave the top (ascender/cap) row empty, which
150        // is what reads as "lowercase".
151        for c in "acemnorsuvwxz".chars() {
152            assert_eq!(glyph(c)[0], 0, "{c} should not reach the cap line");
153        }
154    }
155
156    #[test]
157    fn descenders_use_the_bottom_row() {
158        for c in "gjpqy".chars() {
159            assert_ne!(glyph(c)[5], 0, "{c} should have a descender");
160        }
161        // Letters without descenders keep the bottom row clear.
162        for c in "abcdefhiklmnorstuvwxz".chars() {
163            assert_eq!(glyph(c)[5], 0, "{c} should not have a descender");
164        }
165    }
166
167    #[test]
168    fn out_of_range_is_unknown() {
169        assert_eq!(glyph('\u{1F600}'), UNKNOWN);
170        assert_eq!(glyph('\u{7F}'), UNKNOWN);
171    }
172}