diff --git a/crates/voltex_editor/src/lib.rs b/crates/voltex_editor/src/lib.rs index f12304d..45a711c 100644 --- a/crates/voltex_editor/src/lib.rs +++ b/crates/voltex_editor/src/lib.rs @@ -29,3 +29,5 @@ pub use asset_browser::{AssetBrowser, asset_browser_panel}; pub mod ttf_parser; pub mod rasterizer; pub mod glyph_cache; +pub mod ttf_font; +pub use ttf_font::TtfFont; diff --git a/crates/voltex_editor/src/ttf_font.rs b/crates/voltex_editor/src/ttf_font.rs new file mode 100644 index 0000000..ec80c3d --- /dev/null +++ b/crates/voltex_editor/src/ttf_font.rs @@ -0,0 +1,153 @@ +use crate::ttf_parser::TtfParser; +use crate::rasterizer::rasterize; +use crate::glyph_cache::{GlyphCache, GlyphInfo, ATLAS_SIZE}; + +pub struct TtfFont { + parser: TtfParser, + cache: GlyphCache, + pub font_size: f32, + pub line_height: f32, + pub ascender: f32, + pub descender: f32, + scale: f32, +} + +impl TtfFont { + pub fn new(data: &[u8], font_size: f32) -> Result { + let parser = TtfParser::parse(data.to_vec())?; + let scale = font_size / parser.units_per_em as f32; + let ascender = parser.ascender as f32 * scale; + let descender = parser.descender as f32 * scale; + let line_height = (parser.ascender - parser.descender + parser.line_gap) as f32 * scale; + let cache = GlyphCache::new(ATLAS_SIZE, ATLAS_SIZE); + + Ok(TtfFont { + parser, + cache, + font_size, + line_height, + ascender, + descender, + scale, + }) + } + + /// Get glyph info for a character, rasterizing on cache miss. + pub fn glyph(&mut self, ch: char) -> &GlyphInfo { + if self.cache.get(ch).is_some() { + return self.cache.get(ch).unwrap(); + } + + let glyph_id = self.parser.glyph_index(ch as u32); + let metrics = self.parser.glyph_metrics(glyph_id); + let advance = metrics.advance_width as f32 * self.scale; + let bearing_x = metrics.left_side_bearing as f32 * self.scale; + + let outline = self.parser.glyph_outline(glyph_id); + + match outline { + Some(ref o) if !o.contours.is_empty() => { + let result = rasterize(o, self.scale); + // bearing_y = y_max * scale (distance above baseline) + // When rendering: glyph_y = baseline_y - bearing_y + // For our UI: text_y is the top of the line, baseline = text_y + ascender + // So: glyph_y = text_y + ascender - bearing_y + let bearing_y = o.y_max as f32 * self.scale; + + self.cache.insert( + ch, + &result.bitmap, + result.width, + result.height, + advance, + bearing_x, + bearing_y, + ); + } + _ => { + // No outline (space, etc.) — insert empty glyph + self.cache.insert(ch, &[], 0, 0, advance, 0.0, 0.0); + } + } + + self.cache.get(ch).unwrap() + } + + /// Calculate total width of a text string. + pub fn text_width(&mut self, text: &str) -> f32 { + let mut width = 0.0; + for ch in text.chars() { + let info = self.glyph(ch); + width += info.advance; + } + width + } + + pub fn atlas_data(&self) -> &[u8] { + &self.cache.atlas_data + } + + pub fn atlas_size(&self) -> (u32, u32) { + (self.cache.atlas_width, self.cache.atlas_height) + } + + pub fn is_dirty(&self) -> bool { + self.cache.dirty + } + + pub fn clear_dirty(&mut self) { + self.cache.clear_dirty(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn load_test_font() -> Option { + let paths = ["C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/consola.ttf"]; + for p in &paths { + if let Ok(data) = std::fs::read(p) { + if let Ok(font) = TtfFont::new(&data, 24.0) { + return Some(font); + } + } + } + None + } + + #[test] + fn test_new() { + let font = load_test_font().expect("no test font"); + assert!(font.font_size == 24.0); + assert!(font.ascender > 0.0); + assert!(font.line_height > 0.0); + } + + #[test] + fn test_glyph_caches() { + let mut font = load_test_font().expect("no test font"); + let _g1 = font.glyph('A'); + assert!(font.is_dirty()); + font.clear_dirty(); + let _g2 = font.glyph('A'); // cache hit + assert!(!font.is_dirty()); + } + + #[test] + fn test_text_width() { + let mut font = load_test_font().expect("no test font"); + let w1 = font.text_width("Hello"); + let w2 = font.text_width("Hello World"); + assert!(w1 > 0.0); + assert!(w2 > w1); + } + + #[test] + fn test_space_glyph() { + let mut font = load_test_font().expect("no test font"); + let info = font.glyph(' '); + assert!(info.advance > 0.0); // space has advance but no bitmap + assert!(info.width == 0.0); + } +}