1use crate::{
2 ComputedNode, ComputedUiRenderTargetInfo, ContentSize, FixedMeasure, Measure, MeasureArgs,
3 Node, NodeMeasure,
4};
5use bevy_asset::Assets;
6use bevy_color::Color;
7use bevy_derive::{Deref, DerefMut};
8use bevy_ecs::{
9 change_detection::DetectChanges,
10 component::Component,
11 entity::Entity,
12 query::With,
13 reflect::ReflectComponent,
14 system::{Query, Res, ResMut},
15 world::{Mut, Ref},
16};
17use bevy_image::prelude::*;
18use bevy_math::Vec2;
19use bevy_reflect::{std_traits::ReflectDefault, Reflect};
20use bevy_text::{
21 ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds,
22 TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline,
23 TextReader, TextRoot, TextSpanAccess, TextWriter,
24};
25use taffy::style::AvailableSpace;
26use tracing::error;
27
28#[derive(Component, Debug, Clone, Reflect)]
32#[reflect(Component, Default, Debug, Clone)]
33pub struct TextNodeFlags {
34 needs_measure_fn: bool,
36 needs_recompute: bool,
38}
39
40impl Default for TextNodeFlags {
41 fn default() -> Self {
42 Self {
43 needs_measure_fn: true,
44 needs_recompute: true,
45 }
46 }
47}
48
49#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect, PartialEq)]
97#[reflect(Component, Default, Debug, PartialEq, Clone)]
98#[require(Node, TextLayout, TextFont, TextColor, TextNodeFlags, ContentSize)]
99pub struct Text(pub String);
100
101impl Text {
102 pub fn new(text: impl Into<String>) -> Self {
104 Self(text.into())
105 }
106}
107
108impl TextRoot for Text {}
109
110impl TextSpanAccess for Text {
111 fn read_span(&self) -> &str {
112 self.as_str()
113 }
114 fn write_span(&mut self) -> &mut String {
115 &mut *self
116 }
117}
118
119impl From<&str> for Text {
120 fn from(value: &str) -> Self {
121 Self(String::from(value))
122 }
123}
124
125impl From<String> for Text {
126 fn from(value: String) -> Self {
127 Self(value)
128 }
129}
130
131#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
135#[reflect(Component, Default, Debug, Clone, PartialEq)]
136pub struct TextShadow {
137 pub offset: Vec2,
140 pub color: Color,
142}
143
144impl Default for TextShadow {
145 fn default() -> Self {
146 Self {
147 offset: Vec2::splat(4.),
148 color: Color::linear_rgba(0., 0., 0., 0.75),
149 }
150 }
151}
152
153pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
155
156pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
158
159pub struct TextMeasure {
161 pub info: TextMeasureInfo,
162}
163
164impl TextMeasure {
165 #[inline]
167 pub const fn needs_buffer(height: Option<f32>, available_width: AvailableSpace) -> bool {
168 height.is_none() && matches!(available_width, AvailableSpace::Definite(_))
169 }
170}
171
172impl Measure for TextMeasure {
173 fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
174 let MeasureArgs {
175 width,
176 height,
177 available_width,
178 buffer,
179 font_system,
180 ..
181 } = measure_args;
182 let x = width.unwrap_or_else(|| match available_width {
183 AvailableSpace::Definite(x) => {
184 x.max(self.info.min.x).min(self.info.max.x)
189 }
190 AvailableSpace::MinContent => self.info.min.x,
191 AvailableSpace::MaxContent => self.info.max.x,
192 });
193
194 height
195 .map_or_else(
196 || match available_width {
197 AvailableSpace::Definite(_) => {
198 if let Some(buffer) = buffer {
199 self.info.compute_size(
200 TextBounds::new_horizontal(x),
201 buffer,
202 font_system,
203 )
204 } else {
205 error!("text measure failed, buffer is missing");
206 Vec2::default()
207 }
208 }
209 AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
210 AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
211 },
212 |y| Vec2::new(x, y),
213 )
214 .ceil()
215 }
216}
217
218#[inline]
219fn create_text_measure<'a>(
220 entity: Entity,
221 fonts: &Assets<Font>,
222 scale_factor: f64,
223 spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
224 block: Ref<TextLayout>,
225 text_pipeline: &mut TextPipeline,
226 mut content_size: Mut<ContentSize>,
227 mut text_flags: Mut<TextNodeFlags>,
228 mut computed: Mut<ComputedTextBlock>,
229 font_system: &mut CosmicFontSystem,
230) {
231 match text_pipeline.create_text_measure(
232 entity,
233 fonts,
234 spans,
235 scale_factor,
236 &block,
237 computed.as_mut(),
238 font_system,
239 ) {
240 Ok(measure) => {
241 if block.linebreak == LineBreak::NoWrap {
242 content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
243 } else {
244 content_size.set(NodeMeasure::Text(TextMeasure { info: measure }));
245 }
246
247 text_flags.needs_measure_fn = false;
249 text_flags.needs_recompute = true;
250 }
251 Err(TextError::NoSuchFont) => {
252 text_flags.needs_measure_fn = true;
254 }
255 Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
256 panic!("Fatal error when processing text: {e}.");
257 }
258 };
259}
260
261pub fn measure_text_system(
272 fonts: Res<Assets<Font>>,
273 mut text_query: Query<
274 (
275 Entity,
276 Ref<TextLayout>,
277 &mut ContentSize,
278 &mut TextNodeFlags,
279 &mut ComputedTextBlock,
280 Ref<ComputedUiRenderTargetInfo>,
281 &ComputedNode,
282 ),
283 With<Node>,
284 >,
285 mut text_reader: TextUiReader,
286 mut text_pipeline: ResMut<TextPipeline>,
287 mut font_system: ResMut<CosmicFontSystem>,
288) {
289 for (entity, block, content_size, text_flags, computed, computed_target, computed_node) in
290 &mut text_query
291 {
292 if 1e-5
295 < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs()
296 || computed.needs_rerender()
297 || text_flags.needs_measure_fn
298 || content_size.is_added()
299 {
300 create_text_measure(
301 entity,
302 &fonts,
303 computed_target.scale_factor.into(),
304 text_reader.iter(entity),
305 block,
306 &mut text_pipeline,
307 content_size,
308 text_flags,
309 computed,
310 &mut font_system,
311 );
312 }
313 }
314}
315
316#[inline]
317fn queue_text(
318 entity: Entity,
319 fonts: &Assets<Font>,
320 text_pipeline: &mut TextPipeline,
321 font_atlas_sets: &mut FontAtlasSets,
322 texture_atlases: &mut Assets<TextureAtlasLayout>,
323 textures: &mut Assets<Image>,
324 scale_factor: f32,
325 inverse_scale_factor: f32,
326 block: &TextLayout,
327 node: Ref<ComputedNode>,
328 mut text_flags: Mut<TextNodeFlags>,
329 text_layout_info: Mut<TextLayoutInfo>,
330 computed: &mut ComputedTextBlock,
331 text_reader: &mut TextUiReader,
332 font_system: &mut CosmicFontSystem,
333 swash_cache: &mut SwashCache,
334) {
335 if text_flags.needs_measure_fn {
337 return;
338 }
339
340 let physical_node_size = if block.linebreak == LineBreak::NoWrap {
341 TextBounds::UNBOUNDED
343 } else {
344 TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
346 };
347
348 let text_layout_info = text_layout_info.into_inner();
349 match text_pipeline.queue_text(
350 text_layout_info,
351 fonts,
352 text_reader.iter(entity),
353 scale_factor.into(),
354 block,
355 physical_node_size,
356 font_atlas_sets,
357 texture_atlases,
358 textures,
359 computed,
360 font_system,
361 swash_cache,
362 ) {
363 Err(TextError::NoSuchFont) => {
364 text_flags.needs_recompute = true;
366 }
367 Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
368 panic!("Fatal error when processing text: {e}.");
369 }
370 Ok(()) => {
371 text_layout_info.scale_factor = scale_factor;
372 text_layout_info.size *= inverse_scale_factor;
373 text_flags.needs_recompute = false;
374 }
375 }
376}
377
378pub fn text_system(
387 mut textures: ResMut<Assets<Image>>,
388 fonts: Res<Assets<Font>>,
389 mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
390 mut font_atlas_sets: ResMut<FontAtlasSets>,
391 mut text_pipeline: ResMut<TextPipeline>,
392 mut text_query: Query<(
393 Entity,
394 Ref<ComputedNode>,
395 &TextLayout,
396 &mut TextLayoutInfo,
397 &mut TextNodeFlags,
398 &mut ComputedTextBlock,
399 )>,
400 mut text_reader: TextUiReader,
401 mut font_system: ResMut<CosmicFontSystem>,
402 mut swash_cache: ResMut<SwashCache>,
403) {
404 for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query {
405 if node.is_changed() || text_flags.needs_recompute {
406 queue_text(
407 entity,
408 &fonts,
409 &mut text_pipeline,
410 &mut font_atlas_sets,
411 &mut texture_atlases,
412 &mut textures,
413 node.inverse_scale_factor.recip(),
414 node.inverse_scale_factor,
415 block,
416 node,
417 text_flags,
418 text_layout_info,
419 computed.as_mut(),
420 &mut text_reader,
421 &mut font_system,
422 &mut swash_cache,
423 );
424 }
425 }
426}