1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use fontdue::{Font as FdFont, FontSettings, Metrics};
5
6const TARGET_PX: f32 = 15.0;
7
8struct TtfFace {
9 font: FdFont,
10 advance: usize,
11 cap_height: usize,
15 cache: std::sync::Mutex<HashMap<char, (Metrics, Vec<u8>)>>,
16}
17
18enum FontFace {
19 Ttf(Box<TtfFace>),
20 Bitmap,
21}
22
23static FACE: OnceLock<FontFace> = OnceLock::new();
24
25fn load_face() -> FontFace {
26 let mut db = fontdb::Database::new();
27 db.load_system_fonts();
28
29 let names = [
30 "Hack",
31 "JetBrains Mono",
32 "DejaVu Sans Mono",
33 "Liberation Mono",
34 "monospace",
35 ];
36
37 for name in names {
38 let family = if name == "monospace" {
39 fontdb::Family::Monospace
40 } else {
41 fontdb::Family::Name(name)
42 };
43 let query = fontdb::Query {
44 families: &[family],
45 ..fontdb::Query::default()
46 };
47 let Some(id) = db.query(&query) else {
48 continue;
49 };
50 let mut result = None;
51 db.with_face_data(id, |data, idx| {
52 let settings = FontSettings {
53 collection_index: idx,
54 scale: TARGET_PX,
55 ..FontSettings::default()
56 };
57 if let Ok(font) = FdFont::from_bytes(data, settings) {
58 let (metrics, _) = font.rasterize('M', TARGET_PX);
59 let advance = metrics.advance_width.round() as usize;
60 result = Some(TtfFace {
61 font,
62 advance: advance.max(1),
63 cap_height: metrics.height.max(1),
64 cache: std::sync::Mutex::new(HashMap::new()),
65 });
66 }
67 });
68 if let Some(face) = result {
69 return FontFace::Ttf(Box::new(face));
70 }
71 }
72
73 FontFace::Bitmap
74}
75
76fn face() -> &'static FontFace {
77 FACE.get_or_init(load_face)
78}
79
80pub fn glyph_w() -> usize {
81 match face() {
82 FontFace::Ttf(f) => f.advance,
83 FontFace::Bitmap => BITMAP_GLYPH_W,
84 }
85}
86
87pub fn glyph_h() -> usize {
88 match face() {
89 FontFace::Ttf(_) => TARGET_PX as usize,
90 FontFace::Bitmap => BITMAP_GLYPH_H,
91 }
92}
93
94pub fn text_width(s: &str) -> usize {
95 let n = s.chars().count();
96 if n == 0 {
97 return 0;
98 }
99 n * (glyph_w() + 1) - 1
100}
101
102pub fn draw_text(buf: &mut [u32], width: usize, height: usize, x: i32, y: i32, s: &str, fg: u32) {
103 let advance = (glyph_w() as i32) + 1;
104 let mut pen_x = x;
105 for c in s.chars() {
106 draw_char(buf, width, height, pen_x, y, c, fg);
107 pen_x += advance;
108 }
109}
110
111fn draw_char(buf: &mut [u32], width: usize, height: usize, x: i32, y: i32, c: char, fg: u32) {
112 match face() {
113 FontFace::Ttf(f) => draw_ttf_char(f, buf, width, height, x, y, c, fg),
114 FontFace::Bitmap => draw_bitmap_char(buf, width, height, x, y, bitmap_glyph(c), fg),
115 }
116}
117
118#[allow(clippy::too_many_arguments)]
119fn draw_ttf_char(
120 f: &TtfFace,
121 buf: &mut [u32],
122 width: usize,
123 height: usize,
124 x: i32,
125 y: i32,
126 c: char,
127 fg: u32,
128) {
129 let (metrics, bitmap) = {
130 let mut cache = f.cache.lock().unwrap();
131 cache
132 .entry(c)
133 .or_insert_with(|| f.font.rasterize(c, TARGET_PX))
134 .clone()
135 };
136
137 let fg_r = (fg >> 16) & 0xFF;
138 let fg_g = (fg >> 8) & 0xFF;
139 let fg_b = fg & 0xFF;
140
141 let baseline_y = y + (TARGET_PX as i32 + f.cap_height as i32) / 2;
142 let glyph_x = x + metrics.xmin;
143 let glyph_y = baseline_y - metrics.height as i32 - metrics.ymin;
144
145 for row in 0..metrics.height {
146 let py = glyph_y + row as i32;
147 if py < 0 || py as usize >= height {
148 continue;
149 }
150 for col in 0..metrics.width {
151 let coverage = bitmap[row * metrics.width + col];
152 if coverage == 0 {
153 continue;
154 }
155 let px = glyph_x + col as i32;
156 if px < 0 || px as usize >= width {
157 continue;
158 }
159 let idx = (py as usize) * width + (px as usize);
160 let Some(slot) = buf.get_mut(idx) else {
161 continue;
162 };
163 if coverage == 255 {
164 *slot = fg;
165 continue;
166 }
167 let bg = *slot;
168 let bg_r = (bg >> 16) & 0xFF;
169 let bg_g = (bg >> 8) & 0xFF;
170 let bg_b = bg & 0xFF;
171 let c = coverage as u32;
172 let inv = 255 - c;
173 let out_r = (fg_r * c + bg_r * inv) / 255;
174 let out_g = (fg_g * c + bg_g * inv) / 255;
175 let out_b = (fg_b * c + bg_b * inv) / 255;
176 *slot = 0xFF_00_00_00 | (out_r << 16) | (out_g << 8) | out_b;
177 }
178 }
179}
180
181fn draw_bitmap_char(
182 buf: &mut [u32],
183 width: usize,
184 height: usize,
185 x: i32,
186 y: i32,
187 g: BitmapGlyph,
188 fg: u32,
189) {
190 for (row_idx, row) in g.iter().enumerate() {
191 let py = y + row_idx as i32;
192 if py < 0 || py as usize >= height {
193 continue;
194 }
195 for col in 0..BITMAP_GLYPH_W {
196 let bit = 1u8 << (BITMAP_GLYPH_W - 1 - col);
197 if row & bit == 0 {
198 continue;
199 }
200 let px = x + col as i32;
201 if px < 0 || px as usize >= width {
202 continue;
203 }
204 let idx = (py as usize) * width + (px as usize);
205 if let Some(slot) = buf.get_mut(idx) {
206 *slot = fg;
207 }
208 }
209 }
210}
211
212const BITMAP_GLYPH_W: usize = 6;
213const BITMAP_GLYPH_H: usize = 10;
214type BitmapGlyph = [u8; BITMAP_GLYPH_H];
215
216const BITMAP_MISSING: BitmapGlyph = [
217 0b00_0000, 0b01_1110, 0b01_0010, 0b01_0010, 0b01_0010, 0b01_0010, 0b01_0010, 0b01_1110,
218 0b00_0000, 0b00_0000,
219];
220
221fn bitmap_glyph(c: char) -> BitmapGlyph {
222 if (c as u32) > 0x7e {
223 return BITMAP_MISSING;
224 }
225 for &(ch, g) in BITMAP_GLYPHS {
226 if ch == c {
227 return g;
228 }
229 }
230 BITMAP_MISSING
231}
232
233const __: u8 = 0b00_0000;
234
235#[rustfmt::skip]
236const BITMAP_GLYPHS: &[(char, BitmapGlyph)] = &[
237 (' ', [__, __, __, __, __, __, __, __, __, __]),
238 ('!', [__, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, __, 0b00_1000, __, __]),
239 ('"', [__, 0b01_0100, 0b01_0100, __, __, __, __, __, __, __]),
240 ('#', [__, 0b01_0100, 0b01_0100, 0b11_1110, 0b01_0100, 0b11_1110, 0b01_0100, 0b01_0100, __, __]),
241 ('$', [__, 0b00_1000, 0b01_1110, 0b10_1000, 0b01_1100, 0b00_1010, 0b11_1100, 0b00_1000, __, __]),
242 ('%', [__, 0b11_0010, 0b10_0100, 0b00_1000, 0b01_0000, 0b10_0110, 0b00_0110, __, __, __]),
243 ('&', [__, 0b01_1000, 0b10_0100, 0b01_1000, 0b10_1010, 0b10_0100, 0b01_1010, __, __, __]),
244 ('\'', [__, 0b00_1000, 0b00_1000, __, __, __, __, __, __, __]),
245 ('(', [__, 0b00_0100, 0b00_1000, 0b01_0000, 0b01_0000, 0b01_0000, 0b00_1000, 0b00_0100, __, __]),
246 (')', [__, 0b01_0000, 0b00_1000, 0b00_0100, 0b00_0100, 0b00_0100, 0b00_1000, 0b01_0000, __, __]),
247 ('*', [__, __, 0b10_1010, 0b01_1100, 0b11_1110, 0b01_1100, 0b10_1010, __, __, __]),
248 ('+', [__, __, __, 0b00_1000, 0b00_1000, 0b11_1110, 0b00_1000, 0b00_1000, __, __]),
249 (',', [__, __, __, __, __, __, 0b00_1000, 0b00_1000, 0b01_0000, __]),
250 ('-', [__, __, __, __, __, 0b11_1110, __, __, __, __]),
251 ('.', [__, __, __, __, __, __, __, 0b00_1000, __, __]),
252 ('/', [__, 0b00_0010, 0b00_0100, 0b00_0100, 0b00_1000, 0b01_0000, 0b01_0000, 0b10_0000, __, __]),
253 ('0', [__, 0b01_1100, 0b10_0010, 0b10_0110, 0b10_1010, 0b11_0010, 0b10_0010, 0b01_1100, __, __]),
254 ('1', [__, 0b00_1000, 0b01_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b01_1100, __, __]),
255 ('2', [__, 0b01_1100, 0b10_0010, 0b00_0010, 0b00_0100, 0b00_1000, 0b01_0000, 0b11_1110, __, __]),
256 ('3', [__, 0b01_1100, 0b10_0010, 0b00_0010, 0b00_1100, 0b00_0010, 0b10_0010, 0b01_1100, __, __]),
257 ('4', [__, 0b00_0100, 0b00_1100, 0b01_0100, 0b10_0100, 0b11_1110, 0b00_0100, 0b00_0100, __, __]),
258 ('5', [__, 0b11_1110, 0b10_0000, 0b11_1100, 0b00_0010, 0b00_0010, 0b10_0010, 0b01_1100, __, __]),
259 ('6', [__, 0b01_1100, 0b10_0000, 0b10_0000, 0b11_1100, 0b10_0010, 0b10_0010, 0b01_1100, __, __]),
260 ('7', [__, 0b11_1110, 0b00_0010, 0b00_0100, 0b00_1000, 0b01_0000, 0b01_0000, 0b01_0000, __, __]),
261 ('8', [__, 0b01_1100, 0b10_0010, 0b10_0010, 0b01_1100, 0b10_0010, 0b10_0010, 0b01_1100, __, __]),
262 ('9', [__, 0b01_1100, 0b10_0010, 0b10_0010, 0b01_1110, 0b00_0010, 0b00_0010, 0b01_1100, __, __]),
263 (':', [__, __, __, 0b00_1000, __, __, 0b00_1000, __, __, __]),
264 (';', [__, __, __, 0b00_1000, __, __, 0b00_1000, 0b00_1000, 0b01_0000, __]),
265 ('<', [__, __, 0b00_0100, 0b00_1000, 0b01_0000, 0b00_1000, 0b00_0100, __, __, __]),
266 ('=', [__, __, __, 0b11_1110, __, 0b11_1110, __, __, __, __]),
267 ('>', [__, __, 0b01_0000, 0b00_1000, 0b00_0100, 0b00_1000, 0b01_0000, __, __, __]),
268 ('?', [__, 0b01_1100, 0b10_0010, 0b00_0010, 0b00_0100, 0b00_1000, __, 0b00_1000, __, __]),
269 ('@', [__, 0b01_1100, 0b10_0010, 0b10_1110, 0b10_1010, 0b10_1110, 0b10_0000, 0b01_1100, __, __]),
270 ('A', [__, 0b01_1100, 0b10_0010, 0b10_0010, 0b11_1110, 0b10_0010, 0b10_0010, 0b10_0010, __, __]),
271 ('B', [__, 0b11_1100, 0b10_0010, 0b10_0010, 0b11_1100, 0b10_0010, 0b10_0010, 0b11_1100, __, __]),
272 ('C', [__, 0b01_1100, 0b10_0010, 0b10_0000, 0b10_0000, 0b10_0000, 0b10_0010, 0b01_1100, __, __]),
273 ('D', [__, 0b11_1100, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b11_1100, __, __]),
274 ('E', [__, 0b11_1110, 0b10_0000, 0b10_0000, 0b11_1100, 0b10_0000, 0b10_0000, 0b11_1110, __, __]),
275 ('F', [__, 0b11_1110, 0b10_0000, 0b10_0000, 0b11_1100, 0b10_0000, 0b10_0000, 0b10_0000, __, __]),
276 ('G', [__, 0b01_1100, 0b10_0010, 0b10_0000, 0b10_1110, 0b10_0010, 0b10_0010, 0b01_1100, __, __]),
277 ('H', [__, 0b10_0010, 0b10_0010, 0b10_0010, 0b11_1110, 0b10_0010, 0b10_0010, 0b10_0010, __, __]),
278 ('I', [__, 0b01_1100, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b01_1100, __, __]),
279 ('J', [__, 0b00_1110, 0b00_0100, 0b00_0100, 0b00_0100, 0b00_0100, 0b10_0100, 0b01_1000, __, __]),
280 ('K', [__, 0b10_0010, 0b10_0100, 0b10_1000, 0b11_0000, 0b10_1000, 0b10_0100, 0b10_0010, __, __]),
281 ('L', [__, 0b10_0000, 0b10_0000, 0b10_0000, 0b10_0000, 0b10_0000, 0b10_0000, 0b11_1110, __, __]),
282 ('M', [__, 0b10_0010, 0b11_0110, 0b10_1010, 0b10_1010, 0b10_0010, 0b10_0010, 0b10_0010, __, __]),
283 ('N', [__, 0b10_0010, 0b11_0010, 0b10_1010, 0b10_0110, 0b10_0010, 0b10_0010, 0b10_0010, __, __]),
284 ('O', [__, 0b01_1100, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b01_1100, __, __]),
285 ('P', [__, 0b11_1100, 0b10_0010, 0b10_0010, 0b11_1100, 0b10_0000, 0b10_0000, 0b10_0000, __, __]),
286 ('Q', [__, 0b01_1100, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_1010, 0b10_0100, 0b01_1010, __, __]),
287 ('R', [__, 0b11_1100, 0b10_0010, 0b10_0010, 0b11_1100, 0b10_1000, 0b10_0100, 0b10_0010, __, __]),
288 ('S', [__, 0b01_1110, 0b10_0000, 0b10_0000, 0b01_1100, 0b00_0010, 0b00_0010, 0b11_1100, __, __]),
289 ('T', [__, 0b11_1110, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, __, __]),
290 ('U', [__, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b01_1100, __, __]),
291 ('V', [__, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b01_0100, 0b00_1000, __, __]),
292 ('W', [__, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_1010, 0b10_1010, 0b11_0110, 0b10_0010, __, __]),
293 ('X', [__, 0b10_0010, 0b10_0010, 0b01_0100, 0b00_1000, 0b01_0100, 0b10_0010, 0b10_0010, __, __]),
294 ('Y', [__, 0b10_0010, 0b10_0010, 0b01_0100, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, __, __]),
295 ('Z', [__, 0b11_1110, 0b00_0010, 0b00_0100, 0b00_1000, 0b01_0000, 0b10_0000, 0b11_1110, __, __]),
296 ('[', [__, 0b01_1100, 0b01_0000, 0b01_0000, 0b01_0000, 0b01_0000, 0b01_0000, 0b01_1100, __, __]),
297 ('\\', [__, 0b10_0000, 0b01_0000, 0b01_0000, 0b00_1000, 0b00_0100, 0b00_0100, 0b00_0010, __, __]),
298 (']', [__, 0b01_1100, 0b00_0100, 0b00_0100, 0b00_0100, 0b00_0100, 0b00_0100, 0b01_1100, __, __]),
299 ('^', [__, 0b00_1000, 0b01_0100, 0b10_0010, __, __, __, __, __, __]),
300 ('_', [__, __, __, __, __, __, __, __, 0b11_1110, __]),
301 ('`', [0b01_0000, 0b00_1000, __, __, __, __, __, __, __, __]),
302 ('a', [__, __, __, 0b01_1100, 0b00_0010, 0b01_1110, 0b10_0010, 0b01_1110, __, __]),
303 ('b', [__, 0b10_0000, 0b10_0000, 0b11_1100, 0b10_0010, 0b10_0010, 0b10_0010, 0b11_1100, __, __]),
304 ('c', [__, __, __, 0b01_1100, 0b10_0010, 0b10_0000, 0b10_0010, 0b01_1100, __, __]),
305 ('d', [__, 0b00_0010, 0b00_0010, 0b01_1110, 0b10_0010, 0b10_0010, 0b10_0010, 0b01_1110, __, __]),
306 ('e', [__, __, __, 0b01_1100, 0b10_0010, 0b11_1110, 0b10_0000, 0b01_1100, __, __]),
307 ('f', [__, 0b00_1100, 0b01_0010, 0b01_0000, 0b11_1100, 0b01_0000, 0b01_0000, 0b01_0000, __, __]),
308 ('g', [__, __, __, 0b01_1110, 0b10_0010, 0b01_1110, 0b00_0010, 0b00_0010, 0b01_1100, __]),
309 ('h', [__, 0b10_0000, 0b10_0000, 0b11_1100, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, __, __]),
310 ('i', [__, 0b00_1000, __, 0b01_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b01_1100, __, __]),
311 ('j', [__, 0b00_0100, __, 0b00_1100, 0b00_0100, 0b00_0100, 0b00_0100, 0b00_0100, 0b01_1000, __]),
312 ('k', [__, 0b10_0000, 0b10_0000, 0b10_0100, 0b10_1000, 0b11_0000, 0b10_1000, 0b10_0100, __, __]),
313 ('l', [__, 0b01_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b01_1100, __, __]),
314 ('m', [__, __, __, 0b11_0100, 0b10_1010, 0b10_1010, 0b10_0010, 0b10_0010, __, __]),
315 ('n', [__, __, __, 0b11_1100, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, __, __]),
316 ('o', [__, __, __, 0b01_1100, 0b10_0010, 0b10_0010, 0b10_0010, 0b01_1100, __, __]),
317 ('p', [__, __, __, 0b11_1100, 0b10_0010, 0b10_0010, 0b11_1100, 0b10_0000, 0b10_0000, __]),
318 ('q', [__, __, __, 0b01_1110, 0b10_0010, 0b10_0010, 0b01_1110, 0b00_0010, 0b00_0010, __]),
319 ('r', [__, __, __, 0b10_1100, 0b11_0010, 0b10_0000, 0b10_0000, 0b10_0000, __, __]),
320 ('s', [__, __, __, 0b01_1110, 0b10_0000, 0b01_1100, 0b00_0010, 0b11_1100, __, __]),
321 ('t', [__, 0b01_0000, 0b01_0000, 0b11_1100, 0b01_0000, 0b01_0000, 0b01_0010, 0b00_1100, __, __]),
322 ('u', [__, __, __, 0b10_0010, 0b10_0010, 0b10_0010, 0b10_0010, 0b01_1110, __, __]),
323 ('v', [__, __, __, 0b10_0010, 0b10_0010, 0b10_0010, 0b01_0100, 0b00_1000, __, __]),
324 ('w', [__, __, __, 0b10_0010, 0b10_0010, 0b10_1010, 0b10_1010, 0b01_0100, __, __]),
325 ('x', [__, __, __, 0b10_0010, 0b01_0100, 0b00_1000, 0b01_0100, 0b10_0010, __, __]),
326 ('y', [__, __, __, 0b10_0010, 0b10_0010, 0b01_1110, 0b00_0010, 0b00_0010, 0b01_1100, __]),
327 ('z', [__, __, __, 0b11_1110, 0b00_0100, 0b00_1000, 0b01_0000, 0b11_1110, __, __]),
328 ('{', [__, 0b00_0100, 0b00_1000, 0b00_1000, 0b01_0000, 0b00_1000, 0b00_1000, 0b00_0100, __, __]),
329 ('|', [__, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, 0b00_1000, __, __]),
330 ('}', [__, 0b01_0000, 0b00_1000, 0b00_1000, 0b00_0100, 0b00_1000, 0b00_1000, 0b01_0000, __, __]),
331 ('~', [__, __, __, 0b01_0010, 0b10_1010, 0b10_0100, __, __, __, __]),
332];
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn glyph_w_positive() {
340 assert!(glyph_w() > 0);
341 }
342
343 #[test]
344 fn glyph_h_positive() {
345 assert!(glyph_h() > 0);
346 }
347
348 #[test]
349 fn text_width_zero_for_empty() {
350 assert_eq!(text_width(""), 0);
351 }
352
353 #[test]
354 fn text_width_one_glyph_no_trailing_gap() {
355 assert_eq!(text_width("A"), glyph_w());
356 }
357
358 #[test]
359 fn text_width_n_glyphs_includes_gaps() {
360 assert_eq!(text_width("AB"), 2 * glyph_w() + 1);
361 assert_eq!(text_width("ABC"), 3 * glyph_w() + 2);
362 }
363
364 #[test]
365 fn text_width_hi_sane() {
366 let w = text_width("hi");
367 assert!(w > 0, "text_width(\"hi\") must be positive, got {w}");
368 }
369
370 #[test]
371 fn draw_text_clips_offscreen() {
372 let mut buf = vec![0u32; 20 * 20];
373 draw_text(&mut buf, 20, 20, 100, 0, "HI", 0xFF_FFFF);
374 draw_text(&mut buf, 20, 20, -50, 0, "HI", 0xFF_FFFF);
375 draw_text(&mut buf, 20, 20, 0, 100, "HI", 0xFF_FFFF);
376 }
377
378 #[test]
379 fn draw_text_writes_non_bg_pixels() {
380 let bg = 0u32;
381 let fg = 0xEE_EE_EE;
382 let w = 200;
383 let h = 40;
384 let mut buf = vec![bg; w * h];
385 draw_text(&mut buf, w, h, 0, 0, "Hi", fg);
386 assert!(
387 buf.iter().any(|&px| px != bg),
388 "draw_text must write at least one non-background pixel"
389 );
390 }
391}