diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index 069882cb4e3c6..1d6eee466c7e4 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -11,11 +11,8 @@ use bevy_ecs::{ system::{Commands, Query, Res, Resource}, }; use bevy_hierarchy::{BuildChildren, ChildBuild}; -use bevy_text::{Font, Text, TextSection, TextStyle}; -use bevy_ui::{ - node_bundles::{NodeBundle, TextBundle}, - PositionType, Style, ZIndex, -}; +use bevy_text::{Font, TextSection, TextStyle}; +use bevy_ui::{node_bundles::TextBundle, PositionType, Style, ZIndex}; use bevy_utils::default; /// Global [`ZIndex`] used to render the fps overlay. @@ -78,7 +75,7 @@ struct FpsText; fn setup(mut commands: Commands, overlay_config: Res) { commands - .spawn(NodeBundle { + .spawn((TextBundle { style: Style { // We need to make sure the overlay doesn't affect the position of other UI nodes position_type: PositionType::Absolute, @@ -87,23 +84,28 @@ fn setup(mut commands: Commands, overlay_config: Res) { // Render overlay on top of everything z_index: ZIndex::Global(FPS_OVERLAY_ZINDEX), ..default() - }) + },)) .with_children(|c| { + c.spawn(TextSection::new( + "FPS: ", + overlay_config.text_config.clone(), + )); + c.spawn(( - TextBundle::from_sections([ - TextSection::new("FPS: ", overlay_config.text_config.clone()), - TextSection::from_style(overlay_config.text_config.clone()), - ]), + TextSection::from_style(overlay_config.text_config.clone()), FpsText, )); }); } -fn update_text(diagnostic: Res, mut query: Query<&mut Text, With>) { - for mut text in &mut query { +fn update_text( + diagnostic: Res, + mut query: Query<&mut TextSection, With>, +) { + for mut section in &mut query { if let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS) { if let Some(value) = fps.smoothed() { - text.sections[1].value = format!("{value:.2}"); + section.value = format!("{value:.2}"); } } } @@ -111,11 +113,9 @@ fn update_text(diagnostic: Res, mut query: Query<&mut Text, Wi fn customize_text( overlay_config: Res, - mut query: Query<&mut Text, With>, + mut query: Query<&mut TextSection, With>, ) { - for mut text in &mut query { - for section in text.sections.iter_mut() { - section.style = overlay_config.text_config.clone(); - } + for mut section in &mut query { + section.style = overlay_config.text_config.clone(); } } diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index a5a337c676d56..ce22010470582 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -18,6 +18,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 6568a9204462e..4a496b315e706 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use bevy_asset::{AssetId, Assets}; -use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource, world::Ref}; use bevy_math::{UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::texture::Image; @@ -61,7 +61,7 @@ impl TextPipeline { pub fn update_buffer( &mut self, fonts: &Assets, - sections: &[TextSection], + sections: &[(usize, Ref)], linebreak_behavior: BreakLineOn, bounds: TextBounds, scale_factor: f64, @@ -72,7 +72,7 @@ impl TextPipeline { // return early if the fonts are not loaded yet let mut font_size = 0.; - for section in sections { + for (_, section) in sections { if section.style.font_size > font_size { font_size = section.style.font_size; } @@ -85,7 +85,7 @@ impl TextPipeline { // Load Bevy fonts into cosmic-text's font system. // This is done as as separate pre-pass to avoid borrow checker issues - for section in sections.iter() { + for (_, section) in sections { load_font_to_fontdb(section, font_system, &mut self.map_handle_to_font_id, fonts); } @@ -97,14 +97,13 @@ impl TextPipeline { // in cosmic-text. let spans: Vec<(&str, Attrs)> = sections .iter() - .enumerate() .filter(|(_section_index, section)| section.style.font_size > 0.0) .map(|(section_index, section)| { ( §ion.value[..], get_attrs( section, - section_index, + *section_index, font_system, &self.map_handle_to_font_id, scale_factor, @@ -146,7 +145,7 @@ impl TextPipeline { pub fn queue_text( &mut self, fonts: &Assets, - sections: &[TextSection], + sections: &[(usize, Ref)], scale_factor: f64, text_alignment: JustifyText, linebreak_behavior: BreakLineOn, @@ -185,7 +184,7 @@ impl TextPipeline { .map(|(layout_glyph, line_y)| { let section_index = layout_glyph.metadata; - let font_handle = sections[section_index].style.font.clone_weak(); + let font_handle = sections[section_index].1.style.font.clone_weak(); let font_atlas_set = font_atlas_sets.sets.entry(font_handle.id()).or_default(); let physical_glyph = layout_glyph.physical((0., 0.), 1.); @@ -238,10 +237,11 @@ impl TextPipeline { /// /// Produces a [`TextMeasureInfo`] which can be used by a layout system /// to measure the text area on demand. + #[allow(clippy::too_many_arguments)] pub fn create_text_measure( &mut self, fonts: &Assets, - sections: &[TextSection], + sections: &[(usize, Ref)], scale_factor: f64, linebreak_behavior: BreakLineOn, buffer: &mut CosmicBuffer, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 9172b6896e96b..157032040ed89 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -29,8 +29,6 @@ impl Default for CosmicBuffer { #[derive(Component, Debug, Clone, Default, Reflect)] #[reflect(Component, Default)] pub struct Text { - /// The text's sections - pub sections: Vec, /// The text's internal alignment. /// Should not affect its position within a container. pub justify: JustifyText, @@ -39,79 +37,6 @@ pub struct Text { } impl Text { - /// Constructs a [`Text`] with a single section. - /// - /// ``` - /// # use bevy_asset::Handle; - /// # use bevy_color::Color; - /// # use bevy_text::{Font, Text, TextStyle, JustifyText}; - /// # - /// # let font_handle: Handle = Default::default(); - /// # - /// // Basic usage. - /// let hello_world = Text::from_section( - /// // Accepts a String or any type that converts into a String, such as &str. - /// "hello world!", - /// TextStyle { - /// font: font_handle.clone().into(), - /// font_size: 60.0, - /// color: Color::WHITE, - /// }, - /// ); - /// - /// let hello_bevy = Text::from_section( - /// "hello world\nand bevy!", - /// TextStyle { - /// font: font_handle.into(), - /// font_size: 60.0, - /// color: Color::WHITE, - /// }, - /// ) // You can still add text justifaction. - /// .with_justify(JustifyText::Center); - /// ``` - pub fn from_section(value: impl Into, style: TextStyle) -> Self { - Self { - sections: vec![TextSection::new(value, style)], - ..default() - } - } - - /// Constructs a [`Text`] from a list of sections. - /// - /// ``` - /// # use bevy_asset::Handle; - /// # use bevy_color::Color; - /// # use bevy_color::palettes::basic::{RED, BLUE}; - /// # use bevy_text::{Font, Text, TextStyle, TextSection}; - /// # - /// # let font_handle: Handle = Default::default(); - /// # - /// let hello_world = Text::from_sections([ - /// TextSection::new( - /// "Hello, ", - /// TextStyle { - /// font: font_handle.clone().into(), - /// font_size: 60.0, - /// color: BLUE.into(), - /// }, - /// ), - /// TextSection::new( - /// "World!", - /// TextStyle { - /// font: font_handle.into(), - /// font_size: 60.0, - /// color: RED.into(), - /// }, - /// ), - /// ]); - /// ``` - pub fn from_sections(sections: impl IntoIterator) -> Self { - Self { - sections: sections.into_iter().collect(), - ..default() - } - } - /// Returns this [`Text`] with a new [`JustifyText`]. pub const fn with_justify(mut self, justify: JustifyText) -> Self { self.justify = justify; @@ -127,7 +52,7 @@ impl Text { } /// Contains the value of the text in a section and how it should be styled. -#[derive(Debug, Default, Clone, Reflect)] +#[derive(Component, Debug, Default, Clone, Reflect)] #[reflect(Default)] pub struct TextSection { /// The content (in `String` form) of the text in the section. diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 8c28fe3079aca..ac4a79e2ff008 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,6 +1,6 @@ use crate::{ BreakLineOn, CosmicBuffer, Font, FontAtlasSets, PositionedGlyph, Text, TextBounds, TextError, - TextLayoutInfo, TextPipeline, YAxisOrientation, + TextLayoutInfo, TextPipeline, TextSection, YAxisOrientation, }; use bevy_asset::Assets; use bevy_color::LinearRgba; @@ -13,6 +13,7 @@ use bevy_ecs::{ query::{Changed, Without}, system::{Commands, Local, Query, Res, ResMut}, }; +use bevy_hierarchy::{Children, Parent}; use bevy_math::Vec2; use bevy_render::{ primitives::Aabb, @@ -70,15 +71,19 @@ pub fn extract_text2d_sprite( texture_atlases: Extract>>, windows: Extract>>, text2d_query: Extract< - Query<( - Entity, - &ViewVisibility, - &Text, - &TextLayoutInfo, - &Anchor, - &GlobalTransform, - )>, + Query< + ( + Entity, + &ViewVisibility, + &TextLayoutInfo, + &Anchor, + &GlobalTransform, + Option<&Children>, + ), + With, + >, >, + text2d_section_query: Extract>>, ) { // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows @@ -87,7 +92,7 @@ pub fn extract_text2d_sprite( .unwrap_or(1.0); let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.)); - for (original_entity, view_visibility, text, text_layout_info, anchor, global_transform) in + for (original_entity, view_visibility, text_layout_info, anchor, global_transform, children) in text2d_query.iter() { if !view_visibility.get() { @@ -108,8 +113,12 @@ pub fn extract_text2d_sprite( .. } in &text_layout_info.glyphs { + // unwrapping the children is fine here, because if the section index != 0, there must be children + let section = text2d_section_query + .get(children.unwrap()[*section_index]) + .unwrap(); if *section_index != current_section { - color = LinearRgba::from(text.sections[*section_index].style.color); + color = LinearRgba::from(section.style.color); current_section = *section_index; } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); @@ -155,9 +164,11 @@ pub fn update_text2d_layout( Entity, Ref, Ref, + Option>, &mut TextLayoutInfo, &mut CosmicBuffer, )>, + text_section_query: Query>, With>, ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.read().last().is_some(); @@ -170,8 +181,31 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, bounds, mut text_layout_info, mut buffer) in &mut text_query { - if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) { + let mut sections = Vec::new(); + + for (entity, text, bounds, children, mut text_layout_info, mut buffer) in &mut text_query { + sections.clear(); + + let mut children_changed = false; + if let Some(children) = children { + children_changed = children.is_changed(); + sections.extend( + text_section_query + .iter_many(children.iter()) + .enumerate() + .filter_map(|(index, maybe_section)| { + maybe_section.map(|section| (index, section)) + }), + ); + } + + if factor_changed + || text.is_changed() + || bounds.is_changed() + || children_changed + || sections.iter().any(|(_, section)| section.is_changed()) + || queue.remove(&entity) + { let text_bounds = TextBounds { width: if text.linebreak_behavior == BreakLineOn::NoWrap { None @@ -185,7 +219,7 @@ pub fn update_text2d_layout( match text_pipeline.queue_text( &fonts, - &text.sections, + §ions, scale_factor.into(), text.justify, text.linebreak_behavior, @@ -256,6 +290,7 @@ mod tests { use bevy_app::{App, Update}; use bevy_asset::{load_internal_binary_asset, Handle}; use bevy_ecs::{event::Events, schedule::IntoSystemConfigs}; + use bevy_hierarchy::BuildChildren; use bevy_utils::default; use super::*; @@ -263,7 +298,7 @@ mod tests { const FIRST_TEXT: &str = "Sample text."; const SECOND_TEXT: &str = "Another, longer sample text."; - fn setup() -> (App, Entity) { + fn setup() -> (App, Entity, Entity) { let mut app = App::new(); app.init_resource::>() .init_resource::>() @@ -287,20 +322,23 @@ mod tests { |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } ); + let text_entity = app + .world_mut() + .spawn(TextSection::new(FIRST_TEXT, default())) + .id(); + let entity = app .world_mut() - .spawn((Text2dBundle { - text: Text::from_section(FIRST_TEXT, default()), - ..default() - },)) + .spawn(Text2dBundle::default()) + .add_child(text_entity) .id(); - (app, entity) + (app, entity, text_entity) } #[test] fn calculate_bounds_text2d_create_aabb() { - let (mut app, entity) = setup(); + let (mut app, entity, _) = setup(); assert!(!app .world() @@ -328,7 +366,7 @@ mod tests { #[test] fn calculate_bounds_text2d_update_aabb() { - let (mut app, entity) = setup(); + let (mut app, entity, text_entity) = setup(); // Creates the initial AABB after text layouting. app.update(); @@ -342,11 +380,11 @@ mod tests { let mut entity_ref = app .world_mut() - .get_entity_mut(entity) + .get_entity_mut(text_entity) .expect("Could not find entity"); *entity_ref - .get_mut::() - .expect("Missing Text on entity") = Text::from_section(SECOND_TEXT, default()); + .get_mut::() + .expect("Missing Text on entity") = TextSection::new(SECOND_TEXT, default()); // Recomputes the AABB. app.update(); diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index cbcf8e82d36ca..3db8bb121a158 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -9,31 +9,55 @@ use bevy_a11y::{ use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ prelude::{DetectChanges, Entity}, - query::{Changed, Without}, + query::{Changed, With, Without}, schedule::IntoSystemConfigs, system::{Commands, Query}, world::Ref, }; -use bevy_hierarchy::Children; +use bevy_hierarchy::{Children, Parent}; use bevy_render::{camera::CameraUpdateSystem, prelude::Camera}; -use bevy_text::Text; +use bevy_text::{Text, TextSection}; use bevy_transform::prelude::GlobalTransform; -fn calc_name(texts: &Query<&Text>, children: &Children) -> Option> { +fn calc_name( + texts: &Query, With>, + text_sections: &Query<&TextSection, With>, + children: &Children, +) -> Option> { let mut name = None; + let mut sections = Vec::new(); for child in children { - if let Ok(text) = texts.get(*child) { - let values = text - .sections - .iter() - .map(|v| v.value.to_string()) - .collect::>(); - name = Some(values.join(" ")); + if let Ok(maybe_children) = texts.get(*child) { + sections.clear(); + + if let Ok(section) = text_sections.get(*child) { + sections.push(section); + } + + if let Some(children) = maybe_children { + for section in text_sections.iter_many(children) { + sections.push(section); + } + } + + let iterator = sections.iter().map(|v| v.value.as_ref()); + name = Some(join_strs(iterator, " ")); } } name.map(String::into_boxed_str) } +fn join_strs<'a>(values: impl Iterator, sep: &str) -> String { + let mut s = String::new(); + for (i, value) in values.enumerate() { + if i > 0 { + s.push_str(sep); + } + s.push_str(value); + } + s +} + fn calc_bounds( camera: Query<(&Camera, &GlobalTransform)>, mut nodes: Query<(&mut AccessibilityNode, Ref, Ref)>, @@ -60,10 +84,11 @@ fn calc_bounds( fn button_changed( mut commands: Commands, mut query: Query<(Entity, &Children, Option<&mut AccessibilityNode>), Changed