Use glyph_brush_layout and add text alignment support (#765)

Use glyph_brush_layout and add text alignment support

Co-authored-by: Olivier Pinon <op@impero.com>
Co-authored-by: tigregalis <anak.harimau@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Olivier Pinon 2020-11-13 01:21:48 +01:00 committed by GitHub
parent 1eff53462a
commit 465c3d4f7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 641 additions and 338 deletions

View file

@ -282,6 +282,10 @@ path = "examples/ui/button.rs"
name = "text" name = "text"
path = "examples/ui/text.rs" path = "examples/ui/text.rs"
[[example]]
name = "text_debug"
path = "examples/ui/text_debug.rs"
[[example]] [[example]]
name = "font_atlas_debug" name = "font_atlas_debug"
path = "examples/ui/font_atlas_debug.rs" path = "examples/ui/font_atlas_debug.rs"
@ -335,3 +339,4 @@ icon = "@mipmap/ic_launcher"
build_targets = ["aarch64-linux-android", "armv7-linux-androideabi"] build_targets = ["aarch64-linux-android", "armv7-linux-androideabi"]
min_sdk_version = 16 min_sdk_version = 16
target_sdk_version = 29 target_sdk_version = 29

View file

@ -17,6 +17,7 @@ keywords = ["bevy"]
bevy_app = { path = "../bevy_app", version = "0.3.0" } bevy_app = { path = "../bevy_app", version = "0.3.0" }
bevy_asset = { path = "../bevy_asset", version = "0.3.0" } bevy_asset = { path = "../bevy_asset", version = "0.3.0" }
bevy_core = { path = "../bevy_core", 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_math = { path = "../bevy_math", version = "0.3.0" }
bevy_render = { path = "../bevy_render", version = "0.3.0" } bevy_render = { path = "../bevy_render", version = "0.3.0" }
bevy_sprite = { path = "../bevy_sprite", 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" } bevy_utils = { path = "../bevy_utils", version = "0.3.0" }
# other # other
ab_glyph = "0.2.5"
anyhow = "1.0" anyhow = "1.0"
ab_glyph = "0.2.6"
glyph_brush_layout = "0.2.1"
thiserror = "1.0"

View file

@ -1,7 +1,4 @@
use crate::{Font, FontAtlasSet}; use bevy_math::{Mat4, Vec3};
use ab_glyph::{Glyph, PxScale, ScaleFont};
use bevy_asset::Assets;
use bevy_math::{Mat4, Vec2, Vec3};
use bevy_render::{ use bevy_render::{
color::Color, color::Color,
draw::{Draw, DrawContext, DrawError, Drawable}, draw::{Draw, DrawContext, DrawError, Drawable},
@ -13,12 +10,31 @@ use bevy_render::{
RenderResourceId, 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)] #[derive(Clone, Debug)]
pub struct TextStyle { pub struct TextStyle {
pub font_size: f32, pub font_size: f32,
pub color: Color, pub color: Color,
pub alignment: TextAlignment,
} }
impl Default for TextStyle { impl Default for TextStyle {
@ -26,20 +42,17 @@ impl Default for TextStyle {
Self { Self {
color: Color::WHITE, color: Color::WHITE,
font_size: 12.0, font_size: 12.0,
alignment: TextAlignment::default(),
} }
} }
} }
pub struct DrawableText<'a> { pub struct DrawableText<'a> {
pub font: &'a Font,
pub font_atlas_set: &'a FontAtlasSet,
pub texture_atlases: &'a Assets<TextureAtlas>,
pub render_resource_bindings: &'a mut RenderResourceBindings, pub render_resource_bindings: &'a mut RenderResourceBindings,
pub asset_render_resource_bindings: &'a mut AssetRenderResourceBindings, pub asset_render_resource_bindings: &'a mut AssetRenderResourceBindings,
pub position: Vec3, pub position: Vec3,
pub container_size: Vec2,
pub style: &'a TextStyle, pub style: &'a TextStyle,
pub text: &'a str, pub text_glyphs: &'a Vec<PositionedGlyph>,
pub msaa: &'a Msaa, pub msaa: &'a Msaa,
pub font_quad_vertex_descriptor: &'a VertexBufferDescriptor, pub font_quad_vertex_descriptor: &'a VertexBufferDescriptor,
} }
@ -81,80 +94,37 @@ impl<'a> Drawable for DrawableText<'a> {
// set global bindings // set global bindings
context.set_bind_groups_from_bindings(draw, &mut [self.render_resource_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 for tv in self.text_glyphs {
let font = &self.font.font; let atlas_render_resource_bindings = self
let scale = PxScale::from(self.style.font_size); .asset_render_resource_bindings
let scaled_font = ab_glyph::Font::as_scaled(&font, scale); .get_mut(&tv.atlas_info.texture_atlas)
let mut caret = self.position; .unwrap();
let mut last_glyph: Option<Glyph> = None; context.set_bind_groups_from_bindings(draw, &mut [atlas_render_resource_bindings])?;
// set local per-character bindings let sprite = TextureAtlasSprite {
for character in self.text.chars() { index: tv.atlas_info.glyph_index,
if character.is_control() { color: self.style.color,
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 glyph = scaled_font.scaled_glyph(character); let transform = Mat4::from_translation(self.position + tv.position.extend(0.));
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 bounds = outlined.px_bounds(); let transform_buffer = context
let x = bounds.min.x + glyph_width / 2.0; .shared_buffers
// the 0.5 accounts for odd-numbered heights (bump up by 1 pixel) .get_buffer(&transform, BufferUsage::UNIFORM)
let y = -bounds.max.y + glyph_height / 2.0 - scaled_font.descent() + 0.5; .unwrap();
let transform = Mat4::from_translation(caret + Vec3::new(x, y, 0.0)); let sprite_buffer = context
let sprite = TextureAtlasSprite { .shared_buffers
index: glyph_atlas_info.char_index, .get_buffer(&sprite, BufferUsage::UNIFORM)
color: self.style.color, .unwrap();
}; let sprite_bind_group = BindGroup::build()
.add_binding(0, transform_buffer)
let transform_buffer = context .add_binding(1, sprite_buffer)
.shared_buffers .finish();
.get_buffer(&transform, BufferUsage::UNIFORM) context.create_bind_group_resource(2, &sprite_bind_group)?;
.unwrap(); draw.set_bind_group(2, &sprite_bind_group);
let sprite_buffer = context draw.draw_indexed(indices.clone(), 0, 0..1);
.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);
} }
Ok(()) Ok(())
} }
} }

View file

@ -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),
}

