bevy_ui/widget/
text.rs

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/// UI text system flags.
29///
30/// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing.
31#[derive(Component, Debug, Clone, Reflect)]
32#[reflect(Component, Default, Debug, Clone)]
33pub struct TextNodeFlags {
34    /// If set then a new measure function for the text node will be created.
35    needs_measure_fn: bool,
36    /// If set then the text will be recomputed.
37    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/// The top-level UI text component.
50///
51/// Adding [`Text`] to an entity will pull in required components for setting up a UI text node.
52///
53/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into
54/// a [`ComputedTextBlock`]. See [`TextSpan`](bevy_text::TextSpan) for the component used by children of entities with [`Text`].
55///
56/// Note that [`Transform`](bevy_transform::components::Transform) on this entity is managed automatically by the UI layout system.
57///
58///
59/// ```
60/// # use bevy_asset::Handle;
61/// # use bevy_color::Color;
62/// # use bevy_color::palettes::basic::BLUE;
63/// # use bevy_ecs::world::World;
64/// # use bevy_text::{Font, Justify, TextLayout, TextFont, TextColor, TextSpan};
65/// # use bevy_ui::prelude::Text;
66/// #
67/// # let font_handle: Handle<Font> = Default::default();
68/// # let mut world = World::default();
69/// #
70/// // Basic usage.
71/// world.spawn(Text::new("hello world!"));
72///
73/// // With non-default style.
74/// world.spawn((
75///     Text::new("hello world!"),
76///     TextFont {
77///         font: font_handle.clone().into(),
78///         font_size: 60.0,
79///         ..Default::default()
80///     },
81///     TextColor(BLUE.into()),
82/// ));
83///
84/// // With text justification.
85/// world.spawn((
86///     Text::new("hello world\nand bevy!"),
87///     TextLayout::new_with_justify(Justify::Center)
88/// ));
89///
90/// // With spans
91/// world.spawn(Text::new("hello ")).with_children(|parent| {
92///     parent.spawn(TextSpan::new("world"));
93///     parent.spawn((TextSpan::new("!"), TextColor(BLUE.into())));
94/// });
95/// ```
96#[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    /// Makes a new text component.
103    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/// Adds a shadow behind text
132///
133/// Use the `Text2dShadow` component for `Text2d` shadows
134#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
135#[reflect(Component, Default, Debug, Clone, PartialEq)]
136pub struct TextShadow {
137    /// Shadow displacement in logical pixels
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
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
153/// UI alias for [`TextReader`].
154pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
155
156/// UI alias for [`TextWriter`].
157pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
158
159/// Text measurement for UI layout. See [`NodeMeasure`].
160pub struct TextMeasure {
161    pub info: TextMeasureInfo,
162}
163
164impl TextMeasure {
165    /// Checks if the cosmic text buffer is needed for measuring the text.
166    #[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                // It is possible for the "min content width" to be larger than
185                // the "max content width" when soft-wrapping right-aligned text
186                // and possibly other situations.
187
188                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 measure func created successfully, so set `TextNodeFlags` to schedule a recompute
248            text_flags.needs_measure_fn = false;
249            text_flags.needs_recompute = true;
250        }
251        Err(TextError::NoSuchFont) => {
252            // Try again next frame
253            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
261/// Generates a new [`Measure`] for a text node on changes to its [`Text`] component.
262///
263/// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space
264/// to provide for the text given the fonts, the text itself and the constraints of the layout.
265///
266/// * Measures are regenerated on changes to either [`ComputedTextBlock`] or [`ComputedUiRenderTargetInfo`].
267/// * Changes that only modify the colors of a `Text` do not require a new `Measure`. This system
268///   is only able to detect that a `Text` component has changed and will regenerate the `Measure` on
269///   color changes. This can be expensive, particularly for large blocks of text, and the [`bypass_change_detection`](bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection)
270///   method should be called when only changing the `Text`'s colors.
271pub 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        // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure().
293        // 1e-5 epsilon to ignore tiny scale factor float errors
294        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    // Skip the text node if it is waiting for a new measure func
336    if text_flags.needs_measure_fn {
337        return;
338    }
339
340    let physical_node_size = if block.linebreak == LineBreak::NoWrap {
341        // With `NoWrap` set, no constraints are placed on the width of the text.
342        TextBounds::UNBOUNDED
343    } else {
344        // `scale_factor` is already multiplied by `UiScale`
345        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            // There was an error processing the text layout, try again next frame
365            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
378/// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component,
379/// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true.
380/// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`].
381///
382/// ## World Resources
383///
384/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
385/// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`].
386pub 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}