From be8e768a0dfaed7d0778bbab8994ed02fd7a25e9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 10:49:47 +0100 Subject: [PATCH 01/16] Added style module --- crates/bevy_text/src/lib.rs | 1 + crates/bevy_text/src/style.rs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 crates/bevy_text/src/style.rs diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 1e341880e5336..9abdba112faf2 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -39,6 +39,7 @@ mod font_atlas_set; mod font_loader; mod glyph; mod pipeline; +pub mod style; mod text; mod text_access; diff --git a/crates/bevy_text/src/style.rs b/crates/bevy_text/src/style.rs new file mode 100644 index 0000000000000..ecf9fad9cc8ed --- /dev/null +++ b/crates/bevy_text/src/style.rs @@ -0,0 +1,18 @@ +use crate::*; +use bevy_app::Propagate; +use bevy_color::Color; +use bevy_ecs::component::Component; +use bevy_ecs::prelude::*; +use bevy_ecs::query::AnyOf; + +#[derive(Resource)] +pub struct DefaultTextStyle { + font: TextFont, + color: Color, +} + +#[derive(Component)] +pub struct ComputedTextStyle { + font: TextFont, + color: Color, +} From 41ce2f1af080fb406d21e3499770f89c7af3da87 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 14:50:18 +0100 Subject: [PATCH 02/16] * New component `ComputedTextStyle`. * Removed `TextFont` and `TextColor` from the `Text`, `Text2d`, and `TextSpan` requires, replaced with `ComputedTextStyle`. * `update_text_styles` updates the `ComputedTextStyle`s each frame from the text entities nearest ancestors with `TextFont` or `TextColor` components. --- crates/bevy_feathers/src/font_styles.rs | 7 +- crates/bevy_feathers/src/lib.rs | 18 +----- crates/bevy_feathers/src/theme.rs | 7 +- crates/bevy_sprite/src/lib.rs | 3 + crates/bevy_sprite/src/text2d.rs | 8 +-- crates/bevy_sprite_render/src/text2d/mod.rs | 7 +- crates/bevy_text/src/lib.rs | 12 +++- crates/bevy_text/src/pipeline.rs | 58 ++++++++--------- crates/bevy_text/src/style.rs | 71 +++++++++++++++++++-- crates/bevy_text/src/text.rs | 4 +- crates/bevy_text/src/text_access.rs | 33 +++++----- crates/bevy_ui/src/accessibility.rs | 4 +- crates/bevy_ui/src/lib.rs | 3 + crates/bevy_ui/src/widget/text.rs | 8 +-- crates/bevy_ui_render/src/lib.rs | 15 ++--- examples/ui/text.rs | 1 + 16 files changed, 153 insertions(+), 106 deletions(-) diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs index 9ea7783db5752..2813d96f8b4e2 100644 --- a/crates/bevy_feathers/src/font_styles.rs +++ b/crates/bevy_feathers/src/font_styles.rs @@ -1,5 +1,4 @@ //! A framework for inheritable font styles. -use bevy_app::{Propagate, PropagateOver}; use bevy_asset::{AssetServer, Handle}; use bevy_ecs::{ component::Component, @@ -17,7 +16,7 @@ use crate::{handle_or_path::HandleOrPath, theme::ThemedText}; /// downward to any child text entity that has the [`ThemedText`] marker. #[derive(Component, Default, Clone, Debug, Reflect)] #[reflect(Component, Default)] -#[require(ThemedText, PropagateOver::::default())] +#[require(ThemedText)] pub struct InheritableFont { /// The font handle or path. pub font: HandleOrPath, @@ -57,10 +56,10 @@ pub(crate) fn on_changed_font( HandleOrPath::Path(ref p) => Some(assets.load::(p)), } { - commands.entity(insert.entity).insert(Propagate(TextFont { + commands.entity(insert.entity).insert(TextFont { font, font_size: style.font_size, ..Default::default() - })); + }); } } diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index 348b677f98e53..dd7cce9123fb1 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -18,14 +18,9 @@ //! Please report issues, submit fixes and propose changes. //! Thanks for stress-testing; let's build something better together. -use bevy_app::{ - HierarchyPropagatePlugin, Plugin, PluginGroup, PluginGroupBuilder, PostUpdate, PropagateSet, -}; +use bevy_app::{Plugin, PluginGroup, PluginGroupBuilder, PostUpdate}; use bevy_asset::embedded_asset; -use bevy_ecs::{query::With, schedule::IntoScheduleConfigs}; use bevy_input_focus::{tab_navigation::TabNavigationPlugin, InputDispatchPlugin}; -use bevy_text::{TextColor, TextFont}; -use bevy_ui::UiSystems; use bevy_ui_render::UiMaterialPlugin; use bevy_ui_widgets::UiWidgetsPlugins; @@ -33,7 +28,7 @@ use crate::{ alpha_pattern::{AlphaPatternMaterial, AlphaPatternResource}, controls::ControlsPlugin, cursor::{CursorIconPlugin, DefaultCursor, EntityCursor}, - theme::{ThemedText, UiTheme}, + theme::UiTheme, }; mod alpha_pattern; @@ -68,18 +63,9 @@ impl Plugin for FeathersPlugin { app.add_plugins(( ControlsPlugin, CursorIconPlugin, - HierarchyPropagatePlugin::>::new(PostUpdate), - HierarchyPropagatePlugin::>::new(PostUpdate), UiMaterialPlugin::::default(), )); - // This needs to run in UiSystems::Propagate so the fonts are up-to-date for `measure_text_system` - // and `detect_text_needs_rerender` in UiSystems::Content - app.configure_sets( - PostUpdate, - PropagateSet::::default().in_set(UiSystems::Propagate), - ); - app.insert_resource(DefaultCursor(EntityCursor::System( bevy_window::SystemCursorIcon::Default, ))); diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs index 07c7cb1cbf48c..b2377ff0dd641 100644 --- a/crates/bevy_feathers/src/theme.rs +++ b/crates/bevy_feathers/src/theme.rs @@ -1,5 +1,4 @@ //! A framework for theming. -use bevy_app::{Propagate, PropagateOver}; use bevy_color::{palettes, Color}; use bevy_ecs::{ change_detection::DetectChanges, @@ -105,7 +104,7 @@ pub struct ThemeBorderColor(pub ThemeToken); #[component(immutable)] #[derive(Reflect)] #[reflect(Component, Clone)] -#[require(ThemedText, PropagateOver::::default())] +#[require(ThemedText)] pub struct ThemeFontColor(pub ThemeToken); /// A marker component that is used to indicate that the text entity wants to opt-in to using @@ -167,8 +166,6 @@ pub(crate) fn on_changed_font_color( ) { if let Ok(token) = font_color.get(insert.entity) { let color = theme.color(&token.0); - commands - .entity(insert.entity) - .insert(Propagate(TextColor(color))); + commands.entity(insert.entity).insert(TextColor(color)); } } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index d1ebee1935101..61dd2e2b702bd 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::update_text_styles; #[cfg(feature = "bevy_sprite_picking_backend")] pub use picking_backend::*; pub use sprite::*; @@ -87,6 +89,7 @@ impl Plugin for SpritePlugin { bevy_text::detect_text_needs_rerender::, update_text2d_layout .after(bevy_camera::CameraUpdateSystems) + .after(update_text_styles) .after(bevy_text::remove_dropped_font_atlas_sets), calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 3c0e5fa56564f..268f69c805780 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -19,10 +19,11 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_text::ComputedTextStyle; use bevy_text::{ ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds, - TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, - TextSpanAccess, TextWriter, + TextError, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, + TextWriter, }; use bevy_transform::components::Transform; use core::any::TypeId; @@ -81,12 +82,11 @@ use core::any::TypeId; #[reflect(Component, Default, Debug, Clone)] #[require( TextLayout, - TextFont, - TextColor, TextBounds, Anchor, Visibility, VisibilityClass, + ComputedTextStyle, Transform )] #[component(on_add = visibility::add_visibility_class::)] diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 5dbd603ed21df..ee82b09bf9a10 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -14,7 +14,8 @@ use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::Extract; use bevy_sprite::{Anchor, Text2dShadow}; use bevy_text::{ - ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextBounds, TextColor, TextLayoutInfo, + ComputedTextBlock, ComputedTextStyle, PositionedGlyph, TextBackgroundColor, TextBounds, + TextLayoutInfo, }; use bevy_transform::prelude::GlobalTransform; @@ -37,7 +38,7 @@ pub fn extract_text2d_sprite( &GlobalTransform, )>, >, - text_colors: Extract>, + text_colors: Extract>, text_background_colors_query: Extract>, ) { let mut start = extracted_slices.slices.len(); @@ -170,7 +171,7 @@ pub fn extract_text2d_sprite( .map(|t| t.entity) .unwrap_or(Entity::PLACEHOLDER), ) - .map(|text_color| LinearRgba::from(text_color.0)) + .map(|style| LinearRgba::from(style.color())) .unwrap_or_default(); current_span = *span_index; } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 9abdba112faf2..4ac0fa91a7464 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -39,7 +39,7 @@ mod font_atlas_set; mod font_loader; mod glyph; mod pipeline; -pub mod style; +mod style; mod text; mod text_access; @@ -51,6 +51,7 @@ pub use font_atlas_set::*; pub use font_loader::*; pub use glyph::*; pub use pipeline::*; +pub use style::*; pub use text::*; pub use text_access::*; @@ -60,7 +61,8 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, Justify, LineBreak, TextColor, TextError, TextFont, TextLayout, TextSpan, + ComputedTextStyle, Font, Justify, LineBreak, TextColor, TextError, TextFont, TextLayout, + TextSpan, }; } @@ -96,9 +98,13 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( PostUpdate, - remove_dropped_font_atlas_sets.before(AssetEventSystems), + ( + update_text_styles, + remove_dropped_font_atlas_sets.before(AssetEventSystems), + ), ) .add_systems(Last, trim_cosmic_cache); diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 03939d47d16f0..ba49c48fdc094 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,7 +1,6 @@ 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, @@ -16,8 +15,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, - PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, + error::TextError, style::ComputedTextStyle, ComputedTextBlock, Font, FontAtlasSets, + FontSmoothing, Justify, LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, + TextLayout, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -86,7 +86,7 @@ impl TextPipeline { pub fn update_buffer<'a>( &mut self, fonts: &Assets, - text_spans: impl Iterator, + text_spans: impl Iterator, linebreak: LineBreak, justify: Justify, bounds: TextBounds, @@ -100,15 +100,15 @@ impl TextPipeline { // 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)> = + let mut spans: Vec<(usize, &str, &ComputedTextStyle, FontFaceInfo)> = core::mem::take(&mut self.spans_buffer) .into_iter() - .map(|_| -> (usize, &str, &TextFont, FontFaceInfo, Color) { unreachable!() }) + .map(|_| -> (usize, &str, &ComputedTextStyle, FontFaceInfo) { unreachable!() }) .collect(); computed.entities.clear(); - for (span_index, (entity, depth, span, text_font, color)) in text_spans.enumerate() { + for (span_index, (entity, depth, span, style)) in text_spans.enumerate() { // Save this span entity in the computed text block. computed.entities.push(TextEntity { entity, depth }); @@ -116,7 +116,7 @@ impl TextPipeline { continue; } // Return early if a font is not loaded yet. - if !fonts.contains(text_font.font.id()) { + if !fonts.contains(style.font.font.id()) { spans.clear(); self.spans_buffer = spans .into_iter() @@ -131,26 +131,27 @@ impl TextPipeline { } // 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)); + max_font_size = max_font_size.max(style.font.font_size); + max_line_height = + max_line_height.max(style.font.line_height.eval(style.font.font_size)); // Load Bevy fonts into cosmic-text's font system. let face_info = load_font_to_fontdb( - text_font, + &style.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 { + if scale_factor <= 0.0 || style.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)); + spans.push((span_index, span, style, face_info)); } let mut metrics = Metrics::new(max_font_size, max_line_height).scale(scale_factor as f32); @@ -166,14 +167,12 @@ impl TextPipeline { // 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), - ) - }); + let spans_iter = spans.iter().map(|(span_index, span, style, font_info)| { + ( + *span, + get_attrs(*span_index, style, font_info, scale_factor), + ) + }); // Update the buffer. let buffer = &mut computed.buffer; @@ -225,7 +224,7 @@ impl TextPipeline { &mut self, layout_info: &mut TextLayoutInfo, fonts: &Assets, - text_spans: impl Iterator, + text_spans: impl Iterator, scale_factor: f64, layout: &TextLayout, bounds: TextBounds, @@ -246,8 +245,8 @@ impl TextPipeline { // 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 text_spans = text_spans.inspect(|(_, _, _, text_style)| { + glyph_info.push((text_style.font.font.id(), text_style.font.font_smoothing)); }); let update_result = self.update_buffer( @@ -394,7 +393,7 @@ impl TextPipeline { &mut self, entity: Entity, fonts: &Assets, - text_spans: impl Iterator, + text_spans: impl Iterator, scale_factor: f64, layout: &TextLayout, computed: &mut ComputedTextBlock, @@ -529,8 +528,7 @@ pub fn load_font_to_fontdb( /// Translates [`TextFont`] to [`Attrs`]. fn get_attrs<'a>( span_index: usize, - text_font: &TextFont, - color: Color, + style: &'a ComputedTextStyle, face_info: &'a FontFaceInfo, scale_factor: f64, ) -> Attrs<'a> { @@ -542,12 +540,12 @@ fn get_attrs<'a>( .weight(face_info.weight) .metrics( Metrics { - font_size: text_font.font_size, - line_height: text_font.line_height.eval(text_font.font_size), + font_size: style.font.font_size, + line_height: style.font.line_height.eval(style.font.font_size), } .scale(scale_factor as f32), ) - .color(cosmic_text::Color(color.to_linear().as_u32())) + .color(cosmic_text::Color(style.color.to_linear().as_u32())) } /// Calculate the size of the text area for the given buffer. diff --git a/crates/bevy_text/src/style.rs b/crates/bevy_text/src/style.rs index ecf9fad9cc8ed..dc2b6c0fd183e 100644 --- a/crates/bevy_text/src/style.rs +++ b/crates/bevy_text/src/style.rs @@ -1,18 +1,75 @@ use crate::*; -use bevy_app::Propagate; use bevy_color::Color; use bevy_ecs::component::Component; use bevy_ecs::prelude::*; -use bevy_ecs::query::AnyOf; +/// Default text style #[derive(Resource)] pub struct DefaultTextStyle { - font: TextFont, - color: Color, + /// default font + pub font: TextFont, + /// default color + pub color: Color, } -#[derive(Component)] +impl Default for DefaultTextStyle { + fn default() -> Self { + Self { + font: Default::default(), + color: Color::WHITE, + } + } +} + +/// Computed text style +#[derive(Component, PartialEq, Default)] pub struct ComputedTextStyle { - font: TextFont, - color: Color, + /// From nearest ancestor with a `TextFont` + pub(crate) font: TextFont, + /// From nearest ancestor with a `TextColor` + pub(crate) color: Color, +} + +impl ComputedTextStyle { + /// Computed text font + pub const fn font(&self) -> &TextFont { + &self.font + } + + /// Computed text color + pub const fn color(&self) -> Color { + self.color + } +} + +/// update text styles +pub fn update_text_styles( + default_text_style: Res, + mut computed_text_query: Query<(Entity, &mut ComputedTextStyle)>, + parent_query: Query<&ChildOf>, + font_query: Query<(Option<&TextFont>, Option<&TextColor>)>, +) { + for (start, mut style) in computed_text_query.iter_mut() { + let (mut font, mut color) = font_query.get(start).unwrap(); + let mut ancestors = parent_query.iter_ancestors(start); + + while (font.is_none() || color.is_none()) + && let Some(ancestor) = ancestors.next() + { + let (next_font, next_color) = font_query.get(ancestor).unwrap(); + font = font.or(next_font); + color = color.or(next_color); + } + + let new_style = ComputedTextStyle { + font: font.unwrap_or(&default_text_style.font).clone(), + color: color.map(|t| t.0).unwrap_or(default_text_style.color), + }; + + if new_style.font != style.font { + *style = new_style; + } else { + style.color = new_style.color; + } + } } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index e4da3288d43c0..fc37ef8aeccb1 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,4 +1,4 @@ -use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; +use crate::{style::ComputedTextStyle, Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; @@ -172,7 +172,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(ComputedTextStyle)] pub struct TextSpan(pub String); impl TextSpan { diff --git a/crates/bevy_text/src/text_access.rs b/crates/bevy_text/src/text_access.rs index 7de9e8e323b36..ec75efb6d960b 100644 --- a/crates/bevy_text/src/text_access.rs +++ b/crates/bevy_text/src/text_access.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ system::{Query, SystemParam}, }; -use crate::{TextColor, TextFont, TextSpan}; +use crate::{style::ComputedTextStyle, TextColor, TextFont, TextSpan}; /// Helper trait for using the [`TextReader`] and [`TextWriter`] system params. pub trait TextSpanAccess: Component { @@ -56,8 +56,7 @@ pub struct TextReader<'w, 's, R: TextRoot> { 's, ( &'static R, - &'static TextFont, - &'static TextColor, + &'static ComputedTextStyle, Option<&'static Children>, ), >, @@ -66,8 +65,7 @@ pub struct TextReader<'w, 's, R: TextRoot> { 's, ( &'static TextSpan, - &'static TextFont, - &'static TextColor, + &'static ComputedTextStyle, Option<&'static Children>, ), >, @@ -92,24 +90,25 @@ impl<'w, 's, R: TextRoot> TextReader<'w, 's, R> { &mut self, root_entity: Entity, index: usize, - ) -> Option<(Entity, usize, &str, &TextFont, Color)> { + ) -> Option<(Entity, usize, &str, &ComputedTextStyle)> { self.iter(root_entity).nth(index) } /// Gets the text value of a text span within a text block at a specific index in the flattened span list. pub fn get_text(&mut self, root_entity: Entity, index: usize) -> Option<&str> { - self.get(root_entity, index).map(|(_, _, text, _, _)| text) + self.get(root_entity, index).map(|(_, _, text, _)| text) } /// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list. pub fn get_font(&mut self, root_entity: Entity, index: usize) -> Option<&TextFont> { - self.get(root_entity, index).map(|(_, _, _, font, _)| font) + self.get(root_entity, index) + .map(|(_, _, _, style)| &style.font) } /// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list. pub fn get_color(&mut self, root_entity: Entity, index: usize) -> Option { self.get(root_entity, index) - .map(|(_, _, _, _, color)| color) + .map(|(_, _, _, style)| style.color) } /// Gets the text value of a text span within a text block at a specific index in the flattened span list. @@ -149,8 +148,7 @@ pub struct TextSpanIter<'a, R: TextRoot> { 'a, ( &'static R, - &'static TextFont, - &'static TextColor, + &'static ComputedTextStyle, Option<&'static Children>, ), >, @@ -159,8 +157,7 @@ pub struct TextSpanIter<'a, R: TextRoot> { 'a, ( &'static TextSpan, - &'static TextFont, - &'static TextColor, + &'static ComputedTextStyle, Option<&'static Children>, ), >, @@ -168,15 +165,15 @@ pub struct TextSpanIter<'a, R: TextRoot> { impl<'a, R: TextRoot> 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); + type Item = (Entity, usize, &'a str, &'a ComputedTextStyle); fn next(&mut self) -> Option { // Root if let Some(root_entity) = self.root_entity.take() { - if let Ok((text, text_font, color, maybe_children)) = self.roots.get(root_entity) { + if let Ok((text, style, maybe_children)) = self.roots.get(root_entity) { if let Some(children) = maybe_children { self.stack.push((children, 0)); } - return Some((root_entity, 0, text.read_span(), text_font, color.0)); + return Some((root_entity, 0, text.read_span(), style)); } return None; } @@ -194,7 +191,7 @@ impl<'a, R: TextRoot> Iterator for TextSpanIter<'a, R> { *idx += 1; let entity = *child; - let Ok((span, text_font, color, maybe_children)) = self.spans.get(entity) else { + let Ok((span, style, maybe_children)) = self.spans.get(entity) else { continue; }; @@ -202,7 +199,7 @@ impl<'a, R: TextRoot> Iterator for TextSpanIter<'a, R> { if let Some(children) = maybe_children { self.stack.push((children, 0)); } - return Some((entity, depth, span.read_span(), text_font, color.0)); + return Some((entity, depth, span.read_span(), style)); } // All children at this stack entry have been iterated. diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 81c78a50a2da8..a2b6e423bbdfa 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -26,7 +26,7 @@ fn calc_label( for child in children { let values = text_reader .iter(child) - .map(|(_, _, text, _, _)| text.into()) + .map(|(_, _, text, _)| text.into()) .collect::>(); if !values.is_empty() { name = Some(values.join(" ")); @@ -119,7 +119,7 @@ fn label_changed( for (entity, accessible) in &mut query { let values = text_reader .iter(entity) - .map(|(_, _, text, _, _)| text.into()) + .map(|(_, _, text, _)| text.into()) .collect::>(); let label = Some(values.join(" ").into_boxed_str()); if let Some(mut accessible) = accessible { diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index eb62969c6eeb6..f1dcd10ff0088 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::update_text_styles; pub use focus::*; pub use geometry::*; pub use gradients::*; @@ -235,6 +236,7 @@ fn build_text_interop(app: &mut App) { ) .chain() .in_set(UiSystems::Content) + .after(update_text_styles) // Text and Text2d are independent. .ambiguous_with(bevy_text::detect_text_needs_rerender::) // Potential conflict: `Assets` @@ -245,6 +247,7 @@ 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::text_system + .after(update_text_styles) .in_set(UiSystems::PostLayout) .after(bevy_text::remove_dropped_font_atlas_sets) .before(bevy_asset::AssetEventSystems) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 2998041a500ca..eed72911c4eeb 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -18,8 +18,8 @@ use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds, - TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline, + ComputedTextBlock, ComputedTextStyle, CosmicFontSystem, Font, FontAtlasSets, LineBreak, + SwashCache, TextBounds, TextError, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; @@ -95,7 +95,7 @@ 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, ComputedTextStyle, TextNodeFlags, ContentSize)] pub struct Text(pub String); impl Text { @@ -220,7 +220,7 @@ fn create_text_measure<'a>( entity: Entity, fonts: &Assets, scale_factor: f64, - spans: impl Iterator, + spans: impl Iterator, block: Ref, text_pipeline: &mut TextPipeline, mut content_size: Mut, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 2eb662ae42fa1..6c00942fc06e0 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -24,6 +24,7 @@ use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; +use bevy_text::ComputedTextStyle; use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode}; use bevy_ui::{ BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedUiTargetCamera, Display, @@ -59,9 +60,7 @@ pub use debug_overlay::UiDebugOptions; use gradient::GradientPlugin; use bevy_platform::collections::{HashMap, HashSet}; -use bevy_text::{ - ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextColor, TextLayoutInfo, -}; +use bevy_text::{ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; use bytemuck::{Pod, Zeroable}; @@ -909,11 +908,11 @@ pub fn extract_text_sections( Option<&CalculatedClip>, &ComputedUiTargetCamera, &ComputedTextBlock, - &TextColor, + &ComputedTextStyle, &TextLayoutInfo, )>, >, - text_styles: Extract>, + text_styles: Extract>, camera_map: Extract, ) { let mut start = extracted_uinodes.glyphs.len(); @@ -928,7 +927,7 @@ pub fn extract_text_sections( clip, camera, computed_block, - text_color, + text_style, text_layout_info, ) in &uinode_query { @@ -943,7 +942,7 @@ pub fn extract_text_sections( let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); - let mut color = text_color.0.to_linear(); + let mut color = text_style.color().to_linear(); let mut current_span_index = 0; @@ -963,7 +962,7 @@ pub fn extract_text_sections( { color = text_styles .get(span_entity) - .map(|text_color| LinearRgba::from(text_color.0)) + .map(|style| LinearRgba::from(style.color())) .unwrap_or_default(); current_span_index = *span_index; } diff --git a/examples/ui/text.rs b/examples/ui/text.rs index ec0f6185efc04..b1a39734a64f6 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -48,6 +48,7 @@ fn setup(mut commands: Commands, asset_server: Res) { right: px(5), ..default() }, + TextColor::WHITE, AnimatedText, )); From 2b8fbf94782ee48b12add9dc27980c158fb7a458 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 16:03:30 +0100 Subject: [PATCH 03/16] Fix tests --- crates/bevy_sprite/src/text2d.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 268f69c805780..8a9f45fc309ab 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -309,7 +309,7 @@ mod tests { use bevy_camera::{ComputedCameraValues, RenderTargetInfo}; use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_math::UVec2; - use bevy_text::{detect_text_needs_rerender, TextIterScratch}; + use bevy_text::{detect_text_needs_rerender, update_text_styles, TextIterScratch}; use super::*; @@ -329,6 +329,7 @@ mod tests { .add_systems( Update, ( + update_text_styles, detect_text_needs_rerender::, update_text2d_layout, calculate_bounds_text2d, From 35eae7f602571fbecccd5700cfcf1495249e22b0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 16:28:03 +0100 Subject: [PATCH 04/16] Added `DefaultTextStyle` to test app --- crates/bevy_sprite/src/text2d.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 8a9f45fc309ab..4cb9e56623662 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -309,7 +309,9 @@ mod tests { use bevy_camera::{ComputedCameraValues, RenderTargetInfo}; use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_math::UVec2; - use bevy_text::{detect_text_needs_rerender, update_text_styles, TextIterScratch}; + use bevy_text::{ + detect_text_needs_rerender, update_text_styles, DefaultTextStyle, TextIterScratch, + }; use super::*; @@ -326,6 +328,7 @@ mod tests { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( Update, ( From be52501f84a8ab6064a61bfcf41b00f81ef7ae2c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 16:41:21 +0100 Subject: [PATCH 05/16] Remove more schedule ambiguities --- crates/bevy_ui/src/accessibility.rs | 5 ++--- crates/bevy_ui/src/lib.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index a2b6e423bbdfa..265c4b3684613 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::update_text_styles; fn calc_label( text_reader: &mut TextUiReader, @@ -154,9 +155,7 @@ 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).before(update_text_styles), ), ); } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index f1dcd10ff0088..f73eb5301d7fb 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -236,7 +236,6 @@ fn build_text_interop(app: &mut App) { ) .chain() .in_set(UiSystems::Content) - .after(update_text_styles) // Text and Text2d are independent. .ambiguous_with(bevy_text::detect_text_needs_rerender::) // Potential conflict: `Assets` @@ -247,7 +246,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::text_system - .after(update_text_styles) .in_set(UiSystems::PostLayout) .after(bevy_text::remove_dropped_font_atlas_sets) .before(bevy_asset::AssetEventSystems) @@ -255,7 +253,8 @@ fn build_text_interop(app: &mut App) { .ambiguous_with(bevy_text::detect_text_needs_rerender::) .ambiguous_with(bevy_sprite::update_text2d_layout) .ambiguous_with(bevy_sprite::calculate_bounds_text2d), - ), + ) + .after(update_text_styles), ); app.add_plugins(accessibility::AccessibilityPlugin); From c83d383e4613300448bb081d37416ea2c00e48e4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 16:55:25 +0100 Subject: [PATCH 06/16] In testbed_ui explicitly use default fonts as needed. --- examples/testbed/ui.rs | 60 +++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 6dc2cbe6d7933..0fdb8f4f32cb8 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -116,9 +116,21 @@ mod text { }, DespawnOnExit(super::Scene::Text), children![ - (TextSpan::new("red "), TextColor(RED.into()),), - (TextSpan::new("green "), TextColor(GREEN.into()),), - (TextSpan::new("blue "), TextColor(BLUE.into()),), + ( + TextSpan::new("red "), + TextColor(RED.into()), + TextFont::default() + ), + ( + TextSpan::new("green "), + TextColor(GREEN.into()), + TextFont::default() + ), + ( + TextSpan::new("blue "), + TextColor(BLUE.into()), + TextFont::default() + ), ( TextSpan::new("black"), TextColor(Color::BLACK), @@ -151,9 +163,21 @@ mod text { ..default() } ), - (TextSpan::new("red "), TextColor(RED.into()),), - (TextSpan::new("green "), TextColor(GREEN.into()),), - (TextSpan::new("blue "), TextColor(BLUE.into()),), + ( + TextSpan::new("red "), + TextColor(RED.into()), + TextFont::default() + ), + ( + TextSpan::new("green "), + TextColor(GREEN.into()), + TextFont::default() + ), + ( + TextSpan::new("blue "), + TextColor(BLUE.into()), + TextFont::default() + ), ( TextSpan::new("black"), TextColor(Color::BLACK), @@ -189,14 +213,30 @@ mod text { } ), TextSpan::new(""), - (TextSpan::new("red "), TextColor(RED.into()),), + ( + TextSpan::new("red "), + TextColor(RED.into()), + TextFont::default() + ), TextSpan::new(""), TextSpan::new(""), - (TextSpan::new("green "), TextColor(GREEN.into()),), + ( + TextSpan::new("green "), + TextColor(GREEN.into()), + TextFont::default() + ), (TextSpan::new(""), TextColor(YELLOW.into()),), - (TextSpan::new("blue "), TextColor(BLUE.into()),), + ( + TextSpan::new("blue "), + TextColor(BLUE.into()), + TextFont::default() + ), TextSpan::new(""), - (TextSpan::new(""), TextColor(YELLOW.into()),), + ( + TextSpan::new(""), + TextColor(YELLOW.into()), + TextFont::default() + ), ( TextSpan::new("black"), TextColor(Color::BLACK), From a4c5dea25f714f966d2670132a66ff68b0e3a65d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 17:14:14 +0100 Subject: [PATCH 07/16] Replaced queries for `detect_text_needs_rerender` --- crates/bevy_text/src/text.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index fc37ef8aeccb1..5004245c1064e 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -459,12 +459,12 @@ pub fn detect_text_needs_rerender( ( Or<( Changed, - Changed, + Changed, Changed, Changed, )>, With, - With, + With, With, ), >, @@ -473,13 +473,13 @@ pub fn detect_text_needs_rerender( ( Or<( Changed, - Changed, + Changed, Changed, Changed, // Included to detect broken text block hierarchies. Added, )>, With, - With, + With, ), >, mut computed: Query<( From 2f7956ab23ebe8be22f47755f18c3aafda194205 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 17:17:57 +0100 Subject: [PATCH 08/16] Add comments --- crates/bevy_text/src/style.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_text/src/style.rs b/crates/bevy_text/src/style.rs index dc2b6c0fd183e..9e4df7d48b9de 100644 --- a/crates/bevy_text/src/style.rs +++ b/crates/bevy_text/src/style.rs @@ -42,7 +42,8 @@ impl ComputedTextStyle { } } -/// update text styles +/// Update the `ComputedTextStyle` for each text node from the +/// `TextFont`s and `TextColor`s of its nearest ancestors. pub fn update_text_styles( default_text_style: Res, mut computed_text_query: Query<(Entity, &mut ComputedTextStyle)>, From 176cd1d895fd016fa63afb02d91a6f95907d994c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 24 Sep 2025 11:43:15 +0100 Subject: [PATCH 09/16] Updated the docs for `DefaultTextStyle` and `ComputedTextStyle` --- crates/bevy_text/src/style.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/bevy_text/src/style.rs b/crates/bevy_text/src/style.rs index 9e4df7d48b9de..8f56dfc7f1250 100644 --- a/crates/bevy_text/src/style.rs +++ b/crates/bevy_text/src/style.rs @@ -3,12 +3,12 @@ use bevy_color::Color; use bevy_ecs::component::Component; use bevy_ecs::prelude::*; -/// Default text style +/// Fallback text style used if a text entity and all its ancestors lack text styling components. #[derive(Resource)] pub struct DefaultTextStyle { - /// default font - pub font: TextFont, - /// default color + /// The font used by a text entity when neither it nor any ancestor has a [`TextFont`] component. + font: TextFont, + /// The color used by a text entity when neither it nor any ancestor has a [`TextColor`] component. pub color: Color, } @@ -21,29 +21,35 @@ impl Default for DefaultTextStyle { } } -/// Computed text style +/// The resolved text style for a text entity. +/// +/// Updated by [`update_text_styles`] #[derive(Component, PartialEq, Default)] pub struct ComputedTextStyle { - /// From nearest ancestor with a `TextFont` + /// The resolved font, taken from the nearest ancestor (including self) with a [`TextFont`], + /// or from [`DefaultTextStyle`] if none is found. pub(crate) font: TextFont, - /// From nearest ancestor with a `TextColor` + /// The resolved text color, taken from the nearest ancestor (including self) with a [`TextColor`], + /// or from [`DefaultTextStyle`] if none is found. pub(crate) color: Color, } impl ComputedTextStyle { - /// Computed text font + /// The resolved font, taken from the nearest ancestor (including self) with a [`TextFont`], + /// or from [`DefaultTextStyle`] if none is found. pub const fn font(&self) -> &TextFont { &self.font } - /// Computed text color + /// The resolved text color, taken from the nearest ancestor (including self) with a [`TextColor`], + /// or from [`DefaultTextStyle`] if none is found. pub const fn color(&self) -> Color { self.color } } /// Update the `ComputedTextStyle` for each text node from the -/// `TextFont`s and `TextColor`s of its nearest ancestors. +/// `TextFont`s and `TextColor`s of its nearest ancestors, or from [`DefaultTextStyle`] if none are found. pub fn update_text_styles( default_text_style: Res, mut computed_text_query: Query<(Entity, &mut ComputedTextStyle)>, @@ -70,7 +76,8 @@ pub fn update_text_styles( if new_style.font != style.font { *style = new_style; } else { - style.color = new_style.color; + // bypass change detection, we don't need to do any updates if only the text color has changed + style.bypass_change_detection().color = new_style.color; } } } From 5784bdd65cce3021419dd40af66fa620560fb18c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 24 Sep 2025 14:35:09 +0100 Subject: [PATCH 10/16] * Removed `update_text_styles` system, replaced with seperate systems for 2d and UI, resolve_2d_computed_text_styles and resolve_ui_computed_text_styles. * Added `scale_factor` field to `ComputedTextStyle`. --- crates/bevy_sprite/src/lib.rs | 5 +- crates/bevy_sprite/src/text2d.rs | 149 +++++++++++++++++----------- crates/bevy_text/src/lib.rs | 5 +- crates/bevy_text/src/style.rs | 44 +------- crates/bevy_ui/src/accessibility.rs | 5 +- crates/bevy_ui/src/lib.rs | 7 +- crates/bevy_ui/src/update.rs | 38 +++++++ 7 files changed, 146 insertions(+), 107 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 61dd2e2b702bd..17ea4c10c4151 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -44,8 +44,7 @@ use bevy_camera::{ visibility::VisibilitySystems, }; use bevy_mesh::{Mesh, Mesh2d}; -#[cfg(feature = "bevy_text")] -use bevy_text::update_text_styles; + #[cfg(feature = "bevy_sprite_picking_backend")] pub use picking_backend::*; pub use sprite::*; @@ -86,10 +85,10 @@ impl Plugin for SpritePlugin { app.add_systems( PostUpdate, ( + resolve_2d_computed_text_styles, bevy_text::detect_text_needs_rerender::, update_text2d_layout .after(bevy_camera::CameraUpdateSystems) - .after(update_text_styles) .after(bevy_text::remove_dropped_font_atlas_sets), calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 4cb9e56623662..0fc60156c8ac6 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -7,7 +7,10 @@ use bevy_camera::visibility::{ use bevy_camera::Camera; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::change_detection::DetectChangesMut; use bevy_ecs::entity::EntityHashSet; +use bevy_ecs::hierarchy::ChildOf; +use bevy_ecs::query::With; use bevy_ecs::{ change_detection::{DetectChanges, Ref}, component::Component, @@ -19,13 +22,13 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; -use bevy_text::ComputedTextStyle; use bevy_text::{ ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds, - TextError, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, - TextWriter, + TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, + TextSpanAccess, TextWriter, }; -use bevy_transform::components::Transform; +use bevy_text::{ComputedTextStyle, DefaultTextStyle}; +use bevy_transform::components::{GlobalTransform, Transform}; use core::any::TypeId; /// The top-level 2D text component. @@ -158,67 +161,27 @@ 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 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_sets: ResMut, mut text_pipeline: ResMut, mut text_query: Query<( Entity, - Option<&RenderLayers>, Ref, Ref, &mut TextLayoutInfo, &mut ComputedTextBlock, + &ComputedTextStyle, )>, mut text_reader: Text2dReader, mut font_system: ResMut, mut swash_cache: ResMut, ) { - target_scale_factors.clear(); - target_scale_factors.extend( - camera_query - .iter() - .filter(|(_, visible_entities, _)| { - !visible_entities.get(TypeId::of::()).is_empty() - }) - .filter_map(|(camera, _, maybe_camera_mask)| { - camera.target_scaling_factor().map(|scale_factor| { - (scale_factor, maybe_camera_mask.cloned().unwrap_or_default()) - }) - }), - ); - - 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 - { - let entity_mask = maybe_entity_mask.unwrap_or_default(); - - let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor { - previous_scale_factor - } else { - // `Text2d` only supports generating a single text layout per Text2d entity. If a `Text2d` entity has multiple - // render targets with different scale factors, then we use the maximum of the scale factors. - let Some((scale_factor, mask)) = target_scale_factors - .iter() - .filter(|(_, camera_mask)| camera_mask.intersects(entity_mask)) - .max_by_key(|(scale_factor, _)| FloatOrd(*scale_factor)) - else { - continue; - }; - previous_scale_factor = *scale_factor; - previous_mask = mask; - *scale_factor - }; - - if scale_factor != text_layout_info.scale_factor + for (entity, block, bounds, text_layout_info, mut computed, style) in &mut text_query { + if style.scale_factor != text_layout_info.scale_factor || computed.needs_rerender() || bounds.is_changed() || (!queue.is_empty() && queue.remove(&entity)) @@ -227,9 +190,9 @@ pub fn update_text2d_layout( width: if block.linebreak == LineBreak::NoWrap { None } else { - bounds.width.map(|width| width * scale_factor) + bounds.width.map(|width| width * style.scale_factor) }, - height: bounds.height.map(|height| height * scale_factor), + height: bounds.height.map(|height| height * style.scale_factor), }; let text_layout_info = text_layout_info.into_inner(); @@ -237,7 +200,7 @@ pub fn update_text2d_layout( text_layout_info, &fonts, text_reader.iter(entity), - scale_factor as f64, + style.scale_factor as f64, &block, text_bounds, &mut font_atlas_sets, @@ -256,8 +219,8 @@ pub fn update_text2d_layout( panic!("Fatal error when processing text: {e}."); } Ok(()) => { - text_layout_info.scale_factor = scale_factor; - text_layout_info.size *= scale_factor.recip(); + text_layout_info.scale_factor = style.scale_factor; + text_layout_info.size *= style.scale_factor.recip(); } } } @@ -301,6 +264,82 @@ pub fn calculate_bounds_text2d( } } +/// Update the `ComputedTextStyle` for each `Text2d` entity from the +/// `TextFont`s and `TextColor`s of its nearest ancestors, or from [`DefaultTextStyle`] if none are found. +pub fn resolve_2d_computed_text_styles( + mut target_scale_factors: Local>, + default_text_style: Res, + mut computed_text_query: Query< + (Entity, &mut ComputedTextStyle, Option<&RenderLayers>), + With, + >, + parent_query: Query<&ChildOf>, + font_query: Query<(Option<&TextFont>, Option<&TextColor>)>, + camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, +) { + target_scale_factors.clear(); + target_scale_factors.extend( + camera_query + .iter() + .filter(|(_, visible_entities, _)| { + !visible_entities.get(TypeId::of::()).is_empty() + }) + .filter_map(|(camera, _, maybe_camera_mask)| { + camera.target_scaling_factor().map(|scale_factor| { + (scale_factor, maybe_camera_mask.cloned().unwrap_or_default()) + }) + }), + ); + + let mut previous_scale_factor = 0.; + let mut previous_mask = &RenderLayers::none(); + + for (start, mut style, maybe_entity_mask) in computed_text_query.iter_mut() { + let entity_mask = maybe_entity_mask.unwrap_or_default(); + + let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor { + previous_scale_factor + } else { + // `Text2d` only supports generating a single text layout per Text2d entity. If a `Text2d` entity has multiple + // render targets with different scale factors, then we use the maximum of the scale factors. + let Some((scale_factor, mask)) = target_scale_factors + .iter() + .filter(|(_, camera_mask)| camera_mask.intersects(entity_mask)) + .max_by_key(|(scale_factor, _)| FloatOrd(*scale_factor)) + else { + continue; + }; + previous_scale_factor = *scale_factor; + previous_mask = mask; + *scale_factor + }; + + let (mut font, mut color) = font_query.get(start).unwrap(); + let mut ancestors = parent_query.iter_ancestors(start); + + while (font.is_none() || color.is_none()) + && let Some(ancestor) = ancestors.next() + { + let (next_font, next_color) = font_query.get(ancestor).unwrap(); + font = font.or(next_font); + color = color.or(next_color); + } + + let new_style = ComputedTextStyle { + font: font.unwrap_or(&default_text_style.font).clone(), + color: color.map(|t| t.0).unwrap_or(default_text_style.color), + scale_factor, + }; + + if new_style.font != style.font || new_style.scale_factor != style.scale_factor { + *style = new_style; + } else { + // bypass change detection, we don't need to do any updates if only the text color has changed + style.bypass_change_detection().color = new_style.color; + } + } +} + #[cfg(test)] mod tests { @@ -309,9 +348,7 @@ mod tests { use bevy_camera::{ComputedCameraValues, RenderTargetInfo}; use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_math::UVec2; - use bevy_text::{ - detect_text_needs_rerender, update_text_styles, DefaultTextStyle, TextIterScratch, - }; + use bevy_text::{detect_text_needs_rerender, DefaultTextStyle, TextIterScratch}; use super::*; @@ -332,7 +369,7 @@ mod tests { .add_systems( Update, ( - update_text_styles, + resolve_2d_computed_text_styles, detect_text_needs_rerender::, update_text2d_layout, calculate_bounds_text2d, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 4ac0fa91a7464..49ec8459041d7 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -101,10 +101,7 @@ impl Plugin for TextPlugin { .init_resource::() .add_systems( PostUpdate, - ( - update_text_styles, - remove_dropped_font_atlas_sets.before(AssetEventSystems), - ), + remove_dropped_font_atlas_sets.before(AssetEventSystems), ) .add_systems(Last, trim_cosmic_cache); diff --git a/crates/bevy_text/src/style.rs b/crates/bevy_text/src/style.rs index 8f56dfc7f1250..d467ac69202ff 100644 --- a/crates/bevy_text/src/style.rs +++ b/crates/bevy_text/src/style.rs @@ -7,7 +7,7 @@ use bevy_ecs::prelude::*; #[derive(Resource)] pub struct DefaultTextStyle { /// The font used by a text entity when neither it nor any ancestor has a [`TextFont`] component. - font: TextFont, + pub font: TextFont, /// The color used by a text entity when neither it nor any ancestor has a [`TextColor`] component. pub color: Color, } @@ -22,16 +22,16 @@ impl Default for DefaultTextStyle { } /// The resolved text style for a text entity. -/// -/// Updated by [`update_text_styles`] #[derive(Component, PartialEq, Default)] pub struct ComputedTextStyle { /// The resolved font, taken from the nearest ancestor (including self) with a [`TextFont`], /// or from [`DefaultTextStyle`] if none is found. - pub(crate) font: TextFont, + pub font: TextFont, /// The resolved text color, taken from the nearest ancestor (including self) with a [`TextColor`], /// or from [`DefaultTextStyle`] if none is found. - pub(crate) color: Color, + pub color: Color, + /// Scale factor of the text entity's render target. + pub scale_factor: f32, } impl ComputedTextStyle { @@ -47,37 +47,3 @@ impl ComputedTextStyle { self.color } } - -/// Update the `ComputedTextStyle` for each text node from the -/// `TextFont`s and `TextColor`s of its nearest ancestors, or from [`DefaultTextStyle`] if none are found. -pub fn update_text_styles( - default_text_style: Res, - mut computed_text_query: Query<(Entity, &mut ComputedTextStyle)>, - parent_query: Query<&ChildOf>, - font_query: Query<(Option<&TextFont>, Option<&TextColor>)>, -) { - for (start, mut style) in computed_text_query.iter_mut() { - let (mut font, mut color) = font_query.get(start).unwrap(); - let mut ancestors = parent_query.iter_ancestors(start); - - while (font.is_none() || color.is_none()) - && let Some(ancestor) = ancestors.next() - { - let (next_font, next_color) = font_query.get(ancestor).unwrap(); - font = font.or(next_font); - color = color.or(next_color); - } - - let new_style = ComputedTextStyle { - font: font.unwrap_or(&default_text_style.font).clone(), - color: color.map(|t| t.0).unwrap_or(default_text_style.color), - }; - - if new_style.font != style.font { - *style = new_style; - } else { - // bypass change detection, we don't need to do any updates if only the text color has changed - style.bypass_change_detection().color = new_style.color; - } - } -} diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 265c4b3684613..7e423711faad6 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -2,6 +2,7 @@ use crate::{ experimental::UiChildren, prelude::{Button, Label}, ui_transform::UiGlobalTransform, + update::resolve_ui_computed_text_styles, widget::{ImageNode, TextUiReader}, ComputedNode, }; @@ -17,7 +18,6 @@ use bevy_ecs::{ use accesskit::{Node, Rect, Role}; use bevy_camera::CameraUpdateSystems; -use bevy_text::update_text_styles; fn calc_label( text_reader: &mut TextUiReader, @@ -155,7 +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).before(update_text_styles), + (button_changed, image_changed, label_changed) + .before(resolve_ui_computed_text_styles), ), ); } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index f73eb5301d7fb..e0ea4b7101f47 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -34,7 +34,6 @@ mod layout; mod stack; mod ui_node; -use bevy_text::update_text_styles; pub use focus::*; pub use geometry::*; pub use gradients::*; @@ -79,6 +78,8 @@ use stack::ui_stack_system; pub use stack::UiStack; use update::{propagate_ui_target_cameras, update_clipping_system}; +use crate::update::resolve_ui_computed_text_styles; + /// The basic plugin for Bevy UI #[derive(Default)] pub struct UiPlugin; @@ -231,6 +232,7 @@ fn build_text_interop(app: &mut App) { PostUpdate, ( ( + resolve_ui_computed_text_styles, bevy_text::detect_text_needs_rerender::, widget::measure_text_system, ) @@ -253,8 +255,7 @@ fn build_text_interop(app: &mut App) { .ambiguous_with(bevy_text::detect_text_needs_rerender::) .ambiguous_with(bevy_sprite::update_text2d_layout) .ambiguous_with(bevy_sprite::calculate_bounds_text2d), - ) - .after(update_text_styles), + ), ); app.add_plugins(accessibility::AccessibilityPlugin); diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index ad2746c0a1897..7536e38e9b786 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -11,12 +11,15 @@ use super::ComputedNode; use bevy_app::Propagate; use bevy_camera::Camera; use bevy_ecs::{ + change_detection::DetectChangesMut, entity::Entity, + hierarchy::ChildOf, query::Has, system::{Commands, Query, Res}, }; use bevy_math::{Rect, UVec2}; use bevy_sprite::BorderRect; +use bevy_text::{ComputedTextStyle, DefaultTextStyle, TextColor, TextFont}; /// Updates clipping for all nodes pub fn update_clipping_system( @@ -175,6 +178,41 @@ pub fn propagate_ui_target_cameras( } } +/// Update the `ComputedTextStyle` for each text node from the +/// `TextFont`s and `TextColor`s of its nearest ancestors, or from [`DefaultTextStyle`] if none are found. +pub fn resolve_ui_computed_text_styles( + default_text_style: Res, + mut computed_text_query: Query<(Entity, &mut ComputedTextStyle, &ComputedUiRenderTargetInfo)>, + parent_query: Query<&ChildOf>, + font_query: Query<(Option<&TextFont>, Option<&TextColor>)>, +) { + for (start, mut style, target_info) in computed_text_query.iter_mut() { + let (mut font, mut color) = font_query.get(start).unwrap(); + let mut ancestors = parent_query.iter_ancestors(start); + + while (font.is_none() || color.is_none()) + && let Some(ancestor) = ancestors.next() + { + let (next_font, next_color) = font_query.get(ancestor).unwrap(); + font = font.or(next_font); + color = color.or(next_color); + } + + let new_style = ComputedTextStyle { + font: font.unwrap_or(&default_text_style.font).clone(), + color: color.map(|t| t.0).unwrap_or(default_text_style.color), + scale_factor: target_info.scale_factor, + }; + + if new_style.font != style.font || new_style.scale_factor != style.scale_factor { + *style = new_style; + } else { + // bypass change detection, we don't need to do any updates if only the text color has changed + style.bypass_change_detection().color = new_style.color; + } + } +} + /// Update each `Camera`'s `RenderTargetInfo` from its associated `Window` render target. /// Cameras with non-window render targets are ignored. #[cfg(test)] From 39976267769afc7d9b8ab88d443d842a5ad8ce15 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 24 Sep 2025 16:04:53 +0100 Subject: [PATCH 11/16] Added `FontManager` resource --- crates/bevy_text/src/font_atlas_set.rs | 34 +++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index b1d70ea7e66f3..efac50a2d6589 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,12 +1,16 @@ use bevy_asset::{AssetEvent, AssetId, Assets, RenderAssetUsages}; -use bevy_ecs::{message::MessageReader, resource::Resource, system::ResMut}; +use bevy_ecs::{ + message::MessageReader, + resource::Resource, + system::{Query, ResMut}, +}; use bevy_image::prelude::*; use bevy_math::{IVec2, UVec2}; -use bevy_platform::collections::HashMap; +use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::TypePath; use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; -use crate::{error::TextError, Font, FontAtlas, FontSmoothing, GlyphAtlasInfo}; +use crate::{error::TextError, ComputedTextStyle, Font, FontAtlas, FontSmoothing, GlyphAtlasInfo}; /// A map of font faces to their corresponding [`FontAtlasSet`]s. #[derive(Debug, Default, Resource)] @@ -252,3 +256,27 @@ impl FontAtlasSet { )) } } + +#[derive(Resource)] +pub struct FontManager { + pub max_count: u32, + pub least_recently_used: Vec<(AssetId, FontAtlasKey, u32)>, +} + +pub fn free_unused_font_atlases( + mut font_atlas_sets: ResMut, + mut font_manager: ResMut, + active_font_query: Query<&ComputedTextStyle>, +) { + let mut active_fonts: HashSet<(AssetId, FontAtlasKey)> = HashSet::default(); + + for style in active_font_query.iter() { + active_fonts.insert(( + style.font.font.id(), + FontAtlasKey( + (style.font.font_size * style.scale_factor).to_bits(), + style.font.font_smoothing, + ), + )); + } +} From f3641b47be2012272c3abc5d542a69c9286ddf23 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 24 Sep 2025 16:50:57 +0100 Subject: [PATCH 12/16] added free_unused_font_atlases system --- crates/bevy_text/src/font_atlas_set.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index efac50a2d6589..14f4c104ca503 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -257,18 +257,22 @@ impl FontAtlasSet { } } +/// manage #[derive(Resource)] pub struct FontManager { pub max_count: u32, pub least_recently_used: Vec<(AssetId, FontAtlasKey, u32)>, } +/// free pub fn free_unused_font_atlases( + mut last_active_fonts: HashSet<(AssetId, FontAtlasKey)>, + mut active_fonts: HashSet<(AssetId, FontAtlasKey)>, mut font_atlas_sets: ResMut, mut font_manager: ResMut, active_font_query: Query<&ComputedTextStyle>, ) { - let mut active_fonts: HashSet<(AssetId, FontAtlasKey)> = HashSet::default(); + active_fonts.clear(); for style in active_font_query.iter() { active_fonts.insert(( @@ -279,4 +283,8 @@ pub fn free_unused_font_atlases( ), )); } + + let activated = active_fonts.difference(&last_active_fonts); + + let deactivated = last_active_fonts.difference(&active_fonts); } From 06ff3b1d4c1c19ce26c97f2e2c70a65efc9175b2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 24 Sep 2025 22:56:44 +0100 Subject: [PATCH 13/16] Added `count_fonts` function to `FontAtlasSets` --- crates/bevy_text/src/font_atlas_set.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 14f4c104ca503..85af90b1f8cf4 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -30,6 +30,15 @@ impl FontAtlasSets { let id: AssetId = id.into(); self.sets.get_mut(&id) } + + /// Returns the total number of fonts in all sets + pub fn count_fonts(&self) -> usize { + let mut count = 0; + for (_, set) in self.sets.iter() { + count += set.len() + } + count + } } /// A system that cleans up [`FontAtlasSet`]s for removed [`Font`]s From e2a1a4764ff7c260fb50afee9aa8f2b3e5e12475 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 25 Sep 2025 00:54:35 +0100 Subject: [PATCH 14/16] implemented `free_unused_font_atlases` function --- crates/bevy_text/src/font_atlas_set.rs | 40 +++++++++++++++++++------- crates/bevy_text/src/lib.rs | 3 +- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 85af90b1f8cf4..247ac0debae71 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -2,7 +2,7 @@ use bevy_asset::{AssetEvent, AssetId, Assets, RenderAssetUsages}; use bevy_ecs::{ message::MessageReader, resource::Resource, - system::{Query, ResMut}, + system::{Local, Query, ResMut}, }; use bevy_image::prelude::*; use bevy_math::{IVec2, UVec2}; @@ -56,7 +56,7 @@ pub fn remove_dropped_font_atlas_sets( /// Identifies a font size and smoothing method in a [`FontAtlasSet`]. /// /// Allows an `f32` font size to be used as a key in a `HashMap`, by its binary representation. -#[derive(Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct FontAtlasKey(pub u32, pub FontSmoothing); /// A map of font sizes to their corresponding [`FontAtlas`]es, for a given font face. @@ -69,7 +69,7 @@ pub struct FontAtlasKey(pub u32, pub FontSmoothing); /// A `FontAtlasSet` contains one or more [`FontAtlas`]es for each font size. #[derive(Debug, TypePath)] pub struct FontAtlasSet { - font_atlases: HashMap>, + pub(crate) font_atlases: HashMap>, } impl Default for FontAtlasSet { @@ -268,17 +268,21 @@ impl FontAtlasSet { /// manage #[derive(Resource)] -pub struct FontManager { - pub max_count: u32, - pub least_recently_used: Vec<(AssetId, FontAtlasKey, u32)>, +pub struct MaxFonts(usize); + +impl Default for MaxFonts { + fn default() -> Self { + Self(20) + } } /// free pub fn free_unused_font_atlases( - mut last_active_fonts: HashSet<(AssetId, FontAtlasKey)>, - mut active_fonts: HashSet<(AssetId, FontAtlasKey)>, + mut least_recently_used: Local, FontAtlasKey)>>, + mut last_active_fonts: Local, FontAtlasKey)>>, + mut active_fonts: Local, FontAtlasKey)>>, mut font_atlas_sets: ResMut, - mut font_manager: ResMut, + max_fonts: ResMut, active_font_query: Query<&ComputedTextStyle>, ) { active_fonts.clear(); @@ -293,7 +297,21 @@ pub fn free_unused_font_atlases( )); } - let activated = active_fonts.difference(&last_active_fonts); + least_recently_used.retain(|font| !active_fonts.contains(font)); + + for unused_font in last_active_fonts.difference(&active_fonts) { + least_recently_used.push(unused_font.clone()); + } + + let count = font_atlas_sets.count_fonts(); + if max_fonts.0 < count { + let d = count - max_fonts.0; + for (font, key) in least_recently_used.drain(0..d) { + if let Some(font_atlas_set) = font_atlas_sets.get_mut(font) { + font_atlas_set.font_atlases.remove(&key); + } + } + } - let deactivated = last_active_fonts.difference(&active_fonts); + core::mem::swap(&mut *last_active_fonts, &mut *active_fonts); } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 49ec8459041d7..823876c001a48 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -99,11 +99,12 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( PostUpdate, remove_dropped_font_atlas_sets.before(AssetEventSystems), ) - .add_systems(Last, trim_cosmic_cache); + .add_systems(Last, (trim_cosmic_cache, free_unused_font_atlases)); #[cfg(feature = "default_font")] { From 85898b2c51a451d082f6fa323cb75c5647a31a53 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 25 Sep 2025 10:48:29 +0100 Subject: [PATCH 15/16] * Renamed `count_fonts` to `font_count`. * Finished the `free_unused_font_atlases` system. --- crates/bevy_text/src/font_atlas_set.rs | 51 +++++++++++++++----------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 247ac0debae71..144755947245b 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -32,7 +32,7 @@ impl FontAtlasSets { } /// Returns the total number of fonts in all sets - pub fn count_fonts(&self) -> usize { + pub fn font_count(&self) -> usize { let mut count = 0; for (_, set) in self.sets.iter() { count += set.len() @@ -266,7 +266,7 @@ impl FontAtlasSet { } } -/// manage +/// Maximum number of fonts #[derive(Resource)] pub struct MaxFonts(usize); @@ -276,42 +276,51 @@ impl Default for MaxFonts { } } -/// free +/// Automatically frees unused fonts when the total number of fonts +/// is greater than the [`MaxFonts`] value. Doesn't free in use fonts +/// even if the number of in use fonts is greater than [`MaxFonts`]. pub fn free_unused_font_atlases( + // list of unused fonts in order from least to most recently used mut least_recently_used: Local, FontAtlasKey)>>, - mut last_active_fonts: Local, FontAtlasKey)>>, + // fonts that were in use the previous frame + mut previous_active_fonts: Local, FontAtlasKey)>>, mut active_fonts: Local, FontAtlasKey)>>, mut font_atlas_sets: ResMut, max_fonts: ResMut, active_font_query: Query<&ComputedTextStyle>, ) { - active_fonts.clear(); - - for style in active_font_query.iter() { - active_fonts.insert(( + // collect keys for all fonts currently in use by a text entity + active_fonts.extend(active_font_query.iter().map(|style| { + ( style.font.font.id(), FontAtlasKey( (style.font.font_size * style.scale_factor).to_bits(), style.font.font_smoothing, ), - )); - } + ) + })); + // remove any keys for fonts in use from the least recently used list least_recently_used.retain(|font| !active_fonts.contains(font)); - for unused_font in last_active_fonts.difference(&active_fonts) { - least_recently_used.push(unused_font.clone()); - } + // push keys for any fonts no longer in use onto the least recently used list + least_recently_used.extend( + previous_active_fonts + .difference(&active_fonts) + .into_iter() + .cloned(), + ); - let count = font_atlas_sets.count_fonts(); - if max_fonts.0 < count { - let d = count - max_fonts.0; - for (font, key) in least_recently_used.drain(0..d) { - if let Some(font_atlas_set) = font_atlas_sets.get_mut(font) { - font_atlas_set.font_atlases.remove(&key); - } + // If the total number of fonts is greater than max_fonts, free fonts from the least rcently used list + // until the total is lower than max_fonts or the least recently used list is empty. + for (font, key) in + least_recently_used.drain(0..font_atlas_sets.font_count().saturating_sub(max_fonts.0)) + { + if let Some(font_atlas_set) = font_atlas_sets.get_mut(font) { + font_atlas_set.font_atlases.remove(&key); } } - core::mem::swap(&mut *last_active_fonts, &mut *active_fonts); + previous_active_fonts.clear(); + core::mem::swap(&mut *previous_active_fonts, &mut *active_fonts); } From fa0050cffef30dc04cfb7e2af23517027bc110ba Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 25 Sep 2025 10:53:47 +0100 Subject: [PATCH 16/16] Added draft migration and relase notes --- .../migration-guides/unused-font-atlases-are-now-freed.md | 4 ++++ release-content/release-notes/free-unused-font-atlases.md | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 release-content/migration-guides/unused-font-atlases-are-now-freed.md create mode 100644 release-content/release-notes/free-unused-font-atlases.md diff --git a/release-content/migration-guides/unused-font-atlases-are-now-freed.md b/release-content/migration-guides/unused-font-atlases-are-now-freed.md new file mode 100644 index 0000000000000..d5318829f8559 --- /dev/null +++ b/release-content/migration-guides/unused-font-atlases-are-now-freed.md @@ -0,0 +1,4 @@ +--- +title: Unused font atlases are now freed +pull_requests: [21211] +--- \ No newline at end of file diff --git a/release-content/release-notes/free-unused-font-atlases.md b/release-content/release-notes/free-unused-font-atlases.md new file mode 100644 index 0000000000000..fc880affd8faa --- /dev/null +++ b/release-content/release-notes/free-unused-font-atlases.md @@ -0,0 +1,5 @@ +--- +title: Free unused font atlases +authors: ["@Ickshonpe"] +pull_requests: [21211] +---