Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_sprite/src/text2d.rs
6601 views
1
use crate::{Anchor, Sprite};
2
use bevy_asset::Assets;
3
use bevy_camera::primitives::Aabb;
4
use bevy_camera::visibility::{
5
self, NoFrustumCulling, RenderLayers, Visibility, VisibilityClass, VisibleEntities,
6
};
7
use bevy_camera::Camera;
8
use bevy_color::Color;
9
use bevy_derive::{Deref, DerefMut};
10
use bevy_ecs::entity::EntityHashSet;
11
use bevy_ecs::{
12
change_detection::{DetectChanges, Ref},
13
component::Component,
14
entity::Entity,
15
prelude::ReflectComponent,
16
query::{Changed, Without},
17
system::{Commands, Local, Query, Res, ResMut},
18
};
19
use bevy_image::prelude::*;
20
use bevy_math::{FloatOrd, Vec2, Vec3};
21
use bevy_reflect::{prelude::ReflectDefault, Reflect};
22
use bevy_text::{
23
ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds,
24
TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot,
25
TextSpanAccess, TextWriter,
26
};
27
use bevy_transform::components::Transform;
28
use core::any::TypeId;
29
30
/// The top-level 2D text component.
31
///
32
/// Adding `Text2d` to an entity will pull in required components for setting up 2d text.
33
/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs)
34
///
35
/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into
36
/// a [`ComputedTextBlock`]. See `TextSpan` for the component used by children of entities with [`Text2d`].
37
///
38
/// With `Text2d` the `justify` field of [`TextLayout`] only affects the internal alignment of a block of text and not its
39
/// relative position, which is controlled by the [`Anchor`] component.
40
/// This means that for a block of text consisting of only one line that doesn't wrap, the `justify` field will have no effect.
41
///
42
///
43
/// ```
44
/// # use bevy_asset::Handle;
45
/// # use bevy_color::Color;
46
/// # use bevy_color::palettes::basic::BLUE;
47
/// # use bevy_ecs::world::World;
48
/// # use bevy_text::{Font, Justify, TextLayout, TextFont, TextColor, TextSpan};
49
/// # use bevy_sprite::Text2d;
50
/// #
51
/// # let font_handle: Handle<Font> = Default::default();
52
/// # let mut world = World::default();
53
/// #
54
/// // Basic usage.
55
/// world.spawn(Text2d::new("hello world!"));
56
///
57
/// // With non-default style.
58
/// world.spawn((
59
/// Text2d::new("hello world!"),
60
/// TextFont {
61
/// font: font_handle.clone().into(),
62
/// font_size: 60.0,
63
/// ..Default::default()
64
/// },
65
/// TextColor(BLUE.into()),
66
/// ));
67
///
68
/// // With text justification.
69
/// world.spawn((
70
/// Text2d::new("hello world\nand bevy!"),
71
/// TextLayout::new_with_justify(Justify::Center)
72
/// ));
73
///
74
/// // With spans
75
/// world.spawn(Text2d::new("hello ")).with_children(|parent| {
76
/// parent.spawn(TextSpan::new("world"));
77
/// parent.spawn((TextSpan::new("!"), TextColor(BLUE.into())));
78
/// });
79
/// ```
80
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)]
81
#[reflect(Component, Default, Debug, Clone)]
82
#[require(
83
TextLayout,
84
TextFont,
85
TextColor,
86
TextBounds,
87
Anchor,
88
Visibility,
89
VisibilityClass,
90
Transform
91
)]
92
#[component(on_add = visibility::add_visibility_class::<Sprite>)]
93
pub struct Text2d(pub String);
94
95
impl Text2d {
96
/// Makes a new 2d text component.
97
pub fn new(text: impl Into<String>) -> Self {
98
Self(text.into())
99
}
100
}
101
102
impl TextRoot for Text2d {}
103
104
impl TextSpanAccess for Text2d {
105
fn read_span(&self) -> &str {
106
self.as_str()
107
}
108
fn write_span(&mut self) -> &mut String {
109
&mut *self
110
}
111
}
112
113
impl From<&str> for Text2d {
114
fn from(value: &str) -> Self {
115
Self(String::from(value))
116
}
117
}
118
119
impl From<String> for Text2d {
120
fn from(value: String) -> Self {
121
Self(value)
122
}
123
}
124
125
/// 2d alias for [`TextReader`].
126
pub type Text2dReader<'w, 's> = TextReader<'w, 's, Text2d>;
127
128
/// 2d alias for [`TextWriter`].
129
pub type Text2dWriter<'w, 's> = TextWriter<'w, 's, Text2d>;
130
131
/// Adds a shadow behind `Text2d` text
132
///
133
/// Use `TextShadow` for text drawn with `bevy_ui`
134
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
135
#[reflect(Component, Default, Debug, Clone, PartialEq)]
136
pub struct Text2dShadow {
137
/// Shadow displacement
138
/// With a value of zero the shadow will be hidden directly behind the text
139
pub offset: Vec2,
140
/// Color of the shadow
141
pub color: Color,
142
}
143
144
impl Default for Text2dShadow {
145
fn default() -> Self {
146
Self {
147
offset: Vec2::new(4., -4.),
148
color: Color::BLACK,
149
}
150
}
151
}
152
153
/// Updates the layout and size information whenever the text or style is changed.
154
/// This information is computed by the [`TextPipeline`] on insertion, then stored.
155
///
156
/// ## World Resources
157
///
158
/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
159
/// It does not modify or observe existing ones.
160
pub fn update_text2d_layout(
161
mut target_scale_factors: Local<Vec<(f32, RenderLayers)>>,
162
// Text items which should be reprocessed again, generally when the font hasn't loaded yet.
163
mut queue: Local<EntityHashSet>,
164
mut textures: ResMut<Assets<Image>>,
165
fonts: Res<Assets<Font>>,
166
camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>,
167
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
168
mut font_atlas_sets: ResMut<FontAtlasSets>,
169
mut text_pipeline: ResMut<TextPipeline>,
170
mut text_query: Query<(
171
Entity,
172
Option<&RenderLayers>,
173
Ref<TextLayout>,
174
Ref<TextBounds>,
175
&mut TextLayoutInfo,
176
&mut ComputedTextBlock,
177
)>,
178
mut text_reader: Text2dReader,
179
mut font_system: ResMut<CosmicFontSystem>,
180
mut swash_cache: ResMut<SwashCache>,
181
) {
182
target_scale_factors.clear();
183
target_scale_factors.extend(
184
camera_query
185
.iter()
186
.filter(|(_, visible_entities, _)| {
187
!visible_entities.get(TypeId::of::<Sprite>()).is_empty()
188
})
189
.filter_map(|(camera, _, maybe_camera_mask)| {
190
camera.target_scaling_factor().map(|scale_factor| {
191
(scale_factor, maybe_camera_mask.cloned().unwrap_or_default())
192
})
193
}),
194
);
195
196
let mut previous_scale_factor = 0.;
197
let mut previous_mask = &RenderLayers::none();
198
199
for (entity, maybe_entity_mask, block, bounds, text_layout_info, mut computed) in
200
&mut text_query
201
{
202
let entity_mask = maybe_entity_mask.unwrap_or_default();
203
204
let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor {
205
previous_scale_factor
206
} else {
207
// `Text2d` only supports generating a single text layout per Text2d entity. If a `Text2d` entity has multiple
208
// render targets with different scale factors, then we use the maximum of the scale factors.
209
let Some((scale_factor, mask)) = target_scale_factors
210
.iter()
211
.filter(|(_, camera_mask)| camera_mask.intersects(entity_mask))
212
.max_by_key(|(scale_factor, _)| FloatOrd(*scale_factor))
213
else {
214
continue;
215
};
216
previous_scale_factor = *scale_factor;
217
previous_mask = mask;
218
*scale_factor
219
};
220
221
if scale_factor != text_layout_info.scale_factor
222
|| computed.needs_rerender()
223
|| bounds.is_changed()
224
|| (!queue.is_empty() && queue.remove(&entity))
225
{
226
let text_bounds = TextBounds {
227
width: if block.linebreak == LineBreak::NoWrap {
228
None
229
} else {
230
bounds.width.map(|width| width * scale_factor)
231
},
232
height: bounds.height.map(|height| height * scale_factor),
233
};
234
235
let text_layout_info = text_layout_info.into_inner();
236
match text_pipeline.queue_text(
237
text_layout_info,
238
&fonts,
239
text_reader.iter(entity),
240
scale_factor as f64,
241
&block,
242
text_bounds,
243
&mut font_atlas_sets,
244
&mut texture_atlases,
245
&mut textures,
246
computed.as_mut(),
247
&mut font_system,
248
&mut swash_cache,
249
) {
250
Err(TextError::NoSuchFont) => {
251
// There was an error processing the text layout, let's add this entity to the
252
// queue for further processing
253
queue.insert(entity);
254
}
255
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
256
panic!("Fatal error when processing text: {e}.");
257
}
258
Ok(()) => {
259
text_layout_info.scale_factor = scale_factor;
260
text_layout_info.size *= scale_factor.recip();
261
}
262
}
263
}
264
}
265
}
266
267
/// System calculating and inserting an [`Aabb`] component to entities with some
268
/// [`TextLayoutInfo`] and [`Anchor`] components, and without a [`NoFrustumCulling`] component.
269
///
270
/// Used in system set [`VisibilitySystems::CalculateBounds`](bevy_camera::visibility::VisibilitySystems::CalculateBounds).
271
pub fn calculate_bounds_text2d(
272
mut commands: Commands,
273
mut text_to_update_aabb: Query<
274
(
275
Entity,
276
&TextLayoutInfo,
277
&Anchor,
278
&TextBounds,
279
Option<&mut Aabb>,
280
),
281
(Changed<TextLayoutInfo>, Without<NoFrustumCulling>),
282
>,
283
) {
284
for (entity, layout_info, anchor, text_bounds, aabb) in &mut text_to_update_aabb {
285
let size = Vec2::new(
286
text_bounds.width.unwrap_or(layout_info.size.x),
287
text_bounds.height.unwrap_or(layout_info.size.y),
288
);
289
290
let x1 = (Anchor::TOP_LEFT.0.x - anchor.as_vec().x) * size.x;
291
let x2 = (Anchor::TOP_LEFT.0.x - anchor.as_vec().x + 1.) * size.x;
292
let y1 = (Anchor::TOP_LEFT.0.y - anchor.as_vec().y - 1.) * size.y;
293
let y2 = (Anchor::TOP_LEFT.0.y - anchor.as_vec().y) * size.y;
294
let new_aabb = Aabb::from_min_max(Vec3::new(x1, y1, 0.), Vec3::new(x2, y2, 0.));
295
296
if let Some(mut aabb) = aabb {
297
*aabb = new_aabb;
298
} else {
299
commands.entity(entity).try_insert(new_aabb);
300
}
301
}
302
}
303
304
#[cfg(test)]
305
mod tests {
306
307
use bevy_app::{App, Update};
308
use bevy_asset::{load_internal_binary_asset, Handle};
309
use bevy_camera::{ComputedCameraValues, RenderTargetInfo};
310
use bevy_ecs::schedule::IntoScheduleConfigs;
311
use bevy_math::UVec2;
312
use bevy_text::{detect_text_needs_rerender, TextIterScratch};
313
314
use super::*;
315
316
const FIRST_TEXT: &str = "Sample text.";
317
const SECOND_TEXT: &str = "Another, longer sample text.";
318
319
fn setup() -> (App, Entity) {
320
let mut app = App::new();
321
app.init_resource::<Assets<Font>>()
322
.init_resource::<Assets<Image>>()
323
.init_resource::<Assets<TextureAtlasLayout>>()
324
.init_resource::<FontAtlasSets>()
325
.init_resource::<TextPipeline>()
326
.init_resource::<CosmicFontSystem>()
327
.init_resource::<SwashCache>()
328
.init_resource::<TextIterScratch>()
329
.add_systems(
330
Update,
331
(
332
detect_text_needs_rerender::<Text2d>,
333
update_text2d_layout,
334
calculate_bounds_text2d,
335
)
336
.chain(),
337
);
338
339
let mut visible_entities = VisibleEntities::default();
340
visible_entities.push(Entity::PLACEHOLDER, TypeId::of::<Sprite>());
341
342
app.world_mut().spawn((
343
Camera {
344
computed: ComputedCameraValues {
345
target_info: Some(RenderTargetInfo {
346
physical_size: UVec2::splat(1000),
347
scale_factor: 1.,
348
}),
349
..Default::default()
350
},
351
..Default::default()
352
},
353
visible_entities,
354
));
355
356
// A font is needed to ensure the text is laid out with an actual size.
357
load_internal_binary_asset!(
358
app,
359
Handle::default(),
360
"../../bevy_text/src/FiraMono-subset.ttf",
361
|bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
362
);
363
364
let entity = app.world_mut().spawn(Text2d::new(FIRST_TEXT)).id();
365
366
(app, entity)
367
}
368
369
#[test]
370
fn calculate_bounds_text2d_create_aabb() {
371
let (mut app, entity) = setup();
372
373
assert!(!app
374
.world()
375
.get_entity(entity)
376
.expect("Could not find entity")
377
.contains::<Aabb>());
378
379
// Creates the AABB after text layouting.
380
app.update();
381
382
let aabb = app
383
.world()
384
.get_entity(entity)
385
.expect("Could not find entity")
386
.get::<Aabb>()
387
.expect("Text should have an AABB");
388
389
// Text2D AABB does not have a depth.
390
assert_eq!(aabb.center.z, 0.0);
391
assert_eq!(aabb.half_extents.z, 0.0);
392
393
// AABB has an actual size.
394
assert!(aabb.half_extents.x > 0.0 && aabb.half_extents.y > 0.0);
395
}
396
397
#[test]
398
fn calculate_bounds_text2d_update_aabb() {
399
let (mut app, entity) = setup();
400
401
// Creates the initial AABB after text layouting.
402
app.update();
403
404
let first_aabb = *app
405
.world()
406
.get_entity(entity)
407
.expect("Could not find entity")
408
.get::<Aabb>()
409
.expect("Could not find initial AABB");
410
411
let mut entity_ref = app
412
.world_mut()
413
.get_entity_mut(entity)
414
.expect("Could not find entity");
415
*entity_ref
416
.get_mut::<Text2d>()
417
.expect("Missing Text2d on entity") = Text2d::new(SECOND_TEXT);
418
419
// Recomputes the AABB.
420
app.update();
421
422
let second_aabb = *app
423
.world()
424
.get_entity(entity)
425
.expect("Could not find entity")
426
.get::<Aabb>()
427
.expect("Could not find second AABB");
428
429
// Check that the height is the same, but the width is greater.
430
approx::assert_abs_diff_eq!(first_aabb.half_extents.y, second_aabb.half_extents.y);
431
assert!(FIRST_TEXT.len() < SECOND_TEXT.len());
432
assert!(first_aabb.half_extents.x < second_aabb.half_extents.x);
433
}
434
}
435
436