1use std::collections::HashMap;
7use ab_glyph::{Font, FontVec, PxScale, ScaleFont};
8
9#[derive(Copy, Clone, Debug)]
11pub struct GlyphUv {
12 pub u0: f32, pub v0: f32,
13 pub u1: f32, pub v1: f32,
14}
15impl GlyphUv {
16 pub fn offset(&self) -> [f32; 2] { [self.u0, self.v0] }
17 pub fn size(&self) -> [f32; 2] { [self.u1 - self.u0, self.v1 - self.v0] }
18}
19
20pub const ATLAS_CHARS: &str = concat!(
27 " !\"#$%&'()*+,-./0123456789:;<=>?",
29 "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_",
30 "`abcdefghijklmnopqrstuvwxyz{|}~",
31
32 "░▒▓█▄▀▌▐■□▪▫▬▮▯▰▱",
34
35 "╔╗╚╝║═╠╣╦╩╬╟╢╤╧╪╫",
37 "┌┐└┘│─├┤┬┴┼┃━┏┓┗┛┣┫┳┻╋",
39 "╭╮╯╰╱╲╳",
41
42 "←→↑↓↔↕↖↗↘↙⇐⇒⇑⇓⇔▲▼◀▶▷▸▹►◁◂◃",
44
45 "∞∑∫∂∇∆∏√±∓×÷≈≠≡≤≥«»∈∉⊂⊃∪∩∧∨¬∀∃∅∝∠",
47 "⌈⌉⌊⌋",
48
49 "ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψω",
51
52 "●○◉◎◌◍◐◑◒◓◔◕",
54 "◆◇◈□■▢▣▤▥▦▧▨▩",
55 "△▽▲▼◁▷◀▶",
56 "◯⬠⬡⬢⬣",
57
58 "★☆✦✧✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹",
60 "⁂❖❘❙❚",
61 "⊕⊗⊙⊛⊜⊝",
62
63 "·•‣⁃∙◦°※†‡§¶",
65
66 "♠♣♥♦♤♧♡♢",
68
69 "♩♪♫♬♭♮♯",
71
72 "☠☢☣☯☮☸✓✗✘✔✕❌⚙⚡⚔⚒⚑⚐⚠⚰⚱⛏",
74 "☀☁☂☃☄☾☽❄❆❇",
75 "☹☺☻♻♲♳⚕⚖⚗⚛⚜",
76
77 "¢£¥€₹₽₿",
79
80 "▁▂▃▄▅▆▇█",
82
83 "‹›「」『』【】〈〉《》",
85 "…—–\u{2018}\u{2019}\u{201C}\u{201D}\u{201E}\u{201A}",
86);
87
88pub struct FontAtlas {
95 pub width: u32,
96 pub height: u32,
97 pub pixels: Vec<u8>,
99 pub uvs: HashMap<char, GlyphUv>,
100 pub cell_w: u32,
101 pub cell_h: u32,
102 pub is_sdf: bool,
104}
105
106fn bitmap_to_sdf(bitmap: &[u8], w: u32, h: u32, spread: f32) -> Vec<u8> {
109 let mut sdf = vec![0u8; (w * h) as usize];
110 let threshold = 128u8; for y in 0..h as i32 {
113 for x in 0..w as i32 {
114 let idx = (y as u32 * w + x as u32) as usize;
115 let is_inside = bitmap[idx] > threshold;
116
117 let search = spread.ceil() as i32 + 1;
119 let mut min_dist_sq = (spread * spread) as f32 + 1.0;
120
121 'search: for dy in -search..=search {
122 for dx in -search..=search {
123 let nx = x + dx;
124 let ny = y + dy;
125 if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 { continue; }
126 let ni = (ny as u32 * w + nx as u32) as usize;
127 let neighbor_inside = bitmap[ni] > threshold;
128 if neighbor_inside != is_inside {
129 let d = (dx * dx + dy * dy) as f32;
130 if d < min_dist_sq { min_dist_sq = d; }
131 }
132 }
133 }
134
135 let dist = min_dist_sq.sqrt();
136 let signed_dist = if is_inside { dist } else { -dist };
137 let normalized = (signed_dist / spread * 127.0 + 128.0).clamp(0.0, 255.0);
139 sdf[idx] = normalized as u8;
140 }
141 }
142 sdf
143}
144
145fn load_system_font() -> Option<FontVec> {
146 let paths: &[&str] = &[
147 r"C:\Windows\Fonts\consola.ttf",
148 r"C:\Windows\Fonts\cour.ttf",
149 r"C:\Windows\Fonts\lucon.ttf",
150 "/System/Library/Fonts/Menlo.ttc",
151 "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
152 "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
153 "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
154 ];
155 for p in paths {
156 if let Ok(data) = std::fs::read(p) {
157 if let Ok(f) = FontVec::try_from_vec(data) {
158 log::info!("FontAtlas: loaded '{}'", p);
159 return Some(f);
160 }
161 }
162 }
163 None
164}
165
166const SDF_SPREAD: f32 = 6.0;
168
169impl FontAtlas {
170 pub fn build(px_size: f32) -> Self {
171 let chars: Vec<char> = ATLAS_CHARS.chars().collect();
172 if let Some(font) = load_system_font() {
173 return Self::from_ttf(&font, px_size, &chars);
174 }
175 log::warn!("FontAtlas: no system font found, using fallback");
176 Self::fallback(&chars, px_size as u32)
177 }
178
179 fn from_ttf(font: &FontVec, px_size: f32, chars: &[char]) -> Self {
180 let scale = PxScale::from(px_size);
181 let scaled = font.as_scaled(scale);
182 let ascent = scaled.ascent();
183 let descent = scaled.descent();
184 let cell_h = (ascent - descent).ceil() as u32 + 4;
185 let cell_w = chars.iter().filter_map(|ch| {
186 let id = font.glyph_id(*ch);
187 if id.0 == 0 { return None; }
188 Some(scaled.h_advance(id).ceil() as u32)
189 }).max().unwrap_or(px_size as u32) + 4;
190
191 const GUTTER: u32 = 8;
201 debug_assert!(
202 GUTTER as f32 > SDF_SPREAD,
203 "the gutter must outlast the distance field spread"
204 );
205
206 let cols = 32u32;
207 let rows = (chars.len() as u32 + cols - 1) / cols;
208 let stride_w = cell_w + GUTTER;
209 let stride_h = cell_h + GUTTER;
210 let w = cols * stride_w;
211 let h = rows * stride_h;
212 let mut pixels = vec![0u8; (w * h) as usize];
213 let mut uvs = HashMap::new();
214 let baseline_offset = ascent.ceil() as i32 + 1;
215
216 for (i, ch) in chars.iter().enumerate() {
217 let col = (i as u32 % cols) as i32;
218 let row = (i as u32 / cols) as i32;
219 let cx = col * stride_w as i32;
220 let cy = row * stride_h as i32;
221
222 let glyph = font.glyph_id(*ch).with_scale_and_position(
223 scale,
224 ab_glyph::point(cx as f32 + 1.0, cy as f32 + baseline_offset as f32),
225 );
226 if let Some(outlined) = font.outline_glyph(glyph) {
227 let bounds = outlined.px_bounds();
228 outlined.draw(|x, y, v| {
229 let px = bounds.min.x as i32 + x as i32;
230 let py = bounds.min.y as i32 + y as i32;
231 if px >= 0 && py >= 0 {
232 let idx = (py as u32 * w + px as u32) as usize;
233 if idx < pixels.len() {
234 pixels[idx] = (v * 255.0).min(255.0) as u8;
235 }
236 }
237 });
238 }
239 let (hx, hy) = (0.5 / w as f32, 0.5 / h as f32);
243 uvs.insert(*ch, GlyphUv {
244 u0: cx as f32 / w as f32 + hx,
245 v0: cy as f32 / h as f32 + hy,
246 u1: (cx + cell_w as i32) as f32 / w as f32 - hx,
247 v1: (cy + cell_h as i32) as f32 / h as f32 - hy,
248 });
249 }
250 let sdf_spread = SDF_SPREAD;
252 log::info!("FontAtlas: computing SDF ({}x{}, spread={})...", w, h, sdf_spread);
253 let sdf_pixels = bitmap_to_sdf(&pixels, w, h, sdf_spread);
254 log::info!("FontAtlas: SDF complete");
255
256 Self { width: w, height: h, pixels: sdf_pixels, uvs, cell_w, cell_h, is_sdf: true }
257 }
258
259 fn fallback(chars: &[char], px: u32) -> Self {
260 let cw = (px / 2).max(8);
261 let ch = px.max(12);
262 let cols = 32u32;
263 let rows = (chars.len() as u32 + cols - 1) / cols;
264 let w = cols * cw;
265 let h = rows * ch;
266 let mut pixels = vec![0u8; (w * h) as usize];
267 let mut uvs = HashMap::new();
268 for (i, c) in chars.iter().enumerate() {
269 let col = (i as u32 % cols) as i32;
270 let row = (i as u32 / cols) as i32;
271 let cx = col * cw as i32;
272 let cy = row * ch as i32;
273 for py in 1..ch as i32 - 1 {
274 for px in 1..cw as i32 - 1 {
275 let idx = ((cy + py) as u32 * w + (cx + px) as u32) as usize;
276 if idx < pixels.len() { pixels[idx] = 180; }
277 }
278 }
279 let (hx, hy) = (0.5 / w as f32, 0.5 / h as f32);
280 uvs.insert(*c, GlyphUv {
281 u0: cx as f32 / w as f32 + hx,
282 v0: cy as f32 / h as f32 + hy,
283 u1: (cx + cw as i32) as f32 / w as f32 - hx,
284 v1: (cy + ch as i32) as f32 / h as f32 - hy,
285 });
286 }
287 Self { width: w, height: h, pixels, uvs, cell_w: cw, cell_h: ch, is_sdf: false }
288 }
289
290 pub fn uv_for(&self, ch: char) -> GlyphUv {
291 self.uvs.get(&ch)
292 .or_else(|| self.uvs.get(&'?'))
293 .copied()
294 .unwrap_or(GlyphUv { u0: 0.0, v0: 0.0, u1: 0.01, v1: 0.01 })
295 }
296}