mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
1eff53462a
commit
465c3d4f7b
19 changed files with 641 additions and 338 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<TextureAtlas>,
|
||||
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<PositionedGlyph>,
|
||||
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<Glyph> = 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(())
|
||||
}
|
||||
}
|
||||
|
|
10
crates/bevy_text/src/error.rs
Normal file
10
crates/bevy_text/src/error.rs
Normal 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),
|
||||
}
|
|
@ -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<u8>) -> Result<Self, InvalidFont> {
|
||||
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::<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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<char, u32>,
|
||||
pub glyph_to_atlas_index: HashMap<GlyphId, u32>,
|
||||
pub texture_atlas: Handle<TextureAtlas>,
|
||||
}
|
||||
|
||||
|
@ -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<u32> {
|
||||
self.glyph_to_index.get(&character).cloned()
|
||||
pub fn get_glyph_index(&self, glyph_id: GlyphId) -> Option<u32> {
|
||||
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>,
|
||||
texture_atlases: &mut Assets<TextureAtlas>,
|
||||
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
|
||||
|
|
|
@ -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>,
|
||||
font_atlases: HashMap<FontSizeKey, Vec<FontAtlas>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GlyphAtlasInfo {
|
||||
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 {
|
||||
pub fn new(font: Handle<Font>) -> Self {
|
||||
Self {
|
||||
font,
|
||||
font_atlases: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&FontSizeKey, &Vec<FontAtlas>)> {
|
||||
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<Font>,
|
||||
texture_atlases: &mut Assets<TextureAtlas>,
|
||||
textures: &mut Assets<Texture>,
|
||||
font_size: f32,
|
||||
text: &str,
|
||||
) -> Option<f32> {
|
||||
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<GlyphAtlasInfo, TextError> {
|
||||
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<Glyph> = 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<GlyphAtlasInfo> {
|
||||
pub fn get_glyph_atlas_info(
|
||||
&self,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
) -> Option<GlyphAtlasInfo> {
|
||||
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,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
123
crates/bevy_text/src/glyph_brush.rs
Normal file
123
crates/bevy_text/src/glyph_brush.rs
Normal 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 = §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<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,
|
||||
}
|
|
@ -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<Entity>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextPlugin;
|
||||
|
@ -24,6 +34,7 @@ impl Plugin for TextPlugin {
|
|||
fn build(&self, app: &mut AppBuilder) {
|
||||
app.add_asset::<Font>()
|
||||
.add_asset::<FontAtlasSet>()
|
||||
.init_asset_loader::<FontLoader>();
|
||||
.init_asset_loader::<FontLoader>()
|
||||
.add_resource(DefaultTextPipeline::default());
|
||||
}
|
||||
}
|
||||
|
|
117
crates/bevy_text/src/pipeline.rs
Normal file
117
crates/bevy_text/src/pipeline.rs
Normal 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 = §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(())
|
||||
}
|
||||
}
|
|
@ -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<QueuedText>,
|
||||
mut textures: ResMut<Assets<Texture>>,
|
||||
fonts: Res<Assets<Font>>,
|
||||
mut font_atlas_sets: ResMut<Assets<FontAtlasSet>>,
|
||||
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
|
||||
mut queries: QuerySet<(
|
||||
Query<(Entity, &Text, &mut CalculatedSize), Changed<Text>>,
|
||||
Query<(&Text, &mut CalculatedSize)>,
|
||||
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
|
||||
mut text_pipeline: ResMut<DefaultTextPipeline>,
|
||||
mut text_queries: QuerySet<(
|
||||
Query<Entity, Or<(Changed<Text>, Changed<Style>)>>,
|
||||
Query<(&Text, &Style, &mut CalculatedSize)>,
|
||||
)>,
|
||||
) {
|
||||
// add queued text to atlases
|
||||
let mut new_queued_text = Vec::new();
|
||||
// Adds all entities where the text or the style has changed to the local queue
|
||||
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(..) {
|
||||
if let Ok((text, mut calculated_size)) = queries.q1_mut().get_mut(entity) {
|
||||
let font_atlases = font_atlas_sets
|
||||
.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
|
||||
// without our render resources being a frame behind. Therefore glyph atlasing either needs its own system or the TextureAtlas
|
||||
// resource generation needs to happen AFTER the render graph systems. maybe draw systems should execute within the
|
||||
// render graph so ordering like this can be taken into account? Maybe the RENDER_GRAPH_SYSTEMS stage should be removed entirely
|
||||
// in favor of node.update()? Regardless, in the immediate short term the current approach is fine.
|
||||
if let Some(width) = font_atlases.add_glyphs_to_atlas(
|
||||
&fonts,
|
||||
&mut texture_atlases,
|
||||
&mut textures,
|
||||
text.style.font_size,
|
||||
&text.value,
|
||||
if let Ok((text, style, mut calculated_size)) = query.get_mut(entity) {
|
||||
match add_text_to_pipeline(
|
||||
entity,
|
||||
&*text,
|
||||
&*style,
|
||||
&mut *textures,
|
||||
&*fonts,
|
||||
&mut *texture_atlases,
|
||||
&mut *font_atlas_set_storage,
|
||||
&mut *text_pipeline,
|
||||
) {
|
||||
calculated_size.size = Size::new(width, text.style.font_size);
|
||||
} else {
|
||||
new_queued_text.push(entity);
|
||||
TextPipelineResult::Ok => {
|
||||
let text_layout_info = text_pipeline.get_glyphs(&entity).expect(
|
||||
"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
|
||||
for (entity, text, mut calculated_size) in queries.q0_mut().iter_mut() {
|
||||
let font_atlases = font_atlas_sets
|
||||
.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
|
||||
// without our render resources being a frame behind. Therefore glyph atlasing either needs its own system or the TextureAtlas
|
||||
// resource generation needs to happen AFTER the render graph systems. maybe draw systems should execute within the
|
||||
// render graph so ordering like this can be taken into account? Maybe the RENDER_GRAPH_SYSTEMS stage should be removed entirely
|
||||
// in favor of node.update()? Regardless, in the immediate short term the current approach is fine.
|
||||
if let Some(width) = font_atlases.add_glyphs_to_atlas(
|
||||
&fonts,
|
||||
&mut texture_atlases,
|
||||
&mut textures,
|
||||
text.style.font_size,
|
||||
&text.value,
|
||||
) {
|
||||
calculated_size.size = Size::new(width, text.style.font_size);
|
||||
} else {
|
||||
queued_text.entities.push(entity);
|
||||
enum TextPipelineResult {
|
||||
Ok,
|
||||
Reschedule,
|
||||
}
|
||||
|
||||
/// Computes the text layout and stores it in the TextPipeline resource.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_text_to_pipeline(
|
||||
entity: Entity,
|
||||
text: &Text,
|
||||
style: &Style,
|
||||
textures: &mut Assets<Texture>,
|
||||
fonts: &Assets<Font>,
|
||||
texture_atlases: &mut Assets<TextureAtlas>,
|
||||
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
|
||||
text_pipeline: &mut DefaultTextPipeline,
|
||||
) -> TextPipelineResult {
|
||||
let node_size = Size::new(
|
||||
text_constraint(style.min_size.width, style.size.width, style.max_size.width),
|
||||
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)]
|
||||
pub fn draw_text_system(
|
||||
mut draw_context: DrawContext,
|
||||
fonts: Res<Assets<Font>>,
|
||||
mut context: DrawContext,
|
||||
msaa: Res<Msaa>,
|
||||
font_atlas_sets: Res<Assets<FontAtlasSet>>,
|
||||
texture_atlases: Res<Assets<TextureAtlas>>,
|
||||
meshes: Res<Assets<Mesh>>,
|
||||
mut render_resource_bindings: ResMut<RenderResourceBindings>,
|
||||
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 vertex_buffer_descriptor = font_quad.get_vertex_buffer_descriptor();
|
||||
|
||||
for (mut draw, text, node, global_transform) in query.iter_mut() {
|
||||
if let Some(font) = fonts.get(&text.font) {
|
||||
for (entity, mut draw, text, node, global_transform) in query.iter_mut() {
|
||||
if let Some(text_glyphs) = text_pipeline.get_glyphs(&entity) {
|
||||
let position = global_transform.translation - (node.size / 2.0).extend(0.0);
|
||||
|
||||
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,
|
||||
asset_render_resource_bindings: &mut asset_render_resource_bindings,
|
||||
position,
|
||||
msaa: &msaa,
|
||||
style: &text.style,
|
||||
text: &text.value,
|
||||
container_size: node.size,
|
||||
text_glyphs: &text_glyphs.glyphs,
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,7 @@ fn setup(
|
|||
style: TextStyle {
|
||||
font_size: 60.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
|
|
|
@ -73,6 +73,7 @@ fn setup(
|
|||
style: TextStyle {
|
||||
color: Color::rgb(0.5, 0.5, 1.0),
|
||||
font_size: 40.0,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
style: Style {
|
||||
|
|
|
@ -115,6 +115,7 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
|
|||
style: TextStyle {
|
||||
font_size: 50.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
|
|
|
@ -84,6 +84,7 @@ fn setup(
|
|||
style: TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::rgb(0.9, 0.9, 0.9),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
|
|
|
@ -78,16 +78,13 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>, mut state: Res
|
|||
commands
|
||||
.spawn(UiCameraComponents::default())
|
||||
.spawn(TextComponents {
|
||||
style: Style {
|
||||
size: Size::new(Val::Px(250.0), Val::Px(60.0)),
|
||||
..Default::default()
|
||||
},
|
||||
text: Text {
|
||||
value: "a".to_string(),
|
||||
font: font_handle,
|
||||
style: TextStyle {
|
||||
font_size: 60.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
|
|
|
@ -42,6 +42,7 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
|
|||
style: TextStyle {
|
||||
font_size: 60.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
|
|
137
examples/ui/text_debug.rs
Normal file
137
examples/ui/text_debug.rs
Normal 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>(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -63,6 +63,7 @@ fn setup(
|
|||
style: TextStyle {
|
||||
font_size: 30.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
|
|
Loading…
Reference in a new issue