Rich text support (different fonts / styles within the same text section)
This commit is contained in:
tigregalis 2021-01-25 09:07:43 +08:00 committed by GitHub
parent 3d0c4e380c
commit 40b5bbd028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 482 additions and 228 deletions

View File

@ -1,6 +1,5 @@
use bevy_math::{Mat4, Vec3}; use bevy_math::{Mat4, Vec3};
use bevy_render::{ use bevy_render::{
color::Color,
draw::{Draw, DrawContext, DrawError, Drawable}, draw::{Draw, DrawContext, DrawError, Drawable},
mesh, mesh,
mesh::Mesh, mesh::Mesh,
@ -9,47 +8,14 @@ use bevy_render::{
renderer::{BindGroup, RenderResourceBindings, RenderResourceId}, renderer::{BindGroup, RenderResourceBindings, RenderResourceId},
}; };
use bevy_sprite::TextureAtlasSprite; use bevy_sprite::TextureAtlasSprite;
use glyph_brush_layout::{HorizontalAlign, VerticalAlign};
use crate::PositionedGlyph; use crate::{PositionedGlyph, TextSection};
#[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 {
fn default() -> Self {
Self {
color: Color::WHITE,
font_size: 12.0,
alignment: TextAlignment::default(),
}
}
}
pub struct DrawableText<'a> { pub struct DrawableText<'a> {
pub render_resource_bindings: &'a mut RenderResourceBindings, pub render_resource_bindings: &'a mut RenderResourceBindings,
pub position: Vec3, pub position: Vec3,
pub scale_factor: f32, pub scale_factor: f32,
pub style: &'a TextStyle, pub sections: &'a [TextSection],
pub text_glyphs: &'a Vec<PositionedGlyph>, 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,
@ -103,7 +69,7 @@ impl<'a> Drawable for DrawableText<'a> {
let sprite = TextureAtlasSprite { let sprite = TextureAtlasSprite {
index: tv.atlas_info.glyph_index, index: tv.atlas_info.glyph_index,
color: self.style.color, color: self.sections[tv.section_index].style.color,
}; };
// To get the rendering right for non-one scaling factors, we need // To get the rendering right for non-one scaling factors, we need

View File

@ -4,7 +4,7 @@ use bevy_math::{Size, Vec2};
use bevy_render::prelude::Texture; use bevy_render::prelude::Texture;
use bevy_sprite::TextureAtlas; use bevy_sprite::TextureAtlas;
use glyph_brush_layout::{ use glyph_brush_layout::{
FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, ToSectionText, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, SectionText, ToSectionText,
}; };
use crate::{error::TextError, Font, FontAtlasSet, GlyphAtlasInfo, TextAlignment}; use crate::{error::TextError, Font, FontAtlasSet, GlyphAtlasInfo, TextAlignment};
@ -46,6 +46,7 @@ impl GlyphBrush {
pub fn process_glyphs( pub fn process_glyphs(
&self, &self,
glyphs: Vec<SectionGlyph>, glyphs: Vec<SectionGlyph>,
sections: &[SectionText],
font_atlas_set_storage: &mut Assets<FontAtlasSet>, font_atlas_set_storage: &mut Assets<FontAtlasSet>,
fonts: &Assets<Font>, fonts: &Assets<Font>,
texture_atlases: &mut Assets<TextureAtlas>, texture_atlases: &mut Assets<TextureAtlas>,
@ -55,16 +56,26 @@ impl GlyphBrush {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let first_glyph = glyphs.first().expect("Must have at least one glyph."); let sections_data = sections
let font_id = first_glyph.font_id.0; .iter()
let handle = &self.handles[font_id]; .map(|section| {
let font = fonts.get(handle).ok_or(TextError::NoSuchFont)?; let handle = &self.handles[section.font_id.0];
let font_size = first_glyph.glyph.scale.y; let font = fonts.get(handle).ok_or(TextError::NoSuchFont)?;
let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size); let font_size = section.scale.y;
Ok((
handle,
font,
font_size,
ab_glyph::Font::as_scaled(&font.font, font_size),
))
})
.collect::<Result<Vec<_>, _>>()?;
let mut max_y = std::f32::MIN; let mut max_y = std::f32::MIN;
let mut min_x = std::f32::MAX; let mut min_x = std::f32::MAX;
for section_glyph in glyphs.iter() { for sg in glyphs.iter() {
let glyph = &section_glyph.glyph; let glyph = &sg.glyph;
let scaled_font = sections_data[sg.section_index].3;
max_y = max_y.max(glyph.position.y - scaled_font.descent()); max_y = max_y.max(glyph.position.y - scaled_font.descent());
min_x = min_x.min(glyph.position.x); min_x = min_x.min(glyph.position.x);
} }
@ -82,14 +93,15 @@ impl GlyphBrush {
let glyph_id = glyph.id; let glyph_id = glyph.id;
let glyph_position = glyph.position; let glyph_position = glyph.position;
let adjust = GlyphPlacementAdjuster::new(&mut glyph); let adjust = GlyphPlacementAdjuster::new(&mut glyph);
if let Some(outlined_glyph) = font.font.outline_glyph(glyph) { let section_data = sections_data[sg.section_index];
if let Some(outlined_glyph) = section_data.1.font.outline_glyph(glyph) {
let bounds = outlined_glyph.px_bounds(); let bounds = outlined_glyph.px_bounds();
let handle_font_atlas: Handle<FontAtlasSet> = handle.as_weak(); let handle_font_atlas: Handle<FontAtlasSet> = section_data.0.as_weak();
let font_atlas_set = font_atlas_set_storage let font_atlas_set = font_atlas_set_storage
.get_or_insert_with(handle_font_atlas, FontAtlasSet::default); .get_or_insert_with(handle_font_atlas, FontAtlasSet::default);
let atlas_info = font_atlas_set let atlas_info = font_atlas_set
.get_glyph_atlas_info(font_size, glyph_id, glyph_position) .get_glyph_atlas_info(section_data.2, glyph_id, glyph_position)
.map(Ok) .map(Ok)
.unwrap_or_else(|| { .unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph) font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph)
@ -107,6 +119,7 @@ impl GlyphBrush {
positioned_glyphs.push(PositionedGlyph { positioned_glyphs.push(PositionedGlyph {
position, position,
atlas_info, atlas_info,
section_index: sg.section_index,
}); });
} }
} }
@ -126,6 +139,7 @@ impl GlyphBrush {
pub struct PositionedGlyph { pub struct PositionedGlyph {
pub position: Vec2, pub position: Vec2,
pub atlas_info: GlyphAtlasInfo, pub atlas_info: GlyphAtlasInfo,
pub section_index: usize,
} }
#[cfg(feature = "subpixel_glyph_atlas")] #[cfg(feature = "subpixel_glyph_atlas")]

View File

@ -21,7 +21,7 @@ pub use text::*;
pub use text2d::*; pub use text2d::*;
pub mod prelude { pub mod prelude {
pub use crate::{Font, Text, Text2dBundle, TextAlignment, TextError, TextStyle}; pub use crate::{Font, Text, Text2dBundle, TextAlignment, TextError, TextSection, TextStyle};
pub use glyph_brush_layout::{HorizontalAlign, VerticalAlign}; pub use glyph_brush_layout::{HorizontalAlign, VerticalAlign};
} }

View File

@ -10,7 +10,8 @@ use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, SectionText}; use glyph_brush_layout::{FontId, SectionText};
use crate::{ use crate::{
error::TextError, glyph_brush::GlyphBrush, Font, FontAtlasSet, PositionedGlyph, TextAlignment, error::TextError, glyph_brush::GlyphBrush, scale_value, Font, FontAtlasSet, PositionedGlyph,
TextAlignment, TextSection,
}; };
pub struct TextPipeline<ID> { pub struct TextPipeline<ID> {
@ -35,7 +36,7 @@ pub struct TextLayoutInfo {
} }
impl<ID: Hash + Eq> TextPipeline<ID> { impl<ID: Hash + Eq> TextPipeline<ID> {
pub fn get_or_insert_font_id(&mut self, handle: Handle<Font>, font: &Font) -> FontId { pub fn get_or_insert_font_id(&mut self, handle: &Handle<Font>, font: &Font) -> FontId {
let brush = &mut self.brush; let brush = &mut self.brush;
*self *self
.map_font_id .map_font_id
@ -51,30 +52,40 @@ impl<ID: Hash + Eq> TextPipeline<ID> {
pub fn queue_text( pub fn queue_text(
&mut self, &mut self,
id: ID, id: ID,
font_handle: Handle<Font>,
fonts: &Assets<Font>, fonts: &Assets<Font>,
text: &str, sections: &[TextSection],
font_size: f32, scale_factor: f64,
text_alignment: TextAlignment, text_alignment: TextAlignment,
bounds: Size, bounds: Size,
font_atlas_set_storage: &mut Assets<FontAtlasSet>, font_atlas_set_storage: &mut Assets<FontAtlasSet>,
texture_atlases: &mut Assets<TextureAtlas>, texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Texture>, textures: &mut Assets<Texture>,
) -> Result<(), TextError> { ) -> Result<(), TextError> {
let font = fonts.get(font_handle.id).ok_or(TextError::NoSuchFont)?; let mut scaled_fonts = Vec::new();
let font_id = self.get_or_insert_font_id(font_handle, font); let sections = sections
.iter()
.map(|section| {
let font = fonts
.get(section.style.font.id)
.ok_or(TextError::NoSuchFont)?;
let font_id = self.get_or_insert_font_id(&section.style.font, font);
let font_size = scale_value(section.style.font_size, scale_factor);
let section = SectionText { scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size));
font_id,
scale: PxScale::from(font_size),
text,
};
let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size); let section = SectionText {
font_id,
scale: PxScale::from(font_size),
text: &section.value,
};
Ok(section)
})
.collect::<Result<Vec<_>, _>>()?;
let section_glyphs = self let section_glyphs = self
.brush .brush
.compute_glyphs(&[section], bounds, text_alignment)?; .compute_glyphs(&sections, bounds, text_alignment)?;
if section_glyphs.is_empty() { if section_glyphs.is_empty() {
self.glyph_map.insert( self.glyph_map.insert(
@ -92,8 +103,9 @@ impl<ID: Hash + Eq> TextPipeline<ID> {
let mut max_x: f32 = std::f32::MIN; let mut max_x: f32 = std::f32::MIN;
let mut max_y: f32 = std::f32::MIN; let mut max_y: f32 = std::f32::MIN;
for section_glyph in section_glyphs.iter() { for sg in section_glyphs.iter() {
let glyph = &section_glyph.glyph; let scaled_font = scaled_fonts[sg.section_index];
let glyph = &sg.glyph;
min_x = min_x.min(glyph.position.x); min_x = min_x.min(glyph.position.x);
min_y = min_y.min(glyph.position.y - scaled_font.ascent()); 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_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
@ -104,6 +116,7 @@ impl<ID: Hash + Eq> TextPipeline<ID> {
let glyphs = self.brush.process_glyphs( let glyphs = self.brush.process_glyphs(
section_glyphs, section_glyphs,
&sections,
font_atlas_set_storage, font_atlas_set_storage,
fonts, fonts,
texture_atlases, texture_atlases,

View File

@ -1,15 +1,106 @@
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_math::Size; use bevy_math::Size;
use bevy_render::color::Color;
use glyph_brush_layout::{HorizontalAlign, VerticalAlign};
use crate::{Font, TextStyle}; use crate::Font;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Text { pub struct Text {
pub sections: Vec<TextSection>,
pub alignment: TextAlignment,
}
impl Text {
/// Constructs a [`Text`] with (initially) one section.
///
/// ```
/// # use bevy_asset::{AssetServer, Handle};
/// # use bevy_render::color::Color;
/// # use bevy_text::{Font, Text, TextAlignment, TextStyle};
/// # use glyph_brush_layout::{HorizontalAlign, VerticalAlign};
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// #
/// // basic usage
/// let hello_world = Text::with_section(
/// "hello world!".to_string(),
/// TextStyle {
/// font: font_handle.clone(),
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// TextAlignment {
/// vertical: VerticalAlign::Center,
/// horizontal: HorizontalAlign::Center,
/// },
/// );
///
/// let hello_bevy = Text::with_section(
/// // accepts a String or any type that converts into a String, such as &str
/// "hello bevy!",
/// TextStyle {
/// font: font_handle,
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// // you can still use Default
/// Default::default(),
/// );
/// ```
pub fn with_section<S: Into<String>>(
value: S,
style: TextStyle,
alignment: TextAlignment,
) -> Self {
Self {
sections: vec![TextSection {
value: value.into(),
style,
}],
alignment,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct TextSection {
pub value: String, pub value: String,
pub font: Handle<Font>,
pub style: TextStyle, pub style: TextStyle,
} }
#[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: Handle<Font>,
pub font_size: f32,
pub color: Color,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font: Default::default(),
font_size: 12.0,
color: Color::WHITE,
}
}
}
#[derive(Default, Copy, Clone, Debug)] #[derive(Default, Copy, Clone, Debug)]
pub struct CalculatedSize { pub struct CalculatedSize {
pub size: Size, pub size: Size,

View File

@ -90,12 +90,12 @@ pub fn draw_text2d_system(
if let Some(text_glyphs) = text_pipeline.get_glyphs(&entity) { if let Some(text_glyphs) = text_pipeline.get_glyphs(&entity) {
let position = global_transform.translation let position = global_transform.translation
+ match text.style.alignment.vertical { + match text.alignment.vertical {
VerticalAlign::Top => Vec3::zero(), VerticalAlign::Top => Vec3::zero(),
VerticalAlign::Center => Vec3::new(0.0, -height * 0.5, 0.0), VerticalAlign::Center => Vec3::new(0.0, -height * 0.5, 0.0),
VerticalAlign::Bottom => Vec3::new(0.0, -height, 0.0), VerticalAlign::Bottom => Vec3::new(0.0, -height, 0.0),
} }
+ match text.style.alignment.horizontal { + match text.alignment.horizontal {
HorizontalAlign::Left => Vec3::new(-width, 0.0, 0.0), HorizontalAlign::Left => Vec3::new(-width, 0.0, 0.0),
HorizontalAlign::Center => Vec3::new(-width * 0.5, 0.0, 0.0), HorizontalAlign::Center => Vec3::new(-width * 0.5, 0.0, 0.0),
HorizontalAlign::Right => Vec3::zero(), HorizontalAlign::Right => Vec3::zero(),
@ -108,7 +108,7 @@ pub fn draw_text2d_system(
text_glyphs: &text_glyphs.glyphs, text_glyphs: &text_glyphs.glyphs,
font_quad_vertex_descriptor: &vertex_buffer_descriptor, font_quad_vertex_descriptor: &vertex_buffer_descriptor,
scale_factor, scale_factor,
style: &text.style, sections: &text.sections,
}; };
drawable_text.draw(&mut draw, &mut context).unwrap(); drawable_text.draw(&mut draw, &mut context).unwrap();
@ -158,11 +158,10 @@ pub fn text2d_system(
if let Ok((text, mut calculated_size)) = query.get_mut(entity) { if let Ok((text, mut calculated_size)) = query.get_mut(entity) {
match text_pipeline.queue_text( match text_pipeline.queue_text(
entity, entity,
text.font.clone(),
&fonts, &fonts,
&text.value, &text.sections,
scale_value(text.style.font_size, scale_factor), scale_factor,
text.style.alignment, text.alignment,
Size::new(f32::MAX, f32::MAX), Size::new(f32::MAX, f32::MAX),
&mut *font_atlas_set_storage, &mut *font_atlas_set_storage,
&mut *texture_atlases, &mut *texture_atlases,
@ -191,6 +190,6 @@ pub fn text2d_system(
queued_text.entities = new_queue; queued_text.entities = new_queue;
} }
fn scale_value(value: f32, factor: f64) -> f32 { pub fn scale_value(value: f32, factor: f64) -> f32 {
(value as f64 * factor) as f32 (value as f64 * factor) as f32
} }

View File

@ -93,11 +93,10 @@ pub fn text_system(
match text_pipeline.queue_text( match text_pipeline.queue_text(
entity, entity,
text.font.clone(),
&fonts, &fonts,
&text.value, &text.sections,
scale_value(text.style.font_size, scale_factor), scale_factor,
text.style.alignment, text.alignment,
node_size, node_size,
&mut *font_atlas_set_storage, &mut *font_atlas_set_storage,
&mut *texture_atlases, &mut *texture_atlases,
@ -160,7 +159,7 @@ pub fn draw_text_system(
msaa: &msaa, msaa: &msaa,
text_glyphs: &text_glyphs.glyphs, text_glyphs: &text_glyphs.glyphs,
font_quad_vertex_descriptor: &vertex_buffer_descriptor, font_quad_vertex_descriptor: &vertex_buffer_descriptor,
style: &text.style, sections: &text.sections,
}; };
drawable_text.draw(&mut draw, &mut context).unwrap(); drawable_text.draw(&mut draw, &mut context).unwrap();

View File

@ -40,8 +40,8 @@ struct Velocity {
const GRAVITY: f32 = -9.821 * 100.0; const GRAVITY: f32 = -9.821 * 100.0;
const SPRITE_SIZE: f32 = 75.0; const SPRITE_SIZE: f32 = 75.0;
const COL_DESELECTED: Color = Color::rgb_linear(0.03, 0.03, 0.03); const COL_DESELECTED: Color = Color::rgba_linear(0.03, 0.03, 0.03, 0.92);
const COL_SELECTED: Color = Color::rgb_linear(5.0, 5.0, 5.0); const COL_SELECTED: Color = Color::WHITE;
const SHOWCASE_TIMER_SECS: f32 = 3.0; const SHOWCASE_TIMER_SECS: f32 = 3.0;
@ -113,13 +113,25 @@ fn setup(
..Default::default() ..Default::default()
}, },
text: Text { text: Text {
value: "Contributor showcase".to_string(), sections: vec![
font: asset_server.load("fonts/FiraSans-Bold.ttf"), TextSection {
style: TextStyle { value: "Contributor showcase".to_string(),
font_size: 60.0, style: TextStyle {
color: Color::WHITE, font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..Default::default() font_size: 60.0,
}, color: Color::WHITE,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
},
},
],
..Default::default()
}, },
..Default::default() ..Default::default()
}); });
@ -195,7 +207,9 @@ fn select(
trans.translation.z = 100.0; trans.translation.z = 100.0;
text.value = format!("Contributor: {}", name); text.sections[0].value = "Contributor: ".to_string();
text.sections[1].value = name.to_string();
text.sections[1].style.color = mat.color;
Some(()) Some(())
} }
@ -312,9 +326,14 @@ fn contributors() -> Contributors {
/// Because there is no `Mul<Color> for Color` instead `[f32; 3]` is /// Because there is no `Mul<Color> for Color` instead `[f32; 3]` is
/// used. /// used.
fn gen_color(rng: &mut impl Rng) -> [f32; 3] { fn gen_color(rng: &mut impl Rng) -> [f32; 3] {
let r = rng.gen_range(0.2..1.0); loop {
let g = rng.gen_range(0.2..1.0); let rgb = rng.gen();
let b = rng.gen_range(0.2..1.0); if luminance(rgb) >= 0.6 {
let v = Vec3::new(r, g, b); break rgb;
v.normalize().into() }
}
}
fn luminance([r, g, b]: [f32; 3]) -> f32 {
0.299 * r + 0.587 * g + 0.114 * b
} }

View File

@ -13,18 +13,18 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
// 2d camera // 2d camera
.spawn(Camera2dBundle::default()) .spawn(Camera2dBundle::default())
.spawn(Text2dBundle { .spawn(Text2dBundle {
text: Text { text: Text::with_section(
value: "This text is in the 2D scene.".to_string(), "This text is in the 2D scene.",
font: asset_server.load("fonts/FiraSans-Bold.ttf"), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::WHITE,
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
}, },
}, TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
),
..Default::default() ..Default::default()
}); });
} }

View File

@ -52,15 +52,15 @@ fn setup_menu(
}) })
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle { parent.spawn(TextBundle {
text: Text { text: Text::with_section(
value: "Play".to_string(), "Play",
font: asset_server.load("fonts/FiraSans-Bold.ttf"), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
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() ..Default::default()
}); });
}); });

View File

@ -152,15 +152,15 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>, mut game: ResM
// scoreboard // scoreboard
commands.spawn(TextBundle { commands.spawn(TextBundle {
text: Text { text: Text::with_section(
font: asset_server.load("fonts/FiraSans-Bold.ttf"), "Score:",
value: "Score:".to_string(), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
color: Color::rgb(0.5, 0.5, 1.0),
font_size: 40.0, font_size: 40.0,
..Default::default() color: Color::rgb(0.5, 0.5, 1.0),
}, },
}, Default::default(),
),
style: Style { style: Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
position: Rect { position: Rect {
@ -338,7 +338,7 @@ fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Tra
// update the score displayed during the game // update the score displayed during the game
fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) { fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) {
for mut text in query.iter_mut() { for mut text in query.iter_mut() {
text.value = format!("Sugar Rush: {}", game.score); text.sections[0].value = format!("Sugar Rush: {}", game.score);
} }
} }
@ -369,15 +369,15 @@ fn display_score(
}) })
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle { parent.spawn(TextBundle {
text: Text { text: Text::with_section(
font: asset_server.load("fonts/FiraSans-Bold.ttf"), format!("Cake eaten: {}", game.cake_eaten),
value: format!("Cake eaten: {}", game.cake_eaten), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
color: Color::rgb(0.5, 0.5, 1.0),
font_size: 80.0, font_size: 80.0,
..Default::default() color: Color::rgb(0.5, 0.5, 1.0),
}, },
}, Default::default(),
),
..Default::default() ..Default::default()
}); });
}); });

View File

@ -68,13 +68,25 @@ fn setup(
// scoreboard // scoreboard
.spawn(TextBundle { .spawn(TextBundle {
text: Text { text: Text {
font: asset_server.load("fonts/FiraSans-Bold.ttf"), sections: vec![
value: "Score:".to_string(), TextSection {
style: TextStyle { value: "Score: ".to_string(),
color: Color::rgb(0.5, 0.5, 1.0), style: TextStyle {
font_size: 40.0, font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..Default::default() font_size: 40.0,
}, color: Color::rgb(0.5, 0.5, 1.0),
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 40.0,
color: Color::rgb(1.0, 0.5, 0.5),
},
},
],
..Default::default()
}, },
style: Style { style: Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
@ -191,7 +203,7 @@ fn ball_movement_system(time: Res<Time>, mut ball_query: Query<(&Ball, &mut Tran
fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) { fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
for mut text in query.iter_mut() { for mut text in query.iter_mut() {
text.value = format!("Score: {}", scoreboard.score); text.sections[1].value = scoreboard.score.to_string();
} }
} }

View File

@ -102,15 +102,15 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
align_self: AlignSelf::FlexEnd, align_self: AlignSelf::FlexEnd,
..Default::default() ..Default::default()
}, },
text: Text { text: Text::with_section(
value: "Nothing to see in this window! Check the console output!".to_string(), "Nothing to see in this window! Check the console output!",
font: asset_server.load("fonts/FiraSans-Bold.ttf"), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0, font_size: 50.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, Default::default(),
),
..Default::default() ..Default::default()
}); });
} }

View File

@ -54,13 +54,41 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
.spawn(CameraUiBundle::default()) .spawn(CameraUiBundle::default())
.spawn(TextBundle { .spawn(TextBundle {
text: Text { text: Text {
font: asset_server.load("fonts/FiraSans-Bold.ttf"), sections: vec![
value: "Bird Count:".to_string(), TextSection {
style: TextStyle { value: "Bird Count: ".to_string(),
color: Color::rgb(0.0, 1.0, 0.0), style: TextStyle {
font_size: 40.0, font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..Default::default() font_size: 40.0,
}, color: Color::rgb(0.0, 1.0, 0.0),
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
},
},
TextSection {
value: "\nAverage FPS: ".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 0.0),
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
},
},
],
..Default::default()
}, },
style: Style { style: Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
@ -150,7 +178,8 @@ fn counter_system(
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) { if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() { if let Some(average) = fps.average() {
for mut text in query.iter_mut() { for mut text in query.iter_mut() {
text.value = format!("Bird Count: {}\nAverage FPS: {:.2}", counter.count, average); text.sections[1].value = format!("{}", counter.count);
text.sections[3].value = format!("{:.2}", average);
} }
} }
}; };

View File

@ -39,15 +39,15 @@ fn button_system(
let mut text = text_query.get_mut(children[0]).unwrap(); let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction { match *interaction {
Interaction::Clicked => { Interaction::Clicked => {
text.value = "Press".to_string(); text.sections[0].value = "Press".to_string();
*material = button_materials.pressed.clone(); *material = button_materials.pressed.clone();
} }
Interaction::Hovered => { Interaction::Hovered => {
text.value = "Hover".to_string(); text.sections[0].value = "Hover".to_string();
*material = button_materials.hovered.clone(); *material = button_materials.hovered.clone();
} }
Interaction::None => { Interaction::None => {
text.value = "Button".to_string(); text.sections[0].value = "Button".to_string();
*material = button_materials.normal.clone(); *material = button_materials.normal.clone();
} }
} }
@ -78,15 +78,15 @@ fn setup(
}) })
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle { parent.spawn(TextBundle {
text: Text { text: Text::with_section(
value: "Button".to_string(), "Button",
font: asset_server.load("fonts/FiraSans-Bold.ttf"), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
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() ..Default::default()
}); });
}); });

View File

@ -1,9 +1,11 @@
use bevy::{prelude::*, text::FontAtlasSet}; use bevy::{prelude::*, text::FontAtlasSet};
// TODO: This is now broken. See #1243
/// This example illustrates how FontAtlases are populated. Bevy uses FontAtlases under the hood to optimize text rendering. /// This example illustrates how FontAtlases are populated. Bevy uses FontAtlases under the hood to optimize text rendering.
fn main() { fn main() {
App::build() App::build()
.init_resource::<State>() .init_resource::<State>()
.add_resource(ClearColor(Color::BLACK))
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_startup_system(setup.system()) .add_startup_system(setup.system())
.add_system(text_update_system.system()) .add_system(text_update_system.system())
@ -65,8 +67,8 @@ fn text_update_system(mut state: ResMut<State>, time: Res<Time>, mut query: Quer
if state.timer.tick(time.delta_seconds()).finished() { if state.timer.tick(time.delta_seconds()).finished() {
for mut text in query.iter_mut() { for mut text in query.iter_mut() {
let c = rand::random::<u8>() as char; let c = rand::random::<u8>() as char;
if !text.value.contains(c) { if !text.sections[0].value.contains(c) {
text.value = format!("{}{}", text.value, c); text.sections[0].value.push(c);
} }
} }
@ -78,15 +80,15 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>, mut state: Res
let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf"); let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");
state.handle = font_handle.clone(); state.handle = font_handle.clone();
commands.spawn(CameraUiBundle::default()).spawn(TextBundle { commands.spawn(CameraUiBundle::default()).spawn(TextBundle {
text: Text { text: Text::with_section(
value: "a".to_string(), "a",
font: font_handle, TextStyle {
style: TextStyle { font: font_handle,
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::YELLOW,
..Default::default()
}, },
}, Default::default(),
),
..Default::default() ..Default::default()
}); });
} }

View File

@ -4,49 +4,113 @@ use bevy::{
}; };
/// This example illustrates how to create UI text and update it in a system. It displays the /// This example illustrates how to create UI text and update it in a system. It displays the
/// current FPS in the upper left hand corner. For text within a scene, please see the text2d example. /// current FPS in the top left corner, as well as text that changes colour in the bottom right.
/// For text within a scene, please see the text2d example.
fn main() { fn main() {
App::build() App::build()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugin(FrameTimeDiagnosticsPlugin::default()) .add_plugin(FrameTimeDiagnosticsPlugin::default())
.add_startup_system(setup.system()) .add_startup_system(setup.system())
.add_system(text_update_system.system()) .add_system(text_update_system.system())
.add_system(text_color_system.system())
.run(); .run();
} }
// A unit struct to help identify the FPS UI component, since there may be many Text components // A unit struct to help identify the FPS UI component, since there may be many Text components
struct FpsText; struct FpsText;
fn text_update_system(diagnostics: Res<Diagnostics>, mut query: Query<&mut Text, With<FpsText>>) { // A unit struct to help identify the color-changing Text component
for mut text in query.iter_mut() { struct ColorText;
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
text.value = format!("FPS: {:.2}", average);
}
}
}
}
fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) { fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
commands commands
// UI camera // UI camera
.spawn(CameraUiBundle::default()) .spawn(CameraUiBundle::default())
// texture // Text with one section
.spawn(TextBundle {
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()
},
// Use the `Text::with_section` constructor
text: Text::with_section(
// Accepts a `String` or any type that converts into a `String`, such as `&str`
"hello\nbevy!",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 100.0,
color: Color::WHITE,
},
// Note: You can use `Default::default()` in place of the `TextAlignment`
TextAlignment {
horizontal: HorizontalAlign::Center,
..Default::default()
},
),
..Default::default()
})
.with(ColorText)
// Rich text with multiple sections
.spawn(TextBundle { .spawn(TextBundle {
style: Style { style: Style {
align_self: AlignSelf::FlexEnd, align_self: AlignSelf::FlexEnd,
..Default::default() ..Default::default()
}, },
// Use `Text` directly
text: Text { text: Text {
value: "FPS:".to_string(), // Construct a `Vec` of `TextSection`s
font: asset_server.load("fonts/FiraSans-Bold.ttf"), sections: vec![
style: TextStyle { TextSection {
font_size: 60.0, value: "FPS: ".to_string(),
color: Color::WHITE, style: TextStyle {
..Default::default() font: asset_server.load("fonts/FiraSans-Bold.ttf"),
}, font_size: 60.0,
color: Color::WHITE,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 60.0,
color: Color::GOLD,
},
},
],
..Default::default()
}, },
..Default::default() ..Default::default()
}) })
.with(FpsText); .with(FpsText);
} }
fn text_update_system(diagnostics: Res<Diagnostics>, mut query: Query<&mut Text, With<FpsText>>) {
for mut text in query.iter_mut() {
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
// Update the value of the second section
text.sections[1].value = format!("{:.2}", average);
}
}
}
}
fn text_color_system(time: Res<Time>, mut query: Query<&mut Text, With<ColorText>>) {
for mut text in query.iter_mut() {
let seconds = time.seconds_since_startup() as f32;
// We used the `Text::with_section` helper method, but it is still just a `Text`,
// so to update it, we are still updating the one and only section
text.sections[0]
.style
.color
.set_r((1.25 * seconds).sin() / 2.0 + 0.5)
.set_g((0.75 * seconds).sin() / 2.0 + 0.5)
.set_b((0.50 * seconds).sin() / 2.0 + 0.5);
}
}

