use bevy_asset::{Assets, Handle, RenderAssetUsages};
use bevy_image::{prelude::*, ImageSampler, ToExtents};
use bevy_math::{IVec2, UVec2};
use bevy_platform::collections::HashMap;
use swash::scale::Scaler;
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct GlyphCacheKey {
pub glyph_id: u16,
}
pub struct FontAtlas {
pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
pub glyph_to_atlas_index: HashMap<GlyphCacheKey, GlyphAtlasLocation>,
pub texture_atlas: Handle<TextureAtlasLayout>,
pub texture: Handle<Image>,
}
impl FontAtlas {
pub fn new(
textures: &mut Assets<Image>,
texture_atlases_layout: &mut Assets<TextureAtlasLayout>,
size: UVec2,
font_smoothing: FontSmoothing,
) -> FontAtlas {
let mut image = Image::new_fill(
size.to_extents(),
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
);
if font_smoothing == FontSmoothing::None {
image.sampler = ImageSampler::nearest();
}
let texture = textures.add(image);
let texture_atlas = texture_atlases_layout.add(TextureAtlasLayout::new_empty(size));
Self {
texture_atlas,
glyph_to_atlas_index: HashMap::default(),
dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1),
texture,
}
}
pub fn get_glyph_index(&self, cache_key: GlyphCacheKey) -> Option<GlyphAtlasLocation> {
self.glyph_to_atlas_index.get(&cache_key).copied()
}
pub fn has_glyph(&self, cache_key: GlyphCacheKey) -> bool {
self.glyph_to_atlas_index.contains_key(&cache_key)
}
pub fn add_glyph(
&mut self,
textures: &mut Assets<Image>,
atlas_layouts: &mut Assets<TextureAtlasLayout>,
key: GlyphCacheKey,
texture: &Image,
offset: IVec2,
) -> Result<(), TextError> {
let mut atlas_layout = atlas_layouts
.get_mut(&self.texture_atlas)
.ok_or(TextError::MissingAtlasLayout)?;
let mut atlas_texture = textures
.get_mut(&self.texture)
.ok_or(TextError::MissingAtlasTexture)?;
if let Ok(glyph_index) = self.dynamic_texture_atlas_builder.add_texture(
&mut atlas_layout,
texture,
&mut atlas_texture,
) {
self.glyph_to_atlas_index.insert(
key,
GlyphAtlasLocation {
glyph_index,
offset,
},
);
Ok(())
} else {
Err(TextError::FailedToAddGlyph(key.glyph_id))
}
}
}
impl core::fmt::Debug for FontAtlas {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("FontAtlas")
.field("glyph_to_atlas_index", &self.glyph_to_atlas_index)
.field("texture_atlas", &self.texture_atlas)
.field("texture", &self.texture)
.field("dynamic_texture_atlas_builder", &"[...]")
.finish()
}
}
pub fn add_glyph_to_atlas(
font_atlases: &mut Vec<FontAtlas>,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
scaler: &mut Scaler,
font_smoothing: FontSmoothing,
glyph_id: u16,
) -> Result<GlyphAtlasInfo, TextError> {
let (glyph_texture, offset) = get_outlined_glyph_texture(scaler, glyph_id, font_smoothing)?;
let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> {
atlas.add_glyph(
textures,
texture_atlases,
GlyphCacheKey { glyph_id },
&glyph_texture,
offset,
)
};
if !font_atlases
.iter_mut()
.any(|atlas| add_char_to_font_atlas(atlas).is_ok())
{
let glyph_max_size: u32 = glyph_texture
.texture_descriptor
.size
.height
.max(glyph_texture.width());
let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512);
let mut new_atlas = FontAtlas::new(
textures,
texture_atlases,
UVec2::splat(containing),
font_smoothing,
);
new_atlas.add_glyph(
textures,
texture_atlases,
GlyphCacheKey { glyph_id },
&glyph_texture,
offset,
)?;
font_atlases.push(new_atlas);
}
get_glyph_atlas_info(font_atlases, GlyphCacheKey { glyph_id })
.ok_or(TextError::InconsistentAtlasState)
}
#[expect(
clippy::identity_op,
reason = "Alignment improves clarity during RGBA operations."
)]
pub fn get_outlined_glyph_texture(
scaler: &mut Scaler,
glyph_id: u16,
font_smoothing: FontSmoothing,
) -> Result<(Image, IVec2), TextError> {
let image = swash::scale::Render::new(&[
swash::scale::Source::ColorOutline(0),
swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
swash::scale::Source::Outline,
])
.format(swash::zeno::Format::Alpha)
.render(scaler, glyph_id)
.ok_or(TextError::FailedToGetGlyphImage(glyph_id))?;
let left = image.placement.left;
let top = image.placement.top;
let width = image.placement.width;
let height = image.placement.height;
let px = (width * height) as usize;
let mut rgba = vec![0u8; px * 4];
match font_smoothing {
FontSmoothing::AntiAliased => {
for i in 0..px {
let a = image.data[i];
rgba[i * 4 + 0] = 255;
rgba[i * 4 + 1] = 255;
rgba[i * 4 + 2] = 255;
rgba[i * 4 + 3] = a;
}
}
FontSmoothing::None => {
for i in 0..px {
let a = image.data[i];
rgba[i * 4 + 0] = 255;
rgba[i * 4 + 1] = 255;
rgba[i * 4 + 2] = 255;
rgba[i * 4 + 3] = if 127 < a { 255 } else { 0 };
}
}
}
Ok((
Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
rgba,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::MAIN_WORLD,
),
IVec2::new(left, top),
))
}
pub fn get_glyph_atlas_info(
font_atlases: &mut [FontAtlas],
cache_key: GlyphCacheKey,
) -> Option<GlyphAtlasInfo> {
font_atlases.iter().find_map(|atlas| {
atlas
.get_glyph_index(cache_key)
.map(|location| GlyphAtlasInfo {
location,
texture_atlas: atlas.texture_atlas.id(),
texture: atlas.texture.id(),
})
})
}