diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index a2a5d594d0b79..00fa12ec85190 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -44,6 +44,8 @@ use bevy_camera::{ visibility::VisibilitySystems, }; use bevy_mesh::{Mesh, Mesh2d}; +#[cfg(feature = "bevy_text")] +use bevy_text::TextSystems; #[cfg(feature = "bevy_picking")] pub use picking_backend::*; pub use sprite::*; @@ -81,13 +83,12 @@ impl Plugin for SpritePlugin { PostUpdate, ( bevy_text::detect_text_needs_rerender::, - update_text2d_layout - .after(bevy_camera::CameraUpdateSystems) - .after(bevy_text::free_unused_font_atlases_system), + update_text2d_layout.after(bevy_camera::CameraUpdateSystems), calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) .chain() .in_set(bevy_text::Text2dUpdateSystems) + .after(TextSystems::RegisterFontAssets) .after(bevy_app::AnimationSystems), ); diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 572a9572218bf..f77d981822441 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -7,22 +7,24 @@ use bevy_camera::visibility::{ use bevy_camera::Camera; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::entity::EntityHashSet; + +use bevy_ecs::change_detection::DetectChanges; +use bevy_ecs::system::Res; use bevy_ecs::{ - change_detection::{DetectChanges, Ref}, + change_detection::Ref, component::Component, entity::Entity, prelude::ReflectComponent, query::{Changed, Without}, - system::{Commands, Local, Query, Res, ResMut}, + system::{Commands, Local, Query, ResMut}, }; use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet, LineBreak, LineHeight, SwashCache, - TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, - TextReader, TextRoot, TextSpanAccess, TextWriter, + update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, + LayoutCx, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, + TextLayoutInfo, TextPipeline, TextReader, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -88,7 +90,10 @@ use core::any::TypeId; Anchor, Visibility, VisibilityClass, - Transform + ComputedTextBlock, + TextLayoutInfo, + Transform, + ComputedTextLayout )] #[component(on_add = visibility::add_visibility_class::)] pub struct Text2d(pub String); @@ -100,7 +105,7 @@ impl Text2d { } } -impl TextRoot for Text2d {} +impl TextHead for Text2d {} impl TextSpanAccess for Text2d { fn read_span(&self) -> &str { @@ -160,14 +165,15 @@ impl Default for Text2dShadow { /// It does not modify or observe existing ones. pub fn update_text2d_layout( mut target_scale_factors: Local>, - // Text items which should be reprocessed again, generally when the font hasn't loaded yet. - mut queue: Local, + mut text_pipeline: ResMut, mut textures: ResMut>, fonts: Res>, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, - mut text_pipeline: ResMut, + mut font_cx: ResMut, + mut layout_cx: ResMut, + mut scale_cx: ResMut, mut text_query: Query<( Entity, Option<&RenderLayers>, @@ -175,10 +181,9 @@ pub fn update_text2d_layout( Ref, &mut TextLayoutInfo, &mut ComputedTextBlock, + &mut ComputedTextLayout, )>, mut text_reader: Text2dReader, - mut font_system: ResMut, - mut swash_cache: ResMut, ) { target_scale_factors.clear(); target_scale_factors.extend( @@ -197,8 +202,15 @@ pub fn update_text2d_layout( let mut previous_scale_factor = 0.; let mut previous_mask = &RenderLayers::none(); - for (entity, maybe_entity_mask, block, bounds, text_layout_info, mut computed) in - &mut text_query + for ( + entity, + maybe_entity_mask, + block, + bounds, + mut text_layout_info, + mut computed, + mut layout, + ) in &mut text_query { let entity_mask = maybe_entity_mask.unwrap_or_default(); @@ -219,55 +231,39 @@ pub fn update_text2d_layout( *scale_factor }; - if scale_factor != text_layout_info.scale_factor - || computed.needs_rerender() + if !(computed.needs_rerender() + || block.is_changed() || bounds.is_changed() - || (!queue.is_empty() && queue.remove(&entity)) + || scale_factor != text_layout_info.scale_factor) { - let text_bounds = TextBounds { - width: if block.linebreak == LineBreak::NoWrap { - None - } else { - bounds.width.map(|width| width * scale_factor) - }, - height: bounds.height.map(|height| height * scale_factor), - }; - - let text_layout_info = text_layout_info.into_inner(); - match text_pipeline.queue_text( - text_layout_info, - &fonts, - text_reader.iter(entity), - scale_factor as f64, - &block, - text_bounds, - &mut font_atlas_set, - &mut texture_atlases, - &mut textures, - computed.as_mut(), - &mut font_system, - &mut swash_cache, - ) { - Err(TextError::NoSuchFont) => { - // There was an error processing the text layout, let's add this entity to the - // queue for further processing - queue.insert(entity); - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.scale_factor = scale_factor; - text_layout_info.size *= scale_factor.recip(); - } - } + continue; } + + computed.needs_rerender = false; + computed.entities.clear(); + + text_pipeline.shape_text( + entity, + &mut text_reader, + &mut layout.0, + &mut font_cx.0, + &mut layout_cx.0, + scale_factor, + block.linebreak, + &fonts, + &mut computed.entities, + ); + + update_text_layout_info( + &mut text_layout_info, + &mut layout.0, + bounds.width.map(|w| w * scale_factor), + block.justify.into(), + &mut scale_cx, + &mut font_atlas_set, + &mut texture_atlases, + &mut textures, + ); } } @@ -307,135 +303,3 @@ pub fn calculate_bounds_text2d( } } } - -#[cfg(test)] -mod tests { - - use bevy_app::{App, Update}; - use bevy_asset::{load_internal_binary_asset, Handle}; - use bevy_camera::{ComputedCameraValues, RenderTargetInfo}; - use bevy_ecs::schedule::IntoScheduleConfigs; - use bevy_math::UVec2; - use bevy_text::{detect_text_needs_rerender, TextIterScratch}; - - use super::*; - - const FIRST_TEXT: &str = "Sample text."; - const SECOND_TEXT: &str = "Another, longer sample text."; - - fn setup() -> (App, Entity) { - let mut app = App::new(); - app.init_resource::>() - .init_resource::>() - .init_resource::>() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_systems( - Update, - ( - detect_text_needs_rerender::, - update_text2d_layout, - calculate_bounds_text2d, - ) - .chain(), - ); - - let mut visible_entities = VisibleEntities::default(); - visible_entities.push(Entity::PLACEHOLDER, TypeId::of::()); - - app.world_mut().spawn(( - Camera { - computed: ComputedCameraValues { - target_info: Some(RenderTargetInfo { - physical_size: UVec2::splat(1000), - scale_factor: 1., - }), - ..Default::default() - }, - ..Default::default() - }, - visible_entities, - )); - - // A font is needed to ensure the text is laid out with an actual size. - load_internal_binary_asset!( - app, - Handle::default(), - "../../bevy_text/src/FiraMono-subset.ttf", - |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } - ); - - let entity = app.world_mut().spawn(Text2d::new(FIRST_TEXT)).id(); - - (app, entity) - } - - #[test] - fn calculate_bounds_text2d_create_aabb() { - let (mut app, entity) = setup(); - - assert!(!app - .world() - .get_entity(entity) - .expect("Could not find entity") - .contains::()); - - // Creates the AABB after text layouting. - app.update(); - - let aabb = app - .world() - .get_entity(entity) - .expect("Could not find entity") - .get::() - .expect("Text should have an AABB"); - - // Text2D AABB does not have a depth. - assert_eq!(aabb.center.z, 0.0); - assert_eq!(aabb.half_extents.z, 0.0); - - // AABB has an actual size. - assert!(aabb.half_extents.x > 0.0 && aabb.half_extents.y > 0.0); - } - - #[test] - fn calculate_bounds_text2d_update_aabb() { - let (mut app, entity) = setup(); - - // Creates the initial AABB after text layouting. - app.update(); - - let first_aabb = *app - .world() - .get_entity(entity) - .expect("Could not find entity") - .get::() - .expect("Could not find initial AABB"); - - let mut entity_ref = app - .world_mut() - .get_entity_mut(entity) - .expect("Could not find entity"); - *entity_ref - .get_mut::() - .expect("Missing Text2d on entity") = Text2d::new(SECOND_TEXT); - - // Recomputes the AABB. - app.update(); - - let second_aabb = *app - .world() - .get_entity(entity) - .expect("Could not find entity") - .get::() - .expect("Could not find second AABB"); - - // Check that the height is the same, but the width is greater. - approx::assert_abs_diff_eq!(first_aabb.half_extents.y, second_aabb.half_extents.y); - assert!(FIRST_TEXT.len() < SECOND_TEXT.len()); - assert!(first_aabb.half_extents.x < second_aabb.half_extents.x); - } -} diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index bc9b134f280a2..5f264f2a67384 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -30,12 +30,13 @@ bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev", default-fea # other wgpu-types = { version = "26", default-features = false } -cosmic-text = { version = "0.15", features = ["shape-run-cache"] } thiserror = { version = "2", default-features = false } serde = { version = "1", features = ["derive"] } smallvec = { version = "1", default-features = false } sys-locale = "0.3.0" tracing = { version = "0.1", default-features = false, features = ["std"] } +parley = { version = "0.7.0" } +swash = { version = "0.2.6" } [lints] workspace = true diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs new file mode 100644 index 0000000000000..3016bee4dc759 --- /dev/null +++ b/crates/bevy_text/src/context.rs @@ -0,0 +1,20 @@ +use bevy_derive::Deref; +use bevy_derive::DerefMut; +use bevy_ecs::resource::Resource; +use parley::FontContext; +use parley::LayoutContext; +use swash::scale::ScaleContext; + +use crate::FontSmoothing; + +/// Font context +#[derive(Resource, Default, Deref, DerefMut)] +pub struct FontCx(pub FontContext); + +/// Text layout context +#[derive(Resource, Default, Deref, DerefMut)] +pub struct LayoutCx(pub LayoutContext<(u32, FontSmoothing)>); + +/// Text scaler context +#[derive(Resource, Default, Deref, DerefMut)] +pub struct ScaleCx(pub ScaleContext); diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 9835756877151..e4f8e83aaba05 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -1,4 +1,3 @@ -use cosmic_text::CacheKey; use thiserror::Error; #[derive(Debug, PartialEq, Eq, Error)] @@ -13,7 +12,7 @@ pub enum TextError { FailedToAddGlyph(u16), /// Failed to get scaled glyph image for cache key #[error("failed to get scaled glyph image for cache key: {0:?}")] - FailedToGetGlyphImage(CacheKey), + FailedToGetGlyphImage(u16), /// Missing texture atlas layout for the font #[error("missing texture atlas layout for the font")] MissingAtlasLayout, diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 8830f7158baef..5bcf5d727e82e 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,9 +1,16 @@ -use alloc::sync::Arc; - +use crate::context::FontCx; +use crate::TextFont; use bevy_asset::Asset; +use bevy_asset::AssetEvent; +use bevy_asset::Assets; +use bevy_ecs::change_detection::DetectChangesMut; +use bevy_ecs::message::MessageReader; +use bevy_ecs::system::Query; +use bevy_ecs::system::ResMut; use bevy_reflect::TypePath; -use cosmic_text::skrifa::raw::ReadError; -use cosmic_text::skrifa::FontRef; +use bevy_utils::default; +use parley::fontique::Blob; +use parley::fontique::FontInfoOverride; /// An [`Asset`] that contains the data for a loaded font, if loaded as an asset. /// @@ -19,16 +26,48 @@ use cosmic_text::skrifa::FontRef; /// 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 - pub data: Arc>, + /// raw font data + pub blob: Blob, + /// font family name + pub family_name: String, } impl Font { /// Creates a [`Font`] from bytes - pub fn try_from_bytes(font_data: Vec) -> Result { - let _ = FontRef::from_index(&font_data, 0)?; - Ok(Self { - data: Arc::new(font_data), - }) + pub fn try_from_bytes(font_data: Vec, family_name: String) -> Font { + Font { + blob: Blob::from(font_data), + family_name, + } + } +} + +/// Register new font assets with Parley's FontContext. +pub fn register_font_assets_system( + mut cx: ResMut, + mut fonts: ResMut>, + mut events: MessageReader>, + mut text_font_query: Query<&mut TextFont>, +) { + for event in events.read() { + match event { + AssetEvent::Added { id } => { + if let Some(font) = fonts.get_mut(*id) { + cx.collection.register_fonts( + font.blob.clone(), + Some(FontInfoOverride { + family_name: Some(font.family_name.as_str()), + ..default() + }), + ); + for mut font in text_font_query.iter_mut() { + if font.font.id() == *id { + font.set_changed(); + } + } + } + } + _ => {} + } } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 1e2717106b224..b07689cf9ffcf 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -2,10 +2,18 @@ use bevy_asset::{Assets, Handle, RenderAssetUsages}; use bevy_image::{prelude::*, ImageSampler, ToExtents}; use bevy_math::{IVec2, UVec2}; use bevy_platform::collections::HashMap; +use swash::scale::Scaler; use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError}; +/// Key identifying a glyph +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct GlyphCacheKey { + /// glyph id + pub glyph_id: u16, +} + /// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`. /// /// A `FontAtlas` contains one or more textures, each of which contains one or more glyphs packed into them. @@ -22,7 +30,7 @@ pub struct FontAtlas { /// Used to update the [`TextureAtlasLayout`]. pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, /// A mapping between subpixel-offset glyphs and their [`GlyphAtlasLocation`]. - pub glyph_to_atlas_index: HashMap, + 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 @@ -59,12 +67,12 @@ impl FontAtlas { } /// Get the [`GlyphAtlasLocation`] for a subpixel-offset glyph. - pub fn get_glyph_index(&self, cache_key: cosmic_text::CacheKey) -> Option { + pub fn get_glyph_index(&self, cache_key: GlyphCacheKey) -> 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 { + pub fn has_glyph(&self, cache_key: GlyphCacheKey) -> bool { self.glyph_to_atlas_index.contains_key(&cache_key) } @@ -83,7 +91,7 @@ impl FontAtlas { &mut self, textures: &mut Assets, atlas_layouts: &mut Assets, - cache_key: cosmic_text::CacheKey, + key: GlyphCacheKey, texture: &Image, offset: IVec2, ) -> Result<(), TextError> { @@ -99,7 +107,7 @@ impl FontAtlas { .add_texture(atlas_layout, texture, atlas_texture) { self.glyph_to_atlas_index.insert( - cache_key, + key, GlyphAtlasLocation { glyph_index, offset, @@ -107,7 +115,7 @@ impl FontAtlas { ); Ok(()) } else { - Err(TextError::FailedToAddGlyph(cache_key.glyph_id)) + Err(TextError::FailedToAddGlyph(key.glyph_id)) } } } @@ -128,20 +136,16 @@ pub fn add_glyph_to_atlas( font_atlases: &mut Vec, texture_atlases: &mut Assets, textures: &mut Assets, - font_system: &mut cosmic_text::FontSystem, - swash_cache: &mut cosmic_text::SwashCache, - layout_glyph: &cosmic_text::LayoutGlyph, + scaler: &mut Scaler, font_smoothing: FontSmoothing, + glyph_id: u16, ) -> Result { - let physical_glyph = layout_glyph.physical((0., 0.), 1.0); - - let (glyph_texture, offset) = - get_outlined_glyph_texture(font_system, swash_cache, &physical_glyph, font_smoothing)?; + let (glyph_texture, offset) = get_outlined_glyph_texture(scaler, glyph_id, font_smoothing)?; let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> { atlas.add_glyph( textures, texture_atlases, - physical_glyph.cache_key, + GlyphCacheKey { glyph_id }, &glyph_texture, offset, ) @@ -169,7 +173,7 @@ pub fn add_glyph_to_atlas( new_atlas.add_glyph( textures, texture_atlases, - physical_glyph.cache_key, + GlyphCacheKey { glyph_id }, &glyph_texture, offset, )?; @@ -177,59 +181,52 @@ pub fn add_glyph_to_atlas( font_atlases.push(new_atlas); } - get_glyph_atlas_info(font_atlases, physical_glyph.cache_key) + get_glyph_atlas_info(font_atlases, GlyphCacheKey { glyph_id }) .ok_or(TextError::InconsistentAtlasState) } /// Get the texture of the glyph as a rendered image, and its offset pub fn get_outlined_glyph_texture( - font_system: &mut cosmic_text::FontSystem, - swash_cache: &mut cosmic_text::SwashCache, - physical_glyph: &cosmic_text::PhysicalGlyph, + scaler: &mut Scaler, + glyph_id: u16, font_smoothing: FontSmoothing, ) -> Result<(Image, IVec2), TextError> { - // NOTE: Ideally, we'd ask COSMIC Text to honor the font smoothing setting directly. - // However, since it currently doesn't support that, we render the glyph with antialiasing - // and apply a threshold to the alpha channel to simulate the effect. - // - // This has the side effect of making regular vector fonts look quite ugly when font smoothing - // is turned off, but for fonts that are specifically designed for pixel art, it works well. - // - // See: https://github.com/pop-os/cosmic-text/issues/279 - let image = swash_cache - .get_image_uncached(font_system, physical_glyph.cache_key) - .ok_or(TextError::FailedToGetGlyphImage(physical_glyph.cache_key))?; + let image = swash::scale::Render::new(&[ + swash::scale::Source::ColorOutline(0), + swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit), + swash::scale::Source::Outline, + ]) + .format(swash::zeno::Format::Alpha) + .render(scaler, glyph_id) + .ok_or(TextError::FailedToGetGlyphImage(glyph_id))?; - let cosmic_text::Placement { - left, - top, - width, - height, - } = image.placement; + let left = image.placement.left; + let top = image.placement.top; + let width = image.placement.width; + let height = image.placement.height; - let data = match image.content { - cosmic_text::SwashContent::Mask => { - if font_smoothing == FontSmoothing::None { - image - .data - .iter() - // Apply a 50% threshold to the alpha channel - .flat_map(|a| [255, 255, 255, if *a > 127 { 255 } else { 0 }]) - .collect() - } else { - image - .data - .iter() - .flat_map(|a| [255, 255, 255, *a]) - .collect() + let px = (width * height) as usize; + let mut rgba = vec![0u8; px * 4]; + match font_smoothing { + FontSmoothing::AntiAliased => { + for i in 0..px { + let a = image.data[i]; + rgba[i * 4 + 0] = 255; // R + rgba[i * 4 + 1] = 255; // G + rgba[i * 4 + 2] = 255; // B + rgba[i * 4 + 3] = a; // A from swash } } - cosmic_text::SwashContent::Color => image.data, - cosmic_text::SwashContent::SubpixelMask => { - // TODO: implement - todo!() + FontSmoothing::None => { + for i in 0..px { + let a = image.data[i]; + rgba[i * 4 + 0] = 255; // R + rgba[i * 4 + 1] = 255; // G + rgba[i * 4 + 2] = 255; // B + rgba[i * 4 + 3] = if 127 < a { 255 } else { 0 }; // A from swash + } } - }; + } Ok(( Image::new( @@ -239,7 +236,7 @@ pub fn get_outlined_glyph_texture( depth_or_array_layers: 1, }, TextureDimension::D2, - data, + rgba, TextureFormat::Rgba8UnormSrgb, RenderAssetUsages::MAIN_WORLD, ), @@ -250,7 +247,7 @@ pub fn get_outlined_glyph_texture( /// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph. pub fn get_glyph_atlas_info( font_atlases: &mut [FontAtlas], - cache_key: cosmic_text::CacheKey, + cache_key: GlyphCacheKey, ) -> Option { font_atlases.iter().find_map(|atlas| { atlas diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 76d7bfaa460b2..b03ceed35c6ef 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,21 +1,23 @@ -use crate::{Font, FontAtlas, FontSmoothing, TextFont}; -use bevy_asset::{AssetEvent, AssetId}; +use crate::{FontAtlas, FontSmoothing, GlyphCacheKey}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{message::MessageReader, resource::Resource, system::ResMut}; +use bevy_ecs::resource::Resource; use bevy_platform::collections::HashMap; +use parley::FontData; /// Identifies the font atlases for a particular font in [`FontAtlasSet`] /// /// Allows an `f32` font size to be used as a key in a `HashMap`, by its binary representation. #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct FontAtlasKey(pub AssetId, pub u32, pub FontSmoothing); +pub struct FontAtlasKey(pub u64, pub u32, pub u32, pub FontSmoothing); -impl From<&TextFont> for FontAtlasKey { - fn from(font: &TextFont) -> Self { - FontAtlasKey( - font.font.id(), - font.font_size.to_bits(), - font.font_smoothing, +impl FontAtlasKey { + /// new key + pub fn new(font_data: &FontData, size: f32, smoothing: FontSmoothing) -> Self { + Self( + font_data.data.id(), + font_data.index, + size.to_bits(), + smoothing, ) } } @@ -26,20 +28,8 @@ pub struct FontAtlasSet(HashMap>); impl FontAtlasSet { /// Checks whether the given subpixel-offset glyph is contained in any of the [`FontAtlas`]es for the font identified by the given [`FontAtlasKey`]. - pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_key: &FontAtlasKey) -> bool { + pub fn has_glyph(&self, cache_key: GlyphCacheKey, font_key: &FontAtlasKey) -> bool { self.get(font_key) .is_some_and(|font_atlas| font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key))) } } - -/// A system that automatically frees unused texture atlases when a font asset is removed. -pub fn free_unused_font_atlases_system( - mut font_atlas_sets: ResMut, - mut font_events: MessageReader>, -) { - for event in font_events.read() { - if let AssetEvent::Removed { id } = event { - font_atlas_sets.retain(|key, _| key.0 != *id); - } - } -} diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index 77b38082f2dfc..8808cdd8b5f82 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,6 +1,5 @@ use crate::Font; use bevy_asset::{io::Reader, AssetLoader, LoadContext}; -use cosmic_text::skrifa::raw::ReadError; use thiserror::Error; #[derive(Default)] @@ -12,8 +11,8 @@ pub struct FontLoader; #[derive(Debug, Error)] pub enum FontLoaderError { /// The contents that could not be parsed - #[error(transparent)] - Content(#[from] ReadError), + #[error("Failed to parse font.")] + Content, /// An [IO](std::io) Error #[error(transparent)] Io(#[from] std::io::Error), @@ -27,11 +26,12 @@ impl AssetLoader for FontLoader { &self, reader: &mut dyn Reader, _settings: &(), - _load_context: &mut LoadContext<'_>, + load_context: &mut LoadContext<'_>, ) -> Result { + let path = load_context.path().to_string(); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - let font = Font::try_from_bytes(bytes)?; + let font = Font::try_from_bytes(bytes, path); Ok(font) } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 2beb5b1897034..f70c080dba04b 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -32,6 +32,7 @@ extern crate alloc; mod bounds; +mod context; mod error; mod font; mod font_atlas; @@ -43,6 +44,7 @@ mod text; mod text_access; pub use bounds::*; +pub use context::*; pub use error::*; pub use font::*; pub use font_atlas::*; @@ -83,26 +85,40 @@ pub struct TextPlugin; #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub struct Text2dUpdateSystems; +/// Deprecated alias for [`Text2dUpdateSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Text2dUpdateSystems`.")] +pub type Update2dText = Text2dUpdateSystems; + +/// Text Systems set +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub enum TextSystems { + /// Register new font assets with Parley's `FontContext` after loading + RegisterFontAssets, +} + impl Plugin for TextPlugin { fn build(&self, app: &mut App) { app.init_asset::() .init_asset_loader::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() .add_systems( PostUpdate, - free_unused_font_atlases_system.before(AssetEventSystems), - ) - .add_systems(Last, trim_cosmic_cache); + register_font_assets_system + .in_set(TextSystems::RegisterFontAssets) + .after(AssetEventSystems), + ); #[cfg(feature = "default_font")] { use bevy_asset::{AssetId, Assets}; let mut assets = app.world_mut().resource_mut::>(); - let asset = Font::try_from_bytes(DEFAULT_FONT_DATA.to_vec()).unwrap(); + let asset = + Font::try_from_bytes(DEFAULT_FONT_DATA.to_vec(), "bevy default font".to_string()); assets.insert(AssetId::default(), asset).unwrap(); }; } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index cd40cdab2a011..0aa96ee510bd4 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,698 +1,233 @@ -use alloc::sync::Arc; - -use bevy_asset::{AssetId, Assets}; -use bevy_color::Color; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{ - component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource, - system::ResMut, -}; -use bevy_image::prelude::*; -use bevy_log::{once, warn}; -use bevy_math::{Rect, UVec2, Vec2}; -use bevy_platform::collections::HashMap; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; - -use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; - -use crate::{ - add_glyph_to_atlas, error::TextError, get_glyph_atlas_info, ComputedTextBlock, Font, - FontAtlasKey, FontAtlasSet, FontSmoothing, Justify, LineBreak, LineHeight, PositionedGlyph, - TextBounds, TextEntity, TextFont, TextLayout, -}; - -/// A wrapper resource around a [`cosmic_text::FontSystem`] -/// -/// The font system is used to retrieve fonts and their information, including glyph outlines. -/// -/// This resource is updated by the [`TextPipeline`] resource. -#[derive(Resource, Deref, DerefMut)] -pub struct CosmicFontSystem(pub cosmic_text::FontSystem); - -impl Default for CosmicFontSystem { - fn default() -> Self { - let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); - let db = cosmic_text::fontdb::Database::new(); - // TODO: consider using `cosmic_text::FontSystem::new()` (load system fonts by default) - Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db)) - } -} - -/// A wrapper resource around a [`cosmic_text::SwashCache`] -/// -/// The swash cache rasterizer is used to rasterize glyphs -/// -/// This resource is updated by the [`TextPipeline`] resource. -#[derive(Resource)] -pub struct SwashCache(pub cosmic_text::SwashCache); - -impl Default for SwashCache { - fn default() -> Self { - Self(cosmic_text::SwashCache::new()) - } -} - -/// Information about a font collected as part of preparing for text layout. -#[derive(Clone)] -pub struct FontFaceInfo { - /// Width class: - pub stretch: cosmic_text::fontdb::Stretch, - /// Allows italic or oblique faces to be selected - pub style: cosmic_text::fontdb::Style, - /// The degree of blackness or stroke thickness - pub weight: cosmic_text::fontdb::Weight, - /// Font family name - pub family_name: Arc, -} +use crate::add_glyph_to_atlas; +use crate::get_glyph_atlas_info; +use crate::Font; +use crate::FontAtlasKey; +use crate::FontAtlasSet; +use crate::FontSmoothing; +use crate::GlyphCacheKey; + +use crate::RunGeometry; +use crate::TextEntity; +use crate::TextFont; +use crate::TextHead; +use crate::TextLayoutInfo; +use crate::TextReader; +use bevy_asset::Assets; +use bevy_ecs::entity::Entity; +use bevy_ecs::resource::Resource; +use bevy_image::Image; +use bevy_image::TextureAtlasLayout; +use bevy_math::Rect; +use bevy_math::UVec2; +use bevy_math::Vec2; +use parley::swash::FontRef; +use parley::Alignment; +use parley::AlignmentOptions; +use parley::FontContext; +use parley::FontSettings; +use parley::FontStack; +use parley::Layout; +use parley::LayoutContext; +use parley::LineHeight; +use parley::PositionedLayoutItem; +use parley::StyleProperty; +use parley::WordBreakStrength; +use smallvec::SmallVec; +use std::usize; +use swash::scale::ScaleContext; /// The `TextPipeline` is used to layout and render text blocks (see `Text`/`Text2d`). /// /// 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). - pub map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, Arc)>, /// Buffered vec for collecting spans. /// /// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10). - spans_buffer: Vec<( - usize, - &'static str, - &'static TextFont, - FontFaceInfo, - LineHeight, - )>, - /// Buffered vec for collecting info for glyph assembly. - glyph_info: Vec<(AssetId, FontSmoothing, f32, f32, f32, f32)>, + spans_buffer: Vec<(&'static str, &'static TextFont, LineHeight)>, } impl TextPipeline { - /// Utilizes [`cosmic_text::Buffer`] to shape and layout text - /// - /// Negative or 0.0 font sizes will not be laid out. - pub fn update_buffer<'a>( + /// Create layout given text sections and styles + pub fn shape_text<'a, T: TextHead>( &mut self, + text_root_entity: Entity, + reader: &mut TextReader, + layout: &mut Layout<(u32, FontSmoothing)>, + font_cx: &'a mut FontContext, + layout_cx: &'a mut LayoutContext<(u32, FontSmoothing)>, + scale_factor: f32, + line_break: crate::text::LineBreak, fonts: &Assets, - text_spans: impl Iterator, - linebreak: LineBreak, - justify: Justify, - bounds: TextBounds, - scale_factor: f64, - computed: &mut ComputedTextBlock, - font_system: &mut CosmicFontSystem, - ) -> Result<(), TextError> { - let font_system = &mut font_system.0; + entities: &mut SmallVec<[TextEntity; 1]>, + ) { + entities.clear(); - // Collect span information into a vec. This is necessary because font loading requires mut access - // to FontSystem, which the cosmic-text Buffer also needs. - let mut max_font_size: f32 = 0.; - let mut max_line_height: f32 = 0.0; - let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color, LineHeight)> = - core::mem::take(&mut self.spans_buffer) - .into_iter() - .map( - |_| -> (usize, &str, &TextFont, FontFaceInfo, Color, LineHeight) { - unreachable!() - }, - ) - .collect(); - - computed.entities.clear(); + let mut spans: Vec<(&str, &TextFont, LineHeight)> = core::mem::take(&mut self.spans_buffer) + .into_iter() + .map(|_| -> (&str, &TextFont, LineHeight) { unreachable!() }) + .collect(); - for (span_index, (entity, depth, span, text_font, color, line_height)) in - text_spans.enumerate() + let mut text_len = 0; + for (entity, depth, text_section, text_font, _, line_height) in + reader.iter(text_root_entity) { - // Save this span entity in the computed text block. - computed.entities.push(TextEntity { entity, depth }); - - if span.is_empty() { - continue; - } - // Return early if a font is not loaded yet. - if !fonts.contains(text_font.font.id()) { - spans.clear(); - self.spans_buffer = spans - .into_iter() - .map( - |_| -> ( - usize, - &'static str, - &'static TextFont, - FontFaceInfo, - LineHeight, - ) { unreachable!() }, - ) - .collect(); - - return Err(TextError::NoSuchFont); - } - - // Get max font size for use in cosmic Metrics. - max_font_size = max_font_size.max(text_font.font_size); - max_line_height = max_line_height.max(line_height.eval(text_font.font_size)); - - // Load Bevy fonts into cosmic-text's font system. - let face_info = load_font_to_fontdb( - text_font, - font_system, - &mut self.map_handle_to_font_id, - fonts, - ); - - // Save spans that aren't zero-sized. - if scale_factor <= 0.0 || text_font.font_size <= 0.0 { - once!(warn!( - "Text span {entity} has a font size <= 0.0. Nothing will be displayed.", - )); - - continue; - } - spans.push((span_index, span, text_font, face_info, color, line_height)); + entities.push(TextEntity { entity, depth }); + text_len += text_section.len(); + spans.push((text_section, text_font, line_height.eval())); } - let mut metrics = Metrics::new(max_font_size, max_line_height).scale(scale_factor as f32); - // Metrics of 0.0 cause `Buffer::set_metrics` to panic. We hack around this by 'falling - // through' to call `Buffer::set_rich_text` with zero spans so any cached text will be cleared without - // deallocating the buffer. - metrics.font_size = metrics.font_size.max(0.000001); - metrics.line_height = metrics.line_height.max(0.000001); - - // 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_iter = spans.iter().map( - |(span_index, span, text_font, font_info, color, line_height)| { - ( - *span, - get_attrs( - *span_index, - text_font, - *line_height, - *color, - font_info, - scale_factor, - ), - ) - }, - ); - - // Update the buffer. - let buffer = &mut computed.buffer; - buffer.set_metrics_and_size(font_system, metrics, bounds.width, bounds.height); - - buffer.set_wrap( - font_system, - match linebreak { - LineBreak::WordBoundary => Wrap::Word, - LineBreak::AnyCharacter => Wrap::Glyph, - LineBreak::WordOrCharacter => Wrap::WordOrGlyph, - LineBreak::NoWrap => Wrap::None, - }, - ); - - buffer.set_rich_text( - font_system, - spans_iter, - &Attrs::new(), - Shaping::Advanced, - Some(justify.into()), - ); + let mut text = String::with_capacity(text_len); + for (text_section, ..) in &spans { + text.push_str(text_section); + } - buffer.shape_until_scroll(font_system, false); + let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); + if let Some(word_break_strength) = match line_break { + crate::LineBreak::WordBoundary => Some(WordBreakStrength::Normal), + crate::LineBreak::AnyCharacter => Some(WordBreakStrength::BreakAll), + crate::LineBreak::WordOrCharacter => Some(WordBreakStrength::KeepAll), + _ => None, + } { + builder.push_default(StyleProperty::WordBreak(word_break_strength)); + }; - // Workaround for alignment not working for unbounded text. - // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != Justify::Left { - let dimensions = buffer_dimensions(buffer); - // `set_size` causes a re-layout to occur. - buffer.set_size(font_system, Some(dimensions.x), bounds.height); + let mut start = 0; + for (index, (text_section, text_font, line_height)) in spans.drain(..).enumerate() { + let end = start + text_section.len(); + let range = start..end; + start = end; + if let Some(family) = fonts + .get(text_font.font.id()) + .map(|font| font.family_name.as_str()) + { + builder.push(FontStack::from(family), range.clone()); + }; + builder.push( + StyleProperty::Brush((index as u32, text_font.font_smoothing)), + range.clone(), + ); + builder.push(StyleProperty::FontSize(text_font.font_size), range.clone()); + builder.push(line_height, range.clone()); + builder.push( + StyleProperty::FontFeatures(FontSettings::from(text_font.font_features.as_slice())), + range, + ); } + builder.build_into(layout, &text); // Recover the spans buffer. - spans.clear(); self.spans_buffer = spans .into_iter() - .map( - |_| -> ( - usize, - &'static str, - &'static TextFont, - FontFaceInfo, - LineHeight, - ) { unreachable!() }, - ) + .map(|_| -> (&'static str, &'static TextFont, LineHeight) { unreachable!() }) .collect(); - - Ok(()) } +} - /// Queues text for rendering - /// - /// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s - /// which contain information for rendering the text. - pub fn queue_text<'a>( - &mut self, - layout_info: &mut TextLayoutInfo, - fonts: &Assets, - text_spans: impl Iterator, - scale_factor: f64, - layout: &TextLayout, - bounds: TextBounds, - font_atlas_set: &mut FontAtlasSet, - texture_atlases: &mut Assets, - textures: &mut Assets, - computed: &mut ComputedTextBlock, - font_system: &mut CosmicFontSystem, - swash_cache: &mut SwashCache, - ) -> Result<(), TextError> { - layout_info.glyphs.clear(); - layout_info.run_geometry.clear(); - layout_info.size = Default::default(); - - // Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries. - computed.needs_rerender = false; - - // Extract font ids from the iterator while traversing it. - let mut glyph_info = core::mem::take(&mut self.glyph_info); - glyph_info.clear(); - let text_spans = text_spans.inspect(|(_, _, _, text_font, _, _)| { - glyph_info.push(( - text_font.font.id(), - text_font.font_smoothing, - text_font.font_size, - 0., - 0., - 0., - )); - }); - - let update_result = self.update_buffer( - fonts, - text_spans, - layout.linebreak, - layout.justify, - bounds, - scale_factor, - computed, - font_system, - ); - - self.glyph_info = glyph_info; - - update_result?; - - for (font, _, size, strikethrough_offset, stroke, underline_offset) in - self.glyph_info.iter_mut() - { - let Some((id, _)) = self.map_handle_to_font_id.get(font) else { - continue; - }; - let weight = font_system - .db() - .face(*id) - .map(|f| f.weight) - .unwrap_or(cosmic_text::Weight::NORMAL); - if let Some(font) = font_system.get_font(*id, weight) { - let swash = font.as_swash(); - let metrics = swash.metrics(&[]); - let upem = metrics.units_per_em as f32; - let scalar = *size * scale_factor as f32 / upem; - *strikethrough_offset = (metrics.strikeout_offset * scalar).round(); - *stroke = (metrics.stroke_size * scalar).round().max(1.); - *underline_offset = (metrics.underline_offset * scalar).round(); - } - } - - let buffer = &mut computed.buffer; - let box_size = buffer_dimensions(buffer); - - let result = buffer.layout_runs().try_for_each(|run| { - let mut current_section: Option = None; - let mut start = 0.; - let mut end = 0.; - let result = run - .glyphs - .iter() - .map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i)) - .try_for_each(|(layout_glyph, line_y, line_i)| { - match current_section { - Some(section) => { - if section != layout_glyph.metadata { - layout_info.run_geometry.push(RunGeometry { - span_index: section, - bounds: Rect::new( - start, - run.line_top, - end, - run.line_top + run.line_height, - ), - strikethrough_y: (run.line_y - self.glyph_info[section].3) - .round(), - strikethrough_thickness: self.glyph_info[section].4, - underline_y: (run.line_y - self.glyph_info[section].5).round(), - underline_thickness: self.glyph_info[section].4, - }); - start = end.max(layout_glyph.x); - current_section = Some(layout_glyph.metadata); - } - end = layout_glyph.x + layout_glyph.w; - } - None => { - current_section = Some(layout_glyph.metadata); - start = layout_glyph.x; - end = start + layout_glyph.w; - } - } - - let mut temp_glyph; - let span_index = layout_glyph.metadata; - let font_id = self.glyph_info[span_index].0; - let font_smoothing = self.glyph_info[span_index].1; - - let layout_glyph = if font_smoothing == FontSmoothing::None { - // If font smoothing is disabled, round the glyph positions and sizes, - // effectively discarding all subpixel layout. - temp_glyph = layout_glyph.clone(); - temp_glyph.x = temp_glyph.x.round(); - temp_glyph.y = temp_glyph.y.round(); - temp_glyph.w = temp_glyph.w.round(); - temp_glyph.x_offset = temp_glyph.x_offset.round(); - temp_glyph.y_offset = temp_glyph.y_offset.round(); - temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round); - - &temp_glyph - } else { - layout_glyph - }; - - let physical_glyph = layout_glyph.physical((0., 0.), 1.); - - let font_atlases = font_atlas_set - .entry(FontAtlasKey( - font_id, - physical_glyph.cache_key.font_size_bits, - font_smoothing, - )) - .or_default(); - - let atlas_info = get_glyph_atlas_info(font_atlases, physical_glyph.cache_key) +/// create a TextLayoutInfo +pub fn update_text_layout_info( + info: &mut TextLayoutInfo, + layout: &mut Layout<(u32, FontSmoothing)>, + max_advance: Option, + alignment: Alignment, + scale_cx: &mut ScaleContext, + font_atlas_set: &mut FontAtlasSet, + texture_atlases: &mut Assets, + textures: &mut Assets, +) { + info.clear(); + + layout.break_all_lines(max_advance); + layout.align(None, alignment, AlignmentOptions::default()); + + info.scale_factor = layout.scale(); + info.size = ( + layout.width() / layout.scale(), + layout.height() / layout.scale(), + ) + .into(); + + for line in layout.lines() { + for (line_index, item) in line.items().enumerate() { + match item { + PositionedLayoutItem::GlyphRun(glyph_run) => { + let span_index = glyph_run.style().brush.0; + + let run = glyph_run.run(); + + let font = run.font(); + let font_size = run.font_size(); + let coords = run.normalized_coords(); + + let font_atlas_key = + FontAtlasKey::new(&font, font_size, glyph_run.style().brush.1); + + for glyph in glyph_run.positioned_glyphs() { + let font_atlases = font_atlas_set.entry(font_atlas_key).or_default(); + let Ok(atlas_info) = get_glyph_atlas_info( + font_atlases, + GlyphCacheKey { + glyph_id: glyph.id as u16, + }, + ) .map(Ok) .unwrap_or_else(|| { + let font_ref = + FontRef::from_index(font.data.as_ref(), font.index as usize) + .unwrap(); + let mut scaler = scale_cx + .builder(font_ref) + .size(font_size) + .hint(true) + .normalized_coords(coords) + .build(); add_glyph_to_atlas( font_atlases, texture_atlases, textures, - &mut font_system.0, - &mut swash_cache.0, - layout_glyph, - font_smoothing, + &mut scaler, + glyph_run.style().brush.1, + glyph.id as u16, ) - })?; - - let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap(); - let location = atlas_info.location; - let glyph_rect = texture_atlas.textures[location.glyph_index]; - let left = location.offset.x as f32; - let top = location.offset.y as f32; - let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height()); - - // offset by half the size because the origin is center - let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; - let y = - line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; - - let position = Vec2::new(x, y); + }) else { + continue; + }; + + 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 glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height()); + let x = glyph_size.x as f32 / 2. + glyph.x + location.offset.x as f32; + let y = glyph_size.y as f32 / 2. + glyph.y - location.offset.y as f32; + + info.glyphs.push(crate::PositionedGlyph { + position: (x, y).into(), + size: glyph_size.as_vec2(), + atlas_info, + span_index: span_index as usize, + line_index, + byte_index: line.text_range().start, + byte_length: line.text_range().len(), + }); + } - let pos_glyph = PositionedGlyph { - position, - size: glyph_size.as_vec2(), - atlas_info, - span_index, - byte_index: layout_glyph.start, - byte_length: layout_glyph.end - layout_glyph.start, - line_index: line_i, - }; - layout_info.glyphs.push(pos_glyph); - Ok(()) - }); - if let Some(section) = current_section { - layout_info.run_geometry.push(RunGeometry { - span_index: section, - bounds: Rect::new(start, run.line_top, end, run.line_top + run.line_height), - strikethrough_y: (run.line_y - self.glyph_info[section].3).round(), - strikethrough_thickness: self.glyph_info[section].4, - underline_y: (run.line_y - self.glyph_info[section].5).round(), - underline_thickness: self.glyph_info[section].4, - }); + info.run_geometry.push(RunGeometry { + span_index: span_index as usize, + bounds: Rect { + min: Vec2::new(glyph_run.offset(), line.metrics().min_coord), + max: Vec2::new( + glyph_run.offset() + glyph_run.advance(), + line.metrics().max_coord, + ), + }, + strikethrough_y: glyph_run.baseline() - run.metrics().strikethrough_offset, + strikethrough_thickness: run.metrics().strikethrough_size, + underline_y: glyph_run.baseline() - run.metrics().underline_offset, + underline_thickness: run.metrics().underline_size, + }); + } + _ => {} } - - result - }); - - // Check result. - result?; - - layout_info.size = box_size; - Ok(()) - } - - /// 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<'a>( - &mut self, - entity: Entity, - fonts: &Assets, - text_spans: impl Iterator, - scale_factor: f64, - layout: &TextLayout, - computed: &mut ComputedTextBlock, - font_system: &mut CosmicFontSystem, - ) -> Result { - const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0); - - // Clear this here at the focal point of measured text rendering to ensure the field's lifecycle has - // strong boundaries. - computed.needs_rerender = false; - - self.update_buffer( - fonts, - text_spans, - layout.linebreak, - layout.justify, - MIN_WIDTH_CONTENT_BOUNDS, - scale_factor, - computed, - font_system, - )?; - - let buffer = &mut computed.buffer; - let min_width_content_size = buffer_dimensions(buffer); - - let max_width_content_size = { - let font_system = &mut font_system.0; - buffer.set_size(font_system, None, None); - buffer_dimensions(buffer) - }; - - Ok(TextMeasureInfo { - min: min_width_content_size, - max: max_width_content_size, - entity, - }) - } - - /// Returns the [`cosmic_text::fontdb::ID`] for a given [`Font`] asset. - pub fn get_font_id(&self, asset_id: AssetId) -> Option { - self.map_handle_to_font_id - .get(&asset_id) - .cloned() - .map(|(id, _)| id) - } -} - -/// Render information for a corresponding text block. -/// -/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`] when an entity has -/// [`TextLayout`] and [`ComputedTextBlock`] components. -#[derive(Component, Clone, Default, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -pub struct TextLayoutInfo { - /// The target scale factor for this text layout - pub scale_factor: f32, - /// Scaled and positioned glyphs in screenspace - pub glyphs: Vec, - /// Geometry of each text run used to render text decorations like background colors, strikethrough, and underline. - /// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font, - /// font size, and line height. A text entity that extends over multiple lines will have multiple corresponding runs. - /// - /// The coordinates are unscaled and relative to the top left corner of the text layout. - pub run_geometry: Vec, - /// The glyphs resulting size - pub size: Vec2, -} - -/// Geometry of a text run used to render text decorations like background colors, strikethrough, and underline. -/// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font, -/// font size, and line height. -#[derive(Default, Debug, Clone, Reflect)] -pub struct RunGeometry { - /// The index of the text entity in [`ComputedTextBlock`] that this run belongs to. - pub span_index: usize, - /// Bounding box around the text run - pub bounds: Rect, - /// Y position of the strikethrough in the text layout. - pub strikethrough_y: f32, - /// Strikethrough stroke thickness. - pub strikethrough_thickness: f32, - /// Y position of the underline in the text layout. - pub underline_y: f32, - /// Underline stroke thickness. - pub underline_thickness: f32, -} - -impl RunGeometry { - /// Returns the center of the strikethrough in the text layout. - pub fn strikethrough_position(&self) -> Vec2 { - Vec2::new( - self.bounds.center().x, - self.strikethrough_y + 0.5 * self.strikethrough_thickness, - ) - } - - /// Returns the size of the strikethrough. - pub fn strikethrough_size(&self) -> Vec2 { - Vec2::new(self.bounds.size().x, self.strikethrough_thickness) - } - - /// Get the center of the underline in the text layout. - pub fn underline_position(&self) -> Vec2 { - Vec2::new( - self.bounds.center().x, - self.underline_y + 0.5 * self.underline_thickness, - ) - } - - /// Returns the size of the underline. - pub fn underline_size(&self) -> Vec2 { - Vec2::new(self.bounds.size().x, self.underline_thickness) - } -} - -/// Size information for a corresponding [`ComputedTextBlock`] component. -/// -/// Generated via [`TextPipeline::create_text_measure`]. -#[derive(Debug)] -pub struct TextMeasureInfo { - /// Minimum size for a text area in pixels, to be used when laying out widgets with taffy - pub min: Vec2, - /// Maximum size for a text area in pixels, to be used when laying out widgets with taffy - pub max: Vec2, - /// The entity that is measured. - pub entity: Entity, -} - -impl TextMeasureInfo { - /// Computes the size of the text area within the provided bounds. - pub fn compute_size( - &mut self, - bounds: TextBounds, - computed: &mut ComputedTextBlock, - font_system: &mut CosmicFontSystem, - ) -> Vec2 { - // Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed' - // whenever a canonical state is required. - computed - .buffer - .set_size(&mut font_system.0, bounds.width, bounds.height); - buffer_dimensions(&computed.buffer) - } -} - -/// Add the font to the cosmic text's `FontSystem`'s in-memory font database -pub fn load_font_to_fontdb( - text_font: &TextFont, - font_system: &mut cosmic_text::FontSystem, - map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, Arc)>, - fonts: &Assets, -) -> FontFaceInfo { - let font_handle = text_font.font.clone(); - let (face_id, family_name) = map_handle_to_font_id - .entry(font_handle.id()) - .or_insert_with(|| { - let font = fonts.get(font_handle.id()).expect( - "Tried getting a font that was not available, probably due to not being loaded yet", - ); - let data = Arc::clone(&font.data); - let ids = font_system - .db_mut() - .load_font_source(cosmic_text::fontdb::Source::Binary(data)); - - // TODO: it is assumed this is the right font face - let face_id = *ids.last().unwrap(); - let face = font_system.db().face(face_id).unwrap(); - - let family_name = Arc::from(face.families[0].0.as_str()); - (face_id, family_name) - }); - - let face = font_system.db().face(*face_id).unwrap(); - - FontFaceInfo { - stretch: face.stretch, - style: face.style, - weight: face.weight, - family_name: family_name.clone(), + } } } - -/// Translates [`TextFont`] to [`Attrs`]. -fn get_attrs<'a>( - span_index: usize, - text_font: &TextFont, - line_height: LineHeight, - color: Color, - face_info: &'a FontFaceInfo, - scale_factor: f64, -) -> Attrs<'a> { - Attrs::new() - .metadata(span_index) - .family(Family::Name(&face_info.family_name)) - .stretch(face_info.stretch) - .style(face_info.style) - .weight(face_info.weight) - .metrics( - Metrics { - font_size: text_font.font_size, - line_height: line_height.eval(text_font.font_size), - } - .scale(scale_factor as f32), - ) - .font_features((&text_font.font_features).into()) - .color(cosmic_text::Color(color.to_linear().as_u32())) -} - -/// Calculate the size of the text area for the given buffer. -fn buffer_dimensions(buffer: &Buffer) -> Vec2 { - let (width, height) = buffer - .layout_runs() - .map(|run| (run.line_w, run.line_height)) - .reduce(|(w1, h1), (w2, h2)| (w1.max(w2), h1 + h2)) - .unwrap_or((0.0, 0.0)); - - Vec2::new(width, height).ceil() -} - -/// Discards stale data cached in `FontSystem`. -pub(crate) fn trim_cosmic_cache(mut font_system: ResMut) { - // A trim age of 2 was found to reduce frame time variance vs age of 1 when tested with dynamic text. - // See https://github.com/bevyengine/bevy/pull/15037 - // - // We assume only text updated frequently benefits from the shape cache (e.g. animated text, or - // text that is dynamically measured for UI). - font_system.0.shape_run_cache.trim(2); -} diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index f100eca0c786b..b8ae0049f5494 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,31 +1,24 @@ -use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; +use crate::Font; +use crate::{PositionedGlyph, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; +use bevy_log::once; +use bevy_log::warn; +use bevy_math::{Rect, Vec2}; use bevy_reflect::prelude::*; -use bevy_utils::{default, once}; +use bevy_utils::default; use core::fmt::{Debug, Formatter}; use core::str::from_utf8; -use cosmic_text::{Buffer, Metrics}; +use parley::{FontFeature, Layout}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; -use tracing::warn; - -/// Wrapper for [`cosmic_text::Buffer`] -#[derive(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 sub-entity of a [`ComputedTextBlock`]. /// /// Returned by [`ComputedTextBlock::entities`]. -#[derive(Debug, Copy, Clone, Reflect)] +#[derive(Debug, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct TextEntity { /// The entity. @@ -42,18 +35,10 @@ pub struct TextEntity { #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Debug, Default, Clone)] pub struct ComputedTextBlock { - /// Buffer for managing text layout and creating [`TextLayoutInfo`]. - /// - /// This is private because buffer contents are always refreshed from ECS state when writing glyphs to - /// `TextLayoutInfo`. If you want to control the buffer contents manually or use the `cosmic-text` - /// editor, then you need to not use `TextLayout` and instead manually implement the conversion to - /// `TextLayoutInfo`. - #[reflect(ignore, clone)] - pub(crate) buffer: CosmicBuffer, /// Entities for all text spans in the block, including the root-level text. /// /// The [`TextEntity::depth`] field can be used to reconstruct the hierarchy. - pub(crate) entities: SmallVec<[TextEntity; 1]>, + pub entities: SmallVec<[TextEntity; 1]>, /// Flag set when any change has been made to this block that should cause it to be rerendered. /// /// Includes: @@ -64,7 +49,7 @@ pub struct ComputedTextBlock { // the actual changes are non-structural and can be handled by only rerendering and not remeasuring. A full // solution would probably require splitting TextLayout and TextFont into structural/non-structural // components for more granular change detection. A cost/benefit analysis is needed. - pub(crate) needs_rerender: bool, + pub needs_rerender: bool, } impl ComputedTextBlock { @@ -83,22 +68,11 @@ impl ComputedTextBlock { pub fn needs_rerender(&self) -> bool { self.needs_rerender } - /// Accesses the underlying buffer which can be used for `cosmic-text` APIs such as accessing layout information - /// or calculating a cursor position. - /// - /// Mutable access is not offered because changes would be overwritten during the automated layout calculation. - /// If you want to control the buffer contents manually or use the `cosmic-text` - /// editor, then you need to not use `TextLayout` and instead manually implement the conversion to - /// `TextLayoutInfo`. - pub fn buffer(&self) -> &CosmicBuffer { - &self.buffer - } } impl Default for ComputedTextBlock { fn default() -> Self { Self { - buffer: CosmicBuffer::default(), entities: SmallVec::default(), needs_rerender: true, } @@ -114,7 +88,7 @@ impl Default for ComputedTextBlock { /// See `Text2d` in `bevy_sprite` for the core component of 2d text, and `Text` in `bevy_ui` for UI text. #[derive(Component, Debug, Copy, Clone, Default, Reflect)] #[reflect(Component, Default, Debug, Clone)] -#[require(ComputedTextBlock, TextLayoutInfo)] +#[require(TextLayoutInfo)] pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. @@ -232,15 +206,21 @@ pub enum Justify { /// align with their margins. /// Bounds start from the render position and advance equally left & right. Justified, + /// `TextAlignment::Left` for LTR text and `TextAlignment::Right` for RTL text. + Start, + /// `TextAlignment::Left` for RTL text and `TextAlignment::Right` for LTR text. + End, } -impl From for cosmic_text::Align { +impl From for parley::Alignment { fn from(justify: Justify) -> Self { match justify { - Justify::Left => cosmic_text::Align::Left, - Justify::Center => cosmic_text::Align::Center, - Justify::Right => cosmic_text::Align::Right, - Justify::Justified => cosmic_text::Align::Justified, + Justify::Start => parley::Alignment::Start, + Justify::End => parley::Alignment::End, + Justify::Left => parley::Alignment::Left, + Justify::Center => parley::Alignment::Center, + Justify::Right => parley::Alignment::Right, + Justify::Justified => parley::Alignment::Justify, } } } @@ -269,6 +249,7 @@ pub struct TextFont { /// The antialiasing method to use when rendering text. pub font_smoothing: FontSmoothing, /// OpenType features for .otf fonts that support them. + #[reflect(ignore)] pub font_features: FontFeatures, } @@ -403,6 +384,14 @@ impl Debug for FontFeatureTag { } } +impl From for u32 { + fn from(value: FontFeatureTag) -> Self { + // OpenType use big-endian byte order + // + Self::from_be_bytes(value.0) + } +} + /// OpenType features for .otf fonts that support them. /// /// Examples features include ligatures, small-caps, and fractional number display. For the complete @@ -426,9 +415,9 @@ impl Debug for FontFeatureTag { /// FontFeatureTag::TABULAR_FIGURES /// ].into(); /// ``` -#[derive(Clone, Debug, Default, Reflect, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct FontFeatures { - features: Vec<(FontFeatureTag, u32)>, + features: Vec, } impl FontFeatures { @@ -436,12 +425,17 @@ impl FontFeatures { pub fn builder() -> FontFeaturesBuilder { FontFeaturesBuilder::default() } + + /// Returns the `FontFeature` list as a slice. + pub fn as_slice(&self) -> &[FontFeature] { + &self.features + } } /// A builder for [`FontFeatures`]. #[derive(Clone, Default)] pub struct FontFeaturesBuilder { - features: Vec<(FontFeatureTag, u32)>, + features: Vec, } impl FontFeaturesBuilder { @@ -457,8 +451,11 @@ impl FontFeaturesBuilder { /// /// For most features, the [`FontFeaturesBuilder::enable`] method should be used instead. A few /// features, such as "wght", take numeric values, so this method may be used for these cases. - pub fn set(mut self, feature_tag: FontFeatureTag, value: u32) -> Self { - self.features.push((feature_tag, value)); + pub fn set(mut self, feature_tag: FontFeatureTag, value: u16) -> Self { + self.features.push(FontFeature { + tag: feature_tag.into(), + value, + }); self } @@ -479,20 +476,11 @@ where { fn from(value: T) -> Self { FontFeatures { - features: value.into_iter().map(|x| (x, 1)).collect(), - } - } -} - -impl From<&FontFeatures> for cosmic_text::FontFeatures { - fn from(font_features: &FontFeatures) -> Self { - cosmic_text::FontFeatures { - features: font_features - .features - .iter() - .map(|(tag, value)| cosmic_text::Feature { - tag: cosmic_text::FeatureTag::new(&tag.0), - value: *value, + features: value + .into_iter() + .map(|x| FontFeature { + tag: x.into(), + value: 1, }) .collect(), } @@ -512,10 +500,11 @@ pub enum LineHeight { } impl LineHeight { - pub(crate) fn eval(self, font_size: f32) -> f32 { + /// eval + pub fn eval(self) -> parley::LineHeight { match self { - LineHeight::Px(px) => px, - LineHeight::RelativeToFont(scale) => scale * font_size, + LineHeight::Px(px) => parley::LineHeight::Absolute(px), + LineHeight::RelativeToFont(scale) => parley::LineHeight::FontSizeRelative(scale), } } } @@ -621,6 +610,82 @@ impl> From for StrikethroughColor { #[reflect(Serialize, Deserialize, Clone, Default)] pub struct Underline; +/// Render information for a corresponding text block. +/// +/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`] when an entity has +/// [`TextLayout`] and [`ComputedTextBlock`] components. +#[derive(Component, Clone, Default, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct TextLayoutInfo { + /// The target scale factor for this text layout + pub scale_factor: f32, + /// Scaled and positioned glyphs in screenspace + pub glyphs: Vec, + /// Geometry of each text run used to render text decorations like background colors, strikethrough, and underline. + /// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font, + /// font size, and line height. A text entity that extends over multiple lines will have multiple corresponding runs. + /// + /// The coordinates are unscaled and relative to the top left corner of the text layout. + pub run_geometry: Vec, + /// The glyphs resulting size + pub size: Vec2, +} + +impl TextLayoutInfo { + /// Clear any glyph data + pub fn clear(&mut self) { + self.glyphs.clear(); + self.run_geometry.clear(); + } +} + +/// Geometry of a text run used to render text decorations like background colors, strikethrough, and underline. +/// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font, +/// font size, and line height. +#[derive(Default, Debug, Clone, Reflect)] +pub struct RunGeometry { + /// The index of the text entity in [`ComputedTextBlock`] that this run belongs to. + pub span_index: usize, + /// Bounding box around the text run + pub bounds: Rect, + /// Y position of the strikethrough in the text layout. + pub strikethrough_y: f32, + /// Strikethrough stroke thickness. + pub strikethrough_thickness: f32, + /// Y position of the underline in the text layout. + pub underline_y: f32, + /// Underline stroke thickness. + pub underline_thickness: f32, +} + +impl RunGeometry { + /// Returns the center of the strikethrough in the text layout. + pub fn strikethrough_position(&self) -> Vec2 { + Vec2::new( + self.bounds.center().x, + self.strikethrough_y + 0.5 * self.strikethrough_thickness, + ) + } + + /// Returns the size of the strikethrough. + pub fn strikethrough_size(&self) -> Vec2 { + Vec2::new(self.bounds.size().x, self.strikethrough_thickness) + } + + /// Get the center of the underline in the text layout. + pub fn underline_position(&self) -> Vec2 { + Vec2::new( + self.bounds.center().x, + self.underline_y + 0.5 * self.underline_thickness, + ) + } + + /// Returns the size of the underline. + pub fn underline_size(&self) -> Vec2 { + Vec2::new(self.bounds.size().x, self.underline_thickness) + } +} + /// Color for the text's underline. If this component is not present, its `TextColor` will be used. #[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)] #[reflect(Component, Default, Debug, PartialEq, Clone)] @@ -662,7 +727,11 @@ pub enum FontSmoothing { // SubpixelAntiAliased, } -/// System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`. +/// Computed text layout +#[derive(Component, Default, Deref, DerefMut)] +pub struct ComputedTextLayout(pub Layout<(u32, FontSmoothing)>); + +// System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`. /// /// Generic over the root text component and text span component. For example, `Text2d`/[`TextSpan`] for /// 2d or `Text`/[`TextSpan`] for UI. diff --git a/crates/bevy_text/src/text_access.rs b/crates/bevy_text/src/text_access.rs index 9d04c1dbb5da8..8fef23b2ec9d4 100644 --- a/crates/bevy_text/src/text_access.rs +++ b/crates/bevy_text/src/text_access.rs @@ -16,7 +16,7 @@ pub trait TextSpanAccess: Component { } /// Helper trait for the root text component in a text block. -pub trait TextRoot: TextSpanAccess + From {} +pub trait TextHead: TextSpanAccess + From {} /// Helper trait for the text span components in a text block. pub trait TextSpanComponent: TextSpanAccess + From {} @@ -48,7 +48,7 @@ impl TextIterScratch { /// /// `R` is the root text component. #[derive(SystemParam)] -pub struct TextReader<'w, 's, R: TextRoot> { +pub struct TextReader<'w, 's, R: TextHead> { // This is a local to avoid system ambiguities when TextReaders run in parallel. scratch: Local<'s, TextIterScratch>, roots: Query< @@ -75,7 +75,7 @@ pub struct TextReader<'w, 's, R: TextRoot> { >, } -impl<'w, 's, R: TextRoot> TextReader<'w, 's, R> { +impl<'w, 's, R: TextHead> TextReader<'w, 's, R> { /// Returns an iterator over text spans in a text block, starting with the root entity. pub fn iter(&mut self, root_entity: Entity) -> TextSpanIter<'_, R> { let stack = self.scratch.take(); @@ -154,7 +154,7 @@ impl<'w, 's, R: TextRoot> TextReader<'w, 's, R> { /// Iterates all spans in a text block according to hierarchy traversal order. /// Does *not* flatten interspersed ghost nodes. Only contiguous spans are traversed. // TODO: Use this iterator design in UiChildrenIter to reduce allocations. -pub struct TextSpanIter<'a, R: TextRoot> { +pub struct TextSpanIter<'a, R: TextHead> { scratch: &'a mut TextIterScratch, root_entity: Option, /// Stack of (children, next index into children). @@ -183,7 +183,7 @@ pub struct TextSpanIter<'a, R: TextRoot> { >, } -impl<'a, R: TextRoot> Iterator for TextSpanIter<'a, R> { +impl<'a, R: TextHead> Iterator for TextSpanIter<'a, R> { /// Item = (entity in text block, hierarchy depth in the block, span text, span style). type Item = (Entity, usize, &'a str, &'a TextFont, Color, LineHeight); fn next(&mut self) -> Option { @@ -246,7 +246,7 @@ impl<'a, R: TextRoot> Iterator for TextSpanIter<'a, R> { } } -impl<'a, R: TextRoot> Drop for TextSpanIter<'a, R> { +impl<'a, R: TextHead> Drop for TextSpanIter<'a, R> { fn drop(&mut self) { // Return the internal stack. let stack = core::mem::take(&mut self.stack); @@ -258,7 +258,7 @@ impl<'a, R: TextRoot> Drop for TextSpanIter<'a, R> { /// /// `R` is the root text component, and `S` is the text span component on children. #[derive(SystemParam)] -pub struct TextWriter<'w, 's, R: TextRoot> { +pub struct TextWriter<'w, 's, R: TextHead> { // This is a resource because two TextWriters can't run in parallel. scratch: ResMut<'w, TextIterScratch>, roots: Query< @@ -286,7 +286,7 @@ pub struct TextWriter<'w, 's, R: TextRoot> { children: Query<'w, 's, &'static Children>, } -impl<'w, 's, R: TextRoot> TextWriter<'w, 's, R> { +impl<'w, 's, R: TextHead> TextWriter<'w, 's, R> { /// Gets a mutable reference to a text span within a text block at a specific index in the flattened span list. pub fn get( &mut self, diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 9e8559d218ec9..b1faa32e23b92 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -17,6 +17,7 @@ use bevy_ecs::{ use accesskit::{Node, Rect, Role}; use bevy_camera::CameraUpdateSystems; +use bevy_text::TextSystems; fn calc_label( text_reader: &mut TextUiReader, @@ -154,9 +155,8 @@ impl Plugin for AccessibilityPlugin { .after(CameraUpdateSystems) // the listed systems do not affect calculated size .ambiguous_with(crate::ui_stack_system), - button_changed, - image_changed, - label_changed, + (button_changed, image_changed, label_changed) + .ambiguous_with(TextSystems::RegisterFontAssets), ), ); } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index f4a814cc257c8..3d56519bfad83 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -19,9 +19,7 @@ use bevy_sprite::BorderRect; use thiserror::Error; use ui_surface::UiSurface; -use bevy_text::ComputedTextBlock; - -use bevy_text::CosmicFontSystem; +use bevy_text::ComputedTextLayout; mod convert; pub mod debug; @@ -91,8 +89,7 @@ pub fn ui_layout_system( Option<&ScrollPosition>, Option<&IgnoreScroll>, )>, - mut buffer_query: Query<&mut ComputedTextBlock>, - mut font_system: ResMut, + mut buffer_query: Query<&mut ComputedTextLayout>, mut removed_children: RemovedComponents, mut removed_content_sizes: RemovedComponents, mut removed_nodes: RemovedComponents, @@ -168,7 +165,6 @@ pub fn ui_layout_system( ui_root_entity, computed_target.physical_size, &mut buffer_query, - &mut font_system, ); update_uinode_geometry_recursive( @@ -356,869 +352,3 @@ pub fn ui_layout_system( } } } - -#[cfg(test)] -mod tests { - use crate::update::update_cameras_test_system; - use crate::{ - layout::ui_surface::UiSurface, prelude::*, ui_layout_system, - update::propagate_ui_target_cameras, ContentSize, LayoutContext, - }; - use bevy_app::{App, HierarchyPropagatePlugin, PostUpdate, PropagateSet}; - use bevy_camera::{Camera, Camera2d}; - use bevy_ecs::{prelude::*, system::RunSystemOnce}; - use bevy_math::{Rect, UVec2, Vec2}; - use bevy_platform::collections::HashMap; - use bevy_transform::systems::mark_dirty_trees; - use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms}; - use bevy_utils::prelude::default; - use bevy_window::{PrimaryWindow, Window, WindowResolution}; - use taffy::TraversePartialTree; - - // these window dimensions are easy to convert to and from percentage values - const WINDOW_WIDTH: u32 = 1000; - const WINDOW_HEIGHT: u32 = 100; - - fn setup_ui_test_app() -> App { - let mut app = App::new(); - - app.add_plugins(HierarchyPropagatePlugin::::new( - PostUpdate, - )); - app.add_plugins(HierarchyPropagatePlugin::::new( - PostUpdate, - )); - app.init_resource::(); - app.init_resource::(); - app.init_resource::(); - app.init_resource::(); - app.init_resource::(); - - app.add_systems( - PostUpdate, - ( - update_cameras_test_system, - propagate_ui_target_cameras, - ApplyDeferred, - ui_layout_system, - mark_dirty_trees, - sync_simple_transforms, - propagate_parent_transforms, - ) - .chain(), - ); - - app.configure_sets( - PostUpdate, - PropagateSet::::default() - .after(propagate_ui_target_cameras) - .before(ui_layout_system), - ); - - app.configure_sets( - PostUpdate, - PropagateSet::::default() - .after(propagate_ui_target_cameras) - .before(ui_layout_system), - ); - - let world = app.world_mut(); - // spawn a dummy primary window and camera - world.spawn(( - Window { - resolution: WindowResolution::new(WINDOW_WIDTH, WINDOW_HEIGHT), - ..default() - }, - PrimaryWindow, - )); - world.spawn(Camera2d); - - app - } - - #[test] - fn ui_nodes_with_percent_100_dimensions_should_fill_their_parent() { - let mut app = setup_ui_test_app(); - - let world = app.world_mut(); - - // spawn a root entity with width and height set to fill 100% of its parent - let ui_root = world - .spawn(Node { - width: Val::Percent(100.), - height: Val::Percent(100.), - ..default() - }) - .id(); - - let ui_child = world - .spawn(Node { - width: Val::Percent(100.), - height: Val::Percent(100.), - ..default() - }) - .id(); - - world.entity_mut(ui_root).add_child(ui_child); - - app.update(); - - let mut ui_surface = app.world_mut().resource_mut::(); - - for ui_entity in [ui_root, ui_child] { - let layout = ui_surface.get_layout(ui_entity, true).unwrap().0; - assert_eq!(layout.size.width, WINDOW_WIDTH as f32); - assert_eq!(layout.size.height, WINDOW_HEIGHT as f32); - } - } - - #[test] - fn ui_surface_tracks_ui_entities() { - let mut app = setup_ui_test_app(); - - let world = app.world_mut(); - // no UI entities in world, none in UiSurface - let ui_surface = world.resource::(); - assert!(ui_surface.entity_to_taffy.is_empty()); - - let ui_entity = world.spawn(Node::default()).id(); - - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity)); - assert_eq!(ui_surface.entity_to_taffy.len(), 1); - - world.despawn(ui_entity); - - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - assert!(!ui_surface.entity_to_taffy.contains_key(&ui_entity)); - assert!(ui_surface.entity_to_taffy.is_empty()); - } - - #[test] - #[should_panic] - fn despawning_a_ui_entity_should_remove_its_corresponding_ui_node() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let ui_entity = world.spawn(Node::default()).id(); - - // `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity` - app.update(); - let world = app.world_mut(); - - // retrieve the ui node corresponding to `ui_entity` from ui surface - let ui_surface = world.resource::(); - let ui_node = ui_surface.entity_to_taffy[&ui_entity]; - - world.despawn(ui_entity); - - // `ui_layout_system` will receive a `RemovedComponents` event for `ui_entity` - // and remove `ui_entity` from `ui_node` from the internal layout tree - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - - // `ui_node` is removed, attempting to retrieve a style for `ui_node` panics - let _ = ui_surface.taffy.style(ui_node.id); - } - - #[test] - fn changes_to_children_of_a_ui_entity_change_its_corresponding_ui_nodes_children() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let ui_parent_entity = world.spawn(Node::default()).id(); - - // `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity` - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - let ui_parent_node = ui_surface.entity_to_taffy[&ui_parent_entity]; - - // `ui_parent_node` shouldn't have any children yet - assert_eq!(ui_surface.taffy.child_count(ui_parent_node.id), 0); - - let mut ui_child_entities = (0..10) - .map(|_| { - let child = world.spawn(Node::default()).id(); - world.entity_mut(ui_parent_entity).add_child(child); - child - }) - .collect::>(); - - app.update(); - let world = app.world_mut(); - - // `ui_parent_node` should have children now - let ui_surface = world.resource::(); - assert_eq!( - ui_surface.entity_to_taffy.len(), - 1 + ui_child_entities.len() - ); - assert_eq!( - ui_surface.taffy.child_count(ui_parent_node.id), - ui_child_entities.len() - ); - - let child_node_map = >::from_iter( - ui_child_entities - .iter() - .map(|child_entity| (*child_entity, ui_surface.entity_to_taffy[child_entity])), - ); - - // the children should have a corresponding ui node and that ui node's parent should be `ui_parent_node` - for node in child_node_map.values() { - assert_eq!(ui_surface.taffy.parent(node.id), Some(ui_parent_node.id)); - } - - // delete every second child - let mut deleted_children = vec![]; - for i in (0..ui_child_entities.len()).rev().step_by(2) { - let child = ui_child_entities.remove(i); - world.despawn(child); - deleted_children.push(child); - } - - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - assert_eq!( - ui_surface.entity_to_taffy.len(), - 1 + ui_child_entities.len() - ); - assert_eq!( - ui_surface.taffy.child_count(ui_parent_node.id), - ui_child_entities.len() - ); - - // the remaining children should still have nodes in the layout tree - for child_entity in &ui_child_entities { - let child_node = child_node_map[child_entity]; - assert_eq!(ui_surface.entity_to_taffy[child_entity], child_node); - assert_eq!( - ui_surface.taffy.parent(child_node.id), - Some(ui_parent_node.id) - ); - assert!(ui_surface - .taffy - .children(ui_parent_node.id) - .unwrap() - .contains(&child_node.id)); - } - - // the nodes of the deleted children should have been removed from the layout tree - for deleted_child_entity in &deleted_children { - assert!(!ui_surface - .entity_to_taffy - .contains_key(deleted_child_entity)); - let deleted_child_node = child_node_map[deleted_child_entity]; - assert!(!ui_surface - .taffy - .children(ui_parent_node.id) - .unwrap() - .contains(&deleted_child_node.id)); - } - - // despawn the parent entity and its descendants - world.entity_mut(ui_parent_entity).despawn(); - - app.update(); - let world = app.world_mut(); - - // all nodes should have been deleted - let ui_surface = world.resource::(); - assert!(ui_surface.entity_to_taffy.is_empty()); - } - - /// bugfix test, see [#16288](https://github.com/bevyengine/bevy/pull/16288) - #[test] - fn node_removal_and_reinsert_should_work() { - let mut app = setup_ui_test_app(); - - app.update(); - let world = app.world_mut(); - - // no UI entities in world, none in UiSurface - let ui_surface = world.resource::(); - assert!(ui_surface.entity_to_taffy.is_empty()); - - let ui_entity = world.spawn(Node::default()).id(); - - // `ui_layout_system` should map `ui_entity` to a ui node in `UiSurface::entity_to_taffy` - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity)); - assert_eq!(ui_surface.entity_to_taffy.len(), 1); - - // remove and re-insert Node to trigger removal code in `ui_layout_system` - world.entity_mut(ui_entity).remove::(); - world.entity_mut(ui_entity).insert(Node::default()); - - // `ui_layout_system` should still have `ui_entity` - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity)); - assert_eq!(ui_surface.entity_to_taffy.len(), 1); - } - - #[test] - fn node_addition_should_sync_children() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - // spawn an invalid UI root node - let root_node = world.spawn(()).with_child(Node::default()).id(); - - app.update(); - let world = app.world_mut(); - - // fix the invalid root node by inserting a Node - world.entity_mut(root_node).insert(Node::default()); - - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource_mut::(); - let taffy_root = ui_surface.entity_to_taffy[&root_node]; - - // There should be one child of the root node after fixing it - assert_eq!(ui_surface.taffy.child_count(taffy_root.id), 1); - } - - #[test] - fn node_addition_should_sync_parent_and_children() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let d = world.spawn(Node::default()).id(); - let c = world.spawn(()).add_child(d).id(); - let b = world.spawn(Node::default()).id(); - let a = world.spawn(Node::default()).add_children(&[b, c]).id(); - - app.update(); - let world = app.world_mut(); - - // fix the invalid middle node by inserting a Node - world.entity_mut(c).insert(Node::default()); - - app.update(); - let world = app.world_mut(); - - let ui_surface = world.resource::(); - for (entity, n) in [(a, 2), (b, 0), (c, 1), (d, 0)] { - let taffy_id = ui_surface.entity_to_taffy[&entity].id; - assert_eq!(ui_surface.taffy.child_count(taffy_id), n); - } - } - - /// regression test for >=0.13.1 root node layouts - /// ensure root nodes act like they are absolutely positioned - /// without explicitly declaring it. - #[test] - fn ui_root_node_should_act_like_position_absolute() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let mut size = 150.; - - world.spawn(Node { - // test should pass without explicitly requiring position_type to be set to Absolute - // position_type: PositionType::Absolute, - width: Val::Px(size), - height: Val::Px(size), - ..default() - }); - - size -= 50.; - - world.spawn(Node { - // position_type: PositionType::Absolute, - width: Val::Px(size), - height: Val::Px(size), - ..default() - }); - - size -= 50.; - - world.spawn(Node { - // position_type: PositionType::Absolute, - width: Val::Px(size), - height: Val::Px(size), - ..default() - }); - - app.update(); - let world = app.world_mut(); - - let overlap_check = world - .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without>() - .iter(world) - .fold( - Option::<(Rect, bool)>::None, - |option_rect, (entity, node, transform)| { - let current_rect = Rect::from_center_size(transform.translation, node.size()); - assert!( - current_rect.height().abs() + current_rect.width().abs() > 0., - "root ui node {entity} doesn't have a logical size" - ); - assert_ne!( - *transform, - UiGlobalTransform::default(), - "root ui node {entity} transform is not populated" - ); - let Some((rect, is_overlapping)) = option_rect else { - return Some((current_rect, false)); - }; - if rect.contains(current_rect.center()) { - Some((current_rect, true)) - } else { - Some((current_rect, is_overlapping)) - } - }, - ); - - let Some((_rect, is_overlapping)) = overlap_check else { - unreachable!("test not setup properly"); - }; - assert!(is_overlapping, "root ui nodes are expected to behave like they have absolute position and be independent from each other"); - } - - #[test] - fn ui_node_should_properly_update_when_changing_target_camera() { - #[derive(Component)] - struct MovingUiNode; - - fn update_camera_viewports( - primary_window_query: Query<&Window, With>, - mut cameras: Query<&mut Camera>, - ) { - let primary_window = primary_window_query - .single() - .expect("missing primary window"); - let camera_count = cameras.iter().len(); - for (camera_index, mut camera) in cameras.iter_mut().enumerate() { - let viewport_width = - primary_window.resolution.physical_width() / camera_count as u32; - let viewport_height = primary_window.resolution.physical_height(); - let physical_position = UVec2::new(viewport_width * camera_index as u32, 0); - let physical_size = UVec2::new(viewport_width, viewport_height); - camera.viewport = Some(bevy_camera::Viewport { - physical_position, - physical_size, - ..default() - }); - } - } - - fn move_ui_node( - In(pos): In, - mut commands: Commands, - cameras: Query<(Entity, &Camera)>, - moving_ui_query: Query>, - ) { - let (target_camera_entity, _) = cameras - .iter() - .find(|(_, camera)| { - let Some(logical_viewport_rect) = camera.logical_viewport_rect() else { - panic!("missing logical viewport") - }; - // make sure cursor is in viewport and that viewport has at least 1px of size - logical_viewport_rect.contains(pos) - && logical_viewport_rect.max.cmpge(Vec2::splat(0.)).any() - }) - .expect("cursor position outside of camera viewport"); - for moving_ui_entity in moving_ui_query.iter() { - commands - .entity(moving_ui_entity) - .insert(UiTargetCamera(target_camera_entity)) - .insert(Node { - position_type: PositionType::Absolute, - top: Val::Px(pos.y), - left: Val::Px(pos.x), - ..default() - }); - } - } - - fn do_move_and_test(app: &mut App, new_pos: Vec2, expected_camera_entity: &Entity) { - let world = app.world_mut(); - world.run_system_once_with(move_ui_node, new_pos).unwrap(); - app.update(); - let world = app.world_mut(); - let (ui_node_entity, UiTargetCamera(target_camera_entity)) = world - .query_filtered::<(Entity, &UiTargetCamera), With>() - .single(world) - .expect("missing MovingUiNode"); - assert_eq!(expected_camera_entity, target_camera_entity); - let mut ui_surface = world.resource_mut::(); - - let layout = ui_surface - .get_layout(ui_node_entity, true) - .expect("failed to get layout") - .0; - - // negative test for #12255 - assert_eq!(Vec2::new(layout.location.x, layout.location.y), new_pos); - } - - fn get_taffy_node_count(world: &World) -> usize { - world.resource::().taffy.total_node_count() - } - - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - world.spawn(( - Camera2d, - Camera { - order: 1, - ..default() - }, - )); - - world.spawn(( - Node { - position_type: PositionType::Absolute, - top: Val::Px(0.), - left: Val::Px(0.), - ..default() - }, - MovingUiNode, - )); - - app.update(); - let world = app.world_mut(); - - let pos_inc = Vec2::splat(1.); - let total_cameras = world.query::<&Camera>().iter(world).len(); - // add total cameras - 1 (the assumed default) to get an idea for how many nodes we should expect - let expected_max_taffy_node_count = get_taffy_node_count(world) + total_cameras - 1; - - world.run_system_once(update_camera_viewports).unwrap(); - - app.update(); - let world = app.world_mut(); - - let viewport_rects = world - .query::<(Entity, &Camera)>() - .iter(world) - .map(|(e, c)| (e, c.logical_viewport_rect().expect("missing viewport"))) - .collect::>(); - - for (camera_entity, viewport) in viewport_rects.iter() { - let target_pos = viewport.min + pos_inc; - do_move_and_test(&mut app, target_pos, camera_entity); - } - - // reverse direction - let mut viewport_rects = viewport_rects.clone(); - viewport_rects.reverse(); - for (camera_entity, viewport) in viewport_rects.iter() { - let target_pos = viewport.max - pos_inc; - do_move_and_test(&mut app, target_pos, camera_entity); - } - - let world = app.world(); - let current_taffy_node_count = get_taffy_node_count(world); - if current_taffy_node_count > expected_max_taffy_node_count { - panic!("extra taffy nodes detected: current: {current_taffy_node_count} max expected: {expected_max_taffy_node_count}"); - } - } - - #[test] - fn ui_node_should_be_set_to_its_content_size() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let content_size = Vec2::new(50., 25.); - - let ui_entity = world - .spawn(( - Node { - align_self: AlignSelf::Start, - ..default() - }, - ContentSize::fixed_size(content_size), - )) - .id(); - - app.update(); - let world = app.world_mut(); - - let mut ui_surface = world.resource_mut::(); - let layout = ui_surface.get_layout(ui_entity, true).unwrap().0; - - // the node should takes its size from the fixed size measure func - assert_eq!(layout.size.width, content_size.x); - assert_eq!(layout.size.height, content_size.y); - } - - #[test] - fn measure_funcs_should_be_removed_on_content_size_removal() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let content_size = Vec2::new(50., 25.); - let ui_entity = world - .spawn(( - Node { - align_self: AlignSelf::Start, - ..Default::default() - }, - ContentSize::fixed_size(content_size), - )) - .id(); - - app.update(); - let world = app.world_mut(); - - let mut ui_surface = world.resource_mut::(); - let ui_node = ui_surface.entity_to_taffy[&ui_entity]; - - // a node with a content size should have taffy context - assert!(ui_surface.taffy.get_node_context(ui_node.id).is_some()); - let layout = ui_surface.get_layout(ui_entity, true).unwrap().0; - assert_eq!(layout.size.width, content_size.x); - assert_eq!(layout.size.height, content_size.y); - - world.entity_mut(ui_entity).remove::(); - - app.update(); - let world = app.world_mut(); - - let mut ui_surface = world.resource_mut::(); - // a node without a content size should not have taffy context - assert!(ui_surface.taffy.get_node_context(ui_node.id).is_none()); - - // Without a content size, the node has no width or height constraints so the length of both dimensions is 0. - let layout = ui_surface.get_layout(ui_entity, true).unwrap().0; - assert_eq!(layout.size.width, 0.); - assert_eq!(layout.size.height, 0.); - } - - #[test] - fn ui_rounding_test() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let parent = world - .spawn(Node { - display: Display::Grid, - grid_template_columns: RepeatedGridTrack::min_content(2), - margin: UiRect::all(Val::Px(4.0)), - ..default() - }) - .with_children(|commands| { - for _ in 0..2 { - commands.spawn(Node { - display: Display::Grid, - width: Val::Px(160.), - height: Val::Px(160.), - ..default() - }); - } - }) - .id(); - - let children = world - .entity(parent) - .get::() - .unwrap() - .iter() - .collect::>(); - - for r in [2, 3, 5, 7, 11, 13, 17, 19, 21, 23, 29, 31].map(|n| (n as f32).recip()) { - // This fails with very small / unrealistic scale values - let mut s = 1. - r; - while s <= 5. { - app.world_mut().resource_mut::().0 = s; - app.update(); - let world = app.world_mut(); - let width_sum: f32 = children - .iter() - .map(|child| world.get::(*child).unwrap().size.x) - .sum(); - let parent_width = world.get::(parent).unwrap().size.x; - assert!((width_sum - parent_width).abs() < 0.001); - assert!((width_sum - 320. * s).abs() <= 1.); - s += r; - } - } - } - - #[test] - fn no_camera_ui() { - let mut app = App::new(); - - app.add_systems( - PostUpdate, - (propagate_ui_target_cameras, ApplyDeferred, ui_layout_system).chain(), - ); - - app.add_plugins(HierarchyPropagatePlugin::::new( - PostUpdate, - )); - - app.configure_sets( - PostUpdate, - PropagateSet::::default() - .after(propagate_ui_target_cameras) - .before(ui_layout_system), - ); - - let world = app.world_mut(); - world.init_resource::(); - world.init_resource::(); - - world.init_resource::(); - - world.init_resource::(); - - world.init_resource::(); - - let ui_root = world - .spawn(Node { - width: Val::Percent(100.), - height: Val::Percent(100.), - ..default() - }) - .id(); - - let ui_child = world - .spawn(Node { - width: Val::Percent(100.), - height: Val::Percent(100.), - ..default() - }) - .id(); - - world.entity_mut(ui_root).add_child(ui_child); - - app.update(); - } - - #[test] - fn test_ui_surface_compute_camera_layout() { - use bevy_ecs::prelude::ResMut; - - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let root_node_entity = Entity::from_raw_u32(1).unwrap(); - - struct TestSystemParam { - root_node_entity: Entity, - } - - fn test_system( - params: In, - mut ui_surface: ResMut, - mut computed_text_block_query: Query<&mut bevy_text::ComputedTextBlock>, - mut font_system: ResMut, - ) { - ui_surface.upsert_node( - &LayoutContext::TEST_CONTEXT, - params.root_node_entity, - &Node::default(), - None, - ); - - ui_surface.compute_layout( - params.root_node_entity, - UVec2::new(800, 600), - &mut computed_text_block_query, - &mut font_system, - ); - } - - let _ = world.run_system_once_with(test_system, TestSystemParam { root_node_entity }); - - let ui_surface = world.resource::(); - - let taffy_node = ui_surface.entity_to_taffy.get(&root_node_entity).unwrap(); - assert!(ui_surface.taffy.layout(taffy_node.id).is_ok()); - } - - #[test] - fn no_viewport_node_leak_on_root_despawned() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let ui_root_entity = world.spawn(Node::default()).id(); - - // The UI schedule synchronizes Bevy UI's internal `TaffyTree` with the - // main world's tree of `Node` entities. - app.update(); - let world = app.world_mut(); - - // Two taffy nodes are added to the internal `TaffyTree` for each root UI entity. - // An implicit taffy node representing the viewport and a taffy node corresponding to the - // root UI entity which is parented to the viewport taffy node. - assert_eq!( - world.resource_mut::().taffy.total_node_count(), - 2 - ); - - world.despawn(ui_root_entity); - - // The UI schedule removes both the taffy node corresponding to `ui_root_entity` and its - // parent viewport node. - app.update(); - let world = app.world_mut(); - - // Both taffy nodes should now be removed from the internal `TaffyTree` - assert_eq!( - world.resource_mut::().taffy.total_node_count(), - 0 - ); - } - - #[test] - fn no_viewport_node_leak_on_parented_root() { - let mut app = setup_ui_test_app(); - let world = app.world_mut(); - - let ui_root_entity_1 = world.spawn(Node::default()).id(); - let ui_root_entity_2 = world.spawn(Node::default()).id(); - - app.update(); - let world = app.world_mut(); - - // There are two UI root entities. Each root taffy node is given it's own viewport node parent, - // so a total of four taffy nodes are added to the `TaffyTree` by the UI schedule. - assert_eq!( - world.resource_mut::().taffy.total_node_count(), - 4 - ); - - // Parent `ui_root_entity_2` onto `ui_root_entity_1` so now only `ui_root_entity_1` is a - // UI root entity. - world - .entity_mut(ui_root_entity_1) - .add_child(ui_root_entity_2); - - // Now there is only one root node so the second viewport node is removed by - // the UI schedule. - app.update(); - let world = app.world_mut(); - - // There is only one viewport node now, so the `TaffyTree` contains 3 nodes in total. - assert_eq!( - world.resource_mut::().taffy.total_node_count(), - 3 - ); - } -} diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index 3f33363a32bc6..ae4132b73a21f 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -12,7 +12,6 @@ use bevy_math::{UVec2, Vec2}; use bevy_utils::default; use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure}; -use bevy_text::CosmicFontSystem; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct LayoutNode { @@ -209,8 +208,7 @@ impl UiSurface { &mut self, ui_root_entity: Entity, render_target_resolution: UVec2, - buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>, - font_system: &'a mut CosmicFontSystem, + buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextLayout>, ) { let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity); @@ -232,7 +230,7 @@ impl UiSurface { context .map(|ctx| { let buffer = get_text_buffer( - crate::widget::TextMeasure::needs_buffer( + crate::widget::TextMeasure::needs_text_layout( known_dimensions.height, available_space.width, ), @@ -245,8 +243,7 @@ impl UiSurface { height: known_dimensions.height, available_width: available_space.width, available_height: available_space.height, - font_system, - buffer, + maybe_text_layout: buffer, }, style, ); @@ -310,8 +307,8 @@ impl UiSurface { pub fn get_text_buffer<'a>( needs_buffer: bool, ctx: &mut NodeMeasure, - query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>, -) -> Option<&'a mut bevy_text::ComputedTextBlock> { + query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextLayout>, +) -> Option<&'a mut bevy_text::ComputedTextLayout> { // We avoid a query lookup whenever the buffer is not required. if !needs_buffer { return None; @@ -324,170 +321,3 @@ pub fn get_text_buffer<'a>( }; Some(computed.into_inner()) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ContentSize, FixedMeasure}; - use bevy_math::Vec2; - use taffy::TraversePartialTree; - - #[test] - fn test_initialization() { - let ui_surface = UiSurface::default(); - assert!(ui_surface.entity_to_taffy.is_empty()); - assert_eq!(ui_surface.taffy.total_node_count(), 0); - } - - #[test] - fn test_upsert() { - let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw_u32(1).unwrap(); - let node = Node::default(); - - // standard upsert - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - - // should be inserted into taffy - assert_eq!(ui_surface.taffy.total_node_count(), 1); - assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity)); - - // test duplicate insert 1 - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - - // node count should not have increased - assert_eq!(ui_surface.taffy.total_node_count(), 1); - - // assign root node to camera - ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); - - // each root node will create 2 taffy nodes - assert_eq!(ui_surface.taffy.total_node_count(), 2); - - // test duplicate insert 2 - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - - // node count should not have increased - assert_eq!(ui_surface.taffy.total_node_count(), 2); - } - - #[test] - fn test_remove_entities() { - let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw_u32(1).unwrap(); - let node = Node::default(); - - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - - ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); - - assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity)); - - ui_surface.remove_entities([root_node_entity]); - assert!(!ui_surface.entity_to_taffy.contains_key(&root_node_entity)); - } - - #[test] - fn test_try_update_measure() { - let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw_u32(1).unwrap(); - let node = Node::default(); - - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - let mut content_size = ContentSize::default(); - content_size.set(NodeMeasure::Fixed(FixedMeasure { size: Vec2::ONE })); - let measure_func = content_size.measure.take().unwrap(); - assert!(ui_surface - .update_node_context(root_node_entity, measure_func) - .is_some()); - } - - #[test] - fn test_update_children() { - let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw_u32(1).unwrap(); - let child_entity = Entity::from_raw_u32(2).unwrap(); - let node = Node::default(); - - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None); - - ui_surface.update_children(root_node_entity, vec![child_entity].into_iter()); - - let parent_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap(); - let child_node = *ui_surface.entity_to_taffy.get(&child_entity).unwrap(); - assert_eq!(ui_surface.taffy.parent(child_node.id), Some(parent_node.id)); - } - - #[expect( - unreachable_code, - reason = "Certain pieces of code tested here cause the test to fail if made reachable; see #16362 for progress on fixing this" - )] - #[test] - fn test_set_camera_children() { - let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw_u32(1).unwrap(); - let child_entity = Entity::from_raw_u32(2).unwrap(); - let node = Node::default(); - - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None); - - let root_taffy_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap(); - let child_taffy = *ui_surface.entity_to_taffy.get(&child_entity).unwrap(); - - // set up the relationship manually - ui_surface - .taffy - .add_child(root_taffy_node.id, child_taffy.id) - .unwrap(); - - ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); - - assert_eq!( - ui_surface.taffy.parent(child_taffy.id), - Some(root_taffy_node.id) - ); - let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap(); - assert!( - root_taffy_children.contains(&child_taffy.id), - "root node is not a parent of child node" - ); - assert_eq!( - ui_surface.taffy.child_count(root_taffy_node.id), - 1, - "expected root node child count to be 1" - ); - - // clear camera's root nodes - ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); - - return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code)) - - let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap(); - assert!( - root_taffy_children.contains(&child_taffy.id), - "root node is not a parent of child node" - ); - assert_eq!( - ui_surface.taffy.child_count(root_taffy_node.id), - 1, - "expected root node child count to be 1" - ); - - // re-associate root node with viewport node - ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); - - let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap(); - let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap(); - assert!( - root_taffy_children.contains(&child_taffy.id), - "root node is not a parent of child node" - ); - assert_eq!( - ui_surface.taffy.child_count(root_taffy_node.id), - 1, - "expected root node child count to be 1" - ); - } -} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 4b698a3de4a0c..845ab8b910849 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -34,6 +34,7 @@ mod layout; mod stack; mod ui_node; +use bevy_text::TextSystems; pub use focus::*; pub use geometry::*; pub use gradients::*; @@ -180,8 +181,7 @@ impl Plugin for UiPlugin { let ui_layout_system_config = ui_layout_system_config // Text and Text2D operate on disjoint sets of entities - .ambiguous_with(bevy_sprite::update_text2d_layout) - .ambiguous_with(bevy_text::detect_text_needs_rerender::); + .ambiguous_with(bevy_sprite::update_text2d_layout); app.add_systems( PostUpdate, @@ -191,7 +191,7 @@ impl Plugin for UiPlugin { ui_stack_system .in_set(UiSystems::Stack) // These systems don't care about stack index - .ambiguous_with(widget::measure_text_system) + .ambiguous_with(widget::shape_text_system) .ambiguous_with(update_clipping_system) .ambiguous_with(ui_layout_system) .ambiguous_with(widget::update_viewport_render_target_size) @@ -227,12 +227,12 @@ fn build_text_interop(app: &mut App) { ( ( bevy_text::detect_text_needs_rerender::, - widget::measure_text_system, + widget::shape_text_system, ) .chain() + .after(TextSystems::RegisterFontAssets) .in_set(UiSystems::Content) // Text and Text2d are independent. - .ambiguous_with(bevy_text::detect_text_needs_rerender::) // Potential conflict: `Assets` // Since both systems will only ever insert new [`Image`] assets, // they will never observe each other's effects. @@ -240,12 +240,12 @@ fn build_text_interop(app: &mut App) { // We assume Text is on disjoint UI entities to ImageNode and UiTextureAtlasImage // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(widget::update_image_content_size_system), - widget::text_system + widget::layout_text_system + .after(TextSystems::RegisterFontAssets) .in_set(UiSystems::PostLayout) - .after(bevy_text::free_unused_font_atlases_system) - .before(bevy_asset::AssetEventSystems) + //.after(bevy_text::free_unused_font_atlases_system) + .after(bevy_asset::AssetEventSystems) // Text2d and bevy_ui text are entirely on separate entities - .ambiguous_with(bevy_text::detect_text_needs_rerender::) .ambiguous_with(bevy_sprite::update_text2d_layout) .ambiguous_with(bevy_sprite::calculate_bounds_text2d), ), @@ -262,7 +262,7 @@ fn build_text_interop(app: &mut App) { app.configure_sets( PostUpdate, - AmbiguousWithText.ambiguous_with(widget::text_system), + AmbiguousWithText.ambiguous_with(widget::layout_text_system), ); app.configure_sets( diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 029498ab8de9d..1550ad3f80c29 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -1,7 +1,6 @@ use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_text::CosmicFontSystem; use core::fmt::Formatter; pub use taffy::style::AvailableSpace; @@ -20,8 +19,7 @@ pub struct MeasureArgs<'a> { pub height: Option, pub available_width: AvailableSpace, pub available_height: AvailableSpace, - pub font_system: &'a mut CosmicFontSystem, - pub buffer: Option<&'a mut bevy_text::ComputedTextBlock>, + pub maybe_text_layout: Option<&'a mut bevy_text::ComputedTextLayout>, } /// A `Measure` is used to compute the size of a ui node @@ -37,7 +35,6 @@ pub trait Measure: Send + Sync + 'static { /// by wrapping them in a closure and a Custom variant that allows arbitrary measurement closures if required. pub enum NodeMeasure { Fixed(FixedMeasure), - Text(TextMeasure), Image(ImageMeasure), Custom(Box), diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index d604bc6977cd9..f4df933384992 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -9,39 +9,38 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - query::With, reflect::ReflectComponent, system::{Query, Res, ResMut}, - world::{Mut, Ref}, + world::Ref, }; use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet, LineBreak, LineHeight, SwashCache, - TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, - TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, + update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, + LayoutCx, LineBreak, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, + TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; /// UI text system flags. /// -/// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing. +/// Used internally by [`shape_text_system`] and [`layout_text_system`] to schedule text for processing. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default, Debug, Clone)] pub struct TextNodeFlags { - /// If set then a new measure function for the text node will be created. - needs_measure_fn: bool, + /// If set then the text will be reshaped. + pub needs_shaping: bool, /// If set then the text will be recomputed. - needs_recompute: bool, + pub needs_relayout: bool, } impl Default for TextNodeFlags { fn default() -> Self { Self { - needs_measure_fn: true, - needs_recompute: true, + needs_shaping: true, + needs_relayout: true, } } } @@ -100,9 +99,11 @@ impl Default for TextNodeFlags { TextLayout, TextFont, TextColor, - LineHeight, TextNodeFlags, - ContentSize + ContentSize, + ComputedTextBlock, + ComputedTextLayout, + LineHeight )] pub struct Text(pub String); @@ -113,7 +114,7 @@ impl Text { } } -impl TextRoot for Text {} +impl TextHead for Text {} impl TextSpanAccess for Text { fn read_span(&self) -> &str { @@ -164,6 +165,23 @@ pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>; /// UI alias for [`TextWriter`]. pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>; +/// Data for `TextMeasure` +pub struct TextMeasureInfo { + pub min: Vec2, + pub max: Vec2, + pub entity: Entity, +} + +impl TextMeasureInfo { + /// Computes the size of the text area within the provided bounds. + pub fn compute_size(&mut self, bounds: TextBounds, layout: &mut ComputedTextLayout) -> Vec2 { + // Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed' + // whenever a canonical state is required. + layout.break_all_lines(bounds.width); + Vec2::new(layout.width(), layout.height()) + } +} + /// Text measurement for UI layout. See [`NodeMeasure`]. pub struct TextMeasure { pub info: TextMeasureInfo, @@ -172,7 +190,7 @@ pub struct TextMeasure { impl TextMeasure { /// Checks if the cosmic text buffer is needed for measuring the text. #[inline] - pub const fn needs_buffer(height: Option, available_width: AvailableSpace) -> bool { + pub const fn needs_text_layout(height: Option, available_width: AvailableSpace) -> bool { height.is_none() && matches!(available_width, AvailableSpace::Definite(_)) } } @@ -183,9 +201,8 @@ impl Measure for TextMeasure { width, height, available_width, - buffer, - font_system, - .. + available_height: _, + maybe_text_layout, } = measure_args; let x = width.unwrap_or_else(|| match available_width { AvailableSpace::Definite(x) => { @@ -203,12 +220,9 @@ impl Measure for TextMeasure { .map_or_else( || match available_width { AvailableSpace::Definite(_) => { - if let Some(buffer) = buffer { - self.info.compute_size( - TextBounds::new_horizontal(x), - buffer, - font_system, - ) + if let Some(text_layout) = maybe_text_layout { + self.info + .compute_size(TextBounds::new_horizontal(x), text_layout) } else { error!("text measure failed, buffer is missing"); Vec2::default() @@ -223,55 +237,6 @@ impl Measure for TextMeasure { } } -#[inline] -fn create_text_measure<'a>( - entity: Entity, - fonts: &Assets, - scale_factor: f64, - spans: impl Iterator, - block: Ref, - text_pipeline: &mut TextPipeline, - mut content_size: Mut, - mut text_flags: Mut, - mut computed: Mut, - font_system: &mut CosmicFontSystem, -) { - match text_pipeline.create_text_measure( - entity, - fonts, - spans, - scale_factor, - &block, - computed.as_mut(), - font_system, - ) { - Ok(measure) => { - if block.linebreak == LineBreak::NoWrap { - content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); - } else { - content_size.set(NodeMeasure::Text(TextMeasure { info: measure })); - } - - // Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute - text_flags.needs_measure_fn = false; - text_flags.needs_recompute = true; - } - Err(TextError::NoSuchFont) => { - // Try again next frame - text_flags.needs_measure_fn = true; - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - }; -} - /// Generates a new [`Measure`] for a text node on changes to its [`Text`] component. /// /// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space @@ -282,116 +247,75 @@ fn create_text_measure<'a>( /// is only able to detect that a `Text` component has changed and will regenerate the `Measure` on /// color changes. This can be expensive, particularly for large blocks of text, and the [`bypass_change_detection`](bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection) /// method should be called when only changing the `Text`'s colors. -pub fn measure_text_system( +pub fn shape_text_system( + mut text_pipeline: ResMut, + mut font_cx: ResMut, + mut layout_cx: ResMut, fonts: Res>, - mut text_query: Query< - ( - Entity, - Ref, - &mut ContentSize, - &mut TextNodeFlags, - &mut ComputedTextBlock, - Ref, - &ComputedNode, - ), - With, - >, + mut text_query: Query<( + Entity, + Ref, + &mut ContentSize, + &mut TextNodeFlags, + &mut ComputedTextBlock, + &mut ComputedTextLayout, + Ref, + &ComputedNode, + )>, mut text_reader: TextUiReader, - mut text_pipeline: ResMut, - mut font_system: ResMut, ) { - for (entity, block, content_size, text_flags, computed, computed_target, computed_node) in - &mut text_query + for ( + entity, + block, + mut content_size, + mut text_flags, + mut computed_block, + mut computed_layout, + computed_target, + computed_node, + ) in &mut text_query { // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). // 1e-5 epsilon to ignore tiny scale factor float errors - if 1e-5 + + if !(1e-5 < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() - || computed.needs_rerender() - || text_flags.needs_measure_fn - || content_size.is_added() + || computed_block.needs_rerender() + || text_flags.needs_shaping + || content_size.is_added()) { - create_text_measure( - entity, - &fonts, - computed_target.scale_factor.into(), - text_reader.iter(entity), - block, - &mut text_pipeline, - content_size, - text_flags, - computed, - &mut font_system, - ); + continue; } - } -} + computed_block.needs_rerender = false; + computed_block.entities.clear(); -#[inline] -fn queue_text( - entity: Entity, - fonts: &Assets, - text_pipeline: &mut TextPipeline, - font_atlas_set: &mut FontAtlasSet, - texture_atlases: &mut Assets, - textures: &mut Assets, - scale_factor: f32, - inverse_scale_factor: f32, - block: &TextLayout, - node: Ref, - mut text_flags: Mut, - text_layout_info: Mut, - computed: &mut ComputedTextBlock, - text_reader: &mut TextUiReader, - font_system: &mut CosmicFontSystem, - swash_cache: &mut SwashCache, -) { - // Skip the text node if it is waiting for a new measure func - if text_flags.needs_measure_fn { - return; - } + text_pipeline.shape_text( + entity, + &mut text_reader, + &mut computed_layout.0, + &mut font_cx.0, + &mut layout_cx.0, + computed_target.scale_factor, + block.linebreak, + &fonts, + &mut computed_block.entities, + ); - let physical_node_size = if block.linebreak == LineBreak::NoWrap { - // With `NoWrap` set, no constraints are placed on the width of the text. - TextBounds::UNBOUNDED - } else { - // `scale_factor` is already multiplied by `UiScale` - TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) - }; + computed_layout.break_all_lines(None); + let max = (computed_layout.width(), computed_layout.height()).into(); - let text_layout_info = text_layout_info.into_inner(); - match text_pipeline.queue_text( - text_layout_info, - fonts, - text_reader.iter(entity), - scale_factor.into(), - block, - physical_node_size, - font_atlas_set, - texture_atlases, - textures, - computed, - font_system, - swash_cache, - ) { - Err(TextError::NoSuchFont) => { - // There was an error processing the text layout, try again next frame - text_flags.needs_recompute = true; - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.scale_factor = scale_factor; - text_layout_info.size *= inverse_scale_factor; - text_flags.needs_recompute = false; + if block.linebreak == LineBreak::NoWrap { + content_size.set(NodeMeasure::Fixed(FixedMeasure { size: max })); + } else { + computed_layout.break_all_lines(Some(0.)); + let min = (computed_layout.width(), computed_layout.height()).into(); + content_size.set(NodeMeasure::Text(TextMeasure { + info: TextMeasureInfo { min, max, entity }, + })); } + + text_flags.needs_shaping = false; + text_flags.needs_relayout = true; } } @@ -403,44 +327,33 @@ fn queue_text( /// /// [`ResMut>`](Assets) -- This system only adds new [`Image`] assets. /// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`]. -pub fn text_system( +pub fn layout_text_system( mut textures: ResMut>, - fonts: Res>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, - mut text_pipeline: ResMut, mut text_query: Query<( - Entity, Ref, &TextLayout, &mut TextLayoutInfo, &mut TextNodeFlags, - &mut ComputedTextBlock, + &mut ComputedTextLayout, )>, - mut text_reader: TextUiReader, - mut font_system: ResMut, - mut swash_cache: ResMut, + mut scale_cx: ResMut, ) { - for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query { - if node.is_changed() || text_flags.needs_recompute { - queue_text( - entity, - &fonts, - &mut text_pipeline, + for (node, block, mut text_layout_info, mut text_flags, mut layout) in &mut text_query { + if node.is_changed() || layout.is_changed() || text_flags.needs_relayout { + update_text_layout_info( + &mut text_layout_info, + &mut layout.0, + Some(node.size.x).filter(|_| block.linebreak != LineBreak::NoWrap), + block.justify.into(), + &mut scale_cx, &mut font_atlas_set, &mut texture_atlases, &mut textures, - node.inverse_scale_factor.recip(), - node.inverse_scale_factor, - block, - node, - text_flags, - text_layout_info, - computed.as_mut(), - &mut text_reader, - &mut font_system, - &mut swash_cache, ); + + text_flags.needs_relayout = false; } } } diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 01ea9f8f8b82a..3090b162daf1c 100644 --- a/examples/stress_tests/many_buttons.rs +++ b/examples/stress_tests/many_buttons.rs @@ -6,6 +6,7 @@ use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, prelude::*, text::TextColor, + ui::widget::TextNodeFlags, window::{PresentMode, WindowResolution}, winit::WinitSettings, }; @@ -113,6 +114,11 @@ fn main() { .iter_mut() .for_each(|mut text| text.set_changed()); }); + app.add_systems(Update, |mut text_query: Query<&mut TextNodeFlags>| { + text_query + .iter_mut() + .for_each(|mut text| text.needs_shaping = true); + }); } if args.respawn { diff --git a/examples/stress_tests/many_glyphs.rs b/examples/stress_tests/many_glyphs.rs index e3db576468506..fa81403546bde 100644 --- a/examples/stress_tests/many_glyphs.rs +++ b/examples/stress_tests/many_glyphs.rs @@ -66,6 +66,7 @@ fn setup(mut commands: Commands, args: Res) { commands.spawn(Camera2d); let text_string = "0123456789".repeat(10_000); + let text_font = TextFont { font_size: 4., ..Default::default() diff --git a/examples/stress_tests/many_text2d.rs b/examples/stress_tests/many_text2d.rs index d3646c8d2c45d..f4768b96d211e 100644 --- a/examples/stress_tests/many_text2d.rs +++ b/examples/stress_tests/many_text2d.rs @@ -171,17 +171,15 @@ fn print_counts( mut timer: Local, texts: Query<&ViewVisibility, With>, font_atlas_set: Res, - font: Res, + _font: Res, ) { timer.tick(time.delta()); if !timer.just_finished() { return; } - let font_id = font.0.id(); let num_atlases = font_atlas_set .iter() - .filter(|(key, _)| key.0 == font_id) .map(|(_, atlases)| atlases.len()) .sum::(); diff --git a/release-content/migration-guides/text_node_flags.md b/release-content/migration-guides/text_node_flags.md new file mode 100644 index 0000000000000..26a099762caac --- /dev/null +++ b/release-content/migration-guides/text_node_flags.md @@ -0,0 +1,9 @@ +--- +title: "The fields of TextNodeFlags have been renamed" +pull_requests: [21940] +--- + +The fields of `TextNodeFlags` have been renamed: + +* `needs_measure_fn` -> `needs_shaping` +* `needs_recompute` -> `needs_relayout` diff --git a/release-content/release-notes/parley_migration.md b/release-content/release-notes/parley_migration.md new file mode 100644 index 0000000000000..59aa9683afb8e --- /dev/null +++ b/release-content/release-notes/parley_migration.md @@ -0,0 +1,7 @@ +--- +title: "`bevy_text` parley migration" +authors: ["@ickshonpe"] +pull_requests: [21940] +--- + +`bevy_text` now uses parley 0.7.0