1use std::sync::Arc;
9
10use font8x8::legacy::BASIC_LEGACY;
11
12use crate::application::Application;
13use crate::color::Color;
14use crate::scene::container::Container;
15use crate::texture::Texture;
16
17#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct GlyphMetrics {
20 pub u_min: f32,
22 pub v_min: f32,
24 pub u_max: f32,
26 pub v_max: f32,
28}
29
30#[derive(Clone)]
33pub struct Font {
34 inner: Arc<FontInner>,
35}
36
37struct FontInner {
38 atlas: Texture,
39 glyphs: [Option<GlyphMetrics>; 128],
40 cell_pixels: u32,
42}
43
44impl Font {
45 #[must_use]
50 #[allow(
51 clippy::cast_precision_loss,
52 reason = "all values bounded by 128 (atlas dim) — fit losslessly in f32"
53 )]
54 pub fn bitmap_8x8(app: &Application) -> Self {
55 const CELL: u32 = 8;
56 const COLS: u32 = 16;
57 const ROWS: u32 = 16;
58 let atlas_w = COLS * CELL;
59 let atlas_h = ROWS * CELL;
60
61 let mut bytes = vec![0u8; (atlas_w * atlas_h * 4) as usize];
62 let mut glyphs: [Option<GlyphMetrics>; 128] = [None; 128];
63
64 for c in 0u32..128 {
65 let glyph_rows = BASIC_LEGACY[c as usize];
66 let cell_x = (c % COLS) * CELL;
67 let cell_y = (c / COLS) * CELL;
68
69 for row in 0u32..CELL {
70 let bits = glyph_rows[row as usize];
71 for col in 0u32..CELL {
72 let on = ((bits >> col) & 1) != 0;
73 let px = (cell_y + row) * atlas_w + (cell_x + col);
74 let idx = (px * 4) as usize;
75 if on {
76 bytes[idx] = 255;
77 bytes[idx + 1] = 255;
78 bytes[idx + 2] = 255;
79 bytes[idx + 3] = 255;
80 }
81 }
82 }
83
84 let atlas_width_f = atlas_w as f32;
86 let atlas_height_f = atlas_h as f32;
87 glyphs[c as usize] = Some(GlyphMetrics {
88 u_min: cell_x as f32 / atlas_width_f,
89 v_min: cell_y as f32 / atlas_height_f,
90 u_max: (cell_x + CELL) as f32 / atlas_width_f,
91 v_max: (cell_y + CELL) as f32 / atlas_height_f,
92 });
93 }
94
95 let atlas = Texture::from_rgba(app, atlas_w, atlas_h, &bytes);
96
97 Self {
98 inner: Arc::new(FontInner {
99 atlas,
100 glyphs,
101 cell_pixels: CELL,
102 }),
103 }
104 }
105
106 #[must_use]
108 pub fn cell_pixels(&self) -> u32 {
109 self.inner.cell_pixels
110 }
111
112 pub(crate) fn atlas(&self) -> &Texture {
113 &self.inner.atlas
114 }
115
116 pub(crate) fn glyph(&self, c: char) -> Option<GlyphMetrics> {
117 let cp = u32::from(c);
118 if cp >= 128 {
119 return None;
120 }
121 self.inner.glyphs[cp as usize]
122 }
123}
124
125impl std::fmt::Debug for Font {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 f.debug_struct("Font")
128 .field("cell_pixels", &self.inner.cell_pixels)
129 .finish_non_exhaustive()
130 }
131}
132
133#[derive(Debug, Clone)]
138pub struct Text {
139 pub container: Container,
141 pub content: String,
143 pub font: Font,
145 pub color: Color,
147 pub cell_size: f32,
149}
150
151impl Text {
152 #[must_use]
154 pub fn new(font: Font, content: impl Into<String>) -> Self {
155 Self {
156 container: Container::default(),
157 content: content.into(),
158 font,
159 color: Color::WHITE,
160 cell_size: 0.02,
161 }
162 }
163
164 #[must_use]
166 pub fn with_color(mut self, color: Color) -> Self {
167 self.color = color;
168 self
169 }
170
171 #[must_use]
173 pub fn with_cell_size(mut self, size: f32) -> Self {
174 self.cell_size = size;
175 self
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::application::{AppConfig, Application};
183
184 fn boot() -> Application {
185 pollster::block_on(Application::new(AppConfig::default())).expect("init")
186 }
187
188 #[test]
189 fn font_has_ascii_glyphs() {
190 let app = boot();
191 let font = Font::bitmap_8x8(&app);
192 assert!(font.glyph('A').is_some());
193 assert!(font.glyph('z').is_some());
194 assert!(font.glyph(' ').is_some());
195 assert!(font.glyph('é').is_none());
197 }
198
199 #[test]
200 fn text_defaults() {
201 let app = boot();
202 let font = Font::bitmap_8x8(&app);
203 let text = Text::new(font, "Hello");
204 assert_eq!(text.content, "Hello");
205 assert_eq!(text.color, Color::WHITE);
206 assert!((text.cell_size - 0.02).abs() < f32::EPSILON);
207 }
208
209 #[test]
210 fn builders_set_fields() {
211 let app = boot();
212 let font = Font::bitmap_8x8(&app);
213 let text = Text::new(font, "X")
214 .with_color(Color::RED)
215 .with_cell_size(0.05);
216 assert_eq!(text.color, Color::RED);
217 assert!((text.cell_size - 0.05).abs() < f32::EPSILON);
218 }
219}