Skip to main content

ling_graphics/
font.rs

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