use crate::gpu::GpuContext;
use fontdue::{Font, FontSettings};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FontId(pub(crate) usize);
#[derive(Clone, Copy, Debug)]
pub struct GlyphInfo {
pub uv: [f32; 4],
pub width: u32,
pub height: u32,
pub offset_x: f32,
pub offset_y: f32,
pub advance: f32,
}
pub struct FontAtlas {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
glyphs: HashMap<char, GlyphInfo>,
#[allow(dead_code)]
font: Font,
size: f32,
line_height: f32,
}
impl FontAtlas {
pub fn new(gpu: &GpuContext, font_data: &[u8], size: f32) -> Self {
let font =
Font::from_bytes(font_data, FontSettings::default()).expect("Failed to parse font");
let chars: Vec<char> = (32u8..=126u8).map(|c| c as char).collect();
let rasterized: Vec<(char, fontdue::Metrics, Vec<u8>)> = chars
.iter()
.map(|&c| {
let (metrics, bitmap) = font.rasterize(c, size);
(c, metrics, bitmap)
})
.collect();
let padding = 1u32;
let mut atlas_width = 512u32;
let mut atlas_height = 512u32;
loop {
let mut x = padding;
let mut y = padding;
let mut row_height = 0u32;
let mut fits = true;
for (_, metrics, _) in &rasterized {
let glyph_w = metrics.width as u32;
let glyph_h = metrics.height as u32;
if x + glyph_w + padding > atlas_width {
x = padding;
y += row_height + padding;
row_height = 0;
}
if y + glyph_h + padding > atlas_height {
fits = false;
break;
}
x += glyph_w + padding;
row_height = row_height.max(glyph_h);
}
if fits {
break;
}
if atlas_width <= atlas_height {
atlas_width *= 2;
} else {
atlas_height *= 2;
}
}
let mut atlas_data = vec![0u8; (atlas_width * atlas_height) as usize];
let mut glyphs = HashMap::new();
let mut x = padding;
let mut y = padding;
let mut row_height = 0u32;
for (c, metrics, bitmap) in &rasterized {
let glyph_w = metrics.width as u32;
let glyph_h = metrics.height as u32;
if x + glyph_w + padding > atlas_width {
x = padding;
y += row_height + padding;
row_height = 0;
}
for gy in 0..glyph_h {
for gx in 0..glyph_w {
let src_idx = (gy * glyph_w + gx) as usize;
let dst_idx = ((y + gy) * atlas_width + (x + gx)) as usize;
atlas_data[dst_idx] = bitmap[src_idx];
}
}
let uv = [
x as f32 / atlas_width as f32,
y as f32 / atlas_height as f32,
glyph_w as f32 / atlas_width as f32,
glyph_h as f32 / atlas_height as f32,
];
glyphs.insert(
*c,
GlyphInfo {
uv,
width: glyph_w,
height: glyph_h,
offset_x: metrics.xmin as f32,
offset_y: metrics.ymin as f32,
advance: metrics.advance_width,
},
);
x += glyph_w + padding;
row_height = row_height.max(glyph_h);
}
let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Font Atlas"),
size: wgpu::Extent3d {
width: atlas_width,
height: atlas_height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
gpu.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&atlas_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(atlas_width),
rows_per_image: Some(atlas_height),
},
wgpu::Extent3d {
width: atlas_width,
height: atlas_height,
depth_or_array_layers: 1,
},
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Font Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let line_metrics = font.horizontal_line_metrics(size);
let line_height = line_metrics.map(|m| m.new_line_size).unwrap_or(size * 1.2);
Self {
texture,
view,
sampler,
glyphs,
font,
size,
line_height,
}
}
#[inline]
pub fn glyph(&self, c: char) -> Option<&GlyphInfo> {
self.glyphs.get(&c)
}
#[inline]
pub fn size(&self) -> f32 {
self.size
}
#[inline]
pub fn line_height(&self) -> f32 {
self.line_height
}
pub fn measure(&self, text: &str) -> f32 {
text.chars()
.filter_map(|c| self.glyphs.get(&c))
.map(|g| g.advance)
.sum()
}
}
const EMBEDDED_FONT: &[u8] = include_bytes!("fonts/JetBrainsMono-Regular.ttf");
pub struct Assets {
pub(crate) fonts: Vec<Arc<FontAtlas>>,
}
impl Assets {
pub(crate) fn new() -> Self {
Self { fonts: Vec::new() }
}
pub fn load_font(&mut self, gpu: &GpuContext, path: impl AsRef<Path>, size: f32) -> FontId {
let data = std::fs::read(path.as_ref()).expect("Failed to read font file");
self.load_font_bytes(gpu, &data, size)
}
pub fn load_font_bytes(&mut self, gpu: &GpuContext, data: &[u8], size: f32) -> FontId {
let atlas = FontAtlas::new(gpu, data, size);
let id = FontId(self.fonts.len());
self.fonts.push(Arc::new(atlas));
id
}
pub fn default_font(&mut self, gpu: &GpuContext, size: f32) -> FontId {
self.load_font_bytes(gpu, EMBEDDED_FONT, size)
}
#[inline]
pub fn font(&self, id: FontId) -> Option<Arc<FontAtlas>> {
self.fonts.get(id.0).cloned()
}
}