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?
1011use crate::{format_buf, iif};
1213/// 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.
25pub glyphs: &'glyphs [T],
26/// The first char in `glyphs`.
27pub first_glyph: char,
2829/// A slice of extra paired glyphs.
30pub extra_glyphs: &'glyphs [(char, T)],
3132/// The width of each glyph in pixels.
33pub width: u8,
34/// The height of each glyph in pixels.
35pub height: u8,
36/// Where the base line sits in the height.
37pub baseline: u8,
38/// Horizontal space to advance after each glyph.
39pub advance_x: u8,
40/// Vertical space to advance after each new line.
41pub advance_y: u8,
42}
4344impl<T> core::fmt::Debug for BitmapFont<'_, T> {
45fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
46let mut buf = [0u8; 128];
47let 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}
6061impl<T: Copy + Into<u64>> BitmapFont<'_, T> {
62/// Returns the rendered text width.
63pub fn text_width(&self, text: &str) -> usize {
64 text.chars().count() * self.advance_x as usize
65 }
6667/// Returns the height of any glyph.
68pub const fn height(&self) -> u8 {
69self.height
70 }
71/// Returns the width of any glyph.
72pub const fn width(&self) -> u8 {
73self.width
74 }
7576/// Draws grayscale text into a one-byte-per-pixel buffer.
77pub fn draw_mono(&self, buffer: &mut [u8], width: usize, x: isize, y: isize, text: &str) {
78let height = buffer.len() / width;
79self.for_each_pixel_with_local(x, y, text, |pixel_x, pixel_y, _, _, _| {
80if pixel_x >= 0 && pixel_x < width as isize && pixel_y >= 0 && pixel_y < height as isize
81 {
82let offset = (pixel_y as usize) * width + (pixel_x as usize);
83 buffer[offset] = 1;
84 }
85 });
86 }
8788/// Draws RGBA text into a 4-bytes-per-pixel buffer.
89pub 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 ) {
98let height = buffer.len() / (width * 4);
99self.for_each_pixel_with_local(x, y, text, |pixel_x, pixel_y, _, _, _| {
100if pixel_x >= 0 && pixel_x < width as isize && pixel_y >= 0 && pixel_y < height as isize
101 {
102let offset = ((pixel_y as usize) * width + (pixel_x as usize)) * 4;
103 buffer[offset..offset + 4].copy_from_slice(&color);
104 }
105 });
106 }
107108/// 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.
113pub fn draw_rgba_with<F>(
114&self,
115 buffer: &mut [u8],
116 width: usize,
117 x: isize,
118 y: isize,
119 text: &str,
120mut color_fn: F,
121 ) where
122F: FnMut(usize, usize, usize) -> [u8; 4],
123 {
124let height = buffer.len() / (width * 4);
125self.for_each_pixel_with_local(
126 x,
127 y,
128 text,
129 |global_x, global_y, local_x, local_y, char_index| {
130if global_x >= 0
131&& global_x < width as isize
132 && global_y >= 0
133&& global_y < height as isize
134 {
135let color = color_fn(local_x, local_y, char_index);
136let 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}
143144// 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.
152fn for_each_pixel_with_local<F>(&self, x: isize, y: isize, text: &str, mut f: F)
153where
154F: FnMut(isize, isize, usize, usize, usize),
155 {
156let mut x_pos = x;
157let mut char_index = 0;
158for c in text.chars() {
159if let Some(glyph_index) = (c as usize).checked_sub(self.first_glyph as usize) {
160if glyph_index < self.glyphs.len() {
161let glyph: u64 = self.glyphs[glyph_index].into();
162for row in 0..self.height {
163let global_y = y + row as isize - self.baseline as isize;
164iif![global_y < 0; continue];
165for col in 0..self.width {
166let global_x = x_pos + col as isize;
167let 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);
170if (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}