View file

@ -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_math::Vec2;
use bevy_render::{ use bevy_render::{
color::Color, color::Color,
@ -9,15 +9,13 @@ use bevy_type_registry::TypeUuid;
#[derive(Debug, TypeUuid)] #[derive(Debug, TypeUuid)]
#[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"]
pub struct Font { pub struct Font {
pub font: FontVec, pub font: FontArc,
} }
unsafe impl Send for Font {}
unsafe impl Sync for Font {}
impl Font { impl Font {
pub fn try_from_bytes(font_data: Vec<u8>) -> Result<Self, InvalidFont> { pub fn try_from_bytes(font_data: Vec<u8>) -> Result<Self, InvalidFont> {
let font = FontVec::try_from_vec(font_data)?; let font = FontVec::try_from_vec(font_data)?;
let font = FontArc::new(font);
Ok(Font { font }) Ok(Font { font })
} }
@ -54,109 +52,4 @@ impl Font {
TextureFormat::Rgba8UnormSrgb, 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::<Vec<u8>>(),
TextureFormat::Rgba8UnormSrgb,
)
}
}
fn layout_paragraph<F, SF>(
font: SF,
position: Point,
max_width: f32,
text: &str,
target: &mut Vec<Glyph>,
) where
F: ab_glyph::Font,
SF: ScaleFont<F>,
{
let v_advance = font.height() + font.line_gap();
let mut caret = position + ab_glyph::point(0.0, font.ascent());
let mut last_glyph: Option<Glyph> = 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);
}
} }

View file

