1use crate::{Anchor, Sprite};
2use bevy_asset::Assets;
3use bevy_camera::primitives::Aabb;
4use bevy_camera::visibility::{
5 self, NoFrustumCulling, RenderLayers, Visibility, VisibilityClass, VisibleEntities,
6};
7use bevy_camera::Camera;
8use bevy_color::Color;
9use bevy_derive::{Deref, DerefMut};
10use bevy_ecs::entity::EntityHashSet;
11use 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};
19use bevy_image::prelude::*;
20use bevy_math::{FloatOrd, Vec2, Vec3};
21use bevy_reflect::{prelude::ReflectDefault, Reflect};
22use bevy_text::{
23 ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds,
24 TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot,
25 TextSpanAccess, TextWriter,
26};
27use bevy_transform::components::Transform;
28use core::any::TypeId;
29
30#[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>)]
93pub struct Text2d(pub String);
94
95impl Text2d {
96 pub fn new(text: impl Into<String>) -> Self {
98 Self(text.into())
99 }
100}
101
102impl TextRoot for Text2d {}
103
104impl 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
113impl From<&str> for Text2d {
114 fn from(value: &str) -> Self {
115 Self(String::from(value))
116 }
117}
118
119impl From<String> for Text2d {
120 fn from(value: String) -> Self {
121 Self(value)
122 }
123}
124
125pub type Text2dReader<'w, 's> = TextReader<'w, 's, Text2d>;
127
128pub type Text2dWriter<'w, 's> = TextWriter<'w, 's, Text2d>;
130
131#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
135#[reflect(Component, Default, Debug, Clone, PartialEq)]
136pub struct Text2dShadow {
137 pub offset: Vec2,
140 pub color: Color,
142}
143
144impl Default for Text2dShadow {
145 fn default() -> Self {
146 Self {
147 offset: Vec2::new(4., -4.),
148 color: Color::BLACK,
149 }
150 }
151}
152
153pub fn update_text2d_layout(
161 mut target_scale_factors: Local<Vec<(f32, RenderLayers)>>,
162 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 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 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
267pub 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)]
305mod 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 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 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 assert_eq!(aabb.center.z, 0.0);
391 assert_eq!(aabb.half_extents.z, 0.0);
392
393 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 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 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 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}