Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui/src/widget/text.rs
9391 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::Ref,
16
};
17
use bevy_image::prelude::*;
18
use bevy_log::warn_once;
19
use bevy_math::Vec2;
20
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
21
use bevy_text::{
22
ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, LineHeight,
23
RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo,
24
TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter,
25
};
26
use taffy::style::AvailableSpace;
27
use tracing::error;
28
29
/// UI text system flags.
30
///
31
/// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing.
32
#[derive(Component, Debug, Clone, Reflect)]
33
#[reflect(Component, Default, Debug, Clone)]
34
pub struct TextNodeFlags {
35
/// If set then a new measure function for the text node will be created.
36
needs_measure_fn: bool,
37
/// If set then the text will be recomputed.
38
needs_recompute: bool,
39
}
40
41
impl Default for TextNodeFlags {
42
fn default() -> Self {
43
Self {
44
needs_measure_fn: true,
45
needs_recompute: true,
46
}
47
}
48
}
49
50
/// The top-level UI text component.
51
///
52
/// Adding [`Text`] to an entity will pull in required components for setting up a UI text node.
53
///
54
/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into
55
/// a [`ComputedTextBlock`]. See [`TextSpan`](bevy_text::TextSpan) for the component used by children of entities with [`Text`].
56
///
57
/// Note that [`Transform`](bevy_transform::components::Transform) on this entity is managed automatically by the UI layout system.
58
///
59
///
60
/// ```
61
/// # use bevy_asset::Handle;
62
/// # use bevy_color::Color;
63
/// # use bevy_color::palettes::basic::BLUE;
64
/// # use bevy_ecs::world::World;
65
/// # use bevy_text::{Font, FontSize, Justify, TextLayout, TextFont, TextColor, TextSpan};
66
/// # use bevy_ui::prelude::Text;
67
/// #
68
/// # let font_handle: Handle<Font> = Default::default();
69
/// # let mut world = World::default();
70
/// #
71
/// // Basic usage.
72
/// world.spawn(Text::new("hello world!"));
73
///
74
/// // With non-default style.
75
/// world.spawn((
76
/// Text::new("hello world!"),
77
/// TextFont {
78
/// font: font_handle.clone().into(),
79
/// font_size: FontSize::Px(60.0),
80
/// ..Default::default()
81
/// },
82
/// TextColor(BLUE.into()),
83
/// ));
84
///
85
/// // With text justification.
86
/// world.spawn((
87
/// Text::new("hello world\nand bevy!"),
88
/// TextLayout::new_with_justify(Justify::Center)
89
/// ));
90
///
91
/// // With spans
92
/// world.spawn(Text::new("hello ")).with_children(|parent| {
93
/// parent.spawn(TextSpan::new("world"));
94
/// parent.spawn((TextSpan::new("!"), TextColor(BLUE.into())));
95
/// });
96
/// ```
97
#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect, PartialEq)]
98
#[reflect(Component, Default, Debug, PartialEq, Clone)]
99
#[require(
100
Node,
101
TextLayout,
102
TextFont,
103
TextColor,
104
LineHeight,
105
TextNodeFlags,
106
ContentSize,
107
// Disable hinting.
108
// UI text is normally pixel-aligned, but with hinting enabled sometimes the text bounds are miscalculated slightly.
109
FontHinting::Disabled
110
)]
111
pub struct Text(pub String);
112
113
impl Text {
114
/// Makes a new text component.
115
pub fn new(text: impl Into<String>) -> Self {
116
Self(text.into())
117
}
118
}
119
120
impl TextRoot for Text {}
121
122
impl TextSpanAccess for Text {
123
fn read_span(&self) -> &str {
124
self.as_str()
125
}
126
fn write_span(&mut self) -> &mut String {
127
&mut *self
128
}
129
}
130
131
impl From<&str> for Text {
132
fn from(value: &str) -> Self {
133
Self(String::from(value))
134
}
135
}
136
137
impl From<String> for Text {
138
fn from(value: String) -> Self {
139
Self(value)
140
}
141
}
142
143
/// Adds a shadow behind text
144
///
145
/// Use the `Text2dShadow` component for `Text2d` shadows
146
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
147
#[reflect(Component, Default, Debug, Clone, PartialEq)]
148
pub struct TextShadow {
149
/// Shadow displacement in logical pixels
150
/// With a value of zero the shadow will be hidden directly behind the text
151
pub offset: Vec2,
152
/// Color of the shadow
153
pub color: Color,
154
}
155
156
impl Default for TextShadow {
157
fn default() -> Self {
158
Self {
159
offset: Vec2::splat(4.),
160
color: Color::linear_rgba(0., 0., 0., 0.75),
161
}
162
}
163
}
164
165
/// UI alias for [`TextReader`].
166
pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
167
168
/// UI alias for [`TextWriter`].
169
pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
170
171
/// Text measurement for UI layout. See [`NodeMeasure`].
172
pub struct TextMeasure {
173
pub info: TextMeasureInfo,
174
}
175
176
impl TextMeasure {
177
/// Checks if the cosmic text buffer is needed for measuring the text.
178
#[inline]
179
pub const fn needs_buffer(height: Option<f32>, available_width: AvailableSpace) -> bool {
180
height.is_none() && matches!(available_width, AvailableSpace::Definite(_))
181
}
182
}
183
184
impl Measure for TextMeasure {
185
fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
186
let MeasureArgs {
187
width,
188
height,
189
available_width,
190
buffer,
191
font_system,
192
..
193
} = measure_args;
194
let x = width.unwrap_or_else(|| match available_width {
195
AvailableSpace::Definite(x) => {
196
// It is possible for the "min content width" to be larger than
197
// the "max content width" when soft-wrapping right-aligned text
198
// and possibly other situations.
199
200
x.max(self.info.min.x).min(self.info.max.x)
201
}
202
AvailableSpace::MinContent => self.info.min.x,
203
AvailableSpace::MaxContent => self.info.max.x,
204
});
205
206
height
207
.map_or_else(
208
|| match available_width {
209
AvailableSpace::Definite(_) => {
210
if let Some(buffer) = buffer {
211
self.info.compute_size(
212
TextBounds::new_horizontal(x),
213
buffer,
214
font_system,
215
)
216
} else {
217
error!("text measure failed, buffer is missing");
218
Vec2::default()
219
}
220
}
221
AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
222
AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
223
},
224
|y| Vec2::new(x, y),
225
)
226
.ceil()
227
}
228
}
229
230
/// Generates a new [`Measure`] for a text node on changes to its [`Text`] component.
231
///
232
/// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space
233
/// to provide for the text given the fonts, the text itself and the constraints of the layout.
234
///
235
/// * Measures are regenerated on changes to either [`ComputedTextBlock`] or [`ComputedUiRenderTargetInfo`].
236
/// * Changes that only modify the colors of a `Text` do not require a new `Measure`. This system
237
/// is only able to detect that a `Text` component has changed and will regenerate the `Measure` on
238
/// 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)
239
/// method should be called when only changing the `Text`'s colors.
240
pub fn measure_text_system(
241
fonts: Res<Assets<Font>>,
242
mut text_query: Query<
243
(
244
Entity,
245
Ref<TextLayout>,
246
&mut ContentSize,
247
&mut TextNodeFlags,
248
&mut ComputedTextBlock,
249
Ref<ComputedUiRenderTargetInfo>,
250
&ComputedNode,
251
Ref<FontHinting>,
252
),
253
With<Node>,
254
>,
255
mut text_reader: TextUiReader,
256
mut text_pipeline: ResMut<TextPipeline>,
257
mut font_system: ResMut<FontCx>,
258
mut layout_cx: ResMut<LayoutCx>,
259
rem_size: Res<RemSize>,
260
) {
261
for (
262
entity,
263
block,
264
mut content_size,
265
mut text_flags,
266
mut computed,
267
computed_target,
268
computed_node,
269
hinting,
270
) in &mut text_query
271
{
272
// Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure().
273
// 1e-5 epsilon to ignore tiny scale factor float errors
274
if !(1e-5
275
< (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs()
276
|| computed.needs_rerender(computed_target.is_changed(), rem_size.is_changed())
277
|| text_flags.needs_measure_fn
278
|| content_size.is_added()
279
|| hinting.is_changed())
280
{
281
continue;
282
}
283
284
match text_pipeline.create_text_measure(
285
entity,
286
fonts.as_ref(),
287
text_reader.iter(entity),
288
computed_target.scale_factor,
289
&block,
290
computed.as_mut(),
291
&mut font_system,
292
&mut layout_cx,
293
*hinting,
294
computed_target.logical_size(),
295
rem_size.0,
296
) {
297
Ok(measure) => {
298
if block.linebreak == LineBreak::NoWrap {
299
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
300
} else {
301
content_size.set(NodeMeasure::Text(TextMeasure { info: measure }));
302
}
303
304
// Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute
305
text_flags.needs_measure_fn = false;
306
text_flags.needs_recompute = true;
307
}
308
Err(
309
TextError::NoSuchFont
310
| TextError::NoSuchFontFamily(_)
311
| TextError::DegenerateScaleFactor,
312
) => {
313
// Try again next frame
314
text_flags.needs_measure_fn = true;
315
}
316
Err(
317
e @ (TextError::FailedToAddGlyph(_)
318
| TextError::FailedToGetGlyphImage(_)
319
| TextError::MissingAtlasLayout
320
| TextError::MissingAtlasTexture
321
| TextError::InconsistentAtlasState),
322
) => {
323
panic!("Fatal error when processing text: {e}.");
324
}
325
};
326
}
327
}
328
329
/// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component,
330
/// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true.
331
/// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`].
332
///
333
/// ## World Resources
334
///
335
/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
336
/// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`].
337
pub fn text_system(
338
mut textures: ResMut<Assets<Image>>,
339
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
340
mut font_atlas_set: ResMut<FontAtlasSet>,
341
mut text_pipeline: ResMut<TextPipeline>,
342
mut text_query: Query<(
343
Ref<ComputedNode>,
344
&TextLayout,
345
&mut TextLayoutInfo,
346
&mut TextNodeFlags,
347
&mut ComputedTextBlock,
348
&FontHinting,
349
)>,
350
mut scale_cx: ResMut<ScaleCx>,
351
) {
352
for (node, block, mut text_layout_info, mut text_flags, mut computed, hinting) in
353
&mut text_query
354
{
355
if node.is_changed() || text_flags.needs_recompute {
356
// Skip the text node if it is waiting for a new measure func
357
if text_flags.needs_measure_fn {
358
continue;
359
}
360
361
let physical_node_size = if block.linebreak == LineBreak::NoWrap {
362
// With `NoWrap` set, no constraints are placed on the width of the text.
363
TextBounds::UNBOUNDED
364
} else {
365
// `scale_factor` is already multiplied by `UiScale`
366
TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
367
};
368
369
match text_pipeline.update_text_layout_info(
370
&mut text_layout_info,
371
&mut font_atlas_set,
372
&mut texture_atlases,
373
&mut textures,
374
&mut computed,
375
&mut scale_cx,
376
physical_node_size,
377
block.justify,
378
*hinting,
379
) {
380
Err(
381
TextError::NoSuchFont
382
| TextError::NoSuchFontFamily(_)
383
| TextError::DegenerateScaleFactor,
384
) => {
385
// There was an error processing the text layout, try again next frame
386
text_flags.needs_recompute = true;
387
}
388
Err(e @ TextError::FailedToGetGlyphImage(_)) => {
389
warn_once!("{e}.");
390
text_flags.needs_recompute = false;
391
text_layout_info.clear();
392
}
393
Err(
394
e @ (TextError::FailedToAddGlyph(_)
395
| TextError::MissingAtlasLayout
396
| TextError::MissingAtlasTexture
397
| TextError::InconsistentAtlasState),
398
) => {
399
panic!("Fatal error when processing text: {e}.");
400
}
401
Ok(()) => {
402
text_layout_info.scale_factor = node.inverse_scale_factor().recip();
403
text_layout_info.size *= node.inverse_scale_factor();
404
text_flags.needs_recompute = false;
405
}
406
}
407
}
408
}
409
}
410
411