@ -1,3 +1,4 @@
use ab_glyph::GlyphId;
use bevy_asset::{Assets, Handle}; use bevy_asset::{Assets, Handle};
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_render::texture::{Texture, TextureFormat}; use bevy_render::texture::{Texture, TextureFormat};
@ -6,7 +7,7 @@ use bevy_utils::HashMap;
pub struct FontAtlas { pub struct FontAtlas {
pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
pub glyph_to_index: HashMap<char, u32>, pub glyph_to_atlas_index: HashMap<GlyphId, u32>,
pub texture_atlas: Handle<TextureAtlas>, pub texture_atlas: Handle<TextureAtlas>,
} }
@ -24,20 +25,24 @@ impl FontAtlas {
let texture_atlas = TextureAtlas::new_empty(atlas_texture, size); let texture_atlas = TextureAtlas::new_empty(atlas_texture, size);
Self { Self {
texture_atlas: texture_atlases.add(texture_atlas), 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), dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1),
} }
} }
pub fn get_char_index(&self, character: char) -> Option<u32> { pub fn get_glyph_index(&self, glyph_id: GlyphId) -> Option<u32> {
self.glyph_to_index.get(&character).cloned() 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, &mut self,
textures: &mut Assets<Texture>, textures: &mut Assets<Texture>,
texture_atlases: &mut Assets<TextureAtlas>, texture_atlases: &mut Assets<TextureAtlas>,
character: char, glyph_id: GlyphId,
texture: &Texture, texture: &Texture,
) -> bool { ) -> bool {
let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap();
@ -45,7 +50,7 @@ impl FontAtlas {
self.dynamic_texture_atlas_builder self.dynamic_texture_atlas_builder
.add_texture(texture_atlas, textures, texture) .add_texture(texture_atlas, textures, texture)
{ {
self.glyph_to_index.insert(character, index); self.glyph_to_atlas_index.insert(glyph_id, index);
true true
} else { } else {
false false

View file

@ -1,62 +1,57 @@
use crate::{Font, FontAtlas}; use crate::{error::TextError, Font, FontAtlas};
use ab_glyph::{Glyph, ScaleFont}; use ab_glyph::{GlyphId, OutlinedGlyph};
use bevy_asset::{Assets, Handle}; use bevy_asset::{Assets, Handle};
use bevy_core::FloatOrd; use bevy_core::FloatOrd;
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_render::texture::Texture; use bevy_render::texture::Texture;
use bevy_sprite::TextureAtlas; use bevy_sprite::TextureAtlas;
use bevy_type_registry::TypeUuid; 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; type FontSizeKey = FloatOrd;
#[derive(Default, TypeUuid)] #[derive(TypeUuid)]
#[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] #[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"]
pub struct FontAtlasSet { pub struct FontAtlasSet {
font: Handle<Font>,
font_atlases: HashMap<FontSizeKey, Vec<FontAtlas>>, font_atlases: HashMap<FontSizeKey, Vec<FontAtlas>>,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct GlyphAtlasInfo { pub struct GlyphAtlasInfo {
pub texture_atlas: Handle<TextureAtlas>, pub texture_atlas: Handle<TextureAtlas>,
pub char_index: u32, pub glyph_index: u32,
}
impl Default for FontAtlasSet {
fn default() -> Self {
FontAtlasSet {
font_atlases: HashMap::with_capacity(1),
}
}
} }
impl FontAtlasSet { impl FontAtlasSet {
pub fn new(font: Handle<Font>) -> Self {
Self {
font,
font_atlases: HashMap::default(),
}
}
pub fn iter(&self) -> impl Iterator<Item = (&FontSizeKey, &Vec<FontAtlas>)> { pub fn iter(&self) -> impl Iterator<Item = (&FontSizeKey, &Vec<FontAtlas>)> {
self.font_atlases.iter() 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 self.font_atlases
.get(&FloatOrd(font_size)) .get(&FloatOrd(font_size))
.map_or(false, |font_atlas| { .map_or(false, |font_atlas| {
font_atlas font_atlas.iter().any(|atlas| atlas.has_glyph(glyph_id))
.iter()
.any(|atlas| atlas.get_char_index(character).is_some())
}) })
} }
pub fn add_glyphs_to_atlas( pub fn add_glyph_to_atlas(
&mut self, &mut self,
fonts: &Assets<Font>,
texture_atlases: &mut Assets<TextureAtlas>, texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Texture>, textures: &mut Assets<Texture>,
font_size: f32, outlined_glyph: OutlinedGlyph,
text: &str, ) -> Result<GlyphAtlasInfo, TextError> {
) -> Option<f32> { let glyph = outlined_glyph.glyph();
let mut width = 0.0; let glyph_id = glyph.id;
let font = fonts.get(&self.font)?; let font_size = glyph.scale.y;
let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size);
let font_atlases = self let font_atlases = self
.font_atlases .font_atlases
.entry(FloatOrd(font_size)) .entry(FloatOrd(font_size))
@ -67,63 +62,47 @@ impl FontAtlasSet {
Vec2::new(512.0, 512.0), Vec2::new(512.0, 512.0),
)] )]
}); });
let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph);
let mut last_glyph: Option<Glyph> = None; let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool {
for character in text.chars() { atlas.add_glyph(textures, texture_atlases, glyph_id, &glyph_texture)
if character.is_control() { };
continue; 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<GlyphAtlasInfo> { pub fn get_glyph_atlas_info(
&self,
font_size: f32,
glyph_id: GlyphId,
) -> Option<GlyphAtlasInfo> {
self.font_atlases self.font_atlases
.get(&FloatOrd(font_size)) .get(&FloatOrd(font_size))
.and_then(|font_atlas| { .and_then(|font_atlases| {
font_atlas font_atlases
.iter() .iter()
.find_map(|atlas| { .find_map(|atlas| {
atlas atlas
.get_char_index(character) .get_glyph_index(glyph_id)
.map(|char_index| (char_index, atlas.texture_atlas.clone_weak())) .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak()))
}) })
.map(|(char_index, texture_atlas)| GlyphAtlasInfo { .map(|(glyph_index, texture_atlas)| GlyphAtlasInfo {
texture_atlas, texture_atlas,
char_index, glyph_index,
}) })
}) })
} }

View file

@ -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<FontArc>,
handles: Vec<Handle<Font>>,
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<S: ToSectionText>(
&self,
sections: &[S],
bounds: Size,
text_alignment: TextAlignment,
) -> Result<Vec<SectionGlyph>, 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<SectionGlyph>,
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
fonts: &Assets<Font>,
texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Texture>,
) -> Result<Vec<PositionedGlyph>, 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 = &section_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<FontAtlasSet> = 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>, 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,
}

