From be8e768a0dfaed7d0778bbab8994ed02fd7a25e9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 10:49:47 +0100 Subject: [PATCH 01/94] 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/94] * 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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 259ac1213a914551f7a78131d9b246eabe6940bd Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 27 Sep 2025 15:01:45 +0100 Subject: [PATCH 10/94] Split up `TextFont` into individual components. --- crates/bevy_text/src/style.rs | 64 ++++++++++++++++++--- crates/bevy_text/src/text.rs | 103 +++++++++------------------------- 2 files changed, 83 insertions(+), 84 deletions(-) diff --git a/crates/bevy_text/src/style.rs b/crates/bevy_text/src/style.rs index 8f56dfc7f1250..6e447e20bf800 100644 --- a/crates/bevy_text/src/style.rs +++ b/crates/bevy_text/src/style.rs @@ -1,4 +1,5 @@ use crate::*; +use bevy_asset::Handle; use bevy_color::Color; use bevy_ecs::component::Component; use bevy_ecs::prelude::*; @@ -7,9 +8,12 @@ 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, + font: Handle, /// The color used by a text entity when neither it nor any ancestor has a [`TextColor`] component. pub color: Color, + pub font_smoothing: FontSmoothing, + pub line_height: LineHeight, + pub font_size: f32, } impl Default for DefaultTextStyle { @@ -17,6 +21,9 @@ impl Default for DefaultTextStyle { Self { font: Default::default(), color: Color::WHITE, + font_smoothing: FontSmoothing::default(), + line_height: LineHeight::default(), + font_size: FontSize::default().0, } } } @@ -28,7 +35,14 @@ impl Default for DefaultTextStyle { 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(crate) font: Handle, + /// The vertical height of rasterized glyphs in the font atlas in pixels. + pub(crate) font_size: f32, + /// The antialiasing method to use when rendering text. + pub(crate) font_smoothing: FontSmoothing, + /// The vertical height of a line of text, from the top of one line to the top of the + /// next. + pub(crate) line_height: LineHeight, /// 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, @@ -37,7 +51,7 @@ pub struct ComputedTextStyle { impl ComputedTextStyle { /// 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 { + pub const fn font(&self) -> &Handle { &self.font } @@ -46,6 +60,22 @@ impl ComputedTextStyle { pub const fn color(&self) -> Color { self.color } + + /// The vertical height of a line of text, from the top of one line to the top of the + /// next. + pub const fn line_height(&self) -> LineHeight { + self.line_height + } + + /// The vertical height of rasterized glyphs in the font atlas in pixels. + pub const fn font_size(&self) -> f32 { + self.font_size + } + + /// The antialiasing method to use when rendering text. + pub const fn font_smoothing(&self) -> FontSmoothing { + self.font_smoothing + } } /// Update the `ComputedTextStyle` for each text node from the @@ -54,23 +84,43 @@ 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>)>, + font_query: Query<( + Option<&TextFont>, + Option<&TextColor>, + Option<&FontSize>, + Option<&LineHeight>, + Option<&FontSmoothing>, + )>, ) { for (start, mut style) in computed_text_query.iter_mut() { - let (mut font, mut color) = font_query.get(start).unwrap(); + let (mut font, mut color, mut size, mut line_height, mut smoothing) = + 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(); + let (next_font, next_color, next_size, next_line_height, next_smoothing) = + font_query.get(ancestor).unwrap(); font = font.or(next_font); color = color.or(next_color); + size = size.or(next_size); + line_height = line_height.or(next_line_height); + smoothing = smoothing.or(next_smoothing); } let new_style = ComputedTextStyle { - font: font.unwrap_or(&default_text_style.font).clone(), + font: font + .map_or(&default_text_style.font, |font| &font.0) + .clone(), color: color.map(|t| t.0).unwrap_or(default_text_style.color), + font_size: size.map_or(default_text_style.font_size, |size| size.0), + font_smoothing: smoothing + .copied() + .unwrap_or(default_text_style.font_smoothing), + line_height: line_height + .copied() + .unwrap_or(default_text_style.line_height), }; if new_style.font != style.font { diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 5004245c1064e..b5579d801cb01 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -243,97 +243,44 @@ impl From for cosmic_text::Align { } } -/// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically -/// the font face, the font size, the line height, and the antialiasing method. -#[derive(Component, Clone, Debug, Reflect, PartialEq)] +/// The specific font face to use, as a `Handle` to a [`Font`] asset. +/// +/// If the `font` is not specified, then +/// * if `default_font` feature is enabled (enabled by default in `bevy` crate), +/// `FiraMono-subset.ttf` compiled into the library is used. +/// * otherwise no text will be rendered, unless a custom font is loaded into the default font +/// handle. +#[derive(Default, Component, Clone, Debug, Reflect, PartialEq)] #[reflect(Component, Default, Debug, Clone)] -pub struct TextFont { - /// The specific font face to use, as a `Handle` to a [`Font`] asset. - /// - /// If the `font` is not specified, then - /// * if `default_font` feature is enabled (enabled by default in `bevy` crate), - /// `FiraMono-subset.ttf` compiled into the library is used. - /// * otherwise no text will be rendered, unless a custom font is loaded into the default font - /// handle. - pub font: Handle, - /// The vertical height of rasterized glyphs in the font atlas in pixels. - /// - /// This is multiplied by the window scale factor and `UiScale`, but not the text entity - /// transform or camera projection. - /// - /// A new font atlas is generated for every combination of font handle and scaled font size - /// which can have a strong performance impact. - pub font_size: f32, - /// The vertical height of a line of text, from the top of one line to the top of the - /// next. - /// - /// Defaults to `LineHeight::RelativeToFont(1.2)` - pub line_height: LineHeight, - /// The antialiasing method to use when rendering text. - pub font_smoothing: FontSmoothing, -} - -impl TextFont { - /// Returns a new [`TextFont`] with the specified font size. - pub fn from_font_size(font_size: f32) -> Self { - Self::default().with_font_size(font_size) - } - - /// Returns this [`TextFont`] with the specified font face handle. - pub fn with_font(mut self, font: Handle) -> Self { - self.font = font; - self - } - - /// Returns this [`TextFont`] with the specified font size. - pub const fn with_font_size(mut self, font_size: f32) -> Self { - self.font_size = font_size; - self - } - - /// Returns this [`TextFont`] with the specified [`FontSmoothing`]. - pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self { - self.font_smoothing = font_smoothing; - self - } - - /// Returns this [`TextFont`] with the specified [`LineHeight`]. - pub const fn with_line_height(mut self, line_height: LineHeight) -> Self { - self.line_height = line_height; - self - } -} +pub struct TextFont(pub Handle); impl From> for TextFont { fn from(font: Handle) -> Self { - Self { font, ..default() } + Self(font) } } -impl From for TextFont { - fn from(line_height: LineHeight) -> Self { - Self { - line_height, - ..default() - } - } -} +/// The vertical height of rasterized glyphs in the font atlas in pixels. +/// +/// This is multiplied by the window scale factor and `UiScale`, but not the text entity +/// transform or camera projection. +/// +/// A new font atlas is generated for every combination of font handle and scaled font size +/// which can have a strong performance impact. +#[derive(Component, Copy, Clone, Debug, Reflect, PartialEq)] +#[reflect(Component, Default, Debug, Clone)] +pub struct FontSize(pub f32); -impl Default for TextFont { +impl Default for FontSize { fn default() -> Self { - Self { - font: Default::default(), - font_size: 20.0, - line_height: LineHeight::default(), - font_smoothing: Default::default(), - } + Self(20.) } } /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size -#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Component)] #[reflect(Debug, Clone, PartialEq)] pub enum LineHeight { /// Set line height to a specific number of pixels @@ -429,7 +376,9 @@ pub enum LineBreak { /// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look. /// /// **Note:** Subpixel antialiasing is not currently supported. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)] +#[derive( + Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize, +)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)] #[doc(alias = "antialiasing")] #[doc(alias = "pixelated")] From fd076c16a0273ce83a903f0879010a3731cd0366 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 27 Sep 2025 15:03:42 +0100 Subject: [PATCH 11/94] Removed TextWriter, replaced individual getters from TextReader with a single get_style method --- crates/bevy_text/src/text_access.rs | 287 +--------------------------- 1 file changed, 2 insertions(+), 285 deletions(-) diff --git a/crates/bevy_text/src/text_access.rs b/crates/bevy_text/src/text_access.rs index ec75efb6d960b..68af0f091daeb 100644 --- a/crates/bevy_text/src/text_access.rs +++ b/crates/bevy_text/src/text_access.rs @@ -1,4 +1,3 @@ -use bevy_color::Color; use bevy_ecs::{ component::Mutable, prelude::*, @@ -100,36 +99,8 @@ impl<'w, 's, R: TextRoot> TextReader<'w, 's, R> { } /// 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(|(_, _, _, 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(|(_, _, _, style)| style.color) - } - - /// Gets the text value of a text span within a text block at a specific index in the flattened span list. - /// - /// Panics if there is no span at the requested index. - pub fn text(&mut self, root_entity: Entity, index: usize) -> &str { - self.get_text(root_entity, index).unwrap() - } - - /// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list. - /// - /// Panics if there is no span at the requested index. - pub fn font(&mut self, root_entity: Entity, index: usize) -> &TextFont { - self.get_font(root_entity, index).unwrap() - } - - /// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list. - /// - /// Panics if there is no span at the requested index. - pub fn color(&mut self, root_entity: Entity, index: usize) -> Color { - self.get_color(root_entity, index).unwrap() + pub fn get_style(&mut self, root_entity: Entity, index: usize) -> Option<&ComputedTextStyle> { + self.get(root_entity, index).map(|(_, _, _, style)| style) } } @@ -215,257 +186,3 @@ impl<'a, R: TextRoot> Drop for TextSpanIter<'a, R> { self.scratch.recover(stack); } } - -/// System parameter for reading and writing text spans in a text block. -/// -/// `R` is the root text component, and `S` is the text span component on children. -#[derive(SystemParam)] -pub struct TextWriter<'w, 's, R: TextRoot> { - // This is a resource because two TextWriters can't run in parallel. - scratch: ResMut<'w, TextIterScratch>, - roots: Query< - 'w, - 's, - ( - &'static mut R, - &'static mut TextFont, - &'static mut TextColor, - ), - Without, - >, - spans: Query< - 'w, - 's, - ( - &'static mut TextSpan, - &'static mut TextFont, - &'static mut TextColor, - ), - Without, - >, - children: Query<'w, 's, &'static Children>, -} - -impl<'w, 's, R: TextRoot> 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, - root_entity: Entity, - index: usize, - ) -> Option<( - Entity, - usize, - Mut<'_, String>, - Mut<'_, TextFont>, - Mut<'_, TextColor>, - )> { - // Root - if index == 0 { - let (text, font, color) = self.roots.get_mut(root_entity).ok()?; - return Some(( - root_entity, - 0, - text.map_unchanged(|t| t.write_span()), - font, - color, - )); - } - - // Prep stack. - let mut stack: Vec<(&Children, usize)> = self.scratch.take(); - if let Ok(children) = self.children.get(root_entity) { - stack.push((children, 0)); - } - - // Span - let mut count = 1; - let (depth, entity) = 'l: loop { - let Some((children, idx)) = stack.last_mut() else { - self.scratch.recover(stack); - return None; - }; - - loop { - let Some(child) = children.get(*idx) else { - // All children at this stack entry have been iterated. - stack.pop(); - break; - }; - - // Increment to prep the next entity in this stack level. - *idx += 1; - - if !self.spans.contains(*child) { - continue; - }; - count += 1; - - if count - 1 == index { - let depth = stack.len(); - self.scratch.recover(stack); - break 'l (depth, *child); - } - - if let Ok(children) = self.children.get(*child) { - stack.push((children, 0)); - break; - } - } - }; - - // Note: We do this outside the loop due to borrow checker limitations. - let (text, font, color) = self.spans.get_mut(entity).unwrap(); - Some(( - entity, - depth, - text.map_unchanged(|t| t.write_span()), - font, - color, - )) - } - - /// 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> { - 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> { - self.get(root_entity, index).map(|(_, _, _, font, _)| 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) - } - - /// Gets the text value of a text span within a text block at a specific index in the flattened span list. - /// - /// Panics if there is no span at the requested index. - pub fn text(&mut self, root_entity: Entity, index: usize) -> Mut<'_, String> { - self.get_text(root_entity, index).unwrap() - } - - /// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list. - /// - /// Panics if there is no span at the requested index. - pub fn font(&mut self, root_entity: Entity, index: usize) -> Mut<'_, TextFont> { - self.get_font(root_entity, index).unwrap() - } - - /// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list. - /// - /// Panics if there is no span at the requested index. - pub fn color(&mut self, root_entity: Entity, index: usize) -> Mut<'_, TextColor> { - self.get_color(root_entity, index).unwrap() - } - - /// Invokes a callback on each span in a text block, starting with the root entity. - pub fn for_each( - &mut self, - root_entity: Entity, - mut callback: impl FnMut(Entity, usize, Mut, Mut, Mut), - ) { - self.for_each_until(root_entity, |a, b, c, d, e| { - (callback)(a, b, c, d, e); - true - }); - } - - /// Invokes a callback on each span's string value in a text block, starting with the root entity. - pub fn for_each_text(&mut self, root_entity: Entity, mut callback: impl FnMut(Mut)) { - self.for_each(root_entity, |_, _, text, _, _| { - (callback)(text); - }); - } - - /// Invokes a callback on each span's [`TextFont`] in a text block, starting with the root entity. - pub fn for_each_font(&mut self, root_entity: Entity, mut callback: impl FnMut(Mut)) { - self.for_each(root_entity, |_, _, _, font, _| { - (callback)(font); - }); - } - - /// Invokes a callback on each span's [`TextColor`] in a text block, starting with the root entity. - pub fn for_each_color( - &mut self, - root_entity: Entity, - mut callback: impl FnMut(Mut), - ) { - self.for_each(root_entity, |_, _, _, _, color| { - (callback)(color); - }); - } - - /// Invokes a callback on each span in a text block, starting with the root entity. - /// - /// Traversal will stop when the callback returns `false`. - // TODO: find a way to consolidate get and for_each_until, or provide a real iterator. Lifetime issues are challenging here. - pub fn for_each_until( - &mut self, - root_entity: Entity, - mut callback: impl FnMut(Entity, usize, Mut, Mut, Mut) -> bool, - ) { - // Root - let Ok((text, font, color)) = self.roots.get_mut(root_entity) else { - return; - }; - if !(callback)( - root_entity, - 0, - text.map_unchanged(|t| t.write_span()), - font, - color, - ) { - return; - } - - // Prep stack. - let mut stack: Vec<(&Children, usize)> = self.scratch.take(); - if let Ok(children) = self.children.get(root_entity) { - stack.push((children, 0)); - } - - // Span - loop { - let depth = stack.len(); - let Some((children, idx)) = stack.last_mut() else { - self.scratch.recover(stack); - return; - }; - - loop { - let Some(child) = children.get(*idx) else { - // All children at this stack entry have been iterated. - stack.pop(); - break; - }; - - // Increment to prep the next entity in this stack level. - *idx += 1; - - let entity = *child; - let Ok((text, font, color)) = self.spans.get_mut(entity) else { - continue; - }; - - if !(callback)( - entity, - depth, - text.map_unchanged(|t| t.write_span()), - font, - color, - ) { - self.scratch.recover(stack); - return; - } - - if let Ok(children) = self.children.get(entity) { - stack.push((children, 0)); - break; - } - } - } - } -} From a0349a59c1b9e67b1362670c3717dac07bc4888c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 28 Sep 2025 08:44:18 +0100 Subject: [PATCH 12/94] Clean up, updated dev tools module --- crates/bevy_dev_tools/src/fps_overlay.rs | 49 +++++++--------------- crates/bevy_dev_tools/src/picking_debug.rs | 3 +- crates/bevy_feathers/src/font_styles.rs | 10 ++--- crates/bevy_sprite/src/lib.rs | 2 +- crates/bevy_sprite/src/text2d.rs | 4 -- crates/bevy_text/src/pipeline.rs | 27 +++++------- crates/bevy_text/src/style.rs | 40 ++++++++++++++---- crates/bevy_text/src/text.rs | 2 +- crates/bevy_text/src/text_access.rs | 2 +- crates/bevy_ui/src/lib.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 5 +-- examples/dev_tools/fps_overlay.rs | 18 ++++---- 12 files changed, 80 insertions(+), 84 deletions(-) diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index ea5c0fde4318b..24cd45fd50f38 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -1,12 +1,10 @@ //! Module containing logic for FPS overlay. use bevy_app::{Plugin, Startup, Update}; -use bevy_asset::{Assets, Handle}; -use bevy_color::Color; +use bevy_asset::Assets; use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}; use bevy_ecs::{ component::Component, - entity::Entity, prelude::Local, query::{With, Without}, resource::Resource, @@ -15,12 +13,9 @@ use bevy_ecs::{ }; use bevy_picking::Pickable; use bevy_render::storage::ShaderStorageBuffer; -use bevy_text::{Font, TextColor, TextFont, TextSpan}; +use bevy_text::{TextColor, TextFont, TextSpan, TextStyle}; use bevy_time::Time; -use bevy_ui::{ - widget::{Text, TextUiWriter}, - FlexDirection, GlobalZIndex, Node, PositionType, Val, -}; +use bevy_ui::{widget::Text, FlexDirection, GlobalZIndex, Node, PositionType, Val}; use bevy_ui_render::prelude::MaterialNode; use core::time::Duration; @@ -78,9 +73,7 @@ impl Plugin for FpsOverlayPlugin { #[derive(Resource, Clone)] pub struct FpsOverlayConfig { /// Configuration of text in the overlay. - pub text_config: TextFont, - /// Color of text in the overlay. - pub text_color: Color, + pub text_style: TextStyle, /// Displays the FPS overlay if true. pub enabled: bool, /// The period after which the FPS overlay re-renders. @@ -94,12 +87,7 @@ pub struct FpsOverlayConfig { impl Default for FpsOverlayConfig { fn default() -> Self { FpsOverlayConfig { - text_config: TextFont { - font: Handle::::default(), - font_size: 32.0, - ..Default::default() - }, - text_color: Color::WHITE, + text_style: TextStyle::default(), enabled: true, refresh_interval: Duration::from_millis(100), // TODO set this to display refresh rate if possible @@ -170,14 +158,13 @@ fn setup( .with_children(|p| { p.spawn(( Text::new("FPS: "), - overlay_config.text_config.clone(), - TextColor(overlay_config.text_color), + overlay_config.text_style.bundle(), FpsText, Pickable::IGNORE, )) - .with_child((TextSpan::default(), overlay_config.text_config.clone())); + .with_child(TextSpan::default()); - let font_size = overlay_config.text_config.font_size; + let font_size = overlay_config.text_style.font_size; p.spawn(( Node { width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE), @@ -211,8 +198,7 @@ fn setup( fn update_text( diagnostic: Res, - query: Query>, - mut writer: TextUiWriter, + mut query: Query<&mut Text, With>, time: Res