Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui/src/widget/text.rs
6599 views
1
use crate::{
2
ComputedNode, ComputedUiRenderTargetInfo, ContentSize, FixedMeasure, Measure, MeasureArgs,
3
Node, NodeMeasure,
4
};
5
use bevy_asset::Assets;
6
use bevy_color::Color;
7
use bevy_derive::{Deref, DerefMut};
8
use 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::{Mut, Ref},
16
};
17
use bevy_image::prelude::*;
18
use bevy_math::Vec2;
19
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
20
use bevy_text::{
21
ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds,
22
TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline,
23
TextReader, TextRoot, TextSpanAccess, TextWriter,
24
};
25
use taffy::style::AvailableSpace;
26
use 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)]
33
pub 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
40
impl 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(Node, TextLayout, TextFont, TextColor, TextNodeFlags, ContentSize)]
99
pub struct Text(pub String);
100
101
impl Text {
102
/// Makes a new text component.
103
pub fn new(text: impl Into<String>) -> Self {
104
Self(text.into())
105
}
106
}
107
108
impl TextRoot for Text {}
109
110
impl TextSpanAccess for Text {
111
fn read_span(&self) -> &str {
112
self.as_str()
113
}
114
fn write_span(&mut self) -> &mut String {
115
&mut *self
116
}
117
}
118
119
impl From<&str> for Text {
120
fn from(value: &str) -> Self {
121
Self(String::from(value))
122
}
123
}
124
125
impl From<String> for Text {
126
fn from(value: String) -> Self {
127
Self(value)
128
}
129
}
130
131
/// Adds a shadow behind text
132
///
133
/// Use the `Text2dShadow` component for `Text2d` shadows
134
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
135
#[reflect(Component, Default, Debug, Clone, PartialEq)]
136
pub struct TextShadow {
137
/// Shadow displacement in logical pixels
138
/// With a value of zero the shadow will be hidden directly behind the text
139
pub offset: Vec2,
140
/// Color of the shadow
141
pub color: Color,
142
}
143
144
impl Default for TextShadow {
145
fn default() -> Self {
146
Self {
147
offset: Vec2::splat(4.),
148
color: Color::linear_rgba(0., 0., 0., 0.75),
149
}
150
}
151
}
152
153
/// UI alias for [`TextReader`].
154
pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
155
156
/// UI alias for [`TextWriter`].
157
pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
158
159
/// Text measurement for UI layout. See [`NodeMeasure`].
160
pub struct TextMeasure {
161
pub info: TextMeasureInfo,
162
}
163
164
impl TextMeasure {
165
/// Checks if the cosmic text buffer is needed for measuring the text.
166
#[inline]
167
pub const fn needs_buffer(height: Option<f32>, available_width: AvailableSpace) -> bool {
168
height.is_none() && matches!(available_width, AvailableSpace::Definite(_))
169
}
170
}
171
172
impl Measure for TextMeasure {
173
fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
174
let MeasureArgs {
175
width,
176
height,
177
available_width,
178
buffer,
179
font_system,
180
..
181
} = measure_args;
182
let x = width.unwrap_or_else(|| match available_width {
183
AvailableSpace::Definite(x) => {
184
// It is possible for the "min content width" to be larger than
185
// the "max content width" when soft-wrapping right-aligned text
186
// and possibly other situations.
187
188
x.max(self.info.min.x).min(self.info.max.x)
189
}
190
AvailableSpace::MinContent => self.info.min.x,
191
AvailableSpace::MaxContent => self.info.max.x,
192
});
193
194
height
195
.map_or_else(
196
|| match available_width {
197
AvailableSpace::Definite(_) => {
198
if let Some(buffer) = buffer {
199
self.info.compute_size(
200
TextBounds::new_horizontal(x),
201
buffer,
202
font_system,
203
)
204
} else {
205
error!("text measure failed, buffer is missing");
206
Vec2::default()
207
}
208
}
209
AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
210
AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
211
},
212
|y| Vec2::new(x, y),
213
)
214
.ceil()
215
}
216
}
217
218
#[inline]
219
fn create_text_measure<'a>(
220
entity: Entity,
221
fonts: &Assets<Font>,
222
scale_factor: f64,
223
spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
224
block: Ref<TextLayout>,
225
text_pipeline: &mut TextPipeline,
226
mut content_size: Mut<ContentSize>,
227
mut text_flags: Mut<TextNodeFlags>,
228
mut computed: Mut<ComputedTextBlock>,
229
font_system: &mut CosmicFontSystem,
230
) {
231
match text_pipeline.create_text_measure(
232
entity,
233
fonts,
234
spans,
235
scale_factor,
236
&block,
237
computed.as_mut(),
238
font_system,
239
) {
240
Ok(measure) => {
241
if block.linebreak == LineBreak::NoWrap {
242
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
243
} else {
244
content_size.set(NodeMeasure::Text(TextMeasure { info: measure }));
245
}
246
247
// Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute
248
text_flags.needs_measure_fn = false;
249
text_flags.needs_recompute = true;
250
}
251
Err(TextError::NoSuchFont) => {
252
// Try again next frame
253
text_flags.needs_measure_fn = true;
254
}
255
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
256
panic!("Fatal error when processing text: {e}.");
257
}
258
};
259
}
260
261
/// Generates a new [`Measure`] for a text node on changes to its [`Text`] component.
262
///
263
/// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space
264
/// to provide for the text given the fonts, the text itself and the constraints of the layout.
265
///
266
/// * Measures are regenerated on changes to either [`ComputedTextBlock`] or [`ComputedUiRenderTargetInfo`].
267
/// * Changes that only modify the colors of a `Text` do not require a new `Measure`. This system
268
/// is only able to detect that a `Text` component has changed and will regenerate the `Measure` on
269
/// 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)
270
/// method should be called when only changing the `Text`'s colors.
271
pub fn measure_text_system(
272
fonts: Res<Assets<Font>>,
273
mut text_query: Query<
274
(
275
Entity,
276
Ref<TextLayout>,
277
&mut ContentSize,
278
&mut TextNodeFlags,
279
&mut ComputedTextBlock,
280
Ref<ComputedUiRenderTargetInfo>,
281
&ComputedNode,
282
),
283
With<Node>,
284
>,
285
mut text_reader: TextUiReader,
286
mut text_pipeline: ResMut<TextPipeline>,
287
mut font_system: ResMut<CosmicFontSystem>,
288
) {
289
for (entity, block, content_size, text_flags, computed, computed_target, computed_node) in
290
&mut text_query
291
{
292
// Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure().
293
// 1e-5 epsilon to ignore tiny scale factor float errors
294
if 1e-5
295
< (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs()
296
|| computed.needs_rerender()
297
|| text_flags.needs_measure_fn
298
|| content_size.is_added()
299
{
300
create_text_measure(
301
entity,
302
&fonts,
303
computed_target.scale_factor.into(),
304
text_reader.iter(entity),
305
block,
306
&mut text_pipeline,
307
content_size,
308
text_flags,
309
computed,
310
&mut font_system,
311
);
312
}
313
}
314
}
315
316
#[inline]
317
fn queue_text(
318
entity: Entity,
319
fonts: &Assets<Font>,
320
text_pipeline: &mut TextPipeline,
321
font_atlas_sets: &mut FontAtlasSets,
322
texture_atlases: &mut Assets<TextureAtlasLayout>,
323
textures: &mut Assets<Image>,
324
scale_factor: f32,
325
inverse_scale_factor: f32,
326
block: &TextLayout,
327
node: Ref<ComputedNode>,
328
mut text_flags: Mut<TextNodeFlags>,
329
text_layout_info: Mut<TextLayoutInfo>,
330
computed: &mut ComputedTextBlock,
331
text_reader: &mut TextUiReader,
332
font_system: &mut CosmicFontSystem,
333
swash_cache: &mut SwashCache,
334
) {
335
// Skip the text node if it is waiting for a new measure func
336
if text_flags.needs_measure_fn {
337
return;
338
}
339
340
let physical_node_size = if block.linebreak == LineBreak::NoWrap {
341
// With `NoWrap` set, no constraints are placed on the width of the text.
342
TextBounds::UNBOUNDED
343
} else {
344
// `scale_factor` is already multiplied by `UiScale`
345
TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
346
};
347
348
let text_layout_info = text_layout_info.into_inner();
349
match text_pipeline.queue_text(
350
text_layout_info,
351
fonts,
352
text_reader.iter(entity),
353
scale_factor.into(),
354
block,
355
physical_node_size,
356
font_atlas_sets,
357
texture_atlases,
358
textures,
359
computed,
360
font_system,
361
swash_cache,
362
) {
363
Err(TextError::NoSuchFont) => {
364
// There was an error processing the text layout, try again next frame
365
text_flags.needs_recompute = true;
366
}
367
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
368
panic!("Fatal error when processing text: {e}.");
369
}
370
Ok(()) => {
371
text_layout_info.scale_factor = scale_factor;
372
text_layout_info.size *= inverse_scale_factor;
373
text_flags.needs_recompute = false;
374
}
375
}
376
}
377
378
/// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component,
379
/// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true.
380
/// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`].
381
///
382
/// ## World Resources
383
///
384
/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
385
/// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`].
386
pub fn text_system(
387
mut textures: ResMut<Assets<Image>>,
388
fonts: Res<Assets<Font>>,
389
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
390
mut font_atlas_sets: ResMut<FontAtlasSets>,
391
mut text_pipeline: ResMut<TextPipeline>,
392
mut text_query: Query<(
393
Entity,
394
Ref<ComputedNode>,
395
&TextLayout,
396
&mut TextLayoutInfo,
397
&mut TextNodeFlags,
398
&mut ComputedTextBlock,
399
)>,
400
mut text_reader: TextUiReader,
401
mut font_system: ResMut<CosmicFontSystem>,
402
mut swash_cache: ResMut<SwashCache>,
403
) {
404
for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query {
405
if node.is_changed() || text_flags.needs_recompute {
406
queue_text(
407
entity,
408
&fonts,
409
&mut text_pipeline,
410
&mut font_atlas_sets,
411
&mut texture_atlases,
412
&mut textures,
413
node.inverse_scale_factor.recip(),
414
node.inverse_scale_factor,
415
block,
416
node,
417
text_flags,
418
text_layout_info,
419
computed.as_mut(),
420
&mut text_reader,
421
&mut font_system,
422
&mut swash_cache,
423
);
424
}
425
}
426
}
427
428