diff --git a/Cargo.toml b/Cargo.toml index 4168bcbb78083..936c16606bde8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -205,9 +205,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"] @@ -1832,6 +1829,16 @@ description = "Demonstrates how the to use the size constraints to control the s category = "UI (User Interface)" wasm = true +[[example]] +name = "system_fonts" +path = "examples/ui/system_fonts.rs" + +[package.metadata.example.system_fonts] +name = "System Fonts" +description = "Demonstrates using system fonts." +category = "UI (User Interface)" +wasm = false + [[example]] name = "text" path = "examples/ui/text.rs" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index d3e9a532a44cf..e1610e0e2fa11 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -71,9 +71,6 @@ serialize = ["bevy_core/serialize", "bevy_input/serialize", "bevy_time/serialize wayland = ["bevy_winit/wayland"] x11 = ["bevy_winit/x11"] -# enable rendering of font glyphs using subpixel accuracy -subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] - # Optimise for WebGL2 webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"] diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 7e17ab8c5640e..1561fa21a25d3 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] @@ -27,7 +26,8 @@ bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } # other anyhow = "1.0.4" -ab_glyph = "0.2.6" -glyph_brush_layout = "0.2.1" +cosmic-text = "0.8.0" thiserror = "1.0" serde = {version = "1", features = ["derive"]} +sys-locale = "0.3.0" +unicode-bidi = "0.3.13" diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 1bb7cf1253581..33f8f12647e58 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -1,4 +1,3 @@ -use ab_glyph::GlyphId; use thiserror::Error; #[derive(Debug, PartialEq, Eq, Error)] @@ -6,5 +5,7 @@ pub enum TextError { #[error("font not found")] NoSuchFont, #[error("failed to add glyph to newly-created atlas {0:?}")] - FailedToAddGlyph(GlyphId), + FailedToAddGlyph(u16), + #[error("font system mutex could not be acquired or is poisoned")] + FailedToAcquireMutex, } diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 1d8a465a76ea2..52b528eb999e6 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,45 +1,20 @@ -use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph}; use bevy_reflect::{TypePath, TypeUuid}; -use bevy_render::{ - render_resource::{Extent3d, TextureDimension, TextureFormat}, - texture::Image, -}; #[derive(Debug, TypeUuid, TypePath, Clone)] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] + +/// An [`Asset`](bevy_asset::Asset) that contains the data for a loaded font, if loaded as an asset. +/// +/// Loaded by [`FontLoader`](crate::FontLoader). pub struct Font { - pub font: FontArc, + pub data: std::sync::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(); - let width = bounds.width() as usize; - let height = bounds.height() as usize; - let mut alpha = vec![0.0; width * height]; - outlined_glyph.draw(|x, y, v| { - alpha[y as usize * width + x as usize] = 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, - ) + pub fn from_bytes(font_data: Vec) -> Self { + // TODO: validate font, restore `try_from_bytes` + Self { + data: std::sync::Arc::new(font_data), + } } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index be1903c121b36..a52541841dc76 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::Vec2; +use bevy_math::{IVec2, Vec2}; use bevy_render::{ render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::Image, @@ -8,40 +7,17 @@ use bevy_render::{ use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; 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; +/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`. +/// +/// A [`FontAtlasSet`](crate::FontAtlasSet) contains one or more `FontAtlas`es. pub struct FontAtlas { + /// Used to update the [`TextureAtlas`]. pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - pub glyph_to_atlas_index: HashMap<(GlyphId, SubpixelOffset), usize>, + /// A mapping between subpixel-binned glyphs and their [`GlyphAtlasLocation`]. + pub glyph_to_atlas_index: HashMap, + /// The handle to the [`TextureAtlas`] that holds the rasterized glyphs. pub texture_atlas: Handle, } @@ -69,36 +45,34 @@ impl FontAtlas { } } - pub fn get_glyph_index( - &self, - glyph_id: GlyphId, - subpixel_offset: SubpixelOffset, - ) -> Option { - self.glyph_to_atlas_index - .get(&(glyph_id, subpixel_offset)) - .copied() + 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)) + pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey) -> bool { + self.glyph_to_atlas_index.contains_key(&cache_key) } pub fn add_glyph( &mut self, textures: &mut Assets, texture_atlases: &mut Assets, - glyph_id: GlyphId, - subpixel_offset: SubpixelOffset, + cache_key: cosmic_text::CacheKey, texture: &Image, + offset: IVec2, ) -> bool { let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); - if let Some(index) = + if let Some(glyph_index) = self.dynamic_texture_atlas_builder .add_texture(texture_atlas, textures, texture) { - self.glyph_to_atlas_index - .insert((glyph_id, subpixel_offset), index); + self.glyph_to_atlas_index.insert( + cache_key, + GlyphAtlasLocation { + glyph_index, + offset, + }, + ); true } else { false diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index f28b4138ad8d0..5b03c08002d60 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,28 +1,36 @@ -use crate::{error::TextError, Font, FontAtlas}; -use ab_glyph::{GlyphId, OutlinedGlyph, Point}; -use bevy_asset::{Assets, Handle}; -use bevy_math::Vec2; -use bevy_reflect::TypePath; -use bevy_reflect::TypeUuid; -use bevy_render::texture::Image; +use bevy_asset::Assets; +use bevy_math::{IVec2, Vec2}; +use bevy_reflect::{TypePath, TypeUuid}; +use bevy_render::{ + render_resource::{Extent3d, TextureDimension, TextureFormat}, + texture::Image, +}; use bevy_sprite::TextureAtlas; -use bevy_utils::FloatOrd; use bevy_utils::HashMap; -type FontSizeKey = FloatOrd; +use crate::{error::TextError, FontAtlas, GlyphAtlasInfo}; +type FontSizeKey = u32; + +/// Provides the interface for adding and retrieving rasterized glyphs, and manages the [`FontAtlas`]es. +/// +/// A `FontAtlasSet` is an [`Asset`](bevy_asset::Asset). +/// +/// There is one `FontAtlasSet` for each font: +/// - When a [`Font`](crate::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`. +/// +/// A `FontAtlasSet` contains one or more [`FontAtlas`]es for each font size. +/// +/// It is used by [`TextPipeline::queue_text`](crate::TextPipeline::queue_text). #[derive(TypeUuid, TypePath)] #[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] pub struct FontAtlasSet { font_atlases: HashMap>, } -#[derive(Debug, Clone)] -pub struct GlyphAtlasInfo { - pub texture_atlas: Handle, - pub glyph_index: usize, -} - impl Default for FontAtlasSet { fn default() -> Self { FontAtlasSet { @@ -36,13 +44,11 @@ impl FontAtlasSet { self.font_atlases.iter() } - pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool { + pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: f32) -> bool { self.font_atlases - .get(&FloatOrd(font_size)) + .get(&font_size.to_bits()) .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)) }) } @@ -50,15 +56,13 @@ impl FontAtlasSet { &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 font_atlases = self .font_atlases - .entry(FloatOrd(font_size)) + .entry(layout_glyph.cache_key.font_size_bits) .or_insert_with(|| { vec![FontAtlas::new( textures, @@ -67,14 +71,15 @@ impl FontAtlasSet { )] }); - let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph); + let (glyph_texture, offset) = + Self::get_outlined_glyph_texture(font_system, swash_cache, layout_glyph); let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { atlas.add_glyph( textures, texture_atlases, - glyph_id, - glyph_position.into(), + layout_glyph.cache_key, &glyph_texture, + offset, ) }; if !font_atlases.iter_mut().any(add_char_to_font_atlas) { @@ -94,38 +99,34 @@ impl FontAtlasSet { if !font_atlases.last_mut().unwrap().add_glyph( textures, texture_atlases, - glyph_id, - glyph_position.into(), + layout_glyph.cache_key, &glyph_texture, + offset, ) { - return Err(TextError::FailedToAddGlyph(glyph_id)); + return Err(TextError::FailedToAddGlyph(layout_glyph.cache_key.glyph_id)); } } - Ok(self - .get_glyph_atlas_info(font_size, glyph_id, glyph_position) - .unwrap()) + Ok(self.get_glyph_atlas_info(layout_glyph.cache_key).unwrap()) } 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(&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())) + .get_glyph_index(cache_key) + .map(|location| (location, atlas.texture_atlas.clone_weak())) }) - .map(|(glyph_index, texture_atlas)| GlyphAtlasInfo { + .map(|(location, texture_atlas)| GlyphAtlasInfo { texture_atlas, - glyph_index, + location, }) }) } @@ -133,4 +134,50 @@ impl FontAtlasSet { pub fn num_font_atlases(&self) -> usize { self.font_atlases.len() } + + /// 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, + layout_glyph: &cosmic_text::LayoutGlyph, + ) -> (Image, IVec2) { + let image = swash_cache + .get_image_uncached(font_system, layout_glyph.cache_key) + // TODO: don't unwrap + .unwrap(); + + 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 + todo!() + } + }; + + ( + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + data, + TextureFormat::Rgba8UnormSrgb, + ), + IVec2::new(left, top), + ) + } } diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index e179ec9ccf82e..540a954590a92 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -13,7 +13,7 @@ impl AssetLoader for FontLoader { load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<()>> { Box::pin(async move { - let font = Font::try_from_bytes(bytes.into())?; + let font = Font::from_bytes(bytes.into()); load_context.set_default_asset(LoadedAsset::new(font)); Ok(()) }) diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs new file mode 100644 index 0000000000000..7ba9bc4c3a8c8 --- /dev/null +++ b/crates/bevy_text/src/glyph.rs @@ -0,0 +1,53 @@ +//! This module exports types related to rendering glyphs. + +use bevy_asset::Handle; +use bevy_math::{IVec2, Vec2}; +use bevy_sprite::TextureAtlas; + +/// 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)] +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, + /// 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. + // TODO: re-implement this or equivalent + pub byte_index: usize, +} + +/// 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)] +pub struct GlyphAtlasInfo { + /// A handle to the texture atlas this glyph was placed in. + pub texture_atlas: Handle, + /// Location and offset of a glyph. + 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)] +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/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs deleted file mode 100644 index 9b17f09a7b910..0000000000000 --- a/crates/bevy_text/src/glyph_brush.rs +++ /dev/null @@ -1,211 +0,0 @@ -use ab_glyph::{Font as _, FontArc, Glyph, ScaleFont as _}; -use bevy_asset::{Assets, Handle}; -use bevy_math::Vec2; -use bevy_render::texture::Image; -use bevy_sprite::TextureAtlas; -use bevy_utils::tracing::warn; -use glyph_brush_layout::{ - BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, - SectionText, ToSectionText, -}; - -use crate::{ - error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo, - TextAlignment, TextSettings, YAxisOrientation, -}; - -pub struct GlyphBrush { - fonts: Vec, - handles: Vec>, - latest_font_id: FontId, -} - -impl Default for GlyphBrush { - fn default() -> Self { - GlyphBrush { - fonts: Vec::new(), - handles: Vec::new(), - latest_font_id: FontId(0), - } - } -} - -impl GlyphBrush { - pub fn compute_glyphs( - &self, - sections: &[S], - bounds: Vec2, - text_alignment: TextAlignment, - linebreak_behavior: BreakLineOn, - ) -> Result, TextError> { - let geom = SectionGeometry { - bounds: (bounds.x, bounds.y), - ..Default::default() - }; - - let lbb: BuiltInLineBreaker = linebreak_behavior.into(); - - let section_glyphs = Layout::default() - .h_align(text_alignment.into()) - .line_breaker(lbb) - .calculate_glyphs(&self.fonts, &geom, sections); - Ok(section_glyphs) - } - - #[allow(clippy::too_many_arguments)] - pub fn process_glyphs( - &self, - glyphs: Vec, - sections: &[SectionText], - font_atlas_set_storage: &mut Assets, - fonts: &Assets, - texture_atlases: &mut Assets, - textures: &mut Assets, - text_settings: &TextSettings, - font_atlas_warning: &mut FontAtlasWarning, - y_axis_orientation: YAxisOrientation, - ) -> Result, TextError> { - if glyphs.is_empty() { - return Ok(Vec::new()); - } - - let sections_data = sections - .iter() - .map(|section| { - let handle = &self.handles[section.font_id.0]; - let font = fonts.get(handle).ok_or(TextError::NoSuchFont)?; - let font_size = section.scale.y; - Ok(( - handle, - font, - font_size, - ab_glyph::Font::as_scaled(&font.font, font_size), - )) - }) - .collect::, _>>()?; - - let mut min_x = std::f32::MAX; - let mut min_y = std::f32::MAX; - let mut max_y = std::f32::MIN; - for sg in &glyphs { - let glyph = &sg.glyph; - - let scaled_font = sections_data[sg.section_index].3; - min_x = min_x.min(glyph.position.x); - min_y = min_y.min(glyph.position.y - scaled_font.ascent()); - max_y = max_y.max(glyph.position.y - scaled_font.descent()); - } - min_x = min_x.floor(); - min_y = min_y.floor(); - max_y = max_y.floor(); - - let mut positioned_glyphs = Vec::new(); - for sg in glyphs { - let SectionGlyph { - section_index: _, - byte_index, - mut glyph, - font_id: _, - } = sg; - let glyph_id = glyph.id; - let glyph_position = glyph.position; - let adjust = GlyphPlacementAdjuster::new(&mut glyph); - let section_data = sections_data[sg.section_index]; - if let Some(outlined_glyph) = section_data.1.font.outline_glyph(glyph) { - let bounds = outlined_glyph.px_bounds(); - let handle_font_atlas: Handle = section_data.0.cast_weak(); - let font_atlas_set = font_atlas_set_storage - .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); - - let atlas_info = font_atlas_set - .get_glyph_atlas_info(section_data.2, glyph_id, glyph_position) - .map(Ok) - .unwrap_or_else(|| { - font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph) - })?; - - if !text_settings.allow_dynamic_font_size - && !font_atlas_warning.warned - && font_atlas_set.num_font_atlases() > text_settings.max_font_atlases.get() - { - warn!("warning[B0005]: Number of font atlases has exceeded the maximum of {}. Performance and memory usage may suffer.", text_settings.max_font_atlases.get()); - font_atlas_warning.warned = true; - } - - let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let glyph_rect = texture_atlas.textures[atlas_info.glyph_index]; - let size = Vec2::new(glyph_rect.width(), glyph_rect.height()); - - let x = bounds.min.x + size.x / 2.0 - min_x; - - let y = match y_axis_orientation { - YAxisOrientation::BottomToTop => max_y - bounds.max.y + size.y / 2.0, - YAxisOrientation::TopToBottom => bounds.min.y + size.y / 2.0 - min_y, - }; - - let position = adjust.position(Vec2::new(x, y)); - - positioned_glyphs.push(PositionedGlyph { - position, - size, - atlas_info, - section_index: sg.section_index, - byte_index, - }); - } - } - Ok(positioned_glyphs) - } - - pub fn add_font(&mut self, handle: Handle, font: FontArc) -> FontId { - self.fonts.push(font); - self.handles.push(handle); - let font_id = self.latest_font_id; - self.latest_font_id = FontId(font_id.0 + 1); - font_id - } -} - -#[derive(Debug, Clone)] -pub struct PositionedGlyph { - pub position: Vec2, - pub size: Vec2, - pub atlas_info: GlyphAtlasInfo, - pub section_index: usize, - pub byte_index: usize, -} - -#[cfg(feature = "subpixel_glyph_atlas")] -struct GlyphPlacementAdjuster; - -#[cfg(feature = "subpixel_glyph_atlas")] -impl GlyphPlacementAdjuster { - #[inline(always)] - pub fn new(_: &mut Glyph) -> Self { - Self - } - - #[inline(always)] - pub fn position(&self, p: Vec2) -> Vec2 { - p - } -} - -#[cfg(not(feature = "subpixel_glyph_atlas"))] -struct GlyphPlacementAdjuster(f32); - -#[cfg(not(feature = "subpixel_glyph_atlas"))] -impl GlyphPlacementAdjuster { - #[inline(always)] - pub fn new(glyph: &mut Glyph) -> Self { - let v = glyph.position.x.round(); - glyph.position.x = 0.; - glyph.position.y = glyph.position.y.ceil(); - Self(v) - } - - #[inline(always)] - pub fn position(&self, v: Vec2) -> Vec2 { - Vec2::new(self.0, 0.) + v - } -} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index ed7825adbbf4f..43dde3aabc06d 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -1,3 +1,33 @@ +//! 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`](FontLoader) as assets, +//! or they can be loaded as system fonts through [`TextPipeline::load_system_fonts`]. +//! +//! # `TextPipeline` +//! +//! The [`TextPipeline`] resource does all of the heavy lifting for rendering text. +//! +//! [`Text`](Text) is first measured by creating a [`TextMeasureInfo`] in [`TextPipeline::create_text_measure`], +//! which is called by a system. +//! +//! Note that text measurement is only relevant in a UI context. +//! +//! With the actual text bounds defined, another system 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`](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 error; @@ -5,7 +35,7 @@ mod font; mod font_atlas; mod font_atlas_set; mod font_loader; -mod glyph_brush; +mod glyph; mod pipeline; mod text; mod text2d; @@ -15,7 +45,7 @@ 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::*; @@ -111,7 +141,7 @@ impl Plugin for TextPlugin { app, DEFAULT_FONT_HANDLE, "FiraMono-subset.ttf", - |bytes: &[u8]| { Font::try_from_bytes(bytes.to_vec()).unwrap() } + |bytes: &[u8]| { Font::from_bytes(bytes.to_vec()) } ); } } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index e71e49c030e67..957d59faf7105 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,49 +1,246 @@ -use ab_glyph::{PxScale, ScaleFont}; +use std::sync::{Arc, Mutex}; + use bevy_asset::{Assets, Handle, HandleId}; -use bevy_ecs::component::Component; -use bevy_ecs::system::Resource; +use bevy_ecs::{component::Component, system::Resource}; use bevy_math::Vec2; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; -use bevy_utils::HashMap; +use bevy_utils::{ + tracing::{error, info, warn}, + HashMap, +}; -use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText}; +use cosmic_text::{Attrs, AttrsList, Buffer, BufferLine, Family, Metrics, Wrap}; use crate::{ - error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, - FontAtlasWarning, PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation, + error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, FontRef, PositionedGlyph, + TextAlignment, TextSection, TextSettings, YAxisOrientation, }; -#[derive(Default, Resource)] -pub struct TextPipeline { - brush: GlyphBrush, - map_font_id: HashMap, +// TODO: cache buffers / store buffers on the entity +// TODO: reconstruct byte indices +// TODO: rescale font sizes in all examples +// TODO: fix any broken examples +// TODO: solve spans with different font sizes, see https://github.com/pop-os/cosmic-text/issues/64 +// TODO: (future work) split text entities into section entities +// TODO: (future work) text editing +// TODO: font validation + +// TODO: the only reason we need a mutex is due to TextMeasure +// - is there a way to do this without it? +/// A wrapper around a [`cosmic_text::FontSystem`] +pub struct FontSystem(Arc>); + +impl Default for FontSystem { + 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(Arc::new(Mutex::new( + cosmic_text::FontSystem::new_with_locale_and_db(locale, db), + ))) + } } -/// Render information for a corresponding [`Text`](crate::Text) component. +impl FontSystem { + fn load_system_fonts(&mut self) { + match self.0.try_lock() { + Ok(mut font_system) => { + font_system.db_mut().load_system_fonts(); + } + Err(err) => { + error!("Failed to acquire mutex: {:?}", err); + } + }; + } +} + +/// A wrapper around a [`cosmic_text::SwashCache`] +pub struct SwashCache(cosmic_text::SwashCache); + +impl Default for SwashCache { + fn default() -> Self { + Self(cosmic_text::SwashCache::new()) + } +} + +/// 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)] -pub struct TextLayoutInfo { - pub glyphs: Vec, - pub 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) [`HandleId`]. + map_handle_to_font_id: HashMap, + /// Identifies a [`FontAtlasSet`] handle by its font [`ID`](cosmic_text::fontdb::ID). + /// + /// Note that this is a strong handle, so that textures are not dropped. + map_font_id_to_handle: HashMap>, + /// The font system is used to retrieve fonts and their information, including glyph outlines. + /// + /// See [`cosmic_text::FontSystem`] for more information. + font_system: FontSystem, + /// 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.clone(), font.font.clone())) + pub fn create_buffer( + &mut self, + fonts: &Assets, + sections: &[TextSection], + linebreak_behavior: BreakLineOn, + bounds: Vec2, + scale_factor: f64, + ) -> Result { + // TODO: Support multiple section font sizes, pending upstream implementation in cosmic_text + // For now, just use the first section's size or a default + let font_size = sections + .get(0) + .map(|s| s.style.font_size) + .unwrap_or_else(|| crate::TextStyle::default().font_size) + as f64 + * scale_factor; + // TODO: Support line height as an option. Unitless `1.2` is the default used in browsers (1.2x font size). + let line_height = font_size * 1.2; + let (font_size, line_height) = (font_size as f32, line_height as f32); + let metrics = Metrics::new(font_size, line_height); + + let font_system = &mut acquire_font_system(&mut self.font_system)?; + + // TODO: cache buffers (see Iced / glyphon) + let mut buffer = Buffer::new(font_system, metrics); + + buffer.lines.clear(); + let mut attrs_list = AttrsList::new(Attrs::new()); + let mut line_string = String::new(); + // all sections need to be combined and broken up into lines + // e.g. + // style0"Lorem ipsum\ndolor sit amet," + // style1" consectetur adipiscing\nelit," + // style2" sed do eiusmod tempor\nincididunt" + // style3" ut labore et dolore\nmagna aliqua." + // becomes: + // line0: style0"Lorem ipsum" + // line1: style0"dolor sit amet," + // style1" consectetur adipiscing," + // line2: style1"elit," + // style2" sed do eiusmod tempor" + // line3: style2"incididunt" + // style3"ut labore et dolore" + // line4: style3"magna aliqua." + + // combine all sections into a string + // as well as metadata that links those sections to that string + let mut end = 0; + let (string, sections_data): (String, Vec<_>) = sections + .iter() + .enumerate() + .map(|(section_index, section)| { + let start = end; + end += section.value.len(); + (section.value.as_str(), (section, section_index, start..end)) + }) + .unzip(); + + let mut sections_iter = sections_data.into_iter(); + let mut maybe_section = sections_iter.next(); + + // split the string into lines, as ranges + let string_start = string.as_ptr() as usize; + let mut lines_iter = BidiParagraphs::new(&string).map(|line: &str| { + let start = line.as_ptr() as usize - string_start; + let end = start + line.len(); + start..end + }); + let mut maybe_line = lines_iter.next(); + + loop { + let (Some(line_range), Some((section, section_index, section_range))) = (&maybe_line, &maybe_section) else { + // this is reached only if this text is empty + break; + }; + + // start..end is the intersection of this line and this section + let start = line_range.start.max(section_range.start); + let end = line_range.end.min(section_range.end); + if start < end { + let text = &string[start..end]; + add_span( + &mut line_string, + &mut attrs_list, + section, + *section_index, + text, + font_system, + &mut self.map_handle_to_font_id, + fonts, + ); + } + + // we know that at the end of a line, + // section text's end index is always >= line text's end index + // so if this section ends before this line ends, + // there is another section in this line. + // otherwise, we move on to the next line. + if section_range.end < line_range.end { + maybe_section = sections_iter.next(); + } else { + maybe_line = lines_iter.next(); + if maybe_line.is_some() { + // finalize this line and start a new line + let prev_attrs_list = + std::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); + let prev_line_string = std::mem::take(&mut line_string); + buffer + .lines + .push(BufferLine::new(prev_line_string, prev_attrs_list)); + } else { + // finalize the final line + buffer.lines.push(BufferLine::new(line_string, attrs_list)); + break; + } + } + } + + // node size (bounds) is already scaled by the systems that call queue_text + // TODO: cosmic text does not shape/layout text outside the buffer height + // consider a better way to do this + // let buffer_height = bounds.y; + let buffer_height = f32::INFINITY; + buffer.set_size(font_system, bounds.x.ceil(), buffer_height); + + buffer.set_wrap( + font_system, + match linebreak_behavior { + BreakLineOn::WordBoundary => Wrap::Word, + BreakLineOn::AnyCharacter => Wrap::Glyph, + }, + ); + + // TODO: other shaping methods? + buffer.shape_until_scroll(font_system); + + if buffer.visible_lines() == 0 { + // Presumably the font(s) are not available yet + return Err(TextError::NoSuchFont); + } + + Ok(buffer) } + /// 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: f64, + // TODO: Implement text alignment properly text_alignment: TextAlignment, linebreak_behavior: BreakLineOn, bounds: Vec2, @@ -54,203 +251,361 @@ impl TextPipeline { font_atlas_warning: &mut FontAtlasWarning, y_axis_orientation: YAxisOrientation, ) -> 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()); + } + + let buffer = + self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; + + let font_system = &mut acquire_font_system(&mut self.font_system)?; + let swash_cache = &mut self.swash_cache.0; + + let box_size = buffer_dimensions(&buffer); + + let glyphs = buffer.layout_runs().flat_map(|run| { + run.glyphs + .iter() + .map(move |layout_glyph| (layout_glyph, run.line_w, run.line_y)) + }) + .map(|(layout_glyph, line_w, line_y)| { + let section_index = layout_glyph.metadata; + + let font_atlas_set = match sections[section_index].style.font { + FontRef::Asset(ref font_handle) => { + let handle: Handle = font_handle.cast_weak(); + font_atlas_set_storage + .get_or_insert_with(handle, FontAtlasSet::default) + } + FontRef::Query(ref query) => { + // get the id from the database + // TODO: error handling + // TODO: font may not yet be available, but may be available in future + let font_id = font_system.get_font_matches(cosmic_text::Attrs { + color_opt: None, + family: query.family.as_family(), + stretch: query.stretch, + style: query.style, + weight: query.weight, + metadata: 0, + })[0]; + let handle = self + .map_font_id_to_handle + .entry(font_id) + .or_insert_with(|| font_atlas_set_storage.add(FontAtlasSet::default())); + font_atlas_set_storage.get_mut(handle).unwrap() + } }; - Ok(section) - }) - .collect::, _>>()?; + let atlas_info = font_atlas_set + .get_glyph_atlas_info(layout_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 section_glyphs = - self.brush - .compute_glyphs(§ions, bounds, text_alignment, linebreak_behavior)?; + if !text_settings.allow_dynamic_font_size + && !font_atlas_warning.warned + && font_atlas_set.num_font_atlases() > text_settings.max_font_atlases.get() + { + warn!("warning[B0005]: Number of font atlases has exceeded the maximum of {}. Performance and memory usage may suffer.", text_settings.max_font_atlases.get()); + font_atlas_warning.warned = true; + } - if section_glyphs.is_empty() { - return Ok(TextLayoutInfo::default()); - } + 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 = Vec2::new(glyph_rect.width(), glyph_rect.height()); - let mut min_x: f32 = std::f32::MAX; - let mut min_y: f32 = std::f32::MAX; - let mut max_x: f32 = std::f32::MIN; - let mut max_y: f32 = std::f32::MIN; - - for sg in §ion_glyphs { - let scaled_font = scaled_fonts[sg.section_index]; - let glyph = &sg.glyph; - // The fonts use a coordinate system increasing upwards so ascent is a positive value - // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, - // so we have to subtract from the baseline position to get the minimum and maximum values. - min_x = min_x.min(glyph.position.x); - min_y = min_y.min(glyph.position.y - scaled_font.ascent()); - max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); - max_y = max_y.max(glyph.position.y - scaled_font.descent()); - } + // offset by half the size because the origin is center + let x = glyph_size.x / 2.0 + left + layout_glyph.x_int as f32; + let y = line_y + layout_glyph.y_int as f32 - top + glyph_size.y / 2.0; + // TODO: use cosmic text's implementation (per-BufferLine alignment) as it will be editor aware + // see https://github.com/pop-os/cosmic-text/issues/130 (currently bugged) + let x = x + match text_alignment { + TextAlignment::Left => 0.0, + TextAlignment::Center => (box_size.x - line_w) / 2.0, + TextAlignment::Right => box_size.x - line_w, + }; + let y = match y_axis_orientation { + YAxisOrientation::TopToBottom => y, + YAxisOrientation::BottomToTop => box_size.y - y, + }; - let size = Vec2::new(max_x - min_x, max_y - min_y); + // TODO: confirm whether we need to offset by glyph baseline + // (this should be testable with a single line of text with + // fonts of different sizes and/or baselines) - let glyphs = self.brush.process_glyphs( - section_glyphs, - §ions, - font_atlas_set_storage, - fonts, - texture_atlases, - textures, - text_settings, - font_atlas_warning, - y_axis_orientation, - )?; + let position = Vec2::new(x, y); + + let pos_glyph = PositionedGlyph { + position, + size: glyph_size, + atlas_info, + section_index, + // TODO: recreate the byte index, relevant for #1319 + // alternatively, reimplement cosmic-text's own hit tests for text + byte_index: 0, + }; + Ok(pos_glyph) + }) + .collect::, _>>()?; - Ok(TextLayoutInfo { glyphs, size }) + Ok(TextLayoutInfo { + glyphs, + 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, - text_alignment: TextAlignment, - linebreak_behaviour: BreakLineOn, + // TODO: not currently required + _text_alignment: TextAlignment, + linebreak_behavior: BreakLineOn, ) -> Result { - let mut auto_fonts = Vec::with_capacity(sections.len()); - let mut scaled_fonts = Vec::with_capacity(sections.len()); - let sections = sections - .iter() - .enumerate() - .map(|(i, section)| { - let font = fonts - .get(§ion.style.font) - .ok_or(TextError::NoSuchFont)?; - let font_size = scale_value(section.style.font_size, scale_factor); - auto_fonts.push(font.font.clone()); - let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size); - scaled_fonts.push(px_scale_font); - - let section = TextMeasureSection { - font_id: FontId(i), - scale: PxScale::from(font_size), - text: section.value.clone(), - }; + const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); + const MAX_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(f32::INFINITY, f32::INFINITY); - Ok(section) - }) - .collect::, _>>()?; - - Ok(TextMeasureInfo::new( - auto_fonts, - scaled_fonts, + let mut buffer = self.create_buffer( + fonts, sections, - text_alignment, - linebreak_behaviour.into(), - )) + linebreak_behavior, + MIN_WIDTH_CONTENT_BOUNDS, + scale_factor, + )?; + + let min_width_content_size = buffer_dimensions(&buffer); + + let max_width_content_size = { + let font_system = &mut acquire_font_system(&mut self.font_system)?; + + buffer.set_size( + font_system, + MAX_WIDTH_CONTENT_BOUNDS.x, + MAX_WIDTH_CONTENT_BOUNDS.y, + ); + + buffer_dimensions(&buffer) + }; + + Ok(TextMeasureInfo { + min_width_content_size, + max_width_content_size, + font_system: Arc::clone(&self.font_system.0), + buffer: Mutex::new(buffer), + }) + } + + /// Attempts to load system fonts. + /// + /// Supports Windows, Linux and macOS. + /// + /// System fonts loading is a surprisingly complicated task, + /// mostly unsolvable without interacting with system libraries. + /// And since [`fontdb`](cosmic_text::fontdb) tries to be small and portable, this method + /// will simply scan some predefined directories. + /// Which means that fonts that are not in those directories must + /// be added manually. + /// + /// This allows access to any installed system fonts + /// + /// # Timing + /// + /// This function takes some time to run. On the release build, it can take up to a second, + /// while debug builds can take up to ten times longer. For this reason, it should only be + /// called once, and the resulting [`FontSystem`] should be shared. + /// + /// This should ideally run in a background thread. + // TODO: This should run in a background thread. + pub fn load_system_fonts(&mut self) { + info!("Loading system fonts"); + self.font_system.load_system_fonts(); + info!("Loaded system fonts"); } } -#[derive(Debug, Clone)] -pub struct TextMeasureSection { - pub text: String, - pub scale: PxScale, - 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)] +pub struct TextLayoutInfo { + pub glyphs: Vec, + pub size: Vec2, } -#[derive(Debug, Clone)] +// TODO: is there a way to do this without mutexes? +/// Size information for a corresponding [`Text`](crate::Text) component. +/// +/// Generated via [`TextPipeline::create_text_measure`]. pub struct TextMeasureInfo { - pub fonts: Vec, - pub scaled_fonts: Vec>, - pub sections: Vec, - pub text_alignment: TextAlignment, - pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, pub min_width_content_size: Vec2, pub max_width_content_size: Vec2, + buffer: Mutex, + font_system: Arc>, } -impl TextMeasureInfo { - fn new( - fonts: Vec, - scaled_fonts: Vec>, - sections: Vec, - text_alignment: TextAlignment, - linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, - ) -> Self { - let mut info = Self { - fonts, - scaled_fonts, - sections, - text_alignment, - linebreak_behaviour, - min_width_content_size: Vec2::ZERO, - max_width_content_size: Vec2::ZERO, - }; - - let section_texts = info.prepare_section_texts(); - let min = - info.compute_size_from_section_texts(§ion_texts, Vec2::new(0.0, f32::INFINITY)); - let max = info.compute_size_from_section_texts( - §ion_texts, - Vec2::new(f32::INFINITY, f32::INFINITY), - ); - info.min_width_content_size = min; - info.max_width_content_size = max; - info +impl std::fmt::Debug for TextMeasureInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TextMeasureInfo") + .field("min_width_content_size", &self.min_width_content_size) + .field("max_width_content_size", &self.max_width_content_size) + .field("buffer", &"_") + .field("font_system", &"_") + .finish() } +} - fn prepare_section_texts(&self) -> Vec { - self.sections - .iter() - .map(|section| SectionText { - font_id: section.font_id, - scale: section.scale, - text: §ion.text, - }) - .collect::>() +impl TextMeasureInfo { + pub fn compute_size(&self, bounds: Vec2) -> Vec2 { + let font_system = &mut self.font_system.try_lock().expect("Failed to acquire lock"); + let mut buffer = self.buffer.lock().expect("Failed to acquire the lock"); + buffer.set_size(font_system, bounds.x.ceil(), bounds.y.ceil()); + buffer_dimensions(&buffer) } +} - fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 { - let geom = SectionGeometry { - bounds: (bounds.x, bounds.y), - ..Default::default() - }; - let section_glyphs = glyph_brush_layout::Layout::default() - .h_align(self.text_alignment.into()) - .line_breaker(self.linebreak_behaviour) - .calculate_glyphs(&self.fonts, &geom, sections); - - let mut min_x: f32 = std::f32::MAX; - let mut min_y: f32 = std::f32::MAX; - let mut max_x: f32 = std::f32::MIN; - let mut max_y: f32 = std::f32::MIN; - - for sg in section_glyphs { - let scaled_font = &self.scaled_fonts[sg.section_index]; - let glyph = &sg.glyph; - // The fonts use a coordinate system increasing upwards so ascent is a positive value - // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, - // so we have to subtract from the baseline position to get the minimum and maximum values. - min_x = min_x.min(glyph.position.x); - min_y = min_y.min(glyph.position.y - scaled_font.ascent()); - max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); - max_y = max_y.max(glyph.position.y - scaled_font.descent()); +/// For the current line, +/// adds a span to the attributes list and pushes the text into the line string, +/// loading fonts into the [`Database`](cosmic_text::fontdb::Database) if required. +#[allow(clippy::too_many_arguments)] +fn add_span( + line_string: &mut String, + attrs_list: &mut AttrsList, + section: &TextSection, + section_index: usize, + text: &str, + font_system: &mut cosmic_text::FontSystem, + map_handle_to_font_id: &mut HashMap, + fonts: &Assets, +) { + let start = line_string.len(); + line_string.push_str(text); + let end = line_string.len(); + + let attrs = match section.style.font { + FontRef::Asset(ref font_handle) => { + let font_handle_id = font_handle.id(); + let face_id = map_handle_to_font_id + .entry(font_handle_id) + .or_insert_with(|| { + let font = fonts.get(font_handle).unwrap(); + let data = Arc::clone(&font.data); + font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(data)); + // TODO: it is assumed this is the right font face + // see https://github.com/pop-os/cosmic-text/issues/125 + // fontdb 0.14 returns the font ids from `load_font_source` + let face_id = font_system.db().faces().last().unwrap().id; + // TODO: below may be required if we need to offset by the baseline (TBC) + // see https://github.com/pop-os/cosmic-text/issues/123 + // let font = font_system.get_font(face_id).unwrap(); + // map_font_id_to_metrics + // .entry(face_id) + // .or_insert_with(|| font.as_swash().metrics(&[])); + face_id + }); + let face = font_system.db().face(*face_id).unwrap(); + + // TODO: validate this is the correct string to extract + let family_name = &face.families[0].0; + Attrs::new() + // TODO: validate that we can use metadata + .metadata(section_index) + .family(Family::Name(family_name)) + .stretch(face.stretch) + .style(face.style) + .weight(face.weight) + .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())) + } + FontRef::Query(ref query) => { + Attrs::new() + // TODO: validate that we can use metadata + .metadata(section_index) + .family(query.family.as_family()) + .stretch(query.stretch) + .style(query.style) + .weight(query.weight) + .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())) } + }; + + attrs_list.add_span(start..end, attrs); +} - Vec2::new(max_x - min_x, max_y - min_y) +/// Calculate the size of the text area for the given buffer. +fn buffer_dimensions(buffer: &Buffer) -> Vec2 { + // TODO: see https://github.com/pop-os/cosmic-text/issues/70 Let a Buffer figure out its height during set_size + // TODO: see https://github.com/pop-os/cosmic-text/issues/42 Request: Allow buffer dimensions to be undefined + let width = buffer + .layout_runs() + .map(|run| run.line_w) + .reduce(|max_w, w| max_w.max(w)) + .unwrap(); + // TODO: support multiple line heights / font sizes (once supported by cosmic text), see https://github.com/pop-os/cosmic-text/issues/64 + let line_height = buffer.metrics().line_height.ceil(); + let height = buffer.layout_runs().count() as f32 * line_height; + + // `width.ceil() + 0.001` gets around a rare text layout bug in the tonemapping example. + // See https://github.com/pop-os/cosmic-text/issues/134 + Vec2::new(width.ceil() + 0.001, height).ceil() +} + +/// An iterator over the paragraphs in the input text. +/// It is equivalent to [`core::str::Lines`] but follows [`unicode_bidi`] behavior. +// TODO: upstream to cosmic_text, see https://github.com/pop-os/cosmic-text/pull/124 +// TODO: create separate iterator that keeps the ranges, or simply use memory address introspection (as_ptr()) +// TODO: this breaks for lines ending in newlines, e.g. "foo\n" should split into ["foo", ""] but we actually get ["foo"] +pub struct BidiParagraphs<'text> { + text: &'text str, + info: std::vec::IntoIter, +} + +impl<'text> BidiParagraphs<'text> { + /// Create an iterator to split the input text into paragraphs + /// in accordance with [`unicode_bidi`] behavior. + pub fn new(text: &'text str) -> Self { + let info = unicode_bidi::BidiInfo::new(text, None); + let info = info.paragraphs.into_iter(); + Self { text, info } } +} - pub fn compute_size(&self, bounds: Vec2) -> Vec2 { - let sections = self.prepare_section_texts(); - self.compute_size_from_section_texts(§ions, bounds) +impl<'text> Iterator for BidiParagraphs<'text> { + type Item = &'text str; + + fn next(&mut self) -> Option { + let para = self.info.next()?; + let paragraph = &self.text[para.range]; + // `para.range` includes the newline that splits the line, so remove it if present + let mut char_indices = paragraph.char_indices(); + if let Some(i) = char_indices.next_back().and_then(|(i, c)| { + // `BidiClass::B` is a Paragraph_Separator (various newline characters) + (unicode_bidi::BidiClass::B == unicode_bidi::bidi_class(c)).then_some(i) + }) { + Some(¶graph[0..i]) + } else { + Some(paragraph) + } } } + +/// Helper method to acquire a font system mutex. +#[inline(always)] +fn acquire_font_system( + font_system: &mut FontSystem, +) -> Result, TextError> { + font_system + .0 + .try_lock() + .map_err(|_| TextError::FailedToAcquireMutex) +} diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index eaaf58d2b5273..f5db886244408 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -6,10 +6,18 @@ use bevy_utils::default; use serde::{Deserialize, Serialize}; use crate::{Font, DEFAULT_FONT_HANDLE}; +// TODO: reexport cosmic_text and these types in the prelude +pub use cosmic_text::{ + FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, +}; +/// A component that is the entry point for rendering text. +/// +/// It contains all of the text value and styling information. #[derive(Component, Debug, Clone, 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. @@ -43,7 +51,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, /// }, @@ -52,7 +60,7 @@ impl Text { /// let hello_bevy = Text::from_section( /// "hello bevy!", /// TextStyle { - /// font: font_handle, + /// font: font_handle.into(), /// font_size: 60.0, /// color: Color::WHITE, /// }, @@ -79,7 +87,7 @@ impl Text { /// TextSection::new( /// "Hello, ", /// TextStyle { - /// font: font_handle.clone(), + /// font: font_handle.clone().into(), /// font_size: 60.0, /// color: Color::BLUE, /// }, @@ -87,7 +95,7 @@ impl Text { /// TextSection::new( /// "World!", /// TextStyle { - /// font: font_handle, + /// font: font_handle.into(), /// font_size: 60.0, /// color: Color::RED, /// }, @@ -108,6 +116,7 @@ impl Text { } } +/// Contains the value of the text in a section and how it should be styled. #[derive(Debug, Default, Clone, FromReflect, Reflect)] pub struct TextSection { pub value: String, @@ -147,19 +156,10 @@ pub enum TextAlignment { Right, } -impl From for glyph_brush_layout::HorizontalAlign { - fn from(val: TextAlignment) -> Self { - match val { - TextAlignment::Left => glyph_brush_layout::HorizontalAlign::Left, - TextAlignment::Center => glyph_brush_layout::HorizontalAlign::Center, - TextAlignment::Right => glyph_brush_layout::HorizontalAlign::Right, - } - } -} - +/// Describes the style of a [`TextSection`]. #[derive(Clone, Debug, Reflect, FromReflect)] pub struct TextStyle { - pub font: Handle, + pub font: FontRef, pub font_size: f32, pub color: Color, } @@ -167,7 +167,7 @@ pub struct TextStyle { impl Default for TextStyle { fn default() -> Self { Self { - font: DEFAULT_FONT_HANDLE.typed(), + font: FontRef::Asset(DEFAULT_FONT_HANDLE.typed()), font_size: 12.0, color: Color::WHITE, } @@ -188,11 +188,127 @@ pub enum BreakLineOn { AnyCharacter, } -impl From for glyph_brush_layout::BuiltInLineBreaker { - fn from(val: BreakLineOn) -> Self { - match val { - BreakLineOn::WordBoundary => glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker, - BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker, +/// Identifies a font to use, which is either stored as an [`Asset`](bevy_asset::Asset) or loaded directly from the user's system. +#[derive(Clone, Debug, Reflect, FromReflect)] +pub enum FontRef { + /// A reference to a font loaded as a bevy asset. + Asset(Handle), + /// A reference to a font queried by font family and attributes. + /// This is useful for example for fonts that are not loaded as a bevy asset, + /// such as system fonts. + // TODO: Support Reflect? + Query(#[reflect(ignore)] FontQuery), +} + +impl From> for FontRef { + fn from(handle: Handle) -> Self { + Self::Asset(handle) + } +} + +/// Queries for a font from those already loaded. +/// +/// ``` +/// # use bevy_text::{FontQuery, FontWeight, TextStyle}; +/// +/// let fira_sans_bold = FontQuery::family("FiraSans").weight(FontWeight::BOLD); +/// +/// let text_style = TextStyle { +/// font: fira_sans_bold.into(), +/// ..Default::default() +/// }; +/// ``` +#[derive(Clone, Debug)] +pub struct FontQuery { + /// The font family. See [`cosmic_text::fontdb::Family`] for details. + pub family: FontFamily, + /// The stretch (or width) of the font face in this family, e.g. condensed. + /// See [`cosmic_text::fontdb::Stretch`] for details. + pub stretch: FontStretch, + /// The style of the font face in this family, e.g. italic. + /// See [`cosmic_text::fontdb::Style`] for details. + pub style: FontStyle, + /// The weight of the font face in this family, e.g. bold. + /// See [`cosmic_text::fontdb::Weight`] for details. + pub weight: FontWeight, +} + +impl FontQuery { + pub fn sans_serif() -> Self { + Self { + family: FontFamily::SansSerif, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn serif() -> Self { + Self { + family: FontFamily::Serif, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn fantasy() -> Self { + Self { + family: FontFamily::Fantasy, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn cursive() -> Self { + Self { + family: FontFamily::Cursive, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn monospace() -> Self { + Self { + family: FontFamily::Monospace, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn family>(name: S) -> Self { + Self { + family: FontFamily::Name(name.as_ref().to_string()), + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), } } + + pub fn stretch(self, stretch: FontStretch) -> Self { + Self { stretch, ..self } + } + + pub fn style(self, style: FontStyle) -> Self { + Self { style, ..self } + } + + pub fn weight(self, weight: FontWeight) -> Self { + Self { weight, ..self } + } +} + +impl Default for FontQuery { + fn default() -> Self { + Self::sans_serif() + } +} + +impl From for FontRef { + fn from(query: FontQuery) -> Self { + Self::Query(query) + } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index a17a1c670950e..2e24abaf518c1 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -129,7 +129,7 @@ pub fn extract_text2d_sprite( entity, transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, - rect: Some(atlas.textures[atlas_info.glyph_index]), + rect: Some(atlas.textures[atlas_info.location.glyph_index]), custom_size: None, image_handle_id: atlas.texture.id(), flip_x: false, @@ -197,7 +197,7 @@ pub fn update_text2d_layout( // queue for further processing queue.insert(entity); } - Err(e @ TextError::FailedToAddGlyph(_)) => { + Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { panic!("Fatal error when processing text: {e}."); } Ok(info) => *text_layout_info = info, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index f203c4b227565..42a2a4e85bdcd 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -323,7 +323,7 @@ pub fn extract_text_uinodes( } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let mut rect = atlas.textures[atlas_info.glyph_index]; + let mut rect = atlas.textures[atlas_info.location.glyph_index]; rect.min *= inverse_scale_factor; rect.max *= inverse_scale_factor; extracted_uinodes.uinodes.push(ExtractedUiNode { diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c56ae98bc5997..c89b8714ff88e 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -39,7 +39,6 @@ impl Default for TextFlags { } } -#[derive(Clone)] pub struct TextMeasure { pub info: TextMeasureInfo, } @@ -101,7 +100,7 @@ fn create_text_measure( // Try again next frame text_flags.needs_new_measure_func = true; } - Err(e @ TextError::FailedToAddGlyph(_)) => { + Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { panic!("Fatal error when processing text: {e}."); } }; @@ -194,7 +193,7 @@ fn queue_text( // 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(_) | e @ TextError::FailedToAcquireMutex) => { panic!("Fatal error when processing text: {e}."); } Ok(info) => { diff --git a/docs/cargo_features.md b/docs/cargo_features.md index bf6a5e55fc643..0b4fc0f9f5ec0 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -61,7 +61,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 8fe1b4b05b642..78b8ca0fd657b 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -34,7 +34,7 @@ struct AnimateScale; fn setup(mut commands: Commands, asset_server: Res) { let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let text_style = TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 60.0, color: Color::WHITE, }; @@ -68,7 +68,7 @@ fn setup(mut commands: Commands, asset_server: Res) { )); // Demonstrate text wrapping let slightly_smaller_text_style = TextStyle { - font, + font: font.into(), font_size: 42.0, color: Color::WHITE, }; diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 9ae3c84dc8fc6..9af1d4952ed57 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -474,27 +474,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::H) { *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"); @@ -598,6 +604,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/README.md b/examples/README.md index 4751f537cfabf..86e43ebe46489 100644 --- a/examples/README.md +++ b/examples/README.md @@ -343,6 +343,7 @@ Example | Description [Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior [Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. +[System Fonts](../examples/ui/system_fonts.rs) | Demonstrates using system fonts. [Text](../examples/ui/text.rs) | Illustrates creating and updating text [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping diff --git a/examples/games/contributors.rs b/examples/games/contributors.rs index 1ca223c35d0b7..d4bb43adf066c 100644 --- a/examples/games/contributors.rs +++ b/examples/games/contributors.rs @@ -144,13 +144,13 @@ fn setup(mut commands: Commands, asset_server: Res) { TextSection::new( "Contributor showcase", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 60.0, color: Color::WHITE, }, ), TextSection::from_style(TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 60.0, color: Color::WHITE, }), diff --git a/examples/input/text_input.rs b/examples/input/text_input.rs index 8140325094df2..4d7f0cdfe6efd 100644 --- a/examples/input/text_input.rs +++ b/examples/input/text_input.rs @@ -33,7 +33,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "IME Enabled: ".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 20.0, color: Color::WHITE, }, @@ -41,7 +41,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "false\n".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 30.0, color: Color::WHITE, }, @@ -49,7 +49,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "IME Active: ".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 20.0, color: Color::WHITE, }, @@ -57,7 +57,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "false\n".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 30.0, color: Color::WHITE, }, @@ -65,7 +65,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "click to toggle IME, press return to start a new line\n\n".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 18.0, color: Color::WHITE, }, @@ -73,7 +73,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "".to_string(), style: TextStyle { - font, + font: font.into(), font_size: 25.0, color: Color::WHITE, }, @@ -91,7 +91,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { text: Text::from_section( "".to_string(), TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 100.0, color: Color::WHITE, }, diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 64ffe57ae76df..7c6e974636085 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -35,7 +35,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { TextSection { value: "text".repeat(i), style: TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: (4 + i % 10) as f32, color: Color::BLUE, }, @@ -43,7 +43,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { TextSection { value: "pipeline".repeat(i), style: TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: (4 + i % 11) as f32, color: Color::YELLOW, }, diff --git a/examples/ui/button.rs b/examples/ui/button.rs index aed15cb1142fe..39e71095078d8 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -75,7 +75,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 40.0, color: Color::rgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/flex_layout.rs b/examples/ui/flex_layout.rs index 6e7c736ee24e1..c37be6c398a61 100644 --- a/examples/ui/flex_layout.rs +++ b/examples/ui/flex_layout.rs @@ -177,7 +177,7 @@ fn spawn_nested_text_bundle( builder.spawn(TextBundle::from_section( text, TextStyle { - font, + font: font.into(), font_size: 24.0, color: Color::BLACK, }, diff --git a/examples/ui/font_atlas_debug.rs b/examples/ui/font_atlas_debug.rs index 54f1fe42f975d..51a2b7714390d 100644 --- a/examples/ui/font_atlas_debug.rs +++ b/examples/ui/font_atlas_debug.rs @@ -92,7 +92,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut state: ResM parent.spawn(TextBundle::from_section( "a", TextStyle { - font: font_handle, + font: font_handle.into(), font_size: 60.0, color: Color::YELLOW, }, diff --git a/examples/ui/grid.rs b/examples/ui/grid.rs index 9012e198d8708..e9f1421c8e914 100644 --- a/examples/ui/grid.rs +++ b/examples/ui/grid.rs @@ -142,7 +142,7 @@ fn spawn_layout(mut commands: Commands, asset_server: Res) { builder.spawn(TextBundle::from_section( "Sidebar", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 24.0, color: Color::WHITE, }, @@ -150,7 +150,7 @@ fn spawn_layout(mut commands: Commands, asset_server: Res) { builder.spawn(TextBundle::from_section( "A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely.", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 16.0, color: Color::WHITE, }, @@ -226,7 +226,7 @@ fn spawn_nested_text_bundle(builder: &mut ChildBuilder, font: Handle, text builder.spawn(TextBundle::from_section( text, TextStyle { - font, + font: font.into(), font_size: 24.0, color: Color::BLACK, }, diff --git a/examples/ui/overflow.rs b/examples/ui/overflow.rs index 12c922c92d993..5e036206e77ef 100644 --- a/examples/ui/overflow.rs +++ b/examples/ui/overflow.rs @@ -15,7 +15,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 20.0, color: Color::WHITE, }; diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index 8b71856616895..7ba4a484e101f 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -109,7 +109,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ] .join(" · "), TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 18.0, color: Color::WHITE, }, @@ -184,7 +184,7 @@ fn spawn_text( parent.spawn(TextBundle::from_section( "Bevy", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 120.0, color: Color::WHITE, }, diff --git a/examples/ui/relative_cursor_position.rs b/examples/ui/relative_cursor_position.rs index 57eba52064ed9..45061a586b79e 100644 --- a/examples/ui/relative_cursor_position.rs +++ b/examples/ui/relative_cursor_position.rs @@ -45,7 +45,7 @@ fn setup(mut commands: Commands, asset_server: Res) { text: Text::from_section( "(0.0, 0.0)", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 40.0, color: Color::rgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index 562a892bd93c1..701e8c66f5479 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -43,7 +43,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 40.0, color: Color::rgb(0.9, 0.9, 0.9), }; diff --git a/examples/ui/system_fonts.rs b/examples/ui/system_fonts.rs new file mode 100644 index 0000000000000..6543dbef7912f --- /dev/null +++ b/examples/ui/system_fonts.rs @@ -0,0 +1,121 @@ +//! This example demonstrates using system fonts, which are already installed on the user's computer. +//! +//! System fonts won't always be present: if they're not found, a fallback will be used instead. +//! +//! If you need consistent font rendering for aesthetic reasons, you should package and ship your own font as an asset instead. + +use bevy::{ + prelude::*, + text::{FontQuery, TextPipeline}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, mut text_pipeline: ResMut) { + text_pipeline.load_system_fonts(); + commands.spawn(Camera2dBundle::default()); + + let text_style = TextStyle { + font_size: 42.0, + color: Color::WHITE, + ..default() + }; + let mut sections = vec![]; + + // the default font is sans-serif + sections.push(TextSection { + value: "(The default font)\n".to_string(), + style: TextStyle { + font: FontQuery::default().into(), + ..text_style + }, + }); + + // sans-serif + sections.push(TextSection { + value: "sans-serif\n".to_string(), + style: TextStyle { + font: FontQuery::sans_serif().into(), + ..text_style + }, + }); + + // serif + sections.push(TextSection { + value: "serif\n".to_string(), + style: TextStyle { + font: FontQuery::serif().into(), + ..text_style + }, + }); + + // fantasy + sections.push(TextSection { + value: "fantasy\n".to_string(), + style: TextStyle { + font: FontQuery::fantasy().into(), + ..text_style + }, + }); + + // cursive + sections.push(TextSection { + value: "cursive\n".to_string(), + style: TextStyle { + font: FontQuery::cursive().into(), + ..text_style + }, + }); + + // monospace + sections.push(TextSection { + value: "monospace\n".to_string(), + style: TextStyle { + font: FontQuery::monospace().into(), + ..text_style + }, + }); + + // you can also refer to families by name + for family in [ + "Arial", + "Comic Sans MS", + "Impact", + "Courier New", + "Times New Roman", + "(A fallback when not found)", + ] { + sections.push(TextSection { + value: family.to_string() + "\n", + style: TextStyle { + font: FontQuery::family(family).into(), + ..text_style + }, + }) + } + + // bidirectional text + sections.push(TextSection { + value: "We can even render اللغة العربية and\n".to_string(), + style: TextStyle { + font: FontQuery::serif().into(), + ..text_style + }, + }); + + // and emojis + sections.push(TextSection { + value: "emojis: 🐣🐤🐥🐔🐓🦃🐦🐧🕊️🦅🦆🦢🦉🦩🦚🦜\n".to_string(), + style: TextStyle { + font: FontQuery::cursive().into(), + ..text_style + }, + }); + + commands.spawn(TextBundle::from_sections(sections)); +} diff --git a/examples/ui/text.rs b/examples/ui/text.rs index 841358c2bd3b2..863ac986aa681 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -35,7 +35,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // Accepts a `String` or any type that converts into a `String`, such as `&str` "hello\nbevy!", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 100.0, color: Color::WHITE, }, @@ -57,13 +57,13 @@ fn setup(mut commands: Commands, asset_server: Res) { TextSection::new( "FPS: ", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 60.0, color: Color::WHITE, }, ), TextSection::from_style(TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 60.0, color: Color::GOLD, }), diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index e1756d3792074..053fbe9638c16 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -31,7 +31,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "This is\ntext with\nline breaks\nin the top left", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 50.0, color: Color::WHITE, }, @@ -46,7 +46,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { commands.spawn(TextBundle::from_section( "This text is very long, has a limited width, is centered, is positioned in the top right and is also colored pink.", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 50.0, color: Color::rgb(0.8, 0.2, 0.7), }, @@ -65,7 +65,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextSection::new( "This text changes in the bottom right", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::WHITE, }, @@ -73,33 +73,33 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextSection::new( "\nThis text changes in the bottom right - ", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::RED, }, ), TextSection::from_style(TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::ORANGE_RED, }), TextSection::new( " fps, ", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::YELLOW, }, ), TextSection::from_style(TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::GREEN, }), TextSection::new( " ms/frame", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::BLUE, }, @@ -117,7 +117,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "This\ntext has\nline breaks and also a set width in the bottom left", TextStyle { - font, + font: font.into(), font_size: 50.0, color: Color::WHITE, }, diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 0ef93cf6c25cd..dd4ccc1c6c0a4 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -16,7 +16,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 14.0, color: Color::WHITE, }; diff --git a/examples/ui/transparency_ui.rs b/examples/ui/transparency_ui.rs index 2a6fe7f9b93d4..e8301b01d7ded 100644 --- a/examples/ui/transparency_ui.rs +++ b/examples/ui/transparency_ui.rs @@ -43,7 +43,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button 1", TextStyle { - font: font_handle.clone(), + font: font_handle.clone().into(), font_size: 40.0, // Alpha channel of the color controls transparency. color: Color::rgba(1.0, 1.0, 1.0, 0.2), @@ -69,7 +69,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button 2", TextStyle { - font: font_handle.clone(), + font: font_handle.clone().into(), font_size: 40.0, // Alpha channel of the color controls transparency. color: Color::rgba(1.0, 1.0, 1.0, 0.2), diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index d338446f9136b..e3963e8c6bd71 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -64,7 +64,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "Text Example", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 30.0, color: Color::WHITE, }, @@ -99,7 +99,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "Scrolling list", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 25., color: Color::WHITE, }, @@ -142,7 +142,8 @@ fn setup(mut commands: Commands, asset_server: Res) { format!("Item {i}"), TextStyle { font: asset_server - .load("fonts/FiraSans-Bold.ttf"), + .load("fonts/FiraSans-Bold.ttf") + .into(), font_size: 20., color: Color::WHITE, }, diff --git a/examples/ui/ui_scaling.rs b/examples/ui/ui_scaling.rs index 2b4c8551e6e83..6c02c40524515 100644 --- a/examples/ui/ui_scaling.rs +++ b/examples/ui/ui_scaling.rs @@ -28,7 +28,7 @@ fn setup(mut commands: Commands, asset_server: ResMut) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 16., color: Color::BLACK, }; diff --git a/examples/ui/window_fallthrough.rs b/examples/ui/window_fallthrough.rs index daf744aeae5ee..4136901d37783 100644 --- a/examples/ui/window_fallthrough.rs +++ b/examples/ui/window_fallthrough.rs @@ -32,7 +32,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // Accepts a `String` or any type that converts into a `String`, such as `&str` "Hit 'P' then scroll/click around!", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 100.0, // Nice and big so you can see it! color: Color::WHITE, },