Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_text/src/pipeline.rs
6596 views
1
use alloc::sync::Arc;
2
3
use bevy_asset::{AssetId, Assets};
4
use bevy_color::Color;
5
use bevy_derive::{Deref, DerefMut};
6
use bevy_ecs::{
7
component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource,
8
system::ResMut,
9
};
10
use bevy_image::prelude::*;
11
use bevy_log::{once, warn};
12
use bevy_math::{Rect, UVec2, Vec2};
13
use bevy_platform::collections::HashMap;
14
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
15
16
use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
17
18
use crate::{
19
error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak,
20
PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout,
21
};
22
23
/// A wrapper resource around a [`cosmic_text::FontSystem`]
24
///
25
/// The font system is used to retrieve fonts and their information, including glyph outlines.
26
///
27
/// This resource is updated by the [`TextPipeline`] resource.
28
#[derive(Resource, Deref, DerefMut)]
29
pub struct CosmicFontSystem(pub cosmic_text::FontSystem);
30
31
impl Default for CosmicFontSystem {
32
fn default() -> Self {
33
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
34
let db = cosmic_text::fontdb::Database::new();
35
// TODO: consider using `cosmic_text::FontSystem::new()` (load system fonts by default)
36
Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db))
37
}
38
}
39
40
/// A wrapper resource around a [`cosmic_text::SwashCache`]
41
///
42
/// The swash cache rasterizer is used to rasterize glyphs
43
///
44
/// This resource is updated by the [`TextPipeline`] resource.
45
#[derive(Resource)]
46
pub struct SwashCache(pub cosmic_text::SwashCache);
47
48
impl Default for SwashCache {
49
fn default() -> Self {
50
Self(cosmic_text::SwashCache::new())
51
}
52
}
53
54
/// Information about a font collected as part of preparing for text layout.
55
#[derive(Clone)]
56
pub struct FontFaceInfo {
57
/// Width class: <https://docs.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass>
58
pub stretch: cosmic_text::fontdb::Stretch,
59
/// Allows italic or oblique faces to be selected
60
pub style: cosmic_text::fontdb::Style,
61
/// The degree of blackness or stroke thickness
62
pub weight: cosmic_text::fontdb::Weight,
63
/// Font family name
64
pub family_name: Arc<str>,
65
}
66
67
/// The `TextPipeline` is used to layout and render text blocks (see `Text`/`Text2d`).
68
///
69
/// See the [crate-level documentation](crate) for more information.
70
#[derive(Default, Resource)]
71
pub struct TextPipeline {
72
/// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its [`Font`] [`Asset`](bevy_asset::Asset).
73
pub map_handle_to_font_id: HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
74
/// Buffered vec for collecting spans.
75
///
76
/// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10).
77
spans_buffer: Vec<(usize, &'static str, &'static TextFont, FontFaceInfo)>,
78
/// Buffered vec for collecting info for glyph assembly.
79
glyph_info: Vec<(AssetId<Font>, FontSmoothing)>,
80
}
81
82
impl TextPipeline {
83
/// Utilizes [`cosmic_text::Buffer`] to shape and layout text
84
///
85
/// Negative or 0.0 font sizes will not be laid out.
86
pub fn update_buffer<'a>(
87
&mut self,
88
fonts: &Assets<Font>,
89
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
90
linebreak: LineBreak,
91
justify: Justify,
92
bounds: TextBounds,
93
scale_factor: f64,
94
computed: &mut ComputedTextBlock,
95
font_system: &mut CosmicFontSystem,
96
) -> Result<(), TextError> {
97
let font_system = &mut font_system.0;
98
99
// Collect span information into a vec. This is necessary because font loading requires mut access
100
// to FontSystem, which the cosmic-text Buffer also needs.
101
let mut font_size: f32 = 0.;
102
let mut line_height: f32 = 0.0;
103
let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color)> =
104
core::mem::take(&mut self.spans_buffer)
105
.into_iter()
106
.map(|_| -> (usize, &str, &TextFont, FontFaceInfo, Color) { unreachable!() })
107
.collect();
108
109
computed.entities.clear();
110
111
for (span_index, (entity, depth, span, text_font, color)) in text_spans.enumerate() {
112
// Save this span entity in the computed text block.
113
computed.entities.push(TextEntity { entity, depth });
114
115
if span.is_empty() {
116
continue;
117
}
118
// Return early if a font is not loaded yet.
119
if !fonts.contains(text_font.font.id()) {
120
spans.clear();
121
self.spans_buffer = spans
122
.into_iter()
123
.map(
124
|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) {
125
unreachable!()
126
},
127
)
128
.collect();
129
130
return Err(TextError::NoSuchFont);
131
}
132
133
// Get max font size for use in cosmic Metrics.
134
font_size = font_size.max(text_font.font_size);
135
line_height = line_height.max(text_font.line_height.eval(text_font.font_size));
136
137
// Load Bevy fonts into cosmic-text's font system.
138
let face_info = load_font_to_fontdb(
139
text_font,
140
font_system,
141
&mut self.map_handle_to_font_id,
142
fonts,
143
);
144
145
// Save spans that aren't zero-sized.
146
if scale_factor <= 0.0 || text_font.font_size <= 0.0 {
147
once!(warn!(
148
"Text span {entity} has a font size <= 0.0. Nothing will be displayed.",
149
));
150
151
continue;
152
}
153
spans.push((span_index, span, text_font, face_info, color));
154
}
155
156
let mut metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32);
157
// Metrics of 0.0 cause `Buffer::set_metrics` to panic. We hack around this by 'falling
158
// through' to call `Buffer::set_rich_text` with zero spans so any cached text will be cleared without
159
// deallocating the buffer.
160
metrics.font_size = metrics.font_size.max(0.000001);
161
metrics.line_height = metrics.line_height.max(0.000001);
162
163
// Map text sections to cosmic-text spans, and ignore sections with negative or zero fontsizes,
164
// since they cannot be rendered by cosmic-text.
165
//
166
// The section index is stored in the metadata of the spans, and could be used
167
// to look up the section the span came from and is not used internally
168
// in cosmic-text.
169
let spans_iter = spans
170
.iter()
171
.map(|(span_index, span, text_font, font_info, color)| {
172
(
173
*span,
174
get_attrs(*span_index, text_font, *color, font_info, scale_factor),
175
)
176
});
177
178
// Update the buffer.
179
let buffer = &mut computed.buffer;
180
buffer.set_metrics_and_size(font_system, metrics, bounds.width, bounds.height);
181
182
buffer.set_wrap(
183
font_system,
184
match linebreak {
185
LineBreak::WordBoundary => Wrap::Word,
186
LineBreak::AnyCharacter => Wrap::Glyph,
187
LineBreak::WordOrCharacter => Wrap::WordOrGlyph,
188
LineBreak::NoWrap => Wrap::None,
189
},
190
);
191
192
buffer.set_rich_text(
193
font_system,
194
spans_iter,
195
&Attrs::new(),
196
Shaping::Advanced,
197
Some(justify.into()),
198
);
199
200
buffer.shape_until_scroll(font_system, false);
201
202
// Workaround for alignment not working for unbounded text.
203
// See https://github.com/pop-os/cosmic-text/issues/343
204
if bounds.width.is_none() && justify != Justify::Left {
205
let dimensions = buffer_dimensions(buffer);
206
// `set_size` causes a re-layout to occur.
207
buffer.set_size(font_system, Some(dimensions.x), bounds.height);
208
}
209
210
// Recover the spans buffer.
211
spans.clear();
212
self.spans_buffer = spans
213
.into_iter()
214
.map(|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) { unreachable!() })
215
.collect();
216
217
Ok(())
218
}
219
220
/// Queues text for rendering
221
///
222
/// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s
223
/// which contain information for rendering the text.
224
pub fn queue_text<'a>(
225
&mut self,
226
layout_info: &mut TextLayoutInfo,
227
fonts: &Assets<Font>,
228
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
229
scale_factor: f64,
230
layout: &TextLayout,
231
bounds: TextBounds,
232
font_atlas_sets: &mut FontAtlasSets,
233
texture_atlases: &mut Assets<TextureAtlasLayout>,
234
textures: &mut Assets<Image>,
235
computed: &mut ComputedTextBlock,
236
font_system: &mut CosmicFontSystem,
237
swash_cache: &mut SwashCache,
238
) -> Result<(), TextError> {
239
layout_info.glyphs.clear();
240
layout_info.section_rects.clear();
241
layout_info.size = Default::default();
242
243
// Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries.
244
computed.needs_rerender = false;
245
246
// Extract font ids from the iterator while traversing it.
247
let mut glyph_info = core::mem::take(&mut self.glyph_info);
248
glyph_info.clear();
249
let text_spans = text_spans.inspect(|(_, _, _, text_font, _)| {
250
glyph_info.push((text_font.font.id(), text_font.font_smoothing));
251
});
252
253
let update_result = self.update_buffer(
254
fonts,
255
text_spans,
256
layout.linebreak,
257
layout.justify,
258
bounds,
259
scale_factor,
260
computed,
261
font_system,
262
);
263
if let Err(err) = update_result {
264
self.glyph_info = glyph_info;
265
return Err(err);
266
}
267
268
let buffer = &mut computed.buffer;
269
let box_size = buffer_dimensions(buffer);
270
271
let result = buffer.layout_runs().try_for_each(|run| {
272
let mut current_section: Option<usize> = None;
273
let mut start = 0.;
274
let mut end = 0.;
275
let result = run
276
.glyphs
277
.iter()
278
.map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i))
279
.try_for_each(|(layout_glyph, line_y, line_i)| {
280
match current_section {
281
Some(section) => {
282
if section != layout_glyph.metadata {
283
layout_info.section_rects.push((
284
computed.entities[section].entity,
285
Rect::new(
286
start,
287
run.line_top,
288
end,
289
run.line_top + run.line_height,
290
),
291
));
292
start = end.max(layout_glyph.x);
293
current_section = Some(layout_glyph.metadata);
294
}
295
end = layout_glyph.x + layout_glyph.w;
296
}
297
None => {
298
current_section = Some(layout_glyph.metadata);
299
start = layout_glyph.x;
300
end = start + layout_glyph.w;
301
}
302
}
303
304
let mut temp_glyph;
305
let span_index = layout_glyph.metadata;
306
let font_id = glyph_info[span_index].0;
307
let font_smoothing = glyph_info[span_index].1;
308
309
let layout_glyph = if font_smoothing == FontSmoothing::None {
310
// If font smoothing is disabled, round the glyph positions and sizes,
311
// effectively discarding all subpixel layout.
312
temp_glyph = layout_glyph.clone();
313
temp_glyph.x = temp_glyph.x.round();
314
temp_glyph.y = temp_glyph.y.round();
315
temp_glyph.w = temp_glyph.w.round();
316
temp_glyph.x_offset = temp_glyph.x_offset.round();
317
temp_glyph.y_offset = temp_glyph.y_offset.round();
318
temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round);
319
320
&temp_glyph
321
} else {
322
layout_glyph
323
};
324
325
let font_atlas_set = font_atlas_sets.sets.entry(font_id).or_default();
326
327
let physical_glyph = layout_glyph.physical((0., 0.), 1.);
328
329
let atlas_info = font_atlas_set
330
.get_glyph_atlas_info(physical_glyph.cache_key, font_smoothing)
331
.map(Ok)
332
.unwrap_or_else(|| {
333
font_atlas_set.add_glyph_to_atlas(
334
texture_atlases,
335
textures,
336
&mut font_system.0,
337
&mut swash_cache.0,
338
layout_glyph,
339
font_smoothing,
340
)
341
})?;
342
343
let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap();
344
let location = atlas_info.location;
345
let glyph_rect = texture_atlas.textures[location.glyph_index];
346
let left = location.offset.x as f32;
347
let top = location.offset.y as f32;
348
let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height());
349
350
// offset by half the size because the origin is center
351
let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32;
352
let y =
353
line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0;
354
355
let position = Vec2::new(x, y);
356
357
let pos_glyph = PositionedGlyph {
358
position,
359
size: glyph_size.as_vec2(),
360
atlas_info,
361
span_index,
362
byte_index: layout_glyph.start,
363
byte_length: layout_glyph.end - layout_glyph.start,
364
line_index: line_i,
365
};
366
layout_info.glyphs.push(pos_glyph);
367
Ok(())
368
});
369
if let Some(section) = current_section {
370
layout_info.section_rects.push((
371
computed.entities[section].entity,
372
Rect::new(start, run.line_top, end, run.line_top + run.line_height),
373
));
374
}
375
376
result
377
});
378
379
// Return the scratch vec.
380
self.glyph_info = glyph_info;
381
382
// Check result.
383
result?;
384
385
layout_info.size = box_size;
386
Ok(())
387
}
388
389
/// Queues text for measurement
390
///
391
/// Produces a [`TextMeasureInfo`] which can be used by a layout system
392
/// to measure the text area on demand.
393
pub fn create_text_measure<'a>(
394
&mut self,
395
entity: Entity,
396
fonts: &Assets<Font>,
397
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
398
scale_factor: f64,
399
layout: &TextLayout,
400
computed: &mut ComputedTextBlock,
401
font_system: &mut CosmicFontSystem,
402
) -> Result<TextMeasureInfo, TextError> {
403
const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0);
404
405
// Clear this here at the focal point of measured text rendering to ensure the field's lifecycle has
406
// strong boundaries.
407
computed.needs_rerender = false;
408
409
self.update_buffer(
410
fonts,
411
text_spans,
412
layout.linebreak,
413
layout.justify,
414
MIN_WIDTH_CONTENT_BOUNDS,
415
scale_factor,
416
computed,
417
font_system,
418
)?;
419
420
let buffer = &mut computed.buffer;
421
let min_width_content_size = buffer_dimensions(buffer);
422
423
let max_width_content_size = {
424
let font_system = &mut font_system.0;
425
buffer.set_size(font_system, None, None);
426
buffer_dimensions(buffer)
427
};
428
429
Ok(TextMeasureInfo {
430
min: min_width_content_size,
431
max: max_width_content_size,
432
entity,
433
})
434
}
435
436
/// Returns the [`cosmic_text::fontdb::ID`] for a given [`Font`] asset.
437
pub fn get_font_id(&self, asset_id: AssetId<Font>) -> Option<cosmic_text::fontdb::ID> {
438
self.map_handle_to_font_id
439
.get(&asset_id)
440
.cloned()
441
.map(|(id, _)| id)
442
}
443
}
444
445
/// Render information for a corresponding text block.
446
///
447
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`] when an entity has
448
/// [`TextLayout`] and [`ComputedTextBlock`] components.
449
#[derive(Component, Clone, Default, Debug, Reflect)]
450
#[reflect(Component, Default, Debug, Clone)]
451
pub struct TextLayoutInfo {
452
/// The target scale factor for this text layout
453
pub scale_factor: f32,
454
/// Scaled and positioned glyphs in screenspace
455
pub glyphs: Vec<PositionedGlyph>,
456
/// Rects bounding the text block's text sections.
457
/// A text section spanning more than one line will have multiple bounding rects.
458
pub section_rects: Vec<(Entity, Rect)>,
459
/// The glyphs resulting size
460
pub size: Vec2,
461
}
462
463
/// Size information for a corresponding [`ComputedTextBlock`] component.
464
///
465
/// Generated via [`TextPipeline::create_text_measure`].
466
#[derive(Debug)]
467
pub struct TextMeasureInfo {
468
/// Minimum size for a text area in pixels, to be used when laying out widgets with taffy
469
pub min: Vec2,
470
/// Maximum size for a text area in pixels, to be used when laying out widgets with taffy
471
pub max: Vec2,
472
/// The entity that is measured.
473
pub entity: Entity,
474
}
475
476
impl TextMeasureInfo {
477
/// Computes the size of the text area within the provided bounds.
478
pub fn compute_size(
479
&mut self,
480
bounds: TextBounds,
481
computed: &mut ComputedTextBlock,
482
font_system: &mut CosmicFontSystem,
483
) -> Vec2 {
484
// Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed'
485
// whenever a canonical state is required.
486
computed
487
.buffer
488
.set_size(&mut font_system.0, bounds.width, bounds.height);
489
buffer_dimensions(&computed.buffer)
490
}
491
}
492
493
/// Add the font to the cosmic text's `FontSystem`'s in-memory font database
494
pub fn load_font_to_fontdb(
495
text_font: &TextFont,
496
font_system: &mut cosmic_text::FontSystem,
497
map_handle_to_font_id: &mut HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
498
fonts: &Assets<Font>,
499
) -> FontFaceInfo {
500
let font_handle = text_font.font.clone();
501
let (face_id, family_name) = map_handle_to_font_id
502
.entry(font_handle.id())
503
.or_insert_with(|| {
504
let font = fonts.get(font_handle.id()).expect(
505
"Tried getting a font that was not available, probably due to not being loaded yet",
506
);
507
let data = Arc::clone(&font.data);
508
let ids = font_system
509
.db_mut()
510
.load_font_source(cosmic_text::fontdb::Source::Binary(data));
511
512
// TODO: it is assumed this is the right font face
513
let face_id = *ids.last().unwrap();
514
let face = font_system.db().face(face_id).unwrap();
515
let family_name = Arc::from(face.families[0].0.as_str());
516
517
(face_id, family_name)
518
});
519
let face = font_system.db().face(*face_id).unwrap();
520
521
FontFaceInfo {
522
stretch: face.stretch,
523
style: face.style,
524
weight: face.weight,
525
family_name: family_name.clone(),
526
}
527
}
528
529
/// Translates [`TextFont`] to [`Attrs`].
530
fn get_attrs<'a>(
531
span_index: usize,
532
text_font: &TextFont,
533
color: Color,
534
face_info: &'a FontFaceInfo,
535
scale_factor: f64,
536
) -> Attrs<'a> {
537
Attrs::new()
538
.metadata(span_index)
539
.family(Family::Name(&face_info.family_name))
540
.stretch(face_info.stretch)
541
.style(face_info.style)
542
.weight(face_info.weight)
543
.metrics(
544
Metrics {
545
font_size: text_font.font_size,
546
line_height: text_font.line_height.eval(text_font.font_size),
547
}
548
.scale(scale_factor as f32),
549
)
550
.color(cosmic_text::Color(color.to_linear().as_u32()))
551
}
552
553
/// Calculate the size of the text area for the given buffer.
554
fn buffer_dimensions(buffer: &Buffer) -> Vec2 {
555
let (width, height) = buffer
556
.layout_runs()
557
.map(|run| (run.line_w, run.line_height))
558
.reduce(|(w1, h1), (w2, h2)| (w1.max(w2), h1 + h2))
559
.unwrap_or((0.0, 0.0));
560
561
Vec2::new(width, height).ceil()
562
}
563
564
/// Discards stale data cached in `FontSystem`.
565
pub(crate) fn trim_cosmic_cache(mut font_system: ResMut<CosmicFontSystem>) {
566
// A trim age of 2 was found to reduce frame time variance vs age of 1 when tested with dynamic text.
567
// See https://github.com/bevyengine/bevy/pull/15037
568
//
569
// We assume only text updated frequently benefits from the shape cache (e.g. animated text, or
570
// text that is dynamically measured for UI).
571
font_system.0.shape_run_cache.trim(2);
572
}
573
574