Skip to content

Commit

Permalink
Cosmic text (bevyengine#10193)
Browse files Browse the repository at this point in the history
# Replace ab_glyph with the more capable cosmic-text

Fixes bevyengine#7616.

Cosmic-text is a more mature text-rendering library that handles scripts
and ligatures better than ab_glyph, it can also handle system fonts
which can be implemented in bevy in the future

Rebase of bevyengine#8808

## Changelog

Replaces text renderer ab_glyph with cosmic-text

The definition of the font size has changed with the migration to cosmic
text. The behavior is now consistent with other platforms (e.g. the
web), where the font size in pixels measures the height of the font (the
distance between the top of the highest ascender and the bottom of the
lowest descender). Font sizes in your app need to be rescaled to
approximately 1.2x smaller; for example, if you were using a font size
of 60.0, you should now use a font size of 50.0.

## Migration guide

- `Text2dBounds` has been replaced with `TextBounds`, and it now accepts
`Option`s to the bounds, instead of using `f32::INFINITY` to inidicate
lack of bounds
- Textsizes should be changed, dividing the current size with 1.2 will
result in the same size as before.
- `TextSettings` struct is removed
- Feature `subpixel_alignment` has been removed since cosmic-text
already does this automatically
- TextBundles and things rendering texts requires the `CosmicBuffer`
Component on them as well

## Suggested followups:

- TextPipeline: reconstruct byte indices for keeping track of eventual
cursors in text input
- TextPipeline: (future work) split text entities into section entities
- TextPipeline: (future work) text editing
- Support line height as an option. Unitless `1.2` is the default used
in browsers (1.2x font size).
- Support System Fonts and font families
- Example showing of animated text styles. Eg. throbbing hyperlinks

---------

Co-authored-by: tigregalis <[email protected]>
Co-authored-by: Nico Burns <[email protected]>
Co-authored-by: sam edelsten <[email protected]>
Co-authored-by: Dimchikkk <[email protected]>
Co-authored-by: Alice Cecile <[email protected]>
Co-authored-by: Rob Parrett <[email protected]>
  • Loading branch information
7 people authored and MrGVSV committed Jul 5, 2024
1 parent dc872cf commit 62b94f7
Show file tree
Hide file tree
Showing 33 changed files with 1,079 additions and 851 deletions.
3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,6 @@ wayland = ["bevy_internal/wayland"]
# X11 display server support
x11 = ["bevy_internal/x11"]

# Enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_internal/subpixel_glyph_atlas"]

# Enable systems that allow for automated testing on CI
bevy_ci_testing = ["bevy_internal/bevy_ci_testing"]

Expand Down
3 changes: 0 additions & 3 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,6 @@ async-io = ["bevy_tasks/async-io"]
wayland = ["bevy_winit/wayland"]
x11 = ["bevy_winit/x11"]

# enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]

# Transmission textures in `StandardMaterial`:
pbr_transmission_textures = [
"bevy_pbr?/pbr_transmission_textures",
Expand Down
9 changes: 5 additions & 4 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,14 +375,15 @@ pub fn extract_sprites(
.map(|e| (commands.spawn_empty().id(), e)),
);
} else {
let atlas_rect = sheet.and_then(|s| s.texture_rect(&texture_atlases));
let atlas_rect =
sheet.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));
let rect = match (atlas_rect, sprite.rect) {
(None, None) => None,
(None, Some(sprite_rect)) => Some(sprite_rect),
(Some(atlas_rect), None) => Some(atlas_rect.as_rect()),
(Some(atlas_rect), None) => Some(atlas_rect),
(Some(atlas_rect), Some(mut sprite_rect)) => {
sprite_rect.min += atlas_rect.min.as_vec2();
sprite_rect.max += atlas_rect.min.as_vec2();
sprite_rect.min += atlas_rect.min;
sprite_rect.max += atlas_rect.min;

Some(sprite_rect)
}
Expand Down
40 changes: 20 additions & 20 deletions crates/bevy_sprite/src/texture_atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,6 @@ pub struct TextureAtlasLayout {
pub(crate) texture_handles: Option<HashMap<AssetId<Image>, usize>>,
}

/// Component used to draw a specific section of a texture.
///
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
/// image file for either sprite animation or global mapping.
/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
/// for efficient rendering of related game objects.
///
/// Check the following examples for usage:
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Component, Default, Debug, Clone, Reflect)]
pub struct TextureAtlas {
/// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>,
/// Texture atlas section index
pub index: usize,
}

impl TextureAtlasLayout {
/// Create a new empty layout with custom `dimensions`
pub fn new_empty(dimensions: UVec2) -> Self {
Expand Down Expand Up @@ -149,6 +129,26 @@ impl TextureAtlasLayout {
}
}

/// Component used to draw a specific section of a texture.
///
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
/// image file for either sprite animation or global mapping.
/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
/// for efficient rendering of related game objects.
///
/// Check the following examples for usage:
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Component, Default, Debug, Clone, Reflect)]
pub struct TextureAtlas {
/// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>,
/// Texture atlas section index
pub index: usize,
}

