diff --git a/Cargo.toml b/Cargo.toml index 4776c6fc6d..9f77d6d243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -282,6 +282,10 @@ path = "examples/ui/button.rs" name = "text" path = "examples/ui/text.rs" +[[example]] +name = "text_debug" +path = "examples/ui/text_debug.rs" + [[example]] name = "font_atlas_debug" path = "examples/ui/font_atlas_debug.rs" @@ -335,3 +339,4 @@ icon = "@mipmap/ic_launcher" build_targets = ["aarch64-linux-android", "armv7-linux-androideabi"] min_sdk_version = 16 target_sdk_version = 29 + diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index a77454144c..e222578241 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -17,6 +17,7 @@ keywords = ["bevy"] bevy_app = { path = "../bevy_app", version = "0.3.0" } bevy_asset = { path = "../bevy_asset", version = "0.3.0" } bevy_core = { path = "../bevy_core", version = "0.3.0" } +bevy_ecs = { path = "../bevy_ecs", version = "0.3.0" } bevy_math = { path = "../bevy_math", version = "0.3.0" } bevy_render = { path = "../bevy_render", version = "0.3.0" } bevy_sprite = { path = "../bevy_sprite", version = "0.3.0" } @@ -24,5 +25,7 @@ bevy_type_registry = { path = "../bevy_type_registry", version = "0.3.0" } bevy_utils = { path = "../bevy_utils", version = "0.3.0" } # other -ab_glyph = "0.2.5" anyhow = "1.0" +ab_glyph = "0.2.6" +glyph_brush_layout = "0.2.1" +thiserror = "1.0" diff --git a/crates/bevy_text/src/draw.rs b/crates/bevy_text/src/draw.rs index 4f08bd40d5..d06212c7b5 100644 --- a/crates/bevy_text/src/draw.rs +++ b/crates/bevy_text/src/draw.rs @@ -1,7 +1,4 @@ -use crate::{Font, FontAtlasSet}; -use ab_glyph::{Glyph, PxScale, ScaleFont}; -use bevy_asset::Assets; -use bevy_math::{Mat4, Vec2, Vec3}; +use bevy_math::{Mat4, Vec3}; use bevy_render::{ color::Color, draw::{Draw, DrawContext, DrawError, Drawable}, @@ -13,12 +10,31 @@ use bevy_render::{ RenderResourceId, }, }; -use bevy_sprite::{TextureAtlas, TextureAtlasSprite}; +use bevy_sprite::TextureAtlasSprite; +use glyph_brush_layout::{HorizontalAlign, VerticalAlign}; + +use crate::PositionedGlyph; + +#[derive(Debug, Clone, Copy)] +pub struct TextAlignment { + pub vertical: VerticalAlign, + pub horizontal: HorizontalAlign, +} + +impl Default for TextAlignment { + fn default() -> Self { + TextAlignment { + vertical: VerticalAlign::Top, + horizontal: HorizontalAlign::Left, + } + } +} #[derive(Clone, Debug)] pub struct TextStyle { pub font_size: f32, pub color: Color, + pub alignment: TextAlignment, } impl Default for TextStyle { @@ -26,20 +42,17 @@ impl Default for TextStyle { Self { color: Color::WHITE, font_size: 12.0, + alignment: TextAlignment::default(), } } } pub struct DrawableText<'a> { - pub font: &'a Font, - pub font_atlas_set: &'a FontAtlasSet, - pub texture_atlases: &'a Assets, pub render_resource_bindings: &'a mut RenderResourceBindings, pub asset_render_resource_bindings: &'a mut AssetRenderResourceBindings, pub position: Vec3, - pub container_size: Vec2, pub style: &'a TextStyle, - pub text: &'a str, + pub text_glyphs: &'a Vec, pub msaa: &'a Msaa, pub font_quad_vertex_descriptor: &'a VertexBufferDescriptor, } @@ -81,80 +94,37 @@ impl<'a> Drawable for DrawableText<'a> { // set global bindings context.set_bind_groups_from_bindings(draw, &mut [self.render_resource_bindings])?; - // NOTE: this uses ab_glyph apis directly. it _might_ be a good idea to add our own layer on top - let font = &self.font.font; - let scale = PxScale::from(self.style.font_size); - let scaled_font = ab_glyph::Font::as_scaled(&font, scale); - let mut caret = self.position; - let mut last_glyph: Option = None; + for tv in self.text_glyphs { + let atlas_render_resource_bindings = self + .asset_render_resource_bindings + .get_mut(&tv.atlas_info.texture_atlas) + .unwrap(); + context.set_bind_groups_from_bindings(draw, &mut [atlas_render_resource_bindings])?; - // set local per-character bindings - for character in self.text.chars() { - if character.is_control() { - if character == '\n' { - caret.set_x(self.position.x()); - // TODO: Necessary to also calculate scaled_font.line_gap() in here? - caret.set_y(caret.y() - scaled_font.height()); - } - continue; - } + let sprite = TextureAtlasSprite { + index: tv.atlas_info.glyph_index, + color: self.style.color, + }; - let glyph = scaled_font.scaled_glyph(character); - if let Some(last_glyph) = last_glyph.take() { - caret.set_x(caret.x() + scaled_font.kern(last_glyph.id, glyph.id)); - } - if let Some(glyph_atlas_info) = self - .font_atlas_set - .get_glyph_atlas_info(self.style.font_size, character) - { - if let Some(outlined) = scaled_font.outline_glyph(glyph.clone()) { - let texture_atlas = self - .texture_atlases - .get(&glyph_atlas_info.texture_atlas) - .unwrap(); - let glyph_rect = texture_atlas.textures[glyph_atlas_info.char_index as usize]; - let glyph_width = glyph_rect.width(); - let glyph_height = glyph_rect.height(); - let atlas_render_resource_bindings = self - .asset_render_resource_bindings - .get_mut(&glyph_atlas_info.texture_atlas) - .unwrap(); - context.set_bind_groups_from_bindings( - draw, - &mut [atlas_render_resource_bindings], - )?; + let transform = Mat4::from_translation(self.position + tv.position.extend(0.)); - let bounds = outlined.px_bounds(); - let x = bounds.min.x + glyph_width / 2.0; - // the 0.5 accounts for odd-numbered heights (bump up by 1 pixel) - let y = -bounds.max.y + glyph_height / 2.0 - scaled_font.descent() + 0.5; - let transform = Mat4::from_translation(caret + Vec3::new(x, y, 0.0)); - let sprite = TextureAtlasSprite { - index: glyph_atlas_info.char_index, - color: self.style.color, - }; - - let transform_buffer = context - .shared_buffers - .get_buffer(&transform, BufferUsage::UNIFORM) - .unwrap(); - let sprite_buffer = context - .shared_buffers - .get_buffer(&sprite, BufferUsage::UNIFORM) - .unwrap(); - let sprite_bind_group = BindGroup::build() - .add_binding(0, transform_buffer) - .add_binding(1, sprite_buffer) - .finish(); - - context.create_bind_group_resource(2, &sprite_bind_group)?; - draw.set_bind_group(2, &sprite_bind_group); - draw.draw_indexed(indices.clone(), 0, 0..1); - } - } - caret.set_x(caret.x() + scaled_font.h_advance(glyph.id)); - last_glyph = Some(glyph); + let transform_buffer = context + .shared_buffers + .get_buffer(&transform, BufferUsage::UNIFORM) + .unwrap(); + let sprite_buffer = context + .shared_buffers + .get_buffer(&sprite, BufferUsage::UNIFORM) + .unwrap(); + let sprite_bind_group = BindGroup::build() + .add_binding(0, transform_buffer) + .add_binding(1, sprite_buffer) + .finish(); + context.create_bind_group_resource(2, &sprite_bind_group)?; + draw.set_bind_group(2, &sprite_bind_group); + draw.draw_indexed(indices.clone(), 0, 0..1); } + Ok(()) } } diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs new file mode 100644 index 0000000000..10967f0e32 --- /dev/null +++ b/crates/bevy_text/src/error.rs @@ -0,0 +1,10 @@ +use ab_glyph::GlyphId; +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq, Error)] +pub enum TextError { + #[error("Font not found")] + NoSuchFont, + #[error("Failed to add glyph to newly-created atlas {0:?}")] + FailedToAddGlyph(GlyphId), +} diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index e89cd698ad..f701bff87c 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,4 +1,4 @@ -use ab_glyph::{FontVec, Glyph, InvalidFont, OutlinedGlyph, Point, PxScale, ScaleFont}; +use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph}; use bevy_math::Vec2; use bevy_render::{ color::Color, @@ -9,15 +9,13 @@ use bevy_type_registry::TypeUuid; #[derive(Debug, TypeUuid)] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] pub struct Font { - pub font: FontVec, + pub font: FontArc, } -unsafe impl Send for Font {} -unsafe impl Sync for Font {} - impl Font { pub fn try_from_bytes(font_data: Vec) -> Result { let font = FontVec::try_from_vec(font_data)?; + let font = FontArc::new(font); Ok(Font { font }) } @@ -54,109 +52,4 @@ impl Font { TextureFormat::Rgba8UnormSrgb, ) } - - // adapted from ab_glyph example: https://github.com/alexheretic/ab-glyph/blob/master/dev/examples/image.rs - pub fn render_text( - &self, - text: &str, - color: Color, - font_size: f32, - width: usize, - height: usize, - ) -> Texture { - let scale = PxScale::from(font_size); - - let scaled_font = ab_glyph::Font::as_scaled(&self.font, scale); - - let mut glyphs = Vec::new(); - layout_paragraph( - scaled_font, - ab_glyph::point(0.0, 0.0), - width as f32, - text, - &mut glyphs, - ); - - let color_u8 = [ - (color.r() * 255.0) as u8, - (color.g() * 255.0) as u8, - (color.b() * 255.0) as u8, - ]; - - // TODO: this offset is a bit hackey - let mut alpha = vec![0.0; width * height]; - for glyph in glyphs { - if let Some(outlined) = scaled_font.outline_glyph(glyph) { - let bounds = outlined.px_bounds(); - // Draw the glyph into the image per-pixel by using the draw closure - outlined.draw(|x, y, v| { - // Offset the position by the glyph bounding box - // Turn the coverage into an alpha value (blended with any previous) - let offset_x = x as usize + bounds.min.x as usize; - let offset_y = y as usize + bounds.min.y as usize; - if offset_x >= width || offset_y >= height { - return; - } - alpha[offset_y * width + offset_x] = v; - }); - } - } - - Texture::new( - Vec2::new(width as f32, height as f32), - alpha - .iter() - .map(|a| { - vec![ - color_u8[0], - color_u8[1], - color_u8[2], - (color.a() * a * 255.0) as u8, - ] - }) - .flatten() - .collect::>(), - TextureFormat::Rgba8UnormSrgb, - ) - } -} - -fn layout_paragraph( - font: SF, - position: Point, - max_width: f32, - text: &str, - target: &mut Vec, -) where - F: ab_glyph::Font, - SF: ScaleFont, -{ - let v_advance = font.height() + font.line_gap(); - let mut caret = position + ab_glyph::point(0.0, font.ascent()); - let mut last_glyph: Option = None; - for c in text.chars() { - if c.is_control() { - if c == '\n' { - caret = ab_glyph::point(position.x, caret.y + v_advance); - last_glyph = None; - } - continue; - } - let mut glyph = font.scaled_glyph(c); - if let Some(previous) = last_glyph.take() { - caret.x += font.kern(previous.id, glyph.id); - } - glyph.position = caret; - - last_glyph = Some(glyph.clone()); - caret.x += font.h_advance(glyph.id); - - if !c.is_whitespace() && caret.x > position.x + max_width { - caret = ab_glyph::point(position.x, caret.y + v_advance); - glyph.position = caret; - last_glyph = None; - } - - target.push(glyph); - } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 9c39fcf89e..bd6d7c6a16 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,3 +1,4 @@ +use ab_glyph::GlyphId; use bevy_asset::{Assets, Handle}; use bevy_math::Vec2; use bevy_render::texture::{Texture, TextureFormat}; @@ -6,7 +7,7 @@ use bevy_utils::HashMap; pub struct FontAtlas { pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - pub glyph_to_index: HashMap, + pub glyph_to_atlas_index: HashMap, pub texture_atlas: Handle, } @@ -24,20 +25,24 @@ impl FontAtlas { let texture_atlas = TextureAtlas::new_empty(atlas_texture, size); Self { texture_atlas: texture_atlases.add(texture_atlas), - glyph_to_index: HashMap::default(), + glyph_to_atlas_index: HashMap::default(), dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1), } } - pub fn get_char_index(&self, character: char) -> Option { - self.glyph_to_index.get(&character).cloned() + pub fn get_glyph_index(&self, glyph_id: GlyphId) -> Option { + self.glyph_to_atlas_index.get(&glyph_id).copied() } - pub fn add_char( + pub fn has_glyph(&self, glyph_id: GlyphId) -> bool { + self.glyph_to_atlas_index.contains_key(&glyph_id) + } + + pub fn add_glyph( &mut self, textures: &mut Assets, texture_atlases: &mut Assets, - character: char, + glyph_id: GlyphId, texture: &Texture, ) -> bool { let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); @@ -45,7 +50,7 @@ impl FontAtlas { self.dynamic_texture_atlas_builder .add_texture(texture_atlas, textures, texture) { - self.glyph_to_index.insert(character, index); + self.glyph_to_atlas_index.insert(glyph_id, index); true } else { false diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index cc6f30b452..decf5dd524 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,62 +1,57 @@ -use crate::{Font, FontAtlas}; -use ab_glyph::{Glyph, ScaleFont}; +use crate::{error::TextError, Font, FontAtlas}; +use ab_glyph::{GlyphId, OutlinedGlyph}; use bevy_asset::{Assets, Handle}; use bevy_core::FloatOrd; use bevy_math::Vec2; use bevy_render::texture::Texture; use bevy_sprite::TextureAtlas; use bevy_type_registry::TypeUuid; -use bevy_utils::HashMap; +use bevy_utils::{AHashExt, HashMap}; -// work around rust's f32 order/hash limitations type FontSizeKey = FloatOrd; -#[derive(Default, TypeUuid)] +#[derive(TypeUuid)] #[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] pub struct FontAtlasSet { - font: Handle, font_atlases: HashMap>, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GlyphAtlasInfo { pub texture_atlas: Handle, - pub char_index: u32, + pub glyph_index: u32, +} + +impl Default for FontAtlasSet { + fn default() -> Self { + FontAtlasSet { + font_atlases: HashMap::with_capacity(1), + } + } } impl FontAtlasSet { - pub fn new(font: Handle) -> Self { - Self { - font, - font_atlases: HashMap::default(), - } - } - pub fn iter(&self) -> impl Iterator)> { self.font_atlases.iter() } - pub fn has_char(&self, character: char, font_size: f32) -> bool { + pub fn has_glyph(&self, glyph_id: GlyphId, font_size: f32) -> bool { self.font_atlases .get(&FloatOrd(font_size)) .map_or(false, |font_atlas| { - font_atlas - .iter() - .any(|atlas| atlas.get_char_index(character).is_some()) + font_atlas.iter().any(|atlas| atlas.has_glyph(glyph_id)) }) } - pub fn add_glyphs_to_atlas( + pub fn add_glyph_to_atlas( &mut self, - fonts: &Assets, texture_atlases: &mut Assets, textures: &mut Assets, - font_size: f32, - text: &str, - ) -> Option { - let mut width = 0.0; - let font = fonts.get(&self.font)?; - let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size); + outlined_glyph: OutlinedGlyph, + ) -> Result { + let glyph = outlined_glyph.glyph(); + let glyph_id = glyph.id; + let font_size = glyph.scale.y; let font_atlases = self .font_atlases .entry(FloatOrd(font_size)) @@ -67,63 +62,47 @@ impl FontAtlasSet { Vec2::new(512.0, 512.0), )] }); - - let mut last_glyph: Option = None; - for character in text.chars() { - if character.is_control() { - continue; + let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph); + let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { + atlas.add_glyph(textures, texture_atlases, glyph_id, &glyph_texture) + }; + if !font_atlases.iter_mut().any(add_char_to_font_atlas) { + font_atlases.push(FontAtlas::new( + textures, + texture_atlases, + Vec2::new(512.0, 512.0), + )); + if !font_atlases.last_mut().unwrap().add_glyph( + textures, + texture_atlases, + glyph_id, + &glyph_texture, + ) { + return Err(TextError::FailedToAddGlyph(glyph_id)); } - let glyph = scaled_font.scaled_glyph(character); - if let Some(last_glyph) = last_glyph.take() { - width += scaled_font.kern(last_glyph.id, glyph.id); - } - if !font_atlases - .iter() - .any(|atlas| atlas.get_char_index(character).is_some()) - { - if let Some(outlined_glyph) = scaled_font.outline_glyph(glyph.clone()) { - let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph); - let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { - atlas.add_char(textures, texture_atlases, character, &glyph_texture) - }; - if !font_atlases.iter_mut().any(add_char_to_font_atlas) { - font_atlases.push(FontAtlas::new( - textures, - texture_atlases, - Vec2::new(512.0, 512.0), - )); - if !font_atlases.last_mut().unwrap().add_char( - textures, - texture_atlases, - character, - &glyph_texture, - ) { - panic!("could not add character to newly created FontAtlas"); - } - } - } - } - width += scaled_font.h_advance(glyph.id); - last_glyph = Some(glyph); } - Some(width) + Ok(self.get_glyph_atlas_info(font_size, glyph_id).unwrap()) } - pub fn get_glyph_atlas_info(&self, font_size: f32, character: char) -> Option { + pub fn get_glyph_atlas_info( + &self, + font_size: f32, + glyph_id: GlyphId, + ) -> Option { self.font_atlases .get(&FloatOrd(font_size)) - .and_then(|font_atlas| { - font_atlas + .and_then(|font_atlases| { + font_atlases .iter() .find_map(|atlas| { atlas - .get_char_index(character) - .map(|char_index| (char_index, atlas.texture_atlas.clone_weak())) + .get_glyph_index(glyph_id) + .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) }) - .map(|(char_index, texture_atlas)| GlyphAtlasInfo { + .map(|(glyph_index, texture_atlas)| GlyphAtlasInfo { texture_atlas, - char_index, + glyph_index, }) }) } diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs new file mode 100644 index 0000000000..20d0537355 --- /dev/null +++ b/crates/bevy_text/src/glyph_brush.rs @@ -0,0 +1,123 @@ +use ab_glyph::{Font as _, FontArc, ScaleFont as _}; +use bevy_asset::{Assets, Handle}; +use bevy_math::{Size, Vec2}; +use bevy_render::prelude::Texture; +use bevy_sprite::TextureAtlas; +use glyph_brush_layout::{ + FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, ToSectionText, +}; + +use crate::{error::TextError, Font, FontAtlasSet, GlyphAtlasInfo, TextAlignment}; + +pub struct GlyphBrush { + fonts: Vec, + handles: Vec>, + latest_font_id: FontId, +} + +impl Default for GlyphBrush { + fn default() -> Self { + GlyphBrush { + fonts: Vec::new(), + handles: Vec::new(), + latest_font_id: FontId(0), + } + } +} + +impl GlyphBrush { + pub fn compute_glyphs( + &self, + sections: &[S], + bounds: Size, + text_alignment: TextAlignment, + ) -> Result, TextError> { + let geom = SectionGeometry { + bounds: (bounds.width, bounds.height), + ..Default::default() + }; + let section_glyphs = Layout::default() + .h_align(text_alignment.horizontal) + .v_align(text_alignment.vertical) + .calculate_glyphs(&self.fonts, &geom, sections); + Ok(section_glyphs) + } + + pub fn process_glyphs( + &self, + glyphs: Vec, + font_atlas_set_storage: &mut Assets, + fonts: &Assets, + texture_atlases: &mut Assets, + textures: &mut Assets, + ) -> Result, TextError> { + if glyphs.is_empty() { + return Ok(Vec::new()); + } + + let first_glyph = glyphs.first().expect("Must have at least one glyph"); + let font_id = first_glyph.font_id.0; + let handle = &self.handles[font_id]; + let font = fonts.get(handle).ok_or(TextError::NoSuchFont)?; + let font_size = first_glyph.glyph.scale.y; + let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size); + let mut max_y = std::f32::MIN; + let mut min_x = std::f32::MAX; + for section_glyph in glyphs.iter() { + let glyph = §ion_glyph.glyph; + max_y = max_y.max(glyph.position.y - scaled_font.descent()); + min_x = min_x.min(glyph.position.x); + } + max_y = max_y.floor(); + min_x = min_x.floor(); + + let mut positioned_glyphs = Vec::new(); + for sg in glyphs { + let glyph_id = sg.glyph.id; + if let Some(outlined_glyph) = font.font.outline_glyph(sg.glyph) { + let bounds = outlined_glyph.px_bounds(); + let handle_font_atlas: Handle = handle.as_weak(); + let font_atlas_set = font_atlas_set_storage + .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); + + let atlas_info = font_atlas_set + .get_glyph_atlas_info(font_size, glyph_id) + .map(Ok) + .unwrap_or_else(|| { + font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph) + })?; + + let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); + let glyph_rect = texture_atlas.textures[atlas_info.glyph_index as usize]; + let glyph_width = glyph_rect.width(); + let glyph_height = glyph_rect.height(); + + let x = bounds.min.x + glyph_width / 2.0 - min_x; + // the 0.5 accounts for odd-numbered heights (bump up by 1 pixel) + // max_y = text block height, and up is negative (whereas for transform, up is positive) + let y = max_y - bounds.max.y + glyph_height / 2.0 + 0.5; + let position = Vec2::new(x, y); + + positioned_glyphs.push(PositionedGlyph { + position, + atlas_info, + }); + } + } + Ok(positioned_glyphs) + } + + pub fn add_font(&mut self, handle: Handle, font: FontArc) -> FontId { + self.fonts.push(font); + self.handles.push(handle); + let font_id = self.latest_font_id; + self.latest_font_id = FontId(font_id.0 + 1); + font_id + } +} + +#[derive(Debug, Clone)] +pub struct PositionedGlyph { + pub position: Vec2, + pub atlas_info: GlyphAtlasInfo, +} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 44d4d5c284..33a3b3e8cc 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -1,21 +1,31 @@ mod draw; +mod error; mod font; mod font_atlas; mod font_atlas_set; mod font_loader; +mod glyph_brush; +mod pipeline; pub use draw::*; +pub use error::*; pub use font::*; pub use font_atlas::*; pub use font_atlas_set::*; pub use font_loader::*; +pub use glyph_brush::*; +pub use pipeline::*; pub mod prelude { - pub use crate::{Font, TextStyle}; + pub use crate::{Font, TextAlignment, TextError, TextStyle}; + pub use glyph_brush_layout::{HorizontalAlign, VerticalAlign}; } use bevy_app::prelude::*; use bevy_asset::AddAsset; +use bevy_ecs::Entity; + +pub type DefaultTextPipeline = TextPipeline; #[derive(Default)] pub struct TextPlugin; @@ -24,6 +34,7 @@ impl Plugin for TextPlugin { fn build(&self, app: &mut AppBuilder) { app.add_asset::() .add_asset::() - .init_asset_loader::(); + .init_asset_loader::() + .add_resource(DefaultTextPipeline::default()); } } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs new file mode 100644 index 0000000000..5fb980ab0c --- /dev/null +++ b/crates/bevy_text/src/pipeline.rs @@ -0,0 +1,117 @@ +use std::hash::Hash; + +use ab_glyph::{PxScale, ScaleFont}; +use bevy_asset::{Assets, Handle, HandleId}; +use bevy_math::Size; +use bevy_render::prelude::Texture; +use bevy_sprite::TextureAtlas; +use bevy_utils::HashMap; + +use glyph_brush_layout::{FontId, SectionText}; + +use crate::{ + error::TextError, glyph_brush::GlyphBrush, Font, FontAtlasSet, PositionedGlyph, TextAlignment, +}; + +pub struct TextPipeline { + brush: GlyphBrush, + glyph_map: HashMap, + map_font_id: HashMap, +} + +impl Default for TextPipeline { + fn default() -> Self { + TextPipeline { + brush: GlyphBrush::default(), + glyph_map: Default::default(), + map_font_id: Default::default(), + } + } +} + +pub struct TextLayoutInfo { + pub glyphs: Vec, + pub size: Size, +} + +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())) + } + + pub fn get_glyphs(&self, id: &ID) -> Option<&TextLayoutInfo> { + self.glyph_map.get(id) + } + + #[allow(clippy::too_many_arguments)] + pub fn queue_text( + &mut self, + id: ID, + font_handle: Handle, + fonts: &Assets, + text: &str, + font_size: f32, + text_alignment: TextAlignment, + bounds: Size, + font_atlas_set_storage: &mut Assets, + texture_atlases: &mut Assets, + textures: &mut Assets, + ) -> Result<(), TextError> { + let font = fonts.get(font_handle.id).ok_or(TextError::NoSuchFont)?; + let font_id = self.get_or_insert_font_id(font_handle, font); + + let section = SectionText { + font_id, + scale: PxScale::from(font_size), + text, + }; + + let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size); + + let section_glyphs = self + .brush + .compute_glyphs(&[section], bounds, text_alignment)?; + + if section_glyphs.is_empty() { + self.glyph_map.insert( + id, + TextLayoutInfo { + glyphs: Vec::new(), + size: Size::new(0., 0.), + }, + ); + return Ok(()); + } + + 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 section_glyph in section_glyphs.iter() { + let glyph = §ion_glyph.glyph; + 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 = Size::new(max_x - min_x, max_y - min_y); + + let glyphs = self.brush.process_glyphs( + section_glyphs, + font_atlas_set_storage, + fonts, + texture_atlases, + textures, + )?; + + self.glyph_map.insert(id, TextLayoutInfo { glyphs, size }); + + Ok(()) + } +} diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index ef72b3b614..c2dc9113c1 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -1,6 +1,6 @@ -use crate::{CalculatedSize, Node}; +use crate::{CalculatedSize, Node, Style, Val}; use bevy_asset::{Assets, Handle}; -use bevy_ecs::{Changed, Entity, Local, Query, QuerySet, Res, ResMut}; +use bevy_ecs::{Changed, Entity, Local, Or, Query, QuerySet, Res, ResMut}; use bevy_math::Size; use bevy_render::{ draw::{Draw, DrawContext, Drawable}, @@ -10,7 +10,7 @@ use bevy_render::{ texture::Texture, }; use bevy_sprite::{TextureAtlas, QUAD_HANDLE}; -use bevy_text::{DrawableText, Font, FontAtlasSet, TextStyle}; +use bevy_text::{DefaultTextPipeline, DrawableText, Font, FontAtlasSet, TextError, TextStyle}; use bevy_transform::prelude::GlobalTransform; #[derive(Debug, Default)] @@ -25,101 +25,148 @@ pub struct Text { pub style: TextStyle, } +/// Defines how min_size, size, and max_size affects the bounds of a text +/// block. +pub fn text_constraint(min_size: Val, size: Val, max_size: Val) -> f32 { + // Needs support for percentages + match (min_size, size, max_size) { + (_, _, Val::Px(max)) => max, + (Val::Px(min), _, _) => min, + (Val::Undefined, Val::Px(size), Val::Undefined) => size, + (Val::Auto, Val::Px(size), Val::Auto) => size, + _ => f32::MAX, + } +} + +/// Computes the size of a text block and updates the TextGlyphs with the +/// new computed glyphs from the layout pub fn text_system( mut queued_text: Local, mut textures: ResMut>, fonts: Res>, - mut font_atlas_sets: ResMut>, mut texture_atlases: ResMut>, - mut queries: QuerySet<( - Query<(Entity, &Text, &mut CalculatedSize), Changed>, - Query<(&Text, &mut CalculatedSize)>, + mut font_atlas_set_storage: ResMut>, + mut text_pipeline: ResMut, + mut text_queries: QuerySet<( + Query, Changed