Skip to main content

embedded_gui/
font.rs

1include!(concat!(env!("OUT_DIR"), "/generated_ascii_3x5.rs"));
2
3/// Trait for custom font providers.
4///
5/// Consumers can implement this trait on custom font types (e.g. anti-aliased fonts,
6/// vector text generators, external BDF/PSF font decoders, TTF parsers) and use
7/// [`FontId::Dynamic`] or [`FontId::from`] to pass them to text styling.
8pub trait Font: Send + Sync {
9    /// Character horizontal advance in pixels.
10    fn advance(&self) -> u32;
11
12    /// Vertical line height in pixels.
13    fn line_height(&self) -> u32;
14
15    /// Render a single glyph by calling `draw_pixel(dx, dy)` for each active pixel
16    /// in the glyph, where `(dx, dy)` are relative coordinates within the glyph bounding box.
17    fn draw_glyph(&self, ch: char, draw_pixel: &mut dyn FnMut(i32, i32));
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct PackedFont {
22    pub first_char: u8,
23    pub advance: u8,
24    pub line_height: u8,
25    pub glyphs: &'static [[u8; 5]],
26}
27
28impl Font for PackedFont {
29    fn advance(&self) -> u32 {
30        self.advance as u32
31    }
32
33    fn line_height(&self) -> u32 {
34        self.line_height as u32
35    }
36
37    fn draw_glyph(&self, ch: char, draw_pixel: &mut dyn FnMut(i32, i32)) {
38        let code = ch as u32;
39        let rows = if code >= self.first_char as u32 {
40            let idx = (code as usize).saturating_sub(self.first_char as usize);
41            self.glyphs.get(idx).copied().unwrap_or([0; 5])
42        } else {
43            [0; 5]
44        };
45        for (row, bits) in rows.iter().enumerate() {
46            for col in 0..3 {
47                if bits & (1 << (2 - col)) != 0 {
48                    draw_pixel(col, row as i32);
49                }
50            }
51        }
52    }
53}
54
55pub static ASCII_3X5_FONT: PackedFont = PackedFont {
56    first_char: 32,
57    advance: 4,
58    line_height: 6,
59    glyphs: &ASCII_3X5_GLYPHS,
60};
61
62pub static ASCII_4X7_FONT: PackedFont = PackedFont {
63    first_char: 32,
64    advance: 5,
65    line_height: 8,
66    glyphs: &ASCII_4X7_GLYPHS,
67};
68
69/// Bounding-box rendering operation for [`BitmapFont`].
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum GlyphOp {
72    /// Draw a single pixel at relative offset `(dx, dy)`.
73    Pixel(i32, i32),
74    /// Draw a contiguous horizontal span of `len` pixels starting at `(dx, dy)`.
75    Span(i32, i32, u32),
76}
77
78/// Flexible monospaced or packed raw bitmap font definition.
79///
80/// Unlike [`PackedFont`] (which is fixed to 3x5 5-row bitpacks), [`BitmapFont`]
81/// supports arbitrary glyph dimensions (e.g., 8x8, 8x16, 12x16, 16x24),
82/// configurable advance/line-height, and multi-byte row bit-masks.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub struct BitmapFont {
85    /// Bounding box width in pixels.
86    pub width: u8,
87    /// Bounding box height in pixels.
88    pub height: u8,
89    /// Character horizontal advance in pixels.
90    pub advance: u8,
91    /// Vertical line height in pixels.
92    pub line_height: u8,
93    /// The ASCII character code of the first glyph in the buffer (usually 32 / space).
94    pub first_char: u8,
95    /// Number of bytes per row for each glyph (e.g. 1 byte for width <= 8, 2 bytes for width <= 16).
96    pub bytes_per_row: u8,
97    /// Contiguous byte slice containing glyph bitmaps stored row by row (MSB left-to-right).
98    pub glyphs: &'static [u8],
99}
100
101impl BitmapFont {
102    /// Creates a new `BitmapFont` for standard 8x8 glyphs (1 byte per row, 8 rows per glyph).
103    pub const fn new_8x8(
104        first_char: u8,
105        advance: u8,
106        line_height: u8,
107        glyphs: &'static [u8],
108    ) -> Self {
109        Self {
110            width: 8,
111            height: 8,
112            advance,
113            line_height,
114            first_char,
115            bytes_per_row: 1,
116            glyphs,
117        }
118    }
119
120    /// Creates a new `BitmapFont` for standard 8x16 glyphs (1 byte per row, 16 rows per glyph).
121    pub const fn new_8x16(
122        first_char: u8,
123        advance: u8,
124        line_height: u8,
125        glyphs: &'static [u8],
126    ) -> Self {
127        Self {
128            width: 8,
129            height: 16,
130            advance,
131            line_height,
132            first_char,
133            bytes_per_row: 1,
134            glyphs,
135        }
136    }
137
138    /// Get the raw byte slice for a character's row data.
139    pub fn glyph_bytes(&self, ch: char) -> Option<&'static [u8]> {
140        let code = ch as u32;
141        if code < self.first_char as u32 {
142            return None;
143        }
144        let idx = (code - self.first_char as u32) as usize;
145        let bytes_per_glyph = self.height as usize * self.bytes_per_row as usize;
146        let start = idx * bytes_per_glyph;
147        let end = start + bytes_per_glyph;
148        if end <= self.glyphs.len() {
149            Some(&self.glyphs[start..end])
150        } else {
151            None
152        }
153    }
154
155    /// Renders a glyph by emitting [`GlyphOp`] commands to a single closure callback.
156    pub fn draw_glyph_to<F>(&self, ch: char, mut emit: F)
157    where
158        F: FnMut(GlyphOp),
159    {
160        if let Some(data) = self.glyph_bytes(ch) {
161            let bpr = self.bytes_per_row as usize;
162            for row in 0..(self.height as usize) {
163                let row_data = &data[row * bpr..(row + 1) * bpr];
164                let mut span_start: Option<usize> = None;
165                let mut span_len = 0u32;
166
167                for col in 0..(self.width as usize) {
168                    let byte_idx = col / 8;
169                    let bit_idx = 7 - (col % 8);
170                    let is_set =
171                        byte_idx < row_data.len() && (row_data[byte_idx] & (1 << bit_idx)) != 0;
172
173                    if is_set {
174                        if span_start.is_none() {
175                            span_start = Some(col);
176                            span_len = 1;
177                        } else {
178                            span_len += 1;
179                        }
180                    } else if let Some(start) = span_start {
181                        if span_len == 1 {
182                            emit(GlyphOp::Pixel(start as i32, row as i32));
183                        } else {
184                            emit(GlyphOp::Span(start as i32, row as i32, span_len));
185                        }
186                        span_start = None;
187                        span_len = 0;
188                    }
189                }
190                if let Some(start) = span_start {
191                    if span_len == 1 {
192                        emit(GlyphOp::Pixel(start as i32, row as i32));
193                    } else {
194                        emit(GlyphOp::Span(start as i32, row as i32, span_len));
195                    }
196                }
197            }
198        }
199    }
200}
201
202impl Font for BitmapFont {
203    fn advance(&self) -> u32 {
204        self.advance as u32
205    }
206
207    fn line_height(&self) -> u32 {
208        self.line_height as u32
209    }
210
211    fn draw_glyph(&self, ch: char, draw_pixel: &mut dyn FnMut(i32, i32)) {
212        self.draw_glyph_to(ch, |op| match op {
213            GlyphOp::Pixel(dx, dy) => draw_pixel(dx, dy),
214            GlyphOp::Span(dx, dy, len) => {
215                for col in 0..len {
216                    draw_pixel(dx + col as i32, dy);
217                }
218            }
219        });
220    }
221}
222
223#[cfg(feature = "embedded-graphics")]
224impl Font for embedded_graphics::mono_font::MonoFont<'static> {
225    fn advance(&self) -> u32 {
226        self.character_size.width + self.character_spacing
227    }
228
229    fn line_height(&self) -> u32 {
230        self.character_size.height
231    }
232
233    fn draw_glyph(&self, ch: char, draw_pixel: &mut dyn FnMut(i32, i32)) {
234        use embedded_graphics::Drawable;
235        use embedded_graphics::draw_target::DrawTarget;
236        use embedded_graphics::geometry::{OriginDimensions, Point, Size};
237        use embedded_graphics::mono_font::MonoTextStyle;
238        use embedded_graphics::pixelcolor::BinaryColor;
239        use embedded_graphics::text::Text;
240
241        struct Collector<'a> {
242            f: &'a mut dyn FnMut(i32, i32),
243        }
244
245        impl OriginDimensions for Collector<'_> {
246            fn size(&self) -> Size {
247                Size::new(u32::MAX, u32::MAX)
248            }
249        }
250
251        impl DrawTarget for Collector<'_> {
252            type Color = BinaryColor;
253            type Error = core::convert::Infallible;
254
255            fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
256            where
257                I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
258            {
259                for embedded_graphics::Pixel(pos, color) in pixels {
260                    if color.is_on() {
261                        (self.f)(pos.x, pos.y);
262                    }
263                }
264                Ok(())
265            }
266        }
267
268        let text_style = MonoTextStyle::new(self, BinaryColor::On);
269        let mut buf = [0u8; 4];
270        let ch_str = ch.encode_utf8(&mut buf);
271        let mut collector = Collector { f: draw_pixel };
272        let _ = Text::new(ch_str, Point::zero(), text_style).draw(&mut collector);
273    }
274}
275
276#[derive(Clone, Copy)]
277pub enum FontId {
278    Tiny3x5,
279    Medium4x7,
280    Scaled6x10,
281    Vector(u8),
282    Custom(&'static PackedFont),
283    Bitmap(&'static BitmapFont),
284    Dynamic(&'static dyn Font),
285    #[cfg(feature = "embedded-graphics")]
286    MonoFont(&'static embedded_graphics::mono_font::MonoFont<'static>),
287}
288
289impl core::fmt::Debug for FontId {
290    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
291        match self {
292            Self::Tiny3x5 => write!(f, "Tiny3x5"),
293            Self::Medium4x7 => write!(f, "Medium4x7"),
294            Self::Scaled6x10 => write!(f, "Scaled6x10"),
295            Self::Vector(scale) => f.debug_tuple("Vector").field(scale).finish(),
296            Self::Custom(font) => f.debug_tuple("Custom").field(font).finish(),
297            Self::Bitmap(font) => f.debug_tuple("Bitmap").field(font).finish(),
298            Self::Dynamic(_) => f.write_str("Dynamic"),
299            #[cfg(feature = "embedded-graphics")]
300            Self::MonoFont(font) => f.debug_tuple("MonoFont").field(font).finish(),
301        }
302    }
303}
304
305impl PartialEq for FontId {
306    fn eq(&self, other: &Self) -> bool {
307        match (self, other) {
308            (Self::Tiny3x5, Self::Tiny3x5) => true,
309            (Self::Medium4x7, Self::Medium4x7) => true,
310            (Self::Scaled6x10, Self::Scaled6x10) => true,
311            (Self::Vector(a), Self::Vector(b)) => a == b,
312            (Self::Custom(a), Self::Custom(b)) => core::ptr::eq(*a, *b),
313            (Self::Bitmap(a), Self::Bitmap(b)) => core::ptr::eq(*a, *b),
314            (Self::Dynamic(a), Self::Dynamic(b)) => core::ptr::eq(
315                *a as *const dyn Font as *const (),
316                *b as *const dyn Font as *const (),
317            ),
318            #[cfg(feature = "embedded-graphics")]
319            (Self::MonoFont(a), Self::MonoFont(b)) => core::ptr::eq(*a, *b),
320            _ => false,
321        }
322    }
323}
324
325impl Eq for FontId {}
326
327impl FontId {
328    pub fn advance(self) -> u32 {
329        match self {
330            Self::Tiny3x5 => 4,
331            Self::Medium4x7 => 5,
332            Self::Scaled6x10 => 7,
333            Self::Vector(scale) => (8 * scale) as u32,
334            Self::Custom(font) => font.advance as u32,
335            Self::Bitmap(font) => font.advance as u32,
336            Self::Dynamic(font) => font.advance(),
337            #[cfg(feature = "embedded-graphics")]
338            Self::MonoFont(font) => font.character_size.width + font.character_spacing,
339        }
340    }
341
342    pub fn line_height(self) -> u32 {
343        match self {
344            Self::Tiny3x5 => 6,
345            Self::Medium4x7 => 8,
346            Self::Scaled6x10 => 11,
347            Self::Vector(scale) => (12 * scale) as u32,
348            Self::Custom(font) => font.line_height as u32,
349            Self::Bitmap(font) => font.line_height as u32,
350            Self::Dynamic(font) => font.line_height(),
351            #[cfg(feature = "embedded-graphics")]
352            Self::MonoFont(font) => font.character_size.height,
353        }
354    }
355}
356
357pub const fn packed_font(font: FontId) -> &'static PackedFont {
358    match font {
359        FontId::Tiny3x5 => &ASCII_3X5_FONT,
360        FontId::Medium4x7 => &ASCII_4X7_FONT,
361        FontId::Scaled6x10 => &ASCII_3X5_FONT,
362        FontId::Vector(_) => &ASCII_3X5_FONT,
363        FontId::Custom(font) => font,
364        FontId::Bitmap(_) => &ASCII_3X5_FONT,
365        FontId::Dynamic(_) => &ASCII_3X5_FONT,
366        #[cfg(feature = "embedded-graphics")]
367        FontId::MonoFont(_) => &ASCII_3X5_FONT,
368    }
369}
370
371impl From<&'static PackedFont> for FontId {
372    fn from(font: &'static PackedFont) -> Self {
373        FontId::Custom(font)
374    }
375}
376
377impl From<&'static BitmapFont> for FontId {
378    fn from(font: &'static BitmapFont) -> Self {
379        FontId::Bitmap(font)
380    }
381}
382
383impl From<&'static dyn Font> for FontId {
384    fn from(font: &'static dyn Font) -> Self {
385        FontId::Dynamic(font)
386    }
387}
388
389#[cfg(feature = "embedded-graphics")]
390impl From<&'static embedded_graphics::mono_font::MonoFont<'static>> for FontId {
391    fn from(font: &'static embedded_graphics::mono_font::MonoFont<'static>) -> Self {
392        FontId::MonoFont(font)
393    }
394}
395
396pub fn get_vector_glyph(ch: char) -> &'static [(u8, u8)] {
397    match ch {
398        ' ' => &[],
399        '0' => &[
400            (2, 0),
401            (6, 0),
402            (6, 10),
403            (2, 10),
404            (2, 0),
405            (0xFF, 0xFF),
406            (2, 10),
407            (6, 0),
408        ],
409        '1' => &[
410            (4, 0),
411            (4, 10),
412            (0xFF, 0xFF),
413            (2, 2),
414            (4, 0),
415            (0xFF, 0xFF),
416            (2, 10),
417            (6, 10),
418        ],
419        '2' => &[(2, 0), (6, 0), (6, 5), (2, 5), (2, 10), (6, 10)],
420        '3' => &[
421            (2, 0),
422            (6, 0),
423            (6, 10),
424            (2, 10),
425            (0xFF, 0xFF),
426            (2, 5),
427            (6, 5),
428        ],
429        '4' => &[(2, 0), (2, 5), (6, 5), (0xFF, 0xFF), (6, 0), (6, 10)],
430        '5' => &[(6, 0), (2, 0), (2, 5), (6, 5), (6, 10), (2, 10)],
431        '6' => &[(6, 0), (2, 0), (2, 10), (6, 10), (6, 5), (2, 5)],
432        '7' => &[(2, 0), (6, 0), (2, 10)],
433        '8' => &[
434            (2, 0),
435            (6, 0),
436            (6, 10),
437            (2, 10),
438            (2, 0),
439            (0xFF, 0xFF),
440            (2, 5),
441            (6, 5),
442        ],
443        '9' => &[(6, 5), (2, 5), (2, 0), (6, 0), (6, 10), (2, 10)],
444        'A' | 'a' => &[(2, 10), (4, 0), (6, 10), (0xFF, 0xFF), (3, 5), (5, 5)],
445        'B' | 'b' => &[
446            (2, 0),
447            (5, 0),
448            (6, 2),
449            (6, 4),
450            (5, 5),
451            (2, 5),
452            (5, 5),
453            (6, 6),
454            (6, 8),
455            (5, 10),
456            (2, 10),
457            (2, 0),
458        ],
459        'C' | 'c' => &[(6, 0), (2, 0), (2, 10), (6, 10)],
460        'D' | 'd' => &[(2, 0), (5, 0), (6, 3), (6, 7), (5, 10), (2, 10), (2, 0)],
461        'E' | 'e' => &[
462            (6, 0),
463            (2, 0),
464            (2, 10),
465            (6, 10),
466            (0xFF, 0xFF),
467            (2, 5),
468            (5, 5),
469        ],
470        'F' | 'f' => &[(6, 0), (2, 0), (2, 10), (0xFF, 0xFF), (2, 5), (5, 5)],
471        'G' | 'g' => &[(6, 2), (6, 0), (2, 0), (2, 10), (6, 10), (6, 5), (4, 5)],
472        'H' | 'h' => &[
473            (2, 0),
474            (2, 10),
475            (0xFF, 0xFF),
476            (6, 0),
477            (6, 10),
478            (0xFF, 0xFF),
479            (2, 5),
480            (6, 5),
481        ],
482        'I' | 'i' => &[
483            (4, 0),
484            (4, 10),
485            (0xFF, 0xFF),
486            (2, 0),
487            (6, 0),
488            (0xFF, 0xFF),
489            (2, 10),
490            (6, 10),
491        ],
492        'J' | 'j' => &[(6, 0), (6, 8), (4, 10), (2, 8)],
493        'K' | 'k' => &[(2, 0), (2, 10), (0xFF, 0xFF), (6, 0), (2, 5), (6, 10)],
494        'L' | 'l' => &[(2, 0), (2, 10), (6, 10)],
495        'M' | 'm' => &[(2, 10), (2, 0), (4, 5), (6, 0), (6, 10)],
496        'N' | 'n' => &[(2, 10), (2, 0), (6, 10), (6, 0)],
497        'O' | 'o' => &[(2, 0), (6, 0), (6, 10), (2, 10), (2, 0)],
498        'P' | 'p' => &[(2, 10), (2, 0), (6, 0), (6, 5), (2, 5)],
499        'Q' | 'q' => &[
500            (2, 0),
501            (6, 0),
502            (6, 10),
503            (2, 10),
504            (2, 0),
505            (0xFF, 0xFF),
506            (4, 7),
507            (7, 10),
508        ],
509        'R' | 'r' => &[
510            (2, 10),
511            (2, 0),
512            (6, 0),
513            (6, 5),
514            (2, 5),
515            (0xFF, 0xFF),
516            (4, 5),
517            (6, 10),
518        ],
519        'S' | 's' => &[(6, 0), (2, 0), (2, 5), (6, 5), (6, 10), (2, 10)],
520        'T' | 't' => &[(2, 0), (6, 0), (0xFF, 0xFF), (4, 0), (4, 10)],
521        'U' | 'u' => &[(2, 0), (2, 10), (6, 10), (6, 0)],
522        'V' | 'v' => &[(2, 0), (4, 10), (6, 0)],
523        'W' | 'w' => &[(2, 0), (2, 10), (4, 5), (6, 10), (6, 0)],
524        'X' | 'x' => &[(2, 0), (6, 10), (0xFF, 0xFF), (6, 0), (2, 10)],
525        'Y' | 'y' => &[(2, 0), (4, 5), (6, 0), (0xFF, 0xFF), (4, 5), (4, 10)],
526        'Z' | 'z' => &[(2, 0), (6, 0), (2, 10), (6, 10)],
527        '-' => &[(2, 5), (6, 5)],
528        '+' => &[(2, 5), (6, 5), (0xFF, 0xFF), (4, 2), (4, 8)],
529        '.' => &[(4, 9), (4, 10)],
530        ':' => &[(4, 2), (4, 3), (0xFF, 0xFF), (4, 7), (4, 8)],
531        '/' => &[(2, 10), (6, 0)],
532        _ => &[(2, 0), (6, 0), (6, 10), (2, 10), (2, 0), (2, 0), (6, 10)],
533    }
534}
535
536pub fn glyph_rows(font: FontId, ch: char) -> [u8; 5] {
537    let packed = packed_font(font);
538    let code = ch as u32;
539    if code >= packed.first_char as u32 {
540        let idx = (code as usize).saturating_sub(packed.first_char as usize);
541        if idx < packed.glyphs.len() {
542            return packed.glyphs[idx];
543        }
544    }
545    let fallback = b'?'.saturating_sub(packed.first_char) as usize;
546    packed.glyphs.get(fallback).copied().unwrap_or([0; 5])
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use std::vec::Vec;
553
554    static MY_CUSTOM_GLYPHS: [[u8; 5]; 2] = [
555        [0b111, 0b101, 0b111, 0b101, 0b101], // Space / 'A'
556        [0b111, 0b111, 0b111, 0b111, 0b111],
557    ];
558
559    static MY_CUSTOM_FONT: PackedFont = PackedFont {
560        first_char: 32,
561        advance: 8,
562        line_height: 12,
563        glyphs: &MY_CUSTOM_GLYPHS,
564    };
565
566    static MY_BITMAP_GLYPHS: [u8; 16] = [
567        // 'A' (8x16)
568        0b00111100, 0b01100110, 0b01100110, 0b01111110, 0b01100110, 0b01100110, 0b01100110,
569        0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
570        0b00000000, 0b00000000,
571    ];
572
573    static MY_BITMAP_FONT: BitmapFont =
574        BitmapFont::new_8x16(65 /* 'A' */, 8, 16, &MY_BITMAP_GLYPHS);
575
576    struct CustomFontImpl;
577    impl Font for CustomFontImpl {
578        fn advance(&self) -> u32 {
579            10
580        }
581        fn line_height(&self) -> u32 {
582            14
583        }
584        fn draw_glyph(&self, _ch: char, draw_pixel: &mut dyn FnMut(i32, i32)) {
585            draw_pixel(0, 0);
586            draw_pixel(1, 1);
587        }
588    }
589
590    static DYN_FONT_INSTANCE: CustomFontImpl = CustomFontImpl;
591
592    #[test]
593    fn test_custom_font_id() {
594        let font_id = FontId::Custom(&MY_CUSTOM_FONT);
595        assert_eq!(font_id.advance(), 8);
596        assert_eq!(font_id.line_height(), 12);
597        assert_eq!(packed_font(font_id).first_char, 32);
598        assert_eq!(
599            glyph_rows(font_id, ' '),
600            [0b111, 0b101, 0b111, 0b101, 0b101]
601        );
602    }
603
604    #[test]
605    fn test_bitmap_font() {
606        let font_id = FontId::from(&MY_BITMAP_FONT);
607        assert_eq!(font_id.advance(), 8);
608        assert_eq!(font_id.line_height(), 16);
609
610        let mut pixels = Vec::new();
611        MY_BITMAP_FONT.draw_glyph('A', &mut |x, y| pixels.push((x, y)));
612        assert!(!pixels.is_empty());
613        assert!(pixels.contains(&(2, 0))); // 0b00111100 has bit at col 2
614    }
615
616    #[test]
617    fn test_dynamic_font() {
618        let font_id = FontId::from(&DYN_FONT_INSTANCE as &'static dyn Font);
619        assert_eq!(font_id.advance(), 10);
620        assert_eq!(font_id.line_height(), 14);
621    }
622}