Rich text (#1245)
Rich text support (different fonts / styles within the same text section)
This commit is contained in:
parent
3d0c4e380c
commit
40b5bbd028
@ -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
|
||||
|
@ -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 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 = first_glyph.glyph.scale.y;
|
||||
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 min_x = std::f32::MAX;
|
||||
for section_glyph in glyphs.iter() {
|
||||
let glyph = §ion_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")]
|
||||
|
@ -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};
|
||||
}
|
||||
|
||||
|
@ -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(§ion.style.font, font);
|
||||
let font_size = scale_value(section.style.font_size, scale_factor);
|
||||
|
||||
scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size));
|
||||
|
||||
let section = SectionText {
|
||||
font_id,
|
||||
scale: PxScale::from(font_size),
|
||||
text,
|
||||
text: §ion.value,
|
||||
};
|
||||
|
||||
let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size);
|
||||
Ok(section)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let section_glyphs = self
|
||||
.brush
|
||||
.compute_glyphs(&[section], bounds, text_alignment)?;
|
||||
.compute_glyphs(§ions, 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 = §ion_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,
|
||||
§ions,
|
||||
font_atlas_set_storage,
|
||||
fonts,
|
||||
texture_atlases,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,14 +113,26 @@ fn setup(
|
||||
..Default::default()
|
||||
},
|
||||
text: Text {
|
||||
sections: vec![
|
||||
TextSection {
|
||||
value: "Contributor showcase".to_string(),
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 60.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
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
|
||||
}
|
||||
|
@ -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(),
|
||||
text: Text::with_section(
|
||||
"This text is in the 2D scene.",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font_size: 60.0,
|
||||
color: Color::WHITE,
|
||||
alignment: TextAlignment {
|
||||
},
|
||||
TextAlignment {
|
||||
vertical: VerticalAlign::Center,
|
||||
horizontal: HorizontalAlign::Center,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
@ -52,15 +52,15 @@ fn setup_menu(
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle {
|
||||
text: Text {
|
||||
value: "Play".to_string(),
|
||||
text: Text::with_section(
|
||||
"Play",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::rgb(0.9, 0.9, 0.9),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
@ -152,15 +152,15 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>, mut game: ResM
|
||||
|
||||
// scoreboard
|
||||
commands.spawn(TextBundle {
|
||||
text: Text {
|
||||
text: Text::with_section(
|
||||
"Score:",
|
||||
TextStyle {
|
||||
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()
|
||||
},
|
||||
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 {
|
||||
text: Text::with_section(
|
||||
format!("Cake eaten: {}", game.cake_eaten),
|
||||
TextStyle {
|
||||
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),
|
||||
font_size: 80.0,
|
||||
..Default::default()
|
||||
},
|
||||
color: Color::rgb(0.5, 0.5, 1.0),
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
@ -68,14 +68,26 @@ fn setup(
|
||||
// scoreboard
|
||||
.spawn(TextBundle {
|
||||
text: Text {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
sections: vec![
|
||||
TextSection {
|
||||
value: "Score: ".to_string(),
|
||||
style: TextStyle {
|
||||
color: Color::rgb(0.5, 0.5, 1.0),
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 40.0,
|
||||
..Default::default()
|
||||
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,
|
||||
position: Rect {
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
text: Text::with_section(
|
||||
"Nothing to see in this window! Check the console output!",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font_size: 50.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
@ -54,14 +54,42 @@ fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
|
||||
.spawn(CameraUiBundle::default())
|
||||
.spawn(TextBundle {
|
||||
text: Text {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
sections: vec![
|
||||
TextSection {
|
||||
value: "Bird Count: ".to_string(),
|
||||
style: TextStyle {
|
||||
color: Color::rgb(0.0, 1.0, 0.0),
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 40.0,
|
||||
..Default::default()
|
||||
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,
|
||||
position: Rect {
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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(),
|
||||
text: Text::with_section(
|
||||
"Button",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::rgb(0.9, 0.9, 0.9),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
@ -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(),
|
||||
text: Text::with_section(
|
||||
"a",
|
||||
TextStyle {
|
||||
font: font_handle,
|
||||
style: TextStyle {
|
||||
font_size: 60.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
color: Color::YELLOW,
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
@ -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 {
|
||||
// Construct a `Vec` of `TextSection`s
|
||||
sections: vec![
|
||||
TextSection {
|
||||
value: "FPS: ".to_string(),
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 60.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
text: Text::with_section(
|
||||
"This is\ntext with\nline breaks\nin the top left",
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
style: TextStyle {
|
||||
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(),
|
||||
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(),
|
||||
style: TextStyle {
|
||||
font_size: 50.0,
|
||||
color: Color::rgb(0.8, 0.2, 0.7),
|
||||
alignment: TextAlignment {
|
||||
},
|
||||
TextAlignment {
|
||||
horizontal: HorizontalAlign::Center,
|
||||
vertical: VerticalAlign::Center,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
commands
|
||||
@ -87,14 +86,58 @@ fn infotext_system(commands: &mut Commands, asset_server: Res<AssetServer>) {
|
||||
..Default::default()
|
||||
},
|
||||
text: Text {
|
||||
sections: vec![
|
||||
TextSection {
|
||||
value: "This text changes in the bottom right".to_string(),
|
||||
font: font.clone(),
|
||||
style: TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: 30.0,
|
||||
color: Color::WHITE,
|
||||
alignment: TextAlignment::default(),
|
||||
},
|
||||
},
|
||||
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()
|
||||
})
|
||||
.with(TextChanges);
|
||||
@ -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(),
|
||||
text: Text::with_section(
|
||||
"This\ntext has\nline breaks and also a set width in the bottom left".to_string(),
|
||||
TextStyle {
|
||||
font,
|
||||
style: TextStyle {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -57,15 +57,15 @@ fn setup(
|
||||
margin: Rect::all(Val::Px(5.0)),
|
||||
..Default::default()
|
||||
},
|
||||
text: Text {
|
||||
value: "Text Example".to_string(),
|
||||
text: Text::with_section(
|
||||
"Text Example",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font_size: 30.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
@ -51,15 +51,15 @@ fn setup(
|
||||
align_self: AlignSelf::FlexEnd,
|
||||
..Default::default()
|
||||
},
|
||||
text: Text {
|
||||
value: "Example text".to_string(),
|
||||
text: Text::with_section(
|
||||
"Example text",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
style: TextStyle {
|
||||
font_size: 30.0,
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user