diff --git a/Cargo.toml b/Cargo.toml index 31ad40696b4b4..b8d70f9cbe7e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -266,9 +266,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"] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 533b96b8a06ad..470615066cded 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -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", diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 0e7efc1f75c23..d60efb0977eb2 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -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) } diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_sprite/src/texture_atlas.rs index 9af933a43a2cc..ea3c67b640660 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -31,26 +31,6 @@ pub struct TextureAtlasLayout { pub(crate) texture_handles: Option, 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, - /// Texture atlas section index - pub index: usize, -} - impl TextureAtlasLayout { /// Create a new empty layout with custom `dimensions` pub fn new_empty(dimensions: UVec2) -> Self { @@ -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, + /// 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) -> Option { diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index e6a693daa3b2b..c5d09c09d6d9f 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -subpixel_glyph_atlas = [] default_font = [] [dependencies] @@ -29,10 +28,11 @@ bevy_window = { path = "../bevy_window", version = "0.14.1" } bevy_utils = { path = "../bevy_utils", version = "0.14.1" } # 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" diff --git a/crates/bevy_text/src/bounds.rs b/crates/bevy_text/src/bounds.rs new file mode 100644 index 0000000000000..57b8d95f1a5d0 --- /dev/null +++ b/crates/bevy_text/src/bounds.rs @@ -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, + /// The maximum height of text in logical pixels. + /// If `None`, the height is unbounded. + pub height: Option, +} + +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 for TextBounds { + #[inline] + fn from(v: Vec2) -> Self { + Self::new(v.x, v.y) + } +} diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 1bb7cf1253581..ef9f7ea590deb 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -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), } diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 780995aa299aa..d24d89eea337d 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -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>, } impl Font { - pub fn try_from_bytes(font_data: Vec) -> Result { - 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::>(), - 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, + ) -> Result { + use cosmic_text::ttf_parser; + ttf_parser::Face::parse(&font_data, 0)?; + Ok(Self { + data: Arc::new(font_data), + }) } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index f6dd47d5a6bcd..aa426c67e5550 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,6 +1,5 @@ -use ab_glyph::{GlyphId, Point}; use bevy_asset::{Assets, Handle}; -use bevy_math::UVec2; +use bevy_math::{IVec2, UVec2}; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, @@ -9,48 +8,36 @@ use bevy_render::{ use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlasLayout}; use bevy_utils::HashMap; -#[cfg(feature = "subpixel_glyph_atlas")] -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] -pub struct SubpixelOffset { - x: u16, - y: u16, -} - -#[cfg(feature = "subpixel_glyph_atlas")] -impl From for SubpixelOffset { - fn from(p: Point) -> Self { - fn f(v: f32) -> u16 { - ((v % 1.) * (u16::MAX as f32)) as u16 - } - Self { - x: f(p.x), - y: f(p.y), - } - } -} - -#[cfg(not(feature = "subpixel_glyph_atlas"))] -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] -pub struct SubpixelOffset; - -#[cfg(not(feature = "subpixel_glyph_atlas"))] -impl From for SubpixelOffset { - fn from(_: Point) -> Self { - Self - } -} +use crate::{GlyphAtlasLocation, TextError}; +/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`. +/// +/// A `FontAtlas` contains one or more textures, each of which contains one or more glyphs packed into them. +/// +/// A [`FontAtlasSet`](crate::FontAtlasSet) contains a `FontAtlas` for each font size in the same font face. +/// +/// For the same font face and font size, a glyph will be rasterized differently for different subpixel offsets. +/// In practice, ranges of subpixel offsets are grouped into subpixel bins to limit the number of rasterized glyphs, +/// providing a trade-off between visual quality and performance. +/// +/// A [`CacheKey`](cosmic_text::CacheKey) encodes all of the information of a subpixel-offset glyph and is used to +/// find that glyphs raster in a [`TextureAtlas`] through its corresponding [`GlyphAtlasLocation`]. pub struct FontAtlas { + /// Used to update the [`TextureAtlasLayout`]. pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - pub glyph_to_atlas_index: HashMap<(GlyphId, SubpixelOffset), usize>, + /// A mapping between subpixel-offset glyphs and their [`GlyphAtlasLocation`]. + pub glyph_to_atlas_index: HashMap, + /// The handle to the [`TextureAtlasLayout`] that holds the rasterized glyphs. pub texture_atlas: Handle, + /// The texture where this font atlas is located pub texture: Handle, } impl FontAtlas { + /// Create a new [`FontAtlas`] with the given size, adding it to the appropriate asset collections. pub fn new( textures: &mut Assets, - texture_atlases: &mut Assets, + texture_atlases_layout: &mut Assets, size: UVec2, ) -> FontAtlas { let texture = textures.add(Image::new_fill( @@ -65,50 +52,72 @@ impl FontAtlas { // Need to keep this image CPU persistent in order to add additional glyphs later on RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, )); - let texture_atlas = TextureAtlasLayout::new_empty(size); + let texture_atlas = texture_atlases_layout.add(TextureAtlasLayout::new_empty(size)); Self { - texture_atlas: texture_atlases.add(texture_atlas), + texture_atlas, glyph_to_atlas_index: HashMap::default(), - dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 0), + dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1), texture, } } - pub fn get_glyph_index( - &self, - glyph_id: GlyphId, - subpixel_offset: SubpixelOffset, - ) -> Option { - self.glyph_to_atlas_index - .get(&(glyph_id, subpixel_offset)) - .copied() + /// Get the [`GlyphAtlasLocation`] for a subpixel-offset glyph. + pub fn get_glyph_index(&self, cache_key: cosmic_text::CacheKey) -> Option { + self.glyph_to_atlas_index.get(&cache_key).copied() } - pub fn has_glyph(&self, glyph_id: GlyphId, subpixel_offset: SubpixelOffset) -> bool { - self.glyph_to_atlas_index - .contains_key(&(glyph_id, subpixel_offset)) + /// Checks if the given subpixel-offset glyph is contained in this [`FontAtlas`]. + pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey) -> bool { + self.glyph_to_atlas_index.contains_key(&cache_key) } + /// Add a glyph to the atlas, updating both its texture and layout. + /// + /// The glyph is represented by `glyph`, and its image content is `glyph_texture`. + /// This content is copied into the atlas texture, and the atlas layout is updated + /// to store the location of that glyph into the atlas. + /// + /// # Returns + /// + /// Returns `()` if the glyph is successfully added, or [`TextError::FailedToAddGlyph`] otherwise. + /// In that case, neither the atlas texture nor the atlas layout are + /// modified. pub fn add_glyph( &mut self, textures: &mut Assets, - texture_atlases: &mut Assets, - glyph_id: GlyphId, - subpixel_offset: SubpixelOffset, + atlas_layouts: &mut Assets, + cache_key: cosmic_text::CacheKey, texture: &Image, - ) -> bool { - let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); - if let Some(index) = self.dynamic_texture_atlas_builder.add_texture( - texture_atlas, - textures, - texture, - &self.texture, - ) { - self.glyph_to_atlas_index - .insert((glyph_id, subpixel_offset), index); - true + offset: IVec2, + ) -> Result<(), TextError> { + let atlas_layout = atlas_layouts.get_mut(&self.texture_atlas).unwrap(); + let atlas_texture = textures.get_mut(&self.texture).unwrap(); + + if let Some(glyph_index) = + self.dynamic_texture_atlas_builder + .add_texture(atlas_layout, texture, atlas_texture) + { + self.glyph_to_atlas_index.insert( + cache_key, + GlyphAtlasLocation { + glyph_index, + offset, + }, + ); + Ok(()) } else { - false + Err(TextError::FailedToAddGlyph(cache_key.glyph_id)) } } } + +impl std::fmt::Debug for FontAtlas { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FontAtlas") + .field("glyph_to_atlas_index", &self.glyph_to_atlas_index) + .field("texture_atlas", &self.texture_atlas) + .field("texture", &self.texture) + .field("dynamic_texture_atlas_builder", &"[...]") + .finish() + } +} diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 25980bdf340e3..e4d11b60c0617 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,34 +1,45 @@ -use crate::{error::TextError, Font, FontAtlas}; -use ab_glyph::{GlyphId, OutlinedGlyph, Point}; -use bevy_asset::{AssetEvent, AssetId}; -use bevy_asset::{Assets, Handle}; -use bevy_ecs::prelude::*; -use bevy_math::{FloatOrd, UVec2}; -use bevy_reflect::Reflect; -use bevy_render::texture::Image; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; +use bevy_ecs::{ + event::EventReader, + system::{ResMut, Resource}, +}; +use bevy_math::{IVec2, UVec2}; +use bevy_reflect::TypePath; +use bevy_render::{ + render_asset::RenderAssetUsages, + render_resource::{Extent3d, TextureDimension, TextureFormat}, + texture::Image, +}; use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; -type FontSizeKey = FloatOrd; +use crate::{error::TextError, Font, FontAtlas, GlyphAtlasInfo}; -#[derive(Default, Resource)] +/// A map of font faces to their corresponding [`FontAtlasSet`]s. +#[derive(Debug, Default, Resource)] pub struct FontAtlasSets { // PERF: in theory this could be optimized with Assets storage ... consider making some fast "simple" AssetMap pub(crate) sets: HashMap, FontAtlasSet>, } impl FontAtlasSets { + /// Get a reference to the [`FontAtlasSet`] with the given font asset id. pub fn get(&self, id: impl Into>) -> Option<&FontAtlasSet> { let id: AssetId = id.into(); self.sets.get(&id) } + /// Get a mutable reference to the [`FontAtlasSet`] with the given font asset id. + pub fn get_mut(&mut self, id: impl Into>) -> Option<&mut FontAtlasSet> { + let id: AssetId = id.into(); + self.sets.get_mut(&id) + } } +/// A system that cleans up [`FontAtlasSet`]s for removed [`Font`]s pub fn remove_dropped_font_atlas_sets( mut font_atlas_sets: ResMut, mut font_events: EventReader>, ) { - // Clean up font atlas sets for removed fonts for event in font_events.read() { if let AssetEvent::Removed { id } = event { font_atlas_sets.sets.remove(id); @@ -36,15 +47,37 @@ pub fn remove_dropped_font_atlas_sets( } } -pub struct FontAtlasSet { - font_atlases: HashMap>, +/// Identifies a font size in a [`FontAtlasSet`]. +/// +/// Allows an `f32` font size to be used as a key in a `HashMap`, by its binary representation. +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct FontSizeKey(pub u32); + +impl From for FontSizeKey { + fn from(val: u32) -> FontSizeKey { + Self(val) + } } -#[derive(Debug, Clone, Reflect)] -pub struct GlyphAtlasInfo { - pub texture_atlas: Handle, - pub texture: Handle, - pub glyph_index: usize, +/// A map of font sizes to their corresponding [`FontAtlas`]es, for a given font face. +/// +/// Provides the interface for adding and retrieving rasterized glyphs, and manages the [`FontAtlas`]es. +/// +/// A `FontAtlasSet` is an [`Asset`]. +/// +/// There is one `FontAtlasSet` for each font: +/// - When a [`Font`] is loaded as an asset and then used in [`Text`](crate::Text), +/// a `FontAtlasSet` asset is created from a weak handle to the `Font`. +/// - ~When a font is loaded as a system font, and then used in [`Text`](crate::Text), +/// a `FontAtlasSet` asset is created and stored with a strong handle to the `FontAtlasSet`.~ +/// (*Note that system fonts are not currently supported by the `TextPipeline`.*) +/// +/// A `FontAtlasSet` contains one or more [`FontAtlas`]es for each font size. +/// +/// It is used by [`TextPipeline::queue_text`](crate::TextPipeline::queue_text). +#[derive(Debug, TypePath, Asset)] +pub struct FontAtlasSet { + font_atlases: HashMap>, } impl Default for FontAtlasSet { @@ -56,46 +89,51 @@ impl Default for FontAtlasSet { } impl FontAtlasSet { + /// Returns an iterator over the [`FontAtlas`]es in this set pub fn iter(&self) -> impl Iterator)> { self.font_atlases.iter() } - pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool { + /// Checks if the given subpixel-offset glyph is contained in any of the [`FontAtlas`]es in this set + pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: &FontSizeKey) -> bool { self.font_atlases - .get(&FloatOrd(font_size)) + .get(font_size) .map_or(false, |font_atlas| { - font_atlas - .iter() - .any(|atlas| atlas.has_glyph(glyph_id, glyph_position.into())) + font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)) }) } + /// Adds the given subpixel-offset glyph to the [`FontAtlas`]es in this set pub fn add_glyph_to_atlas( &mut self, texture_atlases: &mut Assets, textures: &mut Assets, - outlined_glyph: OutlinedGlyph, + font_system: &mut cosmic_text::FontSystem, + swash_cache: &mut cosmic_text::SwashCache, + layout_glyph: &cosmic_text::LayoutGlyph, ) -> Result { - let glyph = outlined_glyph.glyph(); - let glyph_id = glyph.id; - let glyph_position = glyph.position; - let font_size = glyph.scale.y; + let physical_glyph = layout_glyph.physical((0., 0.), 1.0); + let font_atlases = self .font_atlases - .entry(FloatOrd(font_size)) + .entry(physical_glyph.cache_key.font_size_bits.into()) .or_insert_with(|| vec![FontAtlas::new(textures, texture_atlases, UVec2::splat(512))]); - let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph); - let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { + let (glyph_texture, offset) = + Self::get_outlined_glyph_texture(font_system, swash_cache, &physical_glyph)?; + let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> { atlas.add_glyph( textures, texture_atlases, - glyph_id, - glyph_position.into(), + physical_glyph.cache_key, &glyph_texture, + offset, ) }; - if !font_atlases.iter_mut().any(add_char_to_font_atlas) { + if !font_atlases + .iter_mut() + .any(|atlas| add_char_to_font_atlas(atlas).is_ok()) + { // Find the largest dimension of the glyph, either its width or its height let glyph_max_size: u32 = glyph_texture .texture_descriptor @@ -109,48 +147,42 @@ impl FontAtlasSet { texture_atlases, UVec2::splat(containing), )); - if !font_atlases.last_mut().unwrap().add_glyph( + + font_atlases.last_mut().unwrap().add_glyph( textures, texture_atlases, - glyph_id, - glyph_position.into(), + physical_glyph.cache_key, &glyph_texture, - ) { - return Err(TextError::FailedToAddGlyph(glyph_id)); - } + offset, + )?; } - Ok(self - .get_glyph_atlas_info(font_size, glyph_id, glyph_position) - .unwrap()) + Ok(self.get_glyph_atlas_info(physical_glyph.cache_key).unwrap()) } + /// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph. pub fn get_glyph_atlas_info( &mut self, - font_size: f32, - glyph_id: GlyphId, - position: Point, + cache_key: cosmic_text::CacheKey, ) -> Option { self.font_atlases - .get(&FloatOrd(font_size)) + .get(&FontSizeKey(cache_key.font_size_bits)) .and_then(|font_atlases| { font_atlases .iter() .find_map(|atlas| { - atlas - .get_glyph_index(glyph_id, position.into()) - .map(|glyph_index| { - ( - glyph_index, - atlas.texture_atlas.clone_weak(), - atlas.texture.clone_weak(), - ) - }) + atlas.get_glyph_index(cache_key).map(|location| { + ( + location, + atlas.texture_atlas.clone_weak(), + atlas.texture.clone_weak(), + ) + }) }) - .map(|(glyph_index, texture_atlas, texture)| GlyphAtlasInfo { + .map(|(location, texture_atlas, texture)| GlyphAtlasInfo { texture_atlas, + location, texture, - glyph_index, }) }) } @@ -159,9 +191,54 @@ impl FontAtlasSet { pub fn len(&self) -> usize { self.font_atlases.len() } - - /// Returns `true` if the font atlas set contains no elements + /// Returns the number of font atlases in this set pub fn is_empty(&self) -> bool { - self.font_atlases.is_empty() + self.font_atlases.len() == 0 + } + + /// Get the texture of the glyph as a rendered image, and its offset + pub fn get_outlined_glyph_texture( + font_system: &mut cosmic_text::FontSystem, + swash_cache: &mut cosmic_text::SwashCache, + physical_glyph: &cosmic_text::PhysicalGlyph, + ) -> Result<(Image, IVec2), TextError> { + let image = swash_cache + .get_image_uncached(font_system, physical_glyph.cache_key) + .ok_or(TextError::FailedToGetGlyphImage(physical_glyph.cache_key))?; + + let cosmic_text::Placement { + left, + top, + width, + height, + } = image.placement; + + let data = match image.content { + cosmic_text::SwashContent::Mask => image + .data + .iter() + .flat_map(|a| [255, 255, 255, *a]) + .collect(), + cosmic_text::SwashContent::Color => image.data, + cosmic_text::SwashContent::SubpixelMask => { + // TODO: implement + todo!() + } + }; + + Ok(( + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + data, + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::MAIN_WORLD, + ), + IVec2::new(left, top), + )) } } diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index a03e96f036862..945987d748b8d 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -3,18 +3,19 @@ use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; use thiserror::Error; #[derive(Default)] +/// An [`AssetLoader`] for [`Font`]s, for use by the [`AssetServer`] pub struct FontLoader; /// Possible errors that can be produced by [`FontLoader`] #[non_exhaustive] #[derive(Debug, Error)] pub enum FontLoaderError { + /// The contents that could not be parsed + #[error(transparent)] + Content(#[from] cosmic_text::ttf_parser::FaceParsingError), /// An [IO](std::io) Error #[error(transparent)] Io(#[from] std::io::Error), - /// An [`InvalidFont`](ab_glyph::InvalidFont) Error - #[error(transparent)] - FontInvalid(#[from] ab_glyph::InvalidFont), } impl AssetLoader for FontLoader { @@ -29,7 +30,8 @@ impl AssetLoader for FontLoader { ) -> Result { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - Ok(Font::try_from_bytes(bytes)?) + let font = Font::try_from_bytes(bytes)?; + Ok(font) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs new file mode 100644 index 0000000000000..ba949efbfd000 --- /dev/null +++ b/crates/bevy_text/src/glyph.rs @@ -0,0 +1,79 @@ +//! This module exports types related to rendering glyphs. + +use bevy_asset::Handle; +use bevy_math::{IVec2, Vec2}; +use bevy_reflect::Reflect; +use bevy_render::texture::Image; +use bevy_sprite::TextureAtlasLayout; + +/// A glyph of a font, typically representing a single character, positioned in screen space. +/// +/// Contains information about how and where to render a glyph. +/// +/// Used in [`TextPipeline::queue_text`](crate::TextPipeline::queue_text) and [`crate::TextLayoutInfo`] for rendering glyphs. +#[derive(Debug, Clone, Reflect)] +pub struct PositionedGlyph { + /// The position of the glyph in the [`Text`](crate::Text)'s bounding box. + pub position: Vec2, + /// The width and height of the glyph in logical pixels. + pub size: Vec2, + /// Information about the glyph's atlas. + pub atlas_info: GlyphAtlasInfo, + /// The index of the glyph in the [`Text`](crate::Text)'s sections. + pub section_index: usize, + /// TODO: In order to do text editing, we need access to the size of glyphs and their index in the associated String. + /// For example, to figure out where to place the cursor in an input box from the mouse's position. + /// Without this, it's only possible in texts where each glyph is one byte. Cosmic text has methods for this + /// cosmic-texts [hit detection](https://pop-os.github.io/cosmic-text/cosmic_text/struct.Buffer.html#method.hit) + byte_index: usize, +} + +impl PositionedGlyph { + /// Creates a new [`PositionedGlyph`] + pub fn new( + position: Vec2, + size: Vec2, + atlas_info: GlyphAtlasInfo, + section_index: usize, + ) -> Self { + Self { + position, + size, + atlas_info, + section_index, + byte_index: 0, + } + } +} + +/// Information about a glyph in an atlas. +/// +/// Rasterized glyphs are stored as rectangles +/// in one or more [`FontAtlas`](crate::FontAtlas)es. +/// +/// Used in [`PositionedGlyph`] and [`FontAtlasSet`](crate::FontAtlasSet). +#[derive(Debug, Clone, Reflect)] +pub struct GlyphAtlasInfo { + /// A handle to the [`Image`] data for the texture atlas this glyph was placed in. + /// + /// A (weak) clone of the handle held by the [`FontAtlas`]. + pub texture: Handle, + /// A handle to the [`TextureAtlasLayout`] map for the texture atlas this glyph was placed in. + /// + /// A (weak) clone of the handle held by the [`FontAtlas`]. + pub texture_atlas: Handle, + /// Location and offset of a glyph within the texture atlas. + pub location: GlyphAtlasLocation, +} + +/// The location of a glyph in an atlas, +/// and how it should be positioned when placed. +/// +/// Used in [`GlyphAtlasInfo`] and [`FontAtlas`](crate::FontAtlas). +#[derive(Debug, Clone, Copy, Reflect)] +pub struct GlyphAtlasLocation { + /// The index of the glyph in the atlas + pub glyph_index: usize, + /// The required offset (relative positioning) when placed + pub offset: IVec2, +} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index e689e93f9345f..0550883f13830 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -1,32 +1,61 @@ -// FIXME(3492): remove once docs are ready -#![allow(missing_docs)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![forbid(unsafe_code)] -#![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" -)] +//! This crate provides the tools for positioning and rendering text in Bevy. +//! +//! # `Font` +//! +//! Fonts contain information for drawing glyphs, which are shapes that typically represent a single character, +//! but in some cases part of a "character" (grapheme clusters) or more than one character (ligatures). +//! +//! A font *face* is part of a font family, +//! and is distinguished by its style (e.g. italic), its weight (e.g. bold) and its stretch (e.g. condensed). +//! +//! In Bevy, [`Font`]s are loaded by the [`FontLoader`] as [assets](bevy_asset::AssetPlugin). +//! +//! # `TextPipeline` +//! +//! The [`TextPipeline`] resource does all of the heavy lifting for rendering text. +//! +//! [`Text`] is first measured by creating a [`TextMeasureInfo`] in [`TextPipeline::create_text_measure`], +//! which is called by the `measure_text_system` system of `bevy_ui`. +//! +//! Note that text measurement is only relevant in a UI context. +//! +//! With the actual text bounds defined, the `bevy_ui::widget::text::text_system` system (in a UI context) +//! or [`bevy_text::text2d::update_text2d_layout`] system (in a 2d world space context) +//! passes it into [`TextPipeline::queue_text`], which: +//! +//! 1. creates a [`Buffer`](cosmic_text::Buffer) from the [`TextSection`]s, generating new [`FontAtlasSet`]s if necessary. +//! 2. iterates over each glyph in the [`Buffer`](cosmic_text::Buffer) to create a [`PositionedGlyph`], +//! retrieving glyphs from the cache, or rasterizing to a [`FontAtlas`] if necessary. +//! 3. [`PositionedGlyph`]s are stored in a [`TextLayoutInfo`], +//! which contains all the information that downstream systems need for rendering. +#![allow(clippy::type_complexity)] + +mod bounds; mod error; mod font; mod font_atlas; mod font_atlas_set; mod font_loader; -mod glyph_brush; +mod glyph; mod pipeline; mod text; mod text2d; +pub use cosmic_text; + +pub use bounds::*; pub use error::*; pub use font::*; pub use font_atlas::*; pub use font_atlas_set::*; pub use font_loader::*; -pub use glyph_brush::*; +pub use glyph::*; pub use pipeline::*; pub use text::*; pub use text2d::*; +/// Most commonly used re-exported types. pub mod prelude { #[doc(hidden)] pub use crate::{Font, JustifyText, Text, Text2dBundle, TextError, TextSection, TextStyle}; @@ -41,7 +70,6 @@ use bevy_render::{ camera::CameraUpdateSystem, view::VisibilitySystems, ExtractSchedule, RenderApp, }; use bevy_sprite::SpriteSystem; -use std::num::NonZeroUsize; /// Adds text rendering support to an app. /// @@ -50,31 +78,15 @@ use std::num::NonZeroUsize; #[derive(Default)] pub struct TextPlugin; -/// Settings used to configure the [`TextPlugin`]. -#[derive(Resource)] -pub struct TextSettings { - /// Soft maximum number of font atlases supported in a [`FontAtlasSet`]. When this is exceeded, - /// a warning will be emitted a single time. - pub soft_max_font_atlases: NonZeroUsize, - /// Allows font size to be set dynamically exceeding the amount set in `soft_max_font_atlases`. - /// Note each font size has to be generated which can have a strong performance impact. - pub allow_dynamic_font_size: bool, -} - -impl Default for TextSettings { - fn default() -> Self { - Self { - soft_max_font_atlases: NonZeroUsize::new(16).unwrap(), - allow_dynamic_font_size: false, - } - } -} - -/// Text is rendered for two different view projections, a [`Text2dBundle`] is rendered with a -/// `BottomToTop` y axis, while UI is rendered with a `TopToBottom` y axis. This matters for text because -/// the glyph positioning is different in either layout. +/// Text is rendered for two different view projections; +/// 2-dimensional text ([`Text2dBundle`]) is rendered in "world space" with a `BottomToTop` Y-axis, +/// while UI is rendered with a `TopToBottom` Y-axis. +/// This matters for text because the glyph positioning is different in either layout. +/// For `TopToBottom`, 0 is the top of the text, while for `BottomToTop` 0 is the bottom. pub enum YAxisOrientation { + /// Top to bottom Y-axis orientation, for UI TopToBottom, + /// Bottom to top Y-axis orientation, for 2d world space BottomToTop, } @@ -86,9 +98,8 @@ impl Plugin for TextPlugin { fn build(&self, app: &mut App) { app.init_asset::() .register_type::() - .register_type::() + .register_type::() .init_asset_loader::() - .init_resource::() .init_resource::() .insert_resource(TextPipeline::default()) .add_systems( diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 298857997e896..7f7004e71ee04 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,219 +1,397 @@ -use crate::{ - compute_text_bounds, error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, - FontAtlasSets, JustifyText, PositionedGlyph, Text, TextSection, TextSettings, YAxisOrientation, -}; -use ab_glyph::PxScale; -use bevy_asset::{AssetId, Assets, Handle}; -use bevy_ecs::component::Component; -use bevy_ecs::prelude::ReflectComponent; -use bevy_ecs::system::Resource; -use bevy_math::Vec2; -use bevy_reflect::prelude::ReflectDefault; -use bevy_reflect::Reflect; +use std::sync::Arc; + +use bevy_asset::{AssetId, Assets}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; +use bevy_math::{UVec2, Vec2}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::texture::Image; use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; -use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText, ToSectionText}; -#[derive(Default, Resource)] -pub struct TextPipeline { - brush: GlyphBrush, - map_font_id: HashMap, FontId>, +use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; + +use crate::{ + error::TextError, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, PositionedGlyph, + TextBounds, TextSection, YAxisOrientation, +}; + +/// A wrapper around a [`cosmic_text::FontSystem`] +struct CosmicFontSystem(cosmic_text::FontSystem); + +impl Default for CosmicFontSystem { + fn default() -> Self { + let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); + let db = cosmic_text::fontdb::Database::new(); + // TODO: consider using `cosmic_text::FontSystem::new()` (load system fonts by default) + Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db)) + } +} + +/// A wrapper around a [`cosmic_text::SwashCache`] +struct SwashCache(cosmic_text::SwashCache); + +impl Default for SwashCache { + fn default() -> Self { + Self(cosmic_text::SwashCache::new()) + } } -/// Render information for a corresponding [`Text`] component. +/// The `TextPipeline` is used to layout and render [`Text`](crate::Text). /// -/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`]. -#[derive(Component, Clone, Default, Debug, Reflect)] -#[reflect(Component, Default)] -pub struct TextLayoutInfo { - pub glyphs: Vec, - pub logical_size: Vec2, +/// See the [crate-level documentation](crate) for more information. +#[derive(Default, Resource)] +pub struct TextPipeline { + /// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its [`Font`] [`Asset`](bevy_asset::Asset). + map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, String)>, + /// The font system is used to retrieve fonts and their information, including glyph outlines. + /// + /// See [`cosmic_text::FontSystem`] for more information. + font_system: CosmicFontSystem, + /// The swash cache rasterizer is used to rasterize glyphs + /// + /// See [`cosmic_text::SwashCache`] for more information. + swash_cache: SwashCache, } impl TextPipeline { - pub fn get_or_insert_font_id(&mut self, handle: &Handle, font: &Font) -> FontId { - let brush = &mut self.brush; - *self - .map_font_id - .entry(handle.id()) - .or_insert_with(|| brush.add_font(handle.id(), font.font.clone())) + /// Utilizes [`cosmic_text::Buffer`] to shape and layout text + /// + /// Negative or 0.0 font sizes will not be laid out. + #[allow(clippy::too_many_arguments)] + pub fn update_buffer( + &mut self, + fonts: &Assets, + sections: &[TextSection], + linebreak_behavior: BreakLineOn, + bounds: TextBounds, + scale_factor: f64, + buffer: &mut CosmicBuffer, + alignment: JustifyText, + ) -> Result<(), TextError> { + let font_system = &mut self.font_system.0; + + // return early if the fonts are not loaded yet + let mut font_size = 0.; + for section in sections { + if section.style.font_size > font_size { + font_size = section.style.font_size; + } + fonts + .get(section.style.font.id()) + .ok_or(TextError::NoSuchFont)?; + } + let line_height = font_size * 1.2; + let metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32); + + // Load Bevy fonts into cosmic-text's font system. + // This is done as as separate pre-pass to avoid borrow checker issues + for section in sections.iter() { + load_font_to_fontdb(section, font_system, &mut self.map_handle_to_font_id, fonts); + } + + // Map text sections to cosmic-text spans, and ignore sections with negative or zero fontsizes, + // since they cannot be rendered by cosmic-text. + // + // The section index is stored in the metadata of the spans, and could be used + // to look up the section the span came from and is not used internally + // in cosmic-text. + let spans: Vec<(&str, Attrs)> = sections + .iter() + .enumerate() + .filter(|(_section_index, section)| section.style.font_size > 0.0) + .map(|(section_index, section)| { + ( + §ion.value[..], + get_attrs( + section, + section_index, + font_system, + &self.map_handle_to_font_id, + scale_factor, + ), + ) + }) + .collect(); + + buffer.set_metrics(font_system, metrics); + buffer.set_size(font_system, bounds.width, bounds.height); + + buffer.set_wrap( + font_system, + match linebreak_behavior { + BreakLineOn::WordBoundary => Wrap::Word, + BreakLineOn::AnyCharacter => Wrap::Glyph, + BreakLineOn::WordOrCharacter => Wrap::WordOrGlyph, + BreakLineOn::NoWrap => Wrap::None, + }, + ); + + buffer.set_rich_text(font_system, spans, Attrs::new(), Shaping::Advanced); + + // PERF: https://github.com/pop-os/cosmic-text/issues/166: + // Setting alignment afterwards appears to invalidate some layouting performed by `set_text` which is presumably not free? + for buffer_line in buffer.lines.iter_mut() { + buffer_line.set_align(Some(alignment.into())); + } + buffer.shape_until_scroll(font_system, false); + + Ok(()) } + /// Queues text for rendering + /// + /// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s + /// which contain information for rendering the text. #[allow(clippy::too_many_arguments)] pub fn queue_text( &mut self, fonts: &Assets, sections: &[TextSection], - scale_factor: f32, + scale_factor: f64, text_alignment: JustifyText, linebreak_behavior: BreakLineOn, - bounds: Vec2, + bounds: TextBounds, font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, - text_settings: &TextSettings, y_axis_orientation: YAxisOrientation, + buffer: &mut CosmicBuffer, ) -> Result { - let mut scaled_fonts = Vec::with_capacity(sections.len()); - let sections = sections - .iter() - .map(|section| { - let font = fonts - .get(§ion.style.font) - .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, - }; + if sections.is_empty() { + return Ok(TextLayoutInfo::default()); + } + + self.update_buffer( + fonts, + sections, + linebreak_behavior, + bounds, + scale_factor, + buffer, + text_alignment, + )?; + + let box_size = buffer_dimensions(buffer); + let font_system = &mut self.font_system.0; + let swash_cache = &mut self.swash_cache.0; - Ok(section) + let glyphs = buffer + .layout_runs() + .flat_map(|run| { + run.glyphs + .iter() + .map(move |layout_glyph| (layout_glyph, run.line_y)) }) - .collect::, _>>()?; + .map(|(layout_glyph, line_y)| { + let section_index = layout_glyph.metadata; - let section_glyphs = - self.brush - .compute_glyphs(§ions, bounds, text_alignment, linebreak_behavior)?; + let font_handle = sections[section_index].style.font.clone_weak(); + let font_atlas_set = font_atlas_sets.sets.entry(font_handle.id()).or_default(); - if section_glyphs.is_empty() { - return Ok(TextLayoutInfo::default()); - } + let physical_glyph = layout_glyph.physical((0., 0.), 1.); - let size = compute_text_bounds(§ion_glyphs, |index| scaled_fonts[index]).size(); + let atlas_info = font_atlas_set + .get_glyph_atlas_info(physical_glyph.cache_key) + .map(Ok) + .unwrap_or_else(|| { + font_atlas_set.add_glyph_to_atlas( + texture_atlases, + textures, + font_system, + swash_cache, + layout_glyph, + ) + })?; - let h_limit = if bounds.x.is_finite() { - bounds.x - } else { - size.x - }; + let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); + let location = atlas_info.location; + let glyph_rect = texture_atlas.textures[location.glyph_index]; + let left = location.offset.x as f32; + let top = location.offset.y as f32; + let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height()); - let h_anchor = match text_alignment { - JustifyText::Left => 0.0, - JustifyText::Center => h_limit * 0.5, - JustifyText::Right => h_limit * 1.0, - } - .floor(); + // offset by half the size because the origin is center + let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; + let y = line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; + let y = match y_axis_orientation { + YAxisOrientation::TopToBottom => y, + YAxisOrientation::BottomToTop => box_size.y - y, + }; - let glyphs = self.brush.process_glyphs( - section_glyphs, - §ions, - font_atlas_sets, - fonts, - texture_atlases, - textures, - text_settings, - y_axis_orientation, - h_anchor, - )?; + let position = Vec2::new(x, y); + + // TODO: recreate the byte index, that keeps track of where a cursor is, + // when glyphs are not limited to single byte representation, relevant for #1319 + let pos_glyph = + PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, section_index); + Ok(pos_glyph) + }) + .collect::, _>>()?; Ok(TextLayoutInfo { glyphs, - logical_size: size, + size: box_size, }) } + + /// Queues text for measurement + /// + /// Produces a [`TextMeasureInfo`] which can be used by a layout system + /// to measure the text area on demand. + pub fn create_text_measure( + &mut self, + fonts: &Assets, + sections: &[TextSection], + scale_factor: f64, + linebreak_behavior: BreakLineOn, + buffer: &mut CosmicBuffer, + text_alignment: JustifyText, + ) -> Result { + const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0); + + self.update_buffer( + fonts, + sections, + linebreak_behavior, + MIN_WIDTH_CONTENT_BOUNDS, + scale_factor, + buffer, + text_alignment, + )?; + + let min_width_content_size = buffer_dimensions(buffer); + + let max_width_content_size = { + let font_system = &mut self.font_system.0; + buffer.set_size(font_system, None, None); + buffer_dimensions(buffer) + }; + + Ok(TextMeasureInfo { + min: min_width_content_size, + max: max_width_content_size, + // TODO: This clone feels wasteful, is there another way to structure TextMeasureInfo + // that it doesn't need to own a buffer? - bytemunch + buffer: buffer.0.clone(), + }) + } + + /// Get a mutable reference to the [`cosmic_text::FontSystem`]. + /// + /// Used internally. + pub fn font_system_mut(&mut self) -> &mut cosmic_text::FontSystem { + &mut self.font_system.0 + } } -#[derive(Debug, Clone)] -pub struct TextMeasureSection { - pub text: Box, - pub scale: f32, - pub font_id: FontId, +/// Render information for a corresponding [`Text`](crate::Text) component. +/// +/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`]. +#[derive(Component, Clone, Default, Debug, Reflect)] +#[reflect(Component, Default)] +pub struct TextLayoutInfo { + /// Scaled and positioned glyphs in screenspace + pub glyphs: Vec, + /// The glyphs resulting size + pub size: Vec2, } -#[derive(Debug, Clone, Default)] +/// Size information for a corresponding [`Text`](crate::Text) component. +/// +/// Generated via [`TextPipeline::create_text_measure`]. pub struct TextMeasureInfo { - pub fonts: Box<[ab_glyph::FontArc]>, - pub sections: Box<[TextMeasureSection]>, - pub justification: JustifyText, - pub linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker, + /// Minimum size for a text area in pixels, to be used when laying out widgets with taffy pub min: Vec2, + /// Maximum size for a text area in pixels, to be used when laying out widgets with taffy pub max: Vec2, + buffer: cosmic_text::Buffer, } -impl TextMeasureInfo { - pub fn from_text( - text: &Text, - fonts: &Assets, - scale_factor: f32, - ) -> Result { - let sections = &text.sections; - let mut auto_fonts = Vec::with_capacity(sections.len()); - let mut out_sections = Vec::with_capacity(sections.len()); - for (i, section) in sections.iter().enumerate() { - match fonts.get(§ion.style.font) { - Some(font) => { - auto_fonts.push(font.font.clone()); - out_sections.push(TextMeasureSection { - font_id: FontId(i), - scale: scale_value(section.style.font_size, scale_factor), - text: section.value.clone().into_boxed_str(), - }); - } - None => return Err(TextError::NoSuchFont), - } - } - - Ok(Self::new( - auto_fonts, - out_sections, - text.justify, - text.linebreak_behavior.into(), - )) +impl std::fmt::Debug for TextMeasureInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TextMeasureInfo") + .field("min", &self.min) + .field("max", &self.max) + .field("buffer", &"_") + .field("font_system", &"_") + .finish() } - fn new( - fonts: Vec, - sections: Vec, - justification: JustifyText, - linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker, - ) -> Self { - let mut info = Self { - fonts: fonts.into_boxed_slice(), - sections: sections.into_boxed_slice(), - justification, - linebreak_behavior, - min: Vec2::ZERO, - max: Vec2::ZERO, - }; +} - let min = info.compute_size(Vec2::new(0.0, f32::INFINITY)); - let max = info.compute_size(Vec2::INFINITY); - info.min = min; - info.max = max; - info +impl TextMeasureInfo { + /// Computes the size of the text area within the provided bounds. + pub fn compute_size( + &mut self, + bounds: TextBounds, + font_system: &mut cosmic_text::FontSystem, + ) -> Vec2 { + self.buffer + .set_size(font_system, bounds.width, bounds.height); + buffer_dimensions(&self.buffer) } +} - pub fn compute_size(&self, bounds: Vec2) -> Vec2 { - let sections = &self.sections; - let geom = SectionGeometry { - bounds: (bounds.x, bounds.y), - ..Default::default() - }; - let section_glyphs = glyph_brush_layout::Layout::default() - .h_align(self.justification.into()) - .line_breaker(self.linebreak_behavior) - .calculate_glyphs(&self.fonts, &geom, sections); - - compute_text_bounds(§ion_glyphs, |index| { - let font = &self.fonts[index]; - let font_size = self.sections[index].scale; - ab_glyph::Font::into_scaled(font, font_size) - }) - .size() - } +fn load_font_to_fontdb( + section: &TextSection, + font_system: &mut cosmic_text::FontSystem, + map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, String)>, + fonts: &Assets, +) { + let font_handle = section.style.font.clone(); + map_handle_to_font_id + .entry(font_handle.id()) + .or_insert_with(|| { + let font = fonts.get(font_handle.id()).expect( + "Tried getting a font that was not available, probably due to not being loaded yet", + ); + let data = Arc::clone(&font.data); + let ids = font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(data)); + + // TODO: it is assumed this is the right font face + let face_id = *ids.last().unwrap(); + let face = font_system.db().face(face_id).unwrap(); + let family_name = face.families[0].0.to_owned(); + + (face_id, family_name) + }); } -impl ToSectionText for TextMeasureSection { - #[inline(always)] - fn to_section_text(&self) -> SectionText<'_> { - SectionText { - text: &self.text, - scale: PxScale::from(self.scale), - font_id: self.font_id, - } - } + +/// Translates [`TextSection`] to [`Attrs`](cosmic_text::attrs::Attrs), +/// loading fonts into the [`Database`](cosmic_text::fontdb::Database) if required. +fn get_attrs<'a>( + section: &TextSection, + section_index: usize, + font_system: &mut cosmic_text::FontSystem, + map_handle_to_font_id: &'a HashMap, (cosmic_text::fontdb::ID, String)>, + scale_factor: f64, +) -> Attrs<'a> { + let (face_id, family_name) = map_handle_to_font_id + .get(§ion.style.font.id()) + .expect("Already loaded with load_font_to_fontdb"); + let face = font_system.db().face(*face_id).unwrap(); + + let attrs = Attrs::new() + .metadata(section_index) + .family(Family::Name(family_name)) + .stretch(face.stretch) + .style(face.style) + .weight(face.weight) + .metrics(Metrics::relative(section.style.font_size, 1.2).scale(scale_factor as f32)) + .color(cosmic_text::Color(section.style.color.to_linear().as_u32())); + attrs +} + +/// Calculate the size of the text area for the given buffer. +fn buffer_dimensions(buffer: &Buffer) -> Vec2 { + let width = buffer + .layout_runs() + .map(|run| run.line_w) + .reduce(f32::max) + .unwrap_or(0.0); + let line_height = buffer.metrics().line_height.ceil(); + let height = buffer.layout_runs().count() as f32 * line_height; + + Vec2::new(width.ceil(), height).ceil() } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 15532e36c1491..39723a06ee413 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,15 +1,35 @@ use bevy_asset::Handle; use bevy_color::Color; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_reflect::prelude::*; use bevy_utils::default; +use cosmic_text::{Buffer, Metrics}; use serde::{Deserialize, Serialize}; use crate::Font; +pub use cosmic_text::{ + self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, + Weight as FontWeight, +}; +/// Wrapper for [`cosmic_text::Buffer`] +#[derive(Component, Deref, DerefMut, Debug, Clone)] +pub struct CosmicBuffer(pub Buffer); + +impl Default for CosmicBuffer { + fn default() -> Self { + Self(Buffer::new_empty(Metrics::new(0.0, 0.000001))) + } +} + +/// A component that is the entry point for rendering text. +/// +/// It contains all of the text value and styling information. #[derive(Component, Debug, Clone, Default, Reflect)] #[reflect(Component, Default)] pub struct Text { + /// The text's sections pub sections: Vec, /// The text's internal alignment. /// Should not affect its position within a container. @@ -33,7 +53,7 @@ impl Text { /// // Accepts a String or any type that converts into a String, such as &str. /// "hello world!", /// TextStyle { - /// font: font_handle.clone(), + /// font: font_handle.clone().into(), /// font_size: 60.0, /// color: Color::WHITE, /// }, @@ -42,7 +62,7 @@ impl Text { /// let hello_bevy = Text::from_section( /// "hello world\nand bevy!", /// TextStyle { - /// font: font_handle, + /// font: font_handle.into(), /// font_size: 60.0, /// color: Color::WHITE, /// }, @@ -70,7 +90,7 @@ impl Text { /// TextSection::new( /// "Hello, ", /// TextStyle { - /// font: font_handle.clone(), + /// font: font_handle.clone().into(), /// font_size: 60.0, /// color: BLUE.into(), /// }, @@ -78,7 +98,7 @@ impl Text { /// TextSection::new( /// "World!", /// TextStyle { - /// font: font_handle, + /// font: font_handle.into(), /// font_size: 60.0, /// color: RED.into(), /// }, @@ -106,9 +126,12 @@ impl Text { } } +/// Contains the value of the text in a section and how it should be styled. #[derive(Debug, Default, Clone, Reflect)] pub struct TextSection { + /// The content (in `String` form) of the text in the section. pub value: String, + /// The style of the text in the section, including the font face, font size, and color. pub style: TextStyle, } @@ -168,23 +191,32 @@ pub enum JustifyText { /// Rightmost character is immediately to the left of the render position. /// Bounds start from the render position and advance leftwards. Right, + /// Words are spaced so that leftmost & rightmost characters + /// align with their margins. + /// Bounds start from the render position and advance equally left & right. + Justified, } -impl From for glyph_brush_layout::HorizontalAlign { - fn from(val: JustifyText) -> Self { - match val { - JustifyText::Left => glyph_brush_layout::HorizontalAlign::Left, - JustifyText::Center => glyph_brush_layout::HorizontalAlign::Center, - JustifyText::Right => glyph_brush_layout::HorizontalAlign::Right, +impl From for cosmic_text::Align { + fn from(justify: JustifyText) -> Self { + match justify { + JustifyText::Left => cosmic_text::Align::Left, + JustifyText::Center => cosmic_text::Align::Center, + JustifyText::Right => cosmic_text::Align::Right, + JustifyText::Justified => cosmic_text::Align::Justified, } } } #[derive(Clone, Debug, Reflect)] +/// `TextStyle` determines the style of the text in a section, specifically +/// the font face, the font size, and the color. pub struct TextStyle { - /// If this is not specified, then + /// The specific font face to use, as a `Handle` to a [`Font`] asset. + /// + /// If the `font` is not specified, then /// * if `default_font` feature is enabled (enabled by default in `bevy` crate), - /// `FiraMono-subset.ttf` compiled into the library is used. + /// `FiraMono-subset.ttf` compiled into the library is used. /// * otherwise no text will be rendered. pub font: Handle, /// The vertical height of rasterized glyphs in the font atlas in pixels. @@ -195,6 +227,7 @@ pub struct TextStyle { /// A new font atlas is generated for every combination of font handle and scaled font size /// which can have a strong performance impact. pub font_size: f32, + /// The color of the text for this section. pub color: Color, } @@ -202,7 +235,7 @@ impl Default for TextStyle { fn default() -> Self { Self { font: Default::default(), - font_size: 24.0, + font_size: 20.0, color: Color::WHITE, } } @@ -221,20 +254,9 @@ pub enum BreakLineOn { /// This is closer to the behavior one might expect from text in a terminal. /// However it may lead to words being broken up across linebreaks. AnyCharacter, + /// Wraps at the word level, or fallback to character level if a word can’t fit on a line by itself + WordOrCharacter, /// No soft wrapping, where text is automatically broken up into separate lines when it overflows a boundary, will ever occur. /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, is still enabled. NoWrap, } - -impl From for glyph_brush_layout::BuiltInLineBreaker { - fn from(val: BreakLineOn) -> Self { - match val { - // If `NoWrap` is set the choice of `BuiltInLineBreaker` doesn't matter as the text is given unbounded width and soft wrapping will never occur. - // But `NoWrap` does not disable hard breaks where a [`Text`] contains a newline character. - BreakLineOn::WordBoundary | BreakLineOn::NoWrap => { - glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker - } - BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker, - } - } -} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 0b3ae44dec6f6..8c28fe3079aca 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,22 +1,19 @@ use crate::{ - BreakLineOn, Font, FontAtlasSets, PositionedGlyph, Text, TextError, TextLayoutInfo, - TextPipeline, TextSettings, YAxisOrientation, + BreakLineOn, CosmicBuffer, Font, FontAtlasSets, PositionedGlyph, Text, TextBounds, TextError, + TextLayoutInfo, TextPipeline, YAxisOrientation, }; use bevy_asset::Assets; use bevy_color::LinearRgba; use bevy_ecs::{ bundle::Bundle, change_detection::{DetectChanges, Ref}, - component::Component, entity::Entity, event::EventReader, prelude::With, query::{Changed, Without}, - reflect::ReflectComponent, system::{Commands, Local, Query, Res, ResMut}, }; use bevy_math::Vec2; -use bevy_reflect::Reflect; use bevy_render::{ primitives::Aabb, texture::Image, @@ -28,34 +25,6 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; -/// 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 Text2dBounds { - /// The maximum width and height of text in logical pixels. - pub size: Vec2, -} - -impl Default for Text2dBounds { - #[inline] - fn default() -> Self { - Self::UNBOUNDED - } -} - -impl Text2dBounds { - /// Unbounded text will not be truncated or wrapped. - pub const UNBOUNDED: Self = Self { - size: Vec2::splat(f32::INFINITY), - }; -} - /// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) #[derive(Bundle, Clone, Debug, Default)] @@ -66,13 +35,15 @@ pub struct Text2dBundle { /// relative position which is controlled by the `Anchor` component. /// This means that for a block of text consisting of only one line that doesn't wrap, the `alignment` field will have no effect. pub text: Text, + /// Cached buffer for layout with cosmic-text + pub buffer: CosmicBuffer, /// How the text is positioned relative to its transform. /// /// `text_anchor` does not affect the internal alignment of the block of text, only /// its position. pub text_anchor: Anchor, /// The maximum width and height of the text. - pub text_2d_bounds: Text2dBounds, + pub text_2d_bounds: TextBounds, /// The transform of the text. pub transform: Transform, /// The global transform of the text. @@ -124,7 +95,7 @@ pub fn extract_text2d_sprite( } let text_anchor = -(anchor.as_vec() + 0.5); - let alignment_translation = text_layout_info.logical_size * text_anchor; + let alignment_translation = text_layout_info.size * text_anchor; let transform = *global_transform * GlobalTransform::from_translation(alignment_translation.extend(0.)) * scaling; @@ -149,7 +120,7 @@ pub fn extract_text2d_sprite( ExtractedSprite { transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, - rect: Some(atlas.textures[atlas_info.glyph_index].as_rect()), + rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()), custom_size: None, image_handle_id: atlas_info.texture.id(), flip_x: false, @@ -175,13 +146,18 @@ pub fn update_text2d_layout( mut queue: Local>, mut textures: ResMut>, fonts: Res>, - text_settings: Res, windows: Query<&Window, With>, mut scale_factor_changed: EventReader, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, - mut text_query: Query<(Entity, Ref, Ref, &mut TextLayoutInfo)>, + mut text_query: Query<( + Entity, + Ref, + Ref, + &mut TextLayoutInfo, + &mut CosmicBuffer, + )>, ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.read().last().is_some(); @@ -194,40 +170,43 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, bounds, mut text_layout_info) in &mut text_query { + for (entity, text, bounds, mut text_layout_info, mut buffer) in &mut text_query { if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) { - let text_bounds = Vec2::new( - if text.linebreak_behavior == BreakLineOn::NoWrap { - f32::INFINITY + let text_bounds = TextBounds { + width: if text.linebreak_behavior == BreakLineOn::NoWrap { + None } else { - scale_value(bounds.size.x, scale_factor) + bounds.width.map(|width| scale_value(width, scale_factor)) }, - scale_value(bounds.size.y, scale_factor), - ); + height: bounds + .height + .map(|height| scale_value(height, scale_factor)), + }; + match text_pipeline.queue_text( &fonts, &text.sections, - scale_factor, + scale_factor.into(), text.justify, text.linebreak_behavior, text_bounds, &mut font_atlas_sets, &mut texture_atlases, &mut textures, - text_settings.as_ref(), YAxisOrientation::BottomToTop, + buffer.as_mut(), ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, let's add this entity to the // queue for further processing queue.insert(entity); } - Err(e @ TextError::FailedToAddGlyph(_)) => { + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { - info.logical_size.x = scale_value(info.logical_size.x, inverse_scale_factor); - info.logical_size.y = scale_value(info.logical_size.y, inverse_scale_factor); + info.size.x = scale_value(info.size.x, inverse_scale_factor); + info.size.y = scale_value(info.size.y, inverse_scale_factor); *text_layout_info = info; } } @@ -254,11 +233,9 @@ pub fn calculate_bounds_text2d( for (entity, layout_info, anchor, aabb) in &mut text_to_update_aabb { // `Anchor::as_vec` gives us an offset relative to the text2d bounds, by negating it and scaling // by the logical size we compensate the transform offset in local space to get the center. - let center = (-anchor.as_vec() * layout_info.logical_size) - .extend(0.0) - .into(); + let center = (-anchor.as_vec() * layout_info.size).extend(0.0).into(); // Distance in local space from the center to the x and y limits of the text2d bounds. - let half_extents = (layout_info.logical_size / 2.0).extend(0.0).into(); + let half_extents = (layout_info.size / 2.0).extend(0.0).into(); if let Some(mut aabb) = aabb { *aabb = Aabb { center, @@ -291,7 +268,6 @@ mod tests { app.init_resource::>() .init_resource::>() .init_resource::>() - .init_resource::() .init_resource::() .init_resource::>() .insert_resource(TextPipeline::default()) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 11cacda0690e3..a862f5719b495 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,3 +1,4 @@ +use bevy_text::TextPipeline; use thiserror::Error; use crate::{ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale}; @@ -94,6 +95,7 @@ pub fn ui_layout_system( just_children_query: Query<&Children>, mut removed_components: UiLayoutSystemRemovedComponentParam, mut node_transform_query: Query<(&mut Node, &mut Transform)>, + #[cfg(feature = "bevy_text")] mut text_pipeline: ResMut, ) { struct CameraLayoutInfo { size: UVec2, @@ -214,6 +216,8 @@ pub fn ui_layout_system( } } + #[cfg(feature = "bevy_text")] + let font_system = text_pipeline.font_system_mut(); // clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used) ui_surface.remove_entities(removed_components.removed_nodes.read()); @@ -227,7 +231,12 @@ pub fn ui_layout_system( for (camera_id, camera) in &camera_layout_info { let inverse_target_scale_factor = camera.scale_factor.recip(); - ui_surface.compute_camera_layout(*camera_id, camera.size); + ui_surface.compute_camera_layout( + *camera_id, + camera.size, + #[cfg(feature = "bevy_text")] + font_system, + ); for root in &camera.root_nodes { update_uinode_geometry_recursive( *root, @@ -408,6 +417,8 @@ mod tests { world.init_resource::>>(); world.init_resource::>(); world.init_resource::(); + #[cfg(feature = "bevy_text")] + world.init_resource::(); // spawn a dummy primary window and camera world.spawn(( @@ -1040,6 +1051,8 @@ mod tests { world.init_resource::>>(); world.init_resource::>(); world.init_resource::(); + #[cfg(feature = "bevy_text")] + world.init_resource::(); // spawn a dummy primary window and camera world.spawn(( diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index 378cb7b78a616..75b4428af0f9e 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -10,7 +10,7 @@ use bevy_utils::default; use bevy_utils::tracing::warn; use crate::layout::convert; -use crate::{LayoutContext, LayoutError, Measure, NodeMeasure, Style}; +use crate::{LayoutContext, LayoutError, Measure, MeasureArgs, NodeMeasure, Style}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct RootNodePair { @@ -196,7 +196,12 @@ without UI components as a child of an entity with UI components, results may be } /// Compute the layout for each window entity's corresponding root node in the layout. - pub fn compute_camera_layout(&mut self, camera: Entity, render_target_resolution: UVec2) { + pub fn compute_camera_layout( + &mut self, + camera: Entity, + render_target_resolution: UVec2, + #[cfg(feature = "bevy_text")] font_system: &mut bevy_text::cosmic_text::FontSystem, + ) { let Some(camera_root_nodes) = self.camera_roots.get(&camera) else { return; }; @@ -219,10 +224,14 @@ without UI components as a child of an entity with UI components, results may be context .map(|ctx| { let size = ctx.measure( - known_dimensions.width, - known_dimensions.height, - available_space.width, - available_space.height, + MeasureArgs { + width: known_dimensions.width, + height: known_dimensions.height, + available_width: available_space.width, + available_height: available_space.height, + #[cfg(feature = "bevy_text")] + font_system, + }, style, ); taffy::Size { diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index f84391896e426..1d4d7beb8021c 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -16,18 +16,20 @@ impl std::fmt::Debug for ContentSize { } } +pub struct MeasureArgs<'a> { + pub width: Option, + pub height: Option, + pub available_width: AvailableSpace, + pub available_height: AvailableSpace, + #[cfg(feature = "bevy_text")] + pub font_system: &'a mut bevy_text::cosmic_text::FontSystem, +} + /// A `Measure` is used to compute the size of a ui node /// when the size of that node is based on its content. pub trait Measure: Send + Sync + 'static { /// Calculate the size of the node given the constraints. - fn measure( - &self, - width: Option, - height: Option, - available_width: AvailableSpace, - available_height: AvailableSpace, - style: &taffy::Style, - ) -> Vec2; + fn measure(&mut self, measure_args: MeasureArgs<'_>, style: &taffy::Style) -> Vec2; } /// A type to serve as Taffy's node context (which allows the content size of leaf nodes to be computed) @@ -43,28 +45,13 @@ pub enum NodeMeasure { } impl Measure for NodeMeasure { - fn measure( - &self, - width: Option, - height: Option, - available_width: AvailableSpace, - available_height: AvailableSpace, - style: &taffy::Style, - ) -> Vec2 { + fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 { match self { - NodeMeasure::Fixed(fixed) => { - fixed.measure(width, height, available_width, available_height, style) - } + NodeMeasure::Fixed(fixed) => fixed.measure(measure_args, style), #[cfg(feature = "bevy_text")] - NodeMeasure::Text(text) => { - text.measure(width, height, available_width, available_height, style) - } - NodeMeasure::Image(image) => { - image.measure(width, height, available_width, available_height, style) - } - NodeMeasure::Custom(custom) => { - custom.measure(width, height, available_width, available_height, style) - } + NodeMeasure::Text(text) => text.measure(measure_args, style), + NodeMeasure::Image(image) => image.measure(measure_args, style), + NodeMeasure::Custom(custom) => custom.measure(measure_args, style), } } } @@ -77,14 +64,7 @@ pub struct FixedMeasure { } impl Measure for FixedMeasure { - fn measure( - &self, - _: Option, - _: Option, - _: AvailableSpace, - _: AvailableSpace, - _: &taffy::Style, - ) -> Vec2 { + fn measure(&mut self, _: MeasureArgs, _: &taffy::Style) -> Vec2 { self.size } } diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 42e7517487803..0610fee35a967 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -15,7 +15,9 @@ use bevy_ecs::bundle::Bundle; use bevy_render::view::{InheritedVisibility, ViewVisibility, Visibility}; use bevy_sprite::TextureAtlas; #[cfg(feature = "bevy_text")] -use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle}; +use bevy_text::{ + BreakLineOn, CosmicBuffer, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle, +}; use bevy_transform::prelude::{GlobalTransform, Transform}; /// The basic UI node. @@ -169,6 +171,8 @@ pub struct TextBundle { pub style: Style, /// Contains the text of the node pub text: Text, + /// Cached cosmic buffer for layout + pub buffer: CosmicBuffer, /// Text layout information pub text_layout_info: TextLayoutInfo, /// Text system flags diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index d05c0b541e00e..2a92c5ef8d5b5 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -857,7 +857,7 @@ pub fn extract_uinode_text( } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let mut rect = atlas.textures[atlas_info.glyph_index].as_rect(); + let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect(); rect.min *= inverse_scale_factor; rect.max *= inverse_scale_factor; extracted_uinodes.uinodes.insert( diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 28b50be15c455..4451aed20e8b8 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -1,6 +1,4 @@ -use crate::{ - measurement::AvailableSpace, ContentSize, Measure, Node, NodeMeasure, UiImage, UiScale, -}; +use crate::{ContentSize, Measure, MeasureArgs, Node, NodeMeasure, UiImage, UiScale}; use bevy_asset::Assets; use bevy_ecs::prelude::*; use bevy_math::{UVec2, Vec2}; @@ -37,14 +35,15 @@ pub struct ImageMeasure { } impl Measure for ImageMeasure { - fn measure( - &self, - width: Option, - height: Option, - available_width: AvailableSpace, - available_height: AvailableSpace, - style: &taffy::Style, - ) -> Vec2 { + fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 { + let MeasureArgs { + width, + height, + available_width, + available_height, + .. + } = measure_args; + // Convert available width/height into an option let parent_width = available_width.into_option(); let parent_height = available_height.into_option(); diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 2fd25552d2523..b6cfda92d81a6 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -1,5 +1,6 @@ use crate::{ - ContentSize, DefaultUiCamera, FixedMeasure, Measure, Node, NodeMeasure, TargetCamera, UiScale, + ContentSize, DefaultUiCamera, FixedMeasure, Measure, MeasureArgs, Node, NodeMeasure, + TargetCamera, UiScale, }; use bevy_asset::Assets; use bevy_ecs::{ @@ -15,8 +16,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::Camera, texture::Image}; use bevy_sprite::TextureAtlasLayout; use bevy_text::{ - scale_value, BreakLineOn, Font, FontAtlasSets, Text, TextError, TextLayoutInfo, - TextMeasureInfo, TextPipeline, TextSettings, YAxisOrientation, + scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, TextBounds, + TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation, }; use bevy_utils::Entry; use taffy::style::AvailableSpace; @@ -42,20 +43,19 @@ impl Default for TextFlags { } } -#[derive(Clone)] pub struct TextMeasure { pub info: TextMeasureInfo, } impl Measure for TextMeasure { - fn measure( - &self, - width: Option, - height: Option, - available_width: AvailableSpace, - _available_height: AvailableSpace, - _style: &taffy::Style, - ) -> Vec2 { + fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 { + let MeasureArgs { + width, + height, + available_width, + font_system, + .. + } = measure_args; let x = width.unwrap_or_else(|| match available_width { AvailableSpace::Definite(x) => { // It is possible for the "min content width" to be larger than @@ -71,7 +71,9 @@ impl Measure for TextMeasure { height .map_or_else( || match available_width { - AvailableSpace::Definite(_) => self.info.compute_size(Vec2::new(x, f32::MAX)), + AvailableSpace::Definite(_) => self + .info + .compute_size(TextBounds::new_horizontal(x), font_system), AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), }, @@ -81,15 +83,26 @@ impl Measure for TextMeasure { } } +#[allow(clippy::too_many_arguments)] #[inline] fn create_text_measure( fonts: &Assets, - scale_factor: f32, + scale_factor: f64, text: Ref, + text_pipeline: &mut TextPipeline, mut content_size: Mut, mut text_flags: Mut, + buffer: &mut CosmicBuffer, + text_alignment: JustifyText, ) { - match TextMeasureInfo::from_text(&text, fonts, scale_factor) { + match text_pipeline.create_text_measure( + fonts, + &text.sections, + scale_factor, + text.linebreak_behavior, + buffer, + text_alignment, + ) { Ok(measure) => { if text.linebreak_behavior == BreakLineOn::NoWrap { content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); @@ -105,7 +118,7 @@ fn create_text_measure( // Try again next frame text_flags.needs_new_measure_func = true; } - Err(e @ TextError::FailedToAddGlyph(_)) => { + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } }; @@ -132,13 +145,15 @@ pub fn measure_text_system( &mut ContentSize, &mut TextFlags, Option<&TargetCamera>, + &mut CosmicBuffer, ), With, >, + mut text_pipeline: ResMut, ) { let mut scale_factors: EntityHashMap = EntityHashMap::default(); - for (text, content_size, text_flags, camera) in &mut text_query { + for (text, content_size, text_flags, camera, mut buffer) in &mut text_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; @@ -159,7 +174,17 @@ pub fn measure_text_system( || text_flags.needs_new_measure_func || content_size.is_added() { - create_text_measure(&fonts, scale_factor, text, content_size, text_flags); + let text_alignment = text.justify; + create_text_measure( + &fonts, + scale_factor.into(), + text, + &mut text_pipeline, + content_size, + text_flags, + buffer.as_mut(), + text_alignment, + ); } } *last_scale_factors = scale_factors; @@ -173,22 +198,22 @@ fn queue_text( font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, - text_settings: &TextSettings, scale_factor: f32, inverse_scale_factor: f32, text: &Text, node: Ref, mut text_flags: Mut, mut text_layout_info: Mut, + buffer: &mut CosmicBuffer, ) { // Skip the text node if it is waiting for a new measure func if !text_flags.needs_new_measure_func { let physical_node_size = if text.linebreak_behavior == BreakLineOn::NoWrap { // With `NoWrap` set, no constraints are placed on the width of the text. - Vec2::splat(f32::INFINITY) + TextBounds::UNBOUNDED } else { // `scale_factor` is already multiplied by `UiScale` - Vec2::new( + TextBounds::new( node.unrounded_size.x * scale_factor, node.unrounded_size.y * scale_factor, ) @@ -197,26 +222,26 @@ fn queue_text( match text_pipeline.queue_text( fonts, &text.sections, - scale_factor, + scale_factor.into(), text.justify, text.linebreak_behavior, physical_node_size, font_atlas_sets, texture_atlases, textures, - text_settings, YAxisOrientation::TopToBottom, + buffer, ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, try again next frame text_flags.needs_recompute = true; } - Err(e @ TextError::FailedToAddGlyph(_)) => { + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { - info.logical_size.x = scale_value(info.logical_size.x, inverse_scale_factor); - info.logical_size.y = scale_value(info.logical_size.y, inverse_scale_factor); + info.size.x = scale_value(info.size.x, inverse_scale_factor); + info.size.y = scale_value(info.size.y, inverse_scale_factor); *text_layout_info = info; text_flags.needs_recompute = false; } @@ -239,7 +264,6 @@ pub fn text_system( fonts: Res>, camera_query: Query<(Entity, &Camera)>, default_ui_camera: DefaultUiCamera, - text_settings: Res, ui_scale: Res, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, @@ -250,11 +274,12 @@ pub fn text_system( &mut TextLayoutInfo, &mut TextFlags, Option<&TargetCamera>, + &mut CosmicBuffer, )>, ) { let mut scale_factors: EntityHashMap = EntityHashMap::default(); - for (node, text, text_layout_info, text_flags, camera) in &mut text_query { + for (node, text, text_layout_info, text_flags, camera, mut buffer) in &mut text_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; @@ -282,13 +307,13 @@ pub fn text_system( &mut font_atlas_sets, &mut texture_atlases, &mut textures, - &text_settings, scale_factor, inverse_scale_factor, text, node, text_flags, text_layout_info, + buffer.as_mut(), ); } } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 33e12ee571e06..e8f7e7df136e1 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -77,7 +77,6 @@ The default feature set enables most of the expected features of a game engine, |serialize|Enable serialization support through serde| |shader_format_glsl|Enable support for shaders in GLSL| |shader_format_spirv|Enable support for shaders in SPIR-V| -|subpixel_glyph_atlas|Enable rendering of font glyphs using subpixel accuracy| |symphonia-aac|AAC audio format support (through symphonia)| |symphonia-all|AAC, FLAC, MP3, MP4, OGG/VORBIS, and WAV audio formats support (through symphonia)| |symphonia-flac|FLAC audio format support (through symphonia)| diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index bb7325ac1f895..a420c16e9cdc5 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -9,7 +9,7 @@ use bevy::{ color::palettes::css::*, prelude::*, sprite::Anchor, - text::{BreakLineOn, Text2dBounds}, + text::{BreakLineOn, TextBounds}, }; fn main() { @@ -97,10 +97,8 @@ fn setup(mut commands: Commands, asset_server: Res) { justify: JustifyText::Left, linebreak_behavior: BreakLineOn::WordBoundary, }, - text_2d_bounds: Text2dBounds { - // Wrap text in the rectangle - size: box_size, - }, + // Wrap text in the rectangle + text_2d_bounds: TextBounds::from(box_size), // ensure the text is drawn on top of the box transform: Transform::from_translation(Vec3::Z), ..default() @@ -129,10 +127,8 @@ fn setup(mut commands: Commands, asset_server: Res) { justify: JustifyText::Left, linebreak_behavior: BreakLineOn::AnyCharacter, }, - text_2d_bounds: Text2dBounds { - // Wrap text in the rectangle - size: other_box_size, - }, + // Wrap text in the rectangle + text_2d_bounds: TextBounds::from(other_box_size), // ensure the text is drawn on top of the box transform: Transform::from_translation(Vec3::Z), ..default() diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 406de20dc9802..a0af4690175a9 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -408,27 +408,33 @@ fn update_color_grading_settings( } fn update_ui( - mut text: Query<&mut Text, Without>, + mut text_query: Query<&mut Text, Without>, settings: Query<(&Tonemapping, &ColorGrading)>, current_scene: Res, selected_parameter: Res, mut hide_ui: Local, keys: Res>, ) { - let (method, color_grading) = settings.single(); - let method = *method; - - let mut text = text.single_mut(); - let text = &mut text.sections[0].value; - if keys.just_pressed(KeyCode::KeyH) { *hide_ui = !*hide_ui; } - text.clear(); + + let old_text = &text_query.single().sections[0].value; + if *hide_ui { + if !old_text.is_empty() { + // single_mut() always triggers change detection, + // so only access if text actually needs changing + text_query.single_mut().sections[0].value.clear(); + } return; } + let (method, color_grading) = settings.single(); + let method = *method; + + let mut text = String::with_capacity(old_text.len()); + let scn = current_scene.0; text.push_str("(H) Hide UI\n\n"); text.push_str("Test Scene: \n"); @@ -532,6 +538,12 @@ fn update_ui( if current_scene.0 == 1 { text.push_str("(Enter) Reset all to scene recommendation\n"); } + + if text != old_text.as_str() { + // single_mut() always triggers change detection, + // so only access if text actually changed + text_query.single_mut().sections[0].value = text; + } } // ---------------------------------------------------------------------------- diff --git a/examples/stress_tests/many_glyphs.rs b/examples/stress_tests/many_glyphs.rs index ca4bc6db28c53..51a31f8cd622f 100644 --- a/examples/stress_tests/many_glyphs.rs +++ b/examples/stress_tests/many_glyphs.rs @@ -9,7 +9,7 @@ use bevy::{ color::palettes::basic::RED, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, prelude::*, - text::{BreakLineOn, Text2dBounds}, + text::{BreakLineOn, TextBounds}, window::{PresentMode, WindowResolution}, winit::{UpdateMode, WinitSettings}, }; @@ -83,9 +83,7 @@ fn setup(mut commands: Commands) { commands.spawn(Text2dBundle { text, text_anchor: bevy::sprite::Anchor::Center, - text_2d_bounds: Text2dBounds { - size: Vec2::new(1000., f32::INFINITY), - }, + text_2d_bounds: TextBounds::new_horizontal(1000.), ..Default::default() }); } diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 716d867d0d4e5..1dc69af6ed9e2 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -6,7 +6,7 @@ use bevy::{ color::palettes::basic::{BLUE, YELLOW}, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, prelude::*, - text::{BreakLineOn, Text2dBounds}, + text::{BreakLineOn, TextBounds}, window::{PresentMode, WindowResolution}, winit::{UpdateMode, WinitSettings}, }; @@ -72,9 +72,9 @@ fn spawn(mut commands: Commands, asset_server: Res) { } // changing the bounds of the text will cause a recomputation -fn update_text_bounds(time: Res