Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_text/src/text.rs
6596 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
pub enum Justify {
218
/// Leftmost character is immediately to the right of the render position.
219
/// Bounds start from the render position and advance rightwards.
220
#[default]
221
Left,
222
/// Leftmost & rightmost characters are equidistant to the render position.
223
/// Bounds start from the render position and advance equally left & right.
224
Center,
225
/// Rightmost character is immediately to the left of the render position.
226
/// Bounds start from the render position and advance leftwards.
227
Right,
228
/// Words are spaced so that leftmost & rightmost characters
229
/// align with their margins.
230
/// Bounds start from the render position and advance equally left & right.
231
Justified,
232
}
233
234
impl From<Justify> for cosmic_text::Align {
235
fn from(justify: Justify) -> Self {
236
match justify {
237
Justify::Left => cosmic_text::Align::Left,
238
Justify::Center => cosmic_text::Align::Center,
239
Justify::Right => cosmic_text::Align::Right,
240
Justify::Justified => cosmic_text::Align::Justified,
241
}
242
}
243
}
244
245
/// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically
246
/// the font face, the font size, and the color.
247
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
248
#[reflect(Component, Default, Debug, Clone)]
249
pub struct TextFont {
250
/// The specific font face to use, as a `Handle` to a [`Font`] asset.
251
///
252
/// If the `font` is not specified, then
253
/// * if `default_font` feature is enabled (enabled by default in `bevy` crate),
254
/// `FiraMono-subset.ttf` compiled into the library is used.
255
/// * otherwise no text will be rendered, unless a custom font is loaded into the default font
256
/// handle.
257
pub font: Handle<Font>,
258
/// The vertical height of rasterized glyphs in the font atlas in pixels.
259
///
260
/// This is multiplied by the window scale factor and `UiScale`, but not the text entity
261
/// transform or camera projection.
262
///
263
/// A new font atlas is generated for every combination of font handle and scaled font size
264
/// which can have a strong performance impact.
265
pub font_size: f32,
266
/// The vertical height of a line of text, from the top of one line to the top of the
267
/// next.
268
///
269
/// Defaults to `LineHeight::RelativeToFont(1.2)`
270
pub line_height: LineHeight,
271
/// The antialiasing method to use when rendering text.
272
pub font_smoothing: FontSmoothing,
273
}
274
275
impl TextFont {
276
/// Returns a new [`TextFont`] with the specified font size.
277
pub fn from_font_size(font_size: f32) -> Self {
278
Self::default().with_font_size(font_size)
279
}
280
281
/// Returns this [`TextFont`] with the specified font face handle.
282
pub fn with_font(mut self, font: Handle<Font>) -> Self {
283
self.font = font;
284
self
285
}
286
287
/// Returns this [`TextFont`] with the specified font size.
288
pub const fn with_font_size(mut self, font_size: f32) -> Self {
289
self.font_size = font_size;
290
self
291
}
292
293
/// Returns this [`TextFont`] with the specified [`FontSmoothing`].
294
pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self {
295
self.font_smoothing = font_smoothing;
296
self
297
}
298
299
/// Returns this [`TextFont`] with the specified [`LineHeight`].
300
pub const fn with_line_height(mut self, line_height: LineHeight) -> Self {
301
self.line_height = line_height;
302
self
303
}
304
}
305
306
impl From<Handle<Font>> for TextFont {
307
fn from(font: Handle<Font>) -> Self {
308
Self { font, ..default() }
309
}
310
}
311
312
impl From<LineHeight> for TextFont {
313
fn from(line_height: LineHeight) -> Self {
314
Self {
315
line_height,
316
..default()
317
}
318
}
319
}
320
321
impl Default for TextFont {
322
fn default() -> Self {
323
Self {
324
font: Default::default(),
325
font_size: 20.0,
326
line_height: LineHeight::default(),
327
font_smoothing: Default::default(),
328
}
329
}
330
}
331
332
/// Specifies the height of each line of text for `Text` and `Text2d`
333
///
334
/// Default is 1.2x the font size
335
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
336
#[reflect(Debug, Clone, PartialEq)]
337
pub enum LineHeight {
338
/// Set line height to a specific number of pixels
339
Px(f32),
340
/// Set line height to a multiple of the font size
341
RelativeToFont(f32),
342
}
343
344
impl LineHeight {
345
pub(crate) fn eval(self, font_size: f32) -> f32 {
346
match self {
347
LineHeight::Px(px) => px,
348
LineHeight::RelativeToFont(scale) => scale * font_size,
349
}
350
}
351
}
352
353
impl Default for LineHeight {
354
fn default() -> Self {
355
LineHeight::RelativeToFont(1.2)
356
}
357
}
358
359
/// The color of the text for this section.
360
#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
361
#[reflect(Component, Default, Debug, PartialEq, Clone)]
362
pub struct TextColor(pub Color);
363
364
impl Default for TextColor {
365
fn default() -> Self {
366
Self::WHITE
367
}
368
}
369
370
impl<T: Into<Color>> From<T> for TextColor {
371
fn from(color: T) -> Self {
372
Self(color.into())
373
}
374
}
375
376
impl TextColor {
377
/// Black colored text
378
pub const BLACK: Self = TextColor(Color::BLACK);
379
/// White colored text
380
pub const WHITE: Self = TextColor(Color::WHITE);
381
}
382
383
/// The background color of the text for this section.
384
#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
385
#[reflect(Component, Default, Debug, PartialEq, Clone)]
386
pub struct TextBackgroundColor(pub Color);
387
388
impl Default for TextBackgroundColor {
389
fn default() -> Self {
390
Self(Color::BLACK)
391
}
392
}
393
394
impl<T: Into<Color>> From<T> for TextBackgroundColor {
395
fn from(color: T) -> Self {
396
Self(color.into())
397
}
398
}
399
400
impl TextBackgroundColor {
401
/// Black background
402
pub const BLACK: Self = TextBackgroundColor(Color::BLACK);
403
/// White background
404
pub const WHITE: Self = TextBackgroundColor(Color::WHITE);
405
}
406
407
/// Determines how lines will be broken when preventing text from running out of bounds.
408
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]
409
#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]
410
pub enum LineBreak {
411
/// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/).
412
/// Lines will be broken up at the nearest suitable word boundary, usually a space.
413
/// This behavior suits most cases, as it keeps words intact across linebreaks.
414
#[default]
415
WordBoundary,
416
/// Lines will be broken without discrimination on any character that would leave bounds.
417
/// This is closer to the behavior one might expect from text in a terminal.
418
/// However it may lead to words being broken up across linebreaks.
419
AnyCharacter,
420
/// Wraps at the word level, or fallback to character level if a word can’t fit on a line by itself
421
WordOrCharacter,
422
/// No soft wrapping, where text is automatically broken up into separate lines when it overflows a boundary, will ever occur.
423
/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, is still enabled.
424
NoWrap,
425
}
426
427
/// Determines which antialiasing method to use when rendering text. By default, text is
428
/// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look.
429
///
430
/// **Note:** Subpixel antialiasing is not currently supported.
431
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]
432
#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]
433
#[doc(alias = "antialiasing")]
434
#[doc(alias = "pixelated")]
435
pub enum FontSmoothing {
436
/// No antialiasing. Useful for when you want to render text with a pixel art aesthetic.
437
///
438
/// Combine this with `UiAntiAlias::Off` and `Msaa::Off` on your 2D camera for a fully pixelated look.
439
///
440
/// **Note:** Due to limitations of the underlying text rendering library,
441
/// this may require specially-crafted pixel fonts to look good, especially at small sizes.
442
None,
443
/// The default grayscale antialiasing. Produces text that looks smooth,
444
/// even at small font sizes and low resolutions with modern vector fonts.
445
#[default]
446
AntiAliased,
447
// TODO: Add subpixel antialias support
448
// SubpixelAntiAliased,
449
}
450
451
/// System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`.
452
///
453
/// Generic over the root text component and text span component. For example, `Text2d`/[`TextSpan`] for
454
/// 2d or `Text`/[`TextSpan`] for UI.
455
pub fn detect_text_needs_rerender<Root: Component>(
456
changed_roots: Query<
457
Entity,
458
(
459
Or<(
460
Changed<Root>,
461
Changed<TextFont>,
462
Changed<TextLayout>,
463
Changed<Children>,
464
)>,
465
With<Root>,
466
With<TextFont>,
467
With<TextLayout>,
468
),
469
>,
470
changed_spans: Query<
471
(Entity, Option<&ChildOf>, Has<TextLayout>),
472
(
473
Or<(
474
Changed<TextSpan>,
475
Changed<TextFont>,
476
Changed<Children>,
477
Changed<ChildOf>, // Included to detect broken text block hierarchies.
478
Added<TextLayout>,
479
)>,
480
With<TextSpan>,
481
With<TextFont>,
482
),
483
>,
484
mut computed: Query<(
485
Option<&ChildOf>,
486
Option<&mut ComputedTextBlock>,
487
Has<TextSpan>,
488
)>,
489
) {
490
// Root entity:
491
// - Root component changed.
492
// - TextFont on root changed.
493
// - TextLayout changed.
494
// - Root children changed (can include additions and removals).
495
for root in changed_roots.iter() {
496
let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else {
497
once!(warn!("found entity {} with a root text component ({}) but no ComputedTextBlock; this warning only \
498
prints once", root, core::any::type_name::<Root>()));
499
continue;
500
};
501
computed.needs_rerender = true;
502
}
503
504
// Span entity:
505
// - Span component changed.
506
// - Span TextFont changed.
507
// - Span children changed (can include additions and removals).
508
for (entity, maybe_span_child_of, has_text_block) in changed_spans.iter() {
509
if has_text_block {
510
once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \
511
text entities (that have {}); this warning only prints once",
512
entity, core::any::type_name::<Root>()));
513
}
514
515
let Some(span_child_of) = maybe_span_child_of else {
516
once!(warn!(
517
"found entity {} with a TextSpan that has no parent; it should have an ancestor \
518
with a root text component ({}); this warning only prints once",
519
entity,
520
core::any::type_name::<Root>()
521
));
522
continue;
523
};
524
let mut parent: Entity = span_child_of.parent();
525
526
// Search for the nearest ancestor with ComputedTextBlock.
527
// Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited
528
// is outweighed by the expense of tracking visited spans.
529
loop {
530
let Ok((maybe_child_of, maybe_computed, has_span)) = computed.get_mut(parent) else {
531
once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a ChildOf \
532
component that points at non-existent entity {}; this warning only prints once",
533
entity, parent));
534
break;
535
};
536
if let Some(mut computed) = maybe_computed {
537
computed.needs_rerender = true;
538
break;
539
}
540
if !has_span {
541
once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \
542
span component or a ComputedTextBlock component; this warning only prints once",
543
entity, parent));
544
break;
545
}
546
let Some(next_child_of) = maybe_child_of else {
547
once!(warn!(
548
"found entity {} with a TextSpan that has no ancestor with the root text \
549
component ({}); this warning only prints once",
550
entity,
551
core::any::type_name::<Root>()
552
));
553
break;
554
};
555
parent = next_child_of.parent();
556
}
557
}
558
}
559
560