impl TextureAtlas {
/// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index`
pub fn texture_rect(&self, texture_atlases: &Assets<TextureAtlasLayout>) -> Option<URect> {
Expand Down
7 changes: 4 additions & 3 deletions crates/bevy_text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[features]
subpixel_glyph_atlas = []
default_font = []

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
Expand All @@ -29,10 +29,11 @@ bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }

# other
ab_glyph = "0.2.6"
glyph_brush_layout = "0.2.1"
cosmic-text = "0.12"
thiserror = "1.0"
serde = { version = "1", features = ["derive"] }
unicode-bidi = "0.3.13"
sys-locale = "0.3.0"

[dev-dependencies]
approx = "0.5.1"
Expand Down
70 changes: 70 additions & 0 deletions crates/bevy_text/src/bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::Vec2;
use bevy_reflect::Reflect;

/// The maximum width and height of text. The text will wrap according to the specified size.
/// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the
/// specified [`JustifyText`](crate::text::JustifyText).
///
/// Note: only characters that are completely out of the bounds will be truncated, so this is not a
/// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this
/// component is mainly useful for text wrapping only.
#[derive(Component, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct TextBounds {
/// The maximum width of text in logical pixels.
/// If `None`, the width is unbounded.
pub width: Option<f32>,
/// The maximum height of text in logical pixels.
/// If `None`, the height is unbounded.
pub height: Option<f32>,
}

impl Default for TextBounds {
#[inline]
fn default() -> Self {
Self::UNBOUNDED
}
}

impl TextBounds {
/// Unbounded text will not be truncated or wrapped.
pub const UNBOUNDED: Self = Self {
width: None,
height: None,
};

/// Creates a new `TextBounds`, bounded with the specified width and height values.
#[inline]
pub const fn new(width: f32, height: f32) -> Self {
Self {
width: Some(width),
height: Some(height),
}
}

/// Creates a new `TextBounds`, bounded with the specified width value and unbounded on height.
#[inline]
pub const fn new_horizontal(width: f32) -> Self {
Self {
width: Some(width),
height: None,
}
}

/// Creates a new `TextBounds`, bounded with the specified height value and unbounded on width.
#[inline]
pub const fn new_vertical(height: f32) -> Self {
Self {
width: None,
height: Some(height),
}
}
}

impl From<Vec2> for TextBounds {
#[inline]
fn from(v: Vec2) -> Self {
Self::new(v.x, v.y)
}
}
11 changes: 9 additions & 2 deletions crates/bevy_text/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use ab_glyph::GlyphId;
use cosmic_text::CacheKey;
use thiserror::Error;

#[derive(Debug, PartialEq, Eq, Error)]
/// Errors related to the textsystem
pub enum TextError {
/// Font was not found, this could be that the font has not yet been loaded, or
/// that the font failed to load for some other reason
#[error("font not found")]
NoSuchFont,
/// Failed to add glyph to a newly created atlas for some reason
#[error("failed to add glyph to newly-created atlas {0:?}")]
FailedToAddGlyph(GlyphId),
FailedToAddGlyph(u16),
/// Failed to get scaled glyph image for cache key
#[error("failed to get scaled glyph image for cache key: {0:?}")]
FailedToGetGlyphImage(CacheKey),
}
70 changes: 26 additions & 44 deletions crates/bevy_text/src/font.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,35 @@
use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph};
use std::sync::Arc;

use bevy_asset::Asset;
use bevy_reflect::TypePath;
use bevy_render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::Image,
};

#[derive(Asset, TypePath, Debug, Clone)]
/// An [`Asset`] that contains the data for a loaded font, if loaded as an asset.
///
/// Loaded by [`FontLoader`](crate::FontLoader).
///
/// # A note on fonts
///
/// `Font` may differ from the everyday notion of what a "font" is.
/// A font *face* (e.g. Fira Sans Semibold Italic) is part of a font *family* (e.g. Fira Sans),
/// and is distinguished from other font faces in the same family
/// by its style (e.g. italic), its weight (e.g. bold) and its stretch (e.g. condensed).
///
/// Bevy currently loads a single font face as a single `Font` asset.
#[derive(Debug, TypePath, Clone, Asset)]
pub struct Font {
pub font: FontArc,
/// Content of a font file as bytes
pub data: Arc<Vec<u8>>,
}

impl Font {
pub fn try_from_bytes(font_data: Vec<u8>) -> Result<Self, InvalidFont> {
let font = FontVec::try_from_vec(font_data)?;
let font = FontArc::new(font);
Ok(Font { font })
}

pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image {
let bounds = outlined_glyph.px_bounds();
// Increase the length of the glyph texture by 2-pixels on each axis to make space
// for a pixel wide transparent border along its edges.
let width = bounds.width() as usize + 2;
let height = bounds.height() as usize + 2;
let mut alpha = vec![0.0; width * height];
outlined_glyph.draw(|x, y, v| {
// Displace the glyph by 1 pixel on each axis so that it is drawn in the center of the texture.
// This leaves a pixel wide transparent border around the glyph.
alpha[(y + 1) as usize * width + x as usize + 1] = v;
});

// TODO: make this texture grayscale
Image::new(
Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
alpha
.iter()
.flat_map(|a| vec![255, 255, 255, (*a * 255.0) as u8])
.collect::<Vec<u8>>(),
TextureFormat::Rgba8UnormSrgb,
// This glyph image never needs to reach the render world because it's placed
// into a font texture atlas that'll be used for rendering.
RenderAssetUsages::MAIN_WORLD,
)
/// Creates a [`Font`] from bytes
pub fn try_from_bytes(
font_data: Vec<u8>,
) -> Result<Self, cosmic_text::ttf_parser::FaceParsingError> {
use cosmic_text::ttf_parser;
ttf_parser::Face::parse(&font_data, 0)?;
Ok(Self {
data: Arc::new(font_data),
})
}
}
Loading

0 comments on commit 62b94f7

Please sign in to comment.