Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_text/src/pipeline.rs
9394 views
1
use alloc::borrow::Cow;
2
3
use core::hash::BuildHasher;
4
5
use bevy_asset::Assets;
6
use bevy_color::Color;
7
use bevy_ecs::{
8
component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource,
9
system::ResMut,
10
};
11
use bevy_image::prelude::*;
12
use bevy_log::warn_once;
13
use bevy_math::{Rect, UVec2, Vec2};
14
use bevy_platform::hash::FixedHasher;
15
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
16
use parley::style::{OverflowWrap, TextWrapMode};
17
use parley::{
18
Alignment, AlignmentOptions, FontFamily, FontStack, Layout, PositionedLayoutItem,
19
StyleProperty, WordBreakStrength,
20
};
21
use swash::FontRef;
22
23
use crate::{
24
add_glyph_to_atlas,
25
error::TextError,
26
get_glyph_atlas_info,
27
parley_context::{FontCx, LayoutCx, ScaleCx},
28
ComputedTextBlock, Font, FontAtlasKey, FontAtlasSet, FontHinting, FontSmoothing, FontSource,
29
Justify, LineBreak, LineHeight, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout,
30
};
31
32
/// The `TextPipeline` is used to layout and render text blocks (see `Text`/`Text2d`).
33
#[derive(Resource, Default)]
34
pub struct TextPipeline {
35
/// Buffered vec for collecting text sections.
36
///
37
/// See <https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10>.
38
sections_buffer: Vec<(usize, &'static str, &'static TextFont, f32, LineHeight)>,
39
/// Buffered string for concatenated text content.
40
text_buffer: String,
41
}
42
43
impl TextPipeline {
44
/// Shapes and lays out text spans into the computed buffer.
45
///
46
/// Negative or 0.0 font sizes will not be laid out.
47
pub fn update_buffer<'a>(
48
&mut self,
49
fonts: &Assets<Font>,
50
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color, LineHeight)>,
51
linebreak: LineBreak,
52
justify: Justify,
53
bounds: TextBounds,
54
scale_factor: f32,
55
computed: &mut ComputedTextBlock,
56
font_system: &mut FontCx,
57
layout_cx: &mut LayoutCx,
58
hinting: FontHinting,
59
logical_viewport_size: Vec2,
60
base_rem_size: f32,
61
) -> Result<(), TextError> {
62
computed.entities.clear();
63
computed.needs_rerender = false;
64
computed.uses_rem_sizes = false;
65
computed.uses_viewport_sizes = false;
66
computed.font_hinting = hinting;
67
68
if scale_factor <= 0.0 {
69
warn_once!("Text scale factor is <= 0.0. No text will be displayed.");
70
return Err(TextError::DegenerateScaleFactor);
71
}
72
73
let mut sections: Vec<(usize, &str, &TextFont, f32, LineHeight)> =
74
core::mem::take(&mut self.sections_buffer)
75
.into_iter()
76
.map(|_| -> (usize, &str, &TextFont, f32, LineHeight) { unreachable!() })
77
.collect();
78
79
let result = {
80
for (span_index, (entity, depth, span, text_font, _color, line_height)) in
81
text_spans.enumerate()
82
{
83
match text_font.font_size {
84
crate::FontSize::Vw(_)
85
| crate::FontSize::Vh(_)
86
| crate::FontSize::VMin(_)
87
| crate::FontSize::VMax(_) => computed.uses_viewport_sizes = true,
88
crate::FontSize::Rem(_) => computed.uses_rem_sizes = true,
89
_ => (),
90
}
91
92
computed.entities.push(TextEntity {
93
entity,
94
depth,
95
font_smoothing: text_font.font_smoothing,
96
});
97
98
if span.is_empty() {
99
continue;
100
}
101
102
if matches!(text_font.font, FontSource::Handle(_))
103
&& resolve_font_source(&text_font.font, fonts).is_err()
104
{
105
return Err(TextError::NoSuchFont);
106
}
107
108
let font_size = text_font
109
.font_size
110
.eval(logical_viewport_size, base_rem_size);
111
112
if font_size <= 0.0 {
113
warn_once!(
114
"Text span {entity} has a font size <= 0.0. Nothing will be displayed."
115
);
116
continue;
117
}
118
119
const WARN_FONT_SIZE: f32 = 1000.0;
120
if font_size > WARN_FONT_SIZE {
121
warn_once!(
122
"Text span {entity} has an excessively large font size ({} with scale factor {}). \
123
Extremely large font sizes will cause performance issues with font atlas \
124
generation and high memory usage.",
125
font_size,
126
scale_factor,
127
);
128
}
129
130
sections.push((span_index, span, text_font, font_size, line_height));
131
}
132
133
self.text_buffer.clear();
134
for (_, span, _, _, _) in &sections {
135
self.text_buffer.push_str(span);
136
}
137
138
let text = self.text_buffer.as_str();
139
let layout = &mut computed.layout;
140
let mut builder =
141
layout_cx
142
.0
143
.ranged_builder(&mut font_system.0, text, scale_factor, true);
144
145
match linebreak {
146
LineBreak::AnyCharacter => {
147
builder.push_default(StyleProperty::WordBreak(WordBreakStrength::BreakAll));
148
}
149
LineBreak::WordOrCharacter => {
150
builder.push_default(StyleProperty::OverflowWrap(OverflowWrap::Anywhere));
151
}
152
LineBreak::NoWrap => {
153
builder.push_default(StyleProperty::TextWrapMode(TextWrapMode::NoWrap));
154
}
155
LineBreak::WordBoundary => {
156
builder.push_default(StyleProperty::WordBreak(WordBreakStrength::Normal));
157
}
158
}
159
160
let mut start = 0;
161
for (span_index, span, text_font, font_size, line_height) in sections.drain(..) {
162
let end = start + span.len();
163
let range = start..end;
164
start = end;
165
166
if range.is_empty() {
167
continue;
168
}
169
170
let family = resolve_font_source(&text_font.font, fonts)?;
171
172
builder.push(
173
StyleProperty::FontStack(FontStack::Single(family)),
174
range.clone(),
175
);
176
builder.push(
177
StyleProperty::Brush((span_index as u32, text_font.font_smoothing)),
178
range.clone(),
179
);
180
builder.push(StyleProperty::FontSize(font_size), range.clone());
181
builder.push(
182
StyleProperty::LineHeight(line_height.eval(font_size)),
183
range.clone(),
184
);
185
builder.push(
186
StyleProperty::FontWeight(text_font.weight.into()),
187
range.clone(),
188
);
189
builder.push(
190
StyleProperty::FontWidth(text_font.width.into()),
191
range.clone(),
192
);
193
builder.push(
194
StyleProperty::FontStyle(text_font.style.into()),
195
range.clone(),
196
);
197
builder.push(
198
StyleProperty::FontFeatures((&text_font.font_features).into()),
199
range,
200
);
201
}
202
203
builder.build_into(layout, text);
204
layout_with_bounds(layout, bounds, justify);
205
Ok(())
206
};
207
208
sections.clear();
209
self.sections_buffer = sections
210
.into_iter()
211
.map(
212
|_| -> (usize, &'static str, &'static TextFont, f32, LineHeight) { unreachable!() },
213
)
214
.collect();
215
216
result
217
}
218
219
/// Queues text for measurement.
220
pub fn create_text_measure<'a>(
221
&mut self,
222
entity: Entity,
223
fonts: &Assets<Font>,
224
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color, LineHeight)>,
225
scale_factor: f32,
226
layout: &TextLayout,
227
computed: &mut ComputedTextBlock,
228
font_system: &mut FontCx,
229
layout_cx: &mut LayoutCx,
230
hinting: FontHinting,
231
logical_viewport_size: Vec2,
232
base_rem_size: f32,
233
) -> Result<TextMeasureInfo, TextError> {
234
const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0);
235
236
computed.needs_rerender = false;
237
238
self.update_buffer(
239
fonts,
240
text_spans,
241
layout.linebreak,
242
layout.justify,
243
MIN_WIDTH_CONTENT_BOUNDS,
244
scale_factor,
245
computed,
246
font_system,
247
layout_cx,
248
hinting,
249
logical_viewport_size,
250
base_rem_size,
251
)?;
252
253
let layout_buffer = &mut computed.layout;
254
let min_width_content_size = buffer_dimensions(layout_buffer);
255
256
layout_with_bounds(layout_buffer, TextBounds::UNBOUNDED, layout.justify);
257
let max_width_content_size = buffer_dimensions(layout_buffer);
258
259
Ok(TextMeasureInfo {
260
min: min_width_content_size,
261
max: max_width_content_size,
262
entity,
263
})
264
}
265
266
/// Update [`TextLayoutInfo`] with the new [`PositionedGlyph`] layout.
267
pub fn update_text_layout_info(
268
&mut self,
269
layout_info: &mut TextLayoutInfo,
270
font_atlas_set: &mut FontAtlasSet,
271
texture_atlases: &mut Assets<TextureAtlasLayout>,
272
textures: &mut Assets<Image>,
273
computed: &mut ComputedTextBlock,
274
scale_cx: &mut ScaleCx,
275
bounds: TextBounds,
276
justify: Justify,
277
hinting: FontHinting,
278
) -> Result<(), TextError> {
279
computed.needs_rerender = false;
280
layout_info.clear();
281
282
let layout = &mut computed.layout;
283
layout_with_bounds(layout, bounds, justify);
284
285
let hint = computed.font_hinting.should_hint();
286
287
for (line_index, line) in layout.lines().enumerate() {
288
for item in line.items() {
289
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
290
let span_index = glyph_run.style().brush.0 as usize;
291
let font_smoothing = glyph_run.style().brush.1;
292
let run = glyph_run.run();
293
let font = run.font();
294
let font_size = run.font_size();
295
let coords = run.normalized_coords();
296
let variations_hash = FixedHasher.hash_one(coords);
297
let text_range = run.text_range();
298
let font_atlas_key = FontAtlasKey {
299
id: font.data.id() as u32,
300
index: font.index,
301
font_size_bits: font_size.to_bits(),
302
variations_hash,
303
hinting,
304
font_smoothing,
305
};
306
307
let Some(font_ref) =
308
FontRef::from_index(font.data.as_ref(), font.index as usize)
309
else {
310
return Err(TextError::NoSuchFont);
311
};
312
313
let mut scaler = scale_cx
314
.0
315
.builder(font_ref)
316
.size(font_size)
317
.hint(hint)
318
.normalized_coords(coords)
319
.build();
320
321
for glyph in glyph_run.positioned_glyphs() {
322
let Ok(glyph_id) = u16::try_from(glyph.id) else {
323
continue;
324
};
325
326
let font_atlases = font_atlas_set.entry(font_atlas_key).or_default();
327
let atlas_info =
328
get_glyph_atlas_info(font_atlases, crate::GlyphCacheKey { glyph_id })
329
.map(Ok)
330
.unwrap_or_else(|| {
331
add_glyph_to_atlas(
332
font_atlases,
333
texture_atlases,
334
textures,
335
&mut scaler,
336
font_smoothing,
337
glyph_id,
338
)
339
})?;
340
341
let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap();
342
let location = atlas_info.location;
343
let glyph_rect = texture_atlas.textures[location.glyph_index];
344
let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height());
345
346
let mut x = glyph_size.x as f32 / 2.0 + glyph.x + location.offset.x as f32;
347
let mut y = glyph_size.y as f32 / 2.0 + glyph.y - location.offset.y as f32;
348
349
if font_smoothing == FontSmoothing::None {
350
x = x.round();
351
y = y.round();
352
}
353
354
layout_info.glyphs.push(PositionedGlyph {
355
position: Vec2::new(x, y),
356
size: glyph_size.as_vec2(),
357
atlas_info,
358
span_index,
359
byte_index: text_range.start,
360
byte_length: text_range.len(),
361
line_index,
362
});
363
}
364
365
layout_info.run_geometry.push(RunGeometry {
366
span_index,
367
bounds: Rect::new(
368
glyph_run.offset(),
369
line.metrics().min_coord,
370
glyph_run.offset() + glyph_run.advance(),
371
line.metrics().max_coord,
372
),
373
strikethrough_y: glyph_run.baseline() - run.metrics().strikethrough_offset,
374
strikethrough_thickness: run.metrics().strikethrough_size,
375
underline_y: glyph_run.baseline() - run.metrics().underline_offset,
376
underline_thickness: run.metrics().underline_size,
377
});
378
}
379
}
380
}
381
382
layout_info.size = Vec2::new(layout.full_width(), layout.height()).ceil();
383
Ok(())
384
}
385
}
386
387
fn resolve_font_source<'a>(
388
font: &'a FontSource,
389
fonts: &'a Assets<Font>,
390
) -> Result<FontFamily<'a>, TextError> {
391
Ok(match font {
392
FontSource::Handle(handle) => {
393
let font = fonts.get(handle.id()).ok_or(TextError::NoSuchFont)?;
394
FontFamily::Named(Cow::Borrowed(font.family_name.as_str()))
395
}
396
FontSource::Family(family) => FontFamily::Named(Cow::Borrowed(family.as_str())),
397
FontSource::Serif => FontFamily::Generic(parley::GenericFamily::Serif),
398
FontSource::SansSerif => FontFamily::Generic(parley::GenericFamily::SansSerif),
399
FontSource::Cursive => FontFamily::Generic(parley::GenericFamily::Cursive),
400
FontSource::Fantasy => FontFamily::Generic(parley::GenericFamily::Fantasy),
401
FontSource::Monospace => FontFamily::Generic(parley::GenericFamily::Monospace),
402
FontSource::SystemUi => FontFamily::Generic(parley::GenericFamily::SystemUi),
403
FontSource::UiSerif => FontFamily::Generic(parley::GenericFamily::UiSerif),
404
FontSource::UiSansSerif => FontFamily::Generic(parley::GenericFamily::UiSansSerif),
405
FontSource::UiMonospace => FontFamily::Generic(parley::GenericFamily::UiMonospace),
406
FontSource::UiRounded => FontFamily::Generic(parley::GenericFamily::UiRounded),
407
FontSource::Emoji => FontFamily::Generic(parley::GenericFamily::Emoji),
408
FontSource::Math => FontFamily::Generic(parley::GenericFamily::Math),
409
FontSource::FangSong => FontFamily::Generic(parley::GenericFamily::FangSong),
410
})
411
}
412
413
/// Render information for a corresponding text block.
414
///
415
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::update_text_layout_info`] when an entity has
416
/// [`TextLayout`] and [`ComputedTextBlock`] components.
417
#[derive(Component, Clone, Default, Debug, Reflect)]
418
#[reflect(Component, Default, Debug, Clone)]
419
pub struct TextLayoutInfo {
420
/// The target scale factor for this text layout
421
pub scale_factor: f32,
422
/// Scaled and positioned glyphs in screenspace
423
pub glyphs: Vec<PositionedGlyph>,
424
/// Geometry of each text run used to render text decorations like background colors, strikethrough, and underline.
425
/// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font,
426
/// font size, and line height. A text entity that extends over multiple lines will have multiple corresponding runs.
427
///
428
/// The coordinates are unscaled and relative to the top left corner of the text layout.
429
pub run_geometry: Vec<RunGeometry>,
430
/// The glyphs resulting size
431
pub size: Vec2,
432
}
433
434
impl TextLayoutInfo {
435
/// Clear the layout, retaining capacity
436
pub fn clear(&mut self) {
437
self.scale_factor = 1.;
438
self.glyphs.clear();
439
self.run_geometry.clear();
440
self.size = Vec2::ZERO;
441
}
442
}
443
444
/// Geometry of a text run used to render text decorations like background colors, strikethrough, and underline.
445
/// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font,
446
/// font size, and line height.
447
#[derive(Default, Debug, Clone, Reflect)]
448
pub struct RunGeometry {
449
/// The index of the text entity in [`ComputedTextBlock`] that this run belongs to.
450
pub span_index: usize,
451
/// Bounding box around the text run.
452
pub bounds: Rect,
453
/// Y position of the strikethrough in the text layout.
454
pub strikethrough_y: f32,
455
/// Strikethrough stroke thickness.
456
pub strikethrough_thickness: f32,
457
/// Y position of the underline in the text layout.
458
pub underline_y: f32,
459
/// Underline stroke thickness.
460
pub underline_thickness: f32,
461
}
462
463
impl RunGeometry {
464
/// Returns the center of the strikethrough in the text layout.
465
pub fn strikethrough_position(&self) -> Vec2 {
466
Vec2::new(
467
self.bounds.center().x,
468
self.strikethrough_y + 0.5 * self.strikethrough_thickness,
469
)
470
}
471
472
/// Returns the size of the strikethrough.
473
pub fn strikethrough_size(&self) -> Vec2 {
474
Vec2::new(self.bounds.size().x, self.strikethrough_thickness)
475
}
476
477
/// Returns the center of the underline in the text layout.
478
pub fn underline_position(&self) -> Vec2 {
479
Vec2::new(
480
self.bounds.center().x,
481
self.underline_y + 0.5 * self.underline_thickness,
482
)
483
}
484
485
/// Returns the size of the underline.
486
pub fn underline_size(&self) -> Vec2 {
487
Vec2::new(self.bounds.size().x, self.underline_thickness)
488
}
489
}
490
491
/// Size information for a corresponding [`ComputedTextBlock`] component.
492
///
493
/// Generated via [`TextPipeline::create_text_measure`].
494
#[derive(Debug)]
495
pub struct TextMeasureInfo {
496
/// Minimum size for a text area in pixels, to be used when laying out widgets with taffy.
497
pub min: Vec2,
498
/// Maximum size for a text area in pixels, to be used when laying out widgets with taffy.
499
pub max: Vec2,
500
/// The entity that is measured.
501
pub entity: Entity,
502
}
503
504
impl TextMeasureInfo {
505
/// Computes the size of the text area within the provided bounds.
506
pub fn compute_size(
507
&mut self,
508
bounds: TextBounds,
509
computed: &mut ComputedTextBlock,
510
_font_system: &mut FontCx,
511
) -> Vec2 {
512
// Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed'
513
// whenever a canonical state is required.
514
let layout = &mut computed.layout;
515
layout.break_all_lines(bounds.width);
516
layout.align(bounds.width, Alignment::Start, AlignmentOptions::default());
517
buffer_dimensions(layout)
518
}
519
}
520
521
fn layout_with_bounds(
522
layout: &mut Layout<(u32, FontSmoothing)>,
523
bounds: TextBounds,
524
justify: Justify,
525
) {
526
layout.break_all_lines(bounds.width);
527
528
let container_width = if bounds.width.is_none() && justify != Justify::Left {
529
Some(layout.width())
530
} else {
531
bounds.width
532
};
533
534
layout.align(container_width, justify.into(), AlignmentOptions::default());
535
}
536
537
/// Calculate the size of the text area for the given buffer.
538
fn buffer_dimensions(buffer: &Layout<(u32, FontSmoothing)>) -> Vec2 {
539
let size = Vec2::new(buffer.full_width(), buffer.height());
540
if size.is_finite() {
541
size.ceil()
542
} else {
543
Vec2::ZERO
544
}
545
}
546
547
/// Discards stale data cached in the font system.
548
pub(crate) fn trim_source_cache(mut font_cx: ResMut<FontCx>) {
549
// A trim age of 2 was found to reduce frame time variance vs age of 1 when tested with dynamic text.
550
// See https://github.com/bevyengine/bevy/pull/15037
551
//
552
// We assume only text updated frequently benefits from the shape cache (e.g. animated text, or
553
// text that is dynamically measured for UI).
554
font_cx.0.source_cache.prune(2, false);
555
}
556
557