View file

@ -1,21 +1,31 @@
mod draw; mod draw;
mod error;
mod font; mod font;
mod font_atlas; mod font_atlas;
mod font_atlas_set; mod font_atlas_set;
mod font_loader; mod font_loader;
mod glyph_brush;
mod pipeline;
pub use draw::*; pub use draw::*;
pub use error::*;
pub use font::*; pub use font::*;
pub use font_atlas::*; pub use font_atlas::*;
pub use font_atlas_set::*; pub use font_atlas_set::*;
pub use font_loader::*; pub use font_loader::*;
pub use glyph_brush::*;
pub use pipeline::*;
pub mod prelude { 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_app::prelude::*;
use bevy_asset::AddAsset; use bevy_asset::AddAsset;
use bevy_ecs::Entity;
pub type DefaultTextPipeline = TextPipeline<Entity>;
#[derive(Default)] #[derive(Default)]
pub struct TextPlugin; pub struct TextPlugin;
@ -24,6 +34,7 @@ impl Plugin for TextPlugin {
fn build(&self, app: &mut AppBuilder) { fn build(&self, app: &mut AppBuilder) {
app.add_asset::<Font>() app.add_asset::<Font>()
.add_asset::<FontAtlasSet>() .add_asset::<FontAtlasSet>()
.init_asset_loader::<FontLoader>(); .init_asset_loader::<FontLoader>()
.add_resource(DefaultTextPipeline::default());
} }
} }

View file

@ -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<ID> {
brush: GlyphBrush,
glyph_map: HashMap<ID, TextLayoutInfo>,
map_font_id: HashMap<HandleId, FontId>,
}
impl<ID> Default for TextPipeline<ID> {
fn default() -> Self {
TextPipeline {
brush: GlyphBrush::default(),
glyph_map: Default::default(),
map_font_id: Default::default(),
}
}
}
pub struct TextLayoutInfo {
pub glyphs: Vec<PositionedGlyph>,
pub size: Size,
}
impl<ID: Hash + Eq> TextPipeline<ID> {
pub fn get_or_insert_font_id(&mut self, handle: Handle<Font>, 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<Font>,
fonts: &Assets<Font>,
text: &str,
font_size: f32,
text_alignment: TextAlignment,
bounds: Size,
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Texture>,
) -> 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 = &section_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(())
}
}

View file

