bevy_text/
font_atlas_set.rs

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