Skip to main content

proof_engine/editor/
font_editor.rs

1
2//! Font and text editor — font atlas generation, glyph metrics, text layout, rich text.
3
4use glam::{Vec2, Vec4};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Unicode / glyph
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy)]
12pub struct GlyphMetrics {
13    pub codepoint: u32,
14    pub advance_x: f32,
15    pub advance_y: f32,
16    pub bearing_x: f32,
17    pub bearing_y: f32,
18    pub width: f32,
19    pub height: f32,
20    pub uv_min: Vec2,
21    pub uv_max: Vec2,
22    pub atlas_page: u32,
23    pub is_fallback: bool,
24}
25
26impl GlyphMetrics {
27    pub fn new(cp: u32, advance: f32, bearing_x: f32, bearing_y: f32, w: f32, h: f32) -> Self {
28        Self {
29            codepoint: cp,
30            advance_x: advance,
31            advance_y: 0.0,
32            bearing_x,
33            bearing_y,
34            width: w,
35            height: h,
36            uv_min: Vec2::ZERO,
37            uv_max: Vec2::new(w / 512.0, h / 512.0),
38            atlas_page: 0,
39            is_fallback: false,
40        }
41    }
42
43    pub fn draw_rect(&self, cursor: Vec2, scale: f32) -> [Vec2; 2] {
44        let x = cursor.x + self.bearing_x * scale;
45        let y = cursor.y - self.bearing_y * scale;
46        [Vec2::new(x, y), Vec2::new(x + self.width * scale, y + self.height * scale)]
47    }
48}
49
50// ---------------------------------------------------------------------------
51// Kerning
52// ---------------------------------------------------------------------------
53
54#[derive(Debug, Clone)]
55pub struct KerningPair {
56    pub first: u32,
57    pub second: u32,
58    pub amount_x: f32,
59    pub amount_y: f32,
60}
61
62// ---------------------------------------------------------------------------
63// Font face
64// ---------------------------------------------------------------------------
65
66#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum FontStyle {
68    Regular, Bold, Italic, BoldItalic,
69}
70
71impl FontStyle {
72    pub fn label(self) -> &'static str {
73        match self { FontStyle::Regular => "Regular", FontStyle::Bold => "Bold", FontStyle::Italic => "Italic", FontStyle::BoldItalic => "Bold Italic" }
74    }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum FontHinting { None, Slight, Normal, Full }
79
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub enum FontRenderMode { Bitmap, SDF, MSDF, MTSDF }
82
83impl FontRenderMode {
84    pub fn label(self) -> &'static str {
85        match self { FontRenderMode::Bitmap => "Bitmap", FontRenderMode::SDF => "SDF", FontRenderMode::MSDF => "MSDF", FontRenderMode::MTSDF => "MTSDF" }
86    }
87    pub fn supports_smooth_scaling(self) -> bool { !matches!(self, FontRenderMode::Bitmap) }
88}
89
90#[derive(Debug, Clone)]
91pub struct FontFace {
92    pub family: String,
93    pub style: FontStyle,
94    pub units_per_em: u32,
95    pub ascent: f32,
96    pub descent: f32,
97    pub line_gap: f32,
98    pub cap_height: f32,
99    pub x_height: f32,
100    pub underline_position: f32,
101    pub underline_thickness: f32,
102    pub strikethrough_position: f32,
103    pub strikethrough_thickness: f32,
104    pub glyphs: HashMap<u32, GlyphMetrics>,
105    pub kerning: Vec<KerningPair>,
106    pub render_mode: FontRenderMode,
107    pub hinting: FontHinting,
108    pub atlas_size: u32,
109    pub sdf_range: f32,
110    pub tab_width: u32,
111}
112
113impl FontFace {
114    pub fn new(family: impl Into<String>, style: FontStyle) -> Self {
115        Self {
116            family: family.into(),
117            style,
118            units_per_em: 2048,
119            ascent: 1890.0,
120            descent: -434.0,
121            line_gap: 0.0,
122            cap_height: 1462.0,
123            x_height: 1098.0,
124            underline_position: -150.0,
125            underline_thickness: 100.0,
126            strikethrough_position: 530.0,
127            strikethrough_thickness: 100.0,
128            glyphs: HashMap::new(),
129            kerning: Vec::new(),
130            render_mode: FontRenderMode::MSDF,
131            hinting: FontHinting::Normal,
132            atlas_size: 512,
133            sdf_range: 6.0,
134            tab_width: 4,
135        }
136    }
137
138    pub fn line_height(&self, size: f32) -> f32 {
139        (self.ascent - self.descent + self.line_gap) * size / self.units_per_em as f32
140    }
141
142    pub fn scale_for_size(&self, size: f32) -> f32 {
143        size / self.units_per_em as f32
144    }
145
146    pub fn kerning_for_pair(&self, first: u32, second: u32) -> f32 {
147        self.kerning.iter()
148            .find(|k| k.first == first && k.second == second)
149            .map(|k| k.amount_x)
150            .unwrap_or(0.0)
151    }
152
153    pub fn glyph(&self, codepoint: u32) -> Option<&GlyphMetrics> {
154        self.glyphs.get(&codepoint)
155    }
156
157    /// Populate with synthetic ASCII glyphs.
158    pub fn populate_ascii(&mut self) {
159        for cp in 32u32..128 {
160            let w = if cp == 32 { 0.0 } else { 800.0 + (cp as f32 * 13.0) % 400.0 };
161            let h = 1300.0_f32;
162            let bearing_x = 50.0_f32;
163            let bearing_y = 1000.0_f32;
164            let advance = w + bearing_x + 100.0;
165            let mut g = GlyphMetrics::new(cp, advance, bearing_x, bearing_y, w, h);
166            g.uv_min = Vec2::new((cp % 16) as f32 / 16.0, (cp / 16) as f32 / 8.0);
167            g.uv_max = g.uv_min + Vec2::new(w / (self.atlas_size as f32 * 128.0), h / (self.atlas_size as f32 * 64.0));
168            self.glyphs.insert(cp, g);
169        }
170        // Some kerning pairs
171        self.kerning.push(KerningPair { first: b'A' as u32, second: b'V' as u32, amount_x: -120.0, amount_y: 0.0 });
172        self.kerning.push(KerningPair { first: b'T' as u32, second: b'a' as u32, amount_x: -100.0, amount_y: 0.0 });
173        self.kerning.push(KerningPair { first: b'f' as u32, second: b'i' as u32, amount_x: -50.0, amount_y: 0.0 });
174    }
175
176    pub fn measure_text(&self, text: &str, size: f32) -> Vec2 {
177        let scale = self.scale_for_size(size);
178        let mut width = 0.0_f32;
179        let mut max_width = 0.0_f32;
180        let mut lines = 1u32;
181        let mut prev_cp: Option<u32> = None;
182        for ch in text.chars() {
183            if ch == '\n' {
184                lines += 1;
185                max_width = max_width.max(width);
186                width = 0.0;
187                prev_cp = None;
188                continue;
189            }
190            let cp = ch as u32;
191            let kern = prev_cp.map(|p| self.kerning_for_pair(p, cp)).unwrap_or(0.0);
192            let advance = self.glyphs.get(&cp).map(|g| g.advance_x).unwrap_or(600.0);
193            width += (advance + kern) * scale;
194            prev_cp = Some(cp);
195        }
196        max_width = max_width.max(width);
197        Vec2::new(max_width, self.line_height(size) * lines as f32)
198    }
199}
200
201// ---------------------------------------------------------------------------
202// Rich text spans
203// ---------------------------------------------------------------------------
204
205#[derive(Debug, Clone, Copy, PartialEq)]
206pub enum TextDecoration { None, Underline, Strikethrough, Overline, DottedUnderline }
207
208#[derive(Debug, Clone)]
209pub struct TextSpan {
210    pub text: String,
211    pub font_size: f32,
212    pub color: Vec4,
213    pub font_family: Option<String>,
214    pub style: FontStyle,
215    pub decoration: TextDecoration,
216    pub letter_spacing: f32,
217    pub word_spacing: f32,
218    pub line_height_multiplier: f32,
219    pub baseline_shift: f32,
220    pub bold: bool,
221    pub italic: bool,
222    pub shadow: Option<TextShadow>,
223    pub outline: Option<TextOutline>,
224    pub link: Option<String>,
225}
226
227#[derive(Debug, Clone, Copy)]
228pub struct TextShadow {
229    pub offset: Vec2,
230    pub blur: f32,
231    pub color: Vec4,
232}
233
234#[derive(Debug, Clone, Copy)]
235pub struct TextOutline {
236    pub width: f32,
237    pub color: Vec4,
238}
239
240impl Default for TextSpan {
241    fn default() -> Self {
242        Self {
243            text: String::new(),
244            font_size: 16.0,
245            color: Vec4::new(1.0, 1.0, 1.0, 1.0),
246            font_family: None,
247            style: FontStyle::Regular,
248            decoration: TextDecoration::None,
249            letter_spacing: 0.0,
250            word_spacing: 0.0,
251            line_height_multiplier: 1.2,
252            baseline_shift: 0.0,
253            bold: false,
254            italic: false,
255            shadow: None,
256            outline: None,
257            link: None,
258        }
259    }
260}
261
262// ---------------------------------------------------------------------------
263// Text layout
264// ---------------------------------------------------------------------------
265
266#[derive(Debug, Clone, Copy, PartialEq)]
267pub enum TextAlign { Left, Center, Right, Justify }
268#[derive(Debug, Clone, Copy, PartialEq)]
269pub enum VerticalAlign { Top, Middle, Bottom, Baseline }
270#[derive(Debug, Clone, Copy, PartialEq)]
271pub enum TextOverflow { Clip, Ellipsis, Scroll, Visible }
272#[derive(Debug, Clone, Copy, PartialEq)]
273pub enum WordWrap { NoWrap, Normal, BreakWord, BreakAll }
274#[derive(Debug, Clone, Copy, PartialEq)]
275pub enum TextDirection { Ltr, Rtl, Auto }
276
277#[derive(Debug, Clone)]
278pub struct TextLayoutSettings {
279    pub align: TextAlign,
280    pub vertical_align: VerticalAlign,
281    pub overflow: TextOverflow,
282    pub word_wrap: WordWrap,
283    pub direction: TextDirection,
284    pub max_width: Option<f32>,
285    pub max_height: Option<f32>,
286    pub tab_size: f32,
287    pub paragraph_spacing: f32,
288    pub indent: f32,
289    pub enable_ligatures: bool,
290    pub enable_smart_quotes: bool,
291    pub auto_size: bool,
292    pub auto_size_min: f32,
293    pub auto_size_max: f32,
294}
295
296impl Default for TextLayoutSettings {
297    fn default() -> Self {
298        Self {
299            align: TextAlign::Left,
300            vertical_align: VerticalAlign::Top,
301            overflow: TextOverflow::Clip,
302            word_wrap: WordWrap::Normal,
303            direction: TextDirection::Auto,
304            max_width: None,
305            max_height: None,
306            tab_size: 32.0,
307            paragraph_spacing: 0.0,
308            indent: 0.0,
309            enable_ligatures: true,
310            enable_smart_quotes: false,
311            auto_size: false,
312            auto_size_min: 8.0,
313            auto_size_max: 72.0,
314        }
315    }
316}
317
318#[derive(Debug, Clone)]
319pub struct LayoutGlyph {
320    pub codepoint: u32,
321    pub position: Vec2,
322    pub size: Vec2,
323    pub uv_min: Vec2,
324    pub uv_max: Vec2,
325    pub color: Vec4,
326    pub atlas_page: u32,
327    pub line_index: u32,
328    pub span_index: u32,
329}
330
331#[derive(Debug, Clone)]
332pub struct TextLayoutResult {
333    pub glyphs: Vec<LayoutGlyph>,
334    pub line_starts: Vec<usize>,
335    pub total_size: Vec2,
336    pub line_count: u32,
337    pub overflows: bool,
338}
339
340impl TextLayoutResult {
341    pub fn empty() -> Self {
342        Self { glyphs: Vec::new(), line_starts: Vec::new(), total_size: Vec2::ZERO, line_count: 0, overflows: false }
343    }
344
345    pub fn glyph_count(&self) -> usize { self.glyphs.len() }
346}
347
348pub fn layout_text(spans: &[TextSpan], font: &FontFace, settings: &TextLayoutSettings) -> TextLayoutResult {
349    let mut result = TextLayoutResult::empty();
350    let max_width = settings.max_width.unwrap_or(f32::INFINITY);
351    let mut cursor = Vec2::ZERO;
352    let mut line_idx = 0u32;
353    result.line_starts.push(0);
354    for (si, span) in spans.iter().enumerate() {
355        let scale = font.scale_for_size(span.font_size);
356        let line_h = font.line_height(span.font_size) * span.line_height_multiplier;
357        let mut word_start = result.glyphs.len();
358        let mut word_width = 0.0_f32;
359        for ch in span.text.chars() {
360            if ch == '\n' {
361                cursor.x = 0.0;
362                cursor.y += line_h;
363                line_idx += 1;
364                result.line_starts.push(result.glyphs.len());
365                continue;
366            }
367            let cp = ch as u32;
368            let glyph = font.glyph(cp);
369            let advance = glyph.map(|g| g.advance_x).unwrap_or(600.0) * scale + span.letter_spacing;
370            // Word-wrap
371            if settings.word_wrap == WordWrap::Normal && cursor.x + advance > max_width {
372                // Move to next line
373                cursor.x = 0.0;
374                cursor.y += line_h;
375                line_idx += 1;
376                result.line_starts.push(result.glyphs.len());
377            }
378            let (uv_min, uv_max, w, h, bx, by) = if let Some(g) = glyph {
379                (g.uv_min, g.uv_max, g.width * scale, g.height * scale, g.bearing_x * scale, g.bearing_y * scale)
380            } else { (Vec2::ZERO, Vec2::ONE / 512.0, advance, span.font_size, 0.0, span.font_size) };
381            result.glyphs.push(LayoutGlyph {
382                codepoint: cp,
383                position: Vec2::new(cursor.x + bx, cursor.y - by + span.baseline_shift),
384                size: Vec2::new(w, h),
385                uv_min, uv_max,
386                color: span.color,
387                atlas_page: 0,
388                line_index: line_idx,
389                span_index: si as u32,
390            });
391            cursor.x += advance;
392        }
393        // Track max width
394        result.total_size.x = result.total_size.x.max(cursor.x);
395    }
396    result.total_size.y = cursor.y + spans.last().map(|s| font.line_height(s.font_size)).unwrap_or(0.0);
397    result.line_count = line_idx + 1;
398    if let Some(mw) = settings.max_width { if result.total_size.x > mw { result.overflows = true; } }
399    if let Some(mh) = settings.max_height { if result.total_size.y > mh { result.overflows = true; } }
400    result
401}
402
403// ---------------------------------------------------------------------------
404// Font atlas packer
405// ---------------------------------------------------------------------------
406
407#[derive(Debug, Clone, Copy, PartialEq)]
408pub enum AtlasPackingMode { Shelf, Skyline, Guillotine, MaxRects }
409
410#[derive(Debug, Clone)]
411pub struct AtlasPackRect {
412    pub id: u32,
413    pub x: u32,
414    pub y: u32,
415    pub width: u32,
416    pub height: u32,
417}
418
419#[derive(Debug, Clone)]
420pub struct AtlasPacker {
421    pub atlas_width: u32,
422    pub atlas_height: u32,
423    pub mode: AtlasPackingMode,
424    pub padding: u32,
425    pub rects: Vec<AtlasPackRect>,
426    pub current_shelf_y: u32,
427    pub current_shelf_x: u32,
428    pub current_shelf_height: u32,
429    pub pages: u32,
430}
431
432impl AtlasPacker {
433    pub fn new(width: u32, height: u32) -> Self {
434        Self {
435            atlas_width: width, atlas_height: height,
436            mode: AtlasPackingMode::Shelf, padding: 1,
437            rects: Vec::new(), current_shelf_y: 0,
438            current_shelf_x: 0, current_shelf_height: 0, pages: 1,
439        }
440    }
441
442    pub fn pack(&mut self, id: u32, width: u32, height: u32) -> Option<AtlasPackRect> {
443        let pw = width + self.padding;
444        let ph = height + self.padding;
445        // Simple shelf algorithm
446        if self.current_shelf_x + pw > self.atlas_width {
447            self.current_shelf_y += self.current_shelf_height;
448            self.current_shelf_x = 0;
449            self.current_shelf_height = 0;
450            if self.current_shelf_y + ph > self.atlas_height {
451                self.pages += 1;
452                self.current_shelf_y = 0;
453            }
454        }
455        let r = AtlasPackRect { id, x: self.current_shelf_x, y: self.current_shelf_y, width, height };
456        self.current_shelf_x += pw;
457        self.current_shelf_height = self.current_shelf_height.max(ph);
458        self.rects.push(r.clone());
459        Some(r)
460    }
461
462    pub fn reset(&mut self) {
463        self.rects.clear();
464        self.current_shelf_x = 0;
465        self.current_shelf_y = 0;
466        self.current_shelf_height = 0;
467        self.pages = 1;
468    }
469
470    pub fn utilization(&self) -> f32 {
471        let used: u32 = self.rects.iter().map(|r| r.width * r.height).sum();
472        let total = self.atlas_width * self.atlas_height * self.pages;
473        used as f32 / total as f32
474    }
475}
476
477// ---------------------------------------------------------------------------
478// Font asset / registry
479// ---------------------------------------------------------------------------
480
481#[derive(Debug, Clone)]
482pub struct FontAsset {
483    pub id: u64,
484    pub family: String,
485    pub faces: Vec<FontFace>,
486    pub atlas_packer: AtlasPacker,
487    pub fallback_font: Option<u64>,
488}
489
490impl FontAsset {
491    pub fn new(id: u64, family: impl Into<String>) -> Self {
492        let family_str: String = family.into();
493        let mut regular = FontFace::new(family_str.clone(), FontStyle::Regular);
494        regular.populate_ascii();
495        let mut bold = FontFace::new(family_str.clone(), FontStyle::Bold);
496        bold.populate_ascii();
497        let mut italic = FontFace::new(family_str.clone(), FontStyle::Italic);
498        italic.populate_ascii();
499        Self {
500            id,
501            family: family_str,
502            faces: vec![regular, bold, italic],
503            atlas_packer: AtlasPacker::new(512, 512),
504            fallback_font: None,
505        }
506    }
507
508    pub fn get_face(&self, style: FontStyle) -> Option<&FontFace> {
509        self.faces.iter().find(|f| f.style == style)
510    }
511
512    pub fn measure_text(&self, text: &str, size: f32, style: FontStyle) -> Vec2 {
513        self.get_face(style).map(|f| f.measure_text(text, size)).unwrap_or(Vec2::ZERO)
514    }
515}
516
517// ---------------------------------------------------------------------------
518// Font editor
519// ---------------------------------------------------------------------------
520
521#[derive(Debug, Clone, Copy, PartialEq)]
522pub enum FontEditorTab { Preview, GlyphMap, AtlasViewer, Metrics, KerningTable, Settings }
523
524#[derive(Debug, Clone)]
525pub struct FontEditorState {
526    pub fonts: Vec<FontAsset>,
527    pub selected_font: Option<u64>,
528    pub selected_glyph: Option<u32>,
529    pub active_tab: FontEditorTab,
530    pub preview_text: String,
531    pub preview_size: f32,
532    pub preview_style: FontStyle,
533    pub preview_color: Vec4,
534    pub preview_background: Vec4,
535    pub layout_settings: TextLayoutSettings,
536    pub show_baseline: bool,
537    pub show_ascender: bool,
538    pub show_descender: bool,
539    pub show_cap_height: bool,
540    pub show_x_height: bool,
541    pub show_advance: bool,
542    pub show_bearings: bool,
543    pub atlas_zoom: f32,
544    pub search_query: String,
545    pub unicode_page: u32,
546}
547
548impl FontEditorState {
549    pub fn new() -> Self {
550        let fonts = vec![
551            FontAsset::new(1, "Inter"),
552            FontAsset::new(2, "JetBrains Mono"),
553            FontAsset::new(3, "Roboto"),
554        ];
555        Self {
556            fonts,
557            selected_font: Some(1),
558            selected_glyph: Some(b'A' as u32),
559            active_tab: FontEditorTab::Preview,
560            preview_text: "The quick brown fox jumps over the lazy dog.\nABCDEFGHIJKLMNOPQRSTUVWXYZ\n0123456789".into(),
561            preview_size: 24.0,
562            preview_style: FontStyle::Regular,
563            preview_color: Vec4::ONE,
564            preview_background: Vec4::new(0.1, 0.1, 0.1, 1.0),
565            layout_settings: TextLayoutSettings::default(),
566            show_baseline: true,
567            show_ascender: false,
568            show_descender: false,
569            show_cap_height: false,
570            show_x_height: false,
571            show_advance: false,
572            show_bearings: false,
573            atlas_zoom: 1.0,
574            search_query: String::new(),
575            unicode_page: 0,
576        }
577    }
578
579    pub fn selected_font(&self) -> Option<&FontAsset> {
580        self.selected_font.and_then(|id| self.fonts.iter().find(|f| f.id == id))
581    }
582
583    pub fn compute_preview_layout(&self) -> Option<TextLayoutResult> {
584        let font_asset = self.selected_font()?;
585        let face = font_asset.get_face(self.preview_style)?;
586        let span = TextSpan {
587            text: self.preview_text.clone(),
588            font_size: self.preview_size,
589            color: self.preview_color,
590            style: self.preview_style,
591            ..Default::default()
592        };
593        Some(layout_text(&[span], face, &self.layout_settings))
594    }
595
596    pub fn selected_glyph_metrics(&self) -> Option<&GlyphMetrics> {
597        let font = self.selected_font()?;
598        let face = font.get_face(self.preview_style)?;
599        let cp = self.selected_glyph?;
600        face.glyph(cp)
601    }
602
603    pub fn atlas_utilization(&self) -> f32 {
604        self.selected_font()
605            .map(|f| f.atlas_packer.utilization())
606            .unwrap_or(0.0)
607    }
608}
609
610// ---------------------------------------------------------------------------
611// Tests
612// ---------------------------------------------------------------------------
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn test_glyph_measure() {
619        let mut face = FontFace::new("Test", FontStyle::Regular);
620        face.populate_ascii();
621        let sz = face.measure_text("Hello", 16.0);
622        assert!(sz.x > 0.0);
623    }
624
625    #[test]
626    fn test_text_layout() {
627        let mut face = FontFace::new("Test", FontStyle::Regular);
628        face.populate_ascii();
629        let span = TextSpan { text: "Hello World".into(), font_size: 16.0, ..Default::default() };
630        let result = layout_text(&[span], &face, &TextLayoutSettings::default());
631        assert!(!result.glyphs.is_empty());
632    }
633
634    #[test]
635    fn test_atlas_packer() {
636        let mut packer = AtlasPacker::new(128, 128);
637        packer.pack(1, 32, 32);
638        packer.pack(2, 32, 32);
639        assert_eq!(packer.rects.len(), 2);
640        assert!(packer.utilization() > 0.0);
641    }
642
643    #[test]
644    fn test_font_editor() {
645        let ed = FontEditorState::new();
646        assert!(!ed.fonts.is_empty());
647        let layout = ed.compute_preview_layout();
648        assert!(layout.is_some());
649    }
650}