bevy_text/
font_atlas_set.rs

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