Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_text/src/font_atlas_set.rs
6596 views
1
use bevy_asset::{AssetEvent, AssetId, Assets, RenderAssetUsages};
2
use bevy_ecs::{event::EventReader, resource::Resource, system::ResMut};
3
use bevy_image::prelude::*;
4
use bevy_math::{IVec2, UVec2};
5
use bevy_platform::collections::HashMap;
6
use bevy_reflect::TypePath;
7
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
8
9
use crate::{error::TextError, Font, FontAtlas, FontSmoothing, GlyphAtlasInfo};
10
11
/// A map of font faces to their corresponding [`FontAtlasSet`]s.
12
#[derive(Debug, Default, Resource)]
13
pub 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
18
impl 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
32
pub fn remove_dropped_font_atlas_sets(
33
mut font_atlas_sets: ResMut<FontAtlasSets>,
34
mut font_events: EventReader<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)]
47
pub 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)]
58
pub struct FontAtlasSet {
59
font_atlases: HashMap<FontAtlasKey, Vec<FontAtlas>>,
60
}
61
62
impl Default for FontAtlasSet {
63
fn default() -> Self {
64
FontAtlasSet {
65
font_atlases: HashMap::with_capacity_and_hasher(1, Default::default()),
66
}
67
}
68
}
69
70
impl 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
}
255
256