From be8e768a0dfaed7d0778bbab8994ed02fd7a25e9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 23 Sep 2025 10:49:47 +0100 Subject: [PATCH 1/9] 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 2/9] * 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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; } } }