1use std::collections::HashMap;
11
12const DEFAULT_FONT_BYTES: &[u8] = include_bytes!("../../fonts/Inter-Regular.ttf");
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct FontHandle(pub usize);
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33struct GlyphKey {
34 font_index: usize,
35 glyph_index: u16,
36 size_tenths: u32,
39}
40
41#[derive(Debug, Clone, Copy)]
43struct GlyphEntry {
44 x: u32,
46 y: u32,
47 width: u32,
49 height: u32,
50 offset_x: f32,
52 offset_y: f32,
53}
54
55#[derive(Debug, Clone, Copy)]
61pub(crate) struct GlyphQuad {
62 pub pos: [f32; 2],
64 pub size: [f32; 2],
66 pub uv_min: [f32; 2],
68 pub uv_max: [f32; 2],
70}
71
72#[derive(Debug, Clone)]
74pub(crate) struct TextLayout {
75 pub quads: Vec<GlyphQuad>,
77 pub total_width: f32,
79 pub height: f32,
81}
82
83pub(crate) struct GlyphAtlas {
91 fonts: Vec<fontdue::Font>,
93
94 entries: HashMap<GlyphKey, GlyphEntry>,
96
97 pixels: Vec<[u8; 4]>,
100
101 size: u32,
103
104 cursor_x: u32,
106 cursor_y: u32,
107 row_height: u32,
108
109 pub texture: wgpu::Texture,
111 pub view: wgpu::TextureView,
113
114 dirty: bool,
117}
118
119impl GlyphAtlas {
120 const INITIAL_SIZE: u32 = 512;
122
123 pub fn new(device: &wgpu::Device) -> Self {
125 let default_font =
126 fontdue::Font::from_bytes(DEFAULT_FONT_BYTES, fontdue::FontSettings::default())
127 .expect("built-in default font must parse");
128
129 let size = Self::INITIAL_SIZE;
130 let pixel_count = (size * size) as usize;
131 let pixels = vec![[255, 255, 255, 0]; pixel_count];
132
133 let (texture, view) = Self::create_texture(device, size);
134
135 Self {
136 fonts: vec![default_font],
137 entries: HashMap::new(),
138 pixels,
139 size,
140 cursor_x: 0,
141 cursor_y: 0,
142 row_height: 0,
143 texture,
144 view,
145 dirty: false,
146 }
147 }
148
149 pub fn upload_font(&mut self, ttf_bytes: &[u8]) -> Result<FontHandle, FontError> {
152 let font = fontdue::Font::from_bytes(ttf_bytes, fontdue::FontSettings::default())
153 .map_err(|e| FontError::ParseFailed(e.to_string()))?;
154 let index = self.fonts.len();
155 self.fonts.push(font);
156 Ok(FontHandle(index))
157 }
158
159 pub fn layout_text(
165 &mut self,
166 text: &str,
167 font_size: f32,
168 font: Option<FontHandle>,
169 device: &wgpu::Device,
170 ) -> TextLayout {
171 let font_index = font.map_or(0, |h| h.0);
172 let size_tenths = (font_size * 10.0).round() as u32;
173 let px = font_size;
174
175 let metrics = self.fonts[font_index].horizontal_line_metrics(px);
176 let line_height = metrics
177 .map(|m| m.ascent - m.descent + m.line_gap)
178 .unwrap_or(px * 1.2);
179
180 let mut quads = Vec::new();
181 let mut pen_x: f32 = 0.0;
182 let mut pen_y: f32 = 0.0;
183 let mut max_width: f32 = 0.0;
184
185 let mut prev_glyph: Option<u16> = None;
186 for ch in text.chars() {
187 if ch == '\n' {
188 max_width = max_width.max(pen_x);
189 pen_x = 0.0;
190 pen_y += line_height;
191 prev_glyph = None;
192 continue;
193 }
194
195 let glyph_index = self.fonts[font_index].lookup_glyph_index(ch);
196
197 if let Some(prev) = prev_glyph {
199 if let Some(kern) =
200 self.fonts[font_index].horizontal_kern_indexed(prev, glyph_index, px)
201 {
202 pen_x += kern;
203 }
204 }
205 prev_glyph = Some(glyph_index);
206
207 let m = self.fonts[font_index].metrics_indexed(glyph_index, px);
209
210 if m.width > 0 && m.height > 0 {
212 let entry = self.ensure_glyph(device, font_index, glyph_index, size_tenths, px);
213 let atlas_size = self.size as f32;
214
215 quads.push(GlyphQuad {
216 pos: [pen_x + entry.offset_x, pen_y + entry.offset_y],
217 size: [entry.width as f32, entry.height as f32],
218 uv_min: [entry.x as f32 / atlas_size, entry.y as f32 / atlas_size],
219 uv_max: [
220 (entry.x + entry.width) as f32 / atlas_size,
221 (entry.y + entry.height) as f32 / atlas_size,
222 ],
223 });
224 }
225
226 pen_x += m.advance_width;
227 }
228 max_width = max_width.max(pen_x);
229
230 TextLayout {
231 quads,
232 total_width: max_width,
233 height: pen_y + line_height,
234 }
235 }
236
237 pub fn layout_text_wrapped(
243 &mut self,
244 text: &str,
245 font_size: f32,
246 font: Option<FontHandle>,
247 max_width: f32,
248 device: &wgpu::Device,
249 ) -> TextLayout {
250 let font_index = font.map_or(0, |h| h.0);
251 let px = font_size;
252
253 let metrics = self.fonts[font_index].horizontal_line_metrics(px);
254 let line_height = metrics
255 .map(|m| m.ascent - m.descent + m.line_gap)
256 .unwrap_or(px * 1.2);
257
258 let size_tenths = (px * 10.0).round() as u32;
259 let space_advance = {
260 let gi = self.fonts[font_index].lookup_glyph_index(' ');
261 self.fonts[font_index].metrics_indexed(gi, px).advance_width
262 };
263
264 let mut quads = Vec::new();
265 let mut line_x: f32 = 0.0;
266 let mut line_y: f32 = 0.0;
267 let mut max_line_width: f32 = 0.0;
268
269 for (logical_line_idx, logical_line) in text.split('\n').enumerate() {
271 if logical_line_idx > 0 {
272 max_line_width = max_line_width.max(line_x);
273 line_x = 0.0;
274 line_y += line_height;
275 }
276
277 let words: Vec<&str> = logical_line.split_whitespace().collect();
278 if words.is_empty() {
279 continue;
280 }
281
282 let mut first_on_line = true;
283
284 for word in &words {
285 let mut word_quads: Vec<GlyphQuad> = Vec::new();
286 let mut pen_x: f32 = 0.0;
287 let mut prev_glyph: Option<u16> = None;
288
289 for ch in word.chars() {
290 let glyph_index = self.fonts[font_index].lookup_glyph_index(ch);
291 if let Some(prev) = prev_glyph {
292 if let Some(kern) =
293 self.fonts[font_index].horizontal_kern_indexed(prev, glyph_index, px)
294 {
295 pen_x += kern;
296 }
297 }
298 prev_glyph = Some(glyph_index);
299 let m = self.fonts[font_index].metrics_indexed(glyph_index, px);
300 if m.width > 0 && m.height > 0 {
301 let entry =
302 self.ensure_glyph(device, font_index, glyph_index, size_tenths, px);
303 let atlas_size = self.size as f32;
304 word_quads.push(GlyphQuad {
305 pos: [pen_x + entry.offset_x, entry.offset_y],
306 size: [entry.width as f32, entry.height as f32],
307 uv_min: [entry.x as f32 / atlas_size, entry.y as f32 / atlas_size],
308 uv_max: [
309 (entry.x + entry.width) as f32 / atlas_size,
310 (entry.y + entry.height) as f32 / atlas_size,
311 ],
312 });
313 }
314 pen_x += m.advance_width;
315 }
316 let word_width = pen_x;
317
318 let test_x = if first_on_line {
320 line_x
321 } else {
322 line_x + space_advance
323 };
324 if !first_on_line && test_x + word_width > max_width {
325 max_line_width = max_line_width.max(line_x);
326 line_x = 0.0;
327 line_y += line_height;
328 first_on_line = true;
329 }
330
331 let start_x = if first_on_line {
332 line_x
333 } else {
334 line_x + space_advance
335 };
336 for mut gq in word_quads {
337 gq.pos[0] += start_x;
338 gq.pos[1] += line_y;
339 quads.push(gq);
340 }
341 line_x = start_x + word_width;
342 first_on_line = false;
343 }
344 }
345
346 max_line_width = max_line_width.max(line_x);
347 let total_height = if quads.is_empty() && text.is_empty() {
348 line_height
349 } else {
350 line_y + line_height
351 };
352
353 TextLayout {
354 quads,
355 total_width: max_line_width,
356 height: total_height,
357 }
358 }
359
360 pub fn font_ascent(&self, font_index: usize, font_size: f32) -> f32 {
366 self.fonts[font_index]
367 .horizontal_line_metrics(font_size)
368 .map(|m| m.ascent)
369 .unwrap_or(font_size * 0.8)
370 }
371
372 pub fn upload_if_dirty(&mut self, queue: &wgpu::Queue) {
375 if !self.dirty {
376 return;
377 }
378 let flat: Vec<u8> = self.pixels.iter().flat_map(|p| p.iter().copied()).collect();
379 queue.write_texture(
380 wgpu::TexelCopyTextureInfo {
381 texture: &self.texture,
382 mip_level: 0,
383 origin: wgpu::Origin3d::ZERO,
384 aspect: wgpu::TextureAspect::All,
385 },
386 &flat,
387 wgpu::TexelCopyBufferLayout {
388 offset: 0,
389 bytes_per_row: Some(self.size * 4),
390 rows_per_image: Some(self.size),
391 },
392 wgpu::Extent3d {
393 width: self.size,
394 height: self.size,
395 depth_or_array_layers: 1,
396 },
397 );
398 self.dirty = false;
399 }
400
401 fn ensure_glyph(
408 &mut self,
409 device: &wgpu::Device,
410 font_index: usize,
411 glyph_index: u16,
412 size_tenths: u32,
413 px: f32,
414 ) -> GlyphEntry {
415 let key = GlyphKey {
416 font_index,
417 glyph_index,
418 size_tenths,
419 };
420
421 if let Some(&entry) = self.entries.get(&key) {
422 return entry;
423 }
424
425 let (metrics, bitmap) = self.fonts[font_index].rasterize_indexed(glyph_index, px);
427 let w = metrics.width as u32;
428 let h = metrics.height as u32;
429
430 if w == 0 || h == 0 {
431 let entry = GlyphEntry {
433 x: 0,
434 y: 0,
435 width: 0,
436 height: 0,
437 offset_x: metrics.xmin as f32,
438 offset_y: -(metrics.ymin as f32 + h as f32),
439 };
440 self.entries.insert(key, entry);
441 return entry;
442 }
443
444 let pad = 1;
446 if self.cursor_x + w + pad > self.size {
447 self.cursor_y += self.row_height + pad;
449 self.cursor_x = 0;
450 self.row_height = 0;
451 }
452 if self.cursor_y + h + pad > self.size {
453 self.grow(device);
455 }
456
457 let x = self.cursor_x;
458 let y = self.cursor_y;
459
460 for row in 0..h {
462 for col in 0..w {
463 let src = (row * w + col) as usize;
464 let dst = ((y + row) * self.size + (x + col)) as usize;
465 self.pixels[dst] = [255, 255, 255, bitmap[src]];
466 }
467 }
468 self.dirty = true;
469
470 self.cursor_x = x + w + pad;
471 self.row_height = self.row_height.max(h);
472
473 let entry = GlyphEntry {
474 x,
475 y,
476 width: w,
477 height: h,
478 offset_x: metrics.xmin as f32,
479 offset_y: -(metrics.ymin as f32 + h as f32),
480 };
481 self.entries.insert(key, entry);
482 entry
483 }
484
485 fn grow(&mut self, device: &wgpu::Device) {
488 let old_size = self.size;
489 let new_size = old_size * 2;
490 tracing::info!(
491 "Growing glyph atlas from {}x{} to {}x{}",
492 old_size,
493 old_size,
494 new_size,
495 new_size
496 );
497
498 let mut new_pixels = vec![[255, 255, 255, 0u8]; (new_size * new_size) as usize];
499 for row in 0..old_size {
500 let src_start = (row * old_size) as usize;
501 let dst_start = (row * new_size) as usize;
502 new_pixels[dst_start..dst_start + old_size as usize]
503 .copy_from_slice(&self.pixels[src_start..src_start + old_size as usize]);
504 }
505
506 self.pixels = new_pixels;
507 self.size = new_size;
508
509 let (texture, view) = Self::create_texture(device, new_size);
510 self.texture = texture;
511 self.view = view;
512 self.dirty = true; }
514
515 fn create_texture(device: &wgpu::Device, size: u32) -> (wgpu::Texture, wgpu::TextureView) {
516 let texture = device.create_texture(&wgpu::TextureDescriptor {
517 label: Some("glyph_atlas"),
518 size: wgpu::Extent3d {
519 width: size,
520 height: size,
521 depth_or_array_layers: 1,
522 },
523 mip_level_count: 1,
524 sample_count: 1,
525 dimension: wgpu::TextureDimension::D2,
526 format: wgpu::TextureFormat::Rgba8Unorm,
527 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
528 view_formats: &[],
529 });
530 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
531 (texture, view)
532 }
533}
534
535#[derive(Debug, Clone, thiserror::Error)]
541pub enum FontError {
542 #[error("font parsing failed: {0}")]
544 ParseFailed(String),
545}
546
547impl crate::resources::DeviceResources {
552 pub fn upload_font(&mut self, ttf_bytes: &[u8]) -> Result<FontHandle, FontError> {
560 self.content.glyph_atlas.upload_font(ttf_bytes)
561 }
562}