@ -1,6 +1,6 @@
use crate::{CalculatedSize, Node}; use crate::{CalculatedSize, Node, Style, Val};
use bevy_asset::{Assets, Handle}; 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_math::Size;
use bevy_render::{ use bevy_render::{
draw::{Draw, DrawContext, Drawable}, draw::{Draw, DrawContext, Drawable},
@ -10,7 +10,7 @@ use bevy_render::{
texture::Texture, texture::Texture,
}; };
use bevy_sprite::{TextureAtlas, QUAD_HANDLE}; 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; use bevy_transform::prelude::GlobalTransform;
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -25,101 +25,148 @@ pub struct Text {
pub style: TextStyle, 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( pub fn text_system(
mut queued_text: Local<QueuedText>, mut queued_text: Local<QueuedText>,
mut textures: ResMut<Assets<Texture>>, mut textures: ResMut<Assets<Texture>>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
mut font_atlas_sets: ResMut<Assets<FontAtlasSet>>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>, mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut queries: QuerySet<( mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
Query<(Entity, &Text, &mut CalculatedSize), Changed<Text>>, mut text_pipeline: ResMut<DefaultTextPipeline>,
Query<(&Text, &mut CalculatedSize)>, mut text_queries: QuerySet<(
Query<Entity, Or<(Changed<Text>, Changed<Style>)>>,
Query<(&Text, &Style, &mut CalculatedSize)>,
)>, )>,
) { ) {
// add queued text to atlases // Adds all entities where the text or the style has changed to the local queue
let mut new_queued_text = Vec::new(); for entity in text_queries.q0_mut().iter_mut() {
queued_text.entities.push(entity);
}
if queued_text.entities.is_empty() {
return;
}
// Computes all text in the local queue
let mut new_queue = Vec::new();
let query = text_queries.q1_mut();
for entity in queued_text.entities.drain(..) { for entity in queued_text.entities.drain(..) {
if let Ok((text, mut calculated_size)) = queries.q1_mut().get_mut(entity) { if let Ok((text, style, mut calculated_size)) = query.get_mut(entity) {
let font_atlases = font_atlas_sets match add_text_to_pipeline(
.get_or_insert_with(text.font.id, || FontAtlasSet::new(text.font.clone_weak())); entity,
// TODO: this call results in one or more TextureAtlases, whose render resources are created in the RENDER_GRAPH_SYSTEMS &*text,
// stage. That logic runs _before_ the DRAW stage, which means we cant call add_glyphs_to_atlas in the draw stage &*style,
// without our render resources being a frame behind. Therefore glyph atlasing either needs its own system or the TextureAtlas &mut *textures,
// resource generation needs to happen AFTER the render graph systems. maybe draw systems should execute within the &*fonts,
// render graph so ordering like this can be taken into account? Maybe the RENDER_GRAPH_SYSTEMS stage should be removed entirely &mut *texture_atlases,
// in favor of node.update()? Regardless, in the immediate short term the current approach is fine. &mut *font_atlas_set_storage,
if let Some(width) = font_atlases.add_glyphs_to_atlas( &mut *text_pipeline,
&fonts,
&mut texture_atlases,
&mut textures,
text.style.font_size,
&text.value,
) { ) {
calculated_size.size = Size::new(width, text.style.font_size); TextPipelineResult::Ok => {
} else { let text_layout_info = text_pipeline.get_glyphs(&entity).expect(
new_queued_text.push(entity); "Failed to get glyphs from the pipeline that have just been computed",
);
calculated_size.size = text_layout_info.size;
}
TextPipelineResult::Reschedule => {
// There was an error processing the text layout, let's add this entity to the queue for further processing
new_queue.push(entity);
}
} }
} }
} }
queued_text.entities = new_queued_text; queued_text.entities = new_queue;
}
// add changed text to atlases enum TextPipelineResult {
for (entity, text, mut calculated_size) in queries.q0_mut().iter_mut() { Ok,
let font_atlases = font_atlas_sets Reschedule,
.get_or_insert_with(text.font.id, || FontAtlasSet::new(text.font.clone_weak())); }
// TODO: this call results in one or more TextureAtlases, whose render resources are created in the RENDER_GRAPH_SYSTEMS
// stage. That logic runs _before_ the DRAW stage, which means we cant call add_glyphs_to_atlas in the draw stage /// Computes the text layout and stores it in the TextPipeline resource.
// without our render resources being a frame behind. Therefore glyph atlasing either needs its own system or the TextureAtlas #[allow(clippy::too_many_arguments)]
// resource generation needs to happen AFTER the render graph systems. maybe draw systems should execute within the fn add_text_to_pipeline(
// render graph so ordering like this can be taken into account? Maybe the RENDER_GRAPH_SYSTEMS stage should be removed entirely entity: Entity,
// in favor of node.update()? Regardless, in the immediate short term the current approach is fine. text: &Text,
if let Some(width) = font_atlases.add_glyphs_to_atlas( style: &Style,
&fonts, textures: &mut Assets<Texture>,
&mut texture_atlases, fonts: &Assets<Font>,
&mut textures, texture_atlases: &mut Assets<TextureAtlas>,
text.style.font_size, font_atlas_set_storage: &mut Assets<FontAtlasSet>,
&text.value, text_pipeline: &mut DefaultTextPipeline,
) { ) -> TextPipelineResult {
calculated_size.size = Size::new(width, text.style.font_size); let node_size = Size::new(
} else { text_constraint(style.min_size.width, style.size.width, style.max_size.width),
queued_text.entities.push(entity); text_constraint(
style.min_size.height,
style.size.height,
style.max_size.height,
),
);
match text_pipeline.queue_text(
entity,
text.font.clone(),
&fonts,
&text.value,
text.style.font_size,
text.style.alignment,
node_size,
font_atlas_set_storage,
texture_atlases,
textures,
) {
Err(TextError::NoSuchFont) => TextPipelineResult::Reschedule,
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {}", e);
} }
Ok(()) => TextPipelineResult::Ok,
} }
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn draw_text_system( pub fn draw_text_system(
mut draw_context: DrawContext, mut context: DrawContext,
fonts: Res<Assets<Font>>,
msaa: Res<Msaa>, msaa: Res<Msaa>,
font_atlas_sets: Res<Assets<FontAtlasSet>>,
texture_atlases: Res<Assets<TextureAtlas>>,
meshes: Res<Assets<Mesh>>, meshes: Res<Assets<Mesh>>,
mut render_resource_bindings: ResMut<RenderResourceBindings>, mut render_resource_bindings: ResMut<RenderResourceBindings>,
mut asset_render_resource_bindings: ResMut<AssetRenderResourceBindings>, mut asset_render_resource_bindings: ResMut<AssetRenderResourceBindings>,
mut query: Query<(&mut Draw, &Text, &Node, &GlobalTransform)>, text_pipeline: Res<DefaultTextPipeline>,
mut query: Query<(Entity, &mut Draw, &Text, &Node, &GlobalTransform)>,
) { ) {
let font_quad = meshes.get(&QUAD_HANDLE).unwrap(); let font_quad = meshes.get(&QUAD_HANDLE).unwrap();
let vertex_buffer_descriptor = font_quad.get_vertex_buffer_descriptor(); let vertex_buffer_descriptor = font_quad.get_vertex_buffer_descriptor();
for (mut draw, text, node, global_transform) in query.iter_mut() { for (entity, mut draw, text, node, global_transform) in query.iter_mut() {
if let Some(font) = fonts.get(&text.font) { if let Some(text_glyphs) = text_pipeline.get_glyphs(&entity) {
let position = global_transform.translation - (node.size / 2.0).extend(0.0); let position = global_transform.translation - (node.size / 2.0).extend(0.0);
let mut drawable_text = DrawableText { let mut drawable_text = DrawableText {
font,
font_atlas_set: font_atlas_sets.get(text.font.id).unwrap(),
texture_atlases: &texture_atlases,
render_resource_bindings: &mut render_resource_bindings, render_resource_bindings: &mut render_resource_bindings,
asset_render_resource_bindings: &mut asset_render_resource_bindings, asset_render_resource_bindings: &mut asset_render_resource_bindings,
position, position,
msaa: &msaa, msaa: &msaa,
style: &text.style, text_glyphs: &text_glyphs.glyphs,
text: &text.value,
container_size: node.size,
font_quad_vertex_descriptor: &vertex_buffer_descriptor, font_quad_vertex_descriptor: &vertex_buffer_descriptor,
style: &text.style,
}; };
drawable_text.draw(&mut draw, &mut draw_context).unwrap();
drawable_text.draw(&mut draw, &mut context).unwrap();
} }
} }
} }

View file

@ -118,6 +118,7 @@ fn setup(
style: TextStyle { style: TextStyle {
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, },
..Default::default() ..Default::default()

View file

@ -73,6 +73,7 @@ fn setup(
style: TextStyle { style: TextStyle {
color: Color::rgb(0.5, 0.5, 1.0), color: Color::rgb(0.5, 0.5, 1.0),
font_size: 40.0, font_size: 40.0,
..Default::default()
}, },
}, },
style: Style { style: Style {

View file

@ -115,6 +115,7 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
style: TextStyle { style: TextStyle {
font_size: 50.0, font_size: 50.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, },
..Default::default() ..Default::default()

View file

@ -84,6 +84,7 @@ fn setup(
style: TextStyle { style: TextStyle {
font_size: 40.0, font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9), color: Color::rgb(0.9, 0.9, 0.9),
..Default::default()
}, },
}, },
..Default::default() ..Default::default()

View file

@ -78,16 +78,13 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>, mut state: Res
commands commands
.spawn(UiCameraComponents::default()) .spawn(UiCameraComponents::default())
.spawn(TextComponents { .spawn(TextComponents {
style: Style {
size: Size::new(Val::Px(250.0), Val::Px(60.0)),
..Default::default()
},
text: Text { text: Text {
value: "a".to_string(), value: "a".to_string(),
font: font_handle, font: font_handle,
style: TextStyle { style: TextStyle {
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, },
..Default::default() ..Default::default()

View file

@ -42,6 +42,7 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
style: TextStyle { style: TextStyle {
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, },
..Default::default() ..Default::default()

137
examples/ui/text_debug.rs Normal file
View file

@ -0,0 +1,137 @@
use bevy::prelude::*;
extern crate rand;
/// This example is for debugging text layout
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_startup_system(infotext_system.system())
.add_system(change_text_system.system())
.run();
}
struct TextChanges;
fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
commands
.spawn(UiCameraComponents::default())
.spawn(TextComponents {
style: Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: Rect {
top: Val::Px(5.0),
left: Val::Px(15.0),
..Default::default()
},
..Default::default()
},
text: Text {
value: "This is\ntext with\nline breaks\nin the top left".to_string(),
font: font.clone(),
style: TextStyle {
font_size: 50.0,
color: Color::WHITE,
alignment: TextAlignment::default(),
},
},
..Default::default()
});
commands
.spawn(UiCameraComponents::default())
.spawn(TextComponents {
style: Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: Rect {
top: Val::Px(5.0),
right: Val::Px(15.0),
..Default::default()
},
max_size: Size {
width: Val::Px(400.),
height: Val::Undefined,
},
..Default::default()
},
text: Text {
value:
"This is very long text with limited width in the top right and is also pink"
.to_string(),
font: font.clone(),
style: TextStyle {
font_size: 50.0,
color: Color::rgb(0.8, 0.2, 0.7),
alignment: TextAlignment {
horizontal: HorizontalAlign::Center,
vertical: VerticalAlign::Center,
},
},
},
..Default::default()
});
commands
.spawn(UiCameraComponents::default())
.spawn(TextComponents {
style: Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: Rect {
bottom: Val::Px(5.0),
right: Val::Px(15.0),
..Default::default()
},
..Default::default()
},
text: Text {
value: "This text changes in the bottom right".to_string(),
font: font.clone(),
style: TextStyle {
font_size: 50.0,
color: Color::WHITE,
alignment: TextAlignment::default(),
},
},
..Default::default()
})
.with(TextChanges);
commands
.spawn(UiCameraComponents::default())
.spawn(TextComponents {
style: Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: Rect {
bottom: Val::Px(5.0),
left: Val::Px(15.0),
..Default::default()
},
size: Size {
width: Val::Px(200.0),
..Default::default()
},
..Default::default()
},
text: Text {
value: "This\ntext has\nline breaks and also a set width in the bottom left"
.to_string(),
font,
style: TextStyle {
font_size: 50.0,
color: Color::WHITE,
alignment: TextAlignment::default(),
},
},
..Default::default()
});
}
fn change_text_system(mut query: Query<(&mut Text, &TextChanges)>) {
for (mut text, _text_changes) in query.iter_mut() {
text.value = format!(
"This text changes in the bottom right {}",
rand::random::<u16>(),
);
}
}

View file

@ -63,6 +63,7 @@ fn setup(
style: TextStyle { style: TextStyle {
font_size: 30.0, font_size: 30.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, },
..Default::default() ..Default::default()