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(crate) 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 = fontdue::Font::from_bytes(
126 DEFAULT_FONT_BYTES,
127 fontdue::FontSettings::default(),
128 )
129 .expect("built-in default font must parse");
130
131 let size = Self::INITIAL_SIZE;
132 let pixel_count = (size * size) as usize;
133 let pixels = vec![[255, 255, 255, 0]; pixel_count];
134
135 let (texture, view) = Self::create_texture(device, size);
136
137 Self {
138 fonts: vec![default_font],
139 entries: HashMap::new(),
140 pixels,
141 size,
142 cursor_x: 0,
143 cursor_y: 0,
144 row_height: 0,
145 texture,
146 view,
147 dirty: false,
148 }
149 }
150
151 pub fn upload_font(&mut self, ttf_bytes: &[u8]) -> Result<FontHandle, FontError> {
154 let font = fontdue::Font::from_bytes(ttf_bytes, fontdue::FontSettings::default())
155 .map_err(|e| FontError::ParseFailed(e.to_string()))?;
156 let index = self.fonts.len();
157 self.fonts.push(font);
158 Ok(FontHandle(index))
159 }
160
161 pub fn layout_text(
167 &mut self,
168 text: &str,
169 font_size: f32,
170 font: Option<FontHandle>,
171 device: &wgpu::Device,
172 ) -> TextLayout {
173 let font_index = font.map_or(0, |h| h.0);
174 let size_tenths = (font_size * 10.0).round() as u32;
175 let px = font_size;
176
177 let metrics = self.fonts[font_index].horizontal_line_metrics(px);
178 let height = metrics
179 .map(|m| m.ascent - m.descent + m.line_gap)
180 .unwrap_or(px * 1.2);
181
182 let mut quads = Vec::new();
183 let mut pen_x: f32 = 0.0;
184
185 let mut prev_glyph: Option<u16> = None;
186 for ch in text.chars() {
187 let glyph_index = self.fonts[font_index].lookup_glyph_index(ch);
188
189 if let Some(prev) = prev_glyph {
191 if let Some(kern) = self.fonts[font_index]
192 .horizontal_kern_indexed(prev, glyph_index, px)
193 {
194 pen_x += kern;
195 }
196 }
197 prev_glyph = Some(glyph_index);
198
199 let m = self.fonts[font_index].metrics_indexed(glyph_index, px);
201
202 if m.width > 0 && m.height > 0 {
204 let entry = self.ensure_glyph(device, font_index, glyph_index, size_tenths, px);
205 let atlas_size = self.size as f32;
206
207 quads.push(GlyphQuad {
208 pos: [
209 pen_x + entry.offset_x,
210 entry.offset_y,
211 ],
212 size: [entry.width as f32, entry.height as f32],
213 uv_min: [
214 entry.x as f32 / atlas_size,
215 entry.y as f32 / atlas_size,
216 ],
217 uv_max: [
218 (entry.x + entry.width) as f32 / atlas_size,
219 (entry.y + entry.height) as f32 / atlas_size,
220 ],
221 });
222 }
223
224 pen_x += m.advance_width;
225 }
226
227 TextLayout {
228 quads,
229 total_width: pen_x,
230 height,
231 }
232 }
233
234 pub fn layout_text_wrapped(
240 &mut self,
241 text: &str,
242 font_size: f32,
243 font: Option<FontHandle>,
244 max_width: f32,
245 device: &wgpu::Device,
246 ) -> TextLayout {
247 let font_index = font.map_or(0, |h| h.0);
248 let px = font_size;
249
250 let metrics = self.fonts[font_index].horizontal_line_metrics(px);
251 let line_height = metrics
252 .map(|m| m.ascent - m.descent + m.line_gap)
253 .unwrap_or(px * 1.2);
254
255 let words: Vec<&str> = text.split_whitespace().collect();
257 if words.is_empty() {
258 return TextLayout {
259 quads: Vec::new(),
260 total_width: 0.0,
261 height: line_height,
262 };
263 }
264
265 let size_tenths = (px * 10.0).round() as u32;
266 let space_advance = {
267 let gi = self.fonts[font_index].lookup_glyph_index(' ');
268 self.fonts[font_index].metrics_indexed(gi, px).advance_width
269 };
270
271 let mut quads = Vec::new();
272 let mut line_x: f32 = 0.0;
273 let mut line_y: f32 = 0.0;
274 let mut max_line_width: f32 = 0.0;
275 let mut first_on_line = true;
276
277 for word in &words {
278 let mut word_quads: Vec<GlyphQuad> = Vec::new();
280 let mut pen_x: f32 = 0.0;
281 let mut prev_glyph: Option<u16> = None;
282
283 for ch in word.chars() {
284 let glyph_index = self.fonts[font_index].lookup_glyph_index(ch);
285 if let Some(prev) = prev_glyph {
286 if let Some(kern) = self.fonts[font_index]
287 .horizontal_kern_indexed(prev, glyph_index, px)
288 {
289 pen_x += kern;
290 }
291 }
292 prev_glyph = Some(glyph_index);
293 let m = self.fonts[font_index].metrics_indexed(glyph_index, px);
294 if m.width > 0 && m.height > 0 {
295 let entry = self.ensure_glyph(device, font_index, glyph_index, size_tenths, px);
296 let atlas_size = self.size as f32;
297 word_quads.push(GlyphQuad {
298 pos: [pen_x + entry.offset_x, entry.offset_y],
299 size: [entry.width as f32, entry.height as f32],
300 uv_min: [
301 entry.x as f32 / atlas_size,
302 entry.y as f32 / atlas_size,
303 ],
304 uv_max: [
305 (entry.x + entry.width) as f32 / atlas_size,
306 (entry.y + entry.height) as f32 / atlas_size,
307 ],
308 });
309 }
310 pen_x += m.advance_width;
311 }
312 let word_width = pen_x;
313
314 let test_x = if first_on_line { line_x } else { line_x + space_advance };
316 if !first_on_line && test_x + word_width > max_width {
317 max_line_width = max_line_width.max(line_x);
318 line_x = 0.0;
319 line_y += line_height;
320 first_on_line = true;
321 }
322
323 let start_x = if first_on_line { line_x } else { line_x + space_advance };
324 for mut gq in word_quads {
325 gq.pos[0] += start_x;
326 gq.pos[1] += line_y;
327 quads.push(gq);
328 }
329 line_x = start_x + word_width;
330 first_on_line = false;
331 }
332
333 max_line_width = max_line_width.max(line_x);
334 let total_height = line_y + line_height;
335
336 TextLayout {
337 quads,
338 total_width: max_line_width,
339 height: total_height,
340 }
341 }
342
343 pub fn font_ascent(&self, font_index: usize, font_size: f32) -> f32 {
349 self.fonts[font_index]
350 .horizontal_line_metrics(font_size)
351 .map(|m| m.ascent)
352 .unwrap_or(font_size * 0.8)
353 }
354
355 pub fn upload_if_dirty(&mut self, queue: &wgpu::Queue) {
358 if !self.dirty {
359 return;
360 }
361 let flat: Vec<u8> = self.pixels.iter().flat_map(|p| p.iter().copied()).collect();
362 queue.write_texture(
363 wgpu::TexelCopyTextureInfo {
364 texture: &self.texture,
365 mip_level: 0,
366 origin: wgpu::Origin3d::ZERO,
367 aspect: wgpu::TextureAspect::All,
368 },
369 &flat,
370 wgpu::TexelCopyBufferLayout {
371 offset: 0,
372 bytes_per_row: Some(self.size * 4),
373 rows_per_image: Some(self.size),
374 },
375 wgpu::Extent3d {
376 width: self.size,
377 height: self.size,
378 depth_or_array_layers: 1,
379 },
380 );
381 self.dirty = false;
382 }
383
384 fn ensure_glyph(
391 &mut self,
392 device: &wgpu::Device,
393 font_index: usize,
394 glyph_index: u16,
395 size_tenths: u32,
396 px: f32,
397 ) -> GlyphEntry {
398 let key = GlyphKey {
399 font_index,
400 glyph_index,
401 size_tenths,
402 };
403
404 if let Some(&entry) = self.entries.get(&key) {
405 return entry;
406 }
407
408 let (metrics, bitmap) = self.fonts[font_index].rasterize_indexed(glyph_index, px);
410 let w = metrics.width as u32;
411 let h = metrics.height as u32;
412
413 if w == 0 || h == 0 {
414 let entry = GlyphEntry {
416 x: 0,
417 y: 0,
418 width: 0,
419 height: 0,
420 offset_x: metrics.xmin as f32,
421 offset_y: -(metrics.ymin as f32 + h as f32),
422 };
423 self.entries.insert(key, entry);
424 return entry;
425 }
426
427 let pad = 1;
429 if self.cursor_x + w + pad > self.size {
430 self.cursor_y += self.row_height + pad;
432 self.cursor_x = 0;
433 self.row_height = 0;
434 }
435 if self.cursor_y + h + pad > self.size {
436 self.grow(device);
438 }
439
440 let x = self.cursor_x;
441 let y = self.cursor_y;
442
443 for row in 0..h {
445 for col in 0..w {
446 let src = (row * w + col) as usize;
447 let dst = ((y + row) * self.size + (x + col)) as usize;
448 self.pixels[dst] = [255, 255, 255, bitmap[src]];
449 }
450 }
451 self.dirty = true;
452
453 self.cursor_x = x + w + pad;
454 self.row_height = self.row_height.max(h);
455
456 let entry = GlyphEntry {
457 x,
458 y,
459 width: w,
460 height: h,
461 offset_x: metrics.xmin as f32,
462 offset_y: -(metrics.ymin as f32 + h as f32),
463 };
464 self.entries.insert(key, entry);
465 entry
466 }
467
468 fn grow(&mut self, device: &wgpu::Device) {
471 let old_size = self.size;
472 let new_size = old_size * 2;
473 tracing::info!("Growing glyph atlas from {}x{} to {}x{}", old_size, old_size, new_size, new_size);
474
475 let mut new_pixels = vec![[255, 255, 255, 0u8]; (new_size * new_size) as usize];
476 for row in 0..old_size {
477 let src_start = (row * old_size) as usize;
478 let dst_start = (row * new_size) as usize;
479 new_pixels[dst_start..dst_start + old_size as usize]
480 .copy_from_slice(&self.pixels[src_start..src_start + old_size as usize]);
481 }
482
483 self.pixels = new_pixels;
484 self.size = new_size;
485
486 let (texture, view) = Self::create_texture(device, new_size);
487 self.texture = texture;
488 self.view = view;
489 self.dirty = true; }
491
492 fn create_texture(device: &wgpu::Device, size: u32) -> (wgpu::Texture, wgpu::TextureView) {
493 let texture = device.create_texture(&wgpu::TextureDescriptor {
494 label: Some("glyph_atlas"),
495 size: wgpu::Extent3d {
496 width: size,
497 height: size,
498 depth_or_array_layers: 1,
499 },
500 mip_level_count: 1,
501 sample_count: 1,
502 dimension: wgpu::TextureDimension::D2,
503 format: wgpu::TextureFormat::Rgba8Unorm,
504 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
505 view_formats: &[],
506 });
507 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
508 (texture, view)
509 }
510}
511
512#[derive(Debug, Clone, thiserror::Error)]
518pub enum FontError {
519 #[error("font parsing failed: {0}")]
521 ParseFailed(String),
522}
523
524impl super::ViewportGpuResources {
529 pub fn upload_font(&mut self, ttf_bytes: &[u8]) -> Result<FontHandle, FontError> {
537 self.glyph_atlas.upload_font(ttf_bytes)
538 }
539}