From abac10f446196a0545aad57fab778238ce2e84f0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 16 Oct 2025 12:46:55 +0100 Subject: [PATCH 01/84] Added parley depedency --- crates/bevy_text/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 12a36d5826b7d..7397122ea0883 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -36,6 +36,7 @@ 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.6.0" } [lints] workspace = true From e8cc12e8855f8cf3865cda1aef06f3b318a17d7b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 16 Oct 2025 13:46:03 +0100 Subject: [PATCH 02/84] Changed font asset to hold font collection information returned from `Collection::register_fonts`. --- crates/bevy_text/src/font.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index b748f4a111fdd..5dca3d6a24eb2 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,7 +1,9 @@ -use alloc::sync::Arc; - use bevy_asset::Asset; use bevy_reflect::TypePath; +use parley::fontique::Blob; +use parley::fontique::FamilyId; +use parley::fontique::FontInfo; +use parley::FontContext; /// An [`Asset`] that contains the data for a loaded font, if loaded as an asset. /// @@ -17,19 +19,24 @@ use bevy_reflect::TypePath; /// 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>, + collection: Vec<(FamilyId, Vec)>, } +pub struct NoFontsFoundError; + impl Font { /// Creates a [`Font`] from bytes pub fn try_from_bytes( + font_cx: &mut FontContext, font_data: Vec, - ) -> Result { - use cosmic_text::ttf_parser; - ttf_parser::Face::parse(&font_data, 0)?; - Ok(Self { - data: Arc::new(font_data), - }) + ) -> Result { + let collection = font_cx + .collection + .register_fonts(Blob::from(font_data), None); + if collection.is_empty() { + Ok(Font { collection }) + } else { + Err(NoFontsFoundError) + } } } From f0bad8d9bb980d2ed3eaccdc4d67d9d81a2aa015 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 16 Oct 2025 13:57:02 +0100 Subject: [PATCH 03/84] New Font type wrapping a `Blob` and `Collection` --- crates/bevy_text/src/font.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 5dca3d6a24eb2..b95fc23e4fbbe 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -3,7 +3,6 @@ use bevy_reflect::TypePath; use parley::fontique::Blob; use parley::fontique::FamilyId; use parley::fontique::FontInfo; -use parley::FontContext; /// An [`Asset`] that contains the data for a loaded font, if loaded as an asset. /// @@ -19,6 +18,7 @@ use parley::FontContext; /// Bevy currently loads a single font face as a single `Font` asset. #[derive(Debug, TypePath, Clone, Asset)] pub struct Font { + blob: Blob, collection: Vec<(FamilyId, Vec)>, } @@ -26,17 +26,10 @@ pub struct NoFontsFoundError; impl Font { /// Creates a [`Font`] from bytes - pub fn try_from_bytes( - font_cx: &mut FontContext, - font_data: Vec, - ) -> Result { - let collection = font_cx - .collection - .register_fonts(Blob::from(font_data), None); - if collection.is_empty() { - Ok(Font { collection }) - } else { - Err(NoFontsFoundError) + pub fn try_from_bytes(font_data: Vec) -> Font { + Font { + blob: Blob::from(font_data), + collection: vec![], } } } From 970c1f7e412cded468587640682ca9dc87f65e93 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 16 Oct 2025 13:57:18 +0100 Subject: [PATCH 04/84] fixed default font asset loader --- crates/bevy_text/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index acd43f92edd82..ad17b273e4cdf 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -105,7 +105,7 @@ impl Plugin for TextPlugin { { 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()); assets.insert(AssetId::default(), asset).unwrap(); }; } From 7183b0309b61fa42bfe37cd187168fb73d5d3756 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 16 Oct 2025 13:57:55 +0100 Subject: [PATCH 05/84] Fixed asset loader for Font --- crates/bevy_text/src/font_loader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index 9e0f2185a234e..ac5a526fb2c6b 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -30,7 +30,7 @@ impl AssetLoader for FontLoader { ) -> Result { 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); Ok(font) } From 4af0ac23d02adc21f721156eef60b40552e3a309 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 16 Oct 2025 14:53:59 +0100 Subject: [PATCH 06/84] Added `Context` module with `TextContext` resource wrapping `FontContext`. --- crates/bevy_text/src/context.rs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 crates/bevy_text/src/context.rs diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs new file mode 100644 index 0000000000000..21594c87b3612 --- /dev/null +++ b/crates/bevy_text/src/context.rs @@ -0,0 +1,7 @@ +use bevy_ecs::resource::Resource; +use parley::FontContext; + +#[derive(Resource)] +pub struct TextContext { + pub font_cx: FontContext, +} From 9c6641d76e6de3b43292911d4a249366fd8d5b18 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 16 Oct 2025 14:55:15 +0100 Subject: [PATCH 07/84] Added `register_font_assets_system` that registers new `Font` assets with the parley `FontContext`. --- crates/bevy_text/src/font.rs | 28 ++++++++++++++++++++++++++-- crates/bevy_text/src/lib.rs | 12 ++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index b95fc23e4fbbe..b9613990c7558 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,4 +1,9 @@ +use crate::context::TextContext; use bevy_asset::Asset; +use bevy_asset::AssetEvent; +use bevy_asset::Assets; +use bevy_ecs::message::MessageReader; +use bevy_ecs::system::ResMut; use bevy_reflect::TypePath; use parley::fontique::Blob; use parley::fontique::FamilyId; @@ -22,8 +27,6 @@ pub struct Font { collection: Vec<(FamilyId, Vec)>, } -pub struct NoFontsFoundError; - impl Font { /// Creates a [`Font`] from bytes pub fn try_from_bytes(font_data: Vec) -> Font { @@ -33,3 +36,24 @@ impl Font { } } } + +pub fn register_font_assets_system( + mut cx: ResMut, + mut fonts: ResMut>, + mut events: MessageReader>, +) { + for event in events.read() { + match event { + AssetEvent::Added { id } => { + if let Some(font) = fonts.get_mut(*id) { + let collection = cx + .font_cx + .collection + .register_fonts(font.blob.clone(), None); + font.collection = collection; + } + } + _ => {} + } + } +} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index ad17b273e4cdf..748ed69a91053 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; @@ -86,6 +87,11 @@ pub struct Text2dUpdateSystems; #[deprecated(since = "0.17.0", note = "Renamed to `Text2dUpdateSystems`.")] pub type Update2dText = Text2dUpdateSystems; +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub enum TextSystems { + RegisterFontAssets, +} + impl Plugin for TextPlugin { fn build(&self, app: &mut App) { app.init_asset::() @@ -95,6 +101,12 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() + .add_systems( + PostUpdate, + register_font_assets_system + .in_set(TextSystems::RegisterFontAssets) + .after(AssetEventSystems), + ) .add_systems( PostUpdate, free_unused_font_atlases_system.before(AssetEventSystems), From 570f81fe6e62057b930b0d24f3e8d535b6ad6728 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 17 Oct 2025 08:52:28 +0100 Subject: [PATCH 08/84] Added layout and scale context to TextContext, made Font's field's pub --- crates/bevy_text/src/context.rs | 6 ++- crates/bevy_text/src/font.rs | 4 +- crates/bevy_text/src/layout.rs | 74 +++++++++++++++++++++++++++++++++ crates/bevy_text/src/lib.rs | 4 ++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 crates/bevy_text/src/layout.rs diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs index 21594c87b3612..905bb0c8bf99a 100644 --- a/crates/bevy_text/src/context.rs +++ b/crates/bevy_text/src/context.rs @@ -1,7 +1,11 @@ use bevy_ecs::resource::Resource; +use parley::swash::scale::ScaleContext; use parley::FontContext; +use parley::LayoutContext; -#[derive(Resource)] +#[derive(Resource, Default)] pub struct TextContext { pub font_cx: FontContext, + pub layout_cx: LayoutContext, + pub scale_cx: ScaleContext, } diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index b9613990c7558..0a417288f8776 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -23,8 +23,8 @@ use parley::fontique::FontInfo; /// Bevy currently loads a single font face as a single `Font` asset. #[derive(Debug, TypePath, Clone, Asset)] pub struct Font { - blob: Blob, - collection: Vec<(FamilyId, Vec)>, + pub blob: Blob, + pub collection: Vec<(FamilyId, Vec)>, } impl Font { diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs new file mode 100644 index 0000000000000..3b97683b9dd7c --- /dev/null +++ b/crates/bevy_text/src/layout.rs @@ -0,0 +1,74 @@ +use crate::context::TextContext; +use crate::ComputedTextBlock; +use crate::Font; +use crate::Justify; +use crate::LineBreak; +use crate::TextBounds; +use crate::TextFont; +use bevy_asset::Assets; +use bevy_color::Color; +use parley::swash::FontRef; +use parley::FontFamily; +use parley::FontStack; +use parley::LineHeight; +use parley::StyleProperty; + +pub fn update_buffer( + fonts: &Assets, + text: String, + text_font: TextFont, + linebreak: LineBreak, + justify: Justify, + bounds: TextBounds, + scale_factor: f32, + context: &mut TextContext, +) { + let font = fonts.get(text_font.font.id()).unwrap(); + let (family_id, info) = &font.collection[0]; + + let TextContext { + font_cx, + layout_cx, + scale_cx, + } = context; + + let family_name = font_cx + .collection + .family_name(*family_id) + .unwrap() + .to_string(); + + let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); + + builder.push_default(StyleProperty::FontSize(text_font.font_size)); + builder.push_default(LineHeight::Absolute( + text_font.line_height.eval(text_font.font_size), + )); + + let stack = FontStack::from(family_name.as_str()); + builder.push_default(stack); + + let layout = builder.build(&text); + + for line in layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let style = glyph_run.style(); + + let run = glyph_run.run(); + let font = run.font(); + let font_size = run.font_size(); + let normalized_coords = run.normalized_coords(); + + // Convert from parley::Font to swash::FontRef + let font_ref = + FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap(); + } + parley::PositionedLayoutItem::InlineBox(positioned_inline_box) => {} + } + } + } +} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 748ed69a91053..460027e1569ed 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -39,6 +39,7 @@ mod font_atlas; mod font_atlas_set; mod font_loader; mod glyph; +mod layout; mod pipeline; mod text; mod text_access; @@ -68,6 +69,8 @@ use bevy_app::prelude::*; use bevy_asset::{AssetApp, AssetEventSystems}; use bevy_ecs::prelude::*; +use crate::context::TextContext; + /// The raw data for the default font used by `bevy_text` #[cfg(feature = "default_font")] pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); @@ -101,6 +104,7 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( PostUpdate, register_font_assets_system From c355a170e028c14c9ca0a9c82482466fbd983c4a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 17 Oct 2025 18:13:11 +0100 Subject: [PATCH 09/84] Renamed `Justify` to `TextAlign`. New variants `Start` and `End`. Replaced cosmic CacheKey with dummy GlyphCacheKey. New Parley Context wrapper resources `FontCx`, `LayoutCx`. New Swash context wrapper resource `ScaleCx`. --- crates/bevy_text/Cargo.toml | 2 +- crates/bevy_text/src/context.rs | 20 ++++++++----- crates/bevy_text/src/error.rs | 5 ++-- crates/bevy_text/src/font.rs | 9 ++---- crates/bevy_text/src/font_atlas.rs | 13 +++++---- crates/bevy_text/src/font_atlas_set.rs | 4 +-- crates/bevy_text/src/font_loader.rs | 4 +-- crates/bevy_text/src/layout.rs | 10 +++---- crates/bevy_text/src/lib.rs | 9 ++++-- crates/bevy_text/src/pipeline.rs | 6 ++-- crates/bevy_text/src/text.rs | 29 +++++++++++-------- examples/2d/sprite_scale.rs | 4 +-- examples/2d/sprite_slice.rs | 2 +- examples/2d/text2d.rs | 8 ++--- examples/2d/texture_atlas.rs | 2 +- examples/3d/tonemapping.rs | 2 +- examples/animation/animated_ui.rs | 2 +- examples/animation/animation_graph.rs | 2 +- examples/animation/animation_masks.rs | 2 +- .../external_source_external_thread.rs | 2 +- examples/ecs/one_shot_systems.rs | 2 +- examples/math/render_primitives.rs | 2 +- examples/mobile/src/lib.rs | 2 +- examples/stress_tests/many_glyphs.rs | 2 +- examples/stress_tests/many_text2d.rs | 4 +-- examples/stress_tests/text_pipeline.rs | 2 +- examples/testbed/2d.rs | 10 +++---- examples/testbed/ui.rs | 2 +- examples/time/virtual_time.rs | 4 +-- examples/ui/directional_navigation.rs | 2 +- examples/ui/display_and_visibility.rs | 8 ++--- examples/ui/size_constraints.rs | 2 +- examples/ui/text.rs | 2 +- examples/ui/text_background_colors.rs | 2 +- examples/ui/text_debug.rs | 8 ++--- examples/ui/text_wrap_debug.rs | 2 +- 36 files changed, 103 insertions(+), 90 deletions(-) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 7397122ea0883..c398e5a40d8ef 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -30,13 +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.14", 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.6.0" } +swash = { version = "0.2.6", default-features = false } [lints] workspace = true diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs index 905bb0c8bf99a..2404fc73d7d61 100644 --- a/crates/bevy_text/src/context.rs +++ b/crates/bevy_text/src/context.rs @@ -1,11 +1,17 @@ +use bevy_derive::Deref; +use bevy_derive::DerefMut; use bevy_ecs::resource::Resource; -use parley::swash::scale::ScaleContext; use parley::FontContext; use parley::LayoutContext; -#[derive(Resource, Default)] -pub struct TextContext { - pub font_cx: FontContext, - pub layout_cx: LayoutContext, - pub scale_cx: ScaleContext, -} +/// 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); + +/// Text scaler context +#[derive(Resource, Default, Deref, DerefMut)] +pub struct ScaleCx(pub LayoutContext); diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index ef9f7ea590deb..56c7eddbce680 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)] @@ -12,6 +11,6 @@ pub enum TextError { #[error("failed to add glyph to newly-created atlas {0:?}")] FailedToAddGlyph(u16), /// Failed to get scaled glyph image for cache key - #[error("failed to get scaled glyph image for cache key: {0:?}")] - FailedToGetGlyphImage(CacheKey), + #[error("failed to get scaled glyph image for cache key")] + FailedToGetGlyphImage, } diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 0a417288f8776..c1583793a6246 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,4 +1,4 @@ -use crate::context::TextContext; +use crate::context::FontCx; use bevy_asset::Asset; use bevy_asset::AssetEvent; use bevy_asset::Assets; @@ -38,7 +38,7 @@ impl Font { } pub fn register_font_assets_system( - mut cx: ResMut, + mut cx: ResMut, mut fonts: ResMut>, mut events: MessageReader>, ) { @@ -46,10 +46,7 @@ pub fn register_font_assets_system( match event { AssetEvent::Added { id } => { if let Some(font) = fonts.get_mut(*id) { - let collection = cx - .font_cx - .collection - .register_fonts(font.blob.clone(), None); + let collection = cx.collection.register_fonts(font.blob.clone(), None); font.collection = collection; } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index d340345c2c87c..502db7077079c 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -6,6 +6,9 @@ use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError}; +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct GlyphCacheKey; + /// 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 +25,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 +62,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 +86,7 @@ impl FontAtlas { &mut self, textures: &mut Assets, atlas_layouts: &mut Assets, - cache_key: cosmic_text::CacheKey, + cache_key: GlyphCacheKey, texture: &Image, offset: IVec2, ) -> Result<(), TextError> { @@ -242,7 +245,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..3a52897f3144b 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,4 +1,4 @@ -use crate::{Font, FontAtlas, FontSmoothing, TextFont}; +use crate::{Font, FontAtlas, FontSmoothing, GlyphCacheKey, SwashCache, TextFont}; use bevy_asset::{AssetEvent, AssetId}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{message::MessageReader, resource::Resource, system::ResMut}; @@ -26,7 +26,7 @@ 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))) } diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index ac5a526fb2c6b..4b6a056196228 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -11,8 +11,8 @@ pub struct FontLoader; #[derive(Debug, Error)] pub enum FontLoaderError { /// The contents that could not be parsed - #[error(transparent)] - Content(#[from] cosmic_text::ttf_parser::FaceParsingError), + #[error("Failed to parse font.")] + Content, /// An [IO](std::io) Error #[error(transparent)] Io(#[from] std::io::Error), diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 3b97683b9dd7c..214ea1f1454e0 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -1,8 +1,8 @@ -use crate::context::TextContext; +use crate::context::FontCx; use crate::ComputedTextBlock; use crate::Font; -use crate::Justify; use crate::LineBreak; +use crate::TextAlign; use crate::TextBounds; use crate::TextFont; use bevy_asset::Assets; @@ -18,15 +18,15 @@ pub fn update_buffer( text: String, text_font: TextFont, linebreak: LineBreak, - justify: Justify, + justify: TextAlign, bounds: TextBounds, scale_factor: f32, - context: &mut TextContext, + context: &mut FontCx, ) { let font = fonts.get(text_font.font.id()).unwrap(); let (family_id, info) = &font.collection[0]; - let TextContext { + let FontCx { font_cx, layout_cx, scale_cx, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 460027e1569ed..2f74714052129 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -45,6 +45,7 @@ mod text; mod text_access; pub use bounds::*; +pub use context::*; pub use error::*; pub use font::*; pub use font_atlas::*; @@ -61,7 +62,7 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, Justify, LineBreak, TextColor, TextError, TextFont, TextLayout, TextSpan, + Font, LineBreak, TextAlign, TextColor, TextError, TextFont, TextLayout, TextSpan, }; } @@ -69,7 +70,7 @@ use bevy_app::prelude::*; use bevy_asset::{AssetApp, AssetEventSystems}; use bevy_ecs::prelude::*; -use crate::context::TextContext; +use crate::context::FontCx; /// The raw data for the default font used by `bevy_text` #[cfg(feature = "default_font")] @@ -104,7 +105,9 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() .add_systems( PostUpdate, register_font_assets_system diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 35c97ad26c75d..0356376b2f7a9 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -17,7 +17,7 @@ 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, PositionedGlyph, TextBounds, + FontAtlasKey, FontAtlasSet, FontSmoothing, LineBreak, PositionedGlyph, TextAlign, TextBounds, TextEntity, TextFont, TextLayout, }; @@ -89,7 +89,7 @@ impl TextPipeline { fonts: &Assets, text_spans: impl Iterator, linebreak: LineBreak, - justify: Justify, + justify: TextAlign, bounds: TextBounds, scale_factor: f64, computed: &mut ComputedTextBlock, @@ -202,7 +202,7 @@ impl TextPipeline { // 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 { + if bounds.width.is_none() && justify != TextAlign::Left { let dimensions = buffer_dimensions(buffer); // `set_size` causes a re-layout to occur. buffer.set_size(font_system, Some(dimensions.x), bounds.height); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index e4da3288d43c0..ac93af8e47fd3 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -5,7 +5,6 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_reflect::prelude::*; use bevy_utils::{default, once}; -use cosmic_text::{Buffer, Metrics}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use tracing::warn; @@ -116,19 +115,19 @@ impl Default for ComputedTextBlock { pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. - pub justify: Justify, + pub justify: TextAlign, /// How the text should linebreak when running out of the bounds determined by `max_size`. pub linebreak: LineBreak, } impl TextLayout { /// Makes a new [`TextLayout`]. - pub const fn new(justify: Justify, linebreak: LineBreak) -> Self { + pub const fn new(justify: TextAlign, linebreak: LineBreak) -> Self { Self { justify, linebreak } } /// Makes a new [`TextLayout`] with the specified [`Justify`]. - pub fn new_with_justify(justify: Justify) -> Self { + pub fn new_with_justify(justify: TextAlign) -> Self { Self::default().with_justify(justify) } @@ -144,7 +143,7 @@ impl TextLayout { } /// Returns this [`TextLayout`] with the specified [`Justify`]. - pub const fn with_justify(mut self, justify: Justify) -> Self { + pub const fn with_justify(mut self, justify: TextAlign) -> Self { self.justify = justify; self } @@ -215,7 +214,7 @@ impl From for TextSpan { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] #[doc(alias = "JustifyText")] -pub enum Justify { +pub enum TextAlign { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] @@ -230,15 +229,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 { - fn from(justify: Justify) -> Self { +impl From for parley::Alignment { + fn from(justify: TextAlign) -> 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, + TextAlign::Start => parley::Alignment::Start, + TextAlign::End => parley::Alignment::End, + TextAlign::Left => parley::Alignment::Left, + TextAlign::Center => parley::Alignment::Center, + TextAlign::Right => parley::Alignment::Right, + TextAlign::Justified => parley::Alignment::Justify, } } } diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index aff8ab3b1be46..434cf762775db 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -123,7 +123,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res) { rect.transform, children![( Text2d::new(rect.text), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, @@ -265,7 +265,7 @@ fn setup_texture_atlas( sprite_sheet.transform, children![( Text2d::new(sprite_sheet.text), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 91918b1d66091..ec459bc82ffff 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -94,7 +94,7 @@ fn spawn_sprites( children![( Text2d::new(label), text_style, - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), bevy::sprite::Anchor::TOP_CENTER, )], diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index c51be59a1d1ac..e26d37fb1bd62 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 50.0, ..default() }; - let text_justification = Justify::Center; + let text_justification = TextAlign::Center; commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( @@ -86,7 +86,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(Unicode linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(Justify::Left, LineBreak::WordBoundary), + TextLayout::new(TextAlign::Left, LineBreak::WordBoundary), // Wrap text in the rectangle TextBounds::from(box_size), // Ensure the text is drawn on top of the box @@ -107,7 +107,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(Justify::Left, LineBreak::AnyCharacter), + TextLayout::new(TextAlign::Left, LineBreak::AnyCharacter), // Wrap text in the rectangle TextBounds::from(other_box_size), // Ensure the text is drawn on top of the box @@ -126,7 +126,7 @@ fn setup(mut commands: Commands, asset_server: Res) { slightly_smaller_text_font .clone() .with_font_smoothing(FontSmoothing::None), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)), // Add a black shadow to the text Text2dShadow::default(), diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 5380fc393b875..8555487c83fa6 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -279,7 +279,7 @@ fn create_label( commands.spawn(( Text2d::new(text), text_style, - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Transform { translation: Vec3::new(translation.0, translation.1, translation.2), ..default() diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 5d8b19e4367e1..fd05a19a1802f 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -181,7 +181,7 @@ fn setup_image_viewer_scene( ..default() }, TextColor(Color::BLACK), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Node { align_self: AlignSelf::Center, margin: UiRect::all(auto()), diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index 8243592034abc..0f781c07f18e1 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -147,7 +147,7 @@ fn setup( ..default() }, TextColor(Color::Srgba(Srgba::RED)), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), animation_target_id, AnimatedBy(player), animation_target_name, diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index bc4b8b2bdf3a1..e931e76e75ffc 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -283,7 +283,7 @@ fn setup_node_rects(commands: &mut Commands) { ..default() }, TextColor(ANTIQUE_WHITE.into()), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), )) .id(); diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs index 4566d06034e33..e685f3b0d660b 100644 --- a/examples/animation/animation_masks.rs +++ b/examples/animation/animation_masks.rs @@ -268,7 +268,7 @@ fn new_mask_group_control(label: &str, width: Val, mask_group_id: u32) -> impl B } else { selected_button_text_style.clone() }, - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Node { flex_grow: 1.0, margin: UiRect::vertical(px(3)), diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 34ac20cab41a6..990a9e9de9dcf 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -54,7 +54,7 @@ fn spawn_text(mut commands: Commands, mut reader: MessageReader) for (per_frame, message) in reader.read().enumerate() { commands.spawn(( Text2d::new(message.0.to_string()), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Transform::from_xyz(per_frame as f32 * 100.0, 300.0, 0.0), )); } diff --git a/examples/ecs/one_shot_systems.rs b/examples/ecs/one_shot_systems.rs index 59b8dedb045f6..1888d2d9ebe75 100644 --- a/examples/ecs/one_shot_systems.rs +++ b/examples/ecs/one_shot_systems.rs @@ -93,7 +93,7 @@ fn setup_ui(mut commands: Commands) { commands.spawn(Camera2d); commands.spawn(( Text::default(), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, diff --git a/examples/math/render_primitives.rs b/examples/math/render_primitives.rs index b33700ea18157..767caa2042baa 100644 --- a/examples/math/render_primitives.rs +++ b/examples/math/render_primitives.rs @@ -352,7 +352,7 @@ fn setup_text(mut commands: Commands, cameras: Query<(Entity, &Camera)>) { children![( Text::default(), HeaderText, - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), children![ TextSpan::new("Primitive: "), TextSpan(format!("{text}", text = PrimitiveSelected::default())), diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index b70da4c672a0f..04e23303c9410 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -158,7 +158,7 @@ fn setup_scene( ..default() }, TextColor::BLACK, - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), )); } diff --git a/examples/stress_tests/many_glyphs.rs b/examples/stress_tests/many_glyphs.rs index e3db576468506..ea7a4bbf8229c 100644 --- a/examples/stress_tests/many_glyphs.rs +++ b/examples/stress_tests/many_glyphs.rs @@ -71,7 +71,7 @@ fn setup(mut commands: Commands, args: Res) { ..Default::default() }; let text_block = TextLayout { - justify: Justify::Left, + justify: TextAlign::Left, linebreak: LineBreak::AnyCharacter, }; diff --git a/examples/stress_tests/many_text2d.rs b/examples/stress_tests/many_text2d.rs index d3646c8d2c45d..a54ad8445d4b0 100644 --- a/examples/stress_tests/many_text2d.rs +++ b/examples/stress_tests/many_text2d.rs @@ -134,9 +134,9 @@ fn setup(mut commands: Commands, font: Res, args: Res) { random_text_font(&mut rng, &args, font.0.clone()), TextColor(color.into()), TextLayout::new_with_justify(if args.center { - Justify::Center + TextAlign::Center } else { - Justify::Left + TextAlign::Left }), Transform { translation, diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index f2c43cb162978..b548b8faf1ef6 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -65,7 +65,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { .spawn(( Text2d::default(), TextLayout { - justify: Justify::Center, + justify: TextAlign::Center, linebreak: LineBreak::AnyCharacter, }, TextBounds::default(), diff --git a/examples/testbed/2d.rs b/examples/testbed/2d.rs index 0a10ee04e408d..48d54e97ff56f 100644 --- a/examples/testbed/2d.rs +++ b/examples/testbed/2d.rs @@ -148,10 +148,10 @@ mod text { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Text))); for (i, justify) in [ - Justify::Left, - Justify::Right, - Justify::Center, - Justify::Justified, + TextAlign::Left, + TextAlign::Right, + TextAlign::Center, + TextAlign::Justified, ] .into_iter() .enumerate() @@ -193,7 +193,7 @@ mod text { fn spawn_anchored_text( commands: &mut Commands, dest: Vec3, - justify: Justify, + justify: TextAlign, bounds: Option, ) { commands.spawn(( diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 6dc2cbe6d7933..7133123378a07 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -482,7 +482,7 @@ mod text_wrap { for (j, message) in messages.into_iter().enumerate() { commands.entity(root).with_child(( Text(message.clone()), - TextLayout::new(Justify::Left, linebreak), + TextLayout::new(TextAlign::Left, linebreak), BackgroundColor(Color::srgb(0.8 - j as f32 * 0.3, 0., 0.)), )); } diff --git a/examples/time/virtual_time.rs b/examples/time/virtual_time.rs index c9c6bd112b65a..ffbdf198e3667 100644 --- a/examples/time/virtual_time.rs +++ b/examples/time/virtual_time.rs @@ -102,7 +102,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut time: ResMu ..default() }, TextColor(Color::srgb(0.85, 0.85, 0.85)), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), ), ( Text::default(), @@ -111,7 +111,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut time: ResMu ..default() }, TextColor(virtual_color), - TextLayout::new_with_justify(Justify::Right), + TextLayout::new_with_justify(TextAlign::Right), VirtualTime, ), ], diff --git a/examples/ui/directional_navigation.rs b/examples/ui/directional_navigation.rs index e0f303290cd09..320b86a986436 100644 --- a/examples/ui/directional_navigation.rs +++ b/examples/ui/directional_navigation.rs @@ -186,7 +186,7 @@ fn setup_ui( Text::new(button_name), // And center the text if it flows onto multiple lines TextLayout { - justify: Justify::Center, + justify: TextAlign::Center, ..default() }, )) diff --git a/examples/ui/display_and_visibility.rs b/examples/ui/display_and_visibility.rs index ce96648c88636..ce37d97f1afd4 100644 --- a/examples/ui/display_and_visibility.rs +++ b/examples/ui/display_and_visibility.rs @@ -96,7 +96,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(( Text::new("Use the panel on the right to change the Display and Visibility properties for the respective nodes of the panel on the left"), text_font.clone(), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Node { margin: UiRect::bottom(px(10)), ..Default::default() @@ -150,13 +150,13 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("Display::None\nVisibility::Hidden\nVisibility::Inherited"), text_font.clone(), TextColor(HIDDEN_COLOR), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), )); builder.spawn(( Text::new("-\n-\n-"), text_font.clone(), TextColor(DARK_GRAY.into()), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), )); builder.spawn((Text::new("The UI Node and its descendants will not be visible and will not be allotted any space in the UI layout.\nThe UI Node will not be visible but will still occupy space in the UI layout.\nThe UI node will inherit the visibility property of its parent. If it has no parent it will be visible."), text_font)); }); @@ -393,7 +393,7 @@ where builder.spawn(( Text(format!("{}::{:?}", Target::::NAME, T::default())), text_font, - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), )); }); } diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index 585ed321912c2..1431c9db9cbac 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -251,7 +251,7 @@ fn spawn_button( } else { UNHOVERED_TEXT_COLOR }), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), )); }); } diff --git a/examples/ui/text.rs b/examples/ui/text.rs index ec0f6185efc04..e3b24cfbaea57 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, TextShadow::default(), // Set the justification of the Text - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), // Set the style of the Node itself. Node { position_type: PositionType::Absolute, diff --git a/examples/ui/text_background_colors.rs b/examples/ui/text_background_colors.rs index 8f32c8d29e85b..92f932b97d6af 100644 --- a/examples/ui/text_background_colors.rs +++ b/examples/ui/text_background_colors.rs @@ -43,7 +43,7 @@ fn setup(mut commands: Commands) { .spawn(( Text::default(), TextLayout { - justify: Justify::Center, + justify: TextAlign::Center, ..Default::default() }, )) diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 84e4db4e52b43..4645b91ff80e7 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -72,7 +72,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(YELLOW.into()), - TextLayout::new_with_justify(Justify::Right), + TextLayout::new_with_justify(TextAlign::Right), Node { max_width: px(300), ..default() @@ -114,7 +114,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(Color::srgb(0.8, 0.2, 0.7)), - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Node { max_width: px(400), ..default() @@ -130,7 +130,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(YELLOW.into()), - TextLayout::new_with_justify(Justify::Left), + TextLayout::new_with_justify(TextAlign::Left), Node { max_width: px(300), ..default() @@ -145,7 +145,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { font_size: 29.0, ..default() }, - TextLayout::new_with_justify(Justify::Justified), + TextLayout::new_with_justify(TextAlign::Justified), TextColor(GREEN_YELLOW.into()), Node { max_width: px(300), diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 212a820abca75..1a4aea1bfa634 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -116,7 +116,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { commands.entity(column_id).with_child(( Text(message.clone()), text_font.clone(), - TextLayout::new(Justify::Left, linebreak), + TextLayout::new(TextAlign::Left, linebreak), BackgroundColor(Color::srgb(0.8 - j as f32 * 0.2, 0., 0.)), )); } From 0b9cd59e79c9409b3a99ec3bb3e9906ae24f3a4e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 17 Oct 2025 22:54:13 +0100 Subject: [PATCH 10/84] Implement some sort of wretched text layout --- crates/bevy_text/Cargo.toml | 4 +- crates/bevy_text/src/context.rs | 3 +- crates/bevy_text/src/font_atlas.rs | 80 ++-- crates/bevy_text/src/font_atlas_set.rs | 31 +- crates/bevy_text/src/layout.rs | 188 +++++--- crates/bevy_text/src/lib.rs | 14 +- crates/bevy_text/src/pipeline.rs | 576 ------------------------- crates/bevy_text/src/text.rs | 217 +--------- 8 files changed, 202 insertions(+), 911 deletions(-) delete mode 100644 crates/bevy_text/src/pipeline.rs diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index c398e5a40d8ef..5cc4f4aaa178b 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -35,8 +35,8 @@ 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.6.0" } -swash = { version = "0.2.6", default-features = false } +parley = { version = "0.6.0", default-features = true } +swash = { version = "0.2.6", default-features = true } [lints] workspace = true diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs index 2404fc73d7d61..b9d6071459d65 100644 --- a/crates/bevy_text/src/context.rs +++ b/crates/bevy_text/src/context.rs @@ -3,6 +3,7 @@ use bevy_derive::DerefMut; use bevy_ecs::resource::Resource; use parley::FontContext; use parley::LayoutContext; +use swash::scale::ScaleContext; /// Font context #[derive(Resource, Default, Deref, DerefMut)] @@ -14,4 +15,4 @@ pub struct LayoutCx(pub LayoutContext); /// Text scaler context #[derive(Resource, Default, Deref, DerefMut)] -pub struct ScaleCx(pub LayoutContext); +pub struct ScaleCx(pub ScaleContext); diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 502db7077079c..b0a230ade3bfe 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -2,12 +2,16 @@ 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; +pub struct GlyphCacheKey { + pub glyph_id: u16, +} /// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`. /// @@ -86,7 +90,7 @@ impl FontAtlas { &mut self, textures: &mut Assets, atlas_layouts: &mut Assets, - cache_key: GlyphCacheKey, + key: GlyphCacheKey, texture: &Image, offset: IVec2, ) -> Result<(), TextError> { @@ -98,7 +102,7 @@ impl FontAtlas { .add_texture(atlas_layout, texture, atlas_texture) { self.glyph_to_atlas_index.insert( - cache_key, + key, GlyphAtlasLocation { glyph_index, offset, @@ -106,7 +110,7 @@ impl FontAtlas { ); Ok(()) } else { - Err(TextError::FailedToAddGlyph(cache_key.glyph_id)) + Err(TextError::FailedToAddGlyph(key.glyph_id)) } } } @@ -127,20 +131,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)?; 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, ) @@ -167,21 +167,19 @@ pub fn add_glyph_to_atlas( font_atlases.last_mut().unwrap().add_glyph( textures, texture_atlases, - physical_glyph.cache_key, + GlyphCacheKey { glyph_id }, &glyph_texture, offset, )?; } - Ok(get_glyph_atlas_info(font_atlases, physical_glyph.cache_key).unwrap()) + Ok(get_glyph_atlas_info(font_atlases, GlyphCacheKey { glyph_id }).unwrap()) } /// 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, - font_smoothing: FontSmoothing, + scaler: &mut Scaler, + glyph_id: u16, ) -> 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 @@ -191,40 +189,20 @@ pub fn get_outlined_glyph_texture( // 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 cosmic_text::Placement { - left, - top, - width, - height, - } = image.placement; + 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)?; - 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() - } - } - cosmic_text::SwashContent::Color => image.data, - cosmic_text::SwashContent::SubpixelMask => { - // TODO: implement - todo!() - } - }; + let left = image.placement.left; + let top = image.placement.top; + let width = image.placement.width; + let height = image.placement.height; Ok(( Image::new( @@ -234,7 +212,7 @@ pub fn get_outlined_glyph_texture( depth_or_array_layers: 1, }, TextureDimension::D2, - data, + image.data, TextureFormat::Rgba8UnormSrgb, RenderAssetUsages::MAIN_WORLD, ), diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 3a52897f3144b..c3bc9de21a427 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,22 +1,19 @@ -use crate::{Font, FontAtlas, FontSmoothing, GlyphCacheKey, SwashCache, 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::fontique; /// 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 fontique::SourceId, 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(info: &fontique::FontInfo, size: f32, smoothing: FontSmoothing) -> Self { + Self(info.source().id(), info.index(), size.to_bits(), smoothing) } } @@ -31,15 +28,3 @@ impl FontAtlasSet { .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/layout.rs b/crates/bevy_text/src/layout.rs index 214ea1f1454e0..fb85845f486ad 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -1,74 +1,162 @@ -use crate::context::FontCx; -use crate::ComputedTextBlock; -use crate::Font; -use crate::LineBreak; -use crate::TextAlign; -use crate::TextBounds; -use crate::TextFont; +use crate::add_glyph_to_atlas; +use crate::get_glyph_atlas_info; +use crate::FontAtlas; +use crate::FontAtlasKey; +use crate::FontAtlasSet; +use crate::FontSmoothing; +use crate::GlyphCacheKey; +use crate::TextLayoutInfo; use bevy_asset::Assets; -use bevy_color::Color; +use bevy_image::Image; +use bevy_image::TextureAtlasLayout; +use bevy_math::UVec2; use parley::swash::FontRef; -use parley::FontFamily; +use parley::Alignment; +use parley::AlignmentOptions; +use parley::Brush; +use parley::FontContext; use parley::FontStack; +use parley::Layout; +use parley::LayoutContext; use parley::LineHeight; +use parley::PositionedLayoutItem; use parley::StyleProperty; +use std::ops::Range; +use swash::scale::ScaleContext; -pub fn update_buffer( - fonts: &Assets, - text: String, - text_font: TextFont, - linebreak: LineBreak, - justify: TextAlign, - bounds: TextBounds, - scale_factor: f32, - context: &mut FontCx, -) { - let font = fonts.get(text_font.font.id()).unwrap(); - let (family_id, info) = &font.collection[0]; - - let FontCx { - font_cx, - layout_cx, - scale_cx, - } = context; - - let family_name = font_cx - .collection - .family_name(*family_id) - .unwrap() - .to_string(); +fn concat_text_for_layout<'a>( + text_sections: impl Iterator, +) -> (String, Vec>) { + let mut out = String::new(); + let mut ranges = Vec::new(); + + for text_section in text_sections { + let start = out.len(); + out.push_str(text_section); + let end = out.len(); + ranges.push(start..end); + } + + (out, ranges) +} +pub struct TextSectionStyle<'a> { + font_family: &'a str, + font_size: f32, + line_height: LineHeight, +} + +pub fn build_layout_from_text_sections<'a, B: Brush>( + font_cx: &'a mut FontContext, + layout_cx: &'a mut LayoutContext, + text_sections: impl Iterator, + text_section_styles: impl Iterator>, + scale_factor: f32, +) -> Layout { + let (text, section_ranges) = concat_text_for_layout(text_sections); let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); + for (style, range) in text_section_styles.zip(section_ranges) { + builder.push(FontStack::from(style.font_family), range.clone()); + builder.push(StyleProperty::FontSize(style.font_size), range.clone()); + builder.push(style.line_height, range); + } + builder.build(&text) +} - builder.push_default(StyleProperty::FontSize(text_font.font_size)); - builder.push_default(LineHeight::Absolute( - text_font.line_height.eval(text_font.font_size), - )); +pub fn build_text_layout_info( + mut layout: Layout, + max_advance: Option, + alignment: Alignment, + scale_cx: &mut ScaleContext, + font_cx: &mut FontContext, + font_atlas_set: &mut FontAtlasSet, + font_atlases: &mut Vec, + texture_atlases: &mut Assets, + textures: &mut Assets, + font_smoothing: FontSmoothing, +) -> TextLayoutInfo { + layout.break_all_lines(max_advance); + layout.align(None, alignment, AlignmentOptions::default()); - let stack = FontStack::from(family_name.as_str()); - builder.push_default(stack); + let mut info = TextLayoutInfo::default(); - let layout = builder.build(&text); + info.scale_factor = layout.scale(); + info.size = (layout.width(), layout.height()).into(); for line in layout.lines() { - for item in line.items() { + for (line_index, item) in line.items().enumerate() { match item { - parley::PositionedLayoutItem::GlyphRun(glyph_run) => { - let mut run_x = glyph_run.offset(); - let run_y = glyph_run.baseline(); - let style = glyph_run.style(); - + PositionedLayoutItem::GlyphRun(glyph_run) => { let run = glyph_run.run(); let font = run.font(); let font_size = run.font_size(); - let normalized_coords = run.normalized_coords(); - - // Convert from parley::Font to swash::FontRef + let coords = run.normalized_coords(); let font_ref = FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap(); + + let font_collection = + font_cx.collection.register_fonts(font.data.clone(), None); + + let (_family_id, font_info) = font_collection + .into_iter() + .find_map(|(fid, faces)| { + faces + .into_iter() + .find(|fi| fi.index() == font.index) + .map(|fi| (fid, fi)) + }) + .unwrap(); + + let font_atlas_key = FontAtlasKey::new(&font_info, font_size, font_smoothing); + + let mut scaler = scale_cx + .builder(font_ref) + .size(font_size) + .hint(true) + .normalized_coords(coords) + .build(); + + 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(|| { + add_glyph_to_atlas( + font_atlases, + texture_atlases, + textures, + &mut scaler, + font_smoothing, + glyph.id as u16, + ) + }) 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()); + info.glyphs.push(crate::PositionedGlyph { + position: (glyph.x, glyph.y).into(), + size: glyph_size.as_vec2(), + atlas_info, + span_index: 0, + line_index, + byte_index: line.text_range().start, + byte_length: line.text_range().len(), + }); + } } - parley::PositionedLayoutItem::InlineBox(positioned_inline_box) => {} + _ => {} } } } + + info } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 2f74714052129..4e616bf394dd9 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -40,7 +40,6 @@ mod font_atlas_set; mod font_loader; mod glyph; mod layout; -mod pipeline; mod text; mod text_access; @@ -52,7 +51,6 @@ pub use font_atlas::*; pub use font_atlas_set::*; pub use font_loader::*; pub use glyph::*; -pub use pipeline::*; pub use text::*; pub use text_access::*; @@ -70,8 +68,6 @@ use bevy_app::prelude::*; use bevy_asset::{AssetApp, AssetEventSystems}; use bevy_ecs::prelude::*; -use crate::context::FontCx; - /// The raw data for the default font used by `bevy_text` #[cfg(feature = "default_font")] pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); @@ -101,9 +97,6 @@ impl Plugin for TextPlugin { app.init_asset::() .init_asset_loader::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() .init_resource::() .init_resource::() .init_resource::() @@ -113,12 +106,7 @@ impl Plugin for TextPlugin { register_font_assets_system .in_set(TextSystems::RegisterFontAssets) .after(AssetEventSystems), - ) - .add_systems( - PostUpdate, - free_unused_font_atlases_system.before(AssetEventSystems), - ) - .add_systems(Last, trim_cosmic_cache); + ); #[cfg(feature = "default_font")] { diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs deleted file mode 100644 index 0356376b2f7a9..0000000000000 --- a/crates/bevy_text/src/pipeline.rs +++ /dev/null @@ -1,576 +0,0 @@ -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, LineBreak, PositionedGlyph, TextAlign, 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, -} - -/// 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)>, - /// Buffered vec for collecting info for glyph assembly. - glyph_info: Vec<(AssetId, FontSmoothing)>, -} - -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>( - &mut self, - fonts: &Assets, - text_spans: impl Iterator, - linebreak: LineBreak, - justify: TextAlign, - bounds: TextBounds, - scale_factor: f64, - computed: &mut ComputedTextBlock, - font_system: &mut CosmicFontSystem, - ) -> Result<(), TextError> { - let font_system = &mut font_system.0; - - // 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)> = - core::mem::take(&mut self.spans_buffer) - .into_iter() - .map(|_| -> (usize, &str, &TextFont, FontFaceInfo, Color) { unreachable!() }) - .collect(); - - computed.entities.clear(); - - for (span_index, (entity, depth, span, text_font, color)) in text_spans.enumerate() { - // 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) { - 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(text_font.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)); - } - - 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)| { - ( - *span, - get_attrs(*span_index, text_font, *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()), - ); - - buffer.shape_until_scroll(font_system, false); - - // Workaround for alignment not working for unbounded text. - // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != TextAlign::Left { - let dimensions = buffer_dimensions(buffer); - // `set_size` causes a re-layout to occur. - buffer.set_size(font_system, Some(dimensions.x), bounds.height); - } - - // Recover the spans buffer. - spans.clear(); - self.spans_buffer = spans - .into_iter() - .map(|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) { 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.section_rects.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)); - }); - - 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?; - - 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.section_rects.push(( - computed.entities[section].entity, - Rect::new( - start, - run.line_top, - end, - run.line_top + run.line_height, - ), - )); - 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) - .map(Ok) - .unwrap_or_else(|| { - add_glyph_to_atlas( - font_atlases, - texture_atlases, - textures, - &mut font_system.0, - &mut swash_cache.0, - layout_glyph, - font_smoothing, - ) - })?; - - 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); - - 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.section_rects.push(( - computed.entities[section].entity, - Rect::new(start, run.line_top, end, run.line_top + run.line_height), - )); - } - - 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, - /// Rects bounding the text block's text sections. - /// A text section spanning more than one line will have multiple bounding rects. - pub section_rects: Vec<(Entity, Rect)>, - /// The glyphs resulting size - pub size: Vec2, -} - -/// 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, - 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: text_font.line_height.eval(text_font.font_size), - } - .scale(scale_factor as f32), - ) - .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 ac93af8e47fd3..9ddd2ffe7d4c6 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,23 +1,12 @@ -use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; +use crate::{Font, PositionedGlyph, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; +use bevy_math::{Rect, Vec2}; use bevy_reflect::prelude::*; -use bevy_utils::{default, once}; +use bevy_utils::default; 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`]. /// @@ -31,77 +20,6 @@ pub struct TextEntity { pub depth: usize, } -/// Computed information for a text block. -/// -/// See [`TextLayout`]. -/// -/// Automatically updated by 2d and UI text systems. -#[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]>, - /// Flag set when any change has been made to this block that should cause it to be rerendered. - /// - /// Includes: - /// - [`TextLayout`] changes. - /// - [`TextFont`] or `Text2d`/`Text`/`TextSpan` changes anywhere in the block's entity hierarchy. - // TODO: This encompasses both structural changes like font size or justification and non-structural - // changes like text color and font smoothing. This field currently causes UI to 'remeasure' text, even if - // 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, -} - -impl ComputedTextBlock { - /// Accesses entities in this block. - /// - /// Can be used to look up [`TextFont`] components for glyphs in [`TextLayoutInfo`] using the `span_index` - /// stored there. - pub fn entities(&self) -> &[TextEntity] { - &self.entities - } - - /// Indicates if the text needs to be refreshed in [`TextLayoutInfo`]. - /// - /// Updated automatically by [`detect_text_needs_rerender`] and cleared - /// by [`TextPipeline`](crate::TextPipeline) methods. - 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, - } - } -} - /// Component with text format settings for a block of text. /// /// A block of text is composed of text spans, which each have a separate string value and [`TextFont`]. Text @@ -111,7 +29,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. @@ -430,6 +348,24 @@ pub enum LineBreak { NoWrap, } +/// 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, + /// Rects bounding the text block's text sections. + /// A text section spanning more than one line will have multiple bounding rects. + pub section_rects: Vec<(Entity, Rect)>, + /// The glyphs resulting size + pub size: Vec2, +} + /// Determines which antialiasing method to use when rendering text. By default, text is /// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look. /// @@ -453,112 +389,3 @@ pub enum FontSmoothing { // TODO: Add subpixel antialias support // SubpixelAntiAliased, } - -/// 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. -pub fn detect_text_needs_rerender( - changed_roots: Query< - Entity, - ( - Or<( - Changed, - Changed, - Changed, - Changed, - )>, - With, - With, - With, - ), - >, - changed_spans: Query< - (Entity, Option<&ChildOf>, Has), - ( - Or<( - Changed, - Changed, - Changed, - Changed, // Included to detect broken text block hierarchies. - Added, - )>, - With, - With, - ), - >, - mut computed: Query<( - Option<&ChildOf>, - Option<&mut ComputedTextBlock>, - Has, - )>, -) { - // Root entity: - // - Root component changed. - // - TextFont on root changed. - // - TextLayout changed. - // - Root children changed (can include additions and removals). - for root in changed_roots.iter() { - let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else { - once!(warn!("found entity {} with a root text component ({}) but no ComputedTextBlock; this warning only \ - prints once", root, core::any::type_name::())); - continue; - }; - computed.needs_rerender = true; - } - - // Span entity: - // - Span component changed. - // - Span TextFont changed. - // - Span children changed (can include additions and removals). - for (entity, maybe_span_child_of, has_text_block) in changed_spans.iter() { - if has_text_block { - once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \ - text entities (that have {}); this warning only prints once", - entity, core::any::type_name::())); - } - - let Some(span_child_of) = maybe_span_child_of else { - once!(warn!( - "found entity {} with a TextSpan that has no parent; it should have an ancestor \ - with a root text component ({}); this warning only prints once", - entity, - core::any::type_name::() - )); - continue; - }; - let mut parent: Entity = span_child_of.parent(); - - // Search for the nearest ancestor with ComputedTextBlock. - // Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited - // is outweighed by the expense of tracking visited spans. - loop { - let Ok((maybe_child_of, maybe_computed, has_span)) = computed.get_mut(parent) else { - once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a ChildOf \ - component that points at non-existent entity {}; this warning only prints once", - entity, parent)); - break; - }; - if let Some(mut computed) = maybe_computed { - computed.needs_rerender = true; - break; - } - if !has_span { - once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \ - span component or a ComputedTextBlock component; this warning only prints once", - entity, parent)); - break; - } - let Some(next_child_of) = maybe_child_of else { - once!(warn!( - "found entity {} with a TextSpan that has no ancestor with the root text \ - component ({}); this warning only prints once", - entity, - core::any::type_name::() - )); - break; - }; - parent = next_child_of.parent(); - } - } -} From 0be7d6efc5f1a440aa7a02f11ef4a61628b1ae32 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 18 Oct 2025 16:34:01 +0100 Subject: [PATCH 11/84] Remove old unneeded params. New ComputedTextBlock --- crates/bevy_sprite/src/lib.rs | 5 +- crates/bevy_sprite/src/text2d.rs | 83 ++++++++++++++------------------ crates/bevy_text/src/text.rs | 3 ++ 3 files changed, 41 insertions(+), 50 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index c14aab0c623a1..ff7ce997a573f 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -84,10 +84,7 @@ impl Plugin for SpritePlugin { app.add_systems( 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() diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index c33dfe8fd6c40..6aa4a49119187 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -20,9 +20,8 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet, LineBreak, SwashCache, TextBounds, - TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, - TextSpanAccess, TextWriter, + Font, FontAtlasSet, LineBreak, TextBounds, TextColor, TextError, TextFont, TextLayout, + TextLayoutInfo, TextReader, TextRoot, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -166,7 +165,7 @@ pub fn update_text2d_layout( camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, - mut text_pipeline: ResMut, + mut text_query: Query<( Entity, Option<&RenderLayers>, @@ -176,8 +175,6 @@ pub fn update_text2d_layout( &mut ComputedTextBlock, )>, mut text_reader: Text2dReader, - mut font_system: ResMut, - mut swash_cache: ResMut, ) { target_scale_factors.clear(); target_scale_factors.extend( @@ -218,47 +215,41 @@ pub fn update_text2d_layout( *scale_factor }; - if scale_factor != text_layout_info.scale_factor - || computed.needs_rerender() - || bounds.is_changed() - || (!queue.is_empty() && queue.remove(&entity)) - { - 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_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(_))) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.scale_factor = scale_factor; - text_layout_info.size *= scale_factor.recip(); - } + 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)) => { + panic!("Fatal error when processing text: {e}."); + } + Ok(()) => { + text_layout_info.scale_factor = scale_factor; + text_layout_info.size *= scale_factor.recip(); } } } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 9ddd2ffe7d4c6..726c1fbb0e4a8 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -389,3 +389,6 @@ pub enum FontSmoothing { // TODO: Add subpixel antialias support // SubpixelAntiAliased, } + +#[derive(Component)] +pub struct ComputedTextBlock(Vec); From 62a99c475cac0ce97b9fb6cedcebd4f0ca738d9f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 10:16:48 +0100 Subject: [PATCH 12/84] Removed old tests and most of the old UI text implementation to get the it to compile. --- crates/bevy_sprite/src/text2d.rs | 228 ++--- crates/bevy_sprite_render/src/text2d/mod.rs | 4 +- crates/bevy_text/src/font_atlas.rs | 1 + crates/bevy_text/src/layout.rs | 13 +- crates/bevy_text/src/lib.rs | 3 + crates/bevy_text/src/text.rs | 21 +- crates/bevy_text/src/text_access.rs | 16 +- crates/bevy_text/src/text_hierarchy.rs | 58 ++ crates/bevy_ui/src/layout/mod.rs | 870 -------------------- crates/bevy_ui/src/layout/ui_surface.rs | 193 +---- crates/bevy_ui/src/lib.rs | 2 +- crates/bevy_ui/src/measurement.rs | 2 - crates/bevy_ui/src/widget/text.rs | 321 ++++---- crates/bevy_ui_render/src/lib.rs | 3 +- 14 files changed, 314 insertions(+), 1421 deletions(-) create mode 100644 crates/bevy_text/src/text_hierarchy.rs diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 6aa4a49119187..7796033c7b9b1 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -7,21 +7,23 @@ 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::query::With; 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::{ - Font, FontAtlasSet, LineBreak, TextBounds, TextColor, TextError, TextFont, TextLayout, - TextLayoutInfo, TextReader, TextRoot, TextSpanAccess, TextWriter, + build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, FontAtlasSet, + FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, + TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -98,7 +100,7 @@ impl Text2d { } } -impl TextRoot for Text2d {} +impl TextHead for Text2d {} impl TextSpanAccess for Text2d { fn read_span(&self) -> &str { @@ -158,22 +160,24 @@ 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 textures: ResMut>, - fonts: Res>, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, - - mut text_query: Query<( - Entity, - Option<&RenderLayers>, - Ref, - Ref, - &mut TextLayoutInfo, - &mut ComputedTextBlock, - )>, + mut font_cx: ResMut, + mut layout_cx: ResMut, + mut scale_cx: ResMut, + mut text_query: Query< + ( + Entity, + Option<&RenderLayers>, + Ref, + Ref, + &mut TextLayoutInfo, + &mut ComputedTextBlock, + ), + With, + >, mut text_reader: Text2dReader, ) { target_scale_factors.clear(); @@ -193,9 +197,7 @@ 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, text_layout_info, _computed) in &mut text_query { let entity_mask = maybe_entity_mask.unwrap_or_default(); let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor { @@ -224,34 +226,38 @@ pub fn update_text2d_layout( height: bounds.height.map(|height| height * scale_factor), }; + let mut text_sections: Vec<&str> = Vec::new(); + let mut text_section_styles: Vec = Vec::new(); + for (_, _, text, font, _) in text_reader.iter(entity) { + text_sections.push(text); + text_section_styles.push(TextSectionStyle::new( + font.font.as_str(), + font.font_size, + font.line_height.eval(font.font_size), + )); + } + 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, + + let layout = build_layout_from_text_sections( + &mut font_cx.0, + &mut layout_cx.0, + text_sections.iter().copied(), + text_section_styles.iter().copied(), + scale_factor, + ); + + *text_layout_info = build_text_layout_info( + layout, + bounds.width, + block.justify.into(), + &mut scale_cx, + &mut font_cx, &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)) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.scale_factor = scale_factor; - text_layout_info.size *= scale_factor.recip(); - } - } + bevy_text::FontSmoothing::AntiAliased, + ); } } @@ -291,135 +297,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_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 5dbd603ed21df..7b4eceaeecb4d 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -165,9 +165,9 @@ pub fn extract_text2d_sprite( color = text_colors .get( computed_block - .entities() + .0 .get(*span_index) - .map(|t| t.entity) + .map(|t| *t) .unwrap_or(Entity::PLACEHOLDER), ) .map(|text_color| LinearRgba::from(text_color.0)) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index b0a230ade3bfe..5814ed65d6dd7 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -10,6 +10,7 @@ use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError}; /// Key identifying a glyph #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct GlyphCacheKey { + /// glyh id pub glyph_id: u16, } diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index fb85845f486ad..4fc9095dbc633 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -40,12 +40,23 @@ fn concat_text_for_layout<'a>( (out, ranges) } +#[derive(Clone, Copy, Debug)] pub struct TextSectionStyle<'a> { font_family: &'a str, font_size: f32, line_height: LineHeight, } +impl<'a> TextSectionStyle<'a> { + pub fn new(family: &'a str, size: f32, line_height: f32) -> Self { + Self { + font_family: family, + font_size: size, + line_height: LineHeight::Absolute(line_height), + } + } +} + pub fn build_layout_from_text_sections<'a, B: Brush>( font_cx: &'a mut FontContext, layout_cx: &'a mut LayoutContext, @@ -63,6 +74,7 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( builder.build(&text) } +/// create a TextLayoutInfo pub fn build_text_layout_info( mut layout: Layout, max_advance: Option, @@ -70,7 +82,6 @@ pub fn build_text_layout_info( scale_cx: &mut ScaleContext, font_cx: &mut FontContext, font_atlas_set: &mut FontAtlasSet, - font_atlases: &mut Vec, texture_atlases: &mut Assets, textures: &mut Assets, font_smoothing: FontSmoothing, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 4e616bf394dd9..5d88a5ddac30c 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -42,6 +42,7 @@ mod glyph; mod layout; mod text; mod text_access; +mod text_hierarchy; pub use bounds::*; pub use context::*; @@ -51,8 +52,10 @@ pub use font_atlas::*; pub use font_atlas_set::*; pub use font_loader::*; pub use glyph::*; +pub use layout::*; pub use text::*; pub use text_access::*; +pub use text_hierarchy::*; /// The text prelude. /// diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 726c1fbb0e4a8..3d84958e486ab 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -178,7 +178,7 @@ pub struct TextFont { /// `FiraMono-subset.ttf` compiled into the library is used. /// * otherwise no text will be rendered, unless a custom font is loaded into the default font /// handle. - pub font: Handle, + pub font: String, /// The vertical height of rasterized glyphs in the font atlas in pixels. /// /// This is multiplied by the window scale factor and `UiScale`, but not the text entity @@ -203,8 +203,8 @@ impl TextFont { } /// Returns this [`TextFont`] with the specified font face handle. - pub fn with_font(mut self, font: Handle) -> Self { - self.font = font; + pub fn with_font(mut self, font: &str) -> Self { + self.font = font.to_string(); self } @@ -227,9 +227,12 @@ impl TextFont { } } -impl From> for TextFont { - fn from(font: Handle) -> Self { - Self { font, ..default() } +impl From<&str> for TextFont { + fn from(font: &str) -> Self { + Self { + font: font.to_string(), + ..default() + } } } @@ -266,7 +269,8 @@ pub enum LineHeight { } impl LineHeight { - pub(crate) fn eval(self, font_size: f32) -> f32 { + /// eval + pub fn eval(self, font_size: f32) -> f32 { match self { LineHeight::Px(px) => px, LineHeight::RelativeToFont(scale) => scale * font_size, @@ -389,6 +393,3 @@ pub enum FontSmoothing { // TODO: Add subpixel antialias support // SubpixelAntiAliased, } - -#[derive(Component)] -pub struct ComputedTextBlock(Vec); diff --git a/crates/bevy_text/src/text_access.rs b/crates/bevy_text/src/text_access.rs index 7de9e8e323b36..8c8cdbc87ea74 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< @@ -73,7 +73,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(); @@ -139,7 +139,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). @@ -166,7 +166,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); fn next(&mut self) -> Option { @@ -211,7 +211,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); @@ -223,7 +223,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< @@ -249,7 +249,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_text/src/text_hierarchy.rs b/crates/bevy_text/src/text_hierarchy.rs new file mode 100644 index 0000000000000..fa01490f3ec8b --- /dev/null +++ b/crates/bevy_text/src/text_hierarchy.rs @@ -0,0 +1,58 @@ +use bevy_derive::Deref; +use bevy_ecs::{prelude::*, relationship::Relationship}; + +use crate::TextSpan; + +#[derive(Component)] +pub struct ComputedTextBlock(pub Vec); + +#[derive(Component)] +/// Root text element +pub struct TextRoot; + +/// Output target id +#[derive(Component, Debug, PartialEq, Deref)] +pub struct TextTarget(Entity); + +impl Default for TextTarget { + fn default() -> Self { + Self(Entity::PLACEHOLDER) + } +} + +/// update text entities lists +pub fn update_text_entities_system( + mut buffer: Local>, + mut root_query: Query<(Entity, &mut ComputedTextBlock), With>, + mut targets_query: Query<&mut TextTarget>, + children_query: Query<&Children, With>, +) { + for (root_id, mut entities) in root_query.iter_mut() { + buffer.push(root_id); + for entity in children_query.iter_descendants_depth_first(root_id) { + buffer.push(entity); + } + if buffer.as_slice() != entities.0.as_slice() { + entities.0.clear(); + entities.0.extend_from_slice(&buffer); + + let mut targets_iter = targets_query.iter_many_mut(entities.0.as_slice()); + while let Some(mut target) = targets_iter.fetch_next() { + target.0 = root_id; + } + } + buffer.clear(); + } +} + +/// detect changes +pub fn detect_text_needs_rerender( + text_query: Query<&TextTarget, Or<(Changed, Changed)>>, + mut output_query: Query<&mut ComputedTextBlock>, +) { + for target in text_query.iter() { + if let Ok(mut computed) = output_query.get_mut(target.0) { + computed.set_changed(); + } + } +} diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 3de05629ff294..42641edcdd5ae 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -21,8 +21,6 @@ use ui_surface::UiSurface; use bevy_text::ComputedTextBlock; -use bevy_text::CosmicFontSystem; - mod convert; pub mod debug; pub(crate) mod ui_surface; @@ -92,7 +90,6 @@ pub fn ui_layout_system( Option<&ScrollPosition>, )>, mut buffer_query: Query<&mut ComputedTextBlock>, - mut font_system: ResMut, 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( @@ -353,869 +349,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 2df6afa947dac..5e323c0c336d0 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -11,7 +11,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 { @@ -186,7 +185,6 @@ impl UiSurface { 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, ) { let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity); @@ -221,7 +219,6 @@ impl UiSurface { height: known_dimensions.height, available_width: available_space.width, available_height: available_space.height, - font_system, buffer, }, style, @@ -288,182 +285,16 @@ pub fn get_text_buffer<'a>( ctx: &mut NodeMeasure, query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>, ) -> Option<&'a mut bevy_text::ComputedTextBlock> { - // We avoid a query lookup whenever the buffer is not required. - if !needs_buffer { - return None; - } - let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else { - return None; - }; - let Ok(computed) = query.get_mut(info.entity) else { - return None; - }; - 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" - ); - } + // // We avoid a query lookup whenever the buffer is not required. + // if !needs_buffer { + // return None; + // } + // let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else { + // return None; + // }; + // let Ok(computed) = query.get_mut(info.entity) else { + // return None; + // }; + // Some(computed.into_inner()) + None } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index fdb9c7a117796..f2abcaada9b44 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -246,7 +246,7 @@ fn build_text_interop(app: &mut App) { .ambiguous_with(widget::update_image_content_size_system), widget::text_system .in_set(UiSystems::PostLayout) - .after(bevy_text::free_unused_font_atlases_system) + //.after(bevy_text::free_unused_font_atlases_system) .before(bevy_asset::AssetEventSystems) // Text2d and bevy_ui text are entirely on separate entities .ambiguous_with(bevy_text::detect_text_needs_rerender::) diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 029498ab8de9d..19457de7569ec 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,7 +19,6 @@ 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>, } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index a84a6689f1f47..984fa6c4e86d9 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -18,9 +18,8 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet, LineBreak, SwashCache, TextBounds, - TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline, - TextReader, TextRoot, TextSpanAccess, TextWriter, + ComputedTextBlock, Font, FontAtlasSet, LineBreak, TextBounds, TextColor, TextError, TextFont, + TextHead, TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; @@ -105,7 +104,7 @@ impl Text { } } -impl TextRoot for Text {} +impl TextHead for Text {} impl TextSpanAccess for Text { fn read_span(&self) -> &str { @@ -158,7 +157,7 @@ pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>; /// Text measurement for UI layout. See [`NodeMeasure`]. pub struct TextMeasure { - pub info: TextMeasureInfo, + //pub info: TextMeasureInfo, } impl TextMeasure { @@ -171,47 +170,47 @@ impl TextMeasure { impl Measure for TextMeasure { fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 { - let MeasureArgs { - width, - height, - available_width, - buffer, - font_system, - .. - } = measure_args; - let x = width.unwrap_or_else(|| match available_width { - AvailableSpace::Definite(x) => { - // It is possible for the "min content width" to be larger than - // the "max content width" when soft-wrapping right-aligned text - // and possibly other situations. + // let MeasureArgs { + // width, + // height, + // available_width, + // buffer, + // .. + // } = measure_args; + // let x = width.unwrap_or_else(|| match available_width { + // AvailableSpace::Definite(x) => { + // // It is possible for the "min content width" to be larger than + // // the "max content width" when soft-wrapping right-aligned text + // // and possibly other situations. - x.max(self.info.min.x).min(self.info.max.x) - } - AvailableSpace::MinContent => self.info.min.x, - AvailableSpace::MaxContent => self.info.max.x, - }); + // x.max(self.info.min.x).min(self.info.max.x) + // } + // AvailableSpace::MinContent => self.info.min.x, + // AvailableSpace::MaxContent => self.info.max.x, + // }); - height - .map_or_else( - || match available_width { - AvailableSpace::Definite(_) => { - if let Some(buffer) = buffer { - self.info.compute_size( - TextBounds::new_horizontal(x), - buffer, - font_system, - ) - } else { - error!("text measure failed, buffer is missing"); - Vec2::default() - } - } - AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), - AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), - }, - |y| Vec2::new(x, y), - ) - .ceil() + // height + // .map_or_else( + // || match available_width { + // AvailableSpace::Definite(_) => { + // if let Some(buffer) = buffer { + // self.info.compute_size( + // TextBounds::new_horizontal(x), + // buffer, + // font_system, + // ) + // } else { + // error!("text measure failed, buffer is missing"); + // Vec2::default() + // } + // } + // AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), + // AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), + // }, + // |y| Vec2::new(x, y), + // ) + // .ceil() + Vec2::ZERO } } @@ -222,40 +221,38 @@ fn create_text_measure<'a>( 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 })); - } + // 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(_))) => { - panic!("Fatal error when processing text: {e}."); - } - }; + // // 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) => { + // panic!("Fatal error when processing text: {e}."); + // } + // }; } /// Generates a new [`Measure`] for a text node on changes to its [`Text`] component. @@ -283,41 +280,38 @@ pub fn measure_text_system( With, >, 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 - { - // 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 - < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() - || computed.needs_rerender() - || text_flags.needs_measure_fn - || 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, - ); - } - } + // for (entity, block, content_size, text_flags, computed, 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 + // < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() + // || computed.needs_rerender() + // || text_flags.needs_measure_fn + // || 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, + // ); + // } + //} } #[inline] fn queue_text( entity: Entity, fonts: &Assets, - text_pipeline: &mut TextPipeline, font_atlas_set: &mut FontAtlasSet, texture_atlases: &mut Assets, textures: &mut Assets, @@ -329,50 +323,48 @@ fn queue_text( 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; - } + // // Skip the text node if it is waiting for a new measure func + // if text_flags.needs_measure_fn { + // return; + // } - 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) - }; + // 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) + // }; - 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(_))) => { - 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; - } - } + // 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)) => { + // 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; + // } + //} } /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, @@ -388,7 +380,6 @@ pub fn text_system( fonts: Res>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, - mut text_pipeline: ResMut, mut text_query: Query<( Entity, Ref, @@ -398,29 +389,25 @@ pub fn text_system( &mut ComputedTextBlock, )>, mut text_reader: TextUiReader, - mut font_system: ResMut, - mut swash_cache: 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, - &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, - ); - } - } + // 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, + // &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(), + + // ); + // } + // } } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index d7a152868dd3c..3dbc154c982b9 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -958,8 +958,7 @@ pub fn extract_text_sections( ) in text_layout_info.glyphs.iter().enumerate() { if current_span_index != *span_index - && let Some(span_entity) = - computed_block.entities().get(*span_index).map(|t| t.entity) + && let Some(span_entity) = computed_block.0.get(*span_index).copied() { color = text_styles .get(span_entity) From e10b6de47479175d76414942238f7a0c0b293d3c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 12:49:59 +0100 Subject: [PATCH 13/84] Fixed Text2d coords. Updated text2d example --- crates/bevy_sprite/src/lib.rs | 3 +++ crates/bevy_sprite/src/text2d.rs | 14 ++++++---- crates/bevy_sprite_render/src/text2d/mod.rs | 29 ++++++-------------- crates/bevy_text/src/context.rs | 3 ++- crates/bevy_text/src/font_atlas.rs | 21 ++++++++------- crates/bevy_text/src/glyph.rs | 2 ++ crates/bevy_text/src/layout.rs | 30 ++++++++++++++------- crates/bevy_text/src/lib.rs | 5 ++++ crates/bevy_text/src/text_hierarchy.rs | 4 +-- examples/2d/text2d.rs | 27 ++++++++++++++++--- 10 files changed, 87 insertions(+), 51 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index ff7ce997a573f..5af25a2512299 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::{TextSectionStyle, TextSystems}; #[cfg(feature = "bevy_picking")] pub use picking_backend::*; pub use sprite::*; @@ -89,6 +91,7 @@ impl Plugin for SpritePlugin { ) .chain() .in_set(bevy_text::Text2dUpdateSystems) + .after(TextSystems::Hierarchy) .after(bevy_app::AnimationSystems), ); diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 7796033c7b9b1..44f2e29f94346 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -5,7 +5,7 @@ use bevy_camera::visibility::{ self, NoFrustumCulling, RenderLayers, Visibility, VisibilityClass, VisibleEntities, }; use bevy_camera::Camera; -use bevy_color::Color; +use bevy_color::{Color, LinearRgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::query::With; @@ -23,7 +23,7 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, - TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, + TextLayoutInfo, TextReader, TextRoot, TextSectionStyle, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -88,6 +88,9 @@ use core::any::TypeId; Anchor, Visibility, VisibilityClass, + ComputedTextBlock, + TextRoot, + TextLayoutInfo, Transform )] #[component(on_add = visibility::add_visibility_class::)] @@ -197,7 +200,7 @@ 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, _computed) in &mut text_query { + for (entity, maybe_entity_mask, block, bounds, text_layout_info, computed) in &mut text_query { let entity_mask = maybe_entity_mask.unwrap_or_default(); let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor { @@ -227,13 +230,14 @@ pub fn update_text2d_layout( }; let mut text_sections: Vec<&str> = Vec::new(); - let mut text_section_styles: Vec = Vec::new(); - for (_, _, text, font, _) in text_reader.iter(entity) { + let mut text_section_styles: Vec> = Vec::new(); + for (_, _, text, font, color) in text_reader.iter(entity) { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( font.font.as_str(), font.font_size, font.line_height.eval(font.font_size), + color.to_linear(), )); } diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 7b4eceaeecb4d..848bc7928a80c 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -37,7 +37,6 @@ pub fn extract_text2d_sprite( &GlobalTransform, )>, >, - text_colors: Extract>, text_background_colors_query: Extract>, ) { let mut start = extracted_slices.slices.len(); @@ -148,32 +147,18 @@ pub fn extract_text2d_sprite( let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling; - let mut color = LinearRgba::WHITE; - let mut current_span = usize::MAX; for ( i, PositionedGlyph { position, atlas_info, - span_index, + + color, .. }, ) in text_layout_info.glyphs.iter().enumerate() { - if *span_index != current_span { - color = text_colors - .get( - computed_block - .0 - .get(*span_index) - .map(|t| *t) - .unwrap_or(Entity::PLACEHOLDER), - ) - .map(|text_color| LinearRgba::from(text_color.0)) - .unwrap_or_default(); - current_span = *span_index; - } let rect = texture_atlases .get(atlas_info.texture_atlas) .unwrap() @@ -185,15 +170,17 @@ pub fn extract_text2d_sprite( size: rect.size(), }); - if text_layout_info.glyphs.get(i + 1).is_none_or(|info| { - info.span_index != current_span || info.atlas_info.texture != atlas_info.texture - }) { + if text_layout_info + .glyphs + .get(i + 1) + .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) + { let render_entity = commands.spawn(TemporaryRenderEntity).id(); extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, transform, - color, + color: *color, image_handle_id: atlas_info.texture, flip_x: false, flip_y: false, diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs index b9d6071459d65..edc060126c593 100644 --- a/crates/bevy_text/src/context.rs +++ b/crates/bevy_text/src/context.rs @@ -1,3 +1,4 @@ +use bevy_color::LinearRgba; use bevy_derive::Deref; use bevy_derive::DerefMut; use bevy_ecs::resource::Resource; @@ -11,7 +12,7 @@ pub struct FontCx(pub FontContext); /// Text layout context #[derive(Resource, Default, Deref, DerefMut)] -pub struct LayoutCx(pub LayoutContext); +pub struct LayoutCx(pub LayoutContext); /// Text scaler context #[derive(Resource, Default, Deref, DerefMut)] diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 5814ed65d6dd7..deec8e2e00081 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -182,15 +182,6 @@ pub fn get_outlined_glyph_texture( scaler: &mut Scaler, glyph_id: u16, ) -> 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::scale::Render::new(&[ swash::scale::Source::ColorOutline(0), swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit), @@ -205,6 +196,16 @@ pub fn get_outlined_glyph_texture( let width = image.placement.width; let height = image.placement.height; + let px = (width * height) as usize; + let mut rgba = vec![0u8; px * 4]; + 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 + } + Ok(( Image::new( Extent3d { @@ -213,7 +214,7 @@ pub fn get_outlined_glyph_texture( depth_or_array_layers: 1, }, TextureDimension::D2, - image.data, + rgba, TextureFormat::Rgba8UnormSrgb, RenderAssetUsages::MAIN_WORLD, ), diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 453fc579a6b56..b19a601ceda3d 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -1,6 +1,7 @@ //! This module exports types related to rendering glyphs. use bevy_asset::AssetId; +use bevy_color::LinearRgba; use bevy_image::prelude::*; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; @@ -27,6 +28,7 @@ pub struct PositionedGlyph { pub byte_index: usize, /// The byte length of the glyph. pub byte_length: usize, + pub color: LinearRgba, } /// Information about a glyph in an atlas. diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 4fc9095dbc633..767a7fd3c2712 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -1,12 +1,12 @@ use crate::add_glyph_to_atlas; use crate::get_glyph_atlas_info; -use crate::FontAtlas; use crate::FontAtlasKey; use crate::FontAtlasSet; use crate::FontSmoothing; use crate::GlyphCacheKey; use crate::TextLayoutInfo; use bevy_asset::Assets; +use bevy_color::LinearRgba; use bevy_image::Image; use bevy_image::TextureAtlasLayout; use bevy_math::UVec2; @@ -41,27 +41,30 @@ fn concat_text_for_layout<'a>( } #[derive(Clone, Copy, Debug)] -pub struct TextSectionStyle<'a> { +pub struct TextSectionStyle<'a, B> { font_family: &'a str, font_size: f32, line_height: LineHeight, + brush: B, } -impl<'a> TextSectionStyle<'a> { - pub fn new(family: &'a str, size: f32, line_height: f32) -> Self { +impl<'a, B: Brush> TextSectionStyle<'a, B> { + pub fn new(family: &'a str, size: f32, line_height: f32, brush: B) -> Self { Self { font_family: family, font_size: size, line_height: LineHeight::Absolute(line_height), + brush, } } } +/// Create layout given text sections and styles pub fn build_layout_from_text_sections<'a, B: Brush>( font_cx: &'a mut FontContext, layout_cx: &'a mut LayoutContext, text_sections: impl Iterator, - text_section_styles: impl Iterator>, + text_section_styles: impl Iterator>, scale_factor: f32, ) -> Layout { let (text, section_ranges) = concat_text_for_layout(text_sections); @@ -75,8 +78,8 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( } /// create a TextLayoutInfo -pub fn build_text_layout_info( - mut layout: Layout, +pub fn build_text_layout_info( + mut layout: Layout, max_advance: Option, alignment: Alignment, scale_cx: &mut ScaleContext, @@ -92,12 +95,17 @@ pub fn build_text_layout_info( let mut info = TextLayoutInfo::default(); info.scale_factor = layout.scale(); - info.size = (layout.width(), layout.height()).into(); + 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 color = glyph_run.style().brush; let run = glyph_run.run(); let font = run.font(); let font_size = run.font_size(); @@ -153,14 +161,18 @@ pub fn build_text_layout_info( 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: (glyph.x, glyph.y).into(), + position: (x, y).into(), size: glyph_size.as_vec2(), atlas_info, span_index: 0, line_index, byte_index: line.text_range().start, byte_length: line.text_range().len(), + color, }); } } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 5d88a5ddac30c..f8e25bed34ad3 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -92,6 +92,7 @@ pub type Update2dText = Text2dUpdateSystems; #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum TextSystems { + Hierarchy, RegisterFontAssets, } @@ -104,6 +105,10 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() + .add_systems( + PostUpdate, + update_text_entities_system.in_set(TextSystems::Hierarchy), + ) .add_systems( PostUpdate, register_font_assets_system diff --git a/crates/bevy_text/src/text_hierarchy.rs b/crates/bevy_text/src/text_hierarchy.rs index fa01490f3ec8b..80afe887323ba 100644 --- a/crates/bevy_text/src/text_hierarchy.rs +++ b/crates/bevy_text/src/text_hierarchy.rs @@ -3,10 +3,10 @@ use bevy_ecs::{prelude::*, relationship::Relationship}; use crate::TextSpan; -#[derive(Component)] +#[derive(Component, Default)] pub struct ComputedTextBlock(pub Vec); -#[derive(Component)] +#[derive(Component, Default)] /// Root text element pub struct TextRoot; diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index e26d37fb1bd62..4646f43f38a3f 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -16,6 +16,7 @@ use bevy::{ fn main() { App::new() .add_plugins(DefaultPlugins) + //.add_systems(Startup, hello_setup) .add_systems(Startup, setup) .add_systems( Update, @@ -24,6 +25,22 @@ fn main() { .run(); } +fn hello_setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + commands.insert_resource(FontHolder(font)); + let text_font = TextFont { + font: "Fira Sans".to_string(), + font_size: 50.0, + ..default() + }; + commands.spawn(( + Text2d::new("hello\nworld"), + text_font.clone(), + TextBackgroundColor(MAGENTA.into()), + )); +} + #[derive(Component)] struct AnimateTranslation; @@ -33,10 +50,14 @@ struct AnimateRotation; #[derive(Component)] struct AnimateScale; +#[derive(Resource)] +struct FontHolder(Handle); + fn setup(mut commands: Commands, asset_server: Res) { - let font = asset_server.load("fonts/FiraSans-Bold.ttf"); + let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + commands.insert_resource(FontHolder(font)); let text_font = TextFont { - font: font.clone(), + font: "Fira Sans".to_string(), font_size: 50.0, ..default() }; @@ -72,7 +93,7 @@ fn setup(mut commands: Commands, asset_server: Res) { )); // Demonstrate text wrapping let slightly_smaller_text_font = TextFont { - font, + font: "Fira Sans".to_string(), font_size: 35.0, ..default() }; From 743ae2e79df2abe6ab62dcc4f2b05370cb590751 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 14:06:09 +0100 Subject: [PATCH 14/84] Map bevy LineHeight to parley LineHeight in its eval function --- crates/bevy_sprite/src/lib.rs | 1 + crates/bevy_sprite/src/text2d.rs | 14 ++------- crates/bevy_text/src/font_atlas_set.rs | 13 +++++--- crates/bevy_text/src/layout.rs | 42 +++++++++----------------- crates/bevy_text/src/text.rs | 6 ++-- 5 files changed, 31 insertions(+), 45 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 5af25a2512299..a9b6904b76e4f 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -92,6 +92,7 @@ impl Plugin for SpritePlugin { .chain() .in_set(bevy_text::Text2dUpdateSystems) .after(TextSystems::Hierarchy) + .after(TextSystems::RegisterFontAssets) .after(bevy_app::AnimationSystems), ); diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 44f2e29f94346..ed1950e1c5591 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -220,23 +220,15 @@ pub fn update_text2d_layout( *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 mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); + for (_, _, text, font, color) in text_reader.iter(entity) { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( font.font.as_str(), font.font_size, - font.line_height.eval(font.font_size), + font.line_height, color.to_linear(), )); } @@ -253,7 +245,7 @@ pub fn update_text2d_layout( *text_layout_info = build_text_layout_info( layout, - bounds.width, + bounds.width.map(|w| w * scale_factor), block.justify.into(), &mut scale_cx, &mut font_cx, diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index c3bc9de21a427..b03ceed35c6ef 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -2,18 +2,23 @@ use crate::{FontAtlas, FontSmoothing, GlyphCacheKey}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::resource::Resource; use bevy_platform::collections::HashMap; -use parley::fontique; +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 fontique::SourceId, pub u32, pub u32, pub FontSmoothing); +pub struct FontAtlasKey(pub u64, pub u32, pub u32, pub FontSmoothing); impl FontAtlasKey { /// new key - pub fn new(info: &fontique::FontInfo, size: f32, smoothing: FontSmoothing) -> Self { - Self(info.source().id(), info.index(), size.to_bits(), smoothing) + pub fn new(font_data: &FontData, size: f32, smoothing: FontSmoothing) -> Self { + Self( + font_data.data.id(), + font_data.index, + size.to_bits(), + smoothing, + ) } } diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 767a7fd3c2712..402fa433bbb43 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -44,16 +44,16 @@ fn concat_text_for_layout<'a>( pub struct TextSectionStyle<'a, B> { font_family: &'a str, font_size: f32, - line_height: LineHeight, + line_height: crate::text::LineHeight, brush: B, } impl<'a, B: Brush> TextSectionStyle<'a, B> { - pub fn new(family: &'a str, size: f32, line_height: f32, brush: B) -> Self { + pub fn new(family: &'a str, size: f32, line_height: crate::LineHeight, brush: B) -> Self { Self { font_family: family, font_size: size, - line_height: LineHeight::Absolute(line_height), + line_height, brush, } } @@ -70,9 +70,10 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( let (text, section_ranges) = concat_text_for_layout(text_sections); let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); for (style, range) in text_section_styles.zip(section_ranges) { + builder.push(StyleProperty::Brush(style.brush), range.clone()); builder.push(FontStack::from(style.font_family), range.clone()); builder.push(StyleProperty::FontSize(style.font_size), range.clone()); - builder.push(style.line_height, range); + builder.push(style.line_height.eval(), range); } builder.build(&text) } @@ -110,30 +111,8 @@ pub fn build_text_layout_info( let font = run.font(); let font_size = run.font_size(); let coords = run.normalized_coords(); - let font_ref = - FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap(); - let font_collection = - font_cx.collection.register_fonts(font.data.clone(), None); - - let (_family_id, font_info) = font_collection - .into_iter() - .find_map(|(fid, faces)| { - faces - .into_iter() - .find(|fi| fi.index() == font.index) - .map(|fi| (fid, fi)) - }) - .unwrap(); - - let font_atlas_key = FontAtlasKey::new(&font_info, font_size, font_smoothing); - - let mut scaler = scale_cx - .builder(font_ref) - .size(font_size) - .hint(true) - .normalized_coords(coords) - .build(); + let font_atlas_key = FontAtlasKey::new(&font, font_size, font_smoothing); for glyph in glyph_run.positioned_glyphs() { let font_atlases = font_atlas_set.entry(font_atlas_key).or_default(); @@ -145,6 +124,15 @@ pub fn build_text_layout_info( ) .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, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 3d84958e486ab..60f6d32bb31c6 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -270,10 +270,10 @@ pub enum LineHeight { impl LineHeight { /// eval - pub fn eval(self, font_size: f32) -> f32 { + 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), } } } From e0742068418de9d646a8aa17299d464529fc6b8e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 14:06:24 +0100 Subject: [PATCH 15/84] updated more examples --- examples/2d/text2d.rs | 18 +++++++++++++++++- examples/stress_tests/many_glyphs.rs | 1 + examples/stress_tests/many_text2d.rs | 7 +++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 4646f43f38a3f..2af8260dfcf4f 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -16,7 +16,7 @@ use bevy::{ fn main() { App::new() .add_plugins(DefaultPlugins) - //.add_systems(Startup, hello_setup) + //.add_systems(Startup, glyph_setup) .add_systems(Startup, setup) .add_systems( Update, @@ -25,6 +25,22 @@ fn main() { .run(); } +fn glyph_setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + commands.insert_resource(FontHolder(font)); + let text_font = TextFont { + font: "Fira Sans".to_string(), + font_size: 50.0, + ..default() + }; + commands.spawn(( + Text2d::new("a"), + text_font.clone(), + TextBackgroundColor(MAGENTA.into()), + )); +} + fn hello_setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); diff --git a/examples/stress_tests/many_glyphs.rs b/examples/stress_tests/many_glyphs.rs index ea7a4bbf8229c..368d3db3bfee1 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 a54ad8445d4b0..3841f726300b7 100644 --- a/examples/stress_tests/many_text2d.rs +++ b/examples/stress_tests/many_text2d.rs @@ -131,7 +131,7 @@ fn setup(mut commands: Commands, font: Res, args: Res) { text2ds.push(( Text2d(random_text(&mut rng, &args)), - random_text_font(&mut rng, &args, font.0.clone()), + random_text_font(&mut rng, &args), TextColor(color.into()), TextLayout::new_with_justify(if args.center { TextAlign::Center @@ -181,7 +181,6 @@ fn print_counts( let font_id = font.0.id(); let num_atlases = font_atlas_set .iter() - .filter(|(key, _)| key.0 == font_id) .map(|(_, atlases)| atlases.len()) .sum::(); @@ -195,7 +194,7 @@ fn print_counts( ); } -fn random_text_font(rng: &mut ChaCha8Rng, args: &Args, font: Handle) -> TextFont { +fn random_text_font(rng: &mut ChaCha8Rng, args: &Args) -> TextFont { let font_size = if args.many_font_sizes { *[10.0, 20.0, 30.0, 40.0, 50.0, 60.0].choose(rng).unwrap() } else { @@ -204,7 +203,7 @@ fn random_text_font(rng: &mut ChaCha8Rng, args: &Args, font: Handle) -> Te TextFont { font_size, - font, + font: "fira sans".to_string(), ..default() } } From 83a343f2698605bf9dbcef017ab05824c8cf64e6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 14:46:39 +0100 Subject: [PATCH 16/84] Added support for LineBreak --- crates/bevy_sprite/src/text2d.rs | 6 +++--- crates/bevy_text/src/layout.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index ed1950e1c5591..487a9b95c011e 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,7 +22,7 @@ use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, FontAtlasSet, - FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, + FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextRoot, TextSectionStyle, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; @@ -200,7 +200,7 @@ 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, computed) in &mut text_query { + for (entity, maybe_entity_mask, block, bounds, text_layout_info, _computed) in &mut text_query { let entity_mask = maybe_entity_mask.unwrap_or_default(); let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor { @@ -222,7 +222,6 @@ pub fn update_text2d_layout( let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); - for (_, _, text, font, color) in text_reader.iter(entity) { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( @@ -241,6 +240,7 @@ pub fn update_text2d_layout( text_sections.iter().copied(), text_section_styles.iter().copied(), scale_factor, + block.linebreak, ); *text_layout_info = build_text_layout_info( diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 402fa433bbb43..692148960e6c2 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -21,8 +21,10 @@ use parley::LayoutContext; use parley::LineHeight; use parley::PositionedLayoutItem; use parley::StyleProperty; +use parley::WordBreakStrength; use std::ops::Range; use swash::scale::ScaleContext; +use swash::text::LineBreak; fn concat_text_for_layout<'a>( text_sections: impl Iterator, @@ -66,9 +68,18 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( text_sections: impl Iterator, text_section_styles: impl Iterator>, scale_factor: f32, + line_break: crate::text::LineBreak, ) -> Layout { let (text, section_ranges) = concat_text_for_layout(text_sections); 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)); + }; for (style, range) in text_section_styles.zip(section_ranges) { builder.push(StyleProperty::Brush(style.brush), range.clone()); builder.push(FontStack::from(style.font_family), range.clone()); From c8173dc33b55dd4c36d5d42cdabf808ef55f75c6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 15:07:05 +0100 Subject: [PATCH 17/84] reuse parley Layout --- crates/bevy_sprite/src/text2d.rs | 17 +++++++++++------ crates/bevy_text/src/layout.rs | 7 ++++--- crates/bevy_text/src/text.rs | 6 +++++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 487a9b95c011e..cd38cb4041d1f 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -21,8 +21,8 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, FontAtlasSet, - FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, + build_layout_from_text_sections, build_text_layout_info, ComputedLayout, ComputedTextBlock, + FontAtlasSet, FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextRoot, TextSectionStyle, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; @@ -91,7 +91,8 @@ use core::any::TypeId; ComputedTextBlock, TextRoot, TextLayoutInfo, - Transform + Transform, + ComputedLayout )] #[component(on_add = visibility::add_visibility_class::)] pub struct Text2d(pub String); @@ -178,6 +179,7 @@ pub fn update_text2d_layout( Ref, &mut TextLayoutInfo, &mut ComputedTextBlock, + &mut ComputedLayout, ), With, >, @@ -200,7 +202,9 @@ 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, _computed) in &mut text_query { + for (entity, maybe_entity_mask, block, bounds, text_layout_info, _computed, mut clayout) in + &mut text_query + { let entity_mask = maybe_entity_mask.unwrap_or_default(); let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor { @@ -234,7 +238,8 @@ pub fn update_text2d_layout( let text_layout_info = text_layout_info.into_inner(); - let layout = build_layout_from_text_sections( + build_layout_from_text_sections( + &mut clayout.0, &mut font_cx.0, &mut layout_cx.0, text_sections.iter().copied(), @@ -244,7 +249,7 @@ pub fn update_text2d_layout( ); *text_layout_info = build_text_layout_info( - layout, + &mut clayout.0, bounds.width.map(|w| w * scale_factor), block.justify.into(), &mut scale_cx, diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 692148960e6c2..272be753df73c 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -63,13 +63,14 @@ impl<'a, B: Brush> TextSectionStyle<'a, B> { /// Create layout given text sections and styles pub fn build_layout_from_text_sections<'a, B: Brush>( + layout: &mut Layout, font_cx: &'a mut FontContext, layout_cx: &'a mut LayoutContext, text_sections: impl Iterator, text_section_styles: impl Iterator>, scale_factor: f32, line_break: crate::text::LineBreak, -) -> Layout { +) { let (text, section_ranges) = concat_text_for_layout(text_sections); let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); if let Some(word_break_strength) = match line_break { @@ -86,12 +87,12 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( builder.push(StyleProperty::FontSize(style.font_size), range.clone()); builder.push(style.line_height.eval(), range); } - builder.build(&text) + builder.build_into(layout, &text); } /// create a TextLayoutInfo pub fn build_text_layout_info( - mut layout: Layout, + layout: &mut Layout, max_advance: Option, alignment: Alignment, scale_cx: &mut ScaleContext, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 60f6d32bb31c6..9459a18198a88 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,11 +1,12 @@ use crate::{Font, PositionedGlyph, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; -use bevy_color::Color; +use bevy_color::{Color, LinearRgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_math::{Rect, Vec2}; use bevy_reflect::prelude::*; use bevy_utils::default; +use parley::Layout; use serde::{Deserialize, Serialize}; /// A sub-entity of a [`ComputedTextBlock`]. @@ -393,3 +394,6 @@ pub enum FontSmoothing { // TODO: Add subpixel antialias support // SubpixelAntiAliased, } + +#[derive(Component, Default)] +pub struct ComputedLayout(pub Layout); From 883e464634bc3a8fa1a2d47c2304829ae6075b67 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 15:32:23 +0100 Subject: [PATCH 18/84] Enabled basic change detection for Text2d --- crates/bevy_sprite/src/lib.rs | 1 + crates/bevy_sprite/src/text2d.rs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index a9b6904b76e4f..8c5acd78c4dc2 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -86,6 +86,7 @@ impl Plugin for SpritePlugin { app.add_systems( PostUpdate, ( + bevy_text::detect_text_needs_rerender::, update_text2d_layout.after(bevy_camera::CameraUpdateSystems), calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index cd38cb4041d1f..ac517b788b309 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -8,6 +8,7 @@ use bevy_camera::Camera; use bevy_color::{Color, LinearRgba}; use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::change_detection::DetectChanges; use bevy_ecs::query::With; use bevy_ecs::{ change_detection::Ref, @@ -27,6 +28,7 @@ use bevy_text::{ }; use bevy_transform::components::Transform; use core::any::TypeId; +use std::collections::HashSet; /// The top-level 2D text component. /// @@ -163,6 +165,8 @@ impl Default for Text2dShadow { /// [`ResMut>`](Assets) -- This system only adds new [`Image`] assets. /// It does not modify or observe existing ones. pub fn update_text2d_layout( + mut previous: Local>, + mut rerender: Local>, mut target_scale_factors: Local>, mut textures: ResMut>, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, @@ -202,7 +206,7 @@ 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, _computed, mut clayout) in + for (entity, maybe_entity_mask, block, bounds, text_layout_info, computed, mut clayout) in &mut text_query { let entity_mask = maybe_entity_mask.unwrap_or_default(); @@ -217,6 +221,7 @@ pub fn update_text2d_layout( .filter(|(_, camera_mask)| camera_mask.intersects(entity_mask)) .max_by_key(|(scale_factor, _)| FloatOrd(*scale_factor)) else { + rerender.insert(entity); continue; }; previous_scale_factor = *scale_factor; @@ -224,6 +229,14 @@ pub fn update_text2d_layout( *scale_factor }; + if !previous.contains(&entity) { + if !computed.is_changed() { + continue; + } + } + + println!("\n************** UPDATE ************\n"); + let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); for (_, _, text, font, color) in text_reader.iter(entity) { @@ -260,6 +273,8 @@ pub fn update_text2d_layout( bevy_text::FontSmoothing::AntiAliased, ); } + core::mem::swap(&mut *previous, &mut *rerender); + rerender.clear(); } /// System calculating and inserting an [`Aabb`] component to entities with some From f201e14debdc054af82df4539ef5ad3665cc1b09 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 16:29:42 +0100 Subject: [PATCH 19/84] split up hierarhy updates --- crates/bevy_sprite/src/text2d.rs | 3 --- crates/bevy_text/src/layout.rs | 5 ++--- crates/bevy_text/src/lib.rs | 2 +- crates/bevy_text/src/text_hierarchy.rs | 21 ++++++++++++++++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index ac517b788b309..048b562dab7b8 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -235,8 +235,6 @@ pub fn update_text2d_layout( } } - println!("\n************** UPDATE ************\n"); - let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); for (_, _, text, font, color) in text_reader.iter(entity) { @@ -266,7 +264,6 @@ pub fn update_text2d_layout( bounds.width.map(|w| w * scale_factor), block.justify.into(), &mut scale_cx, - &mut font_cx, &mut font_atlas_set, &mut texture_atlases, &mut textures, diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 272be753df73c..5fef1ef27924c 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -18,13 +18,11 @@ use parley::FontContext; use parley::FontStack; use parley::Layout; use parley::LayoutContext; -use parley::LineHeight; use parley::PositionedLayoutItem; use parley::StyleProperty; use parley::WordBreakStrength; use std::ops::Range; use swash::scale::ScaleContext; -use swash::text::LineBreak; fn concat_text_for_layout<'a>( text_sections: impl Iterator, @@ -42,6 +40,7 @@ fn concat_text_for_layout<'a>( (out, ranges) } +/// Resolved text style #[derive(Clone, Copy, Debug)] pub struct TextSectionStyle<'a, B> { font_family: &'a str, @@ -51,6 +50,7 @@ pub struct TextSectionStyle<'a, B> { } impl<'a, B: Brush> TextSectionStyle<'a, B> { + /// new text section style pub fn new(family: &'a str, size: f32, line_height: crate::LineHeight, brush: B) -> Self { Self { font_family: family, @@ -96,7 +96,6 @@ pub fn build_text_layout_info( max_advance: Option, alignment: Alignment, scale_cx: &mut ScaleContext, - font_cx: &mut FontContext, font_atlas_set: &mut FontAtlasSet, texture_atlases: &mut Assets, textures: &mut Assets, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index f8e25bed34ad3..aad437f6f96d3 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -107,7 +107,7 @@ impl Plugin for TextPlugin { .init_resource::() .add_systems( PostUpdate, - update_text_entities_system.in_set(TextSystems::Hierarchy), + (update_roots, update_text_entities_system).in_set(TextSystems::Hierarchy), ) .add_systems( PostUpdate, diff --git a/crates/bevy_text/src/text_hierarchy.rs b/crates/bevy_text/src/text_hierarchy.rs index 80afe887323ba..f46d9c0743b67 100644 --- a/crates/bevy_text/src/text_hierarchy.rs +++ b/crates/bevy_text/src/text_hierarchy.rs @@ -23,20 +23,25 @@ impl Default for TextTarget { /// update text entities lists pub fn update_text_entities_system( mut buffer: Local>, - mut root_query: Query<(Entity, &mut ComputedTextBlock), With>, + mut root_query: Query<(Entity, &mut ComputedTextBlock, Option<&Children>), With>, mut targets_query: Query<&mut TextTarget>, children_query: Query<&Children, With>, ) { - for (root_id, mut entities) in root_query.iter_mut() { + for (root_id, mut entities, maybe_children) in root_query.iter_mut() { buffer.push(root_id); - for entity in children_query.iter_descendants_depth_first(root_id) { - buffer.push(entity); + if let Some(children) = maybe_children { + for entity in children.iter() { + buffer.push(entity); + for entity in children_query.iter_descendants_depth_first(root_id) { + buffer.push(entity); + } + } } if buffer.as_slice() != entities.0.as_slice() { entities.0.clear(); entities.0.extend_from_slice(&buffer); - let mut targets_iter = targets_query.iter_many_mut(entities.0.as_slice()); + let mut targets_iter = targets_query.iter_many_mut(entities.0.iter().skip(1).copied()); while let Some(mut target) = targets_iter.fetch_next() { target.0 = root_id; } @@ -45,6 +50,12 @@ pub fn update_text_entities_system( } } +pub fn update_roots(mut root_query: Query<(Entity, &mut TextTarget), With>) { + for (e, mut t) in root_query.iter_mut() { + t.0 = e; + } +} + /// detect changes pub fn detect_text_needs_rerender( text_query: Query<&TextTarget, Or<(Changed, Changed)>>, From e68ca3eb0e405ceb81b42d4d42a4897f15d85484 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 20:25:36 +0100 Subject: [PATCH 20/84] Clean up change detection --- crates/bevy_sprite/src/text2d.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 048b562dab7b8..69d18b293d8d9 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -28,7 +28,6 @@ use bevy_text::{ }; use bevy_transform::components::Transform; use core::any::TypeId; -use std::collections::HashSet; /// The top-level 2D text component. /// @@ -165,8 +164,6 @@ impl Default for Text2dShadow { /// [`ResMut>`](Assets) -- This system only adds new [`Image`] assets. /// It does not modify or observe existing ones. pub fn update_text2d_layout( - mut previous: Local>, - mut rerender: Local>, mut target_scale_factors: Local>, mut textures: ResMut>, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, @@ -221,7 +218,6 @@ pub fn update_text2d_layout( .filter(|(_, camera_mask)| camera_mask.intersects(entity_mask)) .max_by_key(|(scale_factor, _)| FloatOrd(*scale_factor)) else { - rerender.insert(entity); continue; }; previous_scale_factor = *scale_factor; @@ -229,10 +225,12 @@ pub fn update_text2d_layout( *scale_factor }; - if !previous.contains(&entity) { - if !computed.is_changed() { - continue; - } + if !(computed.is_changed() + || block.is_changed() + || bounds.is_changed() + || scale_factor != text_layout_info.scale_factor) + { + continue; } let mut text_sections: Vec<&str> = Vec::new(); @@ -270,8 +268,6 @@ pub fn update_text2d_layout( bevy_text::FontSmoothing::AntiAliased, ); } - core::mem::swap(&mut *previous, &mut *rerender); - rerender.clear(); } /// System calculating and inserting an [`Aabb`] component to entities with some From 7bd68e5f908a01b6374aa8ea7479e304a75d1321 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 20:31:24 +0100 Subject: [PATCH 21/84] Removed unused --- crates/bevy_sprite_render/src/text2d/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 848bc7928a80c..6559d1a9ac496 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -3,7 +3,6 @@ use crate::{ }; use bevy_asset::{AssetId, Assets}; use bevy_camera::visibility::ViewVisibility; -use bevy_color::LinearRgba; use bevy_ecs::{ entity::Entity, system::{Commands, Query, Res, ResMut}, @@ -13,9 +12,7 @@ use bevy_math::Vec2; use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::Extract; use bevy_sprite::{Anchor, Text2dShadow}; -use bevy_text::{ - ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextBounds, TextColor, TextLayoutInfo, -}; +use bevy_text::{PositionedGlyph, TextBackgroundColor, TextBounds, TextLayoutInfo}; use bevy_transform::prelude::GlobalTransform; /// This system extracts the sprites from the 2D text components and adds them to the @@ -29,7 +26,6 @@ pub fn extract_text2d_sprite( Query<( Entity, &ViewVisibility, - &ComputedTextBlock, &TextLayoutInfo, &TextBounds, &Anchor, @@ -45,7 +41,6 @@ pub fn extract_text2d_sprite( for ( main_entity, view_visibility, - computed_block, text_layout_info, text_bounds, anchor, From c08f94622b629fb0842fa2f8ccda2744ebf2861d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 20:31:46 +0100 Subject: [PATCH 22/84] Renamed ComputedLayout to ComputedTextLayout --- crates/bevy_sprite/src/text2d.rs | 6 +++--- crates/bevy_text/src/text.rs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 69d18b293d8d9..a090c6c778ff5 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,7 +22,7 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - build_layout_from_text_sections, build_text_layout_info, ComputedLayout, ComputedTextBlock, + build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, ComputedTextLayout, FontAtlasSet, FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextRoot, TextSectionStyle, TextSpanAccess, TextWriter, }; @@ -93,7 +93,7 @@ use core::any::TypeId; TextRoot, TextLayoutInfo, Transform, - ComputedLayout + ComputedTextLayout )] #[component(on_add = visibility::add_visibility_class::)] pub struct Text2d(pub String); @@ -180,7 +180,7 @@ pub fn update_text2d_layout( Ref, &mut TextLayoutInfo, &mut ComputedTextBlock, - &mut ComputedLayout, + &mut ComputedTextLayout, ), With, >, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 9459a18198a88..3d50f410f7151 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -395,5 +395,6 @@ pub enum FontSmoothing { // SubpixelAntiAliased, } +/// Computed text layout #[derive(Component, Default)] -pub struct ComputedLayout(pub Layout); +pub struct ComputedTextLayout(pub Layout); From 75f6319a099bfaa671b2d78d8672a749428f543b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 20:41:40 +0100 Subject: [PATCH 23/84] Reenable text measurement for UI layout --- crates/bevy_ui/src/layout/mod.rs | 2 +- crates/bevy_ui/src/layout/ui_surface.rs | 29 ++++++++++++------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 42641edcdd5ae..5ef8e26fc0e67 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -89,7 +89,7 @@ pub fn ui_layout_system( Option<&Outline>, Option<&ScrollPosition>, )>, - mut buffer_query: Query<&mut ComputedTextBlock>, + mut buffer_query: Query<&mut ComputedTextLayout>, mut removed_children: RemovedComponents, mut removed_content_sizes: RemovedComponents, mut removed_nodes: RemovedComponents, diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index 5e323c0c336d0..d215243325c0c 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -219,7 +219,7 @@ impl UiSurface { height: known_dimensions.height, available_width: available_space.width, available_height: available_space.height, - buffer, + text_layout: buffer, }, style, ); @@ -283,18 +283,17 @@ 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> { - // // We avoid a query lookup whenever the buffer is not required. - // if !needs_buffer { - // return None; - // } - // let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else { - // return None; - // }; - // let Ok(computed) = query.get_mut(info.entity) else { - // return None; - // }; - // Some(computed.into_inner()) - None + 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; + } + let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else { + return None; + }; + let Ok(computed) = query.get_mut(info.entity) else { + return None; + }; + Some(computed.into_inner()) } From 0b66657ae97549749ad0c68f5b811bc70a2b85c9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 20:42:18 +0100 Subject: [PATCH 24/84] MeasureArgs takes ComputedTextLayout not ComputedTextBlock --- crates/bevy_ui/src/measurement.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 19457de7569ec..85e0b2345c2f7 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -19,7 +19,7 @@ pub struct MeasureArgs<'a> { pub height: Option, pub available_width: AvailableSpace, pub available_height: AvailableSpace, - pub buffer: Option<&'a mut bevy_text::ComputedTextBlock>, + pub text_layout: Option<&'a mut bevy_text::ComputedTextLayout>, } /// A `Measure` is used to compute the size of a ui node @@ -35,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), From d79662961e8f96591b384565251c4fb4ae1ef876 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 20:44:03 +0100 Subject: [PATCH 25/84] Restored some of the ui text wiget module code --- crates/bevy_ui/src/widget/text.rs | 197 ++++++++++++++++-------------- 1 file changed, 102 insertions(+), 95 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 984fa6c4e86d9..5ef208c2d2df8 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -157,7 +157,7 @@ pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>; /// Text measurement for UI layout. See [`NodeMeasure`]. pub struct TextMeasure { - //pub info: TextMeasureInfo, + pub info: TextMeasureInfo, } impl TextMeasure { @@ -169,48 +169,47 @@ impl TextMeasure { } impl Measure for TextMeasure { - fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 { - // let MeasureArgs { - // width, - // height, - // available_width, - // buffer, - // .. - // } = measure_args; - // let x = width.unwrap_or_else(|| match available_width { - // AvailableSpace::Definite(x) => { - // // It is possible for the "min content width" to be larger than - // // the "max content width" when soft-wrapping right-aligned text - // // and possibly other situations. + fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 { + let MeasureArgs { + width, + height, + available_width, + text_layout: buffer, + .. + } = measure_args; + let x = width.unwrap_or_else(|| match available_width { + AvailableSpace::Definite(x) => { + // It is possible for the "min content width" to be larger than + // the "max content width" when soft-wrapping right-aligned text + // and possibly other situations. - // x.max(self.info.min.x).min(self.info.max.x) - // } - // AvailableSpace::MinContent => self.info.min.x, - // AvailableSpace::MaxContent => self.info.max.x, - // }); + x.max(self.info.min.x).min(self.info.max.x) + } + AvailableSpace::MinContent => self.info.min.x, + AvailableSpace::MaxContent => self.info.max.x, + }); - // height - // .map_or_else( - // || match available_width { - // AvailableSpace::Definite(_) => { - // if let Some(buffer) = buffer { - // self.info.compute_size( - // TextBounds::new_horizontal(x), - // buffer, - // font_system, - // ) - // } else { - // error!("text measure failed, buffer is missing"); - // Vec2::default() - // } - // } - // AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), - // AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), - // }, - // |y| Vec2::new(x, y), - // ) - // .ceil() - Vec2::ZERO + height + .map_or_else( + || match available_width { + AvailableSpace::Definite(_) => { + if let Some(buffer) = buffer { + self.info.compute_size( + TextBounds::new_horizontal(x), + buffer, + font_system, + ) + } else { + error!("text measure failed, buffer is missing"); + Vec2::default() + } + } + AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), + AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), + }, + |y| Vec2::new(x, y), + ) + .ceil() } } @@ -223,36 +222,35 @@ fn create_text_measure<'a>( block: Ref, mut content_size: Mut, mut text_flags: Mut, - mut computed: Mut, + mut computed: Mut, ) { - // 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 })); - // } + match text_pipeline.create_text_measure( + entity, + fonts, + spans, + scale_factor, + &block, + computed.as_mut(), + ) { + 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) => { - // panic!("Fatal error when processing text: {e}."); - // } - // }; + // 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) => { + panic!("Fatal error when processing text: {e}."); + } + } } /// Generates a new [`Measure`] for a text node on changes to its [`Text`] component. @@ -273,7 +271,7 @@ pub fn measure_text_system( Ref, &mut ContentSize, &mut TextNodeFlags, - &mut ComputedTextBlock, + &mut ComputedTextLayout, Ref, &ComputedNode, ), @@ -281,31 +279,30 @@ pub fn measure_text_system( >, mut text_reader: TextUiReader, ) { - // for (entity, block, content_size, text_flags, computed, 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 - // < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() - // || computed.needs_rerender() - // || text_flags.needs_measure_fn - // || 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, - // ); - // } - //} + for (entity, block, content_size, text_flags, computed, 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 + < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() + || computed.needs_rerender() + || text_flags.needs_measure_fn + || 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, + ); + } + } } #[inline] @@ -411,3 +408,13 @@ pub fn text_system( // } // } } + + +pub fn update_text_layout( +) { +} + + +pub fn update_text_layout_info( +) { +} \ No newline at end of file From 148e825d565b97447f259c06235f6ca90044e56a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 21:41:20 +0100 Subject: [PATCH 26/84] added new update_text_system --- crates/bevy_ui/src/lib.rs | 4 +- crates/bevy_ui/src/widget/text.rs | 107 +++++------------------------- 2 files changed, 18 insertions(+), 93 deletions(-) diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index f2abcaada9b44..01c97133a0607 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -244,7 +244,7 @@ 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::update_text_system .in_set(UiSystems::PostLayout) //.after(bevy_text::free_unused_font_atlases_system) .before(bevy_asset::AssetEventSystems) @@ -266,7 +266,7 @@ fn build_text_interop(app: &mut App) { app.configure_sets( PostUpdate, - AmbiguousWithText.ambiguous_with(widget::text_system), + AmbiguousWithText.ambiguous_with(widget::update_text_system), ); app.configure_sets( diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 5ef208c2d2df8..c64bced8716d7 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -305,65 +305,6 @@ pub fn measure_text_system( } } -#[inline] -fn queue_text( - entity: Entity, - fonts: &Assets, - 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, -) { - // // Skip the text node if it is waiting for a new measure func - // if text_flags.needs_measure_fn { - // return; - // } - - // 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) - // }; - - // 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)) => { - // 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; - // } - //} -} - /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, /// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true. /// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`]. @@ -372,7 +313,7 @@ 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 update_text_system( mut textures: ResMut>, fonts: Res>, mut texture_atlases: ResMut>, @@ -384,37 +325,21 @@ pub fn text_system( &mut TextLayoutInfo, &mut TextNodeFlags, &mut ComputedTextBlock, + &mut ComputedTextLayout, )>, - mut text_reader: TextUiReader, -) { - // 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, - // &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(), - - // ); - // } - // } -} - - -pub fn update_text_layout( ) { + for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query { + if node.is_changed() || text_flags.needs_recompute { + *text_layout_info = build_text_layout_info( + &mut clayout.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, + bevy_text::FontSmoothing::AntiAliased, + ); + } + } } - - -pub fn update_text_layout_info( -) { -} \ No newline at end of file From 90a6b23ba76ae31c87aa61e376d4a18460535310 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 21:44:09 +0100 Subject: [PATCH 27/84] added TextMeasureInfo --- crates/bevy_ui/src/widget/text.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c64bced8716d7..2cb421a888809 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -155,6 +155,14 @@ 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, +} + /// Text measurement for UI layout. See [`NodeMeasure`]. pub struct TextMeasure { pub info: TextMeasureInfo, From 29b09924d6c26aaf8bd953dc4bd60b80ecf26c7d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 20 Oct 2025 21:50:15 +0100 Subject: [PATCH 28/84] Reimplemented `compute_size` using `parley::Layout` --- crates/bevy_text/src/text.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 3d50f410f7151..9b94f12995e18 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -396,5 +396,5 @@ pub enum FontSmoothing { } /// Computed text layout -#[derive(Component, Default)] +#[derive(Component, Default, Deref, DerefMut)] pub struct ComputedTextLayout(pub Layout); diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 2cb421a888809..fc9e7795635fe 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -18,8 +18,7 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, Font, FontAtlasSet, LineBreak, TextBounds, TextColor, TextError, TextFont, - TextHead, TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter, + ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, LineBreak, TextBounds, TextColor, TextError, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter }; use taffy::style::AvailableSpace; use tracing::error; @@ -163,6 +162,20 @@ pub struct TextMeasureInfo { 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, From 34e81a33b70cfe89f65147a675fbb0563b7ed2fd Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 09:37:20 +0100 Subject: [PATCH 29/84] Finished new UI systems. --- crates/bevy_sprite/src/lib.rs | 2 +- crates/bevy_ui/src/layout/mod.rs | 2 +- crates/bevy_ui/src/layout/ui_surface.rs | 4 +- crates/bevy_ui/src/lib.rs | 4 +- crates/bevy_ui/src/measurement.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 254 +++++++++++++----------- examples/ui/text.rs | 10 +- examples/ui/text_wrap_debug.rs | 4 +- 8 files changed, 152 insertions(+), 130 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 8c5acd78c4dc2..0bafc6c1fa5a3 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -45,7 +45,7 @@ use bevy_camera::{ }; use bevy_mesh::{Mesh, Mesh2d}; #[cfg(feature = "bevy_text")] -use bevy_text::{TextSectionStyle, TextSystems}; +use bevy_text::TextSystems; #[cfg(feature = "bevy_picking")] pub use picking_backend::*; pub use sprite::*; diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 5ef8e26fc0e67..d13ced9aaf54d 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -19,7 +19,7 @@ use bevy_sprite::BorderRect; use thiserror::Error; use ui_surface::UiSurface; -use bevy_text::ComputedTextBlock; +use bevy_text::{ComputedTextBlock, ComputedTextLayout}; mod convert; pub mod debug; diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index d215243325c0c..f08e4b77d542c 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -184,7 +184,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>, + 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); @@ -219,7 +219,7 @@ impl UiSurface { height: known_dimensions.height, available_width: available_space.width, available_height: available_space.height, - text_layout: buffer, + maybe_text_layout: buffer, }, style, ); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 01c97133a0607..5d55a27a8b085 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -195,7 +195,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::prepare_text_layout_system) .ambiguous_with(update_clipping_system) .ambiguous_with(ui_layout_system) .ambiguous_with(widget::update_viewport_render_target_size) @@ -231,7 +231,7 @@ fn build_text_interop(app: &mut App) { ( ( bevy_text::detect_text_needs_rerender::, - widget::measure_text_system, + widget::prepare_text_layout_system, ) .chain() .in_set(UiSystems::Content) diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 85e0b2345c2f7..1550ad3f80c29 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -19,7 +19,7 @@ pub struct MeasureArgs<'a> { pub height: Option, pub available_width: AvailableSpace, pub available_height: AvailableSpace, - pub text_layout: Option<&'a mut bevy_text::ComputedTextLayout>, + pub maybe_text_layout: Option<&'a mut bevy_text::ComputedTextLayout>, } /// A `Measure` is used to compute the size of a ui node diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index fc9e7795635fe..9f99bb7d9a0dc 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -3,7 +3,7 @@ use crate::{ Node, NodeMeasure, }; use bevy_asset::Assets; -use bevy_color::Color; +use bevy_color::{Color, LinearRgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, @@ -18,7 +18,10 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, LineBreak, TextBounds, TextColor, TextError, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter + build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, ComputedTextLayout, + Font, FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextError, + TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, + TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; @@ -154,21 +157,16 @@ 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 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 { + 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); @@ -191,86 +189,52 @@ impl TextMeasure { impl Measure for TextMeasure { fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 { - let MeasureArgs { - width, - height, - available_width, - text_layout: buffer, - .. - } = measure_args; - let x = width.unwrap_or_else(|| match available_width { - AvailableSpace::Definite(x) => { - // It is possible for the "min content width" to be larger than - // the "max content width" when soft-wrapping right-aligned text - // and possibly other situations. + let MeasureArgs { + width, + height, + available_width, + available_height, + maybe_text_layout, + .. + } = measure_args; - x.max(self.info.min.x).min(self.info.max.x) - } - AvailableSpace::MinContent => self.info.min.x, - AvailableSpace::MaxContent => self.info.max.x, - }); + let Some(text_layout) = maybe_text_layout else { + error!("text measure failed, buffer is missing"); + return Vec2::ZERO; + }; - height - .map_or_else( - || match available_width { - AvailableSpace::Definite(_) => { - if let Some(buffer) = buffer { - self.info.compute_size( - TextBounds::new_horizontal(x), - buffer, - font_system, - ) - } else { - error!("text measure failed, buffer is missing"); - Vec2::default() - } - } - AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), - AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), - }, - |y| Vec2::new(x, y), - ) - .ceil() - } -} + let max = self.info.compute_size(TextBounds::default(), text_layout); -#[inline] -fn create_text_measure<'a>( - entity: Entity, - fonts: &Assets, - scale_factor: f64, - spans: impl Iterator, - block: Ref, - mut content_size: Mut, - mut text_flags: Mut, - mut computed: Mut, -) { - match text_pipeline.create_text_measure( - entity, - fonts, - spans, - scale_factor, - &block, - computed.as_mut(), - ) { - 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 })); + let min = self + .info + .compute_size(TextBounds::new_horizontal(0.), text_layout); + + let x = width.unwrap_or_else(|| match available_width { + AvailableSpace::Definite(x) => { + self.info + .compute_size(TextBounds::new_horizontal(x), text_layout) + .x } + AvailableSpace::MinContent => { + self.info + .compute_size(TextBounds::new_horizontal(0.), text_layout) + .x + } + AvailableSpace::MaxContent => { + self.info.compute_size(TextBounds::default(), text_layout).x + } + }); - // 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) => { - panic!("Fatal error when processing text: {e}."); - } + height + .map_or_else( + || match available_height { + AvailableSpace::Definite(y) => (x, y.min(min.y)).into(), + AvailableSpace::MinContent => max, + AvailableSpace::MaxContent => min, + }, + |y| Vec2::new(x, y), + ) + .ceil() } } @@ -284,14 +248,17 @@ 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( - fonts: Res>, +pub fn prepare_text_layout_system( + mut font_cx: ResMut, + mut layout_cx: ResMut, + mut text_query: Query< ( Entity, Ref, &mut ContentSize, &mut TextNodeFlags, + Ref, &mut ComputedTextLayout, Ref, &ComputedNode, @@ -300,29 +267,84 @@ pub fn measure_text_system( >, mut text_reader: TextUiReader, ) { - for (entity, block, content_size, text_flags, computed, computed_target, computed_node) in - &mut text_query + for ( + entity, + block, + mut content_size, + text_flags, + 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() + || computed_block.is_changed() || text_flags.needs_measure_fn - || content_size.is_added() + || 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, - ); + continue; } + + let mut text_sections: Vec<&str> = Vec::new(); + let mut text_section_styles: Vec> = Vec::new(); + for (_, _, text, font, color) in text_reader.iter(entity) { + text_sections.push(text); + text_section_styles.push(TextSectionStyle::new( + font.font.as_str(), + font.font_size, + font.line_height, + color.to_linear(), + )); + } + + build_layout_from_text_sections( + &mut computed_layout.0, + &mut font_cx.0, + &mut layout_cx.0, + text_sections.iter().copied(), + text_section_styles.iter().copied(), + computed_target.scale_factor, + block.linebreak, + ); + + content_size.set(NodeMeasure::Text(TextMeasure { + info: TextMeasureInfo { + // min: (), + // max: (), + entity, + }, + })); + + // match text_pipeline.create_text_measure( + // entity, + // fonts, + // spans, + // scale_factor, + // &block, + // computed_layout.as_mut(), + // ) { + // 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)) => { + // panic!("Fatal error when processing text: {e}."); + // } } } @@ -336,31 +358,29 @@ pub fn measure_text_system( /// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`]. pub fn update_text_system( mut textures: ResMut>, - fonts: Res>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, mut text_query: Query<( - Entity, Ref, &TextLayout, &mut TextLayoutInfo, &mut TextNodeFlags, - &mut ComputedTextBlock, &mut ComputedTextLayout, )>, + mut scale_cx: ResMut, ) { - for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query { + for (node, block, mut text_layout_info, text_flags, mut layout) in &mut text_query { if node.is_changed() || text_flags.needs_recompute { - *text_layout_info = build_text_layout_info( - &mut clayout.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, - bevy_text::FontSmoothing::AntiAliased, - ); + *text_layout_info = build_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, + bevy_text::FontSmoothing::AntiAliased, + ); } } } diff --git a/examples/ui/text.rs b/examples/ui/text.rs index e3b24cfbaea57..4acc3b64d1f8c 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -26,6 +26,8 @@ struct FpsText; struct AnimatedText; fn setup(mut commands: Commands, asset_server: Res) { + let _fontx: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + let _font: Handle = asset_server.load("fonts/FiraMono-Medium.ttf"); // UI camera commands.spawn(Camera2d); // Text with one section @@ -34,7 +36,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("hello\nbevy!"), TextFont { // This font is loaded and will be used instead of the default font. - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: "fira_sans".to_string(), font_size: 67.0, ..default() }, @@ -58,7 +60,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("FPS: "), TextFont { // This font is loaded and will be used instead of the default font. - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: "fira sans".to_string(), font_size: 42.0, ..default() }, @@ -78,7 +80,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ( // "default_font" feature is unavailable, load a font to use instead. TextFont { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: "fira mono".to_string(), font_size: 33.0, ..Default::default() }, @@ -105,7 +107,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( Text::new("Default font disabled"), TextFont { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: "fira mono".to_string(), ..default() }, Node { diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 1a4aea1bfa634..10e906d1b1d4b 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -43,9 +43,9 @@ fn main() { fn spawn(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); - + let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); let text_font = TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: "fira sans".to_string(), font_size: 12.0, ..default() }; From d8abacd86d2398eda42ea40346959767522b26c5 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 11:21:50 +0100 Subject: [PATCH 30/84] Fixed Text requirements --- crates/bevy_ui/src/widget/text.rs | 13 ++++++++++-- examples/ui/text.rs | 33 ++++++++++++++++++++++++++++--- examples/ui/text_debug.rs | 7 ++++++- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 9f99bb7d9a0dc..117b085b0c4ad 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -96,7 +96,16 @@ impl Default for TextNodeFlags { /// ``` #[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect, PartialEq)] #[reflect(Component, Default, Debug, PartialEq, Clone)] -#[require(Node, TextLayout, TextFont, TextColor, TextNodeFlags, ContentSize)] +#[require( + Node, + TextLayout, + TextFont, + TextColor, + TextNodeFlags, + ContentSize, + ComputedTextBlock, + ComputedTextLayout +)] pub struct Text(pub String); impl Text { @@ -199,7 +208,7 @@ impl Measure for TextMeasure { } = measure_args; let Some(text_layout) = maybe_text_layout else { - error!("text measure failed, buffer is missing"); + //error!("text measure failed, buffer is missing"); return Vec2::ZERO; }; diff --git a/examples/ui/text.rs b/examples/ui/text.rs index 4acc3b64d1f8c..54f3e0294e69a 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -25,9 +25,36 @@ struct FpsText; #[derive(Component)] struct AnimatedText; +#[derive(Resource)] +struct Fonts(Vec>); + +fn setup_hello(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + commands.insert_resource(Fonts(vec![ + asset_server.load("fonts/FiraSans-Bold.ttf"), + asset_server.load("fonts/FiraMono-Medium.ttf"), + ])); + commands.spawn(( + Text::new("Hello"), + TextFont { + // This font is loaded and will be used instead of the default font. + font: "fira sans".to_string(), + font_size: 67.0, + ..default() + }, + // Outline { + // width: Val::Px(2.), + // offset: Val::Px(1.), + // color: Color::WHITE, + // }, + )); +} + fn setup(mut commands: Commands, asset_server: Res) { - let _fontx: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); - let _font: Handle = asset_server.load("fonts/FiraMono-Medium.ttf"); + commands.insert_resource(Fonts(vec![ + asset_server.load("fonts/FiraSans-Bold.ttf"), + asset_server.load("fonts/FiraMono-Medium.ttf"), + ])); // UI camera commands.spawn(Camera2d); // Text with one section @@ -36,7 +63,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("hello\nbevy!"), TextFont { // This font is loaded and will be used instead of the default font. - font: "fira_sans".to_string(), + font: "fira sans".to_string(), font_size: 67.0, ..default() }, diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 4645b91ff80e7..8e60997401a7e 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -30,8 +30,13 @@ fn main() { #[derive(Component)] struct TextChanges; +#[derive(Resource)] +struct Fonts(Vec>); + fn infotext_system(mut commands: Commands, asset_server: Res) { - let font = asset_server.load("fonts/FiraSans-Bold.ttf"); + let font_handle: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + commands.insert_resource(Fonts(vec![font_handle])); + let font = "fira sans".to_string(); let background_color = MAROON.into(); commands.spawn(Camera2d); From 5f9d8747d81d6fde174af09bd0def1a8ce91f350 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 12:40:40 +0100 Subject: [PATCH 31/84] restored most of the old measure func's design --- crates/bevy_ui/src/layout/ui_surface.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 110 +++++++++--------------- 2 files changed, 40 insertions(+), 72 deletions(-) diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index f08e4b77d542c..f0863c56a486a 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -206,7 +206,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, ), diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 117b085b0c4ad..b204844f977fb 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -11,17 +11,16 @@ use bevy_ecs::{ entity::Entity, query::With, reflect::ReflectComponent, - system::{Query, Res, ResMut}, - world::{Mut, Ref}, + system::{Query, ResMut}, + world::Ref, }; use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, ComputedTextLayout, - Font, FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextError, - TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, - TextWriter, + FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextFont, TextHead, + TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; @@ -168,8 +167,8 @@ pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>; /// Data for `TextMeasure` pub struct TextMeasureInfo { - // pub min: Vec2, - // pub max: Vec2, + pub min: Vec2, + pub max: Vec2, pub entity: Entity, } @@ -191,55 +190,46 @@ 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(_)) } } impl Measure for TextMeasure { - fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 { + fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 { let MeasureArgs { width, height, available_width, - available_height, + available_height: _, maybe_text_layout, - .. } = measure_args; - - let Some(text_layout) = maybe_text_layout else { - //error!("text measure failed, buffer is missing"); - return Vec2::ZERO; - }; - - let max = self.info.compute_size(TextBounds::default(), text_layout); - - let min = self - .info - .compute_size(TextBounds::new_horizontal(0.), text_layout); - let x = width.unwrap_or_else(|| match available_width { AvailableSpace::Definite(x) => { - self.info - .compute_size(TextBounds::new_horizontal(x), text_layout) - .x - } - AvailableSpace::MinContent => { - self.info - .compute_size(TextBounds::new_horizontal(0.), text_layout) - .x - } - AvailableSpace::MaxContent => { - self.info.compute_size(TextBounds::default(), text_layout).x + // It is possible for the "min content width" to be larger than + // the "max content width" when soft-wrapping right-aligned text + // and possibly other situations. + + x.max(self.info.min.x).min(self.info.max.x) } + AvailableSpace::MinContent => self.info.min.x, + AvailableSpace::MaxContent => self.info.max.x, }); height .map_or_else( - || match available_height { - AvailableSpace::Definite(y) => (x, y.min(min.y)).into(), - AvailableSpace::MinContent => max, - AvailableSpace::MaxContent => min, + || match available_width { + AvailableSpace::Definite(_) => { + 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() + } + } + AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), + AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), }, |y| Vec2::new(x, y), ) @@ -320,40 +310,18 @@ pub fn prepare_text_layout_system( block.linebreak, ); - content_size.set(NodeMeasure::Text(TextMeasure { - info: TextMeasureInfo { - // min: (), - // max: (), - entity, - }, - })); + computed_layout.break_all_lines(None); + let max = (computed_layout.width(), computed_layout.height()).into(); - // match text_pipeline.create_text_measure( - // entity, - // fonts, - // spans, - // scale_factor, - // &block, - // computed_layout.as_mut(), - // ) { - // 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)) => { - // panic!("Fatal error when processing text: {e}."); - // } + 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 }, + })); + } } } From 4f950b19e0f90ad29ed624d91cc249779944b6fd Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 12:55:08 +0100 Subject: [PATCH 32/84] updated stress tests --- crates/bevy_ui/src/widget/text.rs | 2 +- examples/stress_tests/text_pipeline.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index b204844f977fb..58816cd301cfa 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -347,7 +347,7 @@ pub fn update_text_system( mut scale_cx: ResMut, ) { for (node, block, mut text_layout_info, text_flags, mut layout) in &mut text_query { - if node.is_changed() || text_flags.needs_recompute { + if node.is_changed() || layout.is_changed() || text_flags.needs_recompute { *text_layout_info = build_text_layout_info( &mut layout.0, Some(node.size.x).filter(|_| block.linebreak != LineBreak::NoWrap), diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index b548b8faf1ef6..61fe2ac4a4c86 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -31,17 +31,25 @@ fn main() { .run(); } +#[derive(Resource)] +struct Fonts(Vec>); + fn spawn(mut commands: Commands, asset_server: Res) { warn!(include_str!("warning_string.txt")); + let font_handle: Handle = asset_server.load("fonts/FiraMono-Medium.ttf"); commands.spawn(Camera2d); + commands.insert_resource(Fonts(vec![font_handle])); + + let font = "fira mono".to_string(); + let make_spans = |i| { [ ( TextSpan("text".repeat(i)), TextFont { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: font.clone(), font_size: (4 + i % 10) as f32, ..Default::default() }, @@ -50,7 +58,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { ( TextSpan("pipeline".repeat(i)), TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: font.clone(), font_size: (4 + i % 11) as f32, ..default() }, From 749d607286c0a8360501101c2a5f52dc9e516551 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 13:53:13 +0100 Subject: [PATCH 33/84] Extract colors per slice in bevy_sprite_render --- crates/bevy_sprite_render/src/render/mod.rs | 23 +++++++++--------- crates/bevy_sprite_render/src/text2d/mod.rs | 6 ++--- .../src/texture_slice/computed_slices.rs | 3 +++ examples/2d/text2d.rs | 24 ++++++++++++++++++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/crates/bevy_sprite_render/src/render/mod.rs b/crates/bevy_sprite_render/src/render/mod.rs index b618f26ad5a40..c9200c1330a9e 100644 --- a/crates/bevy_sprite_render/src/render/mod.rs +++ b/crates/bevy_sprite_render/src/render/mod.rs @@ -273,13 +273,13 @@ pub struct ExtractedSlice { pub offset: Vec2, pub rect: Rect, pub size: Vec2, + pub color: LinearRgba, } pub struct ExtractedSprite { pub main_entity: Entity, pub render_entity: Entity, pub transform: GlobalTransform, - pub color: LinearRgba, /// Change the on-screen size of the sprite /// Asset ID of the [`Image`] of this sprite /// PERF: storing an `AssetId` instead of `Handle` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped) @@ -296,6 +296,7 @@ pub enum ExtractedSpriteKind { rect: Option, scaling_mode: Option, custom_size: Option, + color: LinearRgba, }, /// Indexes into the list of [`ExtractedSlice`]s stored in the [`ExtractedSlices`] resource /// Used for elements composed from multiple sprites such as text or nine-patched borders @@ -356,14 +357,15 @@ pub fn extract_sprites( if let Some(slices) = slices { let start = extracted_slices.slices.len(); - extracted_slices - .slices - .extend(slices.extract_slices(sprite, anchor.as_vec())); + extracted_slices.slices.extend(slices.extract_slices( + sprite, + anchor.as_vec(), + sprite.color.into(), + )); let end = extracted_slices.slices.len(); extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, - color: sprite.color.into(), transform: *transform, flip_x: sprite.flip_x, flip_y: sprite.flip_y, @@ -392,7 +394,6 @@ pub fn extract_sprites( extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, - color: sprite.color.into(), transform: *transform, flip_x: sprite.flip_x, flip_y: sprite.flip_y, @@ -403,6 +404,7 @@ pub fn extract_sprites( scaling_mode: sprite.image_mode.scale(), // Pass the custom size custom_size: sprite.custom_size, + color: sprite.color.into(), }, }); } @@ -685,6 +687,7 @@ pub fn prepare_sprite_image_bind_groups( rect, scaling_mode, custom_size, + color, } => { // By default, the size of the quad is the size of the texture let mut quad_size = batch_image_size; @@ -745,11 +748,7 @@ pub fn prepare_sprite_image_bind_groups( // Store the vertex data and add the item to the render phase sprite_meta .sprite_instance_buffer - .push(SpriteInstance::from( - &transform, - &extracted_sprite.color, - &uv_offset_scale, - )); + .push(SpriteInstance::from(&transform, &color, &uv_offset_scale)); current_batch.as_mut().unwrap().get_mut().range.end += 1; index += 1; @@ -792,7 +791,7 @@ pub fn prepare_sprite_image_bind_groups( .sprite_instance_buffer .push(SpriteInstance::from( &transform, - &extracted_sprite.color, + &slice.color, &uv_offset_scale, )); diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 6559d1a9ac496..e8388447f5c99 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -76,7 +76,6 @@ pub fn extract_text2d_sprite( main_entity, render_entity, transform, - color: text_background_color.0.into(), image_handle_id: AssetId::default(), flip_x: false, flip_y: false, @@ -85,6 +84,7 @@ pub fn extract_text2d_sprite( rect: None, scaling_mode: None, custom_size: Some(rect.size()), + color: text_background_color.0.into(), }, }); } @@ -113,6 +113,7 @@ pub fn extract_text2d_sprite( offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), + color, }); if text_layout_info @@ -125,7 +126,6 @@ pub fn extract_text2d_sprite( main_entity, render_entity, transform: shadow_transform, - color, image_handle_id: atlas_info.texture, flip_x: false, flip_y: false, @@ -163,6 +163,7 @@ pub fn extract_text2d_sprite( offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), + color: *color, }); if text_layout_info @@ -175,7 +176,6 @@ pub fn extract_text2d_sprite( main_entity, render_entity, transform, - color: *color, image_handle_id: atlas_info.texture, flip_x: false, flip_y: false, diff --git a/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs index 55324faa658b0..86fe8fcf4815f 100644 --- a/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs @@ -1,5 +1,6 @@ use crate::{ExtractedSlice, TextureAtlasLayout}; use bevy_asset::{AssetEvent, Assets}; +use bevy_color::LinearRgba; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_math::{Rect, Vec2}; @@ -23,6 +24,7 @@ impl ComputedTextureSlices { &'a self, sprite: &'a Sprite, anchor: Vec2, + color: LinearRgba, ) -> impl ExactSizeIterator + 'a { let mut flip = Vec2::ONE; if sprite.flip_x { @@ -39,6 +41,7 @@ impl ComputedTextureSlices { offset: slice.offset * flip - anchor, rect: slice.texture_rect, size: slice.draw_size, + color, }) } } diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 2af8260dfcf4f..17f3c115516d3 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -25,7 +25,7 @@ fn main() { .run(); } -fn glyph_setup(mut commands: Commands, asset_server: Res) { +fn glyph_setup1(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); commands.insert_resource(FontHolder(font)); @@ -41,6 +41,28 @@ fn glyph_setup(mut commands: Commands, asset_server: Res) { )); } +fn glyph_setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + commands.insert_resource(FontHolder(font)); + let text_font = TextFont { + font: "Fira Sans".to_string(), + font_size: 50.0, + ..default() + }; + commands + .spawn(( + Text2d::new("a"), + text_font.clone(), + TextColor(MAGENTA.into()), + )) + .with_child(( + TextSpan::new("b"), + text_font.clone(), + TextColor(BLUE.into()), + )); +} + fn hello_setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); From 1357140024f293036c08d7e81631dffa40e36e7b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 14:40:00 +0100 Subject: [PATCH 34/84] Fixed text updates --- crates/bevy_sprite_render/src/text2d/mod.rs | 24 ++++++++++++--------- crates/bevy_text/src/layout.rs | 8 +++++++ crates/bevy_ui/src/widget/text.rs | 17 ++++++++++++--- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index e8388447f5c99..8fc68b6ce6924 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -5,13 +5,14 @@ use bevy_asset::{AssetId, Assets}; use bevy_camera::visibility::ViewVisibility; use bevy_ecs::{ entity::Entity, + query::With, system::{Commands, Query, Res, ResMut}, }; use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::Extract; -use bevy_sprite::{Anchor, Text2dShadow}; +use bevy_sprite::{Anchor, Text2d, Text2dShadow}; use bevy_text::{PositionedGlyph, TextBackgroundColor, TextBounds, TextLayoutInfo}; use bevy_transform::prelude::GlobalTransform; @@ -23,15 +24,18 @@ pub fn extract_text2d_sprite( mut extracted_slices: ResMut, texture_atlases: Extract>>, text2d_query: Extract< - Query<( - Entity, - &ViewVisibility, - &TextLayoutInfo, - &TextBounds, - &Anchor, - Option<&Text2dShadow>, - &GlobalTransform, - )>, + Query< + ( + Entity, + &ViewVisibility, + &TextLayoutInfo, + &TextBounds, + &Anchor, + Option<&Text2dShadow>, + &GlobalTransform, + ), + With, + >, >, text_background_colors_query: Extract>, ) { diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 5fef1ef27924c..c4c78d14ee3a4 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -23,6 +23,7 @@ use parley::StyleProperty; use parley::WordBreakStrength; use std::ops::Range; use swash::scale::ScaleContext; +use tracing::info_span; fn concat_text_for_layout<'a>( text_sections: impl Iterator, @@ -71,6 +72,11 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( scale_factor: f32, line_break: crate::text::LineBreak, ) { + let e = info_span!( + "build_layout_from_text_sections", + name = "build_layout_from_text_sections" + ) + .entered(); let (text, section_ranges) = concat_text_for_layout(text_sections); let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); if let Some(word_break_strength) = match line_break { @@ -101,6 +107,8 @@ pub fn build_text_layout_info( textures: &mut Assets, font_smoothing: FontSmoothing, ) -> TextLayoutInfo { + let e = info_span!("build_text_layout_info", name = "build_text_layout_info").entered(); + layout.break_all_lines(max_advance); layout.align(None, alignment, AlignmentOptions::default()); diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 58816cd301cfa..3862b4446af8d 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -23,7 +23,7 @@ use bevy_text::{ TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; -use tracing::error; +use tracing::{error, info_span}; /// UI text system flags. /// @@ -266,11 +266,16 @@ pub fn prepare_text_layout_system( >, mut text_reader: TextUiReader, ) { + let e = info_span!( + "prepare_text_layout_system", + name = "prepare_text_layout_system" + ) + .entered(); for ( entity, block, mut content_size, - text_flags, + mut text_flags, computed_block, mut computed_layout, computed_target, @@ -322,6 +327,9 @@ pub fn prepare_text_layout_system( info: TextMeasureInfo { min, max, entity }, })); } + + text_flags.needs_measure_fn = false; + text_flags.needs_recompute = true; } } @@ -346,7 +354,8 @@ pub fn update_text_system( )>, mut scale_cx: ResMut, ) { - for (node, block, mut text_layout_info, text_flags, mut layout) in &mut text_query { + let e = info_span!("update_text_system", name = "update_text_system").entered(); + 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_recompute { *text_layout_info = build_text_layout_info( &mut layout.0, @@ -358,6 +367,8 @@ pub fn update_text_system( &mut textures, bevy_text::FontSmoothing::AntiAliased, ); + + text_flags.needs_recompute = false; } } } From fbaba1304b1a75bc32575a7f6902410d359aec13 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 14:44:38 +0100 Subject: [PATCH 35/84] trigger recompute text by setting needs_measure flags --- crates/bevy_ui/src/widget/text.rs | 4 ++-- examples/stress_tests/many_buttons.rs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 3862b4446af8d..b23e4eb9202a6 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -32,9 +32,9 @@ use tracing::{error, info_span}; #[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, + pub needs_measure_fn: bool, /// If set then the text will be recomputed. - needs_recompute: bool, + pub needs_recompute: bool, } impl Default for TextNodeFlags { diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 01ea9f8f8b82a..78a4393685dc9 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_measure_fn = true); + }); } if args.respawn { From 65d15ee9fc785454e6a5573f3b362daa0c912403 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 16:50:06 +0100 Subject: [PATCH 36/84] Renamed systems prepare_text_layout_system -> shape_text_system update_text_system -> layout_text_system --- crates/bevy_ui/src/lib.rs | 8 ++++---- crates/bevy_ui/src/widget/text.rs | 9 ++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 5d55a27a8b085..9abfd75da61d6 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -195,7 +195,7 @@ impl Plugin for UiPlugin { ui_stack_system .in_set(UiSystems::Stack) // These systems don't care about stack index - .ambiguous_with(widget::prepare_text_layout_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) @@ -231,7 +231,7 @@ fn build_text_interop(app: &mut App) { ( ( bevy_text::detect_text_needs_rerender::, - widget::prepare_text_layout_system, + widget::shape_text_system, ) .chain() .in_set(UiSystems::Content) @@ -244,7 +244,7 @@ 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::update_text_system + widget::layout_text_system .in_set(UiSystems::PostLayout) //.after(bevy_text::free_unused_font_atlases_system) .before(bevy_asset::AssetEventSystems) @@ -266,7 +266,7 @@ fn build_text_interop(app: &mut App) { app.configure_sets( PostUpdate, - AmbiguousWithText.ambiguous_with(widget::update_text_system), + AmbiguousWithText.ambiguous_with(widget::layout_text_system), ); app.configure_sets( diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index b23e4eb9202a6..634bb08a30369 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -247,7 +247,7 @@ impl Measure for TextMeasure { /// 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 prepare_text_layout_system( +pub fn shape_text_system( mut font_cx: ResMut, mut layout_cx: ResMut, @@ -266,11 +266,6 @@ pub fn prepare_text_layout_system( >, mut text_reader: TextUiReader, ) { - let e = info_span!( - "prepare_text_layout_system", - name = "prepare_text_layout_system" - ) - .entered(); for ( entity, block, @@ -341,7 +336,7 @@ pub fn prepare_text_layout_system( /// /// [`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 update_text_system( +pub fn layout_text_system( mut textures: ResMut>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, From 1d3c28fe51d114ed4f1baf1bddc695c3f2b9f6b9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 16:56:20 +0100 Subject: [PATCH 37/84] clean up renamed build_layout_from_text_sections -> shape_text_from_sections, build_text_layout_info -> update_text_layout_info --- crates/bevy_sprite/src/text2d.rs | 6 +++--- crates/bevy_text/src/layout.rs | 11 ++--------- crates/bevy_ui/src/widget/text.rs | 6 +++--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index a090c6c778ff5..0822c806ac600 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,7 +22,7 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, ComputedTextLayout, + shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, FontAtlasSet, FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextRoot, TextSectionStyle, TextSpanAccess, TextWriter, }; @@ -247,7 +247,7 @@ pub fn update_text2d_layout( let text_layout_info = text_layout_info.into_inner(); - build_layout_from_text_sections( + shape_text_from_sections( &mut clayout.0, &mut font_cx.0, &mut layout_cx.0, @@ -257,7 +257,7 @@ pub fn update_text2d_layout( block.linebreak, ); - *text_layout_info = build_text_layout_info( + *text_layout_info = update_text_layout_info( &mut clayout.0, bounds.width.map(|w| w * scale_factor), block.justify.into(), diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index c4c78d14ee3a4..f92951541a354 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -63,7 +63,7 @@ impl<'a, B: Brush> TextSectionStyle<'a, B> { } /// Create layout given text sections and styles -pub fn build_layout_from_text_sections<'a, B: Brush>( +pub fn shape_text_from_sections<'a, B: Brush>( layout: &mut Layout, font_cx: &'a mut FontContext, layout_cx: &'a mut LayoutContext, @@ -72,11 +72,6 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( scale_factor: f32, line_break: crate::text::LineBreak, ) { - let e = info_span!( - "build_layout_from_text_sections", - name = "build_layout_from_text_sections" - ) - .entered(); let (text, section_ranges) = concat_text_for_layout(text_sections); let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); if let Some(word_break_strength) = match line_break { @@ -97,7 +92,7 @@ pub fn build_layout_from_text_sections<'a, B: Brush>( } /// create a TextLayoutInfo -pub fn build_text_layout_info( +pub fn update_text_layout_info( layout: &mut Layout, max_advance: Option, alignment: Alignment, @@ -107,8 +102,6 @@ pub fn build_text_layout_info( textures: &mut Assets, font_smoothing: FontSmoothing, ) -> TextLayoutInfo { - let e = info_span!("build_text_layout_info", name = "build_text_layout_info").entered(); - layout.break_all_lines(max_advance); layout.align(None, alignment, AlignmentOptions::default()); diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 634bb08a30369..71179dcda3ae5 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -18,7 +18,7 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - build_layout_from_text_sections, build_text_layout_info, ComputedTextBlock, ComputedTextLayout, + shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; @@ -300,7 +300,7 @@ pub fn shape_text_system( )); } - build_layout_from_text_sections( + shape_text_from_sections( &mut computed_layout.0, &mut font_cx.0, &mut layout_cx.0, @@ -352,7 +352,7 @@ pub fn layout_text_system( let e = info_span!("update_text_system", name = "update_text_system").entered(); 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_recompute { - *text_layout_info = build_text_layout_info( + *text_layout_info = update_text_layout_info( &mut layout.0, Some(node.size.x).filter(|_| block.linebreak != LineBreak::NoWrap), block.justify.into(), From 83861ba03e04deb93eedca63a33c6904e47ce527 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 22:06:46 +0100 Subject: [PATCH 38/84] use the text section index for the parley Brush --- crates/bevy_sprite/src/text2d.rs | 8 ++--- crates/bevy_sprite_render/src/text2d/mod.rs | 27 +++++++++++++--- crates/bevy_text/src/context.rs | 3 +- crates/bevy_text/src/glyph.rs | 1 - crates/bevy_text/src/layout.rs | 34 ++++++++++++++++++--- crates/bevy_text/src/text.rs | 2 +- crates/bevy_text/src/text_hierarchy.rs | 4 +-- crates/bevy_ui/src/widget/text.rs | 7 ++--- 8 files changed, 63 insertions(+), 23 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 0822c806ac600..6b2acaebaaefc 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -5,7 +5,7 @@ use bevy_camera::visibility::{ self, NoFrustumCulling, RenderLayers, Visibility, VisibilityClass, VisibleEntities, }; use bevy_camera::Camera; -use bevy_color::{Color, LinearRgba}; +use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::change_detection::DetectChanges; @@ -234,14 +234,14 @@ pub fn update_text2d_layout( } let mut text_sections: Vec<&str> = Vec::new(); - let mut text_section_styles: Vec> = Vec::new(); - for (_, _, text, font, color) in text_reader.iter(entity) { + let mut text_section_styles: Vec> = Vec::new(); + for (_, i, text, font, _) in text_reader.iter(entity) { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( font.font.as_str(), font.font_size, font.line_height, - color.to_linear(), + i as u32, )); } diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 8fc68b6ce6924..46aa03f8be010 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -3,6 +3,7 @@ use crate::{ }; use bevy_asset::{AssetId, Assets}; use bevy_camera::visibility::ViewVisibility; +use bevy_color::LinearRgba; use bevy_ecs::{ entity::Entity, query::With, @@ -13,7 +14,9 @@ use bevy_math::Vec2; use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::Extract; use bevy_sprite::{Anchor, Text2d, Text2dShadow}; -use bevy_text::{PositionedGlyph, TextBackgroundColor, TextBounds, TextLayoutInfo}; +use bevy_text::{ + ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextBounds, TextLayoutInfo, +}; use bevy_transform::prelude::GlobalTransform; /// This system extracts the sprites from the 2D text components and adds them to the @@ -33,10 +36,12 @@ pub fn extract_text2d_sprite( &Anchor, Option<&Text2dShadow>, &GlobalTransform, + &ComputedTextBlock, ), With, >, >, + text_colors_query: Extract>, text_background_colors_query: Extract>, ) { let mut start = extracted_slices.slices.len(); @@ -50,6 +55,7 @@ pub fn extract_text2d_sprite( anchor, maybe_shadow, global_transform, + computed_block, ) in text2d_query.iter() { let scaling = GlobalTransform::from_scale( @@ -146,18 +152,31 @@ pub fn extract_text2d_sprite( let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling; + let mut color = LinearRgba::WHITE; + let mut current_span = usize::MAX; for ( i, PositionedGlyph { position, atlas_info, - - color, + span_index, .. }, ) in text_layout_info.glyphs.iter().enumerate() { + if *span_index != current_span { + color = text_colors_query + .get( + computed_block + .get(*span_index) + .map(|t| *t) + .unwrap_or(Entity::PLACEHOLDER), + ) + .map(|text_color| LinearRgba::from(text_color.0)) + .unwrap_or_default(); + current_span = *span_index; + } let rect = texture_atlases .get(atlas_info.texture_atlas) .unwrap() @@ -167,7 +186,7 @@ pub fn extract_text2d_sprite( offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), - color: *color, + color, }); if text_layout_info diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs index edc060126c593..eb5e0211d1cb3 100644 --- a/crates/bevy_text/src/context.rs +++ b/crates/bevy_text/src/context.rs @@ -1,4 +1,3 @@ -use bevy_color::LinearRgba; use bevy_derive::Deref; use bevy_derive::DerefMut; use bevy_ecs::resource::Resource; @@ -12,7 +11,7 @@ pub struct FontCx(pub FontContext); /// Text layout context #[derive(Resource, Default, Deref, DerefMut)] -pub struct LayoutCx(pub LayoutContext); +pub struct LayoutCx(pub LayoutContext); /// Text scaler context #[derive(Resource, Default, Deref, DerefMut)] diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index b19a601ceda3d..9b569c0284ac0 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -28,7 +28,6 @@ pub struct PositionedGlyph { pub byte_index: usize, /// The byte length of the glyph. pub byte_length: usize, - pub color: LinearRgba, } /// Information about a glyph in an atlas. diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index f92951541a354..6cdd867e39fdd 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -6,7 +6,6 @@ use crate::FontSmoothing; use crate::GlyphCacheKey; use crate::TextLayoutInfo; use bevy_asset::Assets; -use bevy_color::LinearRgba; use bevy_image::Image; use bevy_image::TextureAtlasLayout; use bevy_math::UVec2; @@ -23,7 +22,6 @@ use parley::StyleProperty; use parley::WordBreakStrength; use std::ops::Range; use swash::scale::ScaleContext; -use tracing::info_span; fn concat_text_for_layout<'a>( text_sections: impl Iterator, @@ -62,6 +60,33 @@ impl<'a, B: Brush> TextSectionStyle<'a, B> { } } +fn shape_text_from_indexed_sections<'a>( + layout: &mut Layout, + font_cx: &'a mut FontContext, + layout_cx: &'a mut LayoutContext, + text_sections: impl Iterator, + text_section_styles: impl Iterator>, + scale_factor: f32, + line_break: crate::text::LineBreak, +) { + let (text, section_ranges) = concat_text_for_layout(text_sections); + 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)); + }; + for (section_index, (style, range)) in text_section_styles.zip(section_ranges).enumerate() { + builder.push(StyleProperty::Brush(section_index as u32), range.clone()); + builder.push(FontStack::from(style.font_family), range.clone()); + builder.push(StyleProperty::FontSize(style.font_size), range.clone()); + builder.push(style.line_height.eval(), range); + } + builder.build_into(layout, &text); +} /// Create layout given text sections and styles pub fn shape_text_from_sections<'a, B: Brush>( layout: &mut Layout, @@ -93,7 +118,7 @@ pub fn shape_text_from_sections<'a, B: Brush>( /// create a TextLayoutInfo pub fn update_text_layout_info( - layout: &mut Layout, + layout: &mut Layout, max_advance: Option, alignment: Alignment, scale_cx: &mut ScaleContext, @@ -168,11 +193,10 @@ pub fn update_text_layout_info( position: (x, y).into(), size: glyph_size.as_vec2(), atlas_info, - span_index: 0, + span_index: color as usize, line_index, byte_index: line.text_range().start, byte_length: line.text_range().len(), - color, }); } } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 9b94f12995e18..3d286879e200d 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -397,4 +397,4 @@ pub enum FontSmoothing { /// Computed text layout #[derive(Component, Default, Deref, DerefMut)] -pub struct ComputedTextLayout(pub Layout); +pub struct ComputedTextLayout(pub Layout); diff --git a/crates/bevy_text/src/text_hierarchy.rs b/crates/bevy_text/src/text_hierarchy.rs index f46d9c0743b67..0101ad5447a53 100644 --- a/crates/bevy_text/src/text_hierarchy.rs +++ b/crates/bevy_text/src/text_hierarchy.rs @@ -1,9 +1,9 @@ -use bevy_derive::Deref; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, relationship::Relationship}; use crate::TextSpan; -#[derive(Component, Default)] +#[derive(Component, Default, Deref, DerefMut)] pub struct ComputedTextBlock(pub Vec); #[derive(Component, Default)] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 71179dcda3ae5..05aaeb6b5ff2f 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -289,14 +289,14 @@ pub fn shape_text_system( } let mut text_sections: Vec<&str> = Vec::new(); - let mut text_section_styles: Vec> = Vec::new(); - for (_, _, text, font, color) in text_reader.iter(entity) { + let mut text_section_styles: Vec> = Vec::new(); + for (_, section_index, text, font, _) in text_reader.iter(entity) { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( font.font.as_str(), font.font_size, font.line_height, - color.to_linear(), + section_index as u32, )); } @@ -349,7 +349,6 @@ pub fn layout_text_system( )>, mut scale_cx: ResMut, ) { - let e = info_span!("update_text_system", name = "update_text_system").entered(); 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_recompute { *text_layout_info = update_text_layout_info( From 936ffeed43d405edc2c76780ea7692ec57e0d7a8 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 22:20:56 +0100 Subject: [PATCH 39/84] Fixed span indices, was using hierarchy depth, not enumerating. --- crates/bevy_sprite/src/text2d.rs | 2 +- crates/bevy_sprite_render/src/text2d/mod.rs | 4 +-- crates/bevy_ui/src/widget/text.rs | 4 +-- examples/2d/text2d.rs | 37 +++++++++++++++++---- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 6b2acaebaaefc..8d6bfbde5eab4 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -235,7 +235,7 @@ pub fn update_text2d_layout( let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); - for (_, i, text, font, _) in text_reader.iter(entity) { + for (i, (_, _, text, font, _)) in text_reader.iter(entity).enumerate() { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( font.font.as_str(), diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 46aa03f8be010..f43f2065d415f 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -166,16 +166,16 @@ pub fn extract_text2d_sprite( ) in text_layout_info.glyphs.iter().enumerate() { if *span_index != current_span { + current_span = *span_index; color = text_colors_query .get( computed_block - .get(*span_index) + .get(current_span) .map(|t| *t) .unwrap_or(Entity::PLACEHOLDER), ) .map(|text_color| LinearRgba::from(text_color.0)) .unwrap_or_default(); - current_span = *span_index; } let rect = texture_atlases .get(atlas_info.texture_atlas) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 05aaeb6b5ff2f..65f62b401d4a8 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -290,13 +290,13 @@ pub fn shape_text_system( let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); - for (_, section_index, text, font, _) in text_reader.iter(entity) { + for (i, (_, _, text, font, _)) in text_reader.iter(entity).enumerate() { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( font.font.as_str(), font.font_size, font.line_height, - section_index as u32, + i as u32, )); } diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 17f3c115516d3..b4c8ddcd27a8c 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -16,12 +16,12 @@ use bevy::{ fn main() { App::new() .add_plugins(DefaultPlugins) - //.add_systems(Startup, glyph_setup) - .add_systems(Startup, setup) - .add_systems( - Update, - (animate_translation, animate_rotation, animate_scale), - ) + .add_systems(Startup, glyph_setup3) + // .add_systems(Startup, setup) + // .add_systems( + // Update, + // (animate_translation, animate_rotation, animate_scale), + // ) .run(); } @@ -41,7 +41,7 @@ fn glyph_setup1(mut commands: Commands, asset_server: Res) { )); } -fn glyph_setup(mut commands: Commands, asset_server: Res) { +fn glyph_setup2(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); commands.insert_resource(FontHolder(font)); @@ -63,6 +63,29 @@ fn glyph_setup(mut commands: Commands, asset_server: Res) { )); } +fn glyph_setup3(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + commands.insert_resource(FontHolder(font)); + let text_font = TextFont { + font: "Fira Sans".to_string(), + font_size: 50.0, + ..default() + }; + commands + .spawn(( + Text2d::new("aa"), + text_font.clone(), + TextColor(MAGENTA.into()), + )) + .with_child(( + TextSpan::new("bbb"), + text_font.clone(), + TextColor(BLUE.into()), + )) + .with_child((TextSpan::new("c"), text_font.clone(), TextColor(RED.into()))); +} + fn hello_setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); From f48540b0a044075af9005b0a4cc3fe913a3b8083 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 22:26:11 +0100 Subject: [PATCH 40/84] cleanup --- crates/bevy_ui/src/widget/text.rs | 4 ++-- examples/2d/text2d.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 65f62b401d4a8..b0b30ccfc3bcf 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -3,7 +3,7 @@ use crate::{ Node, NodeMeasure, }; use bevy_asset::Assets; -use bevy_color::{Color, LinearRgba}; +use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, @@ -23,7 +23,7 @@ use bevy_text::{ TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; -use tracing::{error, info_span}; +use tracing::error; /// UI text system flags. /// diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index b4c8ddcd27a8c..116056458953c 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -16,12 +16,12 @@ use bevy::{ fn main() { App::new() .add_plugins(DefaultPlugins) - .add_systems(Startup, glyph_setup3) - // .add_systems(Startup, setup) - // .add_systems( - // Update, - // (animate_translation, animate_rotation, animate_scale), - // ) + //.add_systems(Startup, glyph_setup3) + .add_systems(Startup, setup) + .add_systems( + Update, + (animate_translation, animate_rotation, animate_scale), + ) .run(); } From 9d1465ffc32cc121f2da0da4980ab8b76d7c3264 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 21 Oct 2025 23:03:10 +0100 Subject: [PATCH 41/84] use () brush for for indexed style --- crates/bevy_text/src/layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 6cdd867e39fdd..ffb09c64e04fa 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -65,7 +65,7 @@ fn shape_text_from_indexed_sections<'a>( font_cx: &'a mut FontContext, layout_cx: &'a mut LayoutContext, text_sections: impl Iterator, - text_section_styles: impl Iterator>, + text_section_styles: impl Iterator>, scale_factor: f32, line_break: crate::text::LineBreak, ) { From 7a94323f2bc3a71ba209ddd3c5b9634efa73c8e4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 22 Oct 2025 10:50:40 +0100 Subject: [PATCH 42/84] Required `TextTarget` on `TextSpan` --- crates/bevy_text/src/text.rs | 4 ++-- examples/stress_tests/many_buttons.rs | 2 +- examples/ui/text.rs | 16 +++++++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 3d286879e200d..f5794bc527538 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,4 +1,4 @@ -use crate::{Font, PositionedGlyph, TextSpanAccess, TextSpanComponent}; +use crate::{Font, PositionedGlyph, TextSpanAccess, TextSpanComponent, TextTarget}; use bevy_asset::Handle; use bevy_color::{Color, LinearRgba}; use bevy_derive::{Deref, DerefMut}; @@ -90,7 +90,7 @@ impl TextLayout { /// but each node has its own [`TextFont`] and [`TextColor`]. #[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug, Clone)] -#[require(TextFont, TextColor)] +#[require(TextFont, TextColor, TextTarget)] pub struct TextSpan(pub String); impl TextSpan { diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 78a4393685dc9..3090b162daf1c 100644 --- a/examples/stress_tests/many_buttons.rs +++ b/examples/stress_tests/many_buttons.rs @@ -117,7 +117,7 @@ fn main() { app.add_systems(Update, |mut text_query: Query<&mut TextNodeFlags>| { text_query .iter_mut() - .for_each(|mut text| text.needs_measure_fn = true); + .for_each(|mut text| text.needs_shaping = true); }); } diff --git a/examples/ui/text.rs b/examples/ui/text.rs index 54f3e0294e69a..694e15ead0016 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -42,11 +42,12 @@ fn setup_hello(mut commands: Commands, asset_server: Res) { font_size: 67.0, ..default() }, - // Outline { - // width: Val::Px(2.), - // offset: Val::Px(1.), - // color: Color::WHITE, - // }, + Outline { + width: Val::Px(2.), + offset: Val::Px(1.), + color: Color::WHITE, + }, + children![TextSpan::new(" world!"), TextSpan::new(" orange!"),], )); } @@ -94,6 +95,11 @@ fn setup(mut commands: Commands, asset_server: Res) { )) .with_child(( TextSpan::default(), + Outline { + width: Val::Px(2.), + color: Color::WHITE, + ..Default::default() + }, if cfg!(feature = "default_font") { ( TextFont { From b3a1756c1e46722b55e6a255315028e0890ff46d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 22 Oct 2025 10:52:18 +0100 Subject: [PATCH 43/84] Renamed `TextFlags` fields: * needs_new_measure_func -> needs_shaping * needs_recompute -> needs_relayout `shape_text_system` queries for `Text` and `TextFont` of root text entity and checks for changes --- crates/bevy_ui/src/widget/text.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index b0b30ccfc3bcf..c1416f40172e9 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -32,16 +32,16 @@ use tracing::error; #[reflect(Component, Default, Debug, Clone)] pub struct TextNodeFlags { /// If set then a new measure function for the text node will be created. - pub needs_measure_fn: bool, + pub needs_shaping: bool, /// If set then the text will be recomputed. - pub 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, } } } @@ -261,6 +261,8 @@ pub fn shape_text_system( &mut ComputedTextLayout, Ref, &ComputedNode, + Ref, + Ref, ), With, >, @@ -275,15 +277,20 @@ pub fn shape_text_system( mut computed_layout, computed_target, computed_node, + text, + text_font, ) 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 < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() || computed_block.is_changed() - || text_flags.needs_measure_fn + || text_flags.needs_shaping || content_size.is_added()) + || text.is_changed() + || text_font.is_changed() { continue; } @@ -323,8 +330,8 @@ pub fn shape_text_system( })); } - text_flags.needs_measure_fn = false; - text_flags.needs_recompute = true; + text_flags.needs_shaping = false; + text_flags.needs_relayout = true; } } @@ -350,7 +357,7 @@ pub fn layout_text_system( mut scale_cx: ResMut, ) { 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_recompute { + if node.is_changed() || layout.is_changed() || text_flags.needs_relayout { *text_layout_info = update_text_layout_info( &mut layout.0, Some(node.size.x).filter(|_| block.linebreak != LineBreak::NoWrap), @@ -362,7 +369,7 @@ pub fn layout_text_system( bevy_text::FontSmoothing::AntiAliased, ); - text_flags.needs_recompute = false; + text_flags.needs_relayout = false; } } } From 0ea960086dee571a2279db5d78861052f1b02237 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 22 Oct 2025 10:52:58 +0100 Subject: [PATCH 44/84] Removed `Text`/`Text2d` specific detect changes systems --- crates/bevy_sprite/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 0bafc6c1fa5a3..fc9a90f64721c 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -86,7 +86,6 @@ impl Plugin for SpritePlugin { app.add_systems( PostUpdate, ( - bevy_text::detect_text_needs_rerender::, update_text2d_layout.after(bevy_camera::CameraUpdateSystems), calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) From 99997c17618ef6e1fbcf934959312b0d82f07da7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 22 Oct 2025 10:54:03 +0100 Subject: [PATCH 45/84] update_text2d_layout checks for root text entity changes --- crates/bevy_sprite/src/text2d.rs | 39 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 8d6bfbde5eab4..3ce479fd595f0 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -9,7 +9,6 @@ use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::change_detection::DetectChanges; -use bevy_ecs::query::With; use bevy_ecs::{ change_detection::Ref, component::Component, @@ -172,18 +171,17 @@ pub fn update_text2d_layout( mut font_cx: ResMut, mut layout_cx: ResMut, mut scale_cx: ResMut, - mut text_query: Query< - ( - Entity, - Option<&RenderLayers>, - Ref, - Ref, - &mut TextLayoutInfo, - &mut ComputedTextBlock, - &mut ComputedTextLayout, - ), - With, - >, + mut text_query: Query<( + Entity, + Option<&RenderLayers>, + Ref, + Ref, + &mut TextLayoutInfo, + &mut ComputedTextBlock, + &mut ComputedTextLayout, + Ref, + Ref, + )>, mut text_reader: Text2dReader, ) { target_scale_factors.clear(); @@ -203,8 +201,17 @@ 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, computed, mut clayout) in - &mut text_query + for ( + entity, + maybe_entity_mask, + block, + bounds, + text_layout_info, + computed, + mut clayout, + text2d, + text_font, + ) in &mut text_query { let entity_mask = maybe_entity_mask.unwrap_or_default(); @@ -229,6 +236,8 @@ pub fn update_text2d_layout( || block.is_changed() || bounds.is_changed() || scale_factor != text_layout_info.scale_factor) + || text2d.is_changed() + || text_font.is_changed() { continue; } From 148edac9ab223bbae97e870272eb87e20e90a157 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 22 Oct 2025 10:54:43 +0100 Subject: [PATCH 46/84] `register_font_assets_system` sets all `TextFont`s changed when new font is loaded. --- crates/bevy_text/src/font.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index c1583793a6246..229310433cd53 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,8 +1,11 @@ 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 parley::fontique::Blob; @@ -41,16 +44,25 @@ pub fn register_font_assets_system( mut cx: ResMut, mut fonts: ResMut>, mut events: MessageReader>, + mut text_font_query: Query<&mut TextFont>, ) { + let mut change = false; for event in events.read() { match event { AssetEvent::Added { id } => { if let Some(font) = fonts.get_mut(*id) { let collection = cx.collection.register_fonts(font.blob.clone(), None); font.collection = collection; + change = true; } } _ => {} } } + + if change { + for mut font in text_font_query.iter_mut() { + font.set_changed(); + } + } } From 8ee5abf54a225d7f3f45c7f2425894b0afa37d8a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 22 Oct 2025 10:57:14 +0100 Subject: [PATCH 47/84] Removed `update_roots`, cleaned up system schedule, fixed conflicts --- crates/bevy_text/src/lib.rs | 9 ++++++++- crates/bevy_text/src/text_hierarchy.rs | 19 +++++++------------ crates/bevy_ui/src/lib.rs | 19 ++++++++----------- crates/bevy_ui/src/widget/text.rs | 1 - 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index aad437f6f96d3..478ac8f54db8b 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -92,7 +92,9 @@ pub type Update2dText = Text2dUpdateSystems; #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum TextSystems { + /// Update text hierarchy and check for changes Hierarchy, + /// Register new font assets with Parley's `FontContext` after loading RegisterFontAssets, } @@ -107,7 +109,12 @@ impl Plugin for TextPlugin { .init_resource::() .add_systems( PostUpdate, - (update_roots, update_text_entities_system).in_set(TextSystems::Hierarchy), + ( + update_text_entities_system, + detect_text_spans_needs_rerender, + ) + .chain() + .in_set(TextSystems::Hierarchy), ) .add_systems( PostUpdate, diff --git a/crates/bevy_text/src/text_hierarchy.rs b/crates/bevy_text/src/text_hierarchy.rs index 0101ad5447a53..3e7ac1ccbab7b 100644 --- a/crates/bevy_text/src/text_hierarchy.rs +++ b/crates/bevy_text/src/text_hierarchy.rs @@ -1,8 +1,10 @@ use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{prelude::*, relationship::Relationship}; +use bevy_ecs::prelude::*; use crate::TextSpan; +/// Contains the entities comprising a text hierarchy. +/// In order as read, starting from the root text element. #[derive(Component, Default, Deref, DerefMut)] pub struct ComputedTextBlock(pub Vec); @@ -23,7 +25,7 @@ impl Default for TextTarget { /// update text entities lists pub fn update_text_entities_system( mut buffer: Local>, - mut root_query: Query<(Entity, &mut ComputedTextBlock, Option<&Children>), With>, + mut root_query: Query<(Entity, &mut ComputedTextBlock, Option<&Children>)>, mut targets_query: Query<&mut TextTarget>, children_query: Query<&Children, With>, ) { @@ -40,8 +42,7 @@ pub fn update_text_entities_system( if buffer.as_slice() != entities.0.as_slice() { entities.0.clear(); entities.0.extend_from_slice(&buffer); - - let mut targets_iter = targets_query.iter_many_mut(entities.0.iter().skip(1).copied()); + let mut targets_iter = targets_query.iter_many_mut(entities.0.iter()); while let Some(mut target) = targets_iter.fetch_next() { target.0 = root_id; } @@ -50,15 +51,9 @@ pub fn update_text_entities_system( } } -pub fn update_roots(mut root_query: Query<(Entity, &mut TextTarget), With>) { - for (e, mut t) in root_query.iter_mut() { - t.0 = e; - } -} - /// detect changes -pub fn detect_text_needs_rerender( - text_query: Query<&TextTarget, Or<(Changed, Changed)>>, +pub fn detect_text_spans_needs_rerender( + text_query: Query<&TextTarget, Or<(Changed, Changed)>>, mut output_query: Query<&mut ComputedTextBlock>, ) { for target in text_query.iter() { diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 9abfd75da61d6..a57d864d0fc63 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::*; @@ -184,8 +185,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, @@ -224,19 +224,15 @@ impl Plugin for UiPlugin { } fn build_text_interop(app: &mut App) { - use widget::Text; - app.add_systems( PostUpdate, ( - ( - bevy_text::detect_text_needs_rerender::, - widget::shape_text_system, - ) + (widget::shape_text_system,) .chain() + .after(TextSystems::Hierarchy) + .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. @@ -245,11 +241,12 @@ fn build_text_interop(app: &mut App) { // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(widget::update_image_content_size_system), widget::layout_text_system + .after(TextSystems::Hierarchy) + .after(TextSystems::RegisterFontAssets) .in_set(UiSystems::PostLayout) //.after(bevy_text::free_unused_font_atlases_system) - .before(bevy_asset::AssetEventSystems) + .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), ), diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c1416f40172e9..a6b4872296927 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -250,7 +250,6 @@ impl Measure for TextMeasure { pub fn shape_text_system( mut font_cx: ResMut, mut layout_cx: ResMut, - mut text_query: Query< ( Entity, From 6c07edfc9d8dde40aeda7b3dd964a0564fbfc72f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 22 Oct 2025 11:03:07 +0100 Subject: [PATCH 48/84] Removed unneeded --- crates/bevy_ui/src/layout/mod.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index d13ced9aaf54d..bdb4abae9bea4 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -19,7 +19,7 @@ use bevy_sprite::BorderRect; use thiserror::Error; use ui_surface::UiSurface; -use bevy_text::{ComputedTextBlock, ComputedTextLayout}; +use bevy_text::ComputedTextLayout; mod convert; pub mod debug; diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index a6b4872296927..ea05a9261cd13 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -27,11 +27,11 @@ 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. + /// If set then the text will be reshaped. pub needs_shaping: bool, /// If set then the text will be recomputed. pub needs_relayout: bool, From 0981369465180f0937320a05b52ecac80697d47a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 23 Oct 2025 16:56:45 +0100 Subject: [PATCH 49/84] Another fix for merge with main --- crates/bevy_ui_render/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index ae2ec6c5bbf00..7052f6e4e2a7d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1096,7 +1096,7 @@ pub fn extract_text_shadows( for &(section_index, rect, strikethrough_y, stroke, underline_y) in text_layout_info.section_geometry.iter() { - let section_entity = computed_block.entities()[section_index].entity; + let section_entity = computed_block[section_index]; let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity) else { continue; @@ -1213,7 +1213,7 @@ pub fn extract_text_decorations( for &(section_index, rect, strikethrough_y, stroke, underline_y) in text_layout_info.section_geometry.iter() { - let section_entity = computed_block.entities()[section_index].entity; + let section_entity = computed_block[section_index]; let Ok(((text_background_color, maybe_strikethrough, maybe_underline), text_color)) = text_background_colors_query.get(section_entity) else { From fb66661c5f4eeec2905c38c4ebdc01951c9b457a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 24 Oct 2025 22:47:23 +0100 Subject: [PATCH 50/84] * Added new `SectionGeometry` struct to replace tuple used in `TextLayoutInfo`'s `section_geometry` field. * Update `section_geometry` in the `update_text_layout_info` function. --- crates/bevy_sprite_render/src/text2d/mod.rs | 57 +++++++++++++-------- crates/bevy_text/src/layout.rs | 26 +++++++++- crates/bevy_text/src/text.rs | 15 +++++- crates/bevy_ui_render/src/lib.rs | 40 +++++++-------- 4 files changed, 93 insertions(+), 45 deletions(-) diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index e0f8481e7f0a9..f4d4c005c9f90 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -75,13 +75,13 @@ pub fn extract_text2d_sprite( let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; - for &(section_index, rect, _, _, _) in text_layout_info.section_geometry.iter() { - let section_entity = computed_block[section_index]; + for section in text_layout_info.section_geometry.iter() { + let section_entity = computed_block[section.span_index]; let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { continue; }; let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new(rect.center().x, -rect.center().y); + let offset = Vec2::new(section.rect.center().x, -section.rect.center().y); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling @@ -97,7 +97,7 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(rect.size()), + custom_size: Some(section.rect.size()), color: text_background_color.0.into(), }, }); @@ -153,10 +153,8 @@ pub fn extract_text2d_sprite( end += 1; } - for &(section_index, rect, strikethrough_y, stroke, underline_y) in - text_layout_info.section_geometry.iter() - { - let section_entity = computed_block[section_index]; + for section in text_layout_info.section_geometry.iter() { + let section_entity = computed_block[section.span_index]; let Ok((_, has_strikethrough, has_underline)) = decoration_query.get(section_entity) else { @@ -165,7 +163,10 @@ pub fn extract_text2d_sprite( if has_strikethrough { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke); + let offset = Vec2::new( + section.rect.center().x, + -section.strikethrough_offset - 0.5 * section.strikethrough_size, + ); let transform = shadow_transform * GlobalTransform::from_translation(offset.extend(0.)); extracted_sprites.sprites.push(ExtractedSprite { @@ -179,7 +180,10 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new(rect.size().x, stroke)), + custom_size: Some(Vec2::new( + section.rect.size().x, + section.strikethrough_size, + )), color, }, }); @@ -187,7 +191,10 @@ pub fn extract_text2d_sprite( if has_underline { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke); + let offset = Vec2::new( + section.rect.center().x, + -section.underline_offset - 0.5 * section.underline_size, + ); let transform = shadow_transform * GlobalTransform::from_translation(offset.extend(0.)); extracted_sprites.sprites.push(ExtractedSprite { @@ -201,7 +208,10 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new(rect.size().x, stroke)), + custom_size: Some(Vec2::new( + section.rect.size().x, + section.underline_size, + )), color, }, }); @@ -271,10 +281,8 @@ pub fn extract_text2d_sprite( end += 1; } - for &(section_index, rect, strikethrough_y, stroke, underline_y) in - text_layout_info.section_geometry.iter() - { - let section_entity = computed_block[section_index]; + for section in text_layout_info.section_geometry.iter() { + let section_entity = computed_block[section.span_index]; let Ok((text_color, has_strike_through, has_underline)) = decoration_query.get(section_entity) else { @@ -282,7 +290,10 @@ pub fn extract_text2d_sprite( }; if has_strike_through { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke); + let offset = Vec2::new( + section.rect.center().x, + -section.strikethrough_offset - 0.5 * section.strikethrough_size, + ); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling @@ -298,7 +309,10 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new(rect.size().x, stroke)), + custom_size: Some(Vec2::new( + section.rect.size().x, + section.strikethrough_size, + )), color: text_color.0.into(), }, }); @@ -306,7 +320,10 @@ pub fn extract_text2d_sprite( if has_underline { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke); + let offset = Vec2::new( + section.rect.center().x, + -section.underline_offset - 0.5 * section.underline_size, + ); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling @@ -322,7 +339,7 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new(rect.size().x, stroke)), + custom_size: Some(Vec2::new(section.rect.size().x, section.underline_size)), color: text_color.0.into(), }, }); diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index ffb09c64e04fa..69aadfb1c7c29 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -1,14 +1,18 @@ use crate::add_glyph_to_atlas; use crate::get_glyph_atlas_info; +use crate::text; use crate::FontAtlasKey; use crate::FontAtlasSet; use crate::FontSmoothing; use crate::GlyphCacheKey; +use crate::SectionGeometry; use crate::TextLayoutInfo; use bevy_asset::Assets; 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; @@ -21,6 +25,7 @@ use parley::PositionedLayoutItem; use parley::StyleProperty; use parley::WordBreakStrength; use std::ops::Range; +use std::usize; use swash::scale::ScaleContext; fn concat_text_for_layout<'a>( @@ -143,8 +148,10 @@ pub fn update_text_layout_info( for (line_index, item) in line.items().enumerate() { match item { PositionedLayoutItem::GlyphRun(glyph_run) => { - let color = glyph_run.style().brush; + let span_index = glyph_run.style().brush; + let run = glyph_run.run(); + let font = run.font(); let font_size = run.font_size(); let coords = run.normalized_coords(); @@ -193,12 +200,27 @@ pub fn update_text_layout_info( position: (x, y).into(), size: glyph_size.as_vec2(), atlas_info, - span_index: color as usize, + span_index: span_index as usize, line_index, byte_index: line.text_range().start, byte_length: line.text_range().len(), }); } + + info.section_geometry.push(SectionGeometry { + span_index: span_index as usize, + rect: 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_offset: run.metrics().strikethrough_offset, + strikethrough_size: run.metrics().strikethrough_size, + underline_offset: run.metrics().underline_offset, + underline_size: run.metrics().underline_size, + }); } _ => {} } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index cd59b54ce27c0..2a0eb3b16628d 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -376,11 +376,24 @@ pub struct TextLayoutInfo { pub glyphs: Vec, /// Geometry of each text segment: (section index, bounding rect, strikethrough offset, stroke thickness, underline offset) /// A text section spanning more than one line will have multiple segments. - pub section_geometry: Vec<(usize, Rect, f32, f32, f32)>, + pub section_geometry: Vec<(SectionGeometry)>, /// The glyphs resulting size pub size: Vec2, } +#[derive(Clone, Default, Debug, Reflect)] +pub struct SectionGeometry { + /// index of span + pub span_index: usize, + /// bounding rect of section + pub rect: Rect, + /// offset of strikethrough + pub strikethrough_offset: f32, + pub strikethrough_size: f32, + pub underline_offset: f32, + pub underline_size: f32, +} + /// Determines which antialiasing method to use when rendering text. By default, text is /// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look. /// diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 7052f6e4e2a7d..0f877e22bacde 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1093,10 +1093,8 @@ pub fn extract_text_shadows( end += 1; } - for &(section_index, rect, strikethrough_y, stroke, underline_y) in - text_layout_info.section_geometry.iter() - { - let section_entity = computed_block[section_index]; + for section in text_layout_info.section_geometry.iter() { + let section_entity = computed_block[section.span_index]; let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity) else { continue; @@ -1111,14 +1109,14 @@ pub fn extract_text_shadows( extracted_camera_entity, transform: node_transform * Affine2::from_translation(Vec2::new( - rect.center().x, - strikethrough_y + 0.5 * stroke, + section.rect.center().x, + section.strikethrough_offset + 0.5 * section.strikethrough_size, )), item: ExtractedUiItem::Node { color: shadow.color.into(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(rect.size().x, stroke), + max: Vec2::new(section.rect.size().x, section.strikethrough_size), }, atlas_scaling: None, flip_x: false, @@ -1140,14 +1138,14 @@ pub fn extract_text_shadows( extracted_camera_entity, transform: node_transform * Affine2::from_translation(Vec2::new( - rect.center().x, - underline_y + 0.5 * stroke, + section.rect.center().x, + section.underline_offset + 0.5 * section.underline_size, )), item: ExtractedUiItem::Node { color: shadow.color.into(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(rect.size().x, stroke), + max: Vec2::new(section.rect.size().x, section.underline_size), }, atlas_scaling: None, flip_x: false, @@ -1210,10 +1208,8 @@ pub fn extract_text_decorations( let transform = Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); - for &(section_index, rect, strikethrough_y, stroke, underline_y) in - text_layout_info.section_geometry.iter() - { - let section_entity = computed_block[section_index]; + for section in text_layout_info.section_geometry.iter() { + let section_entity = computed_block[section.span_index]; let Ok(((text_background_color, maybe_strikethrough, maybe_underline), text_color)) = text_background_colors_query.get(section_entity) else { @@ -1227,12 +1223,12 @@ pub fn extract_text_decorations( clip: clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(rect.center()), + transform: transform * Affine2::from_translation(section.rect.center()), item: ExtractedUiItem::Node { color: text_background_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: rect.size(), + max: section.rect.size(), }, atlas_scaling: None, flip_x: false, @@ -1254,14 +1250,14 @@ pub fn extract_text_decorations( extracted_camera_entity, transform: transform * Affine2::from_translation(Vec2::new( - rect.center().x, - strikethrough_y + 0.5 * stroke, + section.rect.center().x, + section.strikethrough_offset + 0.5 * section.strikethrough_size, )), item: ExtractedUiItem::Node { color: text_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(rect.size().x, stroke), + max: Vec2::new(section.rect.size().x, section.strikethrough_size), }, atlas_scaling: None, flip_x: false, @@ -1283,14 +1279,14 @@ pub fn extract_text_decorations( extracted_camera_entity, transform: transform * Affine2::from_translation(Vec2::new( - rect.center().x, - underline_y + 0.5 * stroke, + section.rect.center().x, + section.underline_offset + 0.5 * section.underline_size, )), item: ExtractedUiItem::Node { color: text_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(rect.size().x, stroke), + max: Vec2::new(section.rect.size().x, section.underline_size), }, atlas_scaling: None, flip_x: false, From c97483a53809fcc33b10ee4e467389e79caa9420 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 24 Oct 2025 23:09:03 +0100 Subject: [PATCH 51/84] Cleanup, made strikethrough example work. --- crates/bevy_text/src/layout.rs | 32 +++----------------------------- examples/ui/strikethrough.rs | 13 ++++++++++--- examples/ui/text.rs | 9 ++++++--- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 69aadfb1c7c29..8458ffe7a0cc8 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -65,33 +65,6 @@ impl<'a, B: Brush> TextSectionStyle<'a, B> { } } -fn shape_text_from_indexed_sections<'a>( - layout: &mut Layout, - font_cx: &'a mut FontContext, - layout_cx: &'a mut LayoutContext, - text_sections: impl Iterator, - text_section_styles: impl Iterator>, - scale_factor: f32, - line_break: crate::text::LineBreak, -) { - let (text, section_ranges) = concat_text_for_layout(text_sections); - 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)); - }; - for (section_index, (style, range)) in text_section_styles.zip(section_ranges).enumerate() { - builder.push(StyleProperty::Brush(section_index as u32), range.clone()); - builder.push(FontStack::from(style.font_family), range.clone()); - builder.push(StyleProperty::FontSize(style.font_size), range.clone()); - builder.push(style.line_height.eval(), range); - } - builder.build_into(layout, &text); -} /// Create layout given text sections and styles pub fn shape_text_from_sections<'a, B: Brush>( layout: &mut Layout, @@ -216,9 +189,10 @@ pub fn update_text_layout_info( line.metrics().max_coord, ), }, - strikethrough_offset: run.metrics().strikethrough_offset, + strikethrough_offset: glyph_run.baseline() + - run.metrics().strikethrough_offset, strikethrough_size: run.metrics().strikethrough_size, - underline_offset: run.metrics().underline_offset, + underline_offset: glyph_run.baseline() - run.metrics().underline_offset, underline_size: run.metrics().underline_size, }); } diff --git a/examples/ui/strikethrough.rs b/examples/ui/strikethrough.rs index 2c5112e9ddcc9..60efd1326a2a1 100644 --- a/examples/ui/strikethrough.rs +++ b/examples/ui/strikethrough.rs @@ -12,18 +12,25 @@ fn main() { .run(); } +#[derive(Resource)] +struct Fonts(Vec>); + fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); + commands.insert_resource(Fonts(vec![ + asset_server.load("fonts/FiraSans-Bold.ttf"), + asset_server.load("fonts/FiraMono-Medium.ttf"), + ])); commands.spawn(( Text::new("struck\nstruck"), // Just add the `Strikethrough` component to any `Text`, `Text2d` or `TextSpan` and it's text will be struck through. Strikethrough, TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: "fira sans".to_string(), font_size: 67.0, ..default() }, - TextLayout::new_with_justify(Justify::Center), + TextLayout::new_with_justify(TextAlign::Center), Node { position_type: PositionType::Absolute, bottom: px(5), @@ -70,7 +77,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("2struck\nstruck"), Strikethrough, TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: "fira sans".to_string(), font_size: 67.0, ..default() }, diff --git a/examples/ui/text.rs b/examples/ui/text.rs index a44a5098cd15d..e005a8e041f17 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -13,6 +13,7 @@ use bevy::{ fn main() { App::new() .add_plugins((DefaultPlugins, FrameTimeDiagnosticsPlugin::default())) + //.add_systems(Startup, setup_hello) .add_systems(Startup, setup) .add_systems(Update, (text_update_system, text_color_system)) .run(); @@ -36,11 +37,11 @@ fn setup_hello(mut commands: Commands, asset_server: Res) { asset_server.load("fonts/FiraMono-Medium.ttf"), ])); commands.spawn(( - Text::new("Hello"), + Text::new("Hello\n"), TextFont { // This font is loaded and will be used instead of the default font. font: "fira sans".to_string(), - font_size: 67.0, + font_size: 200.0, ..default() }, Outline { @@ -48,7 +49,9 @@ fn setup_hello(mut commands: Commands, asset_server: Res) { offset: Val::Px(1.), color: Color::WHITE, }, - children![TextSpan::new(" world!"), TextSpan::new(" orange!"),], + Strikethrough, + Underline, + children![(TextSpan::new("world!"), Underline, Strikethrough)], )); } From 52290984081d4024be0c1012c91df6e28998c901 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 25 Oct 2025 21:43:59 +0100 Subject: [PATCH 52/84] removed unused imports --- crates/bevy_text/src/layout.rs | 1 - crates/bevy_text/src/text.rs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index 8458ffe7a0cc8..f72e533db6410 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -1,6 +1,5 @@ use crate::add_glyph_to_atlas; use crate::get_glyph_atlas_info; -use crate::text; use crate::FontAtlasKey; use crate::FontAtlasSet; use crate::FontSmoothing; diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 2a0eb3b16628d..906cf733e977f 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,6 +1,5 @@ -use crate::{Font, PositionedGlyph, TextSpanAccess, TextSpanComponent, TextTarget}; -use bevy_asset::Handle; -use bevy_color::{Color, LinearRgba}; +use crate::{PositionedGlyph, TextSpanAccess, TextSpanComponent, TextTarget}; +use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_math::{Rect, Vec2}; From 348dab206878121a68d09a7ee73c8919ef64a7f0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 25 Oct 2025 21:48:56 +0100 Subject: [PATCH 53/84] Applied changes from `RunGeometry` PR. --- crates/bevy_sprite_render/src/text2d/mod.rs | 43 ++++++++------- crates/bevy_text/src/glyph.rs | 1 - crates/bevy_text/src/layout.rs | 15 +++-- crates/bevy_text/src/lib.rs | 1 + crates/bevy_text/src/text.rs | 61 ++++++++++++++++----- crates/bevy_ui_render/src/lib.rs | 38 +++++++------ 6 files changed, 101 insertions(+), 58 deletions(-) diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index f4d4c005c9f90..f353459c98faf 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -75,13 +75,13 @@ pub fn extract_text2d_sprite( let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; - for section in text_layout_info.section_geometry.iter() { + for section in text_layout_info.run_geometry.iter() { let section_entity = computed_block[section.span_index]; let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { continue; }; let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new(section.rect.center().x, -section.rect.center().y); + let offset = Vec2::new(section.bounds.center().x, -section.bounds.center().y); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling @@ -97,7 +97,7 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(section.rect.size()), + custom_size: Some(section.bounds.size()), color: text_background_color.0.into(), }, }); @@ -153,7 +153,7 @@ pub fn extract_text2d_sprite( end += 1; } - for section in text_layout_info.section_geometry.iter() { + for section in text_layout_info.run_geometry.iter() { let section_entity = computed_block[section.span_index]; let Ok((_, has_strikethrough, has_underline)) = decoration_query.get(section_entity) @@ -164,8 +164,8 @@ pub fn extract_text2d_sprite( if has_strikethrough { let render_entity = commands.spawn(TemporaryRenderEntity).id(); let offset = Vec2::new( - section.rect.center().x, - -section.strikethrough_offset - 0.5 * section.strikethrough_size, + section.bounds.center().x, + -section.strikethrough_y - 0.5 * section.strikethrough_thickness, ); let transform = shadow_transform * GlobalTransform::from_translation(offset.extend(0.)); @@ -181,8 +181,8 @@ pub fn extract_text2d_sprite( rect: None, scaling_mode: None, custom_size: Some(Vec2::new( - section.rect.size().x, - section.strikethrough_size, + section.bounds.size().x, + section.strikethrough_thickness, )), color, }, @@ -192,8 +192,8 @@ pub fn extract_text2d_sprite( if has_underline { let render_entity = commands.spawn(TemporaryRenderEntity).id(); let offset = Vec2::new( - section.rect.center().x, - -section.underline_offset - 0.5 * section.underline_size, + section.bounds.center().x, + -section.underline_y - 0.5 * section.underline_thickness, ); let transform = shadow_transform * GlobalTransform::from_translation(offset.extend(0.)); @@ -209,8 +209,8 @@ pub fn extract_text2d_sprite( rect: None, scaling_mode: None, custom_size: Some(Vec2::new( - section.rect.size().x, - section.underline_size, + section.bounds.size().x, + section.underline_thickness, )), color, }, @@ -281,7 +281,7 @@ pub fn extract_text2d_sprite( end += 1; } - for section in text_layout_info.section_geometry.iter() { + for section in text_layout_info.run_geometry.iter() { let section_entity = computed_block[section.span_index]; let Ok((text_color, has_strike_through, has_underline)) = decoration_query.get(section_entity) @@ -291,8 +291,8 @@ pub fn extract_text2d_sprite( if has_strike_through { let render_entity = commands.spawn(TemporaryRenderEntity).id(); let offset = Vec2::new( - section.rect.center().x, - -section.strikethrough_offset - 0.5 * section.strikethrough_size, + section.bounds.center().x, + -section.strikethrough_y - 0.5 * section.strikethrough_thickness, ); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) @@ -310,8 +310,8 @@ pub fn extract_text2d_sprite( rect: None, scaling_mode: None, custom_size: Some(Vec2::new( - section.rect.size().x, - section.strikethrough_size, + section.bounds.size().x, + section.strikethrough_thickness, )), color: text_color.0.into(), }, @@ -321,8 +321,8 @@ pub fn extract_text2d_sprite( if has_underline { let render_entity = commands.spawn(TemporaryRenderEntity).id(); let offset = Vec2::new( - section.rect.center().x, - -section.underline_offset - 0.5 * section.underline_size, + section.bounds.center().x, + -section.underline_y - 0.5 * section.underline_thickness, ); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) @@ -339,7 +339,10 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new(section.rect.size().x, section.underline_size)), + custom_size: Some(Vec2::new( + section.bounds.size().x, + section.underline_thickness, + )), color: text_color.0.into(), }, }); diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 9b569c0284ac0..453fc579a6b56 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -1,7 +1,6 @@ //! This module exports types related to rendering glyphs. use bevy_asset::AssetId; -use bevy_color::LinearRgba; use bevy_image::prelude::*; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/layout.rs index f72e533db6410..3b39edc3d9c3f 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -4,7 +4,7 @@ use crate::FontAtlasKey; use crate::FontAtlasSet; use crate::FontSmoothing; use crate::GlyphCacheKey; -use crate::SectionGeometry; +use crate::RunGeometry; use crate::TextLayoutInfo; use bevy_asset::Assets; use bevy_image::Image; @@ -179,20 +179,19 @@ pub fn update_text_layout_info( }); } - info.section_geometry.push(SectionGeometry { + info.run_geometry.push(RunGeometry { span_index: span_index as usize, - rect: Rect { + 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_offset: glyph_run.baseline() - - run.metrics().strikethrough_offset, - strikethrough_size: run.metrics().strikethrough_size, - underline_offset: glyph_run.baseline() - run.metrics().underline_offset, - underline_size: run.metrics().underline_size, + 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, }); } _ => {} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 7c92164c11596..fcc35eb98f951 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -91,6 +91,7 @@ pub struct 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 { /// Update text hierarchy and check for changes diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 906cf733e977f..896910e91a467 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -373,24 +373,59 @@ pub struct TextLayoutInfo { pub scale_factor: f32, /// Scaled and positioned glyphs in screenspace pub glyphs: Vec, - /// Geometry of each text segment: (section index, bounding rect, strikethrough offset, stroke thickness, underline offset) - /// A text section spanning more than one line will have multiple segments. - pub section_geometry: Vec<(SectionGeometry)>, + /// 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. + pub run_geometry: Vec, /// The glyphs resulting size pub size: Vec2, } -#[derive(Clone, Default, Debug, Reflect)] -pub struct SectionGeometry { - /// index of span +/// 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 rect of section - pub rect: Rect, - /// offset of strikethrough - pub strikethrough_offset: f32, - pub strikethrough_size: f32, - pub underline_offset: f32, - pub underline_size: f32, + /// 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) + } } /// Determines which antialiasing method to use when rendering text. By default, text is diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 0f877e22bacde..73fb7c636deff 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1093,7 +1093,7 @@ pub fn extract_text_shadows( end += 1; } - for section in text_layout_info.section_geometry.iter() { + for section in text_layout_info.run_geometry.iter() { let section_entity = computed_block[section.span_index]; let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity) else { @@ -1109,14 +1109,17 @@ pub fn extract_text_shadows( extracted_camera_entity, transform: node_transform * Affine2::from_translation(Vec2::new( - section.rect.center().x, - section.strikethrough_offset + 0.5 * section.strikethrough_size, + section.bounds.center().x, + section.strikethrough_y + 0.5 * section.strikethrough_thickness, )), item: ExtractedUiItem::Node { color: shadow.color.into(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(section.rect.size().x, section.strikethrough_size), + max: Vec2::new( + section.bounds.size().x, + section.strikethrough_thickness, + ), }, atlas_scaling: None, flip_x: false, @@ -1138,14 +1141,14 @@ pub fn extract_text_shadows( extracted_camera_entity, transform: node_transform * Affine2::from_translation(Vec2::new( - section.rect.center().x, - section.underline_offset + 0.5 * section.underline_size, + section.bounds.center().x, + section.underline_y + 0.5 * section.underline_thickness, )), item: ExtractedUiItem::Node { color: shadow.color.into(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(section.rect.size().x, section.underline_size), + max: Vec2::new(section.bounds.size().x, section.underline_thickness), }, atlas_scaling: None, flip_x: false, @@ -1208,7 +1211,7 @@ pub fn extract_text_decorations( let transform = Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); - for section in text_layout_info.section_geometry.iter() { + for section in text_layout_info.run_geometry.iter() { let section_entity = computed_block[section.span_index]; let Ok(((text_background_color, maybe_strikethrough, maybe_underline), text_color)) = text_background_colors_query.get(section_entity) @@ -1223,12 +1226,12 @@ pub fn extract_text_decorations( clip: clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(section.rect.center()), + transform: transform * Affine2::from_translation(section.bounds.center()), item: ExtractedUiItem::Node { color: text_background_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: section.rect.size(), + max: section.bounds.size(), }, atlas_scaling: None, flip_x: false, @@ -1250,14 +1253,17 @@ pub fn extract_text_decorations( extracted_camera_entity, transform: transform * Affine2::from_translation(Vec2::new( - section.rect.center().x, - section.strikethrough_offset + 0.5 * section.strikethrough_size, + section.bounds.center().x, + section.strikethrough_y + 0.5 * section.strikethrough_thickness, )), item: ExtractedUiItem::Node { color: text_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(section.rect.size().x, section.strikethrough_size), + max: Vec2::new( + section.bounds.size().x, + section.strikethrough_thickness, + ), }, atlas_scaling: None, flip_x: false, @@ -1279,14 +1285,14 @@ pub fn extract_text_decorations( extracted_camera_entity, transform: transform * Affine2::from_translation(Vec2::new( - section.rect.center().x, - section.underline_offset + 0.5 * section.underline_size, + section.bounds.center().x, + section.underline_y + 0.5 * section.underline_thickness, )), item: ExtractedUiItem::Node { color: text_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(section.rect.size().x, section.underline_size), + max: Vec2::new(section.bounds.size().x, section.underline_thickness), }, atlas_scaling: None, flip_x: false, From 2f5336ec015bb17937246ad0b24b687c17a9047a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 14:59:45 +0000 Subject: [PATCH 54/84] Reverted changes to text example --- examples/ui/text.rs | 48 +++++---------------------------------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/examples/ui/text.rs b/examples/ui/text.rs index e005a8e041f17..5f4add3d7c646 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -13,7 +13,6 @@ use bevy::{ fn main() { App::new() .add_plugins((DefaultPlugins, FrameTimeDiagnosticsPlugin::default())) - //.add_systems(Startup, setup_hello) .add_systems(Startup, setup) .add_systems(Update, (text_update_system, text_color_system)) .run(); @@ -27,39 +26,7 @@ struct FpsText; #[derive(Component)] struct AnimatedText; -#[derive(Resource)] -struct Fonts(Vec>); - -fn setup_hello(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2d); - commands.insert_resource(Fonts(vec![ - asset_server.load("fonts/FiraSans-Bold.ttf"), - asset_server.load("fonts/FiraMono-Medium.ttf"), - ])); - commands.spawn(( - Text::new("Hello\n"), - TextFont { - // This font is loaded and will be used instead of the default font. - font: "fira sans".to_string(), - font_size: 200.0, - ..default() - }, - Outline { - width: Val::Px(2.), - offset: Val::Px(1.), - color: Color::WHITE, - }, - Strikethrough, - Underline, - children![(TextSpan::new("world!"), Underline, Strikethrough)], - )); -} - fn setup(mut commands: Commands, asset_server: Res) { - commands.insert_resource(Fonts(vec![ - asset_server.load("fonts/FiraSans-Bold.ttf"), - asset_server.load("fonts/FiraMono-Medium.ttf"), - ])); // UI camera commands.spawn(Camera2d); // Text with one section @@ -69,13 +36,13 @@ fn setup(mut commands: Commands, asset_server: Res) { Underline, TextFont { // This font is loaded and will be used instead of the default font. - font: "fira sans".to_string(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 67.0, ..default() }, TextShadow::default(), // Set the justification of the Text - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), // Set the style of the Node itself. Node { position_type: PositionType::Absolute, @@ -93,18 +60,13 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("FPS: "), TextFont { // This font is loaded and will be used instead of the default font. - font: "fira sans".to_string(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 42.0, ..default() }, )) .with_child(( TextSpan::default(), - Outline { - width: Val::Px(2.), - color: Color::WHITE, - ..Default::default() - }, if cfg!(feature = "default_font") { ( TextFont { @@ -118,7 +80,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ( // "default_font" feature is unavailable, load a font to use instead. TextFont { - font: "fira mono".to_string(), + font: asset_server.load("fonts/FiraMono-Medium.ttf"), font_size: 33.0, ..Default::default() }, @@ -145,7 +107,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( Text::new("Default font disabled"), TextFont { - font: "fira mono".to_string(), + font: asset_server.load("fonts/FiraMono-Medium.ttf"), ..default() }, Node { From 1737b6f4a2da1c8458a3afbc84078b788364b1fa Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 15:03:09 +0000 Subject: [PATCH 55/84] Reverted Justify renaming --- crates/bevy_text/src/lib.rs | 2 +- crates/bevy_text/src/text.rs | 29 ++++++++++--------- examples/2d/sprite_scale.rs | 4 +-- examples/2d/sprite_slice.rs | 2 +- examples/2d/text2d.rs | 8 ++--- examples/2d/texture_atlas.rs | 2 +- examples/3d/tonemapping.rs | 2 +- examples/animation/animated_ui.rs | 2 +- examples/animation/animation_graph.rs | 2 +- examples/animation/animation_masks.rs | 2 +- .../external_source_external_thread.rs | 2 +- examples/ecs/one_shot_systems.rs | 2 +- examples/math/render_primitives.rs | 2 +- examples/mobile/src/lib.rs | 2 +- examples/stress_tests/many_glyphs.rs | 2 +- examples/stress_tests/many_text2d.rs | 4 +-- examples/stress_tests/text_pipeline.rs | 2 +- examples/testbed/2d.rs | 10 +++---- examples/testbed/ui.rs | 2 +- examples/time/virtual_time.rs | 4 +-- examples/ui/directional_navigation.rs | 2 +- examples/ui/display_and_visibility.rs | 8 ++--- examples/ui/size_constraints.rs | 2 +- examples/ui/strikethrough.rs | 2 +- examples/ui/text_background_colors.rs | 2 +- examples/ui/text_debug.rs | 8 ++--- examples/ui/text_wrap_debug.rs | 2 +- 27 files changed, 57 insertions(+), 56 deletions(-) diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index fcc35eb98f951..74b923d9b0e1c 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -63,7 +63,7 @@ pub use text_hierarchy::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, LineBreak, Strikethrough, TextAlign, TextColor, TextError, TextFont, TextLayout, + Font, Justify, LineBreak, Strikethrough, TextColor, TextError, TextFont, TextLayout, TextSpan, Underline, }; } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 896910e91a467..81f9391f75183 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,3 +1,4 @@ +use crate::Font; use crate::{PositionedGlyph, TextSpanAccess, TextSpanComponent, TextTarget}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; @@ -33,19 +34,19 @@ pub struct TextEntity { pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. - pub justify: TextAlign, + pub justify: Justify, /// How the text should linebreak when running out of the bounds determined by `max_size`. pub linebreak: LineBreak, } impl TextLayout { /// Makes a new [`TextLayout`]. - pub const fn new(justify: TextAlign, linebreak: LineBreak) -> Self { + pub const fn new(justify: Justify, linebreak: LineBreak) -> Self { Self { justify, linebreak } } /// Makes a new [`TextLayout`] with the specified [`Justify`]. - pub fn new_with_justify(justify: TextAlign) -> Self { + pub fn new_with_justify(justify: Justify) -> Self { Self::default().with_justify(justify) } @@ -61,7 +62,7 @@ impl TextLayout { } /// Returns this [`TextLayout`] with the specified [`Justify`]. - pub const fn with_justify(mut self, justify: TextAlign) -> Self { + pub const fn with_justify(mut self, justify: Justify) -> Self { self.justify = justify; self } @@ -132,7 +133,7 @@ impl From for TextSpan { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] #[doc(alias = "JustifyText")] -pub enum TextAlign { +pub enum Justify { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] @@ -153,15 +154,15 @@ pub enum TextAlign { End, } -impl From for parley::Alignment { - fn from(justify: TextAlign) -> Self { +impl From for parley::Alignment { + fn from(justify: Justify) -> Self { match justify { - TextAlign::Start => parley::Alignment::Start, - TextAlign::End => parley::Alignment::End, - TextAlign::Left => parley::Alignment::Left, - TextAlign::Center => parley::Alignment::Center, - TextAlign::Right => parley::Alignment::Right, - TextAlign::Justified => parley::Alignment::Justify, + 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, } } } @@ -178,7 +179,7 @@ pub struct TextFont { /// `FiraMono-subset.ttf` compiled into the library is used. /// * otherwise no text will be rendered, unless a custom font is loaded into the default font /// handle. - pub font: String, + pub font: Handle, /// The vertical height of rasterized glyphs in the font atlas in pixels. /// /// This is multiplied by the window scale factor and `UiScale`, but not the text entity diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index 434cf762775db..aff8ab3b1be46 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -123,7 +123,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res) { rect.transform, children![( Text2d::new(rect.text), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, @@ -265,7 +265,7 @@ fn setup_texture_atlas( sprite_sheet.transform, children![( Text2d::new(sprite_sheet.text), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index ec459bc82ffff..91918b1d66091 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -94,7 +94,7 @@ fn spawn_sprites( children![( Text2d::new(label), text_style, - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), bevy::sprite::Anchor::TOP_CENTER, )], diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index c61d4429fe0e0..9c292f84d77f3 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -122,7 +122,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 50.0, ..default() }; - let text_justification = TextAlign::Center; + let text_justification = Justify::Center; commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( @@ -168,7 +168,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(Unicode linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(TextAlign::Left, LineBreak::WordBoundary), + TextLayout::new(Justify::Left, LineBreak::WordBoundary), // Wrap text in the rectangle TextBounds::from(box_size), // Ensure the text is drawn on top of the box @@ -190,7 +190,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(TextAlign::Left, LineBreak::AnyCharacter), + TextLayout::new(Justify::Left, LineBreak::AnyCharacter), // Wrap text in the rectangle TextBounds::from(other_box_size), // Ensure the text is drawn on top of the box @@ -209,7 +209,7 @@ fn setup(mut commands: Commands, asset_server: Res) { slightly_smaller_text_font .clone() .with_font_smoothing(FontSmoothing::None), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)), // Add a black shadow to the text Text2dShadow::default(), diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 8555487c83fa6..5380fc393b875 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -279,7 +279,7 @@ fn create_label( commands.spawn(( Text2d::new(text), text_style, - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Transform { translation: Vec3::new(translation.0, translation.1, translation.2), ..default() diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index fd05a19a1802f..5d8b19e4367e1 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -181,7 +181,7 @@ fn setup_image_viewer_scene( ..default() }, TextColor(Color::BLACK), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Node { align_self: AlignSelf::Center, margin: UiRect::all(auto()), diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index 0f781c07f18e1..8243592034abc 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -147,7 +147,7 @@ fn setup( ..default() }, TextColor(Color::Srgba(Srgba::RED)), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), animation_target_id, AnimatedBy(player), animation_target_name, diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index ee41251226681..7393105058283 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -283,7 +283,7 @@ fn setup_node_rects(commands: &mut Commands) { ..default() }, TextColor(ANTIQUE_WHITE.into()), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), )) .id(); diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs index 6645bd30cfc1f..f0f0818d6a222 100644 --- a/examples/animation/animation_masks.rs +++ b/examples/animation/animation_masks.rs @@ -268,7 +268,7 @@ fn new_mask_group_control(label: &str, width: Val, mask_group_id: u32) -> impl B } else { selected_button_text_style.clone() }, - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Node { flex_grow: 1.0, margin: UiRect::vertical(px(3)), diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 990a9e9de9dcf..34ac20cab41a6 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -54,7 +54,7 @@ fn spawn_text(mut commands: Commands, mut reader: MessageReader) for (per_frame, message) in reader.read().enumerate() { commands.spawn(( Text2d::new(message.0.to_string()), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(per_frame as f32 * 100.0, 300.0, 0.0), )); } diff --git a/examples/ecs/one_shot_systems.rs b/examples/ecs/one_shot_systems.rs index 1888d2d9ebe75..59b8dedb045f6 100644 --- a/examples/ecs/one_shot_systems.rs +++ b/examples/ecs/one_shot_systems.rs @@ -93,7 +93,7 @@ fn setup_ui(mut commands: Commands) { commands.spawn(Camera2d); commands.spawn(( Text::default(), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, diff --git a/examples/math/render_primitives.rs b/examples/math/render_primitives.rs index a9ff9985c2b73..61047422aae6c 100644 --- a/examples/math/render_primitives.rs +++ b/examples/math/render_primitives.rs @@ -377,7 +377,7 @@ fn setup_text(mut commands: Commands, cameras: Query<(Entity, &Camera)>) { children![( Text::default(), HeaderText, - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), children![ TextSpan::new("Primitive: "), TextSpan(format!("{text}", text = PrimitiveSelected::default())), diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index 04e23303c9410..b70da4c672a0f 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -158,7 +158,7 @@ fn setup_scene( ..default() }, TextColor::BLACK, - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), )); } diff --git a/examples/stress_tests/many_glyphs.rs b/examples/stress_tests/many_glyphs.rs index 368d3db3bfee1..fa81403546bde 100644 --- a/examples/stress_tests/many_glyphs.rs +++ b/examples/stress_tests/many_glyphs.rs @@ -72,7 +72,7 @@ fn setup(mut commands: Commands, args: Res) { ..Default::default() }; let text_block = TextLayout { - justify: TextAlign::Left, + justify: Justify::Left, linebreak: LineBreak::AnyCharacter, }; diff --git a/examples/stress_tests/many_text2d.rs b/examples/stress_tests/many_text2d.rs index 3841f726300b7..e5e817562312f 100644 --- a/examples/stress_tests/many_text2d.rs +++ b/examples/stress_tests/many_text2d.rs @@ -134,9 +134,9 @@ fn setup(mut commands: Commands, font: Res, args: Res) { random_text_font(&mut rng, &args), TextColor(color.into()), TextLayout::new_with_justify(if args.center { - TextAlign::Center + Justify::Center } else { - TextAlign::Left + Justify::Left }), Transform { translation, diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 61fe2ac4a4c86..0e72485067df3 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -73,7 +73,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { .spawn(( Text2d::default(), TextLayout { - justify: TextAlign::Center, + justify: Justify::Center, linebreak: LineBreak::AnyCharacter, }, TextBounds::default(), diff --git a/examples/testbed/2d.rs b/examples/testbed/2d.rs index 48d54e97ff56f..0a10ee04e408d 100644 --- a/examples/testbed/2d.rs +++ b/examples/testbed/2d.rs @@ -148,10 +148,10 @@ mod text { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Text))); for (i, justify) in [ - TextAlign::Left, - TextAlign::Right, - TextAlign::Center, - TextAlign::Justified, + Justify::Left, + Justify::Right, + Justify::Center, + Justify::Justified, ] .into_iter() .enumerate() @@ -193,7 +193,7 @@ mod text { fn spawn_anchored_text( commands: &mut Commands, dest: Vec3, - justify: TextAlign, + justify: Justify, bounds: Option, ) { commands.spawn(( diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 7133123378a07..6dc2cbe6d7933 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -482,7 +482,7 @@ mod text_wrap { for (j, message) in messages.into_iter().enumerate() { commands.entity(root).with_child(( Text(message.clone()), - TextLayout::new(TextAlign::Left, linebreak), + TextLayout::new(Justify::Left, linebreak), BackgroundColor(Color::srgb(0.8 - j as f32 * 0.3, 0., 0.)), )); } diff --git a/examples/time/virtual_time.rs b/examples/time/virtual_time.rs index ffbdf198e3667..c9c6bd112b65a 100644 --- a/examples/time/virtual_time.rs +++ b/examples/time/virtual_time.rs @@ -102,7 +102,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut time: ResMu ..default() }, TextColor(Color::srgb(0.85, 0.85, 0.85)), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), ), ( Text::default(), @@ -111,7 +111,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut time: ResMu ..default() }, TextColor(virtual_color), - TextLayout::new_with_justify(TextAlign::Right), + TextLayout::new_with_justify(Justify::Right), VirtualTime, ), ], diff --git a/examples/ui/directional_navigation.rs b/examples/ui/directional_navigation.rs index 320b86a986436..e0f303290cd09 100644 --- a/examples/ui/directional_navigation.rs +++ b/examples/ui/directional_navigation.rs @@ -186,7 +186,7 @@ fn setup_ui( Text::new(button_name), // And center the text if it flows onto multiple lines TextLayout { - justify: TextAlign::Center, + justify: Justify::Center, ..default() }, )) diff --git a/examples/ui/display_and_visibility.rs b/examples/ui/display_and_visibility.rs index ce37d97f1afd4..ce96648c88636 100644 --- a/examples/ui/display_and_visibility.rs +++ b/examples/ui/display_and_visibility.rs @@ -96,7 +96,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(( Text::new("Use the panel on the right to change the Display and Visibility properties for the respective nodes of the panel on the left"), text_font.clone(), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Node { margin: UiRect::bottom(px(10)), ..Default::default() @@ -150,13 +150,13 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("Display::None\nVisibility::Hidden\nVisibility::Inherited"), text_font.clone(), TextColor(HIDDEN_COLOR), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), )); builder.spawn(( Text::new("-\n-\n-"), text_font.clone(), TextColor(DARK_GRAY.into()), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), )); builder.spawn((Text::new("The UI Node and its descendants will not be visible and will not be allotted any space in the UI layout.\nThe UI Node will not be visible but will still occupy space in the UI layout.\nThe UI node will inherit the visibility property of its parent. If it has no parent it will be visible."), text_font)); }); @@ -393,7 +393,7 @@ where builder.spawn(( Text(format!("{}::{:?}", Target::::NAME, T::default())), text_font, - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), )); }); } diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index 1431c9db9cbac..585ed321912c2 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -251,7 +251,7 @@ fn spawn_button( } else { UNHOVERED_TEXT_COLOR }), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), )); }); } diff --git a/examples/ui/strikethrough.rs b/examples/ui/strikethrough.rs index 60efd1326a2a1..5e1347a0dbc31 100644 --- a/examples/ui/strikethrough.rs +++ b/examples/ui/strikethrough.rs @@ -30,7 +30,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 67.0, ..default() }, - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Node { position_type: PositionType::Absolute, bottom: px(5), diff --git a/examples/ui/text_background_colors.rs b/examples/ui/text_background_colors.rs index 92f932b97d6af..8f32c8d29e85b 100644 --- a/examples/ui/text_background_colors.rs +++ b/examples/ui/text_background_colors.rs @@ -43,7 +43,7 @@ fn setup(mut commands: Commands) { .spawn(( Text::default(), TextLayout { - justify: TextAlign::Center, + justify: Justify::Center, ..Default::default() }, )) diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 8e60997401a7e..fd1020fcd2173 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -77,7 +77,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(YELLOW.into()), - TextLayout::new_with_justify(TextAlign::Right), + TextLayout::new_with_justify(Justify::Right), Node { max_width: px(300), ..default() @@ -119,7 +119,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(Color::srgb(0.8, 0.2, 0.7)), - TextLayout::new_with_justify(TextAlign::Center), + TextLayout::new_with_justify(Justify::Center), Node { max_width: px(400), ..default() @@ -135,7 +135,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(YELLOW.into()), - TextLayout::new_with_justify(TextAlign::Left), + TextLayout::new_with_justify(Justify::Left), Node { max_width: px(300), ..default() @@ -150,7 +150,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { font_size: 29.0, ..default() }, - TextLayout::new_with_justify(TextAlign::Justified), + TextLayout::new_with_justify(Justify::Justified), TextColor(GREEN_YELLOW.into()), Node { max_width: px(300), diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 10e906d1b1d4b..6b5319ad38446 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -116,7 +116,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { commands.entity(column_id).with_child(( Text(message.clone()), text_font.clone(), - TextLayout::new(TextAlign::Left, linebreak), + TextLayout::new(Justify::Left, linebreak), BackgroundColor(Color::srgb(0.8 - j as f32 * 0.2, 0., 0.)), )); } From 86188171607c88b4086b8486657bb1dce1d98596 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 16:46:48 +0000 Subject: [PATCH 56/84] Use the asset path as the family name in the font collections,, store it in `Font`. During layout use font handle to to look up the family name.. --- crates/bevy_sprite/src/text2d.rs | 16 +++++++++------ crates/bevy_text/src/font.rs | 32 +++++++++++++++-------------- crates/bevy_text/src/font_loader.rs | 5 +++-- crates/bevy_text/src/layout.rs | 23 +++++++++++++-------- crates/bevy_text/src/lib.rs | 3 ++- crates/bevy_text/src/text.rs | 14 ++++++------- crates/bevy_ui/src/widget/text.rs | 17 ++++++++------- 7 files changed, 63 insertions(+), 47 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 3ce479fd595f0..a07a6f161ac30 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -9,6 +9,7 @@ use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::change_detection::DetectChanges; +use bevy_ecs::system::Res; use bevy_ecs::{ change_detection::Ref, component::Component, @@ -21,7 +22,7 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, + shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextRoot, TextSectionStyle, TextSpanAccess, TextWriter, }; @@ -165,6 +166,7 @@ impl Default for Text2dShadow { pub fn update_text2d_layout( mut target_scale_factors: Local>, mut textures: ResMut>, + fonts: Res>, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, @@ -244,12 +246,14 @@ pub fn update_text2d_layout( let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); - for (i, (_, _, text, font, _)) in text_reader.iter(entity).enumerate() { + for (i, (_, _, text, text_font, _)) in text_reader.iter(entity).enumerate() { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( - font.font.as_str(), - font.font_size, - font.line_height, + fonts + .get(text_font.font.id()) + .map(|font| font.family_name.as_str()), + text_font.font_size, + text_font.line_height, i as u32, )); } @@ -261,7 +265,7 @@ pub fn update_text2d_layout( &mut font_cx.0, &mut layout_cx.0, text_sections.iter().copied(), - text_section_styles.iter().copied(), + text_section_styles.iter(), scale_factor, block.linebreak, ); diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 229310433cd53..a753fbcb6f527 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -8,9 +8,9 @@ use bevy_ecs::message::MessageReader; use bevy_ecs::system::Query; use bevy_ecs::system::ResMut; use bevy_reflect::TypePath; +use bevy_utils::default; use parley::fontique::Blob; -use parley::fontique::FamilyId; -use parley::fontique::FontInfo; +use parley::fontique::FontInfoOverride; /// An [`Asset`] that contains the data for a loaded font, if loaded as an asset. /// @@ -27,15 +27,15 @@ use parley::fontique::FontInfo; #[derive(Debug, TypePath, Clone, Asset)] pub struct Font { pub blob: Blob, - pub collection: Vec<(FamilyId, Vec)>, + pub family_name: String, } impl Font { /// Creates a [`Font`] from bytes - pub fn try_from_bytes(font_data: Vec) -> Font { + pub fn try_from_bytes(font_data: Vec, family_name: String) -> Font { Font { blob: Blob::from(font_data), - collection: vec![], + family_name, } } } @@ -46,23 +46,25 @@ pub fn register_font_assets_system( mut events: MessageReader>, mut text_font_query: Query<&mut TextFont>, ) { - let mut change = false; for event in events.read() { match event { AssetEvent::Added { id } => { if let Some(font) = fonts.get_mut(*id) { - let collection = cx.collection.register_fonts(font.blob.clone(), None); - font.collection = collection; - change = true; + 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(); + } + } } } _ => {} } } - - if change { - for mut font in text_font_query.iter_mut() { - font.set_changed(); - } - } } diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index 4b6a056196228..d5b47d7c25d32 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -26,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.asset_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/layout.rs b/crates/bevy_text/src/layout.rs index 3b39edc3d9c3f..3a4bef73eb939 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/layout.rs @@ -44,9 +44,9 @@ fn concat_text_for_layout<'a>( } /// Resolved text style -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct TextSectionStyle<'a, B> { - font_family: &'a str, + family_name: Option<&'a str>, font_size: f32, line_height: crate::text::LineHeight, brush: B, @@ -54,10 +54,15 @@ pub struct TextSectionStyle<'a, B> { impl<'a, B: Brush> TextSectionStyle<'a, B> { /// new text section style - pub fn new(family: &'a str, size: f32, line_height: crate::LineHeight, brush: B) -> Self { + pub fn new( + family_id: Option<&'a str>, + font_size: f32, + line_height: crate::LineHeight, + brush: B, + ) -> Self { Self { - font_family: family, - font_size: size, + family_name: family_id, + font_size, line_height, brush, } @@ -70,7 +75,7 @@ pub fn shape_text_from_sections<'a, B: Brush>( font_cx: &'a mut FontContext, layout_cx: &'a mut LayoutContext, text_sections: impl Iterator, - text_section_styles: impl Iterator>, + text_section_styles: impl Iterator>, scale_factor: f32, line_break: crate::text::LineBreak, ) { @@ -85,8 +90,10 @@ pub fn shape_text_from_sections<'a, B: Brush>( builder.push_default(StyleProperty::WordBreak(word_break_strength)); }; for (style, range) in text_section_styles.zip(section_ranges) { - builder.push(StyleProperty::Brush(style.brush), range.clone()); - builder.push(FontStack::from(style.font_family), range.clone()); + if let Some(family) = style.family_name { + builder.push(FontStack::from(family), range.clone()); + }; + builder.push(StyleProperty::Brush(style.brush.clone()), range.clone()); builder.push(StyleProperty::FontSize(style.font_size), range.clone()); builder.push(style.line_height.eval(), range); } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 74b923d9b0e1c..17451427734b9 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -129,7 +129,8 @@ impl Plugin for TextPlugin { { use bevy_asset::{AssetId, Assets}; let mut assets = app.world_mut().resource_mut::>(); - let asset = Font::try_from_bytes(DEFAULT_FONT_DATA.to_vec()); + 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/text.rs b/crates/bevy_text/src/text.rs index 81f9391f75183..5c403f1c9fd81 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,5 +1,6 @@ use crate::Font; use crate::{PositionedGlyph, TextSpanAccess, TextSpanComponent, TextTarget}; +use bevy_asset::Handle; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; @@ -204,8 +205,8 @@ impl TextFont { } /// Returns this [`TextFont`] with the specified font face handle. - pub fn with_font(mut self, font: &str) -> Self { - self.font = font.to_string(); + pub fn with_font(mut self, font: Handle) -> Self { + self.font = font; self } @@ -228,12 +229,9 @@ impl TextFont { } } -impl From<&str> for TextFont { - fn from(font: &str) -> Self { - Self { - font: font.to_string(), - ..default() - } +impl From> for TextFont { + fn from(font: Handle) -> Self { + Self { font, ..default() } } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index ea05a9261cd13..cf05012a3142a 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -11,14 +11,14 @@ use bevy_ecs::{ entity::Entity, query::With, reflect::ReflectComponent, - system::{Query, ResMut}, + system::{Query, Res, ResMut}, world::Ref, }; use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, + shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; @@ -250,6 +250,7 @@ impl Measure for TextMeasure { pub fn shape_text_system( mut font_cx: ResMut, mut layout_cx: ResMut, + fonts: Res>, mut text_query: Query< ( Entity, @@ -296,12 +297,14 @@ pub fn shape_text_system( let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); - for (i, (_, _, text, font, _)) in text_reader.iter(entity).enumerate() { + for (i, (_, _, text, text_font, _)) in text_reader.iter(entity).enumerate() { text_sections.push(text); text_section_styles.push(TextSectionStyle::new( - font.font.as_str(), - font.font_size, - font.line_height, + fonts + .get(text_font.font.id()) + .map(|font| font.family_name.as_str()), + text_font.font_size, + text_font.line_height, i as u32, )); } @@ -311,7 +314,7 @@ pub fn shape_text_system( &mut font_cx.0, &mut layout_cx.0, text_sections.iter().copied(), - text_section_styles.iter().copied(), + text_section_styles.iter(), computed_target.scale_factor, block.linebreak, ); From e294c611a478058889ad8de30b65d85bf1b37d15 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 16:57:26 +0000 Subject: [PATCH 57/84] Added basic doc comments --- crates/bevy_text/src/font.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index a753fbcb6f527..5bcf5d727e82e 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -26,7 +26,9 @@ use parley::fontique::FontInfoOverride; /// Bevy currently loads a single font face as a single `Font` asset. #[derive(Debug, TypePath, Clone, Asset)] pub struct Font { + /// raw font data pub blob: Blob, + /// font family name pub family_name: String, } @@ -40,6 +42,7 @@ impl Font { } } +/// Register new font assets with Parley's FontContext. pub fn register_font_assets_system( mut cx: ResMut, mut fonts: ResMut>, From c6cb6f313007a156fdca661f2b4cf37949014b98 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 17:07:07 +0000 Subject: [PATCH 58/84] reverted example changes --- examples/stress_tests/many_text2d.rs | 7 +++---- examples/stress_tests/text_pipeline.rs | 12 ++---------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/examples/stress_tests/many_text2d.rs b/examples/stress_tests/many_text2d.rs index e5e817562312f..e444a69456034 100644 --- a/examples/stress_tests/many_text2d.rs +++ b/examples/stress_tests/many_text2d.rs @@ -131,7 +131,7 @@ fn setup(mut commands: Commands, font: Res, args: Res) { text2ds.push(( Text2d(random_text(&mut rng, &args)), - random_text_font(&mut rng, &args), + random_text_font(&mut rng, &args, font.0.clone()), TextColor(color.into()), TextLayout::new_with_justify(if args.center { Justify::Center @@ -178,7 +178,6 @@ fn print_counts( return; } - let font_id = font.0.id(); let num_atlases = font_atlas_set .iter() .map(|(_, atlases)| atlases.len()) @@ -194,7 +193,7 @@ fn print_counts( ); } -fn random_text_font(rng: &mut ChaCha8Rng, args: &Args) -> TextFont { +fn random_text_font(rng: &mut ChaCha8Rng, args: &Args, font: Handle) -> TextFont { let font_size = if args.many_font_sizes { *[10.0, 20.0, 30.0, 40.0, 50.0, 60.0].choose(rng).unwrap() } else { @@ -203,7 +202,7 @@ fn random_text_font(rng: &mut ChaCha8Rng, args: &Args) -> TextFont { TextFont { font_size, - font: "fira sans".to_string(), + font, ..default() } } diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 0e72485067df3..f2c43cb162978 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -31,25 +31,17 @@ fn main() { .run(); } -#[derive(Resource)] -struct Fonts(Vec>); - fn spawn(mut commands: Commands, asset_server: Res) { warn!(include_str!("warning_string.txt")); - let font_handle: Handle = asset_server.load("fonts/FiraMono-Medium.ttf"); commands.spawn(Camera2d); - commands.insert_resource(Fonts(vec![font_handle])); - - let font = "fira mono".to_string(); - let make_spans = |i| { [ ( TextSpan("text".repeat(i)), TextFont { - font: font.clone(), + font: asset_server.load("fonts/FiraMono-Medium.ttf"), font_size: (4 + i % 10) as f32, ..Default::default() }, @@ -58,7 +50,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { ( TextSpan("pipeline".repeat(i)), TextFont { - font: font.clone(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: (4 + i % 11) as f32, ..default() }, From 5bb31fe700ee77e473f8ecf549cb20305bbb8faa Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 18:41:36 +0000 Subject: [PATCH 59/84] Revert remaining systems --- crates/bevy_sprite/src/lib.rs | 2 +- crates/bevy_sprite/src/text2d.rs | 3 +- crates/bevy_sprite_render/src/render/mod.rs | 23 +- crates/bevy_sprite_render/src/text2d/mod.rs | 121 ++++------ .../src/texture_slice/computed_slices.rs | 3 - crates/bevy_text/src/lib.rs | 17 +- .../bevy_text/src/{layout.rs => pipeline.rs} | 16 +- crates/bevy_text/src/text.rs | 222 +++++++++++++----- crates/bevy_text/src/text_hierarchy.rs | 64 ----- crates/bevy_ui/src/lib.rs | 9 +- crates/bevy_ui_render/src/lib.rs | 49 ++-- 11 files changed, 270 insertions(+), 259 deletions(-) rename crates/bevy_text/src/{layout.rs => pipeline.rs} (92%) delete mode 100644 crates/bevy_text/src/text_hierarchy.rs diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index fc9a90f64721c..52bac51e6571e 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -86,12 +86,12 @@ impl Plugin for SpritePlugin { app.add_systems( PostUpdate, ( + bevy_text::detect_text_needs_rerender::, update_text2d_layout.after(bevy_camera::CameraUpdateSystems), calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) .chain() .in_set(bevy_text::Text2dUpdateSystems) - .after(TextSystems::Hierarchy) .after(TextSystems::RegisterFontAssets) .after(bevy_app::AnimationSystems), ); diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index a07a6f161ac30..286f4608f5766 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -24,7 +24,7 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, - TextLayoutInfo, TextReader, TextRoot, TextSectionStyle, TextSpanAccess, TextWriter, + TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -90,7 +90,6 @@ use core::any::TypeId; Visibility, VisibilityClass, ComputedTextBlock, - TextRoot, TextLayoutInfo, Transform, ComputedTextLayout diff --git a/crates/bevy_sprite_render/src/render/mod.rs b/crates/bevy_sprite_render/src/render/mod.rs index c9200c1330a9e..b618f26ad5a40 100644 --- a/crates/bevy_sprite_render/src/render/mod.rs +++ b/crates/bevy_sprite_render/src/render/mod.rs @@ -273,13 +273,13 @@ pub struct ExtractedSlice { pub offset: Vec2, pub rect: Rect, pub size: Vec2, - pub color: LinearRgba, } pub struct ExtractedSprite { pub main_entity: Entity, pub render_entity: Entity, pub transform: GlobalTransform, + pub color: LinearRgba, /// Change the on-screen size of the sprite /// Asset ID of the [`Image`] of this sprite /// PERF: storing an `AssetId` instead of `Handle` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped) @@ -296,7 +296,6 @@ pub enum ExtractedSpriteKind { rect: Option, scaling_mode: Option, custom_size: Option, - color: LinearRgba, }, /// Indexes into the list of [`ExtractedSlice`]s stored in the [`ExtractedSlices`] resource /// Used for elements composed from multiple sprites such as text or nine-patched borders @@ -357,15 +356,14 @@ pub fn extract_sprites( if let Some(slices) = slices { let start = extracted_slices.slices.len(); - extracted_slices.slices.extend(slices.extract_slices( - sprite, - anchor.as_vec(), - sprite.color.into(), - )); + extracted_slices + .slices + .extend(slices.extract_slices(sprite, anchor.as_vec())); let end = extracted_slices.slices.len(); extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, + color: sprite.color.into(), transform: *transform, flip_x: sprite.flip_x, flip_y: sprite.flip_y, @@ -394,6 +392,7 @@ pub fn extract_sprites( extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, + color: sprite.color.into(), transform: *transform, flip_x: sprite.flip_x, flip_y: sprite.flip_y, @@ -404,7 +403,6 @@ pub fn extract_sprites( scaling_mode: sprite.image_mode.scale(), // Pass the custom size custom_size: sprite.custom_size, - color: sprite.color.into(), }, }); } @@ -687,7 +685,6 @@ pub fn prepare_sprite_image_bind_groups( rect, scaling_mode, custom_size, - color, } => { // By default, the size of the quad is the size of the texture let mut quad_size = batch_image_size; @@ -748,7 +745,11 @@ pub fn prepare_sprite_image_bind_groups( // Store the vertex data and add the item to the render phase sprite_meta .sprite_instance_buffer - .push(SpriteInstance::from(&transform, &color, &uv_offset_scale)); + .push(SpriteInstance::from( + &transform, + &extracted_sprite.color, + &uv_offset_scale, + )); current_batch.as_mut().unwrap().get_mut().range.end += 1; index += 1; @@ -791,7 +792,7 @@ pub fn prepare_sprite_image_bind_groups( .sprite_instance_buffer .push(SpriteInstance::from( &transform, - &slice.color, + &extracted_sprite.color, &uv_offset_scale, )); diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index f353459c98faf..74e6a69823b5a 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -7,14 +7,13 @@ use bevy_color::LinearRgba; use bevy_ecs::{ entity::Entity, query::Has, - query::With, system::{Commands, Query, Res, ResMut}, }; use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::Extract; -use bevy_sprite::{Anchor, Text2d, Text2dShadow}; +use bevy_sprite::{Anchor, Text2dShadow}; use bevy_text::{ ComputedTextBlock, PositionedGlyph, Strikethrough, TextBackgroundColor, TextBounds, TextColor, TextLayoutInfo, Underline, @@ -29,21 +28,18 @@ pub fn extract_text2d_sprite( mut extracted_slices: ResMut, texture_atlases: Extract>>, text2d_query: Extract< - Query< - ( - Entity, - &ViewVisibility, - &TextLayoutInfo, - &TextBounds, - &Anchor, - Option<&Text2dShadow>, - &GlobalTransform, - &ComputedTextBlock, - ), - With, - >, + Query<( + Entity, + &ViewVisibility, + &ComputedTextBlock, + &TextLayoutInfo, + &TextBounds, + &Anchor, + Option<&Text2dShadow>, + &GlobalTransform, + )>, >, - text_colors_query: Extract>, + text_colors: Extract>, text_background_colors_query: Extract>, decoration_query: Extract, Has)>>, ) { @@ -53,12 +49,12 @@ pub fn extract_text2d_sprite( for ( main_entity, view_visibility, + computed_block, text_layout_info, text_bounds, anchor, maybe_shadow, global_transform, - computed_block, ) in text2d_query.iter() { let scaling = GlobalTransform::from_scale( @@ -75,13 +71,13 @@ pub fn extract_text2d_sprite( let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; - for section in text_layout_info.run_geometry.iter() { - let section_entity = computed_block[section.span_index]; + for &(section_index, rect, _, _, _) in text_layout_info.section_geometry.iter() { + let section_entity = computed_block.entities()[section_index].entity; let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { continue; }; let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new(section.bounds.center().x, -section.bounds.center().y); + let offset = Vec2::new(rect.center().x, -rect.center().y); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling @@ -90,6 +86,7 @@ pub fn extract_text2d_sprite( main_entity, render_entity, transform, + color: text_background_color.0.into(), image_handle_id: AssetId::default(), flip_x: false, flip_y: false, @@ -97,8 +94,7 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(section.bounds.size()), - color: text_background_color.0.into(), + custom_size: Some(rect.size()), }, }); } @@ -127,7 +123,6 @@ pub fn extract_text2d_sprite( offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), - color, }); if text_layout_info @@ -140,6 +135,7 @@ pub fn extract_text2d_sprite( main_entity, render_entity, transform: shadow_transform, + color, image_handle_id: atlas_info.texture, flip_x: false, flip_y: false, @@ -153,8 +149,10 @@ pub fn extract_text2d_sprite( end += 1; } - for section in text_layout_info.run_geometry.iter() { - let section_entity = computed_block[section.span_index]; + for &(section_index, rect, strikethrough_y, stroke, underline_y) in + text_layout_info.section_geometry.iter() + { + let section_entity = computed_block.entities()[section_index].entity; let Ok((_, has_strikethrough, has_underline)) = decoration_query.get(section_entity) else { @@ -163,16 +161,14 @@ pub fn extract_text2d_sprite( if has_strikethrough { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new( - section.bounds.center().x, - -section.strikethrough_y - 0.5 * section.strikethrough_thickness, - ); + let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke); let transform = shadow_transform * GlobalTransform::from_translation(offset.extend(0.)); extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, transform, + color, image_handle_id: AssetId::default(), flip_x: false, flip_y: false, @@ -180,27 +176,21 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new( - section.bounds.size().x, - section.strikethrough_thickness, - )), - color, + custom_size: Some(Vec2::new(rect.size().x, stroke)), }, }); } if has_underline { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new( - section.bounds.center().x, - -section.underline_y - 0.5 * section.underline_thickness, - ); + let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke); let transform = shadow_transform * GlobalTransform::from_translation(offset.extend(0.)); extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, transform, + color, image_handle_id: AssetId::default(), flip_x: false, flip_y: false, @@ -208,11 +198,7 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new( - section.bounds.size().x, - section.underline_thickness, - )), - color, + custom_size: Some(Vec2::new(rect.size().x, stroke)), }, }); } @@ -235,16 +221,17 @@ pub fn extract_text2d_sprite( ) in text_layout_info.glyphs.iter().enumerate() { if *span_index != current_span { - current_span = *span_index; - color = text_colors_query + color = text_colors .get( computed_block - .get(current_span) - .map(|t| *t) + .entities() + .get(*span_index) + .map(|t| t.entity) .unwrap_or(Entity::PLACEHOLDER), ) .map(|text_color| LinearRgba::from(text_color.0)) .unwrap_or_default(); + current_span = *span_index; } let rect = texture_atlases .get(atlas_info.texture_atlas) @@ -255,19 +242,17 @@ pub fn extract_text2d_sprite( offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), - color, }); - if text_layout_info - .glyphs - .get(i + 1) - .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) - { + if text_layout_info.glyphs.get(i + 1).is_none_or(|info| { + info.span_index != current_span || info.atlas_info.texture != atlas_info.texture + }) { let render_entity = commands.spawn(TemporaryRenderEntity).id(); extracted_sprites.sprites.push(ExtractedSprite { main_entity, render_entity, transform, + color, image_handle_id: atlas_info.texture, flip_x: false, flip_y: false, @@ -281,8 +266,10 @@ pub fn extract_text2d_sprite( end += 1; } - for section in text_layout_info.run_geometry.iter() { - let section_entity = computed_block[section.span_index]; + for &(section_index, rect, strikethrough_y, stroke, underline_y) in + text_layout_info.section_geometry.iter() + { + let section_entity = computed_block.entities()[section_index].entity; let Ok((text_color, has_strike_through, has_underline)) = decoration_query.get(section_entity) else { @@ -290,10 +277,7 @@ pub fn extract_text2d_sprite( }; if has_strike_through { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new( - section.bounds.center().x, - -section.strikethrough_y - 0.5 * section.strikethrough_thickness, - ); + let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling @@ -302,6 +286,7 @@ pub fn extract_text2d_sprite( main_entity, render_entity, transform, + color: text_color.0.into(), image_handle_id: AssetId::default(), flip_x: false, flip_y: false, @@ -309,21 +294,14 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new( - section.bounds.size().x, - section.strikethrough_thickness, - )), - color: text_color.0.into(), + custom_size: Some(Vec2::new(rect.size().x, stroke)), }, }); } if has_underline { let render_entity = commands.spawn(TemporaryRenderEntity).id(); - let offset = Vec2::new( - section.bounds.center().x, - -section.underline_y - 0.5 * section.underline_thickness, - ); + let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke); let transform = *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling @@ -332,6 +310,7 @@ pub fn extract_text2d_sprite( main_entity, render_entity, transform, + color: text_color.0.into(), image_handle_id: AssetId::default(), flip_x: false, flip_y: false, @@ -339,11 +318,7 @@ pub fn extract_text2d_sprite( anchor: Vec2::ZERO, rect: None, scaling_mode: None, - custom_size: Some(Vec2::new( - section.bounds.size().x, - section.underline_thickness, - )), - color: text_color.0.into(), + custom_size: Some(Vec2::new(rect.size().x, stroke)), }, }); } diff --git a/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs index 86fe8fcf4815f..55324faa658b0 100644 --- a/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs @@ -1,6 +1,5 @@ use crate::{ExtractedSlice, TextureAtlasLayout}; use bevy_asset::{AssetEvent, Assets}; -use bevy_color::LinearRgba; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_math::{Rect, Vec2}; @@ -24,7 +23,6 @@ impl ComputedTextureSlices { &'a self, sprite: &'a Sprite, anchor: Vec2, - color: LinearRgba, ) -> impl ExactSizeIterator + 'a { let mut flip = Vec2::ONE; if sprite.flip_x { @@ -41,7 +39,6 @@ impl ComputedTextureSlices { offset: slice.offset * flip - anchor, rect: slice.texture_rect, size: slice.draw_size, - color, }) } } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 17451427734b9..5a49a49bccaf9 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -39,10 +39,9 @@ mod font_atlas; mod font_atlas_set; mod font_loader; mod glyph; -mod layout; +mod pipeline; mod text; mod text_access; -mod text_hierarchy; pub use bounds::*; pub use context::*; @@ -52,10 +51,9 @@ pub use font_atlas::*; pub use font_atlas_set::*; pub use font_loader::*; pub use glyph::*; -pub use layout::*; +pub use pipeline::*; pub use text::*; pub use text_access::*; -pub use text_hierarchy::*; /// The text prelude. /// @@ -94,8 +92,6 @@ pub type Update2dText = Text2dUpdateSystems; /// Text Systems set #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum TextSystems { - /// Update text hierarchy and check for changes - Hierarchy, /// Register new font assets with Parley's `FontContext` after loading RegisterFontAssets, } @@ -109,15 +105,6 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() - .add_systems( - PostUpdate, - ( - update_text_entities_system, - detect_text_spans_needs_rerender, - ) - .chain() - .in_set(TextSystems::Hierarchy), - ) .add_systems( PostUpdate, register_font_assets_system diff --git a/crates/bevy_text/src/layout.rs b/crates/bevy_text/src/pipeline.rs similarity index 92% rename from crates/bevy_text/src/layout.rs rename to crates/bevy_text/src/pipeline.rs index 3a4bef73eb939..8d0d9e6004f1a 100644 --- a/crates/bevy_text/src/layout.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -4,7 +4,6 @@ use crate::FontAtlasKey; use crate::FontAtlasSet; use crate::FontSmoothing; use crate::GlyphCacheKey; -use crate::RunGeometry; use crate::TextLayoutInfo; use bevy_asset::Assets; use bevy_image::Image; @@ -186,20 +185,19 @@ pub fn update_text_layout_info( }); } - info.run_geometry.push(RunGeometry { - span_index: span_index as usize, - bounds: Rect { + info.section_geometry.push(( + span_index as usize, + 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, - }); + glyph_run.baseline() - run.metrics().strikethrough_offset, + run.metrics().strikethrough_size, + glyph_run.baseline() - run.metrics().underline_offset, + )); } _ => {} } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 5c403f1c9fd81..a7929c0e4f804 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,14 +1,17 @@ use crate::Font; -use crate::{PositionedGlyph, TextSpanAccess, TextSpanComponent, TextTarget}; +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; use parley::Layout; use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; /// A sub-entity of a [`ComputedTextBlock`]. /// @@ -22,6 +25,58 @@ pub struct TextEntity { pub depth: usize, } +/// Computed information for a text block. +/// +/// See [`TextLayout`]. +/// +/// Automatically updated by 2d and UI text systems. +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct ComputedTextBlock { + /// 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]>, + /// Flag set when any change has been made to this block that should cause it to be rerendered. + /// + /// Includes: + /// - [`TextLayout`] changes. + /// - [`TextFont`] or `Text2d`/`Text`/`TextSpan` changes anywhere in the block's entity hierarchy. + // TODO: This encompasses both structural changes like font size or justification and non-structural + // changes like text color and font smoothing. This field currently causes UI to 'remeasure' text, even if + // 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, +} + +impl ComputedTextBlock { + /// Accesses entities in this block. + /// + /// Can be used to look up [`TextFont`] components for glyphs in [`TextLayoutInfo`] using the `span_index` + /// stored there. + pub fn entities(&self) -> &[TextEntity] { + &self.entities + } + + /// Indicates if the text needs to be refreshed in [`TextLayoutInfo`]. + /// + /// Updated automatically by [`detect_text_needs_rerender`] and cleared + /// by [`TextPipeline`](crate::TextPipeline) methods. + pub fn needs_rerender(&self) -> bool { + self.needs_rerender + } +} + +impl Default for ComputedTextBlock { + fn default() -> Self { + Self { + entities: SmallVec::default(), + needs_rerender: true, + } + } +} + /// Component with text format settings for a block of text. /// /// A block of text is composed of text spans, which each have a separate string value and [`TextFont`]. Text @@ -91,7 +146,7 @@ impl TextLayout { /// but each node has its own [`TextFont`] and [`TextColor`]. #[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug, Clone)] -#[require(TextFont, TextColor, TextTarget)] +#[require(TextFont, TextColor)] pub struct TextSpan(pub String); impl TextSpan { @@ -372,61 +427,13 @@ pub struct TextLayoutInfo { 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. - pub run_geometry: Vec, + /// Geometry of each text segment: (section index, bounding rect, strikethrough offset, stroke thickness, underline offset) + /// A text section spanning more than one line will have multiple segments. + pub section_geometry: Vec<(usize, Rect, f32, f32, f32)>, /// 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) - } -} - /// Determines which antialiasing method to use when rendering text. By default, text is /// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look. /// @@ -454,3 +461,112 @@ pub enum FontSmoothing { /// Computed text layout #[derive(Component, Default, Deref, DerefMut)] pub struct ComputedTextLayout(pub Layout); + +// 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. +pub fn detect_text_needs_rerender( + changed_roots: Query< + Entity, + ( + Or<( + Changed, + Changed, + Changed, + Changed, + )>, + With, + With, + With, + ), + >, + changed_spans: Query< + (Entity, Option<&ChildOf>, Has), + ( + Or<( + Changed, + Changed, + Changed, + Changed, // Included to detect broken text block hierarchies. + Added, + )>, + With, + With, + ), + >, + mut computed: Query<( + Option<&ChildOf>, + Option<&mut ComputedTextBlock>, + Has, + )>, +) { + // Root entity: + // - Root component changed. + // - TextFont on root changed. + // - TextLayout changed. + // - Root children changed (can include additions and removals). + for root in changed_roots.iter() { + let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else { + once!(warn!("found entity {} with a root text component ({}) but no ComputedTextBlock; this warning only \ + prints once", root, core::any::type_name::())); + continue; + }; + computed.needs_rerender = true; + } + + // Span entity: + // - Span component changed. + // - Span TextFont changed. + // - Span children changed (can include additions and removals). + for (entity, maybe_span_child_of, has_text_block) in changed_spans.iter() { + if has_text_block { + once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \ + text entities (that have {}); this warning only prints once", + entity, core::any::type_name::())); + } + + let Some(span_child_of) = maybe_span_child_of else { + once!(warn!( + "found entity {} with a TextSpan that has no parent; it should have an ancestor \ + with a root text component ({}); this warning only prints once", + entity, + core::any::type_name::() + )); + continue; + }; + let mut parent: Entity = span_child_of.parent(); + + // Search for the nearest ancestor with ComputedTextBlock. + // Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited + // is outweighed by the expense of tracking visited spans. + loop { + let Ok((maybe_child_of, maybe_computed, has_span)) = computed.get_mut(parent) else { + once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a ChildOf \ + component that points at non-existent entity {}; this warning only prints once", + entity, parent)); + break; + }; + if let Some(mut computed) = maybe_computed { + computed.needs_rerender = true; + break; + } + if !has_span { + once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \ + span component or a ComputedTextBlock component; this warning only prints once", + entity, parent)); + break; + } + let Some(next_child_of) = maybe_child_of else { + once!(warn!( + "found entity {} with a TextSpan that has no ancestor with the root text \ + component ({}); this warning only prints once", + entity, + core::any::type_name::() + )); + break; + }; + parent = next_child_of.parent(); + } + } +} diff --git a/crates/bevy_text/src/text_hierarchy.rs b/crates/bevy_text/src/text_hierarchy.rs deleted file mode 100644 index 3e7ac1ccbab7b..0000000000000 --- a/crates/bevy_text/src/text_hierarchy.rs +++ /dev/null @@ -1,64 +0,0 @@ -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::*; - -use crate::TextSpan; - -/// Contains the entities comprising a text hierarchy. -/// In order as read, starting from the root text element. -#[derive(Component, Default, Deref, DerefMut)] -pub struct ComputedTextBlock(pub Vec); - -#[derive(Component, Default)] -/// Root text element -pub struct TextRoot; - -/// Output target id -#[derive(Component, Debug, PartialEq, Deref)] -pub struct TextTarget(Entity); - -impl Default for TextTarget { - fn default() -> Self { - Self(Entity::PLACEHOLDER) - } -} - -/// update text entities lists -pub fn update_text_entities_system( - mut buffer: Local>, - mut root_query: Query<(Entity, &mut ComputedTextBlock, Option<&Children>)>, - mut targets_query: Query<&mut TextTarget>, - children_query: Query<&Children, With>, -) { - for (root_id, mut entities, maybe_children) in root_query.iter_mut() { - buffer.push(root_id); - if let Some(children) = maybe_children { - for entity in children.iter() { - buffer.push(entity); - for entity in children_query.iter_descendants_depth_first(root_id) { - buffer.push(entity); - } - } - } - if buffer.as_slice() != entities.0.as_slice() { - entities.0.clear(); - entities.0.extend_from_slice(&buffer); - let mut targets_iter = targets_query.iter_many_mut(entities.0.iter()); - while let Some(mut target) = targets_iter.fetch_next() { - target.0 = root_id; - } - } - buffer.clear(); - } -} - -/// detect changes -pub fn detect_text_spans_needs_rerender( - text_query: Query<&TextTarget, Or<(Changed, Changed)>>, - mut output_query: Query<&mut ComputedTextBlock>, -) { - for target in text_query.iter() { - if let Ok(mut computed) = output_query.get_mut(target.0) { - computed.set_changed(); - } - } -} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index a57d864d0fc63..385a620fe4a05 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -224,12 +224,16 @@ impl Plugin for UiPlugin { } fn build_text_interop(app: &mut App) { + use widget::Text; + app.add_systems( PostUpdate, ( - (widget::shape_text_system,) + ( + bevy_text::detect_text_needs_rerender::, + widget::shape_text_system, + ) .chain() - .after(TextSystems::Hierarchy) .after(TextSystems::RegisterFontAssets) .in_set(UiSystems::Content) // Text and Text2d are independent. @@ -241,7 +245,6 @@ fn build_text_interop(app: &mut App) { // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(widget::update_image_content_size_system), widget::layout_text_system - .after(TextSystems::Hierarchy) .after(TextSystems::RegisterFontAssets) .in_set(UiSystems::PostLayout) //.after(bevy_text::free_unused_font_atlases_system) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 73fb7c636deff..209dc84a9eec9 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -960,7 +960,8 @@ pub fn extract_text_sections( ) in text_layout_info.glyphs.iter().enumerate() { if current_span_index != *span_index - && let Some(span_entity) = computed_block.0.get(*span_index).copied() + && let Some(span_entity) = + computed_block.entities().get(*span_index).map(|t| t.entity) { color = text_styles .get(span_entity) @@ -1093,8 +1094,10 @@ pub fn extract_text_shadows( end += 1; } - for section in text_layout_info.run_geometry.iter() { - let section_entity = computed_block[section.span_index]; + for &(section_index, rect, strikethrough_y, stroke, underline_y) in + text_layout_info.section_geometry.iter() + { + let section_entity = computed_block.entities()[section_index].entity; let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity) else { continue; @@ -1109,17 +1112,14 @@ pub fn extract_text_shadows( extracted_camera_entity, transform: node_transform * Affine2::from_translation(Vec2::new( - section.bounds.center().x, - section.strikethrough_y + 0.5 * section.strikethrough_thickness, + rect.center().x, + strikethrough_y + 0.5 * stroke, )), item: ExtractedUiItem::Node { color: shadow.color.into(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new( - section.bounds.size().x, - section.strikethrough_thickness, - ), + max: Vec2::new(rect.size().x, stroke), }, atlas_scaling: None, flip_x: false, @@ -1141,14 +1141,14 @@ pub fn extract_text_shadows( extracted_camera_entity, transform: node_transform * Affine2::from_translation(Vec2::new( - section.bounds.center().x, - section.underline_y + 0.5 * section.underline_thickness, + rect.center().x, + underline_y + 0.5 * stroke, )), item: ExtractedUiItem::Node { color: shadow.color.into(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(section.bounds.size().x, section.underline_thickness), + max: Vec2::new(rect.size().x, stroke), }, atlas_scaling: None, flip_x: false, @@ -1211,8 +1211,10 @@ pub fn extract_text_decorations( let transform = Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); - for section in text_layout_info.run_geometry.iter() { - let section_entity = computed_block[section.span_index]; + for &(section_index, rect, strikethrough_y, stroke, underline_y) in + text_layout_info.section_geometry.iter() + { + let section_entity = computed_block.entities()[section_index].entity; let Ok(((text_background_color, maybe_strikethrough, maybe_underline), text_color)) = text_background_colors_query.get(section_entity) else { @@ -1226,12 +1228,12 @@ pub fn extract_text_decorations( clip: clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(section.bounds.center()), + transform: transform * Affine2::from_translation(rect.center()), item: ExtractedUiItem::Node { color: text_background_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: section.bounds.size(), + max: rect.size(), }, atlas_scaling: None, flip_x: false, @@ -1253,17 +1255,14 @@ pub fn extract_text_decorations( extracted_camera_entity, transform: transform * Affine2::from_translation(Vec2::new( - section.bounds.center().x, - section.strikethrough_y + 0.5 * section.strikethrough_thickness, + rect.center().x, + strikethrough_y + 0.5 * stroke, )), item: ExtractedUiItem::Node { color: text_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new( - section.bounds.size().x, - section.strikethrough_thickness, - ), + max: Vec2::new(rect.size().x, stroke), }, atlas_scaling: None, flip_x: false, @@ -1285,14 +1284,14 @@ pub fn extract_text_decorations( extracted_camera_entity, transform: transform * Affine2::from_translation(Vec2::new( - section.bounds.center().x, - section.underline_y + 0.5 * section.underline_thickness, + rect.center().x, + underline_y + 0.5 * stroke, )), item: ExtractedUiItem::Node { color: text_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, - max: Vec2::new(section.bounds.size().x, section.underline_thickness), + max: Vec2::new(rect.size().x, stroke), }, atlas_scaling: None, flip_x: false, From bd97a43e335448b1ea41fdb7aa414d11d240bd9a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 19:03:14 +0000 Subject: [PATCH 60/84] Collect entities in ComputedTextBlock. Fixed system ambiguities with aaccessibility. --- crates/bevy_sprite/src/text2d.rs | 13 +++++++++---- crates/bevy_text/src/text.rs | 2 +- crates/bevy_ui/src/accessibility.rs | 6 +++--- crates/bevy_ui/src/widget/text.rs | 19 +++++++++++++------ 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 286f4608f5766..4ea414200dff5 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -23,8 +23,8 @@ use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, - FontAtlasSet, FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, - TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, + FontAtlasSet, FontCx, LayoutCx, ScaleCx, TextBounds, TextColor, TextEntity, TextFont, TextHead, + TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -208,7 +208,7 @@ pub fn update_text2d_layout( block, bounds, text_layout_info, - computed, + mut computed, mut clayout, text2d, text_font, @@ -245,7 +245,12 @@ pub fn update_text2d_layout( let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); - for (i, (_, _, text, text_font, _)) in text_reader.iter(entity).enumerate() { + for (i, (section_entity, depth, text, text_font, _)) in text_reader.iter(entity).enumerate() + { + computed.entities.push(TextEntity { + entity: section_entity, + depth, + }); text_sections.push(text); text_section_styles.push(TextSectionStyle::new( fonts diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index a7929c0e4f804..7cb2d9e2cd411 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -36,7 +36,7 @@ pub struct ComputedTextBlock { /// 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: diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 81c78a50a2da8..52d6394df9551 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/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index cf05012a3142a..aa4c25c1d1503 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -19,8 +19,9 @@ use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, - FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextFont, TextHead, - TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, + FontAtlasSet, FontCx, LayoutCx, LineBreak, ScaleCx, TextBounds, TextColor, TextEntity, + TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, + TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; @@ -257,7 +258,7 @@ pub fn shape_text_system( Ref, &mut ContentSize, &mut TextNodeFlags, - Ref, + &mut ComputedTextBlock, &mut ComputedTextLayout, Ref, &ComputedNode, @@ -273,7 +274,7 @@ pub fn shape_text_system( block, mut content_size, mut text_flags, - computed_block, + mut computed_block, mut computed_layout, computed_target, computed_node, @@ -286,7 +287,7 @@ pub fn shape_text_system( if !(1e-5 < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() - || computed_block.is_changed() + || computed_block.needs_rerender() || text_flags.needs_shaping || content_size.is_added()) || text.is_changed() @@ -295,9 +296,15 @@ pub fn shape_text_system( continue; } + computed_block.entities.clear(); let mut text_sections: Vec<&str> = Vec::new(); let mut text_section_styles: Vec> = Vec::new(); - for (i, (_, _, text, text_font, _)) in text_reader.iter(entity).enumerate() { + for (i, (section_entity, depth, text, text_font, _)) in text_reader.iter(entity).enumerate() + { + computed_block.entities.push(TextEntity { + entity: section_entity, + depth, + }); text_sections.push(text); text_section_styles.push(TextSectionStyle::new( fonts From e3e5e581ce92eebe330b5e27b352ef3259bca6c1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Oct 2025 23:44:47 +0000 Subject: [PATCH 61/84] Fixed some examples --- examples/2d/text2d.rs | 88 +--------------------- examples/ui/strikethrough_and_underline.rs | 11 +-- examples/ui/text_wrap_debug.rs | 4 +- 3 files changed, 7 insertions(+), 96 deletions(-) diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 9c292f84d77f3..6a961a43df9d3 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -16,7 +16,6 @@ use bevy::{ fn main() { App::new() .add_plugins(DefaultPlugins) - //.add_systems(Startup, glyph_setup3) .add_systems(Startup, setup) .add_systems( Update, @@ -25,83 +24,6 @@ fn main() { .run(); } -fn glyph_setup1(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2d); - let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); - commands.insert_resource(FontHolder(font)); - let text_font = TextFont { - font: "Fira Sans".to_string(), - font_size: 50.0, - ..default() - }; - commands.spawn(( - Text2d::new("a"), - text_font.clone(), - TextBackgroundColor(MAGENTA.into()), - )); -} - -fn glyph_setup2(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2d); - let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); - commands.insert_resource(FontHolder(font)); - let text_font = TextFont { - font: "Fira Sans".to_string(), - font_size: 50.0, - ..default() - }; - commands - .spawn(( - Text2d::new("a"), - text_font.clone(), - TextColor(MAGENTA.into()), - )) - .with_child(( - TextSpan::new("b"), - text_font.clone(), - TextColor(BLUE.into()), - )); -} - -fn glyph_setup3(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2d); - let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); - commands.insert_resource(FontHolder(font)); - let text_font = TextFont { - font: "Fira Sans".to_string(), - font_size: 50.0, - ..default() - }; - commands - .spawn(( - Text2d::new("aa"), - text_font.clone(), - TextColor(MAGENTA.into()), - )) - .with_child(( - TextSpan::new("bbb"), - text_font.clone(), - TextColor(BLUE.into()), - )) - .with_child((TextSpan::new("c"), text_font.clone(), TextColor(RED.into()))); -} - -fn hello_setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2d); - let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); - commands.insert_resource(FontHolder(font)); - let text_font = TextFont { - font: "Fira Sans".to_string(), - font_size: 50.0, - ..default() - }; - commands.spawn(( - Text2d::new("hello\nworld"), - text_font.clone(), - TextBackgroundColor(MAGENTA.into()), - )); -} - #[derive(Component)] struct AnimateTranslation; @@ -111,14 +33,10 @@ struct AnimateRotation; #[derive(Component)] struct AnimateScale; -#[derive(Resource)] -struct FontHolder(Handle); - fn setup(mut commands: Commands, asset_server: Res) { - let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); - commands.insert_resource(FontHolder(font)); + let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let text_font = TextFont { - font: "Fira Sans".to_string(), + font: font.clone(), font_size: 50.0, ..default() }; @@ -154,7 +72,7 @@ fn setup(mut commands: Commands, asset_server: Res) { )); // Demonstrate text wrapping let slightly_smaller_text_font = TextFont { - font: "Fira Sans".to_string(), + font, font_size: 35.0, ..default() }; diff --git a/examples/ui/strikethrough_and_underline.rs b/examples/ui/strikethrough_and_underline.rs index 91cbbf1ada178..2995350ab8f9c 100644 --- a/examples/ui/strikethrough_and_underline.rs +++ b/examples/ui/strikethrough_and_underline.rs @@ -12,21 +12,14 @@ fn main() { .run(); } -#[derive(Resource)] -struct Fonts(Vec>); - fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); - commands.insert_resource(Fonts(vec![ - asset_server.load("fonts/FiraSans-Bold.ttf"), - asset_server.load("fonts/FiraMono-Medium.ttf"), - ])); commands.spawn(( Text::new("struck\nstruck"), // Just add the `Strikethrough` component to any `Text`, `Text2d` or `TextSpan` and its text will be struck through Strikethrough, TextFont { - font: "fira sans".to_string(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 67.0, ..default() }, @@ -79,7 +72,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("2struck\nstruck"), Strikethrough, TextFont { - font: "fira sans".to_string(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 67.0, ..default() }, diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 6b5319ad38446..212a820abca75 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -43,9 +43,9 @@ fn main() { fn spawn(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); - let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + let text_font = TextFont { - font: "fira sans".to_string(), + font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 12.0, ..default() }; From c6a8896da2af57ede60841636c64c485665d3869 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 30 Oct 2025 15:41:10 +0000 Subject: [PATCH 62/84] Removed unneeded changed checks in text2d --- crates/bevy_sprite/src/text2d.rs | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 3df717fecb079..2a424d4115b93 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -182,8 +182,6 @@ pub fn update_text2d_layout( &mut TextLayoutInfo, &mut ComputedTextBlock, &mut ComputedTextLayout, - Ref, - Ref, )>, mut text_reader: Text2dReader, ) { @@ -204,17 +202,8 @@ 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, - mut clayout, - text2d, - text_font, - ) in &mut text_query + for (entity, maybe_entity_mask, block, bounds, text_layout_info, mut computed, mut layout) in + &mut text_query { let entity_mask = maybe_entity_mask.unwrap_or_default(); @@ -235,13 +224,10 @@ pub fn update_text2d_layout( *scale_factor }; - if !(computed.is_changed() - || computed.needs_rerender() + if !(computed.needs_rerender() || block.is_changed() || bounds.is_changed() || scale_factor != text_layout_info.scale_factor) - || text2d.is_changed() - || text_font.is_changed() { continue; } @@ -272,7 +258,7 @@ pub fn update_text2d_layout( let text_layout_info = text_layout_info.into_inner(); shape_text_from_sections( - &mut clayout.0, + &mut layout.0, &mut font_cx.0, &mut layout_cx.0, text_sections.iter().copied(), @@ -282,7 +268,7 @@ pub fn update_text2d_layout( ); *text_layout_info = update_text_layout_info( - &mut clayout.0, + &mut layout.0, bounds.width.map(|w| w * scale_factor), block.justify.into(), &mut scale_cx, From fcceae9e072c4cc464e3fad709f670266b9bc841 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 30 Oct 2025 16:10:43 +0000 Subject: [PATCH 63/84] Removed redundant change detection and queries from text widget --- crates/bevy_ui/src/widget/text.rs | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 677bfd647ff5d..2d9188fac1422 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -9,7 +9,6 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - query::With, reflect::ReflectComponent, system::{Query, Res, ResMut}, world::Ref, @@ -253,21 +252,16 @@ pub fn shape_text_system( mut font_cx: ResMut, mut layout_cx: ResMut, fonts: Res>, - mut text_query: Query< - ( - Entity, - Ref, - &mut ContentSize, - &mut TextNodeFlags, - &mut ComputedTextBlock, - &mut ComputedTextLayout, - Ref, - &ComputedNode, - Ref, - Ref, - ), - With, - >, + mut text_query: Query<( + Entity, + Ref, + &mut ContentSize, + &mut TextNodeFlags, + &mut ComputedTextBlock, + &mut ComputedTextLayout, + Ref, + &ComputedNode, + )>, mut text_reader: TextUiReader, ) { for ( @@ -279,8 +273,6 @@ pub fn shape_text_system( mut computed_layout, computed_target, computed_node, - text, - text_font, ) in &mut text_query { // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). @@ -288,12 +280,9 @@ pub fn shape_text_system( if !(1e-5 < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() - || computed_block.is_changed() || computed_block.needs_rerender() || text_flags.needs_shaping || content_size.is_added()) - || text.is_changed() - || text_font.is_changed() { continue; } From 6346f10588dbd688f3016c5756af311e08da3c93 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 4 Nov 2025 13:13:03 +0000 Subject: [PATCH 64/84] use parley fast-line-height changes --- crates/bevy_text/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 5cc4f4aaa178b..c1ef33ed4dc11 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -35,7 +35,8 @@ 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.6.0", default-features = true } +parley = { git = "https://github.com/linebender/parley", rev = "37a99ef" } +# parley = { version = "0.6.0", default-features = true } swash = { version = "0.2.6", default-features = true } [lints] From 3fdc0eb980da8a1c53b149f9fb50df33cb1857cb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 6 Nov 2025 12:19:07 +0000 Subject: [PATCH 65/84] Reverted changes to text_debug example --- examples/ui/text_debug.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index fd1020fcd2173..84e4db4e52b43 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -30,13 +30,8 @@ fn main() { #[derive(Component)] struct TextChanges; -#[derive(Resource)] -struct Fonts(Vec>); - fn infotext_system(mut commands: Commands, asset_server: Res) { - let font_handle: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); - commands.insert_resource(Fonts(vec![font_handle])); - let font = "fira sans".to_string(); + let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let background_color = MAROON.into(); commands.spawn(Camera2d); From 3e472cf4a67760d0a6f0acb87a3983bd40b01424 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 6 Nov 2025 13:19:19 +0000 Subject: [PATCH 66/84] Call `path` not `asset_path` on LoadContext in font_loader. --- crates/bevy_text/src/font_loader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index d5b47d7c25d32..8808cdd8b5f82 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -28,7 +28,7 @@ impl AssetLoader for FontLoader { _settings: &(), load_context: &mut LoadContext<'_>, ) -> Result { - let path = load_context.asset_path().to_string(); + 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, path); From 5cee541c7a15b6c4bb21593656bb36aae0efd651 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 18:24:42 +0000 Subject: [PATCH 67/84] Reimplemented font features for Parley --- crates/bevy_sprite/src/text2d.rs | 3 +++ crates/bevy_text/src/error.rs | 2 +- crates/bevy_text/src/font.rs | 3 +-- crates/bevy_text/src/font_atlas.rs | 2 +- crates/bevy_text/src/font_loader.rs | 1 - crates/bevy_text/src/pipeline.rs | 12 +++++++++++- crates/bevy_ui/src/widget/text.rs | 2 ++ 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 2a424d4115b93..7f1fcee105695 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -29,6 +29,7 @@ use bevy_text::{ }; use bevy_transform::components::Transform; use core::any::TypeId; +use std::arch::x86_64::_MM_FROUND_TO_NEG_INF; /// The top-level 2D text component. /// @@ -245,12 +246,14 @@ pub fn update_text2d_layout( depth, }); text_sections.push(text); + let font_features: Vec<_> = (&text_font.font_features).into(); text_section_styles.push(TextSectionStyle::new( fonts .get(text_font.font.id()) .map(|font| font.family_name.as_str()), text_font.font_size, line_height, + font_features, i as u32, )); } diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 4da5e97379360..e4f8e83aaba05 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -12,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 b68c299b7271d..5bcf5d727e82e 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -34,8 +34,7 @@ pub struct Font { impl Font { /// Creates a [`Font`] from bytes - pub fn try_from_bytes(font_data: Vec, family_name: String) -> Result { - let _ = swash::FontRef::from_index(&font_data, 0)?; + pub fn try_from_bytes(font_data: Vec, family_name: String) -> Font { Font { blob: Blob::from(font_data), family_name, diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 8efc9517476b4..a51e5dcc7dda7 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -197,7 +197,7 @@ pub fn get_outlined_glyph_texture( ]) .format(swash::zeno::Format::Alpha) .render(scaler, glyph_id) - .ok_or(TextError::FailedToGetGlyphImage)?; + .ok_or(TextError::FailedToGetGlyphImage(glyph_id))?; let left = image.placement.left; let top = image.placement.top; diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index 078a31611f487..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)] diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 3a4bef73eb939..bd32e67ca0573 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -17,6 +17,8 @@ use parley::Alignment; use parley::AlignmentOptions; use parley::Brush; use parley::FontContext; +use parley::FontFeature; +use parley::FontSettings; use parley::FontStack; use parley::Layout; use parley::LayoutContext; @@ -49,6 +51,7 @@ pub struct TextSectionStyle<'a, B> { family_name: Option<&'a str>, font_size: f32, line_height: crate::text::LineHeight, + font_features: Vec, brush: B, } @@ -58,6 +61,7 @@ impl<'a, B: Brush> TextSectionStyle<'a, B> { family_id: Option<&'a str>, font_size: f32, line_height: crate::LineHeight, + font_features: Vec, brush: B, ) -> Self { Self { @@ -65,6 +69,7 @@ impl<'a, B: Brush> TextSectionStyle<'a, B> { font_size, line_height, brush, + font_features, } } } @@ -95,7 +100,12 @@ pub fn shape_text_from_sections<'a, B: Brush>( }; builder.push(StyleProperty::Brush(style.brush.clone()), range.clone()); builder.push(StyleProperty::FontSize(style.font_size), range.clone()); - builder.push(style.line_height.eval(), range); + builder.push(style.line_height.eval(), range.clone()); + let font_features: &[FontFeature] = &style.font_features; + builder.push( + StyleProperty::FontFeatures(FontSettings::from(font_features)), + range, + ); } builder.build_into(layout, &text); } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 2d9188fac1422..c82debd5f4a0e 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -297,6 +297,7 @@ pub fn shape_text_system( entity: section_entity, depth, }); + let font_features: Vec<_> = (&text_font.font_features).into(); text_sections.push(text); text_section_styles.push(TextSectionStyle::new( fonts @@ -304,6 +305,7 @@ pub fn shape_text_system( .map(|font| font.family_name.as_str()), text_font.font_size, line_height, + font_features, i as u32, )); } From b44242aac56a4b6e103b3d319e870db313136a5d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 18:25:10 +0000 Subject: [PATCH 68/84] updated parley version to 0.7.0 --- crates/bevy_text/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index c1ef33ed4dc11..5f264f2a67384 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -35,9 +35,8 @@ 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 = { git = "https://github.com/linebender/parley", rev = "37a99ef" } -# parley = { version = "0.6.0", default-features = true } -swash = { version = "0.2.6", default-features = true } +parley = { version = "0.7.0" } +swash = { version = "0.2.6" } [lints] workspace = true From 59c18153f7e393ff933060466e4eda82adb41c0d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 18:30:18 +0000 Subject: [PATCH 69/84] Removed unused import --- crates/bevy_sprite/src/text2d.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 7f1fcee105695..ded89ff28befc 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -29,7 +29,6 @@ use bevy_text::{ }; use bevy_transform::components::Transform; use core::any::TypeId; -use std::arch::x86_64::_MM_FROUND_TO_NEG_INF; /// The top-level 2D text component. /// From 876c267dfa19fa4e30be9882749820b183fd5d58 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 19:41:46 +0000 Subject: [PATCH 70/84] Eliminate some allocations and cloning by passing the `TextReader` into the shaping function. --- crates/bevy_sprite/src/text2d.rs | 37 +++++-------------- crates/bevy_text/src/pipeline.rs | 59 +++++++++++++++++++++++++++++++ crates/bevy_ui/src/widget/text.rs | 31 ++++------------ 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index ded89ff28befc..afc5143a095d2 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,10 +22,9 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, - FontAtlasSet, FontCx, LayoutCx, LineHeight, ScaleCx, TextBounds, TextColor, TextEntity, - TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, - TextWriter, + shape_text_from_reader, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, + FontAtlasSet, FontCx, LayoutCx, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, + TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -235,38 +234,18 @@ pub fn update_text2d_layout( computed.needs_rerender = false; computed.entities.clear(); - let mut text_sections: Vec<&str> = Vec::new(); - let mut text_section_styles: Vec> = Vec::new(); - for (i, (section_entity, depth, text, text_font, _, line_height)) in - text_reader.iter(entity).enumerate() - { - computed.entities.push(TextEntity { - entity: section_entity, - depth, - }); - text_sections.push(text); - let font_features: Vec<_> = (&text_font.font_features).into(); - text_section_styles.push(TextSectionStyle::new( - fonts - .get(text_font.font.id()) - .map(|font| font.family_name.as_str()), - text_font.font_size, - line_height, - font_features, - i as u32, - )); - } - let text_layout_info = text_layout_info.into_inner(); - shape_text_from_sections( + shape_text_from_reader( + entity, + &mut text_reader, &mut layout.0, &mut font_cx.0, &mut layout_cx.0, - text_sections.iter().copied(), - text_section_styles.iter(), scale_factor, block.linebreak, + &fonts, + &mut computed.entities, ); *text_layout_info = update_text_layout_info( diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index bd32e67ca0573..f45869e65a945 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,12 +1,17 @@ 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::TextHead; use crate::TextLayoutInfo; +use crate::TextReader; use bevy_asset::Assets; +use bevy_ecs::entity::Entity; use bevy_image::Image; use bevy_image::TextureAtlasLayout; use bevy_math::Rect; @@ -25,6 +30,7 @@ use parley::LayoutContext; use parley::PositionedLayoutItem; use parley::StyleProperty; use parley::WordBreakStrength; +use smallvec::SmallVec; use std::ops::Range; use std::usize; use swash::scale::ScaleContext; @@ -110,6 +116,59 @@ pub fn shape_text_from_sections<'a, B: Brush>( builder.build_into(layout, &text); } +/// Create layout given text sections and styles +pub fn shape_text_from_reader<'a, T: TextHead>( + text_root_entity: Entity, + reader: &mut TextReader, + layout: &mut Layout, + font_cx: &'a mut FontContext, + layout_cx: &'a mut LayoutContext, + scale_factor: f32, + line_break: crate::text::LineBreak, + fonts: &Assets, + entities: &mut SmallVec<[TextEntity; 1]>, +) { + let mut text = String::new(); + let mut section_ranges = Vec::new(); + + for (entity, depth, text_section, ..) in reader.iter(text_root_entity) { + entities.push(TextEntity { entity, depth }); + let start = text.len(); + text.push_str(text_section); + let end = text.len(); + section_ranges.push(start..end); + } + + 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)); + }; + for (index, (_, _, _, text_font, _, line_height)) in reader.iter(text_root_entity).enumerate() { + let range = section_ranges[index].clone(); + 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), range.clone()); + builder.push(StyleProperty::FontSize(text_font.font_size), range.clone()); + builder.push(line_height.eval(), range.clone()); + let ffv: Vec<_> = (&text_font.font_features).into(); + let font_features: &[FontFeature] = &ffv; + builder.push( + StyleProperty::FontFeatures(FontSettings::from(font_features)), + range, + ); + } + builder.build_into(layout, &text); +} + /// create a TextLayoutInfo pub fn update_text_layout_info( layout: &mut Layout, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c82debd5f4a0e..78e9115ae3316 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -17,7 +17,7 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - shape_text_from_sections, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, + shape_text_from_reader, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, LayoutCx, LineBreak, LineHeight, ScaleCx, TextBounds, TextColor, TextEntity, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, TextSpanAccess, TextWriter, @@ -288,36 +288,17 @@ pub fn shape_text_system( } computed_block.needs_rerender = false; computed_block.entities.clear(); - let mut text_sections: Vec<&str> = Vec::new(); - let mut text_section_styles: Vec> = Vec::new(); - for (i, (section_entity, depth, text, text_font, _, line_height)) in - text_reader.iter(entity).enumerate() - { - computed_block.entities.push(TextEntity { - entity: section_entity, - depth, - }); - let font_features: Vec<_> = (&text_font.font_features).into(); - text_sections.push(text); - text_section_styles.push(TextSectionStyle::new( - fonts - .get(text_font.font.id()) - .map(|font| font.family_name.as_str()), - text_font.font_size, - line_height, - font_features, - i as u32, - )); - } - shape_text_from_sections( + shape_text_from_reader( + entity, + &mut text_reader, &mut computed_layout.0, &mut font_cx.0, &mut layout_cx.0, - text_sections.iter().copied(), - text_section_styles.iter(), computed_target.scale_factor, block.linebreak, + &fonts, + &mut computed_block.entities, ); computed_layout.break_all_lines(None); From beaaa601a2699ded0037192fecb6a33726b6e171 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 19:41:57 +0000 Subject: [PATCH 71/84] Removed unused --- crates/bevy_ui/src/widget/text.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 78e9115ae3316..00bae3a2d95ba 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -19,8 +19,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ shape_text_from_reader, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, LayoutCx, LineBreak, LineHeight, ScaleCx, TextBounds, TextColor, - TextEntity, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSectionStyle, - TextSpanAccess, TextWriter, + TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; From d136c7647b3d8aba6d5b90297bd8c8fb04e7f178 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 20:19:48 +0000 Subject: [PATCH 72/84] Renamed `shape_text_from_reader` to `shape_text`. Added range to `TextEntity` --- crates/bevy_sprite/src/text2d.rs | 8 +-- crates/bevy_text/src/pipeline.rs | 107 +++++------------------------- crates/bevy_text/src/text.rs | 5 +- crates/bevy_ui/src/widget/text.rs | 4 +- 4 files changed, 25 insertions(+), 99 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index afc5143a095d2..f368fd5e838b9 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,9 +22,9 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - shape_text_from_reader, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, - FontAtlasSet, FontCx, LayoutCx, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, - TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter, + shape_text, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, + FontCx, LayoutCx, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, + TextLayoutInfo, TextReader, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -236,7 +236,7 @@ pub fn update_text2d_layout( let text_layout_info = text_layout_info.into_inner(); - shape_text_from_reader( + shape_text( entity, &mut text_reader, &mut layout.0, diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f45869e65a945..5c91b11015b96 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -20,7 +20,6 @@ use bevy_math::Vec2; use parley::swash::FontRef; use parley::Alignment; use parley::AlignmentOptions; -use parley::Brush; use parley::FontContext; use parley::FontFeature; use parley::FontSettings; @@ -31,93 +30,11 @@ use parley::PositionedLayoutItem; use parley::StyleProperty; use parley::WordBreakStrength; use smallvec::SmallVec; -use std::ops::Range; use std::usize; use swash::scale::ScaleContext; -fn concat_text_for_layout<'a>( - text_sections: impl Iterator, -) -> (String, Vec>) { - let mut out = String::new(); - let mut ranges = Vec::new(); - - for text_section in text_sections { - let start = out.len(); - out.push_str(text_section); - let end = out.len(); - ranges.push(start..end); - } - - (out, ranges) -} - -/// Resolved text style -#[derive(Clone, Debug)] -pub struct TextSectionStyle<'a, B> { - family_name: Option<&'a str>, - font_size: f32, - line_height: crate::text::LineHeight, - font_features: Vec, - brush: B, -} - -impl<'a, B: Brush> TextSectionStyle<'a, B> { - /// new text section style - pub fn new( - family_id: Option<&'a str>, - font_size: f32, - line_height: crate::LineHeight, - font_features: Vec, - brush: B, - ) -> Self { - Self { - family_name: family_id, - font_size, - line_height, - brush, - font_features, - } - } -} - /// Create layout given text sections and styles -pub fn shape_text_from_sections<'a, B: Brush>( - layout: &mut Layout, - font_cx: &'a mut FontContext, - layout_cx: &'a mut LayoutContext, - text_sections: impl Iterator, - text_section_styles: impl Iterator>, - scale_factor: f32, - line_break: crate::text::LineBreak, -) { - let (text, section_ranges) = concat_text_for_layout(text_sections); - 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)); - }; - for (style, range) in text_section_styles.zip(section_ranges) { - if let Some(family) = style.family_name { - builder.push(FontStack::from(family), range.clone()); - }; - builder.push(StyleProperty::Brush(style.brush.clone()), range.clone()); - builder.push(StyleProperty::FontSize(style.font_size), range.clone()); - builder.push(style.line_height.eval(), range.clone()); - let font_features: &[FontFeature] = &style.font_features; - builder.push( - StyleProperty::FontFeatures(FontSettings::from(font_features)), - range, - ); - } - builder.build_into(layout, &text); -} - -/// Create layout given text sections and styles -pub fn shape_text_from_reader<'a, T: TextHead>( +pub fn shape_text<'a, T: TextHead>( text_root_entity: Entity, reader: &mut TextReader, layout: &mut Layout, @@ -128,15 +45,21 @@ pub fn shape_text_from_reader<'a, T: TextHead>( fonts: &Assets, entities: &mut SmallVec<[TextEntity; 1]>, ) { - let mut text = String::new(); - let mut section_ranges = Vec::new(); - + let mut text_len = 0; for (entity, depth, text_section, ..) in reader.iter(text_root_entity) { - entities.push(TextEntity { entity, depth }); - let start = text.len(); + let end = text_len + text_section.len(); + let range = text_len..end; + text_len = end; + entities.push(TextEntity { + entity, + depth, + range, + }); + } + + let mut text = String::with_capacity(text_len); + for (_, _, text_section, ..) in reader.iter(text_root_entity) { text.push_str(text_section); - let end = text.len(); - section_ranges.push(start..end); } let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); @@ -149,7 +72,7 @@ pub fn shape_text_from_reader<'a, T: TextHead>( builder.push_default(StyleProperty::WordBreak(word_break_strength)); }; for (index, (_, _, _, text_font, _, line_height)) in reader.iter(text_root_entity).enumerate() { - let range = section_ranges[index].clone(); + let range = entities[index].range.clone(); if let Some(family) = fonts .get(text_font.font.id()) .map(|font| font.family_name.as_str()) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index b2a20f65c0fe4..d030ed1e1d2ef 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -14,17 +14,20 @@ use core::str::from_utf8; use parley::{FontFeature, Layout}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; +use std::ops::Range; /// 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. pub entity: Entity, /// Records the hierarchy depth of the entity within a `TextLayout`. pub depth: usize, + /// Range in the [`Layout`](`parley::Layout`) + pub range: Range, } /// Computed information for a text block. diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 00bae3a2d95ba..fc555e334e66c 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -17,7 +17,7 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - shape_text_from_reader, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, + shape_text, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx, LayoutCx, LineBreak, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, TextSpanAccess, TextWriter, }; @@ -288,7 +288,7 @@ pub fn shape_text_system( computed_block.needs_rerender = false; computed_block.entities.clear(); - shape_text_from_reader( + shape_text( entity, &mut text_reader, &mut computed_layout.0, From 05b32e590ae2d53e15cff2a7b3057d75169849a2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 21:09:29 +0000 Subject: [PATCH 73/84] Added back `TextPipeline` with the resuable spans buffers. --- crates/bevy_sprite/src/text2d.rs | 9 +- crates/bevy_text/src/lib.rs | 1 + crates/bevy_text/src/pipeline.rs | 136 ++++++++++++++++++------------ crates/bevy_text/src/text.rs | 3 - crates/bevy_ui/src/widget/text.rs | 9 +- 5 files changed, 95 insertions(+), 63 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index f368fd5e838b9..6e56ab96dd851 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,9 +22,9 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - shape_text, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, - FontCx, LayoutCx, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout, - TextLayoutInfo, TextReader, 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; @@ -165,6 +165,7 @@ impl Default for Text2dShadow { /// It does not modify or observe existing ones. pub fn update_text2d_layout( mut target_scale_factors: Local>, + mut text_pipeline: ResMut, mut textures: ResMut>, fonts: Res>, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, @@ -236,7 +237,7 @@ pub fn update_text2d_layout( let text_layout_info = text_layout_info.into_inner(); - shape_text( + text_pipeline.shape_text( entity, &mut text_reader, &mut layout.0, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index e2a0fb2a046e6..f70c080dba04b 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -102,6 +102,7 @@ impl Plugin for TextPlugin { .init_asset_loader::() .init_resource::() .init_resource::() + .init_resource::() .init_resource::() .init_resource::() .init_resource::() diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 5c91b11015b96..f8d71beb43506 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -5,13 +5,16 @@ 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; @@ -26,6 +29,7 @@ use parley::FontSettings; use parley::FontStack; use parley::Layout; use parley::LayoutContext; +use parley::LineHeight; use parley::PositionedLayoutItem; use parley::StyleProperty; use parley::WordBreakStrength; @@ -33,63 +37,91 @@ use smallvec::SmallVec; use std::usize; use swash::scale::ScaleContext; -/// Create layout given text sections and styles -pub fn shape_text<'a, T: TextHead>( - text_root_entity: Entity, - reader: &mut TextReader, - layout: &mut Layout, - font_cx: &'a mut FontContext, - layout_cx: &'a mut LayoutContext, - scale_factor: f32, - line_break: crate::text::LineBreak, - fonts: &Assets, - entities: &mut SmallVec<[TextEntity; 1]>, -) { - let mut text_len = 0; - for (entity, depth, text_section, ..) in reader.iter(text_root_entity) { - let end = text_len + text_section.len(); - let range = text_len..end; - text_len = end; - entities.push(TextEntity { - entity, - depth, - range, - }); - } +/// 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 { + /// 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<(&'static str, &'static TextFont, LineHeight)>, +} - let mut text = String::with_capacity(text_len); - for (_, _, text_section, ..) in reader.iter(text_root_entity) { - text.push_str(text_section); - } +impl TextPipeline { + /// 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, + font_cx: &'a mut FontContext, + layout_cx: &'a mut LayoutContext, + scale_factor: f32, + line_break: crate::text::LineBreak, + fonts: &Assets, + entities: &mut SmallVec<[TextEntity; 1]>, + ) { + entities.clear(); + + let mut spans: Vec<(&str, &TextFont, LineHeight)> = core::mem::take(&mut self.spans_buffer) + .into_iter() + .map(|_| -> (&str, &TextFont, LineHeight) { unreachable!() }) + .collect(); - 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)); - }; - for (index, (_, _, _, text_font, _, line_height)) in reader.iter(text_root_entity).enumerate() { - let range = entities[index].range.clone(); - if let Some(family) = fonts - .get(text_font.font.id()) - .map(|font| font.family_name.as_str()) + let mut text_len = 0; + for (entity, depth, text_section, text_font, _, line_height) in + reader.iter(text_root_entity) { - builder.push(FontStack::from(family), range.clone()); + entities.push(TextEntity { entity, depth }); + text_len += text_section.len(); + spans.push((text_section, text_font, line_height.eval())); + } + + let mut text = String::with_capacity(text_len); + for (text_section, ..) in &spans { + text.push_str(*text_section); + } + + 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)); }; - builder.push(StyleProperty::Brush(index as u32), range.clone()); - builder.push(StyleProperty::FontSize(text_font.font_size), range.clone()); - builder.push(line_height.eval(), range.clone()); - let ffv: Vec<_> = (&text_font.font_features).into(); - let font_features: &[FontFeature] = &ffv; - builder.push( - StyleProperty::FontFeatures(FontSettings::from(font_features)), - range, - ); + + 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), range.clone()); + builder.push(StyleProperty::FontSize(text_font.font_size), range.clone()); + builder.push(line_height, range.clone()); + let ffv: Vec<_> = (&text_font.font_features).into(); + let font_features: &[FontFeature] = &ffv; + builder.push( + StyleProperty::FontFeatures(FontSettings::from(font_features)), + range, + ); + } + builder.build_into(layout, &text); + + // Recover the spans buffer. + self.spans_buffer = spans + .into_iter() + .map(|_| -> (&'static str, &'static TextFont, LineHeight) { unreachable!() }) + .collect(); } - builder.build_into(layout, &text); } /// create a TextLayoutInfo diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index d030ed1e1d2ef..308fb668e78fc 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -14,7 +14,6 @@ use core::str::from_utf8; use parley::{FontFeature, Layout}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; -use std::ops::Range; /// A sub-entity of a [`ComputedTextBlock`]. /// @@ -26,8 +25,6 @@ pub struct TextEntity { pub entity: Entity, /// Records the hierarchy depth of the entity within a `TextLayout`. pub depth: usize, - /// Range in the [`Layout`](`parley::Layout`) - pub range: Range, } /// Computed information for a text block. diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index fc555e334e66c..e3cacfa7bca5f 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -17,9 +17,9 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - shape_text, update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, - FontAtlasSet, FontCx, LayoutCx, LineBreak, LineHeight, ScaleCx, TextBounds, TextColor, - TextFont, TextHead, TextLayout, TextLayoutInfo, TextReader, 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; @@ -248,6 +248,7 @@ impl Measure for TextMeasure { /// 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 shape_text_system( + mut text_pipeline: ResMut, mut font_cx: ResMut, mut layout_cx: ResMut, fonts: Res>, @@ -288,7 +289,7 @@ pub fn shape_text_system( computed_block.needs_rerender = false; computed_block.entities.clear(); - shape_text( + text_pipeline.shape_text( entity, &mut text_reader, &mut computed_layout.0, From bdbc5076f5967efead06803a62e8578ce3f804ee Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 21:22:57 +0000 Subject: [PATCH 74/84] silence unused warning in `many_text2d` --- examples/stress_tests/many_text2d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/stress_tests/many_text2d.rs b/examples/stress_tests/many_text2d.rs index e444a69456034..f4768b96d211e 100644 --- a/examples/stress_tests/many_text2d.rs +++ b/examples/stress_tests/many_text2d.rs @@ -171,7 +171,7 @@ 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() { From a968c1b943f1825ee3561f94b1a80d0399e32d80 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 21:35:27 +0000 Subject: [PATCH 75/84] `FontFeatures` refactor. Internally `FontFeatures` now wraps a parley `FontFeature` list. The `From<&FontFeatures> for Vec` impl has been removed, instead use the `FontFeatures::as_slice` method to access the list to pass to parley. --- crates/bevy_text/src/pipeline.rs | 4 +--- crates/bevy_text/src/text.rs | 38 +++++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f8d71beb43506..7be88a62f4858 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -107,10 +107,8 @@ impl TextPipeline { builder.push(StyleProperty::Brush(index as u32), range.clone()); builder.push(StyleProperty::FontSize(text_font.font_size), range.clone()); builder.push(line_height, range.clone()); - let ffv: Vec<_> = (&text_font.font_features).into(); - let font_features: &[FontFeature] = &ffv; builder.push( - StyleProperty::FontFeatures(FontSettings::from(font_features)), + StyleProperty::FontFeatures(FontSettings::from(text_font.font_features.as_slice())), range, ); } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 308fb668e78fc..d34522e667ee1 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -249,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, } @@ -414,9 +415,9 @@ impl From for u32 { /// FontFeatureTag::TABULAR_FIGURES /// ].into(); /// ``` -#[derive(Clone, Debug, Default, Reflect, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct FontFeatures { - features: Vec<(FontFeatureTag, u16)>, + features: Vec, } impl FontFeatures { @@ -424,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, u16)>, + features: Vec, } impl FontFeaturesBuilder { @@ -446,7 +452,10 @@ 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: u16) -> Self { - self.features.push((feature_tag, value)); + self.features.push(FontFeature { + tag: feature_tag.into(), + value, + }); self } @@ -467,24 +476,17 @@ where { fn from(value: T) -> Self { FontFeatures { - features: value.into_iter().map(|x| (x, 1)).collect(), + features: value + .into_iter() + .map(|x| FontFeature { + tag: x.into(), + value: 1, + }) + .collect(), } } } -impl From<&FontFeatures> for Vec { - fn from(font_features: &FontFeatures) -> Self { - font_features - .features - .iter() - .map(|(tag, value)| FontFeature { - tag: (*tag).into(), - value: *value, - }) - .collect() - } -} - /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size From 75312980d6397139de800b39c8859338fdfaf570 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 21:35:59 +0000 Subject: [PATCH 76/84] Removed unused import. --- crates/bevy_text/src/pipeline.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 7be88a62f4858..a84fd60c2f6b4 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -24,7 +24,6 @@ use parley::swash::FontRef; use parley::Alignment; use parley::AlignmentOptions; use parley::FontContext; -use parley::FontFeature; use parley::FontSettings; use parley::FontStack; use parley::Layout; From 6da9ccee6da7a3b665ce66ad9b6c9393727f2ba5 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 21:38:32 +0000 Subject: [PATCH 77/84] Added `clear` method to `TextLayoutInfo`. --- crates/bevy_text/src/text.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index d34522e667ee1..591f2b4a60c1b 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -631,6 +631,14 @@ pub struct TextLayoutInfo { 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. From 2c39d5740fdd825c9b08c01c45b5d98047243d66 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 21:42:32 +0000 Subject: [PATCH 78/84] Changed `update_text_layout_info` to take a `TextLayoutInfo` mutable ref, instead of returning a new instance. --- crates/bevy_sprite/src/text2d.rs | 16 +++++++++++----- crates/bevy_text/src/pipeline.rs | 9 ++++----- crates/bevy_ui/src/widget/text.rs | 3 ++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 6e56ab96dd851..7f159feab7af8 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -202,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, mut layout) 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(); @@ -235,8 +242,6 @@ pub fn update_text2d_layout( computed.needs_rerender = false; computed.entities.clear(); - let text_layout_info = text_layout_info.into_inner(); - text_pipeline.shape_text( entity, &mut text_reader, @@ -249,7 +254,8 @@ pub fn update_text2d_layout( &mut computed.entities, ); - *text_layout_info = update_text_layout_info( + update_text_layout_info( + &mut text_layout_info, &mut layout.0, bounds.width.map(|w| w * scale_factor), block.justify.into(), diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index a84fd60c2f6b4..1628b77ad8bc7 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -123,6 +123,7 @@ impl TextPipeline { /// create a TextLayoutInfo pub fn update_text_layout_info( + info: &mut TextLayoutInfo, layout: &mut Layout, max_advance: Option, alignment: Alignment, @@ -131,12 +132,12 @@ pub fn update_text_layout_info( texture_atlases: &mut Assets, textures: &mut Assets, font_smoothing: FontSmoothing, -) -> TextLayoutInfo { +) { + info.clear(); + layout.break_all_lines(max_advance); layout.align(None, alignment, AlignmentOptions::default()); - let mut info = TextLayoutInfo::default(); - info.scale_factor = layout.scale(); info.size = ( layout.width() / layout.scale(), @@ -226,6 +227,4 @@ pub fn update_text_layout_info( } } } - - info } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index e3cacfa7bca5f..05130d9579c71 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -342,7 +342,8 @@ pub fn layout_text_system( ) { 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 { - *text_layout_info = update_text_layout_info( + update_text_layout_info( + &mut text_layout_info, &mut layout.0, Some(node.size.x).filter(|_| block.linebreak != LineBreak::NoWrap), block.justify.into(), From bfe06e162b2b2d28ececf8b94490940b3dd2e9c6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 21:45:00 +0000 Subject: [PATCH 79/84] removed unneeded deref --- crates/bevy_text/src/pipeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 1628b77ad8bc7..3e2aae1099d82 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -79,7 +79,7 @@ impl TextPipeline { let mut text = String::with_capacity(text_len); for (text_section, ..) in &spans { - text.push_str(*text_section); + text.push_str(text_section); } let mut builder = layout_cx.ranged_builder(font_cx, &text, scale_factor, true); From f9e73f10974ff7fe1b2e3d67cfc4062cacc50b64 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 22:21:40 +0000 Subject: [PATCH 80/84] Add draft release content --- release-content/migration-guides/text_node_flags.md | 8 ++++++++ release-content/release-notes/parley_migration.md | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 release-content/migration-guides/text_node_flags.md create mode 100644 release-content/release-notes/parley_migration.md 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..ebea378233af7 --- /dev/null +++ b/release-content/migration-guides/text_node_flags.md @@ -0,0 +1,8 @@ +--- +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 From bf83a3ddf7a7d409cf1524ab6e7d8997befe3b24 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 26 Nov 2025 11:36:50 +0000 Subject: [PATCH 81/84] Fixed typo --- crates/bevy_text/src/font_atlas.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index a51e5dcc7dda7..c63f48eb2d6ee 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -10,7 +10,7 @@ use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError}; /// Key identifying a glyph #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct GlyphCacheKey { - /// glyh id + /// glyph id pub glyph_id: u16, } From 7982fdbf5549fb43cf5f21f094499670c918702d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 26 Nov 2025 11:48:23 +0000 Subject: [PATCH 82/84] fixed migration note formatting --- release-content/migration-guides/text_node_flags.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-content/migration-guides/text_node_flags.md b/release-content/migration-guides/text_node_flags.md index ebea378233af7..26a099762caac 100644 --- a/release-content/migration-guides/text_node_flags.md +++ b/release-content/migration-guides/text_node_flags.md @@ -4,5 +4,6 @@ pull_requests: [21940] --- The fields of `TextNodeFlags` have been renamed: + * `needs_measure_fn` -> `needs_shaping` * `needs_recompute` -> `needs_relayout` From 4be8f9ccf04648201c07e7b68d98328472aa631a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 26 Nov 2025 15:15:17 +0000 Subject: [PATCH 83/84] Added `font_smoothing: FontSmoothing` parameter to `get_outlined_glyph_texture`, and don't do antialiasing if None. --- crates/bevy_text/src/font_atlas.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index c63f48eb2d6ee..b07689cf9ffcf 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -140,7 +140,7 @@ pub fn add_glyph_to_atlas( font_smoothing: FontSmoothing, glyph_id: u16, ) -> Result { - let (glyph_texture, offset) = get_outlined_glyph_texture(scaler, glyph_id)?; + 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, @@ -189,6 +189,7 @@ pub fn add_glyph_to_atlas( pub fn get_outlined_glyph_texture( scaler: &mut Scaler, glyph_id: u16, + font_smoothing: FontSmoothing, ) -> Result<(Image, IVec2), TextError> { let image = swash::scale::Render::new(&[ swash::scale::Source::ColorOutline(0), @@ -206,12 +207,25 @@ pub fn get_outlined_glyph_texture( let px = (width * height) as usize; let mut rgba = vec![0u8; px * 4]; - 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 + 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 + } + } + 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(( From 936ce74ff7f6e4329179e697f5cb0d0aa467ea47 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 26 Nov 2025 15:26:16 +0000 Subject: [PATCH 84/84] Add the `FontSmoothing` setting to layout brush --- crates/bevy_sprite/src/text2d.rs | 1 - crates/bevy_text/src/context.rs | 4 +++- crates/bevy_text/src/pipeline.rs | 19 +++++++++++-------- crates/bevy_text/src/text.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 1 - 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 7f159feab7af8..f77d981822441 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -263,7 +263,6 @@ pub fn update_text2d_layout( &mut font_atlas_set, &mut texture_atlases, &mut textures, - bevy_text::FontSmoothing::AntiAliased, ); } } diff --git a/crates/bevy_text/src/context.rs b/crates/bevy_text/src/context.rs index eb5e0211d1cb3..3016bee4dc759 100644 --- a/crates/bevy_text/src/context.rs +++ b/crates/bevy_text/src/context.rs @@ -5,13 +5,15 @@ 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); +pub struct LayoutCx(pub LayoutContext<(u32, FontSmoothing)>); /// Text scaler context #[derive(Resource, Default, Deref, DerefMut)] diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 3e2aae1099d82..0aa96ee510bd4 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -53,9 +53,9 @@ impl TextPipeline { &mut self, text_root_entity: Entity, reader: &mut TextReader, - layout: &mut Layout, + layout: &mut Layout<(u32, FontSmoothing)>, font_cx: &'a mut FontContext, - layout_cx: &'a mut LayoutContext, + layout_cx: &'a mut LayoutContext<(u32, FontSmoothing)>, scale_factor: f32, line_break: crate::text::LineBreak, fonts: &Assets, @@ -103,7 +103,10 @@ impl TextPipeline { { builder.push(FontStack::from(family), range.clone()); }; - builder.push(StyleProperty::Brush(index as u32), 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( @@ -124,14 +127,13 @@ impl TextPipeline { /// create a TextLayoutInfo pub fn update_text_layout_info( info: &mut TextLayoutInfo, - layout: &mut Layout, + 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, - font_smoothing: FontSmoothing, ) { info.clear(); @@ -149,7 +151,7 @@ pub fn update_text_layout_info( for (line_index, item) in line.items().enumerate() { match item { PositionedLayoutItem::GlyphRun(glyph_run) => { - let span_index = glyph_run.style().brush; + let span_index = glyph_run.style().brush.0; let run = glyph_run.run(); @@ -157,7 +159,8 @@ pub fn update_text_layout_info( let font_size = run.font_size(); let coords = run.normalized_coords(); - let font_atlas_key = FontAtlasKey::new(&font, font_size, font_smoothing); + 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(); @@ -183,7 +186,7 @@ pub fn update_text_layout_info( texture_atlases, textures, &mut scaler, - font_smoothing, + glyph_run.style().brush.1, glyph.id as u16, ) }) else { diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 591f2b4a60c1b..b8ae0049f5494 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -729,7 +729,7 @@ pub enum FontSmoothing { /// Computed text layout #[derive(Component, Default, Deref, DerefMut)] -pub struct ComputedTextLayout(pub Layout); +pub struct ComputedTextLayout(pub Layout<(u32, FontSmoothing)>); // System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`. /// diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 05130d9579c71..f4df933384992 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -351,7 +351,6 @@ pub fn layout_text_system( &mut font_atlas_set, &mut texture_atlases, &mut textures, - bevy_text::FontSmoothing::AntiAliased, ); text_flags.needs_relayout = false;