use ab_glyph::{PxScale, ScaleFont}; use bevy_asset::{Assets, Handle, HandleId}; use bevy_ecs::component::Component; use bevy_ecs::system::Resource; use bevy_math::Vec2; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::HashMap; use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText}; use crate::{ error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation, }; #[derive(Default, Resource)] pub struct TextPipeline { brush: GlyphBrush, map_font_id: HashMap, } /// Render information for a corresponding [`Text`](crate::Text) component. /// /// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`]. #[derive(Component, Clone, Default, Debug)] pub struct TextLayoutInfo { pub glyphs: Vec, pub size: Vec2, } impl TextPipeline { pub fn get_or_insert_font_id(&mut self, handle: &Handle, font: &Font) -> FontId { let brush = &mut self.brush; *self .map_font_id .entry(handle.id()) .or_insert_with(|| brush.add_font(handle.clone(), font.font.clone())) } #[allow(clippy::too_many_arguments)] pub fn queue_text( &mut self, fonts: &Assets, sections: &[TextSection], scale_factor: f64, text_alignment: TextAlignment, linebreak_behavior: BreakLineOn, bounds: Vec2, font_atlas_set_storage: &mut Assets, texture_atlases: &mut Assets, textures: &mut Assets, text_settings: &TextSettings, font_atlas_warning: &mut FontAtlasWarning, y_axis_orientation: YAxisOrientation, ) -> Result { let mut scaled_fonts = Vec::with_capacity(sections.len()); let sections = sections .iter() .map(|section| { let font = fonts .get(§ion.style.font) .ok_or(TextError::NoSuchFont)?; let font_id = self.get_or_insert_font_id(§ion.style.font, font); let font_size = scale_value(section.style.font_size, scale_factor); scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size)); let section = SectionText { font_id, scale: PxScale::from(font_size), text: §ion.value, }; Ok(section) }) .collect::, _>>()?; let section_glyphs = self.brush .compute_glyphs(§ions, bounds, text_alignment, linebreak_behavior)?; if section_glyphs.is_empty() { return Ok(TextLayoutInfo::default()); } let mut min_x: f32 = std::f32::MAX; let mut min_y: f32 = std::f32::MAX; let mut max_x: f32 = std::f32::MIN; let mut max_y: f32 = std::f32::MIN; for sg in §ion_glyphs { let scaled_font = scaled_fonts[sg.section_index]; let glyph = &sg.glyph; // The fonts use a coordinate system increasing upwards so ascent is a positive value // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, // so we have to subtract from the baseline position to get the minimum and maximum values. min_x = min_x.min(glyph.position.x); min_y = min_y.min(glyph.position.y - scaled_font.ascent()); max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); max_y = max_y.max(glyph.position.y - scaled_font.descent()); } let size = Vec2::new(max_x - min_x, max_y - min_y); let glyphs = self.brush.process_glyphs( section_glyphs, §ions, font_atlas_set_storage, fonts, texture_atlases, textures, text_settings, font_atlas_warning, y_axis_orientation, )?; Ok(TextLayoutInfo { glyphs, size }) } pub fn create_text_measure( &mut self, fonts: &Assets, sections: &[TextSection], scale_factor: f64, text_alignment: TextAlignment, linebreak_behaviour: BreakLineOn, ) -> Result { let mut auto_fonts = Vec::with_capacity(sections.len()); let mut scaled_fonts = Vec::with_capacity(sections.len()); let sections = sections .iter() .enumerate() .map(|(i, section)| { let font = fonts .get(§ion.style.font) .ok_or(TextError::NoSuchFont)?; let font_size = scale_value(section.style.font_size, scale_factor); auto_fonts.push(font.font.clone()); let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size); scaled_fonts.push(px_scale_font); let section = TextMeasureSection { font_id: FontId(i), scale: PxScale::from(font_size), text: section.value.clone(), }; Ok(section) }) .collect::, _>>()?; Ok(TextMeasureInfo::new( auto_fonts, scaled_fonts, sections, text_alignment, linebreak_behaviour.into(), )) } } #[derive(Debug, Clone)] pub struct TextMeasureSection { pub text: String, pub scale: PxScale, pub font_id: FontId, } #[derive(Debug, Clone)] pub struct TextMeasureInfo { pub fonts: Vec, pub scaled_fonts: Vec>, pub sections: Vec, pub text_alignment: TextAlignment, pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, pub min_width_content_size: Vec2, pub max_width_content_size: Vec2, } impl TextMeasureInfo { fn new( fonts: Vec, scaled_fonts: Vec>, sections: Vec, text_alignment: TextAlignment, linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, ) -> Self { let mut info = Self { fonts, scaled_fonts, sections, text_alignment, linebreak_behaviour, min_width_content_size: Vec2::ZERO, max_width_content_size: Vec2::ZERO, }; let section_texts = info.prepare_section_texts(); let min = info.compute_size_from_section_texts(§ion_texts, Vec2::new(0.0, f32::INFINITY)); let max = info.compute_size_from_section_texts( §ion_texts, Vec2::new(f32::INFINITY, f32::INFINITY), ); info.min_width_content_size = min; info.max_width_content_size = max; info } fn prepare_section_texts(&self) -> Vec { self.sections .iter() .map(|section| SectionText { font_id: section.font_id, scale: section.scale, text: §ion.text, }) .collect::>() } fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 { let geom = SectionGeometry { bounds: (bounds.x, bounds.y), ..Default::default() }; let section_glyphs = glyph_brush_layout::Layout::default() .h_align(self.text_alignment.into()) .line_breaker(self.linebreak_behaviour) .calculate_glyphs(&self.fonts, &geom, sections); let mut min_x: f32 = std::f32::MAX; let mut min_y: f32 = std::f32::MAX; let mut max_x: f32 = std::f32::MIN; let mut max_y: f32 = std::f32::MIN; for sg in section_glyphs { let scaled_font = &self.scaled_fonts[sg.section_index]; let glyph = &sg.glyph; // The fonts use a coordinate system increasing upwards so ascent is a positive value // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, // so we have to subtract from the baseline position to get the minimum and maximum values. min_x = min_x.min(glyph.position.x); min_y = min_y.min(glyph.position.y - scaled_font.ascent()); max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); max_y = max_y.max(glyph.position.y - scaled_font.descent()); } Vec2::new(max_x - min_x, max_y - min_y) } pub fn compute_size(&self, bounds: Vec2) -> Vec2 { let sections = self.prepare_section_texts(); self.compute_size_from_section_texts(§ions, bounds) } }