diff --git a/crates/bevy_text/src/draw.rs b/crates/bevy_text/src/draw.rs index e516f4890daff6..82d54c2f4fa38b 100644 --- a/crates/bevy_text/src/draw.rs +++ b/crates/bevy_text/src/draw.rs @@ -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, 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 diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index d225bf9aa5d8d3..0199ad6d810c53 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -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, + sections: &[SectionText], font_atlas_set_storage: &mut Assets, fonts: &Assets, texture_atlases: &mut Assets, @@ -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::, _>>()?; + 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 = handle.as_weak(); + let handle_font_atlas: Handle = 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")] diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 12043a9b9c09fa..d642e561436837 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -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}; } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 5fb980ab0c0124..87cbb488161219 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -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 { @@ -35,7 +36,7 @@ pub struct TextLayoutInfo { } impl TextPipeline { - pub fn get_or_insert_font_id(&mut self, handle: Handle, font: &Font) -> FontId { + pub fn get_or_insert_font_id(&mut self, handle: &Handle, font: &Font) -> FontId { let brush = &mut self.brush; *self .map_font_id @@ -51,30 +52,40 @@ impl TextPipeline { pub fn queue_text( &mut self, id: ID, - font_handle: Handle, fonts: &Assets, - text: &str, - font_size: f32, + sections: &[TextSection], + scale_factor: f64, text_alignment: TextAlignment, bounds: Size, font_atlas_set_storage: &mut Assets, texture_atlases: &mut Assets, textures: &mut Assets, ) -> Result<(), TextError> { - let font = fonts.get(font_handle.id).ok_or(TextError::NoSuchFont)?; - let font_id = self.get_or_insert_font_id(font_handle, font); - - let section = SectionText { - font_id, - scale: PxScale::from(font_size), - text, - }; - - let scaled_font = ab_glyph::Font::as_scaled(&font.font, font_size); + let 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: §ion.value, + }; + + Ok(section) + }) + .collect::, _>>()?; 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 TextPipeline { 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 TextPipeline { let glyphs = self.brush.process_glyphs( section_glyphs, + §ions, font_atlas_set_storage, fonts, texture_atlases, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 1469aa6b50c64f..ddf9be3c6665d1 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -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, + 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 = 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>( + 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, 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, + 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, diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index e1279f0cf07b6a..ad5bbebda4294a 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -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 } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index cf9cb2a52d9b2a..c1f2ab0f438d48 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -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(); diff --git a/examples/2d/contributors.rs b/examples/2d/contributors.rs index d110c7f542feab..851dac3d2b0e8a 100644 --- a/examples/2d/contributors.rs +++ b/examples/2d/contributors.rs @@ -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 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 } diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 33bd0d16c65110..4b3b6ab4f64ac6 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -13,18 +13,18 @@ fn setup(commands: &mut Commands, asset_server: Res) { // 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() }); } diff --git a/examples/ecs/state.rs b/examples/ecs/state.rs index 5d7c31d779eb05..770157aa19ec2a 100644 --- a/examples/ecs/state.rs +++ b/examples/ecs/state.rs @@ -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() }); }); diff --git a/examples/game/alien_cake_addict.rs b/examples/game/alien_cake_addict.rs index 9468fd714ff17b..34ec530dc5ce89 100644 --- a/examples/game/alien_cake_addict.rs +++ b/examples/game/alien_cake_addict.rs @@ -152,15 +152,15 @@ fn setup(commands: &mut Commands, asset_server: Res, 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, time: Res