View File

@ -33,15 +33,15 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
}, },
..Default::default() ..Default::default()
}, },
text: Text { text: Text::with_section(
value: "This is\ntext with\nline breaks\nin the top left".to_string(), "This is\ntext with\nline breaks\nin the top left",
font: font.clone(), TextStyle {
style: TextStyle { font: font.clone(),
font_size: 50.0, font_size: 50.0,
color: Color::WHITE, color: Color::WHITE,
alignment: TextAlignment::default(),
}, },
}, Default::default(),
),
..Default::default() ..Default::default()
}); });
commands.spawn(TextBundle { commands.spawn(TextBundle {
@ -59,19 +59,18 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
}, },
..Default::default() ..Default::default()
}, },
text: Text { text: Text::with_section(
value: "This is very long text with limited width in the top right and is also pink" "This text is very long, has a limited width, is centred, is positioned in the top right and is also coloured pink.",
.to_string(), TextStyle {
font: font.clone(), font: font.clone(),
style: TextStyle { font_size: 50.0,
font_size: 50.0, color: Color::rgb(0.8, 0.2, 0.7),
color: Color::rgb(0.8, 0.2, 0.7),
alignment: TextAlignment {
horizontal: HorizontalAlign::Center,
vertical: VerticalAlign::Center,
}, },
TextAlignment {
horizontal: HorizontalAlign::Center,
vertical: VerticalAlign::Center,
}, },
}, ),
..Default::default() ..Default::default()
}); });
commands commands
@ -87,13 +86,57 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
..Default::default() ..Default::default()
}, },
text: Text { text: Text {
value: "This text changes in the bottom right".to_string(), sections: vec![
font: font.clone(), TextSection {
style: TextStyle { value: "This text changes in the bottom right".to_string(),
font_size: 30.0, style: TextStyle {
color: Color::WHITE, font: font.clone(),
alignment: TextAlignment::default(), font_size: 30.0,
}, color: Color::WHITE,
},
},
TextSection {
value: "\nThis text changes in the bottom right - ".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::RED,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::ORANGE_RED,
},
},
TextSection {
value: " fps, ".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::YELLOW,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::GREEN,
},
},
TextSection {
value: " ms/frame".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::BLUE,
},
},
],
alignment: Default::default(),
}, },
..Default::default() ..Default::default()
}) })
@ -113,16 +156,15 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
}, },
..Default::default() ..Default::default()
}, },
text: Text { text: Text::with_section(
value: "This\ntext has\nline breaks and also a set width in the bottom left" "This\ntext has\nline breaks and also a set width in the bottom left".to_string(),
.to_string(), TextStyle {
font, font,
style: TextStyle {
font_size: 50.0, font_size: 50.0,
color: Color::WHITE, color: Color::WHITE,
alignment: TextAlignment::default(),
}, },
}, Default::default(),
),
..Default::default() ..Default::default()
}); });
} }
@ -148,10 +190,14 @@ fn change_text_system(
} }
} }
text.value = format!( text.sections[0].value = format!(
"This text changes in the bottom right - {:.1} fps, {:.3} ms/frame", "This text changes in the bottom right - {:.1} fps, {:.3} ms/frame",
fps, fps,
frame_time * 1000.0, frame_time * 1000.0,
); );
text.sections[2].value = format!("{:.1}", fps);
text.sections[4].value = format!("{:.3}", frame_time * 1000.0);
} }
} }

View File

@ -57,15 +57,15 @@ fn setup(
margin: Rect::all(Val::Px(5.0)), margin: Rect::all(Val::Px(5.0)),
..Default::default() ..Default::default()
}, },
text: Text { text: Text::with_section(
value: "Text Example".to_string(), "Text Example",
font: asset_server.load("fonts/FiraSans-Bold.ttf"), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0, font_size: 30.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, Default::default(),
),
..Default::default() ..Default::default()
}); });
}); });

View File

@ -51,15 +51,15 @@ fn setup(
align_self: AlignSelf::FlexEnd, align_self: AlignSelf::FlexEnd,
..Default::default() ..Default::default()
}, },
text: Text { text: Text::with_section(
value: "Example text".to_string(), "Example text",
font: asset_server.load("fonts/FiraSans-Bold.ttf"), TextStyle {
style: TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0, font_size: 30.0,
color: Color::WHITE, color: Color::WHITE,
..Default::default()
}, },
}, Default::default(),
),
..Default::default() ..Default::default()
}); });
}); });