From 9b601870a0f3b3e10014ced466b1c521436e1a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Fri, 20 Oct 2023 22:02:12 +0200 Subject: [PATCH 01/47] Copied each affected file from cosmic-text branch, to fix some weird issue with rebasing Co-authored-by: tigregalis Co-authored-by: tigregalis <38416468+tigregalis@users.noreply.github.com --- Cargo.toml | 13 +- crates/bevy_internal/Cargo.toml | 3 - crates/bevy_sprite/src/texture_atlas.rs | 40 +- crates/bevy_text/Cargo.toml | 8 +- crates/bevy_text/src/error.rs | 5 +- crates/bevy_text/src/font.rs | 54 +- crates/bevy_text/src/font_atlas.rs | 102 ++-- crates/bevy_text/src/font_atlas_set.rs | 167 ++++-- crates/bevy_text/src/font_loader.rs | 19 +- crates/bevy_text/src/glyph.rs | 54 ++ crates/bevy_text/src/lib.rs | 45 +- crates/bevy_text/src/pipeline.rs | 671 ++++++++++++++++++++---- crates/bevy_text/src/text.rs | 179 +++++-- crates/bevy_text/src/text2d.rs | 10 +- crates/bevy_ui/src/widget/text.rs | 32 +- docs/cargo_features.md | 1 - examples/2d/text2d.rs | 4 +- examples/3d/tonemapping.rs | 26 +- examples/README.md | 1 + examples/input/text_input.rs | 8 +- examples/stress_tests/text_pipeline.rs | 4 +- examples/ui/button.rs | 2 +- examples/ui/flex_layout.rs | 2 +- examples/ui/font_atlas_debug.rs | 2 +- examples/ui/grid.rs | 6 +- examples/ui/overflow_debug.rs | 34 +- examples/ui/relative_cursor_position.rs | 2 +- examples/ui/size_constraints.rs | 2 +- examples/ui/system_fonts.rs | 121 +++++ examples/ui/text.rs | 17 +- examples/ui/text_debug.rs | 2 +- examples/ui/text_wrap_debug.rs | 2 +- examples/ui/transparency_ui.rs | 4 +- examples/ui/ui.rs | 4 +- examples/ui/window_fallthrough.rs | 2 +- 35 files changed, 1242 insertions(+), 406 deletions(-) create mode 100644 crates/bevy_text/src/glyph.rs create mode 100644 examples/ui/system_fonts.rs diff --git a/Cargo.toml b/Cargo.toml index 53e5114acedb6..accda717bca50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -268,9 +268,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"] @@ -2726,6 +2723,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 36f3597fbf3c1..e30c07062084e 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -93,9 +93,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/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 cdd26c618e174..7749785df45b8 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,8 +28,8 @@ bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } # other -ab_glyph = "0.2.6" -glyph_brush_layout = "0.2.1" +anyhow = "1.0.4" +cosmic-text = "0.8.0" thiserror = "1.0" serde = { version = "1", features = ["derive"] } @@ -43,3 +42,6 @@ workspace = true [package.metadata.docs.rs] rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true +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 780995aa299aa..872059e4e23bd 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,53 +1,19 @@ -use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph}; 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`](bevy_asset::Asset) that contains the data for a loaded font, if loaded as an asset. +/// +/// Loaded by [`FontLoader`](crate::FontLoader). +#[derive(Debug, TypePath, Clone, Asset)] 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(); - // 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, - ) + 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 4d281846569ec..fafb2cfe8bd00 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, Vec2}; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, @@ -9,58 +8,27 @@ 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 - } -} - -/// A font glyph placed at a specific sub-pixel offset. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct PlacedGlyph { - /// The font glyph ID. - pub glyph_id: GlyphId, - /// The sub-pixel offset of the placed glyph. - pub subpixel_offset: SubpixelOffset, -} +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, + /// 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, + /// the texture where this font atlas is located pub texture: Handle, } impl FontAtlas { pub fn new( textures: &mut Assets, - texture_atlases: &mut Assets, - size: UVec2, + texture_atlases_layout: &mut Assets, + size: Vec2, ) -> FontAtlas { let texture = textures.add(Image::new_fill( Extent3d { @@ -74,21 +42,21 @@ 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), texture, } } - pub fn get_glyph_index(&self, glyph: &PlacedGlyph) -> Option { - self.glyph_to_atlas_index.get(glyph).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: &PlacedGlyph) -> bool { - self.glyph_to_atlas_index.contains_key(glyph) + 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. @@ -104,23 +72,27 @@ impl FontAtlas { /// modified. pub fn add_glyph( &mut self, - textures: &mut Assets, - atlas_layouts: &mut Assets, - glyph: &PlacedGlyph, - glyph_texture: &Image, + textures: &mut Assets,crates/bevy_text/src/font_atlas_set.rs + texture_atlases: &mut Assets, + cache_key: cosmcrates/bevy_text/src/font_atlas_set.rsic_text::CacheKey, + texture: &Image, + offset: IVec2, ) -> bool { - let Some(atlas_layout) = atlas_layouts.get_mut(&self.texture_atlas) else { - return false; - }; - let Some(atlas_texture) = textures.get_mut(&self.texture) else { - return false; - }; - if let Some(index) = self.dynamic_texture_atlas_builder.add_texture( - atlas_layout, - glyph_texture, - atlas_texture, + let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); + + if let Some(glyph_index) = self.dynamic_texture_atlas_builder.add_texture( + texture_atlas, + textures, + texture, + &self.texture, ) { - self.glyph_to_atlas_index.insert(*glyph, 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 a21de603e7e4d..aa9e263dc9072 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,15 +1,21 @@ -use crate::{error::TextError, Font, FontAtlas, PlacedGlyph}; -use ab_glyph::{GlyphId, OutlinedGlyph, Point}; -use bevy_asset::{AssetEvent, AssetId}; -use bevy_asset::{Assets, Handle}; +use crate::{error::TextError, Font, FontAtlas}; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::prelude::*; -use bevy_math::{FloatOrd, UVec2}; -use bevy_reflect::Reflect; -use bevy_render::texture::Image; +use bevy_ecs::{ + event::EventReader, + system::{ResMut, Resource}, +}; +use bevy_math::{FloatOrd, IVec2, UVec2, Vec2}; +use bevy_reflect::{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)] pub struct FontAtlasSets { @@ -22,6 +28,10 @@ impl FontAtlasSets { let id: AssetId = id.into(); self.sets.get(&id) } + pub fn get_mut(&mut self, id: impl Into>) -> Option<&mut FontAtlasSet> { + let id: AssetId = id.into(); + self.sets.get_mut(&id) + } } pub fn remove_dropped_font_atlas_sets( @@ -36,17 +46,26 @@ pub fn remove_dropped_font_atlas_sets( } } +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(TypePath, Asset)] pub struct FontAtlasSet { font_atlases: HashMap>, } -#[derive(Debug, Clone, Reflect)] -pub struct GlyphAtlasInfo { - pub texture_atlas: Handle, - pub texture: Handle, - pub glyph_index: usize, -} - impl Default for FontAtlasSet { fn default() -> Self { FontAtlasSet { @@ -60,17 +79,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| { - let placed_glyph = PlacedGlyph { - glyph_id, - subpixel_offset: glyph_position.into(), - }; - font_atlas - .iter() - .any(|atlas| atlas.has_glyph(&placed_glyph)) + font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)) }) } @@ -78,22 +91,31 @@ 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 placed_glyph = PlacedGlyph { - glyph_id: glyph.id, - subpixel_offset: glyph.position.into(), - }; - let font_size = glyph.scale.y; let font_atlases = self .font_atlases - .entry(FloatOrd(font_size)) - .or_insert_with(|| vec![FontAtlas::new(textures, texture_atlases, UVec2::splat(512))]); + .entry(layout_glyph.cache_key.font_size_bits) + .or_insert_with(|| { + vec![FontAtlas::new( + textures, + texture_atlases, + Vec2::splat(512.0), + )] + }); - 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, &placed_glyph, &glyph_texture) + atlas.add_glyph( + textures, + texture_atlases, + layout_glyph.cache_key, + &glyph_texture, + offset, + ) }; if !font_atlases.iter_mut().any(add_char_to_font_atlas) { // Find the largest dimension of the glyph, either its width or its height @@ -112,39 +134,34 @@ impl FontAtlasSet { if !font_atlases.last_mut().unwrap().add_glyph( textures, texture_atlases, - &placed_glyph, + layout_glyph.cache_key, &glyph_texture, + offset, ) { - return Err(TextError::FailedToAddGlyph(placed_glyph.glyph_id)); + return Err(TextError::FailedToAddGlyph(layout_glyph.cache_key.glyph_id)); } } - Ok(self.get_glyph_atlas_info(font_size, &placed_glyph).unwrap()) + Ok(self.get_glyph_atlas_info(layout_glyph.cache_key).unwrap()) } pub fn get_glyph_atlas_info( &mut self, - font_size: f32, - placed_glyph: &PlacedGlyph, + 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(placed_glyph).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())) }) - .map(|(glyph_index, texture_atlas, texture)| GlyphAtlasInfo { + .map(|(location, texture_atlas)| GlyphAtlasInfo { texture_atlas, - texture, - glyph_index, + location, }) }) } @@ -154,8 +171,50 @@ impl FontAtlasSet { self.font_atlases.len() } - /// Returns `true` if the font atlas set contains no elements - pub fn is_empty(&self) -> bool { - self.font_atlases.is_empty() + /// 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, + 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..af871530b59b6 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,5 +1,5 @@ use crate::Font; -use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; +use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext, LoadedAsset}; use thiserror::Error; #[derive(Default)] @@ -12,9 +12,6 @@ pub enum FontLoaderError { /// 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 { @@ -25,11 +22,15 @@ impl AssetLoader for FontLoader { &'a self, reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext<'_>, - ) -> Result { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - Ok(Font::try_from_bytes(bytes)?) + load_context: &'a mut LoadContext, + ) -> bevy_utils::BoxedFuture<'a, Result> { + Box::pin(async move { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let font = Font::from_bytes(bytes.into()); + // load_context.set_default_asset(LoadedAsset::new(font)); + 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..607e3a2ab049d --- /dev/null +++ b/crates/bevy_text/src/glyph.rs @@ -0,0 +1,54 @@ +//! This module exports types related to rendering glyphs. + +use bevy_asset::Handle; +use bevy_math::{IVec2, Vec2}; +use bevy_reflect::Reflect; +use bevy_sprite::{TextureAtlas, 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, + /// 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, Reflect)] +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, 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..4cea5ab6a9e22 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -1,18 +1,41 @@ -// 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`](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; mod font; mod font_atlas; mod font_atlas_set; mod font_loader; -mod glyph_brush; +mod glyph; mod pipeline; mod text; mod text2d; @@ -22,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::*; @@ -120,7 +143,7 @@ impl Plugin for TextPlugin { app, Handle::default(), "FiraMono-subset.ttf", - |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } + |bytes: &[u8], _path: String| { Font::from_bytes(bytes.to_vec()) } ); } } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 298857997e896..64b6c4af0504e 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,51 +1,250 @@ -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 std::sync::{Arc, Mutex}; + use bevy_asset::{AssetId, Assets, Handle}; -use bevy_ecs::component::Component; -use bevy_ecs::prelude::ReflectComponent; -use bevy_ecs::system::Resource; +use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; use bevy_math::Vec2; -use bevy_reflect::prelude::ReflectDefault; -use bevy_reflect::Reflect; +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}; +use bevy_utils::{ + tracing::{error, info, warn}, + HashMap, +}; -#[derive(Default, Resource)] -pub struct TextPipeline { - brush: GlyphBrush, - map_font_id: HashMap, FontId>, +use cosmic_text::{Attrs, AttrsList, Buffer, BufferLine, Family, Metrics, Wrap}; + +use crate::{ + error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasSets, FontRef, JustifyText, + PositionedGlyph, TextSection, TextSettings, YAxisOrientation, +}; + +// 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`] 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, 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) [`HandleId`]. + map_handle_to_font_id: HashMap, cosmic_text::fontdb::ID>, + /// 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.id(), 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, + BreakLineOn::NoWrap => Wrap::None, + }, + ); + + // 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: f32, + scale_factor: f64, + // TODO: Implement text alignment properly text_alignment: JustifyText, linebreak_behavior: BreakLineOn, bounds: Vec2, @@ -55,42 +254,18 @@ impl TextPipeline { text_settings: &TextSettings, 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, - }; - - Ok(section) - }) - .collect::, _>>()?; - - let section_glyphs = - self.brush - .compute_glyphs(§ions, bounds, text_alignment, linebreak_behavior)?; - - if section_glyphs.is_empty() { + if sections.is_empty() { return Ok(TextLayoutInfo::default()); } - let size = compute_text_bounds(§ion_glyphs, |index| scaled_fonts[index]).size(); + let buffer = + self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; + let box_size = buffer_dimensions(&buffer); let h_limit = if bounds.x.is_finite() { bounds.x } else { - size.x + box_size.x }; let h_anchor = match text_alignment { @@ -99,41 +274,210 @@ impl TextPipeline { JustifyText::Right => h_limit * 1.0, } .floor(); + let font_system = &mut acquire_font_system(&mut self.font_system)?; + let swash_cache = &mut self.swash_cache.0; - let glyphs = self.brush.process_glyphs( - section_glyphs, - §ions, - font_atlas_sets, - fonts, - texture_atlases, - textures, - text_settings, - y_axis_orientation, - h_anchor, - )?; + 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; + + // TODO(totalkrill): this is probably very wrong, investigate the + // cause, instead of "what makes it compile" + let font_atlas_set: &mut FontAtlasSet = match sections[section_index].style.font { + FontRef::Asset(ref font_handle) => { + let handle: Handle = font_handle.clone_weak(); + font_atlas_sets.sets.entry(handle.id()).or_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_default(); + + font_atlas_sets + .get_mut(handle.clone().untyped().id()) + .unwrap() + } + }; + + 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 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()); + + // 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 { + JustifyText::Left => 0.0, + JustifyText::Center => (box_size.x - line_w) / 2.0, + JustifyText::Right => box_size.x - line_w, + }; + let y = match y_axis_orientation { + YAxisOrientation::TopToBottom => y, + YAxisOrientation::BottomToTop => box_size.y - 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 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, - 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, + // TODO: not currently required + _text_alignment: JustifyText, + linebreak_behavior: BreakLineOn, + ) -> Result { + const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); + const MAX_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(f32::INFINITY, f32::INFINITY); + + let mut buffer = self.create_buffer( + fonts, + sections, + 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: 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 { + pub glyphs: Vec, + pub size: Vec2, } -#[derive(Debug, Clone, Default)] +// 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: Box<[ab_glyph::FontArc]>, - pub sections: Box<[TextMeasureSection]>, - pub justification: JustifyText, - pub linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker, - pub min: Vec2, - pub max: Vec2, + pub min_width_content_size: Vec2, + pub max_width_content_size: Vec2, + buffer: Mutex, + font_system: Arc>, +} + +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() + } } impl TextMeasureInfo { @@ -189,31 +533,146 @@ impl TextMeasureInfo { } 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() + 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) + } +} + +/// 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, cosmic_text::fontdb::ID>, + 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); +} + +/// 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 } } } -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, + +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 15532e36c1491..2ebc1e111ec41 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -5,11 +5,20 @@ use bevy_reflect::prelude::*; use bevy_utils::default; use serde::{Deserialize, Serialize}; +//use crate DEFAULT_FONT_HANDLE; use crate::Font; +// 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, 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 +42,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 +51,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 +79,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 +87,7 @@ impl Text { /// TextSection::new( /// "World!", /// TextStyle { - /// font: font_handle, + /// font: font_handle.into(), /// font_size: 60.0, /// color: RED.into(), /// }, @@ -106,6 +115,7 @@ 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 { pub value: String, @@ -170,30 +180,9 @@ pub enum JustifyText { Right, } -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, - } - } -} - #[derive(Clone, Debug, Reflect)] pub struct TextStyle { - /// If this 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. - /// * otherwise no text will be rendered. - pub font: Handle, - /// The vertical height of rasterized glyphs in the font atlas in pixels. - /// - /// This is multiplied by the window scale factor and `UiScale`, but not the text entity - /// transform or camera projection. - /// - /// 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: FontRef, pub font_size: f32, pub color: Color, } @@ -202,7 +191,7 @@ impl Default for TextStyle { fn default() -> Self { Self { font: Default::default(), - font_size: 24.0, + font_size: 12.0, color: Color::WHITE, } } @@ -226,15 +215,133 @@ pub enum BreakLineOn { 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, +/// 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)] +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 Default for FontRef { + fn default() -> Self { + Self::Asset(Default::default()) + } +} + +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 0b3ae44dec6f6..8f32969893c0c 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -124,7 +124,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 +149,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]), custom_size: None, image_handle_id: atlas_info.texture.id(), flip_x: false, @@ -222,12 +222,12 @@ 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(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; } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 2fd25552d2523..39e963510cee5 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -42,7 +42,6 @@ impl Default for TextFlags { } } -#[derive(Clone)] pub struct TextMeasure { pub info: TextMeasureInfo, } @@ -72,8 +71,8 @@ impl Measure for TextMeasure { .map_or_else( || match available_width { AvailableSpace::Definite(_) => self.info.compute_size(Vec2::new(x, f32::MAX)), - AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), - AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), + AvailableSpace::MinContent => Vec2::new(x, self.info.min_width_content_size.y), + AvailableSpace::MaxContent => Vec2::new(x, self.info.max_width_content_size.y), }, |y| Vec2::new(x, y), ) @@ -86,10 +85,17 @@ fn create_text_measure( fonts: &Assets, scale_factor: f32, text: Ref, + text_pipeline: &mut TextPipeline, mut content_size: Mut, mut text_flags: Mut, ) { - match TextMeasureInfo::from_text(&text, fonts, scale_factor) { + match text_pipeline.create_text_measure( + fonts, + &text.sections, + scale_factor, + text.alignment, + text.linebreak_behavior, + ) { Ok(measure) => { if text.linebreak_behavior == BreakLineOn::NoWrap { content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); @@ -105,7 +111,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}."); } }; @@ -135,6 +141,7 @@ pub fn measure_text_system( ), With, >, + mut text_pipeline: ResMut, ) { let mut scale_factors: EntityHashMap = EntityHashMap::default(); @@ -159,7 +166,14 @@ 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); + create_text_measure( + &fonts, + scale_factor, + text, + &mut text_pipeline, + content_size, + text_flags, + ); } } *last_scale_factors = scale_factors; @@ -211,12 +225,12 @@ 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(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; } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 8674294ec3efd..061db918e3143 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -76,7 +76,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..49820e9bcad91 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -35,7 +35,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, ..default() }; @@ -71,7 +71,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, ..default() }; diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index e7008f3875ce4..c1f7e2af50f91 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -411,13 +411,14 @@ 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>, ) { +<<<<<<< HEAD let (method, color_grading) = settings.single(); let method = *method; @@ -425,13 +426,28 @@ fn update_ui( let text = &mut text.sections[0].value; if keys.just_pressed(KeyCode::KeyH) { +======= + if keys.just_pressed(KeyCode::H) { +>>>>>>> d64259bcc (Copied each affected file from cosmic-text branch, to fix some weird issue with rebasing) *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"); @@ -535,6 +551,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 78b909ff4a4ee..2c961d9c49b06 100644 --- a/examples/README.md +++ b/examples/README.md @@ -453,6 +453,7 @@ Example | Description [Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world [Rounded Borders](../examples/ui/rounded_borders.rs) | Demonstrates how to create a node with a rounded border [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/input/text_input.rs b/examples/input/text_input.rs index 2adfad7cf4399..63a3568fc64d9 100644 --- a/examples/input/text_input.rs +++ b/examples/input/text_input.rs @@ -62,7 +62,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, ..default() }, @@ -70,7 +70,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, ..default() }, @@ -78,7 +78,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "".to_string(), style: TextStyle { - font, + font: font.into(), font_size: 25.0, ..default() }, @@ -96,7 +96,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, ..default() }, diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 716d867d0d4e5..69c7e7c0076f0 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -45,7 +45,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: BLUE.into(), }, @@ -53,7 +53,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: YELLOW.into(), }, diff --git a/examples/ui/button.rs b/examples/ui/button.rs index c00242fc88a2a..d17f768cf74a1 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -82,7 +82,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::srgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/flex_layout.rs b/examples/ui/flex_layout.rs index 8fff77b2d4e14..108e7f46fb559 100644 --- a/examples/ui/flex_layout.rs +++ b/examples/ui/flex_layout.rs @@ -176,7 +176,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 12da643fea335..e24d0917c9885 100644 --- a/examples/ui/font_atlas_debug.rs +++ b/examples/ui/font_atlas_debug.rs @@ -99,7 +99,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: YELLOW.into(), }, diff --git a/examples/ui/grid.rs b/examples/ui/grid.rs index 5b68e17371f65..30263c8a0e074 100644 --- a/examples/ui/grid.rs +++ b/examples/ui/grid.rs @@ -139,7 +139,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, ..default() }, @@ -147,7 +147,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, ..default() }, @@ -214,7 +214,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_debug.rs b/examples/ui/overflow_debug.rs index a71b32a157dd1..eb5be4c4e6954 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -115,11 +115,43 @@ fn setup(mut commands: Commands, asset_server: Res) { parent .spawn(NodeBundle { style: Style { +<<<<<<< HEAD display: Display::Grid, grid_template_columns: RepeatedGridTrack::px(3, CONTAINER_SIZE), grid_template_rows: RepeatedGridTrack::px(2, CONTAINER_SIZE), row_gap: Val::Px(80.), column_gap: Val::Px(80.), +======= + height: Val::Px(32.), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + background_color: Color::DARK_GRAY.into(), + ..default() + }) + .with_children(|parent| { + parent.spawn(TextBundle::from_section( + [ + "Toggle Overflow (O)", + "Next Container Size (S)", + "Toggle Animation (space)", + ] + .join(" · "), + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font_size: 18.0, + ..default() + }, + )); + }); + + parent + .spawn(NodeBundle { + style: Style { + flex_grow: 1., + flex_direction: FlexDirection::Column, +>>>>>>> d64259bcc (Copied each affected file from cosmic-text branch, to fix some weird issue with rebasing) ..default() }, ..default() @@ -165,7 +197,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, ..default() }, diff --git a/examples/ui/relative_cursor_position.rs b/examples/ui/relative_cursor_position.rs index 98d43edd513b2..e0184ce422113 100644 --- a/examples/ui/relative_cursor_position.rs +++ b/examples/ui/relative_cursor_position.rs @@ -58,7 +58,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::srgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index d32fe9734c698..d93f02df433c6 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::srgb(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 ce48b39e58a9c..643ee5b6e8d7e 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -35,9 +35,8 @@ 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 { - // This font is loaded and will be used instead of the default font. - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 100.0, + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font_size: 100.0 / 1.2, ..default() }, ) // Set the justification of the Text @@ -61,23 +60,23 @@ fn setup(mut commands: Commands, asset_server: Res) { TextStyle { // This font is loaded and will be used instead of the default font. font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 60.0, + font_size: 60.0 / 1.2, ..default() }, ), TextSection::from_style(if cfg!(feature = "default_font") { TextStyle { - font_size: 60.0, - color: GOLD.into(), + font_size: 50.0, + color: Color::GOLD, // If no font is specified, the default font (a minimal subset of FiraMono) will be used. ..default() } } else { // "default_font" feature is unavailable, load a font to use instead. TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), - font_size: 60.0, - color: GOLD.into(), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), + font_size: 50.0, + color: Color::GOLD, } }), ]), diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 789ea4fcca991..adf98c963f224 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -80,7 +80,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.clone(), + font: font.clone().into(), font_size: 30.0, ..default() }, diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 724e1e0eb97ab..4f744f201b3a2 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -49,7 +49,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, ..default() }; diff --git a/examples/ui/transparency_ui.rs b/examples/ui/transparency_ui.rs index 92c27059d5fc5..e56f4e0952ae4 100644 --- a/examples/ui/transparency_ui.rs +++ b/examples/ui/transparency_ui.rs @@ -44,7 +44,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::srgba(1.0, 1.0, 1.0, 0.2), @@ -70,7 +70,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::srgba(1.0, 1.0, 1.0, 0.2), diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index fec5bb825f9d1..bc2e0b0c32424 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -75,7 +75,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, ..default() }, @@ -131,7 +131,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., ..default() }, diff --git a/examples/ui/window_fallthrough.rs b/examples/ui/window_fallthrough.rs index bfe3c6df0c1a4..409592fa27b43 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! ..default() }, From d13442f154d63c75f07275ec2d30a0686d2eb769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Mon, 23 Oct 2023 21:09:50 +0200 Subject: [PATCH 02/47] update cosmic-text to 0.10.0 store systemfont atlases in the pipeline reduce changes introduced due to variable naming remove the system font implementation for now, in a single commit for easier retrieval later use Buffer::set_rich_text from cosmic_text instead of own impl use ID from FontDB::load_font_source use text Shaping::Advanced over Basic Remove system_fonts references and code that was left over from before be71200dd066e5ac343778c9a136f1f5df46ee5e check-dock and ci remove cfg for non-existing feature remove useless conversions in examples Revert "use text Shaping::Advanced over Basic" due to performance issues, Shaping::Advanced was about 10x slower than Shaping::Basic in debug mode, and about 2.2times slower in release mode. tested with cargo run --example text_debug This reverts commit 52d1640289b0bdcd66074f589174af983453670e. Update crates/bevy_text/Cargo.toml Co-authored-by: irate Newtype + RustDoc, as per review impl Debug for FontAtlas hide byte_index make SwashCache and FontSystem private, clean up comments Dont unwrap on swash_cache call add_glyph returns a Result instead of a boolean simplified TextError matches Restore comments, re-export in prelude Rename FontSystem to CosmicFontSystem Dont panic on invalid font-sizes use padding, or else crash add texture to the glyphinfo remove unused warnings update to cosmic-text 0.11 fix forgotten examples fmt Fix some changes --- Cargo.toml | 10 - crates/bevy_sprite/src/render/mod.rs | 6 +- crates/bevy_sprite/src/texture_atlas.rs | 2 +- crates/bevy_text/Cargo.toml | 8 +- crates/bevy_text/src/error.rs | 3 + crates/bevy_text/src/font.rs | 10 +- crates/bevy_text/src/font_atlas.rs | 28 +- crates/bevy_text/src/font_atlas_set.rs | 80 ++++-- crates/bevy_text/src/font_loader.rs | 6 +- crates/bevy_text/src/glyph.rs | 29 +- crates/bevy_text/src/glyph_brush.rs | 19 -- crates/bevy_text/src/lib.rs | 8 +- crates/bevy_text/src/pipeline.rs | 367 ++++++------------------ crates/bevy_text/src/text.rs | 148 +--------- crates/bevy_text/src/text2d.rs | 12 +- crates/bevy_ui/src/render/mod.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 60 +++- examples/2d/text2d.rs | 4 +- examples/3d/tonemapping.rs | 10 - examples/README.md | 1 - examples/input/text_input.rs | 8 +- examples/stress_tests/text_pipeline.rs | 4 +- examples/ui/button.rs | 2 +- examples/ui/flex_layout.rs | 2 +- examples/ui/font_atlas_debug.rs | 2 +- examples/ui/grid.rs | 6 +- examples/ui/overflow_debug.rs | 34 +-- examples/ui/relative_cursor_position.rs | 2 +- examples/ui/size_constraints.rs | 2 +- examples/ui/system_fonts.rs | 121 -------- examples/ui/text.rs | 13 +- examples/ui/text_debug.rs | 2 +- examples/ui/text_wrap_debug.rs | 2 +- examples/ui/transparency_ui.rs | 4 +- examples/ui/ui.rs | 4 +- examples/ui/window_fallthrough.rs | 2 +- 36 files changed, 310 insertions(+), 713 deletions(-) delete mode 100644 examples/ui/system_fonts.rs diff --git a/Cargo.toml b/Cargo.toml index accda717bca50..84e87d4c027f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2723,16 +2723,6 @@ 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_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 0e7efc1f75c23..c8ef166d0758d 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -379,10 +379,10 @@ pub fn extract_sprites( 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 ea3c67b640660..dabec81fffda2 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -1,6 +1,6 @@ use bevy_asset::{Asset, AssetId, Assets, Handle}; use bevy_ecs::component::Component; -use bevy_math::{URect, UVec2}; +use bevy_math::{Rect, URect, UVec2}; use bevy_reflect::Reflect; use bevy_render::texture::Image; use bevy_utils::HashMap; diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 7749785df45b8..6b60acdf449bd 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -28,10 +28,11 @@ bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } # other -anyhow = "1.0.4" -cosmic-text = "0.8.0" +cosmic-text = "0.11.0" thiserror = "1.0" serde = { version = "1", features = ["derive"] } +unicode-bidi = "0.3.13" +sys-locale = "0.3.0" [dev-dependencies] approx = "0.5.1" @@ -42,6 +43,3 @@ workspace = true [package.metadata.docs.rs] rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true -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 33f8f12647e58..ae0d990921499 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -1,3 +1,4 @@ +use cosmic_text::CacheKey; use thiserror::Error; #[derive(Debug, PartialEq, Eq, Error)] @@ -8,4 +9,6 @@ pub enum TextError { FailedToAddGlyph(u16), #[error("font system mutex could not be acquired or is poisoned")] FailedToAcquireMutex, + #[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 872059e4e23bd..e12c49f314dc5 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,19 +1,23 @@ +use std::sync::Arc; + use bevy_asset::Asset; use bevy_reflect::TypePath; -/// An [`Asset`](bevy_asset::Asset) that contains the data for a loaded font, if loaded as an asset. +/// An [`Asset`] that contains the data for a loaded font, if loaded as an asset. /// /// Loaded by [`FontLoader`](crate::FontLoader). #[derive(Debug, TypePath, Clone, Asset)] pub struct Font { - pub data: std::sync::Arc>, + /// Content of a font file as bytes + pub data: Arc>, } impl Font { + /// Creates a [Font] from bytes, without any validation of the content pub fn from_bytes(font_data: Vec) -> Self { // TODO: validate font, restore `try_from_bytes` Self { - data: std::sync::Arc::new(font_data), + data: Arc::new(font_data), } } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index fafb2cfe8bd00..eb7dedcdd69ba 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -8,17 +8,17 @@ use bevy_render::{ use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlasLayout}; use bevy_utils::HashMap; -use crate::GlyphAtlasLocation; +use crate::{GlyphAtlasLocation, TextError}; /// 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`]. + /// Used to update the [`TextureAtlasLayout`]. pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, /// 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. + /// 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, @@ -46,7 +46,7 @@ impl FontAtlas { Self { 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, } } @@ -72,12 +72,12 @@ impl FontAtlas { /// modified. pub fn add_glyph( &mut self, - textures: &mut Assets,crates/bevy_text/src/font_atlas_set.rs + textures: &mut Assets, texture_atlases: &mut Assets, - cache_key: cosmcrates/bevy_text/src/font_atlas_set.rsic_text::CacheKey, + cache_key: cosmic_text::CacheKey, texture: &Image, offset: IVec2, - ) -> bool { + ) -> Result<(), TextError> { let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); if let Some(glyph_index) = self.dynamic_texture_atlas_builder.add_texture( @@ -93,9 +93,19 @@ impl FontAtlas { offset, }, ); - true + 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("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 aa9e263dc9072..f3e6fdc0c8772 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,4 +1,3 @@ -use crate::{error::TextError, Font, FontAtlas}; use bevy_asset::{Asset, AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::prelude::*; use bevy_ecs::{ @@ -17,7 +16,7 @@ use bevy_utils::HashMap; use crate::{error::TextError, Font, FontAtlas, GlyphAtlasInfo}; -#[derive(Default, Resource)] +#[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>, @@ -46,14 +45,21 @@ pub fn remove_dropped_font_atlas_sets( } } -type FontSizeKey = u32; +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct FontSizeKey(pub u32); + +impl From for FontSizeKey { + fn from(val: u32) -> FontSizeKey { + Self(val) + } +} /// Provides the interface for adding and retrieving rasterized glyphs, and manages the [`FontAtlas`]es. /// -/// A `FontAtlasSet` is an [`Asset`](bevy_asset::Asset). +/// A `FontAtlasSet` is an [`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), +/// - 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`. @@ -61,7 +67,7 @@ type FontSizeKey = u32; /// A `FontAtlasSet` contains one or more [`FontAtlas`]es for each font size. /// /// It is used by [`TextPipeline::queue_text`](crate::TextPipeline::queue_text). -#[derive(TypePath, Asset)] +#[derive(Debug, TypePath, Asset)] pub struct FontAtlasSet { font_atlases: HashMap>, } @@ -79,9 +85,9 @@ impl FontAtlasSet { self.font_atlases.iter() } - pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: f32) -> bool { + pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: &FontSizeKey) -> bool { self.font_atlases - .get(&font_size.to_bits()) + .get(font_size) .map_or(false, |font_atlas| { font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)) }) @@ -95,9 +101,11 @@ impl FontAtlasSet { swash_cache: &mut cosmic_text::SwashCache, layout_glyph: &cosmic_text::LayoutGlyph, ) -> Result { + let physical_glyph = layout_glyph.physical((0., 0.), 1.0); + let font_atlases = self .font_atlases - .entry(layout_glyph.cache_key.font_size_bits) + .entry(physical_glyph.cache_key.font_size_bits.into()) .or_insert_with(|| { vec![FontAtlas::new( textures, @@ -107,17 +115,20 @@ impl FontAtlasSet { }); 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 { + 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, - layout_glyph.cache_key, + 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 @@ -131,18 +142,17 @@ 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, - layout_glyph.cache_key, + physical_glyph.cache_key, &glyph_texture, offset, - ) { - return Err(TextError::FailedToAddGlyph(layout_glyph.cache_key.glyph_id)); - } + )? } - Ok(self.get_glyph_atlas_info(layout_glyph.cache_key).unwrap()) + Ok(self.get_glyph_atlas_info(physical_glyph.cache_key).unwrap()) } pub fn get_glyph_atlas_info( @@ -150,18 +160,23 @@ impl FontAtlasSet { cache_key: cosmic_text::CacheKey, ) -> Option { self.font_atlases - .get(&cache_key.font_size_bits) + .get(&FontSizeKey(cache_key.font_size_bits)) .and_then(|font_atlases| { font_atlases .iter() .find_map(|atlas| { - atlas - .get_glyph_index(cache_key) - .map(|location| (location, atlas.texture_atlas.clone_weak())) + atlas.get_glyph_index(cache_key).map(|location| { + ( + location, + atlas.texture_atlas.clone_weak(), + atlas.texture.clone_weak(), + ) + }) }) - .map(|(location, texture_atlas)| GlyphAtlasInfo { + .map(|(location, texture_atlas, texture)| GlyphAtlasInfo { texture_atlas, location, + texture, }) }) } @@ -170,17 +185,20 @@ impl FontAtlasSet { pub fn len(&self) -> usize { self.font_atlases.len() } + /// Returns the number of font atlases in this set + pub fn is_empty(&self) -> bool { + 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, - layout_glyph: &cosmic_text::LayoutGlyph, - ) -> (Image, IVec2) { + physical_glyph: &cosmic_text::PhysicalGlyph, + ) -> Result<(Image, IVec2), TextError> { let image = swash_cache - .get_image_uncached(font_system, layout_glyph.cache_key) - // TODO: don't unwrap - .unwrap(); + .get_image_uncached(font_system, physical_glyph.cache_key) + .ok_or(TextError::FailedToGetGlyphImage(physical_glyph.cache_key))?; let cosmic_text::Placement { left, @@ -202,7 +220,7 @@ impl FontAtlasSet { } }; - ( + Ok(( Image::new( Extent3d { width, @@ -215,6 +233,6 @@ impl FontAtlasSet { 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 af871530b59b6..4cfcdfac81a49 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,5 +1,5 @@ use crate::Font; -use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext, LoadedAsset}; +use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; use thiserror::Error; #[derive(Default)] @@ -22,12 +22,12 @@ impl AssetLoader for FontLoader { &'a self, reader: &'a mut Reader<'_>, _settings: &'a (), - load_context: &'a mut LoadContext, + _load_context: &'a mut LoadContext<'a>, ) -> bevy_utils::BoxedFuture<'a, Result> { Box::pin(async move { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - let font = Font::from_bytes(bytes.into()); + let font = Font::from_bytes(bytes); // load_context.set_default_asset(LoadedAsset::new(font)); Ok(font) }) diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 607e3a2ab049d..3974f16d830e0 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -3,7 +3,8 @@ use bevy_asset::Handle; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; -use bevy_sprite::{TextureAtlas, TextureAtlasLayout}; +use bevy_render::texture::Image; +use bevy_sprite::TextureAtlasLayout; /// A glyph of a font, typically representing a single character, positioned in screen space. /// @@ -20,11 +21,28 @@ pub struct PositionedGlyph { 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. + /// 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. - // TODO: re-implement this or equivalent - pub byte_index: usize, + /// 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 { + 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. @@ -35,6 +53,7 @@ pub struct PositionedGlyph { /// Used in [`PositionedGlyph`] and [`FontAtlasSet`](crate::FontAtlasSet). #[derive(Debug, Clone, Reflect)] pub struct GlyphAtlasInfo { + pub texture: Handle, /// A handle to the texture atlas this glyph was placed in. pub texture_atlas: Handle, /// Location and offset of a glyph. diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index a6189a30be41e..9f79d36496a06 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -1,4 +1,3 @@ -use ab_glyph::{Font as _, FontArc, Glyph, PxScaleFont, ScaleFont as _}; use bevy_asset::{AssetId, Assets}; use bevy_math::{Rect, Vec2}; use bevy_reflect::Reflect; @@ -172,26 +171,8 @@ pub struct PositionedGlyph { 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 { diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 4cea5ab6a9e22..b0b7320c13d28 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -8,14 +8,13 @@ //! 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`]. +//! In Bevy, [`Font`]s are loaded by the [`FontLoader`] as assets, //! //! # `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`], +//! [`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. @@ -24,7 +23,7 @@ //! //! 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. +//! 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. @@ -40,6 +39,7 @@ mod pipeline; mod text; mod text2d; +pub use cosmic_text; pub use error::*; pub use font::*; pub use font_atlas::*; diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 64b6c4af0504e..cb2f139e2f2fe 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,38 +1,24 @@ use std::sync::{Arc, Mutex}; -use bevy_asset::{AssetId, Assets, Handle}; +use bevy_asset::{AssetId, Assets}; use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::texture::Image; use bevy_sprite::TextureAtlasLayout; -use bevy_utils::{ - tracing::{error, info, warn}, - HashMap, -}; +use bevy_utils::HashMap; -use cosmic_text::{Attrs, AttrsList, Buffer, BufferLine, Family, Metrics, Wrap}; +use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasSets, FontRef, JustifyText, - PositionedGlyph, TextSection, TextSettings, YAxisOrientation, + error::TextError, scale_value, BreakLineOn, Font, FontAtlasSets, JustifyText, PositionedGlyph, + Text, TextSection, YAxisOrientation, }; -// 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>); +struct CosmicFontSystem(Arc>); -impl Default for 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(); @@ -43,21 +29,8 @@ impl Default for FontSystem { } } -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); +struct SwashCache(cosmic_text::SwashCache); impl Default for SwashCache { fn default() -> Self { @@ -70,16 +43,12 @@ impl Default for SwashCache { /// 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`]. + /// 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>, - /// 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, + font_system: CosmicFontSystem, /// The swash cache rasterizer is used to rasterize glyphs /// /// See [`cosmic_text::SwashCache`] for more information. @@ -87,6 +56,9 @@ pub struct TextPipeline { } impl TextPipeline { + /// Utilizes [cosmic_text::Buffer] to shape and layout text + /// + /// Negative or 0.0 font sizes will not be laid out, and an empty buffer will be returned. pub fn create_buffer( &mut self, fonts: &Assets, @@ -103,6 +75,14 @@ impl TextPipeline { .unwrap_or_else(|| crate::TextStyle::default().font_size) as f64 * scale_factor; + + // TODO: maybe we would like to render negative fontsizes or scaling upside down or something? for now, no text is rendered + if font_size <= 0.0 { + // return empty buffer, making sure that the line height is not zero, + // since that results in a panic in cosmic-text + let metrics = Metrics::new(0.0, 0.000001); + return Ok(Buffer::new_empty(metrics)); + }; // 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); @@ -110,107 +90,25 @@ impl TextPipeline { 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 + let spans: Vec<(&str, Attrs)> = sections .iter() .enumerate() .map(|(section_index, section)| { - let start = end; - end += section.value.len(); - (section.value.as_str(), (section, section_index, start..end)) + ( + §ion.value[..], + get_attrs( + section, + section_index, + font_system, + &mut self.map_handle_to_font_id, + fonts, + ), + ) }) - .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; - } - } - } + .collect(); - // 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; + // TODO: cache buffers (see Iced / glyphon) + let mut buffer = Buffer::new_empty(metrics); let buffer_height = f32::INFINITY; buffer.set_size(font_system, bounds.x.ceil(), buffer_height); @@ -224,7 +122,8 @@ impl TextPipeline { ); // TODO: other shaping methods? - buffer.shape_until_scroll(font_system); + let default_attrs = Attrs::new(); + buffer.set_rich_text(font_system, spans, default_attrs, Shaping::Advanced); if buffer.visible_lines() == 0 { // Presumably the font(s) are not available yet @@ -251,7 +150,6 @@ impl TextPipeline { font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, - text_settings: &TextSettings, y_axis_orientation: YAxisOrientation, ) -> Result { if sections.is_empty() { @@ -287,35 +185,13 @@ impl TextPipeline { .map(|(layout_glyph, line_w, line_y)| { let section_index = layout_glyph.metadata; - // TODO(totalkrill): this is probably very wrong, investigate the - // cause, instead of "what makes it compile" - let font_atlas_set: &mut FontAtlasSet = match sections[section_index].style.font { - FontRef::Asset(ref font_handle) => { - let handle: Handle = font_handle.clone_weak(); - font_atlas_sets.sets.entry(handle.id()).or_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_default(); - - font_atlas_sets - .get_mut(handle.clone().untyped().id()) - .unwrap() - } - }; + let font_handle = sections[section_index].style.font.clone_weak(); + let font_atlas_set = font_atlas_sets.sets.entry(font_handle.id()).or_default(); + + let physical_glyph = layout_glyph.physical((0., 0.), 1.); let atlas_info = font_atlas_set - .get_glyph_atlas_info(layout_glyph.cache_key) + .get_glyph_atlas_info(physical_glyph.cache_key) .map(Ok) .unwrap_or_else(|| { font_atlas_set.add_glyph_to_atlas( @@ -335,8 +211,8 @@ impl TextPipeline { let glyph_size = Vec2::new(glyph_rect.width(), glyph_rect.height()); // 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; + let x = glyph_size.x / 2.0 + left + physical_glyph.x as f32; + let y = line_y + physical_glyph.y 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 { @@ -355,15 +231,10 @@ impl TextPipeline { 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, - }; + // 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, atlas_info, section_index); Ok(pos_glyph) }) .collect::, _>>()?; @@ -413,39 +284,12 @@ impl TextPipeline { }; Ok(TextMeasureInfo { - min_width_content_size, - max_width_content_size, + min: min_width_content_size, + max: 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"); - } } /// Render information for a corresponding [`Text`](crate::Text) component. @@ -463,8 +307,8 @@ pub struct TextLayoutInfo { /// /// Generated via [`TextPipeline::create_text_measure`]. pub struct TextMeasureInfo { - pub min_width_content_size: Vec2, - pub max_width_content_size: Vec2, + pub min: Vec2, + pub max: Vec2, buffer: Mutex, font_system: Arc>, } @@ -472,8 +316,8 @@ pub struct TextMeasureInfo { 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("min", &self.min) + .field("max", &self.max) .field("buffer", &"_") .field("font_system", &"_") .finish() @@ -540,73 +384,48 @@ impl TextMeasureInfo { } } -/// For the current line, -/// adds a span to the attributes list and pushes the text into the line string, +/// get attr for from textstyle /// 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, +fn get_attrs<'a>( + section: &'a TextSection, section_index: usize, - text: &str, font_system: &mut cosmic_text::FontSystem, map_handle_to_font_id: &mut HashMap, cosmic_text::fontdb::ID>, 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); +) -> Attrs<'a> { + let font_handle = section.style.font.clone(); + 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); + 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 + *ids.last().unwrap() + + // 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(&[])); + }); + 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; + let attrs = Attrs::new() + // TODO: validate that we can use metadata + .metadata(section_index) + // TODO: this reference, becomes owned by the font system, which is not really wanted... + // .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())); + attrs } /// Calculate the size of the text area for the given buffer. @@ -617,7 +436,7 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { .layout_runs() .map(|run| run.line_w) .reduce(|max_w, w| max_w.max(w)) - .unwrap(); + .unwrap_or_else(|| 0.0); // 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; @@ -669,7 +488,7 @@ impl<'text> Iterator for BidiParagraphs<'text> { /// Helper method to acquire a font system mutex. #[inline(always)] fn acquire_font_system( - font_system: &mut FontSystem, + font_system: &mut CosmicFontSystem, ) -> Result, TextError> { font_system .0 diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 2ebc1e111ec41..dc306f76d9494 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -5,11 +5,11 @@ use bevy_reflect::prelude::*; use bevy_utils::default; use serde::{Deserialize, Serialize}; -//use crate DEFAULT_FONT_HANDLE; use crate::Font; // 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, + self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, + Weight as FontWeight, }; /// A component that is the entry point for rendering text. @@ -182,7 +182,18 @@ pub enum JustifyText { #[derive(Clone, Debug, Reflect)] pub struct TextStyle { - pub font: FontRef, + /// If this 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. + /// * otherwise no text will be rendered. + pub font: Handle, + /// The vertical height of rasterized glyphs in the font atlas in pixels. + /// + /// This is multiplied by the window scale factor and `UiScale`, but not the text entity + /// transform or camera projection. + /// + /// 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, pub color: Color, } @@ -214,134 +225,3 @@ pub enum BreakLineOn { /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, is still enabled. NoWrap, } - -/// 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)] -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 Default for FontRef { - fn default() -> Self { - Self::Asset(Default::default()) - } -} - -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 8f32969893c0c..bf971ce8dbf15 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,6 +1,6 @@ use crate::{ BreakLineOn, Font, FontAtlasSets, PositionedGlyph, Text, TextError, TextLayoutInfo, - TextPipeline, TextSettings, YAxisOrientation, + TextPipeline, YAxisOrientation, }; use bevy_asset::Assets; use bevy_color::LinearRgba; @@ -175,7 +175,6 @@ 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>, @@ -207,14 +206,13 @@ pub fn update_text2d_layout( 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, ) { Err(TextError::NoSuchFont) => { @@ -222,7 +220,11 @@ pub fn update_text2d_layout( // queue for further processing queue.insert(entity); } - Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToAcquireMutex + | TextError::FailedToGetGlyphImage(_)), + ) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index d05c0b541e00e..ce81adbf86222 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]; rect.min *= inverse_scale_factor; rect.max *= inverse_scale_factor; extracted_uinodes.uinodes.insert( diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 39e963510cee5..b179c9cb41744 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -16,7 +16,7 @@ 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, + TextMeasureInfo, TextPipeline, YAxisOrientation, }; use bevy_utils::Entry; use taffy::style::AvailableSpace; @@ -71,8 +71,8 @@ impl Measure for TextMeasure { .map_or_else( || match available_width { AvailableSpace::Definite(_) => self.info.compute_size(Vec2::new(x, f32::MAX)), - AvailableSpace::MinContent => Vec2::new(x, self.info.min_width_content_size.y), - AvailableSpace::MaxContent => Vec2::new(x, self.info.max_width_content_size.y), + AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), + AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), }, |y| Vec2::new(x, y), ) @@ -83,7 +83,7 @@ impl Measure for TextMeasure { #[inline] fn create_text_measure( fonts: &Assets, - scale_factor: f32, + scale_factor: f64, text: Ref, text_pipeline: &mut TextPipeline, mut content_size: Mut, @@ -93,7 +93,7 @@ fn create_text_measure( fonts, &text.sections, scale_factor, - text.alignment, + text.justify, text.linebreak_behavior, ) { Ok(measure) => { @@ -111,7 +111,11 @@ fn create_text_measure( // Try again next frame text_flags.needs_new_measure_func = true; } - Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToAcquireMutex + | TextError::FailedToGetGlyphImage(_)), + ) => { panic!("Fatal error when processing text: {e}."); } }; @@ -168,7 +172,7 @@ pub fn measure_text_system( { create_text_measure( &fonts, - scale_factor, + scale_factor.into(), text, &mut text_pipeline, content_size, @@ -187,7 +191,6 @@ 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, @@ -211,21 +214,24 @@ 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, ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, try again next frame text_flags.needs_recompute = true; } - Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToAcquireMutex + | TextError::FailedToGetGlyphImage(_)), + ) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { @@ -251,9 +257,13 @@ pub fn text_system( mut textures: ResMut>, mut last_scale_factors: Local>, fonts: Res>, +<<<<<<< HEAD camera_query: Query<(Entity, &Camera)>, default_ui_camera: DefaultUiCamera, text_settings: Res, +======= + windows: Query<&Window, With>, +>>>>>>> 117cdd034 (update cosmic-text to 0.10.0) ui_scale: Res, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, @@ -268,6 +278,7 @@ pub fn text_system( ) { let mut scale_factors: EntityHashMap = EntityHashMap::default(); +<<<<<<< HEAD for (node, text, text_layout_info, text_flags, camera) in &mut text_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -285,6 +296,32 @@ pub fn text_system( ), }; let inverse_scale_factor = scale_factor.recip(); +======= + let scale_factor = ui_scale.0 * window_scale_factor; + let inverse_scale_factor = scale_factor.recip(); + if *last_scale_factor == scale_factor { + // Scale factor unchanged, only recompute text for modified text nodes + for (node, text, text_layout_info, text_flags) in &mut text_query { + if node.is_changed() || text_flags.needs_recompute { + queue_text( + &fonts, + &mut text_pipeline, + &mut font_atlas_sets, + &mut texture_atlases, + &mut textures, + scale_factor, + inverse_scale_factor, + text, + node, + text_flags, + text_layout_info, + ); + } + } + } else { + // Scale factor changed, recompute text for all text nodes + *last_scale_factor = scale_factor; +>>>>>>> 117cdd034 (update cosmic-text to 0.10.0) if last_scale_factors.get(&camera_entity) != Some(&scale_factor) || node.is_changed() @@ -296,7 +333,6 @@ pub fn text_system( &mut font_atlas_sets, &mut texture_atlases, &mut textures, - &text_settings, scale_factor, inverse_scale_factor, text, diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 49820e9bcad91..bb7325ac1f895 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -35,7 +35,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().into(), + font: font.clone(), font_size: 60.0, ..default() }; @@ -71,7 +71,7 @@ fn setup(mut commands: Commands, asset_server: Res) { )); // Demonstrate text wrapping let slightly_smaller_text_style = TextStyle { - font: font.into(), + font, font_size: 42.0, ..default() }; diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index c1f7e2af50f91..38992cd0c45b2 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -418,17 +418,7 @@ fn update_ui( mut hide_ui: Local, keys: Res>, ) { -<<<<<<< HEAD - 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) { -======= - if keys.just_pressed(KeyCode::H) { ->>>>>>> d64259bcc (Copied each affected file from cosmic-text branch, to fix some weird issue with rebasing) *hide_ui = !*hide_ui; } diff --git a/examples/README.md b/examples/README.md index 2c961d9c49b06..78b909ff4a4ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -453,7 +453,6 @@ Example | Description [Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world [Rounded Borders](../examples/ui/rounded_borders.rs) | Demonstrates how to create a node with a rounded border [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/input/text_input.rs b/examples/input/text_input.rs index 63a3568fc64d9..2adfad7cf4399 100644 --- a/examples/input/text_input.rs +++ b/examples/input/text_input.rs @@ -62,7 +62,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "false\n".to_string(), style: TextStyle { - font: font.clone_weak().into(), + font: font.clone_weak(), font_size: 30.0, ..default() }, @@ -70,7 +70,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().into(), + font: font.clone_weak(), font_size: 18.0, ..default() }, @@ -78,7 +78,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "".to_string(), style: TextStyle { - font: font.into(), + font, font_size: 25.0, ..default() }, @@ -96,7 +96,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").into(), + font: asset_server.load("fonts/FiraMono-Medium.ttf"), font_size: 100.0, ..default() }, diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 69c7e7c0076f0..716d867d0d4e5 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -45,7 +45,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { TextSection { value: "text".repeat(i), style: TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), + font: asset_server.load("fonts/FiraMono-Medium.ttf"), font_size: (4 + i % 10) as f32, color: BLUE.into(), }, @@ -53,7 +53,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { TextSection { value: "pipeline".repeat(i), style: TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: (4 + i % 11) as f32, color: YELLOW.into(), }, diff --git a/examples/ui/button.rs b/examples/ui/button.rs index d17f768cf74a1..c00242fc88a2a 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -82,7 +82,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 40.0, color: Color::srgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/flex_layout.rs b/examples/ui/flex_layout.rs index 108e7f46fb559..8fff77b2d4e14 100644 --- a/examples/ui/flex_layout.rs +++ b/examples/ui/flex_layout.rs @@ -176,7 +176,7 @@ fn spawn_nested_text_bundle( builder.spawn(TextBundle::from_section( text, TextStyle { - font: font.into(), + font, font_size: 24.0, color: Color::BLACK, }, diff --git a/examples/ui/font_atlas_debug.rs b/examples/ui/font_atlas_debug.rs index e24d0917c9885..12da643fea335 100644 --- a/examples/ui/font_atlas_debug.rs +++ b/examples/ui/font_atlas_debug.rs @@ -99,7 +99,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut state: ResM parent.spawn(TextBundle::from_section( "a", TextStyle { - font: font_handle.into(), + font: font_handle, font_size: 60.0, color: YELLOW.into(), }, diff --git a/examples/ui/grid.rs b/examples/ui/grid.rs index 30263c8a0e074..5b68e17371f65 100644 --- a/examples/ui/grid.rs +++ b/examples/ui/grid.rs @@ -139,7 +139,7 @@ fn spawn_layout(mut commands: Commands, asset_server: Res) { builder.spawn(TextBundle::from_section( "Sidebar", TextStyle { - font: font.clone().into(), + font: font.clone(), font_size: 24.0, ..default() }, @@ -147,7 +147,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().into(), + font: font.clone(), font_size: 16.0, ..default() }, @@ -214,7 +214,7 @@ fn spawn_nested_text_bundle(builder: &mut ChildBuilder, font: Handle, text builder.spawn(TextBundle::from_section( text, TextStyle { - font: font.into(), + font, font_size: 24.0, color: Color::BLACK, }, diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index eb5be4c4e6954..a71b32a157dd1 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -115,43 +115,11 @@ fn setup(mut commands: Commands, asset_server: Res) { parent .spawn(NodeBundle { style: Style { -<<<<<<< HEAD display: Display::Grid, grid_template_columns: RepeatedGridTrack::px(3, CONTAINER_SIZE), grid_template_rows: RepeatedGridTrack::px(2, CONTAINER_SIZE), row_gap: Val::Px(80.), column_gap: Val::Px(80.), -======= - height: Val::Px(32.), - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - ..default() - }, - background_color: Color::DARK_GRAY.into(), - ..default() - }) - .with_children(|parent| { - parent.spawn(TextBundle::from_section( - [ - "Toggle Overflow (O)", - "Next Container Size (S)", - "Toggle Animation (space)", - ] - .join(" · "), - TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), - font_size: 18.0, - ..default() - }, - )); - }); - - parent - .spawn(NodeBundle { - style: Style { - flex_grow: 1., - flex_direction: FlexDirection::Column, ->>>>>>> d64259bcc (Copied each affected file from cosmic-text branch, to fix some weird issue with rebasing) ..default() }, ..default() @@ -197,7 +165,7 @@ fn spawn_text( parent.spawn(TextBundle::from_section( "Bevy", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 120.0, ..default() }, diff --git a/examples/ui/relative_cursor_position.rs b/examples/ui/relative_cursor_position.rs index e0184ce422113..98d43edd513b2 100644 --- a/examples/ui/relative_cursor_position.rs +++ b/examples/ui/relative_cursor_position.rs @@ -58,7 +58,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").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 40.0, color: Color::srgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index d93f02df433c6..d32fe9734c698 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").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 40.0, color: Color::srgb(0.9, 0.9, 0.9), }; diff --git a/examples/ui/system_fonts.rs b/examples/ui/system_fonts.rs deleted file mode 100644 index 6543dbef7912f..0000000000000 --- a/examples/ui/system_fonts.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! 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 643ee5b6e8d7e..0be0c459518ee 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -35,8 +35,9 @@ 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").into(), - font_size: 100.0 / 1.2, + // This font is loaded and will be used instead of the default font. + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 80.0, ..default() }, ) // Set the justification of the Text @@ -60,13 +61,13 @@ fn setup(mut commands: Commands, asset_server: Res) { TextStyle { // This font is loaded and will be used instead of the default font. font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 60.0 / 1.2, + font_size: 50.0, ..default() }, ), TextSection::from_style(if cfg!(feature = "default_font") { TextStyle { - font_size: 50.0, + font_size: 40.0, color: Color::GOLD, // If no font is specified, the default font (a minimal subset of FiraMono) will be used. ..default() @@ -74,8 +75,8 @@ fn setup(mut commands: Commands, asset_server: Res) { } else { // "default_font" feature is unavailable, load a font to use instead. TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), - font_size: 50.0, + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 40.0, color: Color::GOLD, } }), diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index adf98c963f224..789ea4fcca991 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -80,7 +80,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.clone().into(), + font: font.clone(), font_size: 30.0, ..default() }, diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 4f744f201b3a2..724e1e0eb97ab 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -49,7 +49,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").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 14.0, ..default() }; diff --git a/examples/ui/transparency_ui.rs b/examples/ui/transparency_ui.rs index e56f4e0952ae4..92c27059d5fc5 100644 --- a/examples/ui/transparency_ui.rs +++ b/examples/ui/transparency_ui.rs @@ -44,7 +44,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button 1", TextStyle { - font: font_handle.clone().into(), + font: font_handle.clone(), font_size: 40.0, // Alpha channel of the color controls transparency. color: Color::srgba(1.0, 1.0, 1.0, 0.2), @@ -70,7 +70,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button 2", TextStyle { - font: font_handle.clone().into(), + font: font_handle.clone(), font_size: 40.0, // Alpha channel of the color controls transparency. color: Color::srgba(1.0, 1.0, 1.0, 0.2), diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index bc2e0b0c32424..fec5bb825f9d1 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -75,7 +75,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "Text Example", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 30.0, ..default() }, @@ -131,7 +131,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "Scrolling list", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 25., ..default() }, diff --git a/examples/ui/window_fallthrough.rs b/examples/ui/window_fallthrough.rs index 409592fa27b43..bfe3c6df0c1a4 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").into(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 100.0, // Nice and big so you can see it! ..default() }, From b58d3c7839912c9c09f0d5975a53f93f7061c458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Fri, 7 Jun 2024 22:30:01 +0200 Subject: [PATCH 03/47] Fix compilation issues when rebasing --- crates/bevy_sprite/src/render/mod.rs | 3 +- crates/bevy_text/src/font_atlas.rs | 4 +- crates/bevy_text/src/font_atlas_set.rs | 8 +--- crates/bevy_text/src/font_loader.rs | 17 ++++--- crates/bevy_text/src/pipeline.rs | 65 +++----------------------- crates/bevy_text/src/text2d.rs | 8 ++-- crates/bevy_ui/src/render/mod.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 33 +------------ 8 files changed, 25 insertions(+), 115 deletions(-) diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index c8ef166d0758d..d60efb0977eb2 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -375,7 +375,8 @@ 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), diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index eb7dedcdd69ba..dd81cd62d9b9d 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,5 +1,5 @@ use bevy_asset::{Assets, Handle}; -use bevy_math::{IVec2, Vec2}; +use bevy_math::{IVec2, UVec2, Vec2}; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, @@ -28,7 +28,7 @@ impl FontAtlas { pub fn new( textures: &mut Assets, texture_atlases_layout: &mut Assets, - size: Vec2, + size: UVec2, ) -> FontAtlas { let texture = textures.add(Image::new_fill( Extent3d { diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index f3e6fdc0c8772..48ca3ddbd73ff 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -106,13 +106,7 @@ impl FontAtlasSet { let font_atlases = self .font_atlases .entry(physical_glyph.cache_key.font_size_bits.into()) - .or_insert_with(|| { - vec![FontAtlas::new( - textures, - texture_atlases, - Vec2::splat(512.0), - )] - }); + .or_insert_with(|| vec![FontAtlas::new(textures, texture_atlases, UVec2::splat(512))]); let (glyph_texture, offset) = Self::get_outlined_glyph_texture(font_system, swash_cache, &physical_glyph)?; diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index 4cfcdfac81a49..f5e22848eaeab 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,5 +1,6 @@ use crate::Font; use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; +use bevy_utils::{ConditionalSend, ConditionalSendFuture}; use thiserror::Error; #[derive(Default)] @@ -22,15 +23,13 @@ impl AssetLoader for FontLoader { &'a self, reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext<'a>, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let font = Font::from_bytes(bytes); - // load_context.set_default_asset(LoadedAsset::new(font)); - Ok(font) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let font = Font::from_bytes(bytes); + // load_context.set_default_asset(LoadedAsset::new(font)); + Ok(font) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index cb2f139e2f2fe..4696451ebd93a 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex}; use bevy_asset::{AssetId, Assets}; use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; -use bevy_math::Vec2; +use bevy_math::{UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::texture::Image; use bevy_sprite::TextureAtlasLayout; @@ -208,11 +208,11 @@ impl TextPipeline { 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 glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height()); // offset by half the size because the origin is center - let x = glyph_size.x / 2.0 + left + physical_glyph.x as f32; - let y = line_y + physical_glyph.y as f32 - top + glyph_size.y / 2.0; + let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; + let y = line_y + physical_glyph.y as f32 - top + glyph_size.y as f32 / 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 { @@ -234,7 +234,7 @@ impl TextPipeline { // 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, atlas_info, section_index); + PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, section_index); Ok(pos_glyph) }) .collect::, _>>()?; @@ -325,57 +325,6 @@ impl std::fmt::Debug for TextMeasureInfo { } 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(), - )) - } - 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 - } - 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"); @@ -398,7 +347,7 @@ fn get_attrs<'a>( let face_id = map_handle_to_font_id .entry(font_handle_id) .or_insert_with(|| { - let font = fonts.get(font_handle).unwrap(); + let font = fonts.get(font_handle.id()).unwrap(); let data = Arc::clone(&font.data); let ids = font_system .db_mut() @@ -424,7 +373,7 @@ fn get_attrs<'a>( .stretch(face.stretch) .style(face.style) .weight(face.weight) - .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())); + .color(cosmic_text::Color(section.style.color.linear().as_u32())); attrs } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index bf971ce8dbf15..60692e0b967ec 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -149,7 +149,7 @@ pub fn extract_text2d_sprite( ExtractedSprite { transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, - rect: Some(atlas.textures[atlas_info.location.glyph_index]), + rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()), custom_size: None, image_handle_id: atlas_info.texture.id(), flip_x: false, @@ -256,11 +256,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, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index ce81adbf86222..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.location.glyph_index]; + 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/text.rs b/crates/bevy_ui/src/widget/text.rs index b179c9cb41744..be526146bac8b 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -16,7 +16,7 @@ use bevy_render::{camera::Camera, texture::Image}; use bevy_sprite::TextureAtlasLayout; use bevy_text::{ scale_value, BreakLineOn, Font, FontAtlasSets, Text, TextError, TextLayoutInfo, - TextMeasureInfo, TextPipeline, YAxisOrientation, + TextMeasureInfo, TextPipeline, TextSettings, YAxisOrientation, }; use bevy_utils::Entry; use taffy::style::AvailableSpace; @@ -257,13 +257,9 @@ pub fn text_system( mut textures: ResMut>, mut last_scale_factors: Local>, fonts: Res>, -<<<<<<< HEAD camera_query: Query<(Entity, &Camera)>, default_ui_camera: DefaultUiCamera, text_settings: Res, -======= - windows: Query<&Window, With>, ->>>>>>> 117cdd034 (update cosmic-text to 0.10.0) ui_scale: Res, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, @@ -278,7 +274,6 @@ pub fn text_system( ) { let mut scale_factors: EntityHashMap = EntityHashMap::default(); -<<<<<<< HEAD for (node, text, text_layout_info, text_flags, camera) in &mut text_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -296,32 +291,6 @@ pub fn text_system( ), }; let inverse_scale_factor = scale_factor.recip(); -======= - let scale_factor = ui_scale.0 * window_scale_factor; - let inverse_scale_factor = scale_factor.recip(); - if *last_scale_factor == scale_factor { - // Scale factor unchanged, only recompute text for modified text nodes - for (node, text, text_layout_info, text_flags) in &mut text_query { - if node.is_changed() || text_flags.needs_recompute { - queue_text( - &fonts, - &mut text_pipeline, - &mut font_atlas_sets, - &mut texture_atlases, - &mut textures, - scale_factor, - inverse_scale_factor, - text, - node, - text_flags, - text_layout_info, - ); - } - } - } else { - // Scale factor changed, recompute text for all text nodes - *last_scale_factor = scale_factor; ->>>>>>> 117cdd034 (update cosmic-text to 0.10.0) if last_scale_factors.get(&camera_entity) != Some(&scale_factor) || node.is_changed() From cc5acebc3b9690a389cf5c6d220f8e1f78d36382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Fri, 7 Jun 2024 23:29:44 +0200 Subject: [PATCH 04/47] Fix issue where font that was not loaded yet would crash the program --- crates/bevy_text/src/error.rs | 7 +++++++ crates/bevy_text/src/pipeline.rs | 14 +++++++++++--- examples/ui/text.rs | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index ae0d990921499..78c24ef9144e9 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -2,13 +2,20 @@ 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 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(u16), + /// Failed to acquire mutex to cosmic-texts fontsystem + //TODO: this can be removed since the mutex should be possible to remove as well #[error("font system mutex could not be acquired or is poisoned")] FailedToAcquireMutex, + /// 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/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 4696451ebd93a..26635c079f157 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -90,6 +90,13 @@ impl TextPipeline { let font_system = &mut acquire_font_system(&mut self.font_system)?; + // return early if the fonts are not loaded yet + for section in sections { + fonts + .get(section.style.font.id()) + .ok_or(TextError::NoSuchFont)?; + } + let spans: Vec<(&str, Attrs)> = sections .iter() .enumerate() @@ -343,11 +350,12 @@ fn get_attrs<'a>( fonts: &Assets, ) -> Attrs<'a> { let font_handle = section.style.font.clone(); - let font_handle_id = font_handle.id(); let face_id = map_handle_to_font_id - .entry(font_handle_id) + .entry(font_handle.id()) .or_insert_with(|| { - let font = fonts.get(font_handle.id()).unwrap(); + 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() diff --git a/examples/ui/text.rs b/examples/ui/text.rs index 0be0c459518ee..197c8ddf4c8b0 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -68,7 +68,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextSection::from_style(if cfg!(feature = "default_font") { TextStyle { font_size: 40.0, - color: Color::GOLD, + color: GOLD.into(), // If no font is specified, the default font (a minimal subset of FiraMono) will be used. ..default() } @@ -77,7 +77,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextStyle { font: asset_server.load("fonts/FiraMono-Medium.ttf"), font_size: 40.0, - color: Color::GOLD, + color: GOLD.into(), } }), ]), From 60d59b63a5206e66913f9ef60ecf5181c578a80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Fri, 7 Jun 2024 23:35:56 +0200 Subject: [PATCH 05/47] remove unused module and some unused imports --- crates/bevy_sprite/src/texture_atlas.rs | 2 +- crates/bevy_text/src/font_atlas.rs | 2 +- crates/bevy_text/src/font_atlas_set.rs | 4 +- crates/bevy_text/src/glyph_brush.rs | 218 ------------------------ crates/bevy_text/src/lib.rs | 25 +-- crates/bevy_text/src/pipeline.rs | 3 +- crates/bevy_ui/src/widget/text.rs | 3 +- 7 files changed, 9 insertions(+), 248 deletions(-) delete mode 100644 crates/bevy_text/src/glyph_brush.rs diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_sprite/src/texture_atlas.rs index dabec81fffda2..ea3c67b640660 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -1,6 +1,6 @@ use bevy_asset::{Asset, AssetId, Assets, Handle}; use bevy_ecs::component::Component; -use bevy_math::{Rect, URect, UVec2}; +use bevy_math::{URect, UVec2}; use bevy_reflect::Reflect; use bevy_render::texture::Image; use bevy_utils::HashMap; diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index dd81cd62d9b9d..c288dea5b320c 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,5 +1,5 @@ use bevy_asset::{Assets, Handle}; -use bevy_math::{IVec2, UVec2, Vec2}; +use bevy_math::{IVec2, UVec2}; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 48ca3ddbd73ff..6250785e67560 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,10 +1,10 @@ -use bevy_asset::{Asset, AssetEvent, AssetId, Assets, Handle}; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; use bevy_ecs::prelude::*; use bevy_ecs::{ event::EventReader, system::{ResMut, Resource}, }; -use bevy_math::{FloatOrd, IVec2, UVec2, Vec2}; +use bevy_math::{IVec2, UVec2}; use bevy_reflect::{Reflect, TypePath}; use bevy_render::{ render_asset::RenderAssetUsages, diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs deleted file mode 100644 index 9f79d36496a06..0000000000000 --- a/crates/bevy_text/src/glyph_brush.rs +++ /dev/null @@ -1,218 +0,0 @@ -use bevy_asset::{AssetId, Assets}; -use bevy_math::{Rect, Vec2}; -use bevy_reflect::Reflect; -use bevy_render::texture::Image; -use bevy_sprite::TextureAtlasLayout; -use bevy_utils::warn_once; -use glyph_brush_layout::{ - BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, - SectionText, ToSectionText, -}; - -use crate::{ - error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasSets, GlyphAtlasInfo, JustifyText, - PlacedGlyph, TextSettings, YAxisOrientation, -}; - -pub struct GlyphBrush { - fonts: Vec, - asset_ids: Vec>, - latest_font_id: FontId, -} - -impl Default for GlyphBrush { - fn default() -> Self { - GlyphBrush { - fonts: Vec::new(), - asset_ids: Vec::new(), - latest_font_id: FontId(0), - } - } -} - -impl GlyphBrush { - pub fn compute_glyphs( - &self, - sections: &[S], - bounds: Vec2, - text_alignment: JustifyText, - 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_sets: &mut FontAtlasSets, - fonts: &Assets, - texture_atlases: &mut Assets, - textures: &mut Assets, - text_settings: &TextSettings, - y_axis_orientation: YAxisOrientation, - h_anchor: f32, - ) -> Result, TextError> { - if glyphs.is_empty() { - return Ok(Vec::new()); - } - - let sections_data = sections - .iter() - .map(|section| { - let asset_id = &self.asset_ids[section.font_id.0]; - let font = fonts.get(*asset_id).ok_or(TextError::NoSuchFont)?; - let font_size = section.scale.y; - Ok(( - asset_id, - font, - font_size, - ab_glyph::Font::as_scaled(&font.font, font_size), - )) - }) - .collect::, _>>()?; - - let text_bounds = compute_text_bounds(&glyphs, |index| sections_data[index].3); - - let mut positioned_glyphs = Vec::new(); - for sg in glyphs { - let SectionGlyph { - section_index: _, - byte_index, - mut glyph, - font_id: _, - } = sg; - let placed_glyph = PlacedGlyph { - glyph_id: glyph.id, - subpixel_offset: glyph.position.into(), - }; - 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 font_atlas_set = font_atlas_sets - .sets - .entry(*section_data.0) - .or_insert_with(FontAtlasSet::default); - - let atlas_info = font_atlas_set - .get_glyph_atlas_info(section_data.2, &placed_glyph) - .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_set.len() > text_settings.soft_max_font_atlases.get() - { - warn_once!( - "warning[B0005]: Number of font atlases has exceeded the maximum of {}. Performance and memory usage may suffer. See: https://bevyengine.org/learn/errors/#b0005", - text_settings.soft_max_font_atlases.get()); - } - - let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let glyph_rect = texture_atlas.textures[atlas_info.glyph_index]; - let size = glyph_rect.size().as_vec2(); - - let x = bounds.min.x + size.x / 2.0 + h_anchor; - - let y = match y_axis_orientation { - YAxisOrientation::BottomToTop => { - text_bounds.max.y - bounds.max.y + size.y / 2.0 - } - YAxisOrientation::TopToBottom => { - bounds.min.y + size.y / 2.0 - text_bounds.min.y - } - }; - - // We must offset by 1 to account for glyph texture padding. - // See https://github.com/bevyengine/bevy/pull/11662 - let position = adjust.position(Vec2::new(x, y) - 1.); - - positioned_glyphs.push(PositionedGlyph { - position, - size, - atlas_info, - section_index: sg.section_index, - byte_index, - }); - } - } - Ok(positioned_glyphs) - } - - pub fn add_font(&mut self, asset_id: AssetId, font: FontArc) -> FontId { - self.fonts.push(font); - self.asset_ids.push(asset_id); - let font_id = self.latest_font_id; - self.latest_font_id = FontId(font_id.0 + 1); - font_id - } -} - -#[derive(Debug, Clone, Reflect)] -pub struct PositionedGlyph { - pub position: Vec2, - pub size: Vec2, - pub atlas_info: GlyphAtlasInfo, - pub section_index: usize, - pub byte_index: usize, -} - -struct GlyphPlacementAdjuster(f32); - -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 - } -} - -/// Computes the minimal bounding rectangle for a block of text. -/// Ignores empty trailing lines. -pub(crate) fn compute_text_bounds( - section_glyphs: &[SectionGlyph], - get_scaled_font: impl Fn(usize) -> PxScaleFont, -) -> Rect -where - T: ab_glyph::Font, -{ - let mut text_bounds = Rect { - min: Vec2::splat(f32::MAX), - max: Vec2::splat(f32::MIN), - }; - - for sg in section_glyphs { - let scaled_font = get_scaled_font(sg.section_index); - let glyph = &sg.glyph; - text_bounds = text_bounds.union(Rect { - min: Vec2::new(glyph.position.x, 0.), - max: Vec2::new( - glyph.position.x + scaled_font.h_advance(glyph.id), - glyph.position.y - scaled_font.descent(), - ), - }); - } - - text_bounds -} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index b0b7320c13d28..4f0b1138558ca 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -50,6 +50,7 @@ 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}; @@ -64,7 +65,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. /// @@ -73,31 +73,13 @@ 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. pub enum YAxisOrientation { + /// Ui Y-Axis orientation TopToBottom, + /// Text2dBundles axises BottomToTop, } @@ -111,7 +93,6 @@ impl Plugin for TextPlugin { .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 26635c079f157..4040092ac082f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -11,8 +11,7 @@ use bevy_utils::HashMap; use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, scale_value, BreakLineOn, Font, FontAtlasSets, JustifyText, PositionedGlyph, - Text, TextSection, YAxisOrientation, + error::TextError, BreakLineOn, Font, FontAtlasSets, JustifyText, PositionedGlyph, TextSection, YAxisOrientation, }; /// A wrapper around a [`cosmic_text::FontSystem`] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index be526146bac8b..feb6883f9777e 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -16,7 +16,7 @@ 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, + TextMeasureInfo, TextPipeline, YAxisOrientation, }; use bevy_utils::Entry; use taffy::style::AvailableSpace; @@ -259,7 +259,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, From 8941eb4a316934bc231c04c4367132b08d455ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sat, 8 Jun 2024 00:13:53 +0200 Subject: [PATCH 06/47] enable multi-size font by patching latest main from cosmic-text --- Cargo.toml | 3 +++ crates/bevy_text/Cargo.toml | 2 +- crates/bevy_text/src/lib.rs | 4 ++-- crates/bevy_text/src/pipeline.rs | 4 +++- examples/ui/text_debug.rs | 4 ++-- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 84e87d4c027f1..e539fd3a95189 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3231,3 +3231,6 @@ panic = "abort" rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] + +[patch.crates-io] +cosmic-text = { git = 'https://github.com/pop-os/cosmic-text.git'} diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 6b60acdf449bd..606af99095426 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -28,7 +28,7 @@ bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } # other -cosmic-text = "0.11.0" +cosmic-text = "0.11.2" thiserror = "1.0" serde = { version = "1", features = ["derive"] } unicode-bidi = "0.3.13" diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 4f0b1138558ca..c549499aea4e2 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -77,9 +77,9 @@ pub struct TextPlugin; /// `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. pub enum YAxisOrientation { - /// Ui Y-Axis orientation + /// Ui Y-axis orientation TopToBottom, - /// Text2dBundles axises + /// Text2dBundles Y-axis orientation BottomToTop, } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 4040092ac082f..652f8c4642e2a 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -11,7 +11,8 @@ use bevy_utils::HashMap; use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, BreakLineOn, Font, FontAtlasSets, JustifyText, PositionedGlyph, TextSection, YAxisOrientation, + error::TextError, BreakLineOn, Font, FontAtlasSets, JustifyText, PositionedGlyph, TextSection, + YAxisOrientation, }; /// A wrapper around a [`cosmic_text::FontSystem`] @@ -380,6 +381,7 @@ fn get_attrs<'a>( .stretch(face.stretch) .style(face.style) .weight(face.weight) + .metrics(Metrics::relative(section.style.font_size, 1.2)) .color(cosmic_text::Color(section.style.color.linear().as_u32())); attrs } diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 789ea4fcca991..6391f2ce68104 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -162,7 +162,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { " fps, ", TextStyle { font: font.clone(), - font_size: 25.0, + font_size: 12.0, color: YELLOW.into(), }, ), @@ -175,7 +175,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { " ms/frame", TextStyle { font: font.clone(), - font_size: 25.0, + font_size: 50.0, color: BLUE.into(), }, ), From 85b90e208025bd6782d5e6f03c712d0bee388828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Fri, 14 Jun 2024 22:22:26 +0200 Subject: [PATCH 07/47] Rebase fixes and cosmic-text api changes when using main git branch as dep --- .../src/dynamic_texture_atlas_builder.rs | 5 ++++- crates/bevy_text/src/pipeline.rs | 19 ++++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs index 879a9af3a497a..f6d3e928cdf7b 100644 --- a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs @@ -1,4 +1,5 @@ use crate::TextureAtlasLayout; +use bevy_asset::{Assets, Handle}; use bevy_math::{URect, UVec2}; use bevy_render::{ render_asset::{RenderAsset, RenderAssetUsages}, @@ -43,14 +44,16 @@ impl DynamicTextureAtlasBuilder { pub fn add_texture( &mut self, atlas_layout: &mut TextureAtlasLayout, + textures: &mut Assets, texture: &Image, - atlas_texture: &mut Image, + atlas_texture_handle: &Handle, ) -> Option { let allocation = self.atlas_allocator.allocate(size2( (texture.width() + self.padding).try_into().unwrap(), (texture.height() + self.padding).try_into().unwrap(), )); if let Some(allocation) = allocation { + let atlas_texture = textures.get_mut(atlas_texture_handle).unwrap(); assert!( ::asset_usage(atlas_texture) .contains(RenderAssetUsages::MAIN_WORLD), diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 652f8c4642e2a..294cdec9cfbe0 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -117,7 +117,7 @@ impl TextPipeline { // TODO: cache buffers (see Iced / glyphon) let mut buffer = Buffer::new_empty(metrics); let buffer_height = f32::INFINITY; - buffer.set_size(font_system, bounds.x.ceil(), buffer_height); + buffer.set_size(font_system, Some(bounds.x.ceil()), Some(buffer_height)); buffer.set_wrap( font_system, @@ -128,14 +128,7 @@ impl TextPipeline { }, ); - // TODO: other shaping methods? - let default_attrs = Attrs::new(); - buffer.set_rich_text(font_system, spans, default_attrs, Shaping::Advanced); - - if buffer.visible_lines() == 0 { - // Presumably the font(s) are not available yet - return Err(TextError::NoSuchFont); - } + buffer.set_rich_text(font_system, spans, Attrs::new(), Shaping::Advanced); Ok(buffer) } @@ -283,8 +276,8 @@ impl TextPipeline { buffer.set_size( font_system, - MAX_WIDTH_CONTENT_BOUNDS.x, - MAX_WIDTH_CONTENT_BOUNDS.y, + Some(MAX_WIDTH_CONTENT_BOUNDS.x), + Some(MAX_WIDTH_CONTENT_BOUNDS.y), ); buffer_dimensions(&buffer) @@ -335,7 +328,7 @@ 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.set_size(font_system, Some(bounds.x.ceil()), Some(bounds.y.ceil())); buffer_dimensions(&buffer) } } @@ -382,7 +375,7 @@ fn get_attrs<'a>( .style(face.style) .weight(face.weight) .metrics(Metrics::relative(section.style.font_size, 1.2)) - .color(cosmic_text::Color(section.style.color.linear().as_u32())); + .color(cosmic_text::Color(section.style.color.to_linear().as_u32())); attrs } From 25b9634ba471210a732031e07cb3f2142c1b5eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Fri, 14 Jun 2024 23:37:19 +0200 Subject: [PATCH 08/47] Clear up some code that have been implemented in Cosmic-Text --- .../src/dynamic_texture_atlas_builder.rs | 2 +- crates/bevy_text/src/error.rs | 2 +- crates/bevy_text/src/pipeline.rs | 76 ++++--------------- crates/bevy_ui/src/widget/text.rs | 1 - examples/ui/text_debug.rs | 20 ++++- 5 files changed, 33 insertions(+), 68 deletions(-) diff --git a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs index f6d3e928cdf7b..fe091be6d1580 100644 --- a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs @@ -53,7 +53,7 @@ impl DynamicTextureAtlasBuilder { (texture.height() + self.padding).try_into().unwrap(), )); if let Some(allocation) = allocation { - let atlas_texture = textures.get_mut(atlas_texture_handle).unwrap(); + let atlas_texture = textures.get_mut(atlas_texture_handle)?; assert!( ::asset_usage(atlas_texture) .contains(RenderAssetUsages::MAIN_WORLD), diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 78c24ef9144e9..9d40e0fcdd59c 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -5,7 +5,7 @@ use thiserror::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 reason + /// 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 diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 294cdec9cfbe0..49ce423f3ac6f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -76,13 +76,6 @@ impl TextPipeline { as f64 * scale_factor; - // TODO: maybe we would like to render negative fontsizes or scaling upside down or something? for now, no text is rendered - if font_size <= 0.0 { - // return empty buffer, making sure that the line height is not zero, - // since that results in a panic in cosmic-text - let metrics = Metrics::new(0.0, 0.000001); - return Ok(Buffer::new_empty(metrics)); - }; // 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); @@ -99,6 +92,7 @@ impl TextPipeline { let spans: Vec<(&str, Attrs)> = sections .iter() + .filter(|section| section.style.font_size > 0.0) .enumerate() .map(|(section_index, section)| { ( @@ -114,10 +108,16 @@ impl TextPipeline { }) .collect(); + if spans.is_empty() { + // return empty buffer if no fonts are larger than 0, making sure that the line height is not zero, + // since that results in a panic in cosmic-text + let metrics = Metrics::new(0.0, 0.000001); + return Ok(Buffer::new_empty(metrics)); + } + // TODO: cache buffers (see Iced / glyphon) let mut buffer = Buffer::new_empty(metrics); - let buffer_height = f32::INFINITY; - buffer.set_size(font_system, Some(bounds.x.ceil()), Some(buffer_height)); + buffer.set_size(font_system, Some(bounds.x.ceil()), None); buffer.set_wrap( font_system, @@ -254,12 +254,9 @@ impl TextPipeline { fonts: &Assets, sections: &[TextSection], scale_factor: f64, - // TODO: not currently required - _text_alignment: JustifyText, linebreak_behavior: BreakLineOn, ) -> Result { const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); - const MAX_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(f32::INFINITY, f32::INFINITY); let mut buffer = self.create_buffer( fonts, @@ -273,13 +270,7 @@ impl TextPipeline { let max_width_content_size = { let font_system = &mut acquire_font_system(&mut self.font_system)?; - - buffer.set_size( - font_system, - Some(MAX_WIDTH_CONTENT_BOUNDS.x), - Some(MAX_WIDTH_CONTENT_BOUNDS.y), - ); - + buffer.set_size(font_system, None, None); buffer_dimensions(&buffer) }; @@ -298,7 +289,9 @@ impl TextPipeline { #[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, } @@ -381,59 +374,16 @@ fn get_attrs<'a>( /// 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_or_else(|| 0.0); - // 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 } - } -} - -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) - } - } + Vec2::new(width.ceil(), height).ceil() } /// Helper method to acquire a font system mutex. diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index feb6883f9777e..321177f7521b9 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -93,7 +93,6 @@ fn create_text_measure( fonts, &text.sections, scale_factor, - text.justify, text.linebreak_behavior, ) { Ok(measure) => { diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 6391f2ce68104..bab30176697e4 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -145,6 +145,14 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, ), + TextSection::new( + " this text has zero fontsize", + TextStyle { + font: font.clone(), + font_size: 0.0, + color: BLUE.into(), + }, + ), TextSection::new( "\nThis text changes in the bottom right - ", TextStyle { @@ -179,6 +187,14 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { color: BLUE.into(), }, ), + TextSection::new( + " this text has negative fontsize", + TextStyle { + font: font.clone(), + font_size: -50.0, + color: BLUE.into(), + }, + ), ]), TextChanges, )); @@ -216,8 +232,8 @@ fn change_text_system( "This text changes in the bottom right - {fps:.1} fps, {frame_time:.3} ms/frame", ); - text.sections[2].value = format!("{fps:.1}"); + text.sections[3].value = format!("{fps:.1}"); - text.sections[4].value = format!("{frame_time:.3}"); + text.sections[5].value = format!("{frame_time:.3}"); } } From cde816f7a585ebaf8e2cab2afbcd47ecbf4d8640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sat, 15 Jun 2024 09:43:19 +0200 Subject: [PATCH 09/47] Some cleanup --- crates/bevy_text/src/font_atlas_set.rs | 2 +- crates/bevy_text/src/glyph.rs | 1 + crates/bevy_text/src/text.rs | 1 - crates/bevy_text/src/text2d.rs | 3 +-- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 6250785e67560..c6a81c2a7e22d 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -209,7 +209,7 @@ impl FontAtlasSet { .collect(), cosmic_text::SwashContent::Color => image.data, cosmic_text::SwashContent::SubpixelMask => { - // TODO + // TODO: implement todo!() } }; diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 3974f16d830e0..864838cb4d261 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -29,6 +29,7 @@ pub struct PositionedGlyph { } impl PositionedGlyph { + /// Creates a new [`PositionedGlyph`] pub fn new( position: Vec2, size: Vec2, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index dc306f76d9494..dbbce33e242cc 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -6,7 +6,6 @@ use bevy_utils::default; use serde::{Deserialize, Serialize}; use crate::Font; -// TODO: reexport cosmic_text and these types in the prelude pub use cosmic_text::{ self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 60692e0b967ec..be6584e9fd7d5 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -291,7 +291,6 @@ mod tests { app.init_resource::>() .init_resource::>() .init_resource::>() - .init_resource::() .init_resource::() .init_resource::>() .insert_resource(TextPipeline::default()) @@ -308,7 +307,7 @@ mod tests { app, Handle::default(), "FiraMono-subset.ttf", - |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } + |bytes: &[u8], _path: String| { Font::from_bytes(bytes.to_vec()) } ); let entity = app From c68736d8cc786f878486f2598d16eb71a752f993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sat, 15 Jun 2024 12:48:34 +0200 Subject: [PATCH 10/47] Use cosmic-texts ttf-parser version to verify the font --- Cargo.toml | 4 +++- crates/bevy_text/src/font.rs | 10 +++++++--- crates/bevy_text/src/font_loader.rs | 8 +++++--- crates/bevy_text/src/lib.rs | 2 +- crates/bevy_text/src/text2d.rs | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e539fd3a95189..789ac6d06ab11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3233,4 +3233,6 @@ all-features = true cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] [patch.crates-io] -cosmic-text = { git = 'https://github.com/pop-os/cosmic-text.git'} +# cosmic-text = { git = 'https://github.com/pop-os/cosmic-text.git'} +cosmic-text = { git = 'https://github.com/totalkrill/cosmic-text.git', branch = "dep_upgrade"} +# cosmic-text = { path = '../cosmic-text'} diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index e12c49f314dc5..0edc858c41524 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -14,10 +14,14 @@ pub struct Font { impl Font { /// Creates a [Font] from bytes, without any validation of the content - pub fn from_bytes(font_data: Vec) -> Self { + pub fn try_from_bytes( + font_data: Vec, + ) -> Result { // TODO: validate font, restore `try_from_bytes` - Self { + 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_loader.rs b/crates/bevy_text/src/font_loader.rs index f5e22848eaeab..f7c1bf3c145c9 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,15 +1,18 @@ use crate::Font; use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; -use bevy_utils::{ConditionalSend, ConditionalSendFuture}; use thiserror::Error; #[derive(Default)] +/// FontLoader for usage 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), @@ -27,8 +30,7 @@ impl AssetLoader for FontLoader { ) -> Result { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - let font = Font::from_bytes(bytes); - // load_context.set_default_asset(LoadedAsset::new(font)); + let font = Font::try_from_bytes(bytes)?; Ok(font) } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index c549499aea4e2..60c39c7787436 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -124,7 +124,7 @@ impl Plugin for TextPlugin { app, Handle::default(), "FiraMono-subset.ttf", - |bytes: &[u8], _path: String| { Font::from_bytes(bytes.to_vec()) } + |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } ); } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index be6584e9fd7d5..294868bc682f2 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -307,7 +307,7 @@ mod tests { app, Handle::default(), "FiraMono-subset.ttf", - |bytes: &[u8], _path: String| { Font::from_bytes(bytes.to_vec()) } + |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } ); let entity = app From 13f0740bdda66d972cf338c6e9be5f17ef8289d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sat, 15 Jun 2024 12:51:55 +0200 Subject: [PATCH 11/47] Remove unused --- crates/bevy_text/src/font_atlas_set.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index c6a81c2a7e22d..6eb451e067024 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,11 +1,10 @@ use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; -use bevy_ecs::prelude::*; use bevy_ecs::{ event::EventReader, system::{ResMut, Resource}, }; use bevy_math::{IVec2, UVec2}; -use bevy_reflect::{Reflect, TypePath}; +use bevy_reflect::TypePath; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, From e8144af063b7ae092239c296eea88b8c961e32da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sun, 16 Jun 2024 22:19:09 +0200 Subject: [PATCH 12/47] --amend --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 789ac6d06ab11..3b14a9326b757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3233,6 +3233,6 @@ all-features = true cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] [patch.crates-io] -# cosmic-text = { git = 'https://github.com/pop-os/cosmic-text.git'} -cosmic-text = { git = 'https://github.com/totalkrill/cosmic-text.git', branch = "dep_upgrade"} +cosmic-text = { git = 'https://github.com/pop-os/cosmic-text.git'} +# cosmic-text = { git = 'https://github.com/totalkrill/cosmic-text.git', branch = "dep_upgrade"} # cosmic-text = { path = '../cosmic-text'} From 6ac21550a657a730c99433cfe09d7a1406f06357 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 17 Jun 2024 13:18:34 +1200 Subject: [PATCH 13/47] Remove Mutex from Buffer --- crates/bevy_text/src/pipeline.rs | 12 ++++++------ crates/bevy_ui/src/measurement.rs | 6 +++--- crates/bevy_ui/src/widget/image.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 49ce423f3ac6f..d602a3b748d7d 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -278,7 +278,7 @@ impl TextPipeline { min: min_width_content_size, max: max_width_content_size, font_system: Arc::clone(&self.font_system.0), - buffer: Mutex::new(buffer), + buffer, }) } } @@ -302,7 +302,7 @@ pub struct TextLayoutInfo { pub struct TextMeasureInfo { pub min: Vec2, pub max: Vec2, - buffer: Mutex, + buffer: cosmic_text::Buffer, font_system: Arc>, } @@ -318,11 +318,11 @@ impl std::fmt::Debug for TextMeasureInfo { } impl TextMeasureInfo { - pub fn compute_size(&self, bounds: Vec2) -> Vec2 { + pub fn compute_size(&mut 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, Some(bounds.x.ceil()), Some(bounds.y.ceil())); - buffer_dimensions(&buffer) + self.buffer + .set_size(font_system, Some(bounds.x.ceil()), Some(bounds.y.ceil())); + buffer_dimensions(&self.buffer) } } diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index f84391896e426..3aa2c5a84b763 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -21,7 +21,7 @@ impl std::fmt::Debug for ContentSize { pub trait Measure: Send + Sync + 'static { /// Calculate the size of the node given the constraints. fn measure( - &self, + &mut self, width: Option, height: Option, available_width: AvailableSpace, @@ -44,7 +44,7 @@ pub enum NodeMeasure { impl Measure for NodeMeasure { fn measure( - &self, + &mut self, width: Option, height: Option, available_width: AvailableSpace, @@ -78,7 +78,7 @@ pub struct FixedMeasure { impl Measure for FixedMeasure { fn measure( - &self, + &mut self, _: Option, _: Option, _: AvailableSpace, diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 28b50be15c455..4603daa00c867 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -38,7 +38,7 @@ pub struct ImageMeasure { impl Measure for ImageMeasure { fn measure( - &self, + &mut self, width: Option, height: Option, available_width: AvailableSpace, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 321177f7521b9..2a097d3f34e00 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -48,7 +48,7 @@ pub struct TextMeasure { impl Measure for TextMeasure { fn measure( - &self, + &mut self, width: Option, height: Option, available_width: AvailableSpace, From e7f71495f3de4b6e33b29b7f617b8de03a927d8d Mon Sep 17 00:00:00 2001 From: sam edelsten Date: Mon, 17 Jun 2024 15:30:37 +0100 Subject: [PATCH 14/47] rebase - add persistent buffers --- crates/bevy_text/Cargo.toml | 1 + crates/bevy_text/src/pipeline.rs | 85 ++++++++++++++++++++++++++++-- crates/bevy_text/src/text.rs | 12 +++++ crates/bevy_text/src/text2d.rs | 26 +++++++-- crates/bevy_ui/src/node_bundles.rs | 4 ++ crates/bevy_ui/src/widget/text.rs | 16 +++++- 6 files changed, 134 insertions(+), 10 deletions(-) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 606af99095426..37903314a054f 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -16,6 +16,7 @@ default_font = [] bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index d602a3b748d7d..fe6911a621567 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -11,8 +11,8 @@ use bevy_utils::HashMap; use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, BreakLineOn, Font, FontAtlasSets, JustifyText, PositionedGlyph, TextSection, - YAxisOrientation, + error::TextError, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, PositionedGlyph, + TextSection, YAxisOrientation, }; /// A wrapper around a [`cosmic_text::FontSystem`] @@ -133,6 +133,76 @@ impl TextPipeline { Ok(buffer) } + /// Utilizes [cosmic_text::Buffer] to shape and layout text + /// + /// Negative or 0.0 font sizes will not be laid out. + pub fn update_buffer( + &mut self, + fonts: &Assets, + sections: &[TextSection], + linebreak_behavior: BreakLineOn, + bounds: Vec2, + scale_factor: f64, + buffer: &mut CosmicBuffer, + ) -> Result<(), TextError> { + // 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)?; + + // return early if the fonts are not loaded yet + for section in sections { + fonts + .get(section.style.font.id()) + .ok_or(TextError::NoSuchFont)?; + } + + let spans: Vec<(&str, Attrs)> = sections + .iter() + .filter(|section| section.style.font_size > 0.0) + .enumerate() + .map(|(section_index, section)| { + ( + §ion.value[..], + get_attrs( + section, + section_index, + font_system, + &mut self.map_handle_to_font_id, + fonts, + ), + ) + }) + .collect(); + + buffer.set_metrics(font_system, metrics); + buffer.set_size(font_system, Some(bounds.x.ceil()), None); + + buffer.set_wrap( + font_system, + match linebreak_behavior { + BreakLineOn::WordBoundary => Wrap::Word, + BreakLineOn::AnyCharacter => Wrap::Glyph, + BreakLineOn::NoWrap => Wrap::None, + }, + ); + + buffer.set_rich_text(font_system, spans, Attrs::new(), Shaping::Advanced); + + Ok(()) + } + /// Queues text for rendering /// /// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s @@ -151,13 +221,20 @@ impl TextPipeline { texture_atlases: &mut Assets, textures: &mut Assets, y_axis_orientation: YAxisOrientation, + buffer: &mut CosmicBuffer, ) -> Result { if sections.is_empty() { return Ok(TextLayoutInfo::default()); } - let buffer = - self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; + self.update_buffer( + fonts, + sections, + linebreak_behavior, + bounds, + scale_factor, + buffer, + )?; let box_size = buffer_dimensions(&buffer); let h_limit = if bounds.x.is_finite() { diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index dbbce33e242cc..8f6234e2ac634 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,8 +1,10 @@ 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; @@ -11,6 +13,16 @@ pub use cosmic_text::{ 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. diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 294868bc682f2..0f1291f498459 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,6 +1,6 @@ use crate::{ - BreakLineOn, Font, FontAtlasSets, PositionedGlyph, Text, TextError, TextLayoutInfo, - TextPipeline, YAxisOrientation, + BreakLineOn, CosmicBuffer, Font, FontAtlasSets, PositionedGlyph, Text, TextError, + TextLayoutInfo, TextPipeline, YAxisOrientation, }; use bevy_asset::Assets; use bevy_color::LinearRgba; @@ -66,6 +66,8 @@ 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 @@ -180,7 +182,13 @@ pub fn update_text2d_layout( 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, + Option<&mut CosmicBuffer>, + )>, ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.read().last().is_some(); @@ -193,7 +201,7 @@ 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_opt) 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 { @@ -203,6 +211,13 @@ pub fn update_text2d_layout( }, scale_value(bounds.size.y, scale_factor), ); + + let mut new_buffer = None; + + if buffer_opt.is_none() { + new_buffer = Some(CosmicBuffer::default()); + } + match text_pipeline.queue_text( &fonts, &text.sections, @@ -214,6 +229,9 @@ pub fn update_text2d_layout( &mut texture_atlases, &mut textures, YAxisOrientation::BottomToTop, + buffer_opt + .as_deref_mut() + .unwrap_or_else(|| new_buffer.as_mut().expect("Couldn't create buffer")), ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, let's add this entity to the diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 89d4ecfced791..1195220da8e47 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -14,6 +14,7 @@ use bevy_color::Color; use bevy_ecs::bundle::Bundle; use bevy_render::view::{InheritedVisibility, ViewVisibility, Visibility}; use bevy_sprite::TextureAtlas; +use bevy_text::CosmicBuffer; #[cfg(feature = "bevy_text")] use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle}; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -185,6 +186,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 @@ -219,6 +222,7 @@ impl Default for TextBundle { fn default() -> Self { Self { text: Default::default(), + buffer: Default::default(), text_layout_info: Default::default(), text_flags: Default::default(), calculated_size: Default::default(), diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 2a097d3f34e00..87ada53a99669 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -15,7 +15,7 @@ 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, + scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, Text, TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation, }; use bevy_utils::Entry; @@ -196,6 +196,7 @@ fn queue_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 { @@ -221,6 +222,7 @@ fn queue_text( texture_atlases, textures, YAxisOrientation::TopToBottom, + buffer, ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, try again next frame @@ -268,11 +270,12 @@ pub fn text_system( &mut TextLayoutInfo, &mut TextFlags, Option<&TargetCamera>, + Option<&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_opt) in &mut text_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; @@ -290,6 +293,12 @@ pub fn text_system( }; let inverse_scale_factor = scale_factor.recip(); + let mut new_buffer = None; + + if buffer_opt.is_none() { + new_buffer = Some(CosmicBuffer::default()); + } + if last_scale_factors.get(&camera_entity) != Some(&scale_factor) || node.is_changed() || text_flags.needs_recompute @@ -306,6 +315,9 @@ pub fn text_system( node, text_flags, text_layout_info, + buffer_opt + .as_deref_mut() + .unwrap_or_else(|| new_buffer.as_mut().expect("Couldn't create buffer")), ); } } From 3bdda24eccd3007fda0ba0b9227f3e5614a6cc3e Mon Sep 17 00:00:00 2001 From: Dimchikkk Date: Mon, 17 Jun 2024 17:11:32 +0100 Subject: [PATCH 15/47] fix text scaling --- crates/bevy_text/src/pipeline.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index d602a3b748d7d..228cdd879f966 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -73,13 +73,12 @@ impl TextPipeline { .get(0) .map(|s| s.style.font_size) .unwrap_or_else(|| crate::TextStyle::default().font_size) - as f64 - * scale_factor; + as f64; // 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 metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32); let font_system = &mut acquire_font_system(&mut self.font_system)?; @@ -103,6 +102,7 @@ impl TextPipeline { font_system, &mut self.map_handle_to_font_id, fonts, + scale_factor, ), ) }) @@ -334,6 +334,7 @@ fn get_attrs<'a>( font_system: &mut cosmic_text::FontSystem, map_handle_to_font_id: &mut HashMap, cosmic_text::fontdb::ID>, fonts: &Assets, + scale_factor: f64, ) -> Attrs<'a> { let font_handle = section.style.font.clone(); let face_id = map_handle_to_font_id @@ -367,7 +368,7 @@ fn get_attrs<'a>( .stretch(face.stretch) .style(face.style) .weight(face.weight) - .metrics(Metrics::relative(section.style.font_size, 1.2)) + .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 } From 7f03e2c9115217d991fff73b4fb8a485e50a0bc3 Mon Sep 17 00:00:00 2001 From: sam edelsten Date: Mon, 17 Jun 2024 19:25:17 +0100 Subject: [PATCH 16/47] remove `Option` wrapper for buffer in query --- crates/bevy_text/src/text2d.rs | 14 +++----------- crates/bevy_ui/src/widget/text.rs | 14 +++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 0f1291f498459..45a21886c9e84 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -187,7 +187,7 @@ pub fn update_text2d_layout( Ref, Ref, &mut TextLayoutInfo, - Option<&mut CosmicBuffer>, + &mut CosmicBuffer, )>, ) { // We need to consume the entire iterator, hence `last` @@ -201,7 +201,7 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, bounds, mut text_layout_info, mut buffer_opt) 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 { @@ -212,12 +212,6 @@ pub fn update_text2d_layout( scale_value(bounds.size.y, scale_factor), ); - let mut new_buffer = None; - - if buffer_opt.is_none() { - new_buffer = Some(CosmicBuffer::default()); - } - match text_pipeline.queue_text( &fonts, &text.sections, @@ -229,9 +223,7 @@ pub fn update_text2d_layout( &mut texture_atlases, &mut textures, YAxisOrientation::BottomToTop, - buffer_opt - .as_deref_mut() - .unwrap_or_else(|| new_buffer.as_mut().expect("Couldn't create buffer")), + buffer.as_mut(), ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, let's add this entity to the diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 87ada53a99669..cbf033cc795f1 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -270,12 +270,12 @@ pub fn text_system( &mut TextLayoutInfo, &mut TextFlags, Option<&TargetCamera>, - Option<&mut CosmicBuffer>, + &mut CosmicBuffer, )>, ) { let mut scale_factors: EntityHashMap = EntityHashMap::default(); - for (node, text, text_layout_info, text_flags, camera, mut buffer_opt) 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; @@ -293,12 +293,6 @@ pub fn text_system( }; let inverse_scale_factor = scale_factor.recip(); - let mut new_buffer = None; - - if buffer_opt.is_none() { - new_buffer = Some(CosmicBuffer::default()); - } - if last_scale_factors.get(&camera_entity) != Some(&scale_factor) || node.is_changed() || text_flags.needs_recompute @@ -315,9 +309,7 @@ pub fn text_system( node, text_flags, text_layout_info, - buffer_opt - .as_deref_mut() - .unwrap_or_else(|| new_buffer.as_mut().expect("Couldn't create buffer")), + buffer.as_mut(), ); } } From 6ea20ff1788d47542d53d45d5d2bffbe188eb947 Mon Sep 17 00:00:00 2001 From: sam edelsten Date: Mon, 17 Jun 2024 19:37:52 +0100 Subject: [PATCH 17/47] remove `create_buffer` in favor of `update_buffer` --- crates/bevy_text/src/pipeline.rs | 85 +++---------------------------- crates/bevy_ui/src/widget/text.rs | 6 ++- 2 files changed, 11 insertions(+), 80 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index fe6911a621567..29cb29514c0b3 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -56,83 +56,6 @@ pub struct TextPipeline { } impl TextPipeline { - /// Utilizes [cosmic_text::Buffer] to shape and layout text - /// - /// Negative or 0.0 font sizes will not be laid out, and an empty buffer will be returned. - 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)?; - - // return early if the fonts are not loaded yet - for section in sections { - fonts - .get(section.style.font.id()) - .ok_or(TextError::NoSuchFont)?; - } - - let spans: Vec<(&str, Attrs)> = sections - .iter() - .filter(|section| section.style.font_size > 0.0) - .enumerate() - .map(|(section_index, section)| { - ( - §ion.value[..], - get_attrs( - section, - section_index, - font_system, - &mut self.map_handle_to_font_id, - fonts, - ), - ) - }) - .collect(); - - if spans.is_empty() { - // return empty buffer if no fonts are larger than 0, making sure that the line height is not zero, - // since that results in a panic in cosmic-text - let metrics = Metrics::new(0.0, 0.000001); - return Ok(Buffer::new_empty(metrics)); - } - - // TODO: cache buffers (see Iced / glyphon) - let mut buffer = Buffer::new_empty(metrics); - buffer.set_size(font_system, Some(bounds.x.ceil()), None); - - buffer.set_wrap( - font_system, - match linebreak_behavior { - BreakLineOn::WordBoundary => Wrap::Word, - BreakLineOn::AnyCharacter => Wrap::Glyph, - BreakLineOn::NoWrap => Wrap::None, - }, - ); - - buffer.set_rich_text(font_system, spans, Attrs::new(), Shaping::Advanced); - - Ok(buffer) - } - /// Utilizes [cosmic_text::Buffer] to shape and layout text /// /// Negative or 0.0 font sizes will not be laid out. @@ -332,15 +255,17 @@ impl TextPipeline { sections: &[TextSection], scale_factor: f64, linebreak_behavior: BreakLineOn, + buffer: &mut CosmicBuffer, ) -> Result { const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); - let mut buffer = self.create_buffer( + self.update_buffer( fonts, sections, linebreak_behavior, MIN_WIDTH_CONTENT_BOUNDS, scale_factor, + buffer, )?; let min_width_content_size = buffer_dimensions(&buffer); @@ -355,7 +280,9 @@ impl TextPipeline { min: min_width_content_size, max: max_width_content_size, font_system: Arc::clone(&self.font_system.0), - buffer, + // 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(), }) } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index cbf033cc795f1..d6c126e2f81e5 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -88,12 +88,14 @@ fn create_text_measure( text_pipeline: &mut TextPipeline, mut content_size: Mut, mut text_flags: Mut, + buffer: &mut CosmicBuffer, ) { match text_pipeline.create_text_measure( fonts, &text.sections, scale_factor, text.linebreak_behavior, + buffer, ) { Ok(measure) => { if text.linebreak_behavior == BreakLineOn::NoWrap { @@ -141,6 +143,7 @@ pub fn measure_text_system( &mut ContentSize, &mut TextFlags, Option<&TargetCamera>, + &mut CosmicBuffer, ), With, >, @@ -148,7 +151,7 @@ pub fn measure_text_system( ) { 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; @@ -176,6 +179,7 @@ pub fn measure_text_system( &mut text_pipeline, content_size, text_flags, + buffer.as_mut(), ); } } From de809865ba1daef26942bcdcb9cbf0b4e336c1b9 Mon Sep 17 00:00:00 2001 From: sam edelsten Date: Mon, 17 Jun 2024 20:33:11 +0100 Subject: [PATCH 18/47] fix feature gate --- crates/bevy_ui/src/node_bundles.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 1195220da8e47..8bf2c8291d242 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -14,9 +14,10 @@ use bevy_color::Color; use bevy_ecs::bundle::Bundle; use bevy_render::view::{InheritedVisibility, ViewVisibility, Visibility}; use bevy_sprite::TextureAtlas; -use bevy_text::CosmicBuffer; #[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. From 2deeb4ca00a1022342fbbdbbf38103154c54333a Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 17 Jun 2024 22:15:24 +0800 Subject: [PATCH 19/47] holy scripture to appease the Clippy --- crates/bevy_text/src/font_atlas.rs | 18 ++++++++++++++---- crates/bevy_text/src/font_atlas_set.rs | 19 ++++++++++++++++--- crates/bevy_text/src/font_loader.rs | 2 +- crates/bevy_text/src/glyph.rs | 9 +++++++-- crates/bevy_text/src/lib.rs | 22 +++++++++++++--------- crates/bevy_text/src/pipeline.rs | 7 +++++-- crates/bevy_text/src/text.rs | 20 ++++++++++++++++++-- 7 files changed, 74 insertions(+), 23 deletions(-) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index c288dea5b320c..eced8a4b97691 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -12,19 +12,27 @@ use crate::{GlyphAtlasLocation, TextError}; /// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`. /// -/// A [`FontAtlasSet`](crate::FontAtlasSet) contains one or more `FontAtlas`es. +/// 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. pub struct FontAtlas { /// Used to update the [`TextureAtlasLayout`]. pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - /// A mapping between subpixel-binned glyphs and their [`GlyphAtlasLocation`]. + /// 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 + /// The texture where this font atlas is located pub texture: Handle, } impl FontAtlas { + /// Create a new [`FontAtlas`] with the given size. pub fn new( textures: &mut Assets, texture_atlases_layout: &mut Assets, @@ -51,10 +59,12 @@ impl FontAtlas { } } + /// 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() } + /// 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) } @@ -67,7 +77,7 @@ impl FontAtlas { /// /// # Returns /// - /// Returns `true` if the glyph is successfully added, or `false` otherwise. + /// 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( diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 6eb451e067024..71c2854d0f65c 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -15,6 +15,7 @@ use bevy_utils::HashMap; use crate::{error::TextError, Font, FontAtlas, GlyphAtlasInfo}; +/// 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 @@ -22,21 +23,23 @@ pub struct FontAtlasSets { } 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); @@ -44,6 +47,9 @@ pub fn remove_dropped_font_atlas_sets( } } +/// 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); @@ -53,6 +59,8 @@ impl From for FontSizeKey { } } +/// 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`]. @@ -60,8 +68,9 @@ impl From for FontSizeKey { /// 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`. +/// - ~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. /// @@ -80,10 +89,12 @@ 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() } + /// 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(font_size) @@ -92,6 +103,7 @@ impl FontAtlasSet { }) } + /// 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, @@ -148,6 +160,7 @@ impl FontAtlasSet { 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, cache_key: cosmic_text::CacheKey, diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index f7c1bf3c145c9..945987d748b8d 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -3,7 +3,7 @@ use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; use thiserror::Error; #[derive(Default)] -/// FontLoader for usage by the [`AssetServer`] +/// An [`AssetLoader`] for [`Font`]s, for use by the [`AssetServer`] pub struct FontLoader; /// Possible errors that can be produced by [`FontLoader`] diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 864838cb4d261..ba949efbfd000 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -54,10 +54,15 @@ impl PositionedGlyph { /// 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 texture atlas this glyph was placed in. + /// 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. + /// Location and offset of a glyph within the texture atlas. pub location: GlyphAtlasLocation, } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 60c39c7787436..98c63b7cf08bb 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -8,24 +8,26 @@ //! 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, +//! 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 a system. +//! 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, another system passes it into [`TextPipeline::queue_text`], which: +//! 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. +//! which contains all the information that downstream systems need for rendering. #![allow(clippy::type_complexity)] @@ -73,13 +75,15 @@ use bevy_sprite::SpriteSystem; #[derive(Default)] pub struct TextPlugin; -/// 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 { - /// Ui Y-axis orientation + /// Top to bottom Y-axis orientation, for UI TopToBottom, - /// Text2dBundles Y-axis orientation + /// Bottom to top Y-axis orientation, for 2d world space BottomToTop, } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 228cdd879f966..f01d2f7e4b44f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -56,7 +56,7 @@ pub struct TextPipeline { } impl TextPipeline { - /// Utilizes [cosmic_text::Buffer] to shape and layout text + /// Utilizes [`cosmic_text::Buffer`] to shape and layout text /// /// Negative or 0.0 font sizes will not be laid out, and an empty buffer will be returned. pub fn create_buffer( @@ -300,7 +300,9 @@ pub struct TextLayoutInfo { /// /// Generated via [`TextPipeline::create_text_measure`]. pub struct TextMeasureInfo { + /// Minimum size for a text area pub min: Vec2, + /// Maximum size for a text area pub max: Vec2, buffer: cosmic_text::Buffer, font_system: Arc>, @@ -318,6 +320,7 @@ impl std::fmt::Debug for TextMeasureInfo { } impl TextMeasureInfo { + /// Computes the size of the text area within the provided bounds. pub fn compute_size(&mut self, bounds: Vec2) -> Vec2 { let font_system = &mut self.font_system.try_lock().expect("Failed to acquire lock"); self.buffer @@ -326,7 +329,7 @@ impl TextMeasureInfo { } } -/// get attr for from textstyle +/// Translates [`TextSection`] to [`Attrs`](cosmic_text::attrs::Attrs), /// loading fonts into the [`Database`](cosmic_text::fontdb::Database) if required. fn get_attrs<'a>( section: &'a TextSection, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 3a7361d495778..5759d9a9b4298 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -117,7 +117,9 @@ 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 value 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, } @@ -180,11 +182,24 @@ pub enum JustifyText { } #[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. + /// + /// # 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. pub font: Handle, /// The vertical height of rasterized glyphs in the font atlas in pixels. /// @@ -194,6 +209,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, } From ed658c918098642bb63ffcc00d0ce83d44549505 Mon Sep 17 00:00:00 2001 From: TotalKrill Date: Mon, 17 Jun 2024 20:28:47 +0200 Subject: [PATCH 20/47] Apply suggestions from code review Co-authored-by: Alice Cecile --- crates/bevy_text/src/font_atlas.rs | 3 ++- crates/bevy_text/src/text.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index eced8a4b97691..980085a32fd07 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -19,6 +19,7 @@ use crate::{GlyphAtlasLocation, TextError}; /// 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. pub struct FontAtlas { /// Used to update the [`TextureAtlasLayout`]. @@ -32,7 +33,7 @@ pub struct FontAtlas { } impl FontAtlas { - /// Create a new [`FontAtlas`] with the given size. + /// Create a new [`FontAtlas`] with the given size, adding it to the appropriate asset collections. pub fn new( textures: &mut Assets, texture_atlases_layout: &mut Assets, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 5759d9a9b4298..2bb07b80e9005 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -117,7 +117,7 @@ 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 value of the text in the section. + /// 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, From 14621b576a9d4d9b6a99b4940ef8cb2ba1e856aa Mon Sep 17 00:00:00 2001 From: TotalKrill Date: Mon, 17 Jun 2024 21:45:03 +0200 Subject: [PATCH 21/47] Apply suggestions from code review --- crates/bevy_text/src/pipeline.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f01d2f7e4b44f..1dbed1c1cec14 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -300,9 +300,9 @@ pub struct TextLayoutInfo { /// /// Generated via [`TextPipeline::create_text_measure`]. pub struct TextMeasureInfo { - /// Minimum size for a text area + /// 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 + /// Maximum size for a text area in pixels, to be used when laying out widgets with taffy pub max: Vec2, buffer: cosmic_text::Buffer, font_system: Arc>, From 38f75e324ac7758b39c220c1dac4a3f06a0f4370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Mon, 17 Jun 2024 21:45:22 +0200 Subject: [PATCH 22/47] Move not to fonts --- crates/bevy_text/src/font.rs | 9 +++++++++ crates/bevy_text/src/text.rs | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 0edc858c41524..d53792cbf2cd6 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -6,6 +6,15 @@ use bevy_reflect::TypePath; /// 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 { /// Content of a font file as bytes diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 2bb07b80e9005..1fa1ea4102ad0 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -191,15 +191,6 @@ pub struct TextStyle { /// * if `default_font` feature is enabled (enabled by default in `bevy` crate), /// `FiraMono-subset.ttf` compiled into the library is used. /// * otherwise no text will be rendered. - /// - /// # 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. pub font: Handle, /// The vertical height of rasterized glyphs in the font atlas in pixels. /// From ac33b839b655ae817edd315f358ea2cd87623ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Mon, 17 Jun 2024 22:03:54 +0200 Subject: [PATCH 23/47] remove unused and fix comment --- crates/bevy_text/src/font_atlas.rs | 3 ++- crates/bevy_text/src/pipeline.rs | 12 ------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 980085a32fd07..759ceb3e9b32f 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -20,7 +20,8 @@ use crate::{GlyphAtlasLocation, TextError}; /// 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. +/// 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, diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 1dbed1c1cec14..5eee858a75877 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -160,18 +160,6 @@ impl TextPipeline { self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; let box_size = buffer_dimensions(&buffer); - let h_limit = if bounds.x.is_finite() { - bounds.x - } else { - box_size.x - }; - - let h_anchor = match text_alignment { - JustifyText::Left => 0.0, - JustifyText::Center => h_limit * 0.5, - JustifyText::Right => h_limit * 1.0, - } - .floor(); let font_system = &mut acquire_font_system(&mut self.font_system)?; let swash_cache = &mut self.swash_cache.0; From 064550a7a53ed9540a1e323721403b430538c212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Mon, 17 Jun 2024 22:28:35 +0200 Subject: [PATCH 24/47] use the different font sizes to get the metrics for the buffer --- crates/bevy_text/src/pipeline.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 27a60b40fc3ad..7d8d2e2fdc5b0 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -68,27 +68,21 @@ impl TextPipeline { scale_factor: f64, buffer: &mut CosmicBuffer, ) -> Result<(), TextError> { - // 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; - - // 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).scale(scale_factor as f32); - let font_system = &mut acquire_font_system(&mut self.font_system)?; // 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 (font_size, line_height) = (font_size as f32, line_height as f32); + let metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32); let spans: Vec<(&str, Attrs)> = sections .iter() From 83e11844543cfb9e24ce9572d3b61bdcec1da0c2 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 18 Jun 2024 21:14:31 +0800 Subject: [PATCH 25/47] remove todo: we can use metadata --- crates/bevy_text/src/pipeline.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 7d8d2e2fdc5b0..0b0aaee482055 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -350,7 +350,6 @@ fn get_attrs<'a>( // TODO: validate this is the correct string to extract // let family_name = &face.families[0].0; let attrs = Attrs::new() - // TODO: validate that we can use metadata .metadata(section_index) // TODO: this reference, becomes owned by the font system, which is not really wanted... // .family(Family::Name(family_name)) From 9795a3b8c39e4210e9ce7378b473a2809790433d Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 18 Jun 2024 22:37:32 +0800 Subject: [PATCH 26/47] remove mutex on font system --- crates/bevy_text/src/pipeline.rs | 41 +++++++++---------- crates/bevy_ui/src/layout/mod.rs | 12 +++++- crates/bevy_ui/src/layout/ui_surface.rs | 21 +++++++--- crates/bevy_ui/src/measurement.rs | 52 ++++++++----------------- crates/bevy_ui/src/widget/image.rs | 20 +++++----- crates/bevy_ui/src/widget/text.rs | 23 ++++++----- 6 files changed, 84 insertions(+), 85 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 7d8d2e2fdc5b0..2836de526162a 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use bevy_asset::{AssetId, Assets}; use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; @@ -16,16 +16,14 @@ use crate::{ }; /// A wrapper around a [`cosmic_text::FontSystem`] -struct CosmicFontSystem(Arc>); +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(Arc::new(Mutex::new( - cosmic_text::FontSystem::new_with_locale_and_db(locale, db), - ))) + Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db)) } } @@ -68,7 +66,7 @@ impl TextPipeline { scale_factor: f64, buffer: &mut CosmicBuffer, ) -> Result<(), TextError> { - let font_system = &mut acquire_font_system(&mut self.font_system)?; + let font_system = &mut self.font_system.0; // return early if the fonts are not loaded yet let mut font_size = 0.; @@ -154,7 +152,7 @@ impl TextPipeline { )?; let box_size = buffer_dimensions(&buffer); - let font_system = &mut acquire_font_system(&mut self.font_system)?; + let font_system = &mut self.font_system.0; let swash_cache = &mut self.swash_cache.0; let glyphs = buffer @@ -253,7 +251,7 @@ impl TextPipeline { let min_width_content_size = buffer_dimensions(&buffer); let max_width_content_size = { - let font_system = &mut acquire_font_system(&mut self.font_system)?; + let font_system = &mut self.font_system.0; buffer.set_size(font_system, None, None); buffer_dimensions(&buffer) }; @@ -261,12 +259,18 @@ impl TextPipeline { Ok(TextMeasureInfo { min: min_width_content_size, max: max_width_content_size, - font_system: Arc::clone(&self.font_system.0), // 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 + } } /// Render information for a corresponding [`Text`](crate::Text) component. @@ -291,7 +295,6 @@ pub struct TextMeasureInfo { /// Maximum size for a text area in pixels, to be used when laying out widgets with taffy pub max: Vec2, buffer: cosmic_text::Buffer, - font_system: Arc>, } impl std::fmt::Debug for TextMeasureInfo { @@ -307,8 +310,11 @@ impl std::fmt::Debug for TextMeasureInfo { impl TextMeasureInfo { /// Computes the size of the text area within the provided bounds. - pub fn compute_size(&mut self, bounds: Vec2) -> Vec2 { - let font_system = &mut self.font_system.try_lock().expect("Failed to acquire lock"); + pub fn compute_size( + &mut self, + bounds: Vec2, + font_system: &mut cosmic_text::FontSystem, + ) -> Vec2 { self.buffer .set_size(font_system, Some(bounds.x.ceil()), Some(bounds.y.ceil())); buffer_dimensions(&self.buffer) @@ -375,14 +381,3 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { Vec2::new(width.ceil(), height).ceil() } - -/// Helper method to acquire a font system mutex. -#[inline(always)] -fn acquire_font_system( - font_system: &mut CosmicFontSystem, -) -> Result, TextError> { - font_system - .0 - .try_lock() - .map_err(|_| TextError::FailedToAcquireMutex) -} diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 43d767acdecbc..c05a14377595d 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, @@ -217,10 +219,18 @@ pub fn ui_layout_system( } } + #[cfg(feature = "bevy_text")] + let font_system = text_pipeline.font_system_mut(); + 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, diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index 8bb13d83bcf70..d7139fbd7c304 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 3aa2c5a84b763..cdc279921856a 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( - &mut self, - width: Option, - height: Option, - available_width: AvailableSpace, - available_height: AvailableSpace, - style: &taffy::Style, - ) -> Vec2; + fn measure<'a>(&mut self, measure_args: MeasureArgs<'a>, 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( - &mut 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( - &mut 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/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 4603daa00c867..976a89501a356 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -1,5 +1,6 @@ use crate::{ - measurement::AvailableSpace, ContentSize, Measure, Node, NodeMeasure, UiImage, UiScale, + ContentSize, Measure, MeasureArgs, Node, NodeMeasure, UiImage, + UiScale, }; use bevy_asset::Assets; use bevy_ecs::prelude::*; @@ -37,14 +38,15 @@ pub struct ImageMeasure { } impl Measure for ImageMeasure { - fn measure( - &mut 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 fbef8160d2202..83500a5d62f49 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::{ @@ -47,14 +48,14 @@ pub struct TextMeasure { } impl Measure for TextMeasure { - fn measure( - &mut 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 @@ -70,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(Vec2::new(x, f32::MAX), font_system) + } AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), }, From 5f0cd22ab0f65caa053639dc9053a46b99c94525 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 18 Jun 2024 22:43:34 +0800 Subject: [PATCH 27/47] remove TextError::FailedToAcquireMutex --- crates/bevy_text/src/error.rs | 4 ---- crates/bevy_text/src/text2d.rs | 6 +----- crates/bevy_ui/src/widget/text.rs | 12 ++---------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 9d40e0fcdd59c..ef9f7ea590deb 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -11,10 +11,6 @@ pub enum TextError { /// Failed to add glyph to a newly created atlas for some reason #[error("failed to add glyph to newly-created atlas {0:?}")] FailedToAddGlyph(u16), - /// Failed to acquire mutex to cosmic-texts fontsystem - //TODO: this can be removed since the mutex should be possible to remove as well - #[error("font system mutex could not be acquired or is poisoned")] - FailedToAcquireMutex, /// 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/text2d.rs b/crates/bevy_text/src/text2d.rs index 45a21886c9e84..9391dd459c78f 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -230,11 +230,7 @@ pub fn update_text2d_layout( // queue for further processing queue.insert(entity); } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToAcquireMutex - | TextError::FailedToGetGlyphImage(_)), - ) => { + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 83500a5d62f49..e65f9767a3bdb 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -115,11 +115,7 @@ fn create_text_measure( // Try again next frame text_flags.needs_new_measure_func = true; } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToAcquireMutex - | TextError::FailedToGetGlyphImage(_)), - ) => { + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } }; @@ -235,11 +231,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(_) - | TextError::FailedToAcquireMutex - | TextError::FailedToGetGlyphImage(_)), - ) => { + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { From eb4305c70fd0f57fd193a5f21f6fe96bc940b16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Tue, 18 Jun 2024 23:54:46 +0200 Subject: [PATCH 28/47] remove Patch, use crates.io release of cosmic-text --- Cargo.toml | 4 ---- crates/bevy_text/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2dd7337068aae..d69a42e3fc6d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3243,7 +3243,3 @@ rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] -[patch.crates-io] -cosmic-text = { git = 'https://github.com/pop-os/cosmic-text.git'} -# cosmic-text = { git = 'https://github.com/totalkrill/cosmic-text.git', branch = "dep_upgrade"} -# cosmic-text = { path = '../cosmic-text'} diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 37903314a054f..7f03b30bc170e 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -29,7 +29,7 @@ bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } # other -cosmic-text = "0.11.2" +cosmic-text = "0.12" thiserror = "1.0" serde = { version = "1", features = ["derive"] } unicode-bidi = "0.3.13" From 4cb8a86fa7a128fb8bc16c7eb63bb47d9748d4ee Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 19 Jun 2024 12:47:37 +1200 Subject: [PATCH 29/47] Fix borrow checking issues around family names --- crates/bevy_text/src/pipeline.rs | 54 ++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 246f22af3e937..21194392e34de 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -8,7 +8,7 @@ use bevy_render::texture::Image; use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; -use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap}; +use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ error::TextError, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, PositionedGlyph, @@ -42,7 +42,7 @@ impl Default for SwashCache { #[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>, + 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. @@ -82,6 +82,13 @@ impl TextPipeline { let (font_size, line_height) = (font_size as f32, line_height as f32); 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 let spans: Vec<(&str, Attrs)> = sections .iter() .filter(|section| section.style.font_size > 0.0) @@ -93,8 +100,7 @@ impl TextPipeline { section, section_index, font_system, - &mut self.map_handle_to_font_id, - fonts, + &self.map_handle_to_font_id, scale_factor, ), ) @@ -321,18 +327,14 @@ impl TextMeasureInfo { } } -/// Translates [`TextSection`] to [`Attrs`](cosmic_text::attrs::Attrs), -/// loading fonts into the [`Database`](cosmic_text::fontdb::Database) if required. -fn get_attrs<'a>( - section: &'a TextSection, - section_index: usize, +fn load_font_to_fontdb( + section: &TextSection, font_system: &mut cosmic_text::FontSystem, - map_handle_to_font_id: &mut HashMap, cosmic_text::fontdb::ID>, + map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, String)>, fonts: &Assets, - scale_factor: f64, -) -> Attrs<'a> { +) { let font_handle = section.style.font.clone(); - let face_id = map_handle_to_font_id + map_handle_to_font_id .entry(font_handle.id()) .or_insert_with(|| { let font = fonts.get(font_handle.id()).expect( @@ -342,8 +344,13 @@ fn get_attrs<'a>( 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 - *ids.last().unwrap() + 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) // TODO: below may be required if we need to offset by the baseline (TBC) // see https://github.com/pop-os/cosmic-text/issues/123 @@ -352,13 +359,26 @@ fn get_attrs<'a>( // .entry(face_id) // .or_insert_with(|| font.as_swash().metrics(&[])); }); +} + +/// 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(); + // TODO: validate this is the correct string to extract - // let family_name = &face.families[0].0; let attrs = Attrs::new() .metadata(section_index) - // TODO: this reference, becomes owned by the font system, which is not really wanted... - // .family(Family::Name(family_name)) + .family(Family::Name(family_name)) .stretch(face.stretch) .style(face.style) .weight(face.weight) From b3610994ff4d17bec7d88d0647269acf1f637bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Wed, 19 Jun 2024 19:53:44 +0200 Subject: [PATCH 30/47] default textstyle 24, and taplo fmt fix --- Cargo.toml | 1 - crates/bevy_text/src/text.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d69a42e3fc6d1..3f2bc2cfda70f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3242,4 +3242,3 @@ panic = "abort" rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] - diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 0e5498a24c98d..244dc1c891be0 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -220,7 +220,7 @@ impl Default for TextStyle { fn default() -> Self { Self { font: Default::default(), - font_size: 12.0, + font_size: 24.0, color: Color::WHITE, } } From 43afc6765d1fecf8223f0b263e90915bc03f4581 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Wed, 19 Jun 2024 12:08:16 -0700 Subject: [PATCH 31/47] Fix `ui_scaling` example failing to compile --- examples/ui/ui_scaling.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/ui/ui_scaling.rs b/examples/ui/ui_scaling.rs index 986c12735e151..e3adea12a05d3 100644 --- a/examples/ui/ui_scaling.rs +++ b/examples/ui/ui_scaling.rs @@ -1,16 +1,12 @@ //! This example illustrates the [`UiScale`] resource from `bevy_ui`. -use bevy::{color::palettes::css::*, prelude::*, text::TextSettings, utils::Duration}; +use bevy::{color::palettes::css::*, prelude::*, utils::Duration}; const SCALE_TIME: u64 = 400; fn main() { App::new() .add_plugins(DefaultPlugins) - .insert_resource(TextSettings { - allow_dynamic_font_size: true, - ..default() - }) .insert_resource(TargetScale { start_scale: 1.0, target_scale: 1.0, From d259f38a3d7ee44a7595fc7f33aaef97063ae3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Wed, 19 Jun 2024 21:49:52 +0200 Subject: [PATCH 32/47] fix failing tests --- crates/bevy_text/src/font_atlas.rs | 1 + crates/bevy_ui/src/layout/mod.rs | 4 ++++ crates/bevy_ui/src/widget/image.rs | 5 +---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 759ceb3e9b32f..7be8e5a9a0ca8 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -117,6 +117,7 @@ impl std::fmt::Debug for FontAtlas { 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_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index c05a14377595d..9d29c694491dc 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -411,6 +411,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(( @@ -1043,6 +1045,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/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 976a89501a356..4451aed20e8b8 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -1,7 +1,4 @@ -use crate::{ - ContentSize, Measure, MeasureArgs, 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}; From 9317827e07104e076aefca8c7c82518b0849b323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Wed, 19 Jun 2024 22:15:28 +0200 Subject: [PATCH 33/47] in clippy we trust --- crates/bevy_text/src/font_atlas_set.rs | 2 +- crates/bevy_text/src/pipeline.rs | 9 ++++----- crates/bevy_ui/src/measurement.rs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 71c2854d0f65c..e4d11b60c0617 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -154,7 +154,7 @@ impl FontAtlasSet { physical_glyph.cache_key, &glyph_texture, offset, - )? + )?; } Ok(self.get_glyph_atlas_info(physical_glyph.cache_key).unwrap()) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 21194392e34de..0ee528aea74af 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -79,7 +79,6 @@ impl TextPipeline { .ok_or(TextError::NoSuchFont)?; } 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).scale(scale_factor as f32); // Load Bevy fonts into cosmic-text's font system. @@ -157,7 +156,7 @@ impl TextPipeline { buffer, )?; - let box_size = buffer_dimensions(&buffer); + let box_size = buffer_dimensions(buffer); let font_system = &mut self.font_system.0; let swash_cache = &mut self.swash_cache.0; @@ -254,12 +253,12 @@ impl TextPipeline { buffer, )?; - let min_width_content_size = buffer_dimensions(&buffer); + 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) + buffer_dimensions(buffer) }; Ok(TextMeasureInfo { @@ -394,7 +393,7 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { .layout_runs() .map(|run| run.line_w) .reduce(|max_w, w| max_w.max(w)) - .unwrap_or_else(|| 0.0); + .unwrap_or(0.0); let line_height = buffer.metrics().line_height.ceil(); let height = buffer.layout_runs().count() as f32 * line_height; diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index cdc279921856a..1d4d7beb8021c 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -29,7 +29,7 @@ pub struct MeasureArgs<'a> { /// 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<'a>(&mut self, measure_args: MeasureArgs<'a>, 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) From c0d7363ac2cc4b06fb34c519a5a6a948608a1233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Wed, 19 Jun 2024 22:26:47 +0200 Subject: [PATCH 34/47] Change default to stay the same as before --- crates/bevy_text/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 244dc1c891be0..2788f822e305d 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -220,7 +220,7 @@ impl Default for TextStyle { fn default() -> Self { Self { font: Default::default(), - font_size: 24.0, + font_size: 20.0, color: Color::WHITE, } } From 0a5f0c5881debbe6aebf1cef8c5dbfa55a139acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Wed, 19 Jun 2024 23:03:12 +0200 Subject: [PATCH 35/47] Fix enumeration and add comment as to why it is there --- crates/bevy_text/src/pipeline.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 0ee528aea74af..8339ed9ec58f3 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -87,11 +87,16 @@ impl TextPipeline { load_font_to_fontdb(section, font_system, &mut self.map_handle_to_font_id, fonts); } - // Map text sections to cosmic-text spans + // 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() - .filter(|section| section.style.font_size > 0.0) .enumerate() + .filter(|(_section_index, section)| section.style.font_size > 0.0) .map(|(section_index, section)| { ( §ion.value[..], From 81c1535ba930ccf5c79e57b726000d5c32bd4ca7 Mon Sep 17 00:00:00 2001 From: Dimchikkk Date: Thu, 20 Jun 2024 12:49:39 +0100 Subject: [PATCH 36/47] round line_y --- crates/bevy_text/src/pipeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 8339ed9ec58f3..77c0e08e33c01 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -202,7 +202,7 @@ impl TextPipeline { // 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 + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; + let y = line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 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 { From f3c193852c9cc1707357fdc1bbf373a6e654e63a Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Thu, 20 Jun 2024 07:38:21 -0700 Subject: [PATCH 37/47] Fix merge artifact undoing #13717 --- .../src/dynamic_texture_atlas_builder.rs | 5 +---- crates/bevy_text/src/font_atlas.rs | 15 +++++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs index fe091be6d1580..879a9af3a497a 100644 --- a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs @@ -1,5 +1,4 @@ use crate::TextureAtlasLayout; -use bevy_asset::{Assets, Handle}; use bevy_math::{URect, UVec2}; use bevy_render::{ render_asset::{RenderAsset, RenderAssetUsages}, @@ -44,16 +43,14 @@ impl DynamicTextureAtlasBuilder { pub fn add_texture( &mut self, atlas_layout: &mut TextureAtlasLayout, - textures: &mut Assets, texture: &Image, - atlas_texture_handle: &Handle, + atlas_texture: &mut Image, ) -> Option { let allocation = self.atlas_allocator.allocate(size2( (texture.width() + self.padding).try_into().unwrap(), (texture.height() + self.padding).try_into().unwrap(), )); if let Some(allocation) = allocation { - let atlas_texture = textures.get_mut(atlas_texture_handle)?; assert!( ::asset_usage(atlas_texture) .contains(RenderAssetUsages::MAIN_WORLD), diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 7be8e5a9a0ca8..aa426c67e5550 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -85,19 +85,18 @@ impl FontAtlas { pub fn add_glyph( &mut self, textures: &mut Assets, - texture_atlases: &mut Assets, + atlas_layouts: &mut Assets, cache_key: cosmic_text::CacheKey, texture: &Image, offset: IVec2, ) -> Result<(), TextError> { - let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); + 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( - texture_atlas, - textures, - texture, - &self.texture, - ) { + 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 { From 89b912ec6fb77e8bf31d6fb21d8a1b648c1dfb72 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Sat, 22 Jun 2024 20:57:08 -0700 Subject: [PATCH 38/47] Cleanup resolved TODO comments --- crates/bevy_text/src/font.rs | 1 - crates/bevy_text/src/pipeline.rs | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index d53792cbf2cd6..2413ad52271bd 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -26,7 +26,6 @@ impl Font { pub fn try_from_bytes( font_data: Vec, ) -> Result { - // TODO: validate font, restore `try_from_bytes` use cosmic_text::ttf_parser; ttf_parser::Face::parse(&font_data, 0)?; Ok(Self { diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 77c0e08e33c01..fec5c3ecccb8c 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -215,10 +215,6 @@ impl TextPipeline { YAxisOrientation::BottomToTop => box_size.y - 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 position = Vec2::new(x, y); // TODO: recreate the byte index, that keeps track of where a cursor is, @@ -295,7 +291,6 @@ pub struct TextLayoutInfo { pub size: Vec2, } -// 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`]. @@ -355,13 +350,6 @@ fn load_font_to_fontdb( let family_name = face.families[0].0.to_owned(); (face_id, family_name) - - // 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(&[])); }); } @@ -379,7 +367,6 @@ fn get_attrs<'a>( .expect("Already loaded with load_font_to_fontdb"); let face = font_system.db().face(*face_id).unwrap(); - // TODO: validate this is the correct string to extract let attrs = Attrs::new() .metadata(section_index) .family(Family::Name(family_name)) @@ -393,7 +380,6 @@ fn get_attrs<'a>( /// 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/42 Request: Allow buffer dimensions to be undefined let width = buffer .layout_runs() .map(|run| run.line_w) From 435917eb96cee19f260dabb2e6578761fa4c4dea Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 23 Jun 2024 19:16:51 +0800 Subject: [PATCH 39/47] fix doc comment --- crates/bevy_text/src/font.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 2413ad52271bd..d24d89eea337d 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -22,7 +22,7 @@ pub struct Font { } impl Font { - /// Creates a [Font] from bytes, without any validation of the content + /// Creates a [`Font`] from bytes pub fn try_from_bytes( font_data: Vec, ) -> Result { From 3463d188984429b3423a7d4730b03ba2c6851a49 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 23 Jun 2024 20:14:44 +0800 Subject: [PATCH 40/47] Use cosmic-text text alignment --- crates/bevy_text/src/pipeline.rs | 24 ++++++++++++++---------- crates/bevy_text/src/text.rs | 15 +++++++++++++++ crates/bevy_ui/src/widget/text.rs | 9 +++++++-- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index fec5c3ecccb8c..160fbd4b1db88 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -57,6 +57,7 @@ impl TextPipeline { /// 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, @@ -65,6 +66,7 @@ impl TextPipeline { bounds: Vec2, scale_factor: f64, buffer: &mut CosmicBuffer, + alignment: JustifyText, ) -> Result<(), TextError> { let font_system = &mut self.font_system.0; @@ -125,6 +127,13 @@ impl TextPipeline { 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(()) } @@ -138,7 +147,6 @@ impl TextPipeline { fonts: &Assets, sections: &[TextSection], scale_factor: f64, - // TODO: Implement text alignment properly text_alignment: JustifyText, linebreak_behavior: BreakLineOn, bounds: Vec2, @@ -159,6 +167,7 @@ impl TextPipeline { bounds, scale_factor, buffer, + text_alignment, )?; let box_size = buffer_dimensions(buffer); @@ -170,9 +179,9 @@ impl TextPipeline { .flat_map(|run| { run.glyphs .iter() - .map(move |layout_glyph| (layout_glyph, run.line_w, run.line_y)) + .map(move |layout_glyph| (layout_glyph, run.line_y)) }) - .map(|(layout_glyph, line_w, line_y)| { + .map(|(layout_glyph, line_y)| { let section_index = layout_glyph.metadata; let font_handle = sections[section_index].style.font.clone_weak(); @@ -203,13 +212,6 @@ impl TextPipeline { // 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; - // 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 { - JustifyText::Left => 0.0, - JustifyText::Center => (box_size.x - line_w) / 2.0, - JustifyText::Right => box_size.x - line_w, - }; let y = match y_axis_orientation { YAxisOrientation::TopToBottom => y, YAxisOrientation::BottomToTop => box_size.y - y, @@ -242,6 +244,7 @@ impl TextPipeline { scale_factor: f64, linebreak_behavior: BreakLineOn, buffer: &mut CosmicBuffer, + text_alignment: JustifyText, ) -> Result { const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); @@ -252,6 +255,7 @@ impl TextPipeline { MIN_WIDTH_CONTENT_BOUNDS, scale_factor, buffer, + text_alignment, )?; let min_width_content_size = buffer_dimensions(buffer); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 2788f822e305d..f76b948014641 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -191,6 +191,21 @@ 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 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)] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index e65f9767a3bdb..e68383e13cb88 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -16,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, CosmicBuffer, Font, FontAtlasSets, Text, TextError, TextLayoutInfo, - TextMeasureInfo, TextPipeline, YAxisOrientation, + scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, TextError, + TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation, }; use bevy_utils::Entry; use taffy::style::AvailableSpace; @@ -83,6 +83,7 @@ impl Measure for TextMeasure { } } +#[allow(clippy::too_many_arguments)] #[inline] fn create_text_measure( fonts: &Assets, @@ -92,6 +93,7 @@ fn create_text_measure( mut content_size: Mut, mut text_flags: Mut, buffer: &mut CosmicBuffer, + text_alignment: JustifyText, ) { match text_pipeline.create_text_measure( fonts, @@ -99,6 +101,7 @@ fn create_text_measure( scale_factor, text.linebreak_behavior, buffer, + text_alignment, ) { Ok(measure) => { if text.linebreak_behavior == BreakLineOn::NoWrap { @@ -171,6 +174,7 @@ pub fn measure_text_system( || text_flags.needs_new_measure_func || content_size.is_added() { + let text_alignment = text.justify; create_text_measure( &fonts, scale_factor.into(), @@ -179,6 +183,7 @@ pub fn measure_text_system( content_size, text_flags, buffer.as_mut(), + text_alignment, ); } } From 792d9d09567a586de13cdbb2a474aa8f0557f0f8 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 23 Jun 2024 20:14:58 +0800 Subject: [PATCH 41/47] Update example --- examples/ui/text_debug.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index bab30176697e4..0e4fbe1baca75 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -135,6 +135,22 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { }), ); + builder.spawn( + TextBundle::from_section( + "This text is fully justified and is positioned in the same way.", + TextStyle { + font: font.clone(), + font_size: 35.0, + color: GREEN_YELLOW.into(), + }, + ) + .with_text_justify(JustifyText::Justified) + .with_style(Style { + max_width: Val::Px(300.), + ..default() + }), + ); + builder.spawn(( TextBundle::from_sections([ TextSection::new( From bbcff9002dfc2b00922970ca91d480e776ced468 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Sun, 23 Jun 2024 07:17:09 -0700 Subject: [PATCH 42/47] Add "word or character" wrapping --- crates/bevy_text/src/pipeline.rs | 1 + crates/bevy_text/src/text.rs | 2 ++ examples/ui/text_wrap_debug.rs | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 160fbd4b1db88..1975f12f5616b 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -121,6 +121,7 @@ impl TextPipeline { match linebreak_behavior { BreakLineOn::WordBoundary => Wrap::Word, BreakLineOn::AnyCharacter => Wrap::Glyph, + BreakLineOn::WordOrCharacter => Wrap::WordOrGlyph, BreakLineOn::NoWrap => Wrap::None, }, ); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index f76b948014641..39723a06ee413 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -254,6 +254,8 @@ 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, diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 724e1e0eb97ab..f5ae2ed2b3464 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -70,6 +70,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { for linebreak_behavior in [ BreakLineOn::AnyCharacter, BreakLineOn::WordBoundary, + BreakLineOn::WordOrCharacter, BreakLineOn::NoWrap, ] { let row_id = commands @@ -115,8 +116,9 @@ fn spawn(mut commands: Commands, asset_server: Res) { let messages = [ format!("JustifyContent::{justification:?}"), format!("LineBreakOn::{linebreak_behavior:?}"), - "Line 1\nLine 2\nLine 3".to_string(), + "Line 1\nLine 2".to_string(), "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor, nunc ac faucibus fringilla.".to_string(), + "pneumonoultramicroscopicsilicovolcanoconiosis".to_string() ]; for (j, message) in messages.into_iter().enumerate() { From 78ae9d6d3e910b73f49ba0597a1c7dda7bb3fafd Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 24 Jun 2024 16:38:01 +0800 Subject: [PATCH 43/47] use Text2dBounds as bounds instead of Vec2 --- crates/bevy_text/src/pipeline.rs | 14 +++--- crates/bevy_text/src/text2d.rs | 59 ++++++++++++++++++++++---- crates/bevy_ui/src/widget/text.rs | 14 +++--- examples/2d/text2d.rs | 12 ++---- examples/stress_tests/many_glyphs.rs | 4 +- examples/stress_tests/text_pipeline.rs | 2 +- 6 files changed, 70 insertions(+), 35 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 160fbd4b1db88..c3a97a26260ba 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -12,7 +12,7 @@ use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ error::TextError, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, PositionedGlyph, - TextSection, YAxisOrientation, + Text2dBounds, TextSection, YAxisOrientation, }; /// A wrapper around a [`cosmic_text::FontSystem`] @@ -63,7 +63,7 @@ impl TextPipeline { fonts: &Assets, sections: &[TextSection], linebreak_behavior: BreakLineOn, - bounds: Vec2, + bounds: Text2dBounds, scale_factor: f64, buffer: &mut CosmicBuffer, alignment: JustifyText, @@ -114,7 +114,7 @@ impl TextPipeline { .collect(); buffer.set_metrics(font_system, metrics); - buffer.set_size(font_system, Some(bounds.x.ceil()), None); + buffer.set_size(font_system, bounds.width, bounds.height); buffer.set_wrap( font_system, @@ -149,7 +149,7 @@ impl TextPipeline { scale_factor: f64, text_alignment: JustifyText, linebreak_behavior: BreakLineOn, - bounds: Vec2, + bounds: Text2dBounds, font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, @@ -246,7 +246,7 @@ impl TextPipeline { buffer: &mut CosmicBuffer, text_alignment: JustifyText, ) -> Result { - const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); + const MIN_WIDTH_CONTENT_BOUNDS: Text2dBounds = Text2dBounds::new_horizontal(0.0); self.update_buffer( fonts, @@ -321,11 +321,11 @@ impl TextMeasureInfo { /// Computes the size of the text area within the provided bounds. pub fn compute_size( &mut self, - bounds: Vec2, + bounds: Text2dBounds, font_system: &mut cosmic_text::FontSystem, ) -> Vec2 { self.buffer - .set_size(font_system, Some(bounds.x.ceil()), Some(bounds.y.ceil())); + .set_size(font_system, bounds.width, bounds.height); buffer_dimensions(&self.buffer) } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 9391dd459c78f..122cb39c0ba66 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -38,8 +38,12 @@ use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; #[derive(Component, Copy, Clone, Debug, Reflect)] #[reflect(Component)] pub struct Text2dBounds { - /// The maximum width and height of text in logical pixels. - pub size: Vec2, + /// 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 Text2dBounds { @@ -52,8 +56,43 @@ impl Default for Text2dBounds { impl Text2dBounds { /// Unbounded text will not be truncated or wrapped. pub const UNBOUNDED: Self = Self { - size: Vec2::splat(f32::INFINITY), + width: None, + height: None, }; + + /// Creates a new `Text2dBounds`, 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 `Text2dBounds`, 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 `Text2dBounds`, 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 Text2dBounds { + #[inline] + fn from(v: Vec2) -> Self { + Self::new(v.x, v.y) + } } /// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. @@ -203,14 +242,16 @@ pub fn update_text2d_layout( 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 = Text2dBounds { + 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, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index e68383e13cb88..6637a8068ba0f 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -16,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, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, TextError, - TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation, + scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, Text2dBounds, + TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation, }; use bevy_utils::Entry; use taffy::style::AvailableSpace; @@ -71,9 +71,9 @@ impl Measure for TextMeasure { height .map_or_else( || match available_width { - AvailableSpace::Definite(_) => { - self.info.compute_size(Vec2::new(x, f32::MAX), font_system) - } + AvailableSpace::Definite(_) => self + .info + .compute_size(Text2dBounds::new_horizontal(x), font_system), AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), }, @@ -210,10 +210,10 @@ fn queue_text( 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) + Text2dBounds::UNBOUNDED } else { // `scale_factor` is already multiplied by `UiScale` - Vec2::new( + Text2dBounds::new( node.unrounded_size.x * scale_factor, node.unrounded_size.y * scale_factor, ) diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index bb7325ac1f895..1cd48cc8a97f4 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -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: Text2dBounds::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: Text2dBounds::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/stress_tests/many_glyphs.rs b/examples/stress_tests/many_glyphs.rs index ca4bc6db28c53..35a958e551a96 100644 --- a/examples/stress_tests/many_glyphs.rs +++ b/examples/stress_tests/many_glyphs.rs @@ -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: Text2dBounds::new_horizontal(1000.), ..Default::default() }); } diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 716d867d0d4e5..e11eea8cb76c9 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -75,6 +75,6 @@ fn spawn(mut commands: Commands, asset_server: Res) { fn update_text_bounds(time: Res