Skip to main content

endbasic_std/gfx/lcd/fonts/
mod.rs

1// EndBASIC
2// Copyright 2025 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Support for bitmap fonts directly rendered onto an LCD.
18
19use crate::console::{CharsXY, SizeInPixels};
20use crate::gfx::lcd::LcdSize;
21use std::collections::HashMap;
22use std::io;
23
24mod font_5x8;
25pub use font_5x8::FONT_5X8;
26
27mod font_16x16;
28pub use font_16x16::FONT_16X16;
29
30mod font_vga8x16;
31pub use font_vga8x16::FONT_VGA8X16;
32
33/// Representation of a font.
34pub struct Font {
35    /// The name of the font.
36    pub name: &'static str,
37
38    /// The size of a single glyph, in pixels.
39    pub glyph_size: LcdSize,
40
41    /// The number of bytes in every glyph row.
42    pub stride: usize,
43
44    /// The bitmap data for the font.
45    pub data: &'static [u8],
46}
47
48impl Font {
49    /// Returns the raw font data for `ch`.
50    ///
51    /// Each entry in the array corresponds to a row of pixels and is a bitmask indicating which
52    /// pixels to turn on.
53    pub(crate) fn glyph(&self, mut ch: char) -> &'static [u8] {
54        if !(' '..='~').contains(&ch) {
55            // TODO(jmmv): Would be nicer to draw an empty box, much like how unknown Unicode
56            // characters are typically displayed.
57            ch = '?';
58        }
59        let height = self.glyph_size.height * self.stride;
60        let offset = ((ch as usize) - (' ' as usize)) * height;
61        debug_assert!(offset < (self.data.len() + height));
62        &self.data[offset..offset + height]
63    }
64
65    /// Computes the number of characters that fit within the given pixels `area`.
66    pub fn chars_in_area(&self, area: SizeInPixels) -> CharsXY {
67        CharsXY::new(
68            area.width
69                .checked_div(u16::try_from(self.glyph_size.width).expect("Must fit"))
70                .expect("Glyph size tested for non-zero during init"),
71            area.height
72                .checked_div(u16::try_from(self.glyph_size.height).expect("Must fit"))
73                .expect("Glyph size tested for non-zero during init"),
74        )
75    }
76}
77
78/// Registry of all available fonts.
79pub struct Fonts(HashMap<&'static str, &'static Font>);
80
81impl Fonts {
82    /// Obtains a mapping of all available fonts.
83    pub fn all() -> Self {
84        let mut fonts = HashMap::default();
85        fonts.insert(FONT_5X8.name, &FONT_5X8);
86        fonts.insert(FONT_16X16.name, &FONT_16X16);
87        fonts.insert(FONT_VGA8X16.name, &FONT_VGA8X16);
88        Self(fonts)
89    }
90
91    /// Gets a font by `name`, ensuring that it's present.
92    pub fn get(&self, name: &str) -> io::Result<&'static Font> {
93        match self.0.get(name) {
94            Some(font) => Ok(*font),
95            None => {
96                let mut valid = self.0.keys().copied().collect::<Vec<&'static str>>();
97                valid.sort();
98                Err(io::Error::new(
99                    io::ErrorKind::InvalidInput,
100                    format!("Unknown font: {}; valid names are: {}", name, valid.join(", ")),
101                ))
102            }
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_font_glyph_printable() {
113        let font = &FONT_5X8;
114
115        let offset = (usize::from(b'a') - usize::from(b' ')) * 8;
116        let expected = &font.data[offset..offset + 8];
117
118        let data = font.glyph('a');
119        assert_eq!(expected, data);
120    }
121
122    #[test]
123    fn test_font_glyph_non_printable() {
124        let font = &FONT_5X8;
125
126        let offset = (usize::from(b'?') - usize::from(b' ')) * 8;
127        let expected = &font.data[offset..offset + 8];
128
129        let data = font.glyph(char::from(30));
130        assert_eq!(expected, data);
131    }
132}