Skip to content

Commit

Permalink
Rich text (bevyengine#1245)
Browse files Browse the repository at this point in the history
Rich text support (different fonts / styles within the same text section)
  • Loading branch information
tigregalis authored and rparrett committed Jan 27, 2021
1 parent 9ddcc83 commit d2dc8e8
Show file tree
Hide file tree
Showing 20 changed files with 482 additions and 228 deletions.
40 changes: 3 additions & 37 deletions crates/bevy_text/src/draw.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use bevy_math::{Mat4, Vec3};
use bevy_render::{
color::Color,
draw::{Draw, DrawContext, DrawError, Drawable},
mesh,
mesh::Mesh,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
38 changes: 26 additions & 12 deletions crates/bevy_text/src/glyph_brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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>,
Expand All @@ -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);
}
Expand All @@ -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)
Expand All @@ -107,6 +119,7 @@ impl GlyphBrush {
positioned_glyphs.push(PositionedGlyph {
position,
atlas_info,
section_index: sg.section_index,
});
}
}
Expand All @@ -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")]
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}

Expand Down
49 changes: 31 additions & 18 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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
Expand All @@ -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 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(&section.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: &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(
Expand All @@ -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));
Expand All @@ -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,
Expand Down
95 changes: 93 additions & 2 deletions crates/bevy_text/src/text.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading

0 comments on commit d2dc8e8

Please sign in to comment.