bevy_text/
text.rs

1use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent};
2use bevy_asset::Handle;
3use bevy_color::Color;
4use bevy_derive::{Deref, DerefMut};
5use bevy_ecs::{prelude::*, reflect::ReflectComponent};
6use bevy_reflect::prelude::*;
7use bevy_utils::{default, once};
8use cosmic_text::{Buffer, Metrics};
9use serde::{Deserialize, Serialize};
10use smallvec::SmallVec;
11use tracing::warn;
12
13/// Wrapper for [`cosmic_text::Buffer`]
14#[derive(Deref, DerefMut, Debug, Clone)]
15pub struct CosmicBuffer(pub Buffer);
16
17impl Default for CosmicBuffer {
18    fn default() -> Self {
19        Self(Buffer::new_empty(Metrics::new(0.0, 0.000001)))
20    }
21}
22
23/// A sub-entity of a [`ComputedTextBlock`].
24///
25/// Returned by [`ComputedTextBlock::entities`].
26#[derive(Debug, Copy, Clone, Reflect)]
27#[reflect(Debug, Clone)]
28pub struct TextEntity {
29    /// The entity.
30    pub entity: Entity,
31    /// Records the hierarchy depth of the entity within a `TextLayout`.
32    pub depth: usize,
33}
34
35/// Computed information for a text block.
36///
37/// See [`TextLayout`].
38///
39/// Automatically updated by 2d and UI text systems.
40#[derive(Component, Debug, Clone, Reflect)]
41#[reflect(Component, Debug, Default, Clone)]
42pub struct ComputedTextBlock {
43    /// Buffer for managing text layout and creating [`TextLayoutInfo`].
44    ///
45    /// This is private because buffer contents are always refreshed from ECS state when writing glyphs to
46    /// `TextLayoutInfo`. If you want to control the buffer contents manually or use the `cosmic-text`
47    /// editor, then you need to not use `TextLayout` and instead manually implement the conversion to
48    /// `TextLayoutInfo`.
49    #[reflect(ignore, clone)]
50    pub(crate) buffer: CosmicBuffer,
51    /// Entities for all text spans in the block, including the root-level text.
52    ///
53    /// The [`TextEntity::depth`] field can be used to reconstruct the hierarchy.
54    pub(crate) entities: SmallVec<[TextEntity; 1]>,
55    /// Flag set when any change has been made to this block that should cause it to be rerendered.
56    ///
57    /// Includes:
58    /// - [`TextLayout`] changes.
59    /// - [`TextFont`] or `Text2d`/`Text`/`TextSpan` changes anywhere in the block's entity hierarchy.
60    // TODO: This encompasses both structural changes like font size or justification and non-structural
61    // changes like text color and font smoothing. This field currently causes UI to 'remeasure' text, even if
62    // the actual changes are non-structural and can be handled by only rerendering and not remeasuring. A full
63    // solution would probably require splitting TextLayout and TextFont into structural/non-structural
64    // components for more granular change detection. A cost/benefit analysis is needed.
65    pub(crate) needs_rerender: bool,
66}
67
68impl ComputedTextBlock {
69    /// Accesses entities in this block.
70    ///
71    /// Can be used to look up [`TextFont`] components for glyphs in [`TextLayoutInfo`] using the `span_index`
72    /// stored there.
73    pub fn entities(&self) -> &[TextEntity] {
74        &self.entities
75    }
76
77    /// Indicates if the text needs to be refreshed in [`TextLayoutInfo`].
78    ///
79    /// Updated automatically by [`detect_text_needs_rerender`] and cleared
80    /// by [`TextPipeline`](crate::TextPipeline) methods.
81    pub fn needs_rerender(&self) -> bool {
82        self.needs_rerender
83    }
84    /// Accesses the underlying buffer which can be used for `cosmic-text` APIs such as accessing layout information
85    /// or calculating a cursor position.
86    ///
87    /// Mutable access is not offered because changes would be overwritten during the automated layout calculation.
88    /// If you want to control the buffer contents manually or use the `cosmic-text`
89    /// editor, then you need to not use `TextLayout` and instead manually implement the conversion to
90    /// `TextLayoutInfo`.
91    pub fn buffer(&self) -> &CosmicBuffer {
92        &self.buffer
93    }
94}
95
96impl Default for ComputedTextBlock {
97    fn default() -> Self {
98        Self {
99            buffer: CosmicBuffer::default(),
100            entities: SmallVec::default(),
101            needs_rerender: true,
102        }
103    }
104}
105
106/// Component with text format settings for a block of text.
107///
108/// A block of text is composed of text spans, which each have a separate string value and [`TextFont`]. Text
109/// spans associated with a text block are collected into [`ComputedTextBlock`] for layout, and then inserted
110/// to [`TextLayoutInfo`] for rendering.
111///
112/// See `Text2d` in `bevy_sprite` for the core component of 2d text, and `Text` in `bevy_ui` for UI text.
113#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
114#[reflect(Component, Default, Debug, Clone)]
115#[require(ComputedTextBlock, TextLayoutInfo)]
116pub struct TextLayout {
117    /// The text's internal alignment.
118    /// Should not affect its position within a container.
119    pub justify: Justify,
120    /// How the text should linebreak when running out of the bounds determined by `max_size`.
121    pub linebreak: LineBreak,
122}
123
124impl TextLayout {
125    /// Makes a new [`TextLayout`].
126    pub const fn new(justify: Justify, linebreak: LineBreak) -> Self {
127        Self { justify, linebreak }
128    }
129
130    /// Makes a new [`TextLayout`] with the specified [`Justify`].
131    pub fn new_with_justify(justify: Justify) -> Self {
132        Self::default().with_justify(justify)
133    }
134
135    /// Makes a new [`TextLayout`] with the specified [`LineBreak`].
136    pub fn new_with_linebreak(linebreak: LineBreak) -> Self {
137        Self::default().with_linebreak(linebreak)
138    }
139
140    /// Makes a new [`TextLayout`] with soft wrapping disabled.
141    /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.
142    pub fn new_with_no_wrap() -> Self {
143        Self::default().with_no_wrap()
144    }
145
146    /// Returns this [`TextLayout`] with the specified [`Justify`].
147    pub const fn with_justify(mut self, justify: Justify) -> Self {
148        self.justify = justify;
149        self
150    }
151
152    /// Returns this [`TextLayout`] with the specified [`LineBreak`].
153    pub const fn with_linebreak(mut self, linebreak: LineBreak) -> Self {
154        self.linebreak = linebreak;
155        self
156    }
157
158    /// Returns this [`TextLayout`] with soft wrapping disabled.
159    /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.
160    pub const fn with_no_wrap(mut self) -> Self {
161        self.linebreak = LineBreak::NoWrap;
162        self
163    }
164}
165
166/// A span of text in a tree of spans.
167///
168/// A `TextSpan` is only valid when it exists as a child of a parent that has either `Text` or
169/// `Text2d`. The parent's `Text` / `Text2d` component contains the base text content. Any children
170/// with `TextSpan` extend this text by appending their content to the parent's text in sequence to
171/// form a [`ComputedTextBlock`]. The parent's [`TextLayout`] determines the layout of the block
172/// but each node has its own [`TextFont`] and [`TextColor`].
173#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)]
174#[reflect(Component, Default, Debug, Clone)]
175#[require(TextFont, TextColor)]
176pub struct TextSpan(pub String);
177
178impl TextSpan {
179    /// Makes a new text span component.
180    pub fn new(text: impl Into<String>) -> Self {
181        Self(text.into())
182    }
183}
184
185impl TextSpanComponent for TextSpan {}
186
187impl TextSpanAccess for TextSpan {
188    fn read_span(&self) -> &str {
189        self.as_str()
190    }
191    fn write_span(&mut self) -> &mut String {
192        &mut *self
193    }
194}
195
196impl From<&str> for TextSpan {
197    fn from(value: &str) -> Self {
198        Self(String::from(value))
199    }
200}
201
202impl From<String> for TextSpan {
203    fn from(value: String) -> Self {
204        Self(value)
205    }
206}
207
208/// Describes the horizontal alignment of multiple lines of text relative to each other.
209///
210/// This only affects the internal positioning of the lines of text within a text entity and
211/// does not affect the text entity's position.
212///
213/// _Has no affect on a single line text entity_, unless used together with a
214/// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value.
215#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
216#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)]
217#[doc(alias = "JustifyText")]
218pub enum Justify {
219    /// Leftmost character is immediately to the right of the render position.
220    /// Bounds start from the render position and advance rightwards.
221    #[default]
222    Left,
223    /// Leftmost & rightmost characters are equidistant to the render position.
224    /// Bounds start from the render position and advance equally left & right.
225    Center,
226    /// Rightmost character is immediately to the left of the render position.
227    /// Bounds start from the render position and advance leftwards.
228    Right,
229    /// Words are spaced so that leftmost & rightmost characters
230    /// align with their margins.
231    /// Bounds start from the render position and advance equally left & right.
232    Justified,
233}
234
235impl From<Justify> for cosmic_text::Align {
236    fn from(justify: Justify) -> Self {
237        match justify {
238            Justify::Left => cosmic_text::Align::Left,
239            Justify::Center => cosmic_text::Align::Center,
240            Justify::Right => cosmic_text::Align::Right,
241            Justify::Justified => cosmic_text::Align::Justified,
242        }
243    }
244}
245
246/// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically
247/// the font face, the font size, the line height, and the antialiasing method.
248#[derive(Component, Clone, Debug, Reflect, PartialEq)]
249#[reflect(Component, Default, Debug, Clone)]
250pub struct TextFont {
251    /// The specific font face to use, as a `Handle` to a [`Font`] asset.
252    ///
253    /// If the `font` is not specified, then
254    /// * if `default_font` feature is enabled (enabled by default in `bevy` crate),
255    ///   `FiraMono-subset.ttf` compiled into the library is used.
256    /// * otherwise no text will be rendered, unless a custom font is loaded into the default font
257    ///   handle.
258    pub font: Handle<Font>,
259    /// The vertical height of rasterized glyphs in the font atlas in pixels.
260    ///
261    /// This is multiplied by the window scale factor and `UiScale`, but not the text entity
262    /// transform or camera projection.
263    ///
264    /// A new font atlas is generated for every combination of font handle and scaled font size
265    /// which can have a strong performance impact.
266    pub font_size: f32,
267    /// The vertical height of a line of text, from the top of one line to the top of the
268    /// next.
269    ///
270    /// Defaults to `LineHeight::RelativeToFont(1.2)`
271    pub line_height: LineHeight,
272    /// The antialiasing method to use when rendering text.
273    pub font_smoothing: FontSmoothing,
274}
275
276impl TextFont {
277    /// Returns a new [`TextFont`] with the specified font size.
278    pub fn from_font_size(font_size: f32) -> Self {
279        Self::default().with_font_size(font_size)
280    }
281
282    /// Returns this [`TextFont`] with the specified font face handle.
283    pub fn with_font(mut self, font: Handle<Font>) -> Self {
284        self.font = font;
285        self
286    }
287
288    /// Returns this [`TextFont`] with the specified font size.
289    pub const fn with_font_size(mut self, font_size: f32) -> Self {
290        self.font_size = font_size;
291        self
292    }
293
294    /// Returns this [`TextFont`] with the specified [`FontSmoothing`].
295    pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self {
296        self.font_smoothing = font_smoothing;
297        self
298    }
299
300    /// Returns this [`TextFont`] with the specified [`LineHeight`].
301    pub const fn with_line_height(mut self, line_height: LineHeight) -> Self {
302        self.line_height = line_height;
303        self
304    }
305}
306
307impl From<Handle<Font>> for TextFont {
308    fn from(font: Handle<Font>) -> Self {
309        Self { font, ..default() }
310    }
311}
312
313impl From<LineHeight> for TextFont {
314    fn from(line_height: LineHeight) -> Self {
315        Self {
316            line_height,
317            ..default()
318        }
319    }
320}
321
322impl Default for TextFont {
323    fn default() -> Self {
324        Self {
325            font: Default::default(),
326            font_size: 20.0,
327            line_height: LineHeight::default(),
328            font_smoothing: Default::default(),
329        }
330    }
331}
332
333/// Specifies the height of each line of text for `Text` and `Text2d`
334///
335/// Default is 1.2x the font size
336#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
337#[reflect(Debug, Clone, PartialEq)]
338pub enum LineHeight {
339    /// Set line height to a specific number of pixels
340    Px(f32),
341    /// Set line height to a multiple of the font size
342    RelativeToFont(f32),
343}
344
345impl LineHeight {
346    pub(crate) fn eval(self, font_size: f32) -> f32 {
347        match self {
348            LineHeight::Px(px) => px,
349            LineHeight::RelativeToFont(scale) => scale * font_size,
350        }
351    }
352}
353
354impl Default for LineHeight {
355    fn default() -> Self {
356        LineHeight::RelativeToFont(1.2)
357    }
358}
359
360/// The color of the text for this section.
361#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
362#[reflect(Component, Default, Debug, PartialEq, Clone)]
363pub struct TextColor(pub Color);
364
365impl Default for TextColor {
366    fn default() -> Self {
367        Self::WHITE
368    }
369}
370
371impl<T: Into<Color>> From<T> for TextColor {
372    fn from(color: T) -> Self {
373        Self(color.into())
374    }
375}
376
377impl TextColor {
378    /// Black colored text
379    pub const BLACK: Self = TextColor(Color::BLACK);
380    /// White colored text
381    pub const WHITE: Self = TextColor(Color::WHITE);
382}
383
384/// The background color of the text for this section.
385#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
386#[reflect(Component, Default, Debug, PartialEq, Clone)]
387pub struct TextBackgroundColor(pub Color);
388
389impl Default for TextBackgroundColor {
390    fn default() -> Self {
391        Self(Color::BLACK)
392    }
393}
394
395impl<T: Into<Color>> From<T> for TextBackgroundColor {
396    fn from(color: T) -> Self {
397        Self(color.into())
398    }
399}
400
401impl TextBackgroundColor {
402    /// Black background
403    pub const BLACK: Self = TextBackgroundColor(Color::BLACK);
404    /// White background
405    pub const WHITE: Self = TextBackgroundColor(Color::WHITE);
406}
407
408/// Determines how lines will be broken when preventing text from running out of bounds.
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]
410#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]
411pub enum LineBreak {
412    /// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/).
413    /// Lines will be broken up at the nearest suitable word boundary, usually a space.
414    /// This behavior suits most cases, as it keeps words intact across linebreaks.
415    #[default]
416    WordBoundary,
417    /// Lines will be broken without discrimination on any character that would leave bounds.
418    /// This is closer to the behavior one might expect from text in a terminal.
419    /// However it may lead to words being broken up across linebreaks.
420    AnyCharacter,
421    /// Wraps at the word level, or fallback to character level if a word can’t fit on a line by itself
422    WordOrCharacter,
423    /// No soft wrapping, where text is automatically broken up into separate lines when it overflows a boundary, will ever occur.
424    /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, is still enabled.
425    NoWrap,
426}
427
428/// Determines which antialiasing method to use when rendering text. By default, text is
429/// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look.
430///
431/// **Note:** Subpixel antialiasing is not currently supported.
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]
433#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]
434#[doc(alias = "antialiasing")]
435#[doc(alias = "pixelated")]
436pub enum FontSmoothing {
437    /// No antialiasing. Useful for when you want to render text with a pixel art aesthetic.
438    ///
439    /// Combine this with `UiAntiAlias::Off` and `Msaa::Off` on your 2D camera for a fully pixelated look.
440    ///
441    /// **Note:** Due to limitations of the underlying text rendering library,
442    /// this may require specially-crafted pixel fonts to look good, especially at small sizes.
443    None,
444    /// The default grayscale antialiasing. Produces text that looks smooth,
445    /// even at small font sizes and low resolutions with modern vector fonts.
446    #[default]
447    AntiAliased,
448    // TODO: Add subpixel antialias support
449    // SubpixelAntiAliased,
450}
451
452/// System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`.
453///
454/// Generic over the root text component and text span component. For example, `Text2d`/[`TextSpan`] for
455/// 2d or `Text`/[`TextSpan`] for UI.
456pub fn detect_text_needs_rerender<Root: Component>(
457    changed_roots: Query<
458        Entity,
459        (
460            Or<(
461                Changed<Root>,
462                Changed<TextFont>,
463                Changed<TextLayout>,
464                Changed<Children>,
465            )>,
466            With<Root>,
467            With<TextFont>,
468            With<TextLayout>,
469        ),
470    >,
471    changed_spans: Query<
472        (Entity, Option<&ChildOf>, Has<TextLayout>),
473        (
474            Or<(
475                Changed<TextSpan>,
476                Changed<TextFont>,
477                Changed<Children>,
478                Changed<ChildOf>, // Included to detect broken text block hierarchies.
479                Added<TextLayout>,
480            )>,
481            With<TextSpan>,
482            With<TextFont>,
483        ),
484    >,
485    mut computed: Query<(
486        Option<&ChildOf>,
487        Option<&mut ComputedTextBlock>,
488        Has<TextSpan>,
489    )>,
490) {
491    // Root entity:
492    // - Root component changed.
493    // - TextFont on root changed.
494    // - TextLayout changed.
495    // - Root children changed (can include additions and removals).
496    for root in changed_roots.iter() {
497        let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else {
498            once!(warn!("found entity {} with a root text component ({}) but no ComputedTextBlock; this warning only \
499                prints once", root, core::any::type_name::<Root>()));
500            continue;
501        };
502        computed.needs_rerender = true;
503    }
504
505    // Span entity:
506    // - Span component changed.
507    // - Span TextFont changed.
508    // - Span children changed (can include additions and removals).
509    for (entity, maybe_span_child_of, has_text_block) in changed_spans.iter() {
510        if has_text_block {
511            once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \
512                text entities (that have {}); this warning only prints once",
513                entity, core::any::type_name::<Root>()));
514        }
515
516        let Some(span_child_of) = maybe_span_child_of else {
517            once!(warn!(
518                "found entity {} with a TextSpan that has no parent; it should have an ancestor \
519                with a root text component ({}); this warning only prints once",
520                entity,
521                core::any::type_name::<Root>()
522            ));
523            continue;
524        };
525        let mut parent: Entity = span_child_of.parent();
526
527        // Search for the nearest ancestor with ComputedTextBlock.
528        // Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited
529        // is outweighed by the expense of tracking visited spans.
530        loop {
531            let Ok((maybe_child_of, maybe_computed, has_span)) = computed.get_mut(parent) else {
532                once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a ChildOf \
533                    component that points at non-existent entity {}; this warning only prints once",
534                    entity, parent));
535                break;
536            };
537            if let Some(mut computed) = maybe_computed {
538                computed.needs_rerender = true;
539                break;
540            }
541            if !has_span {
542                once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \
543                span component or a ComputedTextBlock component; this warning only prints once",
544                    entity, parent));
545                break;
546            }
547            let Some(next_child_of) = maybe_child_of else {
548                once!(warn!(
549                    "found entity {} with a TextSpan that has no ancestor with the root text \
550                    component ({}); this warning only prints once",
551                    entity,
552                    core::any::type_name::<Root>()
553                ));
554                break;
555            };
556            parent = next_child_of.parent();
557        }
558    }
559}