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_render::{
color::Color,
draw::{Draw, DrawContext, DrawError, Drawable},
mesh,
mesh::Mesh,
@ -9,47 +8,14 @@ use bevy_render::{
renderer::{BindGroup, RenderResourceBindings, RenderResourceId},
};
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 {
fn default() -> Self {
Self {
color: Color::WHITE,
font_size: 12.0,
alignment: TextAlignment::default(),
}
}
}
use crate::{PositionedGlyph, TextSection};
pub struct DrawableText<'a> {
pub render_resource_bindings: &'a mut RenderResourceBindings,
pub position: Vec3,
pub scale_factor: f32,
pub style: &'a TextStyle,
pub sections: &'a [TextSection],
pub text_glyphs: &'a Vec<PositionedGlyph>,
pub msaa: &'a Msaa,
pub font_quad_vertex_descriptor: &'a VertexBufferDescriptor,
@ -103,7 +69,7 @@ impl<'a> Drawable for DrawableText<'a> {
let sprite = TextureAtlasSprite {
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

View File

@ -4,7 +4,7 @@ use bevy_math::{Size, Vec2};
use bevy_render::prelude::Texture;
use bevy_sprite::TextureAtlas;
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};
@ -46,6 +46,7 @@ impl GlyphBrush {
pub fn process_glyphs(
&self,
glyphs: Vec<SectionGlyph>,
sections: &[SectionText],
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
fonts: &Assets<Font>,
texture_atlases: &mut Assets<TextureAtlas>,
@ -55,16 +56,26 @@ impl GlyphBrush {
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 sections_data = sections
.iter()
.map(|section| {
let handle = &self.handles[section.font_id.0];
let font = fonts.get(handle).ok_or(TextError::NoSuchFont)?;
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 min_x = std::f32::MAX;
for section_glyph in glyphs.iter() {
let glyph = &section_glyph.glyph;
for sg in glyphs.iter() {
let glyph = &sg.glyph;
let scaled_font = sections_data[sg.section_index].3;
max_y = max_y.max(glyph.position.y - scaled_font.descent());
min_x = min_x.min(glyph.position.x);
}
@ -82,14 +93,15 @@ impl GlyphBrush {
let glyph_id = glyph.id;
let glyph_position = glyph.position;
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 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
.get_or_insert_with(handle_font_atlas, FontAtlasSet::default);
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)
.unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph)
@ -107,6 +119,7 @@ impl GlyphBrush {
positioned_glyphs.push(PositionedGlyph {
position,
atlas_info,
section_index: sg.section_index,
});
}
}
@ -126,6 +139,7 @@ impl GlyphBrush {
pub struct PositionedGlyph {
pub position: Vec2,
pub atlas_info: GlyphAtlasInfo,
pub section_index: usize,
}
#[cfg(feature = "subpixel_glyph_atlas")]

View File

@ -21,7 +21,7 @@ pub use text::*;
pub use text2d::*;
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};
}

View File

@ -10,7 +10,8 @@ use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, SectionText};
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> {
@ -35,7 +36,7 @@ pub struct TextLayoutInfo {
}
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;
*self
.map_font_id
@ -51,30 +52,40 @@ impl<ID: Hash + Eq> TextPipeline<ID> {
pub fn queue_text(
&mut self,
id: ID,
font_handle: Handle<Font>,
fonts: &Assets<Font>,
text: &str,
font_size: f32,
sections: &[TextSection],
scale_factor: f64,
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 mut scaled_fonts = Vec::new();
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 {
font_id,
scale: PxScale::from(font_size),
text,
};
scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size));
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
.brush
.compute_glyphs(&[section], bounds, text_alignment)?;
.compute_glyphs(&sections, bounds, text_alignment)?;
if section_glyphs.is_empty() {
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_y: f32 = std::f32::MIN;
for section_glyph in section_glyphs.iter() {
let glyph = &section_glyph.glyph;
for sg in section_glyphs.iter() {
let scaled_font = scaled_fonts[sg.section_index];
let glyph = &sg.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));
@ -104,6 +116,7 @@ impl<ID: Hash + Eq> TextPipeline<ID> {
let glyphs = self.brush.process_glyphs(
section_glyphs,
&sections,
font_atlas_set_storage,
fonts,
texture_atlases,

View File

@ -1,15 +1,106 @@
use bevy_asset::Handle;
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)]
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 font: Handle<Font>,
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)]
pub struct CalculatedSize {
pub size: Size,

View File

@ -90,12 +90,12 @@ pub fn draw_text2d_system(
if let Some(text_glyphs) = text_pipeline.get_glyphs(&entity) {
let position = global_transform.translation
+ match text.style.alignment.vertical {
+ match text.alignment.vertical {
VerticalAlign::Top => Vec3::zero(),
VerticalAlign::Center => Vec3::new(0.0, -height * 0.5, 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::Center => Vec3::new(-width * 0.5, 0.0, 0.0),
HorizontalAlign::Right => Vec3::zero(),
@ -108,7 +108,7 @@ pub fn draw_text2d_system(
text_glyphs: &text_glyphs.glyphs,
font_quad_vertex_descriptor: &vertex_buffer_descriptor,
scale_factor,
style: &text.style,
sections: &text.sections,
};
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) {
match text_pipeline.queue_text(
entity,
text.font.clone(),
&fonts,
&text.value,
scale_value(text.style.font_size, scale_factor),
text.style.alignment,
&text.sections,
scale_factor,
text.alignment,
Size::new(f32::MAX, f32::MAX),
&mut *font_atlas_set_storage,
&mut *texture_atlases,
@ -191,6 +190,6 @@ pub fn text2d_system(
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
}

View File

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

View File

@ -40,8 +40,8 @@ struct Velocity {
const GRAVITY: f32 = -9.821 * 100.0;
const SPRITE_SIZE: f32 = 75.0;
const COL_DESELECTED: Color = Color::rgb_linear(0.03, 0.03, 0.03);
const COL_SELECTED: Color = Color::rgb_linear(5.0, 5.0, 5.0);
const COL_DESELECTED: Color = Color::rgba_linear(0.03, 0.03, 0.03, 0.92);
const COL_SELECTED: Color = Color::WHITE;
const SHOWCASE_TIMER_SECS: f32 = 3.0;
@ -113,13 +113,25 @@ fn setup(
..Default::default()
},
text: Text {
value: "Contributor showcase".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
font_size: 60.0,
color: Color::WHITE,
..Default::default()
},
sections: vec![
TextSection {
value: "Contributor showcase".to_string(),
style: TextStyle {
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/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
},
},
],
..Default::default()
},
..Default::default()
});
@ -195,7 +207,9 @@ fn select(
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(())
}
@ -312,9 +326,14 @@ fn contributors() -> Contributors {
/// Because there is no `Mul<Color> for Color` instead `[f32; 3]` is
/// used.
fn gen_color(rng: &mut impl Rng) -> [f32; 3] {
let r = rng.gen_range(0.2..1.0);
let g = rng.gen_range(0.2..1.0);
let b = rng.gen_range(0.2..1.0);
let v = Vec3::new(r, g, b);
v.normalize().into()
loop {
let rgb = rng.gen();
if luminance(rgb) >= 0.6 {
break rgb;
}
}
}
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
.spawn(Camera2dBundle::default())
.spawn(Text2dBundle {
text: Text {
value: "This text is in the 2D scene.".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
text: Text::with_section(
"This text is in the 2D scene.",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
},
},
TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
),
..Default::default()
});
}

View File

@ -52,15 +52,15 @@ fn setup_menu(
})
.with_children(|parent| {
parent.spawn(TextBundle {
text: Text {
value: "Play".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
text: Text::with_section(
"Play",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
..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
commands.spawn(TextBundle {
text: Text {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
value: "Score:".to_string(),
style: TextStyle {
color: Color::rgb(0.5, 0.5, 1.0),
text: Text::with_section(
"Score:",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
..Default::default()
color: Color::rgb(0.5, 0.5, 1.0),
},
},
Default::default(),
),
style: Style {
position_type: PositionType::Absolute,
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
fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) {
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| {
parent.spawn(TextBundle {
text: Text {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
value: format!("Cake eaten: {}", game.cake_eaten),
style: TextStyle {
color: Color::rgb(0.5, 0.5, 1.0),
text: Text::with_section(
format!("Cake eaten: {}", game.cake_eaten),
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 80.0,
..Default::default()
color: Color::rgb(0.5, 0.5, 1.0),
},
},
Default::default(),
),
..Default::default()
});
});

View File

@ -68,13 +68,25 @@ fn setup(
// scoreboard
.spawn(TextBundle {
text: Text {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
value: "Score:".to_string(),
style: TextStyle {
color: Color::rgb(0.5, 0.5, 1.0),
font_size: 40.0,
..Default::default()
},
sections: vec![
TextSection {
value: "Score: ".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
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 {
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>) {
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,
..Default::default()
},
text: Text {
value: "Nothing to see in this window! Check the console output!".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
text: Text::with_section(
"Nothing to see in this window! Check the console output!",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::WHITE,
..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(TextBundle {
text: Text {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
value: "Bird Count:".to_string(),
style: TextStyle {
color: Color::rgb(0.0, 1.0, 0.0),
font_size: 40.0,
..Default::default()
},
sections: vec![
TextSection {
value: "Bird Count: ".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),
},
},
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 {
position_type: PositionType::Absolute,
@ -150,7 +178,8 @@ fn counter_system(
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
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();
match *interaction {
Interaction::Clicked => {
text.value = "Press".to_string();
text.sections[0].value = "Press".to_string();
*material = button_materials.pressed.clone();
}
Interaction::Hovered => {
text.value = "Hover".to_string();
text.sections[0].value = "Hover".to_string();
*material = button_materials.hovered.clone();
}
Interaction::None => {
text.value = "Button".to_string();
text.sections[0].value = "Button".to_string();
*material = button_materials.normal.clone();
}
}
@ -78,15 +78,15 @@ fn setup(
})
.with_children(|parent| {
parent.spawn(TextBundle {
text: Text {
value: "Button".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
text: Text::with_section(
"Button",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
..Default::default()
},
},
Default::default(),
),
..Default::default()
});
});

View File

@ -1,9 +1,11 @@
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.
fn main() {
App::build()
.init_resource::<State>()
.add_resource(ClearColor(Color::BLACK))
.add_plugins(DefaultPlugins)
.add_startup_system(setup.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() {
for mut text in query.iter_mut() {
let c = rand::random::<u8>() as char;
if !text.value.contains(c) {
text.value = format!("{}{}", text.value, c);
if !text.sections[0].value.contains(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");
state.handle = font_handle.clone();
commands.spawn(CameraUiBundle::default()).spawn(TextBundle {
text: Text {
value: "a".to_string(),
font: font_handle,
style: TextStyle {
text: Text::with_section(
"a",
TextStyle {
font: font_handle,
font_size: 60.0,
color: Color::WHITE,
..Default::default()
color: Color::YELLOW,
},
},
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
/// 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() {
App::build()
.add_plugins(DefaultPlugins)
.add_plugin(FrameTimeDiagnosticsPlugin::default())
.add_startup_system(setup.system())
.add_system(text_update_system.system())
.add_system(text_color_system.system())
.run();
}
// A unit struct to help identify the FPS UI component, since there may be many Text components
struct 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() {
text.value = format!("FPS: {:.2}", average);
}
}
}
}
// A unit struct to help identify the color-changing Text component
struct ColorText;
fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
commands
// UI camera
.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 {
style: Style {
align_self: AlignSelf::FlexEnd,
..Default::default()
},
// Use `Text` directly
text: Text {
value: "FPS:".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
font_size: 60.0,
color: Color::WHITE,
..Default::default()
},
// Construct a `Vec` of `TextSection`s
sections: vec![
TextSection {
value: "FPS: ".to_string(),
style: TextStyle {
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()
})
.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()
},
text: Text {
value: "This is\ntext with\nline breaks\nin the top left".to_string(),
font: font.clone(),
style: TextStyle {
text: Text::with_section(
"This is\ntext with\nline breaks\nin the top left",
TextStyle {
font: font.clone(),
font_size: 50.0,
color: Color::WHITE,
alignment: TextAlignment::default(),
},
},
Default::default(),
),
..Default::default()
});
commands.spawn(TextBundle {
@ -59,19 +59,18 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
},
..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,
text: Text::with_section(
"This text is very long, has a limited width, is centred, is positioned in the top right and is also coloured pink.",
TextStyle {
font: font.clone(),
font_size: 50.0,
color: Color::rgb(0.8, 0.2, 0.7),
},
TextAlignment {
horizontal: HorizontalAlign::Center,
vertical: VerticalAlign::Center,
},
},
),
..Default::default()
});
commands
@ -87,13 +86,57 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
..Default::default()
},
text: Text {
value: "This text changes in the bottom right".to_string(),
font: font.clone(),
style: TextStyle {
font_size: 30.0,
color: Color::WHITE,
alignment: TextAlignment::default(),
},
sections: vec![
TextSection {
value: "This text changes in the bottom right".to_string(),
style: TextStyle {
font: font.clone(),
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()
})
@ -113,16 +156,15 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
},
..Default::default()
},
text: Text {
value: "This\ntext has\nline breaks and also a set width in the bottom left"
.to_string(),
font,
style: TextStyle {
text: Text::with_section(
"This\ntext has\nline breaks and also a set width in the bottom left".to_string(),
TextStyle {
font,
font_size: 50.0,
color: Color::WHITE,
alignment: TextAlignment::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",
fps,
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)),
..Default::default()
},
text: Text {
value: "Text Example".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
text: Text::with_section(
"Text Example",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::WHITE,
..Default::default()
},
},
Default::default(),
),
..Default::default()
});
});

View File

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