Skip to main content

ling_graphics/
font.rs

1use crate::color::Color;
2use crate::geometry::{Mesh, Vertex};
3use crate::material::TextureData;
4use fontdue::{Font, FontSettings};
5use glam::{Vec2, Vec3};
6use std::collections::HashMap;
7
8// ── Glyph metrics after rasterization ────────────────────────────────────────
9
10#[derive(Debug, Clone)]
11pub struct GlyphInfo {
12    /// UV rect in the atlas texture.
13    pub uv_min: Vec2,
14    pub uv_max: Vec2,
15    /// Pixel size of the glyph bitmap.
16    pub size: Vec2,
17    /// Left/bottom bearing in pixels.
18    pub bearing: Vec2,
19    /// How far to advance the cursor after this glyph.
20    pub advance: f32,
21}
22
23impl GlyphInfo {
24    fn empty(advance: f32) -> Self {
25        Self {
26            uv_min: Vec2::ZERO,
27            uv_max: Vec2::ZERO,
28            size: Vec2::ZERO,
29            bearing: Vec2::ZERO,
30            advance,
31        }
32    }
33}
34
35// ── Font atlas ────────────────────────────────────────────────────────────────
36
37pub struct FontAtlas {
38    font: Font,
39    pub texture: TextureData,
40    glyphs: HashMap<(u32, u32), GlyphInfo>, // (char as u32, px.to_bits())
41    cursor_x: usize,
42    cursor_y: usize,
43    row_height: usize,
44}
45
46impl FontAtlas {
47    /// Load a TrueType/OpenType font from raw bytes. `atlas_size` must be a power of two.
48    pub fn from_bytes(font_data: &[u8], atlas_size: usize) -> Result<Self, String> {
49        let font =
50            Font::from_bytes(font_data, FontSettings::default()).map_err(|e| e.to_string())?;
51        let texture = TextureData::new(atlas_size, atlas_size);
52        Ok(Self {
53            font,
54            texture,
55            glyphs: HashMap::new(),
56            cursor_x: 1,
57            cursor_y: 1,
58            row_height: 0,
59        })
60    }
61
62    /// Retrieve (and rasterize if missing) glyph info for a char at a given pixel size.
63    pub fn get_or_rasterize(&mut self, c: char, px: f32) -> GlyphInfo {
64        let key = (c as u32, px.to_bits());
65        if let Some(g) = self.glyphs.get(&key) {
66            return g.clone();
67        }
68        self.rasterize_glyph(c, px);
69        self.glyphs
70            .get(&key)
71            .cloned()
72            .unwrap_or_else(|| GlyphInfo::empty(px * 0.5))
73    }
74
75    fn rasterize_glyph(&mut self, c: char, px: f32) {
76        let (metrics, bitmap) = self.font.rasterize(c, px);
77
78        if metrics.width == 0 || metrics.height == 0 {
79            let key = (c as u32, px.to_bits());
80            self.glyphs
81                .insert(key, GlyphInfo::empty(metrics.advance_width));
82            return;
83        }
84
85        let aw = self.texture.width;
86        let ah = self.texture.height;
87
88        if self.cursor_x + metrics.width + 1 > aw {
89            self.cursor_x = 1;
90            self.cursor_y += self.row_height + 1;
91            self.row_height = 0;
92        }
93
94        if self.cursor_y + metrics.height + 1 > ah {
95            // Atlas full — insert a dummy entry so we don't retry endlessly
96            let key = (c as u32, px.to_bits());
97            self.glyphs
98                .insert(key, GlyphInfo::empty(metrics.advance_width));
99            return;
100        }
101
102        for gy in 0..metrics.height {
103            for gx in 0..metrics.width {
104                let alpha = bitmap[gy * metrics.width + gx];
105                let dx = self.cursor_x + gx;
106                let dy = self.cursor_y + gy;
107                let idx = (dy * aw + dx) * 4;
108                self.texture.data[idx] = 255;
109                self.texture.data[idx + 1] = 255;
110                self.texture.data[idx + 2] = 255;
111                self.texture.data[idx + 3] = alpha;
112            }
113        }
114
115        let uv_min = Vec2::new(
116            self.cursor_x as f32 / aw as f32,
117            self.cursor_y as f32 / ah as f32,
118        );
119        let uv_max = Vec2::new(
120            (self.cursor_x + metrics.width) as f32 / aw as f32,
121            (self.cursor_y + metrics.height) as f32 / ah as f32,
122        );
123
124        self.row_height = self.row_height.max(metrics.height);
125        self.cursor_x += metrics.width + 1;
126
127        let key = (c as u32, px.to_bits());
128        self.glyphs.insert(
129            key,
130            GlyphInfo {
131                uv_min,
132                uv_max,
133                size: Vec2::new(metrics.width as f32, metrics.height as f32),
134                bearing: Vec2::new(metrics.xmin as f32, metrics.ymin as f32),
135                advance: metrics.advance_width,
136            },
137        );
138    }
139}
140
141// ── Text mesh generation ──────────────────────────────────────────────────────
142
143/// Generate a flat Mesh (quads) for `text` in the XY plane, using the given font atlas.
144/// The mesh origin is at the left baseline. Scale with a Transform to place in 3D/4D space.
145pub fn generate_text_mesh(atlas: &mut FontAtlas, text: &str, px: f32, color: Color) -> Mesh {
146    let mut vertices = Vec::new();
147    let mut indices = Vec::new();
148    let mut cursor_x = 0.0f32;
149
150    for ch in text.chars() {
151        let info = atlas.get_or_rasterize(ch, px);
152        if info.size.x > 0.0 && info.size.y > 0.0 {
153            let x0 = cursor_x + info.bearing.x;
154            let y0 = info.bearing.y;
155            let x1 = x0 + info.size.x;
156            let y1 = y0 + info.size.y;
157
158            let base = vertices.len() as u32;
159            vertices.push(Vertex {
160                position: Vec3::new(x0, y0, 0.0),
161                normal: Vec3::Z,
162                uv: info.uv_min,
163                color,
164                tangent: Vec3::X,
165            });
166            vertices.push(Vertex {
167                position: Vec3::new(x1, y0, 0.0),
168                normal: Vec3::Z,
169                uv: Vec2::new(info.uv_max.x, info.uv_min.y),
170                color,
171                tangent: Vec3::X,
172            });
173            vertices.push(Vertex {
174                position: Vec3::new(x1, y1, 0.0),
175                normal: Vec3::Z,
176                uv: info.uv_max,
177                color,
178                tangent: Vec3::X,
179            });
180            vertices.push(Vertex {
181                position: Vec3::new(x0, y1, 0.0),
182                normal: Vec3::Z,
183                uv: Vec2::new(info.uv_min.x, info.uv_max.y),
184                color,
185                tangent: Vec3::X,
186            });
187
188            indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
189        }
190        cursor_x += info.advance;
191    }
192
193    Mesh::new(vertices, indices)
194}
195
196/// Measure the pixel-width of a string without rasterizing.
197pub fn measure_text(atlas: &mut FontAtlas, text: &str, px: f32) -> f32 {
198    text.chars()
199        .map(|c| atlas.get_or_rasterize(c, px).advance)
200        .sum()
201}
202
203// ── Direct framebuffer glyph rendering ────────────────────────────────────────
204//
205// `GlyphFont` rasterizes TrueType/OpenType glyphs with fontdue and alpha-blends
206// them straight into a packed `0x00RRGGBB` software framebuffer (the format used
207// by the native Ling window). This powers the per-language UI fonts: load a
208// TTF once, then draw text in any color with crisp anti-aliased coverage.
209
210/// A loaded font that blits glyphs directly to a `u32` framebuffer.
211pub struct GlyphFont {
212    font: Font,
213    /// Cache of rasterized bitmaps keyed by (char, px.to_bits()) → (metrics, coverage).
214    cache: HashMap<(u32, u32), (fontdue::Metrics, Vec<u8>)>,
215}
216
217impl GlyphFont {
218    /// Load from raw TTF/OTF bytes.
219    pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
220        let font = Font::from_bytes(data, FontSettings::default()).map_err(|e| e.to_string())?;
221        Ok(Self { font, cache: HashMap::new() })
222    }
223
224    /// Load from a file path.
225    pub fn from_path(path: &str) -> Result<Self, String> {
226        let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
227        Self::from_bytes(&bytes)
228    }
229
230    fn glyph(&mut self, c: char, px: f32) -> &(fontdue::Metrics, Vec<u8>) {
231        let key = (c as u32, px.to_bits());
232        self.cache
233            .entry(key)
234            .or_insert_with(|| self.font.rasterize(c, px))
235    }
236
237    /// Total advance width of `text` at size `px`, in pixels.
238    pub fn measure(&mut self, text: &str, px: f32) -> f32 {
239        text.chars()
240            .map(|c| self.font.metrics(c, px).advance_width)
241            .sum()
242    }
243
244    /// Draw `text` into a packed `0x00RRGGBB` framebuffer of `w`×`h` pixels.
245    ///
246    /// `(x, y)` is the top-left of the text box (matching the vector `ui_text`
247    /// convention): the baseline is placed `px` below `y` via the font's ascent.
248    /// Each glyph's coverage alpha-blends `color` over the existing pixels.
249    #[allow(clippy::too_many_arguments)]
250    pub fn draw_text(
251        &mut self,
252        buf: &mut [u32],
253        w: usize,
254        h: usize,
255        x: f32,
256        y: f32,
257        px: f32,
258        color: u32,
259        text: &str,
260    ) {
261        if w == 0 || h == 0 || px <= 0.0 {
262            return;
263        }
264        let lm = self.font.horizontal_line_metrics(px);
265        let ascent = lm.map(|m| m.ascent).unwrap_or(px * 0.8);
266        let baseline = y + ascent;
267
268        let cr = ((color >> 16) & 0xFF) as f32;
269        let cg = ((color >> 8) & 0xFF) as f32;
270        let cb = (color & 0xFF) as f32;
271
272        let mut pen_x = x;
273        for c in text.chars() {
274            let (metrics, bitmap) = self.glyph(c, px).clone();
275            if metrics.width > 0 && metrics.height > 0 {
276                // Top-left of this glyph's bitmap in screen space.
277                let gx0 = (pen_x + metrics.xmin as f32).round() as i32;
278                let gy0 = (baseline - metrics.ymin as f32 - metrics.height as f32).round() as i32;
279                for gy in 0..metrics.height {
280                    let py = gy0 + gy as i32;
281                    if py < 0 || py as usize >= h {
282                        continue;
283                    }
284                    let row = py as usize * w;
285                    for gx in 0..metrics.width {
286                        let px_ = gx0 + gx as i32;
287                        if px_ < 0 || px_ as usize >= w {
288                            continue;
289                        }
290                        let a = bitmap[gy * metrics.width + gx] as f32 / 255.0;
291                        if a <= 0.0 {
292                            continue;
293                        }
294                        let idx = row + px_ as usize;
295                        let dst = buf[idx];
296                        let dr = ((dst >> 16) & 0xFF) as f32;
297                        let dg = ((dst >> 8) & 0xFF) as f32;
298                        let db = (dst & 0xFF) as f32;
299                        let nr = (cr * a + dr * (1.0 - a)).min(255.0) as u32;
300                        let ng = (cg * a + dg * (1.0 - a)).min(255.0) as u32;
301                        let nb = (cb * a + db * (1.0 - a)).min(255.0) as u32;
302                        buf[idx] = (nr << 16) | (ng << 8) | nb;
303                    }
304                }
305            }
306            pen_x += metrics.advance_width;
307        }
308    }
309}