Skip to main content

buffr_ui/
font.rs

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    /// Pixel height of an upper-case glyph (rasterized 'M'). Used to
12    /// position the baseline so caps are visually centred inside the
13    /// `TARGET_PX`-tall cell instead of bottom-aligned.
14    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}