use std::collections::HashMap;
use bevy::image::Image;
use bevy::math::{Rect, UVec2};
use bevy::prelude::*;
use etagere::{AllocId, BucketedAtlasAllocator, size2};
use fontdue::layout::GlyphRasterConfig;
use crate::font_id::FontId;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub font_id: FontId,
pub character: char,
pub size_px: u32,
}
#[derive(Debug, Clone)]
pub struct GlyphInfo {
pub pixel_rect: Rect,
pub metrics: fontdue::Metrics,
pub alloc_id: AllocId,
}
#[derive(Debug, Clone)]
pub struct GlyphCacheConfig {
pub atlas_width: u32,
pub atlas_height: u32,
pub glyph_padding: u32,
pub alpha_mode: GlyphAlphaMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlyphAlphaMode {
Smooth,
Binary { threshold: u8 },
}
impl Default for GlyphCacheConfig {
fn default() -> Self {
Self {
atlas_width: 2048,
atlas_height: 2048,
glyph_padding: 1,
alpha_mode: GlyphAlphaMode::Smooth,
}
}
}
#[derive(Resource)]
pub struct DynamicGlyphCache {
fonts: HashMap<FontId, fontdue::Font>,
glyph_map: HashMap<GlyphKey, GlyphInfo>,
allocator: BucketedAtlasAllocator,
pub config: GlyphCacheConfig,
pub atlas_image: Handle<Image>,
atlas_dirty: bool,
}
impl DynamicGlyphCache {
pub fn new(config: GlyphCacheConfig, images: &mut Assets<Image>) -> Self {
let image = Image::new_fill(
bevy::render::render_resource::Extent3d {
width: config.atlas_width,
height: config.atlas_height,
depth_or_array_layers: 1,
},
bevy::render::render_resource::TextureDimension::D2,
&[0, 0, 0, 0],
bevy::render::render_resource::TextureFormat::Rgba8UnormSrgb,
Default::default(),
);
let handle = images.add(image);
let allocator = BucketedAtlasAllocator::new(size2(
config.atlas_width as i32,
config.atlas_height as i32,
));
Self {
fonts: HashMap::new(),
glyph_map: HashMap::new(),
allocator,
config,
atlas_image: handle,
atlas_dirty: false,
}
}
pub fn add_font(&mut self, id: FontId, data: &[u8]) -> Result<(), String> {
let settings = fontdue::FontSettings {
collection_index: 0,
scale: 40.0,
..Default::default()
};
let font = fontdue::Font::from_bytes(data, settings)
.map_err(|e| format!("Failed to load font {:?}: {}", id, e))?;
self.fonts.insert(id, font);
Ok(())
}
pub fn get_or_insert(
&mut self,
key: &GlyphKey,
images: &mut Assets<Image>,
) -> Option<&GlyphInfo> {
if self.glyph_map.contains_key(key) {
return self.glyph_map.get(key);
}
let font = self.fonts.get(&key.font_id)?;
let (metrics, bitmap) = font.rasterize(key.character, key.size_px as f32);
if metrics.width == 0 || metrics.height == 0 {
let info = GlyphInfo {
pixel_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
metrics,
alloc_id: AllocId::deserialize(0),
};
self.glyph_map.insert(key.clone(), info);
return self.glyph_map.get(key);
}
let padded_w = metrics.width as i32 + self.config.glyph_padding as i32 * 2;
let padded_h = metrics.height as i32 + self.config.glyph_padding as i32 * 2;
let alloc = self.allocator.allocate(size2(padded_w, padded_h))?;
let rect = alloc.rectangle;
let pad = self.config.glyph_padding as i32;
let glyph_x = rect.min.x + pad;
let glyph_y = rect.min.y + pad;
if let Some(image) = images.get_mut(self.atlas_image.id()) {
write_glyph_to_atlas(
image,
&bitmap,
&metrics,
glyph_x as usize,
glyph_y as usize,
self.config.alpha_mode,
);
self.atlas_dirty = true;
}
let pixel_rect = Rect::new(
glyph_x as f32,
glyph_y as f32,
(glyph_x + metrics.width as i32) as f32,
(glyph_y + metrics.height as i32) as f32,
);
let info = GlyphInfo {
pixel_rect,
metrics,
alloc_id: alloc.id,
};
self.glyph_map.insert(key.clone(), info);
self.glyph_map.get(key)
}
pub fn clear(&mut self, images: &mut Assets<Image>) {
self.glyph_map.clear();
self.allocator.clear();
if let Some(image) = images.get_mut(self.atlas_image.id())
&& let Some(ref mut data) = image.data
{
data.fill(0);
}
}
pub fn has_font(&self, id: &FontId) -> bool {
self.fonts.contains_key(id)
}
pub fn glyph_map_get(&self, key: &GlyphKey) -> Option<&GlyphInfo> {
self.glyph_map.get(key)
}
pub fn glyph_metrics(
&self,
font_id: &FontId,
character: char,
size_px: u32,
) -> (u32, u32, f32, f32) {
if let Some(font) = self.fonts.get(font_id) {
let metrics = font.metrics(character, size_px as f32);
(
metrics.width as u32,
metrics.height as u32,
metrics.xmin as f32,
metrics.ymin as f32,
)
} else {
(0, 0, 0.0, 0.0)
}
}
pub fn atlas_size(&self) -> UVec2 {
UVec2::new(self.config.atlas_width, self.config.atlas_height)
}
pub fn is_dirty(&self) -> bool {
self.atlas_dirty
}
pub fn acknowledge_dirty(&mut self) {
self.atlas_dirty = false;
}
pub fn horizontal_advance(&self, font_id: &FontId, character: char, size_px: u32) -> f32 {
if let Some(font) = self.fonts.get(font_id) {
let metrics = font.metrics(character, size_px as f32);
metrics.advance_width
} else {
size_px as f32 * 0.5
}
}
pub fn line_metrics(&self, font_id: &FontId, size_px: u32) -> Option<fontdue::LineMetrics> {
self.fonts
.get(font_id)?
.horizontal_line_metrics(size_px as f32)
}
pub fn raster_config(
&self,
font_id: &FontId,
character: char,
size_px: f32,
) -> Option<GlyphRasterConfig> {
self.fonts.get(font_id).map(|font| GlyphRasterConfig {
glyph_index: font.lookup_glyph_index(character),
px: size_px,
font_hash: font.file_hash(),
})
}
#[cfg(test)]
pub fn new_headless() -> Self {
Self {
fonts: HashMap::new(),
glyph_map: HashMap::new(),
allocator: BucketedAtlasAllocator::new(size2(256, 256)),
config: GlyphCacheConfig::default(),
atlas_image: Handle::default(),
atlas_dirty: false,
}
}
}
fn write_glyph_to_atlas(
image: &mut Image,
bitmap: &[u8],
metrics: &fontdue::Metrics,
glyph_x: usize,
glyph_y: usize,
alpha_mode: GlyphAlphaMode,
) {
let atlas_w = image.width() as usize;
let pixel_size = 4; let Some(ref mut data) = image.data else {
return;
};
for row in 0..metrics.height {
for col in 0..metrics.width {
let src_idx = row * metrics.width + col;
let coverage = apply_alpha_mode(bitmap[src_idx], alpha_mode);
let dst_x = glyph_x + col;
let dst_y = glyph_y + row;
let dst_idx = (dst_y * atlas_w + dst_x) * pixel_size;
if dst_idx + 3 < data.len() {
data[dst_idx] = 255; data[dst_idx + 1] = 255; data[dst_idx + 2] = 255; data[dst_idx + 3] = coverage; }
}
}
}
fn apply_alpha_mode(coverage: u8, alpha_mode: GlyphAlphaMode) -> u8 {
match alpha_mode {
GlyphAlphaMode::Smooth => coverage,
GlyphAlphaMode::Binary { threshold } => {
if coverage >= threshold {
255
} else {
0
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_alpha_mode_quantizes_coverage() {
assert_eq!(
apply_alpha_mode(0, GlyphAlphaMode::Binary { threshold: 128 }),
0
);
assert_eq!(
apply_alpha_mode(127, GlyphAlphaMode::Binary { threshold: 128 }),
0
);
assert_eq!(
apply_alpha_mode(128, GlyphAlphaMode::Binary { threshold: 128 }),
255
);
assert_eq!(
apply_alpha_mode(255, GlyphAlphaMode::Binary { threshold: 128 }),
255
);
}
#[test]
fn smooth_alpha_mode_preserves_coverage() {
assert_eq!(apply_alpha_mode(64, GlyphAlphaMode::Smooth), 64);
assert_eq!(apply_alpha_mode(192, GlyphAlphaMode::Smooth), 192);
}
}