1
2use glam::{Vec2, Vec4};
5use std::collections::HashMap;
6
7#[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#[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#[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 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 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#[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#[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 if settings.word_wrap == WordWrap::Normal && cursor.x + advance > max_width {
372 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 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#[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 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#[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#[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#[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}