bevy_text/
font_atlas_set.rs

1use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
2use bevy_ecs::{event::EventReader, resource::Resource, system::ResMut};
3use bevy_image::prelude::*;
4use bevy_math::{IVec2, UVec2};
5use bevy_platform::collections::HashMap;
6use bevy_reflect::TypePath;
7use bevy_render::{
8    render_asset::RenderAssetUsages,
9    render_resource::{Extent3d, TextureDimension, TextureFormat},
10};
11
12use crate::{error::TextError, Font, FontAtlas, FontSmoothing, GlyphAtlasInfo};
13
14/// A map of font faces to their corresponding [`FontAtlasSet`]s.
15#[derive(Debug, Default, Resource)]
16pub struct FontAtlasSets {
17    // PERF: in theory this could be optimized with Assets storage ... consider making some fast "simple" AssetMap
18    pub(crate) sets: HashMap<AssetId<Font>, FontAtlasSet>,
19}
20
21impl FontAtlasSets {
22    /// Get a reference to the [`FontAtlasSet`] with the given font asset id.
23    pub fn get(&self, id: impl Into<AssetId<Font>>) -> Option<&FontAtlasSet> {
24        let id: AssetId<Font> = id.into();
25        self.sets.get(&id)
26    }
27    /// Get a mutable reference to the [`FontAtlasSet`] with the given font asset id.
28    pub fn get_mut(&mut self, id: impl Into<AssetId<Font>>) -> Option<&mut FontAtlasSet> {
29        let id: AssetId<Font> = id.into();
30        self.sets.get_mut(&id)
31    }
32}
33
34/// A system that cleans up [`FontAtlasSet`]s for removed [`Font`]s
35pub fn remove_dropped_font_atlas_sets(
36    mut font_atlas_sets: ResMut<FontAtlasSets>,
37    mut font_events: EventReader<AssetEvent<Font>>,
38) {
39    for event in font_events.read() {
40        if let AssetEvent::Removed { id } = event {
41            font_atlas_sets.sets.remove(id);
42        }
43    }
44}
45
46/// Identifies a font size and smoothing method in a [`FontAtlasSet`].
47///
48/// Allows an `f32` font size to be used as a key in a `HashMap`, by its binary representation.
49#[derive(Debug, Hash, PartialEq, Eq)]
50pub struct FontAtlasKey(pub u32, pub FontSmoothing);
51
52/// A map of font sizes to their corresponding [`FontAtlas`]es, for a given font face.
53///
54/// Provides the interface for adding and retrieving rasterized glyphs, and manages the [`FontAtlas`]es.
55///
56/// A `FontAtlasSet` is an [`Asset`].
57///
58/// There is one `FontAtlasSet` for each font:
59/// - When a [`Font`] is loaded as an asset and then used in [`TextFont`](crate::TextFont),
60///   a `FontAtlasSet` asset is created from a weak handle to the `Font`.
61/// - ~When a font is loaded as a system font, and then used in [`TextFont`](crate::TextFont),
62///   a `FontAtlasSet` asset is created and stored with a strong handle to the `FontAtlasSet`.~
63///   (*Note that system fonts are not currently supported by the `TextPipeline`.*)
64///
65/// A `FontAtlasSet` contains one or more [`FontAtlas`]es for each font size.
66///
67/// It is used by [`TextPipeline::queue_text`](crate::TextPipeline::queue_text).
68#[derive(Debug, TypePath, Asset)]
69pub struct FontAtlasSet {
70    font_atlases: HashMap<FontAtlasKey, Vec<FontAtlas>>,
71}
72
73impl Default for FontAtlasSet {
74    fn default() -> Self {
75        FontAtlasSet {
76            font_atlases: HashMap::with_capacity_and_hasher(1, Default::default()),
77        }
78    }
79}
80
81impl FontAtlasSet {
82    /// Returns an iterator over the [`FontAtlas`]es in this set
83    pub fn iter(&self) -> impl Iterator<Item = (&FontAtlasKey, &Vec<FontAtlas>)> {
84        self.font_atlases.iter()
85    }
86
87    /// Checks if the given subpixel-offset glyph is contained in any of the [`FontAtlas`]es in this set
88    pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: &FontAtlasKey) -> bool {
89        self.font_atlases
90            .get(font_size)
91            .is_some_and(|font_atlas| font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)))
92    }
93
94    /// Adds the given subpixel-offset glyph to the [`FontAtlas`]es in this set
95    pub fn add_glyph_to_atlas(
96        &mut self,
97        texture_atlases: &mut Assets<TextureAtlasLayout>,
98        textures: &mut Assets<Image>,
99        font_system: &mut cosmic_text::FontSystem,
100        swash_cache: &mut cosmic_text::SwashCache,
101        layout_glyph: &cosmic_text::LayoutGlyph,
102        font_smoothing: FontSmoothing,
103    ) -> Result<GlyphAtlasInfo, TextError> {
104        let physical_glyph = layout_glyph.physical((0., 0.), 1.0);
105
106        let font_atlases = self
107            .font_atlases
108            .entry(FontAtlasKey(
109                physical_glyph.cache_key.font_size_bits,
110                font_smoothing,
111            ))
112            .or_insert_with(|| {
113                vec![FontAtlas::new(
114                    textures,
115                    texture_atlases,
116                    UVec2::splat(512),
117                    font_smoothing,
118                )]
119            });
120
121        let (glyph_texture, offset) = Self::get_outlined_glyph_texture(
122            font_system,
123            swash_cache,
124            &physical_glyph,
125            font_smoothing,
126        )?;
127        let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> {
128            atlas.add_glyph(
129                textures,
130                texture_atlases,
131                physical_glyph.cache_key,
132                &glyph_texture,
133                offset,
134            )
135        };
136        if !font_atlases
137            .iter_mut()
138            .any(|atlas| add_char_to_font_atlas(atlas).is_ok())
139        {
140            // Find the largest dimension of the glyph, either its width or its height
141            let glyph_max_size: u32 = glyph_texture
142                .texture_descriptor
143                .size
144                .height
145                .max(glyph_texture.width());
146            // Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size
147            let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512);
148            font_atlases.push(FontAtlas::new(
149                textures,
150                texture_atlases,
151                UVec2::splat(containing),
152                font_smoothing,
153            ));
154
155            font_atlases.last_mut().unwrap().add_glyph(
156                textures,
157                texture_atlases,
158                physical_glyph.cache_key,
159                &glyph_texture,
160                offset,
161            )?;
162        }
163
164        Ok(self
165            .get_glyph_atlas_info(physical_glyph.cache_key, font_smoothing)
166            .unwrap())
167    }
168
169    /// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph.
170    pub fn get_glyph_atlas_info(
171        &mut self,
172        cache_key: cosmic_text::CacheKey,
173        font_smoothing: FontSmoothing,
174    ) -> Option<GlyphAtlasInfo> {
175        self.font_atlases
176            .get(&FontAtlasKey(cache_key.font_size_bits, font_smoothing))
177            .and_then(|font_atlases| {
178                font_atlases.iter().find_map(|atlas| {
179                    atlas
180                        .get_glyph_index(cache_key)
181                        .map(|location| GlyphAtlasInfo {
182                            location,
183                            texture_atlas: atlas.texture_atlas.clone_weak(),
184                            texture: atlas.texture.clone_weak(),
185                        })
186                })
187            })
188    }
189
190    /// Returns the number of font atlases in this set.
191    pub fn len(&self) -> usize {
192        self.font_atlases.len()
193    }
194
195    /// Returns `true` if the set has no font atlases.
196    pub fn is_empty(&self) -> bool {
197        self.font_atlases.len() == 0
198    }
199
200    /// Get the texture of the glyph as a rendered image, and its offset
201    pub fn get_outlined_glyph_texture(
202        font_system: &mut cosmic_text::FontSystem,
203        swash_cache: &mut cosmic_text::SwashCache,
204        physical_glyph: &cosmic_text::PhysicalGlyph,
205        font_smoothing: FontSmoothing,
206    ) -> Result<(Image, IVec2), TextError> {
207        // NOTE: Ideally, we'd ask COSMIC Text to honor the font smoothing setting directly.
208        // However, since it currently doesn't support that, we render the glyph with antialiasing
209        // and apply a threshold to the alpha channel to simulate the effect.
210        //
211        // This has the side effect of making regular vector fonts look quite ugly when font smoothing
212        // is turned off, but for fonts that are specifically designed for pixel art, it works well.
213        //
214        // See: https://github.com/pop-os/cosmic-text/issues/279
215        let image = swash_cache
216            .get_image_uncached(font_system, physical_glyph.cache_key)
217            .ok_or(TextError::FailedToGetGlyphImage(physical_glyph.cache_key))?;
218
219        let cosmic_text::Placement {
220            left,
221            top,
222            width,
223            height,
224        } = image.placement;
225
226        let data = match image.content {
227            cosmic_text::SwashContent::Mask => {
228                if font_smoothing == FontSmoothing::None {
229                    image
230                        .data
231                        .iter()
232                        // Apply a 50% threshold to the alpha channel
233                        .flat_map(|a| [255, 255, 255, if *a > 127 { 255 } else { 0 }])
234                        .collect()
235                } else {
236                    image
237                        .data
238                        .iter()
239                        .flat_map(|a| [255, 255, 255, *a])
240                        .collect()
241                }
242            }
243            cosmic_text::SwashContent::Color => image.data,
244            cosmic_text::SwashContent::SubpixelMask => {
245                // TODO: implement
246                todo!()
247            }
248        };
249
250        Ok((
251            Image::new(
252                Extent3d {
253                    width,
254                    height,
255                    depth_or_array_layers: 1,
256                },
257                TextureDimension::D2,
258                data,
259                TextureFormat::Rgba8UnormSrgb,
260                RenderAssetUsages::MAIN_WORLD,
261            ),
262            IVec2::new(left, top),
263        ))
264    }
265}