Skip to main content

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::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/// 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(
99    Node,
100    TextLayout,
101    TextFont,
102    TextColor,
103    LineHeight,
104    TextNodeFlags,
105    ContentSize,
106    // Disable hinting.
107    // UI text is normally pixel-aligned, but with hinting enabled sometimes the text bounds are miscalculated slightly.
108    FontHinting::Disabled
109)]
110pub struct Text(pub String);
111
112impl Text {
113    /// Makes a new text component.
114    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/// Adds a shadow behind text
143///
144/// Use the `Text2dShadow` component for `Text2d` shadows
145#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
146#[reflect(Component, Default, Debug, Clone, PartialEq)]
147pub struct TextShadow {
148    /// Shadow displacement in logical pixels
149    /// With a value of zero the shadow will be hidden directly behind the text
150    pub offset: Vec2,
151    /// Color of the shadow
152    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
164/// UI alias for [`TextReader`].
165pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
166
167/// UI alias for [`TextWriter`].
168pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
169
170/// Text measurement for UI layout. See [`NodeMeasure`].
171pub struct TextMeasure {
172    pub info: TextMeasureInfo,
173}
174
175impl TextMeasure {
176    /// Checks if the cosmic text buffer is needed for measuring the text.
177    #[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                // It is possible for the "min content width" to be larger than
196                // the "max content width" when soft-wrapping right-aligned text
197                // and possibly other situations.
198
199                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
229/// Generates a new [`Measure`] for a text node on changes to its [`Text`] component.
230///
231/// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space
232/// to provide for the text given the fonts, the text itself and the constraints of the layout.
233///
234/// * Measures are regenerated on changes to either [`ComputedTextBlock`] or [`ComputedUiRenderTargetInfo`].
235/// * Changes that only modify the colors of a `Text` do not require a new `Measure`. This system
236///   is only able to detect that a `Text` component has changed and will regenerate the `Measure` on
237///   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)
238///   method should be called when only changing the `Text`'s colors.
239pub 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        // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure().
270        // 1e-5 epsilon to ignore tiny scale factor float errors
271        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 measure func created successfully, so set `TextNodeFlags` to schedule a recompute
299                text_flags.needs_measure_fn = false;
300                text_flags.needs_recompute = true;
301            }
302            Err(TextError::NoSuchFont) => {
303                // Try again next frame
304                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
319/// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component,
320/// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true.
321/// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`].
322///
323/// ## World Resources
324///
325/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
326/// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`].
327pub 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            // Skip the text node if it is waiting for a new measure func
346            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                // With `NoWrap` set, no constraints are placed on the width of the text.
353                TextBounds::UNBOUNDED
354            } else {
355                // `scale_factor` is already multiplied by `UiScale`
356                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                    // There was an error processing the text layout, try again next frame
374                    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}