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::Ref,
16};
17use bevy_image::prelude::*;
18use bevy_math::Vec2;
19use bevy_reflect::{std_traits::ReflectDefault, Reflect};
20use bevy_text::{
21 ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet, FontHinting, LineBreak, LineHeight,
22 SwashCache, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo,
23 TextMeasureInfo, TextPipeline, 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(
99 Node,
100 TextLayout,
101 TextFont,
102 TextColor,
103 LineHeight,
104 TextNodeFlags,
105 ContentSize,
106 FontHinting::Disabled
109)]
110pub struct Text(pub String);
111
112impl Text {
113 pub fn new(text: impl Into<String>) -> Self {
115 Self(text.into())
116 }
117}
118
119impl TextRoot for Text {}
120
121impl TextSpanAccess for Text {
122 fn read_span(&self) -> &str {
123 self.as_str()
124 }
125 fn write_span(&mut self) -> &mut String {
126 &mut *self
127 }
128}
129
130impl From<&str> for Text {
131 fn from(value: &str) -> Self {
132 Self(String::from(value))
133 }
134}
135
136impl From<String> for Text {
137 fn from(value: String) -> Self {
138 Self(value)
139 }
140}
141
142#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
146#[reflect(Component, Default, Debug, Clone, PartialEq)]
147pub struct TextShadow {
148 pub offset: Vec2,
151 pub color: Color,
153}
154
155impl Default for TextShadow {
156 fn default() -> Self {
157 Self {
158 offset: Vec2::splat(4.),
159 color: Color::linear_rgba(0., 0., 0., 0.75),
160 }
161 }
162}
163
164pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
166
167pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
169
170pub struct TextMeasure {
172 pub info: TextMeasureInfo,
173}
174
175impl TextMeasure {
176 #[inline]
178 pub const fn needs_buffer(height: Option<f32>, available_width: AvailableSpace) -> bool {
179 height.is_none() && matches!(available_width, AvailableSpace::Definite(_))
180 }
181}
182
183impl Measure for TextMeasure {
184 fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
185 let MeasureArgs {
186 width,
187 height,
188 available_width,
189 buffer,
190 font_system,
191 ..
192 } = measure_args;
193 let x = width.unwrap_or_else(|| match available_width {
194 AvailableSpace::Definite(x) => {
195 x.max(self.info.min.x).min(self.info.max.x)
200 }
201 AvailableSpace::MinContent => self.info.min.x,
202 AvailableSpace::MaxContent => self.info.max.x,
203 });
204
205 height
206 .map_or_else(
207 || match available_width {
208 AvailableSpace::Definite(_) => {
209 if let Some(buffer) = buffer {
210 self.info.compute_size(
211 TextBounds::new_horizontal(x),
212 buffer,
213 font_system,
214 )
215 } else {
216 error!("text measure failed, buffer is missing");
217 Vec2::default()
218 }
219 }
220 AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
221 AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
222 },
223 |y| Vec2::new(x, y),
224 )
225 .ceil()
226 }
227}
228
229pub fn measure_text_system(
240 fonts: Res<Assets<Font>>,
241 mut text_query: Query<
242 (
243 Entity,
244 Ref<TextLayout>,
245 &mut ContentSize,
246 &mut TextNodeFlags,
247 &mut ComputedTextBlock,
248 Ref<ComputedUiRenderTargetInfo>,
249 &ComputedNode,
250 Ref<FontHinting>,
251 ),
252 With<Node>,
253 >,
254 mut text_reader: TextUiReader,
255 mut text_pipeline: ResMut<TextPipeline>,
256 mut font_system: ResMut<CosmicFontSystem>,
257) {
258 for (
259 entity,
260 block,
261 mut content_size,
262 mut text_flags,
263 mut computed,
264 computed_target,
265 computed_node,
266 hinting,
267 ) in &mut text_query
268 {
269 if !(1e-5
272 < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs()
273 || computed.needs_rerender()
274 || text_flags.needs_measure_fn
275 || content_size.is_added()
276 || hinting.is_changed())
277 {
278 continue;
279 }
280
281 match text_pipeline.create_text_measure(
282 entity,
283 fonts.as_ref(),
284 text_reader.iter(entity),
285 computed_target.scale_factor.into(),
286 &block,
287 computed.as_mut(),
288 &mut font_system,
289 *hinting,
290 ) {
291 Ok(measure) => {
292 if block.linebreak == LineBreak::NoWrap {
293 content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
294 } else {
295 content_size.set(NodeMeasure::Text(TextMeasure { info: measure }));
296 }
297
298 text_flags.needs_measure_fn = false;
300 text_flags.needs_recompute = true;
301 }
302 Err(TextError::NoSuchFont) => {
303 text_flags.needs_measure_fn = true;
305 }
306 Err(
307 e @ (TextError::FailedToAddGlyph(_)
308 | TextError::FailedToGetGlyphImage(_)
309 | TextError::MissingAtlasLayout
310 | TextError::MissingAtlasTexture
311 | TextError::InconsistentAtlasState),
312 ) => {
313 panic!("Fatal error when processing text: {e}.");
314 }
315 };
316 }
317}
318
319pub fn text_system(
328 mut textures: ResMut<Assets<Image>>,
329 mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
330 mut font_atlas_set: ResMut<FontAtlasSet>,
331 mut text_pipeline: ResMut<TextPipeline>,
332 mut text_query: Query<(
333 Ref<ComputedNode>,
334 &TextLayout,
335 &mut TextLayoutInfo,
336 &mut TextNodeFlags,
337 &mut ComputedTextBlock,
338 )>,
339 text_font_query: Query<&TextFont>,
340 mut font_system: ResMut<CosmicFontSystem>,
341 mut swash_cache: ResMut<SwashCache>,
342) {
343 for (node, block, mut text_layout_info, mut text_flags, mut computed) in &mut text_query {
344 if node.is_changed() || text_flags.needs_recompute {
345 if text_flags.needs_measure_fn {
347 continue;
348 }
349
350 let scale_factor = node.inverse_scale_factor().recip().into();
351 let physical_node_size = if block.linebreak == LineBreak::NoWrap {
352 TextBounds::UNBOUNDED
354 } else {
355 TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
357 };
358
359 match text_pipeline.update_text_layout_info(
360 &mut text_layout_info,
361 text_font_query,
362 scale_factor,
363 &mut font_atlas_set,
364 &mut texture_atlases,
365 &mut textures,
366 &mut computed,
367 &mut font_system,
368 &mut swash_cache,
369 physical_node_size,
370 block.justify,
371 ) {
372 Err(TextError::NoSuchFont) => {
373 text_flags.needs_recompute = true;
375 }
376 Err(
377 e @ (TextError::FailedToAddGlyph(_)
378 | TextError::FailedToGetGlyphImage(_)
379 | TextError::MissingAtlasLayout
380 | TextError::MissingAtlasTexture
381 | TextError::InconsistentAtlasState),
382 ) => {
383 panic!("Fatal error when processing text: {e}.");
384 }
385 Ok(()) => {
386 text_layout_info.scale_factor = scale_factor as f32;
387 text_layout_info.size *= node.inverse_scale_factor();
388 text_flags.needs_recompute = false;
389 }
390 }
391 }
392 }
393}