bevy_text/
pipeline.rs

1use alloc::sync::Arc;
2
3use bevy_asset::{AssetId, Assets};
4use bevy_color::Color;
5use bevy_derive::{Deref, DerefMut};
6use bevy_ecs::{
7    component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource,
8    system::ResMut,
9};
10use bevy_image::prelude::*;
11use bevy_log::{once, warn};
12use bevy_math::{Rect, UVec2, Vec2};
13use bevy_platform::collections::HashMap;
14use bevy_reflect::{std_traits::ReflectDefault, Reflect};
15
16use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
17
18use 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)]
29pub struct CosmicFontSystem(pub cosmic_text::FontSystem);
30
31impl 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)]
46pub struct SwashCache(pub cosmic_text::SwashCache);
47
48impl 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)]
56pub 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)]
71pub 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
82impl 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 max_font_size: f32 = 0.;
102        let mut max_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            max_font_size = max_font_size.max(text_font.font_size);
135            max_line_height = max_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(max_font_size, max_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)]
451pub 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)]
467pub 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
476impl 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
494pub 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`].
530fn 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.
554fn 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`.
565pub(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}