Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_text/src/text.rs
6849 views
1
use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent};
2
use bevy_asset::Handle;
3
use bevy_color::Color;
4
use bevy_derive::{Deref, DerefMut};
5
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
6
use bevy_reflect::prelude::*;
7
use bevy_utils::{default, once};
8
use cosmic_text::{Buffer, Metrics};
9
use serde::{Deserialize, Serialize};
10
use smallvec::SmallVec;
11
use tracing::warn;
12
13
/// Wrapper for [`cosmic_text::Buffer`]
14
#[derive(Deref, DerefMut, Debug, Clone)]
15
pub struct CosmicBuffer(pub Buffer);
16
17
impl 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)]
28
pub 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)]
42
pub 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
68
impl 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
96
impl 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)]
116
pub 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
124
impl 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)]
176
pub struct TextSpan(pub String);
177
178
impl TextSpan {
179
/// Makes a new text span component.
180
pub fn new(text: impl Into<String>) -> Self {
181
Self(text.into())
182
}
183
}
184
185
impl TextSpanComponent for TextSpan {}
186
187
impl 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
196
impl From<&str> for TextSpan {
197
fn from(value: &str) -> Self {
198
Self(String::from(value))
199
}
200
}
201
202
impl 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")]
218
pub 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
235
impl 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)]
250
pub 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
276
impl 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
307
impl From<Handle<Font>> for TextFont {
308
fn from(font: Handle<Font>) -> Self {
309
Self { font, ..default() }
310
}
311
}
312
313
impl From<LineHeight> for TextFont {
314
fn from(line_height: LineHeight) -> Self {
315
Self {
316
line_height,
317
..default()
318
}
319
}
320
}
321
322
impl 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)]
338
pub 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
345
impl 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
354
impl 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)]
363
pub struct TextColor(pub Color);
364
365
impl Default for TextColor {
366
fn default() -> Self {
367
Self::WHITE
368
}
369
}
370
371
impl<T: Into<Color>> From<T> for TextColor {
372
fn from(color: T) -> Self {
373
Self(color.into())
374
}
375
}
376
377
impl 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)]
387
pub struct TextBackgroundColor(pub Color);
388
389
impl Default for TextBackgroundColor {
390
fn default() -> Self {
391
Self(Color::BLACK)
392
}
393
}
394
395
impl<T: Into<Color>> From<T> for TextBackgroundColor {
396
fn from(color: T) -> Self {
397
Self(color.into())
398
}
399
}
400
401
impl 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)]
411
pub 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")]
436
pub 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.
456
pub 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
}
560
561