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}