1use bevy_asset::{Assets, Handle, RenderAssetUsages};
2use bevy_image::{prelude::*, ImageSampler, ToExtents};
3use bevy_math::{UVec2, Vec2};
4use bevy_platform::collections::HashMap;
5use swash::scale::Scaler;
6use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
7
8use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError};
9
10#[derive(#[automatically_derived]
impl ::core::marker::Copy for GlyphCacheKey { }Copy, #[automatically_derived]
impl ::core::clone::Clone for GlyphCacheKey {
#[inline]
fn clone(&self) -> GlyphCacheKey {
let _: ::core::clone::AssertParamIsClone<u16>;
*self
}
}Clone, #[automatically_derived]
impl ::core::fmt::Debug for GlyphCacheKey {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f, "GlyphCacheKey",
"glyph_id", &&self.glyph_id)
}
}Debug, #[automatically_derived]
impl ::core::cmp::PartialEq for GlyphCacheKey {
#[inline]
fn eq(&self, other: &GlyphCacheKey) -> bool {
self.glyph_id == other.glyph_id
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for GlyphCacheKey {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<u16>;
}
}Eq, #[automatically_derived]
impl ::core::hash::Hash for GlyphCacheKey {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&self.glyph_id, state)
}
}Hash)]
12pub struct GlyphCacheKey {
13 pub glyph_id: u16,
15}
16
17pub struct FontAtlas {
30 pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
32 pub glyph_to_atlas_index: HashMap<GlyphCacheKey, GlyphAtlasLocation>,
34 pub texture_atlas: TextureAtlasLayout,
36 pub texture: Handle<Image>,
38}
39
40impl FontAtlas {
41 pub fn new(
43 textures: &mut Assets<Image>,
44 size: UVec2,
45 font_smoothing: FontSmoothing,
46 ) -> FontAtlas {
47 let mut image = Image::new_fill(
48 size.to_extents(),
49 TextureDimension::D2,
50 &[0, 0, 0, 0],
51 TextureFormat::Rgba8UnormSrgb,
52 RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
54 );
55 if font_smoothing == FontSmoothing::None {
56 image.sampler = ImageSampler::nearest();
57 }
58 let texture = textures.add(image);
59 Self {
60 texture_atlas: TextureAtlasLayout::new_empty(size),
61 glyph_to_atlas_index: HashMap::default(),
62 dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 2),
63 texture,
64 }
65 }
66
67 pub fn get_glyph_index(&self, cache_key: GlyphCacheKey) -> Option<GlyphAtlasLocation> {
69 self.glyph_to_atlas_index.get(&cache_key).copied()
70 }
71
72 pub fn has_glyph(&self, cache_key: GlyphCacheKey) -> bool {
74 self.glyph_to_atlas_index.contains_key(&cache_key)
75 }
76
77 pub fn add_glyph(
89 &mut self,
90 textures: &mut Assets<Image>,
91 key: GlyphCacheKey,
92 texture: &Image,
93 offset: Vec2,
94 is_alpha_mask: bool,
95 ) -> Result<(), TextError> {
96 let mut atlas_texture = textures
97 .get_mut(&self.texture)
98 .ok_or(TextError::MissingAtlasTexture)?;
99
100 if let Ok(glyph_index) = self.dynamic_texture_atlas_builder.add_texture(
101 &mut self.texture_atlas,
102 texture,
103 &mut atlas_texture,
104 ) {
105 self.glyph_to_atlas_index.insert(
106 key,
107 GlyphAtlasLocation {
108 glyph_index,
109 offset,
110 is_alpha_mask,
111 },
112 );
113 Ok(())
114 } else {
115 Err(TextError::FailedToAddGlyph(key.glyph_id))
116 }
117 }
118}
119
120impl core::fmt::Debug for FontAtlas {
121 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
122 f.debug_struct("FontAtlas")
123 .field("glyph_to_atlas_index", &self.glyph_to_atlas_index)
124 .field("texture_atlas", &self.texture_atlas)
125 .field("texture", &self.texture)
126 .field("dynamic_texture_atlas_builder", &"[...]")
127 .finish()
128 }
129}
130
131pub fn add_glyph_to_atlas(
133 font_atlases: &mut Vec<FontAtlas>,
134 textures: &mut Assets<Image>,
135 scaler: &mut Scaler,
136 font_smoothing: FontSmoothing,
137 glyph_id: u16,
138) -> Result<GlyphAtlasInfo, TextError> {
139 let (glyph_texture, offset, is_alpha_mask) =
140 get_outlined_glyph_texture(scaler, glyph_id, font_smoothing)?;
141 let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> {
142 atlas.add_glyph(
143 textures,
144 GlyphCacheKey { glyph_id },
145 &glyph_texture,
146 offset,
147 is_alpha_mask,
148 )
149 };
150 if !font_atlases
151 .iter_mut()
152 .any(|atlas| add_char_to_font_atlas(atlas).is_ok())
153 {
154 let glyph_max_size: u32 = glyph_texture
156 .texture_descriptor
157 .size
158 .height
159 .max(glyph_texture.width());
160 let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512);
162
163 let mut new_atlas = FontAtlas::new(textures, UVec2::splat(containing), font_smoothing);
164
165 new_atlas.add_glyph(
166 textures,
167 GlyphCacheKey { glyph_id },
168 &glyph_texture,
169 offset,
170 is_alpha_mask,
171 )?;
172
173 font_atlases.push(new_atlas);
174 }
175
176 get_glyph_atlas_info(font_atlases, GlyphCacheKey { glyph_id })
177 .ok_or(TextError::InconsistentAtlasState)
178}
179
180#[expect(
182 clippy::identity_op,
183 reason = "Alignment improves clarity during RGBA operations."
184)]
185pub fn get_outlined_glyph_texture(
186 scaler: &mut Scaler,
187 glyph_id: u16,
188 font_smoothing: FontSmoothing,
189) -> Result<(Image, Vec2, bool), TextError> {
190 let image = swash::scale::Render::new(&[
191 swash::scale::Source::ColorOutline(0),
192 swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
193 swash::scale::Source::Outline,
194 ])
195 .format(swash::zeno::Format::Alpha)
196 .render(scaler, glyph_id)
197 .ok_or(TextError::FailedToGetGlyphImage(glyph_id))?;
198
199 let left = image.placement.left;
200 let top = image.placement.top;
201 let width = image.placement.width;
202 let height = image.placement.height;
203
204 let px = (width * height) as usize;
205 let rgba = match image.content {
206 swash::scale::image::Content::Mask => {
207 let mut rgba = ::alloc::vec::from_elem(0u8, px * 4)vec![0u8; px * 4];
208 match font_smoothing {
209 FontSmoothing::AntiAliased => {
210 for i in 0..px {
211 let a = image.data[i];
212 rgba[i * 4 + 0] = 255; rgba[i * 4 + 1] = 255; rgba[i * 4 + 2] = 255; rgba[i * 4 + 3] = a; }
217 }
218 FontSmoothing::None => {
219 for i in 0..px {
220 let a = image.data[i];
221 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 }; }
226 }
227 }
228 rgba
229 }
230 swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
231 image.data
232 }
233 };
234
235 Ok((
236 Image::new(
237 Extent3d {
238 width,
239 height,
240 depth_or_array_layers: 1,
241 },
242 TextureDimension::D2,
243 rgba,
244 TextureFormat::Rgba8UnormSrgb,
245 RenderAssetUsages::MAIN_WORLD,
246 ),
247 Vec2::new(left as f32, -top as f32),
248 image.content == swash::scale::image::Content::Mask,
249 ))
250}
251
252pub fn get_glyph_atlas_info(
254 font_atlases: &mut [FontAtlas],
255 cache_key: GlyphCacheKey,
256) -> Option<GlyphAtlasInfo> {
257 font_atlases.iter().find_map(|atlas| {
258 atlas
259 .get_glyph_index(cache_key)
260 .map(|location| GlyphAtlasInfo {
261 offset: location.offset,
262 rect: atlas.texture_atlas.textures[location.glyph_index].as_rect(),
263 texture: atlas.texture.id(),
264 is_alpha_mask: location.is_alpha_mask,
265 })
266 })
267}