devela/media/font/bitmap/
bitmap.rs

1// devela::media::bitmap::bitmap
2//
3//! Defines the [`BitmapFont`] struct.
4//
5// TODO
6// - wrapping.
7// - max width.
8// DECIDE
9// - what to do with newlines? ignore? another richer mode?
10
11use crate::{format_buf, iif};
12
13/// A simple bitmap font for rendering fixed-size glyphs.
14///
15/// Each glyph is stored as a bitfield in a generic type and is assumed to have
16/// fixed dimensions (`width` × `height`), a baseline, and an advance metric.
17///
18/// The glyphs are arranged sequentially starting from `first_glyph`.
19///
20/// The font supports drawing text into both mono and RGBA buffers,
21/// as well as using a custom per-pixel color function.
22#[derive(Clone, PartialEq, Eq, Hash)] //, Debug,
23pub struct BitmapFont<'glyphs, T> {
24    /// A slice of glyphs.
25    pub glyphs: &'glyphs [T],
26    /// The first char in `glyphs`.
27    pub first_glyph: char,
28
29    /// A slice of extra paired glyphs.
30    pub extra_glyphs: &'glyphs [(char, T)],
31
32    /// The width of each glyph in pixels.
33    pub width: u8,
34    /// The height of each glyph in pixels.
35    pub height: u8,
36    /// Where the base line sits in the height.
37    pub baseline: u8,
38    /// Horizontal space to advance after each glyph.
39    pub advance_x: u8,
40    /// Vertical space to advance after each new line.
41    pub advance_y: u8,
42}
43
44impl<T> core::fmt::Debug for BitmapFont<'_, T> {
45    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
46        let mut buf = [0u8; 128];
47        let name = format_buf![&mut buf, "BitmapFont<{}>", stringify!(T)].unwrap();
48        f.debug_struct(name)
49            .field("glyphs", &self.glyphs.len())
50            .field("first_glyph", &self.first_glyph)
51            .field("extra_glyphs", &self.extra_glyphs.len())
52            .field("width", &self.width)
53            .field("height", &self.height)
54            .field("baseline", &self.baseline)
55            .field("advance_x", &self.advance_x)
56            .field("advance_y", &self.advance_x)
57            .finish()
58    }
59}
60
61impl<T: Copy + Into<u64>> BitmapFont<'_, T> {
62    /// Returns the rendered text width.
63    pub fn text_width(&self, text: &str) -> usize {
64        text.chars().count() * self.advance_x as usize
65    }
66
67    /// Returns the height of any glyph.
68    pub const fn height(&self) -> u8 {
69        self.height
70    }
71    /// Returns the width of any glyph.
72    pub const fn width(&self) -> u8 {
73        self.width
74    }
75
76    /// Draws grayscale text into a one-byte-per-pixel buffer.
77    pub fn draw_mono(&self, buffer: &mut [u8], width: usize, x: isize, y: isize, text: &str) {
78        let height = buffer.len() / width;
79        self.for_each_pixel_with_local(x, y, text, |pixel_x, pixel_y, _, _, _| {
80            if pixel_x >= 0 && pixel_x < width as isize && pixel_y >= 0 && pixel_y < height as isize
81            {
82                let offset = (pixel_y as usize) * width + (pixel_x as usize);
83                buffer[offset] = 1;
84            }
85        });
86    }
87
88    /// Draws RGBA text into a 4-bytes-per-pixel buffer.
89    pub fn draw_rgba(
90        &self,
91        buffer: &mut [u8],
92        width: usize,
93        x: isize,
94        y: isize,
95        text: &str,
96        color: [u8; 4],
97    ) {
98        let height = buffer.len() / (width * 4);
99        self.for_each_pixel_with_local(x, y, text, |pixel_x, pixel_y, _, _, _| {
100            if pixel_x >= 0 && pixel_x < width as isize && pixel_y >= 0 && pixel_y < height as isize
101            {
102                let offset = ((pixel_y as usize) * width + (pixel_x as usize)) * 4;
103                buffer[offset..offset + 4].copy_from_slice(&color);
104            }
105        });
106    }
107
108    /// Draws RGBA text with a custom color function.
109    ///
110    /// The provided closure is called for each "on" pixel and receives the glyph‑local
111    /// x and y coordinates (i.e. within the glyph) and the index of the current character.
112    /// It should return a `[u8; 4]` color (RGBA) for that pixel.
113    pub fn draw_rgba_with<F>(
114        &self,
115        buffer: &mut [u8],
116        width: usize,
117        x: isize,
118        y: isize,
119        text: &str,
120        mut color_fn: F,
121    ) where
122        F: FnMut(usize, usize, usize) -> [u8; 4],
123    {
124        let height = buffer.len() / (width * 4);
125        self.for_each_pixel_with_local(
126            x,
127            y,
128            text,
129            |global_x, global_y, local_x, local_y, char_index| {
130                if global_x >= 0
131                    && global_x < width as isize
132                    && global_y >= 0
133                    && global_y < height as isize
134                {
135                    let color = color_fn(local_x, local_y, char_index);
136                    let offset = ((global_y as usize) * width + (global_x as usize)) * 4;
137                    buffer[offset..offset + 4].copy_from_slice(&color);
138                }
139            },
140        );
141    }
142}
143
144// private methods
145impl<T: Copy + Into<u64>> BitmapFont<'_, T> {
146    /// Iterates over every pixel that should be drawn for the given text.
147    ///
148    /// The closure receives:
149    /// - `global_x` and `global_y`: the final buffer coordinates.
150    /// - `local_x` and `local_y`: the coordinates within the current glyph.
151    /// - `char_index`: the index of the current character in the text.
152    fn for_each_pixel_with_local<F>(&self, x: isize, y: isize, text: &str, mut f: F)
153    where
154        F: FnMut(isize, isize, usize, usize, usize),
155    {
156        let mut x_pos = x;
157        let mut char_index = 0;
158        for c in text.chars() {
159            if let Some(glyph_index) = (c as usize).checked_sub(self.first_glyph as usize) {
160                if glyph_index < self.glyphs.len() {
161                    let glyph: u64 = self.glyphs[glyph_index].into();
162                    for row in 0..self.height {
163                        let global_y = y + row as isize - self.baseline as isize;
164                        iif![global_y < 0; continue];
165                        for col in 0..self.width {
166                            let global_x = x_pos + col as isize;
167                            let bit_pos = row * self.width + col;
168                            // this would read rows top to bottom, draw pixels left to right
169                            // (self.height - 1 - row) * self.width + (self.width - 1 - col);
170                            if (glyph & (1 << bit_pos)) != 0 {
171                                f(global_x, global_y, col as usize, row as usize, char_index);
172                            }
173                        }
174                    }
175                }
176            }
177            x_pos += self.advance_x as isize;
178            char_index += 1;
179        }
180    }
181}