Skip to main content

bevy_text/
font_atlas.rs

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/// Key identifying a glyph
11#[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    /// Id used to look up the glyph
14    pub glyph_id: u16,
15}
16
17/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`.
18///
19/// A `FontAtlas` contains one or more textures, each of which contains one or more glyphs packed into them.
20///
21/// A [`FontAtlasSet`](crate::FontAtlasSet) contains a `FontAtlas` for each font size in the same font face.
22///
23/// For the same font face and font size, a glyph will be rasterized differently for different subpixel offsets.
24/// In practice, ranges of subpixel offsets are grouped into subpixel bins to limit the number of rasterized glyphs,
25/// providing a trade-off between visual quality and performance.
26///
27/// A [`GlyphCacheKey`] encodes all of the information of a subpixel-offset glyph and is used to
28/// find that glyphs raster in a [`TextureAtlas`] through its corresponding [`GlyphAtlasLocation`].
29pub struct FontAtlas {
30    /// Used to update the [`TextureAtlasLayout`].
31    pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
32    /// A mapping between subpixel-offset glyphs and their [`GlyphAtlasLocation`].
33    pub glyph_to_atlas_index: HashMap<GlyphCacheKey, GlyphAtlasLocation>,
34    /// The layout for the font atlas.
35    pub texture_atlas: TextureAtlasLayout,
36    /// The texture where this font atlas is located
37    pub texture: Handle<Image>,
38}
39
40impl FontAtlas {
41    /// Create a new [`FontAtlas`] with the given size, adding it to the appropriate asset collections.
42    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            // Need to keep this image CPU persistent in order to add additional glyphs later on
53            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    /// Get the [`GlyphAtlasLocation`] for a subpixel-offset glyph.
68    pub fn get_glyph_index(&self, cache_key: GlyphCacheKey) -> Option<GlyphAtlasLocation> {
69        self.glyph_to_atlas_index.get(&cache_key).copied()
70    }
71
72    /// Checks if the given subpixel-offset glyph is contained in this [`FontAtlas`].
73    pub fn has_glyph(&self, cache_key: GlyphCacheKey) -> bool {
74        self.glyph_to_atlas_index.contains_key(&cache_key)
75    }
76
77    /// Add a glyph to the atlas, updating both its texture and layout.
78    ///
79    /// The glyph is represented by `glyph`, and its image content is `glyph_texture`.
80    /// This content is copied into the atlas texture, and the atlas layout is updated
81    /// to store the location of that glyph into the atlas.
82    ///
83    /// # Returns
84    ///
85    /// Returns `()` if the glyph is successfully added, or [`TextError::FailedToAddGlyph`] otherwise.
86    /// In that case, neither the atlas texture nor the atlas layout are
87    /// modified.
88    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
131/// Adds the given subpixel-offset glyph to the given font atlases
132pub 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        // Find the largest dimension of the glyph, either its width or its height
155        let glyph_max_size: u32 = glyph_texture
156            .texture_descriptor
157            .size
158            .height
159            .max(glyph_texture.width());
160        // Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size
161        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/// Get the texture of the glyph as a rendered image, and its offset
181#[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; // R
213                        rgba[i * 4 + 1] = 255; // G
214                        rgba[i * 4 + 2] = 255; // B
215                        rgba[i * 4 + 3] = a; // A from swash
216                    }
217                }
218                FontSmoothing::None => {
219                    for i in 0..px {
220                        let a = image.data[i];
221                        rgba[i * 4 + 0] = 255; // R
222                        rgba[i * 4 + 1] = 255; // G
223                        rgba[i * 4 + 2] = 255; // B
224                        rgba[i * 4 + 3] = if 127 < a { 255 } else { 0 }; // A from swash
225                    }
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
252/// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph.
253pub 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}