diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index b6e9c3a699fb3..e71e49c030e67 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -7,7 +7,7 @@ use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::HashMap; -use glyph_brush_layout::{FontId, SectionText}; +use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText}; use crate::{ error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, @@ -54,7 +54,7 @@ impl TextPipeline { font_atlas_warning: &mut FontAtlasWarning, y_axis_orientation: YAxisOrientation, ) -> Result { - let mut scaled_fonts = Vec::new(); + let mut scaled_fonts = Vec::with_capacity(sections.len()); let sections = sections .iter() .map(|section| { @@ -92,6 +92,9 @@ impl TextPipeline { for sg in §ion_glyphs { let scaled_font = scaled_fonts[sg.section_index]; let glyph = &sg.glyph; + // The fonts use a coordinate system increasing upwards so ascent is a positive value + // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, + // so we have to subtract from the baseline position to get the minimum and maximum values. min_x = min_x.min(glyph.position.x); min_y = min_y.min(glyph.position.y - scaled_font.ascent()); max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); @@ -114,4 +117,140 @@ impl TextPipeline { Ok(TextLayoutInfo { glyphs, size }) } + + pub fn create_text_measure( + &mut self, + fonts: &Assets, + sections: &[TextSection], + scale_factor: f64, + text_alignment: TextAlignment, + linebreak_behaviour: BreakLineOn, + ) -> Result { + let mut auto_fonts = Vec::with_capacity(sections.len()); + let mut scaled_fonts = Vec::with_capacity(sections.len()); + let sections = sections + .iter() + .enumerate() + .map(|(i, section)| { + let font = fonts + .get(§ion.style.font) + .ok_or(TextError::NoSuchFont)?; + let font_size = scale_value(section.style.font_size, scale_factor); + auto_fonts.push(font.font.clone()); + let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size); + scaled_fonts.push(px_scale_font); + + let section = TextMeasureSection { + font_id: FontId(i), + scale: PxScale::from(font_size), + text: section.value.clone(), + }; + + Ok(section) + }) + .collect::, _>>()?; + + Ok(TextMeasureInfo::new( + auto_fonts, + scaled_fonts, + sections, + text_alignment, + linebreak_behaviour.into(), + )) + } +} + +#[derive(Debug, Clone)] +pub struct TextMeasureSection { + pub text: String, + pub scale: PxScale, + pub font_id: FontId, +} + +#[derive(Debug, Clone)] +pub struct TextMeasureInfo { + pub fonts: Vec, + pub scaled_fonts: Vec>, + pub sections: Vec, + pub text_alignment: TextAlignment, + pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, + pub min_width_content_size: Vec2, + pub max_width_content_size: Vec2, +} + +impl TextMeasureInfo { + fn new( + fonts: Vec, + scaled_fonts: Vec>, + sections: Vec, + text_alignment: TextAlignment, + linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, + ) -> Self { + let mut info = Self { + fonts, + scaled_fonts, + sections, + text_alignment, + linebreak_behaviour, + min_width_content_size: Vec2::ZERO, + max_width_content_size: Vec2::ZERO, + }; + + let section_texts = info.prepare_section_texts(); + let min = + info.compute_size_from_section_texts(§ion_texts, Vec2::new(0.0, f32::INFINITY)); + let max = info.compute_size_from_section_texts( + §ion_texts, + Vec2::new(f32::INFINITY, f32::INFINITY), + ); + info.min_width_content_size = min; + info.max_width_content_size = max; + info + } + + fn prepare_section_texts(&self) -> Vec { + self.sections + .iter() + .map(|section| SectionText { + font_id: section.font_id, + scale: section.scale, + text: §ion.text, + }) + .collect::>() + } + + fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 { + let geom = SectionGeometry { + bounds: (bounds.x, bounds.y), + ..Default::default() + }; + let section_glyphs = glyph_brush_layout::Layout::default() + .h_align(self.text_alignment.into()) + .line_breaker(self.linebreak_behaviour) + .calculate_glyphs(&self.fonts, &geom, sections); + + let mut min_x: f32 = std::f32::MAX; + let mut min_y: f32 = std::f32::MAX; + let mut max_x: f32 = std::f32::MIN; + let mut max_y: f32 = std::f32::MIN; + + for sg in section_glyphs { + let scaled_font = &self.scaled_fonts[sg.section_index]; + let glyph = &sg.glyph; + // The fonts use a coordinate system increasing upwards so ascent is a positive value + // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, + // so we have to subtract from the baseline position to get the minimum and maximum values. + min_x = min_x.min(glyph.position.x); + min_y = min_y.min(glyph.position.y - scaled_font.ascent()); + max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); + max_y = max_y.max(glyph.position.y - scaled_font.descent()); + } + + Vec2::new(max_x - min_x, max_y - min_y) + } + + pub fn compute_size(&self, bounds: Vec2) -> Vec2 { + let sections = self.prepare_section_texts(); + self.compute_size_from_section_texts(§ions, bounds) + } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 750d0edb7277b..fdabddab07b8f 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ event::EventReader, prelude::With, reflect::ReflectComponent, - system::{Commands, Local, Query, Res, ResMut}, + system::{Local, Query, Res, ResMut}, }; use bevy_math::{Vec2, Vec3}; use bevy_reflect::Reflect; @@ -72,6 +72,8 @@ pub struct Text2dBundle { pub visibility: Visibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering. pub computed_visibility: ComputedVisibility, + /// Contains the size of the text and its glyph's position and scale data. Generated via [`TextPipeline::queue_text`] + pub text_layout_info: TextLayoutInfo, } pub fn extract_text2d_sprite( @@ -147,7 +149,6 @@ pub fn extract_text2d_sprite( /// It does not modify or observe existing ones. #[allow(clippy::too_many_arguments)] pub fn update_text2d_layout( - mut commands: Commands, // Text items which should be reprocessed again, generally when the font hasn't loaded yet. mut queue: Local>, mut textures: ResMut>, @@ -159,12 +160,7 @@ pub fn update_text2d_layout( mut texture_atlases: ResMut>, mut font_atlas_set_storage: ResMut>, mut text_pipeline: ResMut, - mut text_query: Query<( - Entity, - Ref, - Ref, - Option<&mut TextLayoutInfo>, - )>, + mut text_query: Query<(Entity, Ref, Ref, &mut TextLayoutInfo)>, ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.iter().last().is_some(); @@ -175,7 +171,7 @@ pub fn update_text2d_layout( .map(|window| window.resolution.scale_factor()) .unwrap_or(1.0); - for (entity, text, bounds, text_layout_info) in &mut text_query { + for (entity, text, bounds, mut text_layout_info) in &mut text_query { if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) { let text_bounds = Vec2::new( scale_value(bounds.size.x, scale_factor), @@ -204,12 +200,7 @@ pub fn update_text2d_layout( Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } - Ok(info) => match text_layout_info { - Some(mut t) => *t = info, - None => { - commands.entity(entity).insert(info); - } - }, + Ok(info) => *text_layout_info = info, } } } diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index cfe142b088ebf..b927f278feeab 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -31,7 +31,7 @@ bevy_window = { path = "../bevy_window", version = "0.11.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } # other -taffy = { version = "0.3.5", default-features = false, features = ["std"] } +taffy = { version = "0.3.10", default-features = false, features = ["std"] } serde = { version = "1", features = ["derive"] } smallvec = { version = "1.6", features = ["union", "const_generics"] } bytemuck = { version = "1.5", features = ["derive"] } diff --git a/crates/bevy_ui/src/flex/mod.rs b/crates/bevy_ui/src/flex/mod.rs index d59d8c544164c..748b0450e6373 100644 --- a/crates/bevy_ui/src/flex/mod.rs +++ b/crates/bevy_ui/src/flex/mod.rs @@ -97,45 +97,35 @@ impl FlexSurface { &mut self, entity: Entity, style: &Style, - calculated_size: CalculatedSize, + calculated_size: &CalculatedSize, context: &LayoutContext, ) { let taffy = &mut self.taffy; let taffy_style = convert::from_style(context, style); - let scale_factor = context.scale_factor; - let measure = taffy::node::MeasureFunc::Boxed(Box::new( - move |constraints: Size>, _available: Size| { - let mut size = Size { - width: (scale_factor * calculated_size.size.x as f64) as f32, - height: (scale_factor * calculated_size.size.y as f64) as f32, - }; - match (constraints.width, constraints.height) { - (None, None) => {} - (Some(width), None) => { - if calculated_size.preserve_aspect_ratio { - size.height = width * size.height / size.width; - } - size.width = width; - } - (None, Some(height)) => { - if calculated_size.preserve_aspect_ratio { - size.width = height * size.width / size.height; - } - size.height = height; - } - (Some(width), Some(height)) => { - size.width = width; - size.height = height; - } + let measure = calculated_size.measure.dyn_clone(); + let measure_func = taffy::node::MeasureFunc::Boxed(Box::new( + move |constraints: Size>, available: Size| { + let size = measure.measure( + constraints.width, + constraints.height, + available.width, + available.height, + ); + taffy::geometry::Size { + width: size.x, + height: size.y, } - size }, )); if let Some(taffy_node) = self.entity_to_taffy.get(&entity) { self.taffy.set_style(*taffy_node, taffy_style).unwrap(); - self.taffy.set_measure(*taffy_node, Some(measure)).unwrap(); + self.taffy + .set_measure(*taffy_node, Some(measure_func)) + .unwrap(); } else { - let taffy_node = taffy.new_leaf_with_measure(taffy_style, measure).unwrap(); + let taffy_node = taffy + .new_leaf_with_measure(taffy_style, measure_func) + .unwrap(); self.entity_to_taffy.insert(entity, taffy_node); } } @@ -307,7 +297,7 @@ pub fn flex_node_system( for (entity, style, calculated_size) in &query { // TODO: remove node from old hierarchy if its root has changed if let Some(calculated_size) = calculated_size { - flex_surface.upsert_leaf(entity, style, *calculated_size, viewport_values); + flex_surface.upsert_leaf(entity, style, calculated_size, viewport_values); } else { flex_surface.upsert_node(entity, style, viewport_values); } @@ -322,7 +312,7 @@ pub fn flex_node_system( } for (entity, style, calculated_size) in &changed_size_query { - flex_surface.upsert_leaf(entity, style, *calculated_size, &viewport_values); + flex_surface.upsert_leaf(entity, style, calculated_size, &viewport_values); } // clean up removed nodes diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index fe63b84670d96..983130d55549a 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -14,6 +14,7 @@ mod ui_node; #[cfg(feature = "bevy_text")] mod accessibility; pub mod camera_config; +pub mod measurement; pub mod node_bundles; pub mod update; pub mod widget; @@ -24,6 +25,7 @@ use bevy_render::extract_component::ExtractComponentPlugin; pub use flex::*; pub use focus::*; pub use geometry::*; +pub use measurement::*; pub use render::*; pub use ui_node::*; @@ -31,10 +33,12 @@ pub use ui_node::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::*, Interaction, UiScale, + camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::Button, widget::Label, + Interaction, UiScale, }; } +use crate::prelude::UiCameraConfig; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_input::InputSystem; @@ -43,8 +47,6 @@ use stack::ui_stack_system; pub use stack::UiStack; use update::update_clipping_system; -use crate::prelude::UiCameraConfig; - /// The basic plugin for Bevy UI #[derive(Default)] pub struct UiPlugin; @@ -114,7 +116,7 @@ impl Plugin for UiPlugin { #[cfg(feature = "bevy_text")] app.add_systems( PostUpdate, - widget::text_system + widget::measure_text_system .before(UiSystem::Flex) // Potential conflict: `Assets` // In practice, they run independently since `bevy_render::camera_update_system` @@ -149,6 +151,7 @@ impl Plugin for UiPlugin { .before(TransformSystem::TransformPropagate), ui_stack_system.in_set(UiSystem::Stack), update_clipping_system.after(TransformSystem::TransformPropagate), + widget::text_system.after(UiSystem::Flex), ), ); diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs new file mode 100644 index 0000000000000..1fffffeacbe36 --- /dev/null +++ b/crates/bevy_ui/src/measurement.rs @@ -0,0 +1,77 @@ +use bevy_ecs::prelude::Component; +use bevy_math::Vec2; +use bevy_reflect::Reflect; +use std::fmt::Formatter; +pub use taffy::style::AvailableSpace; + +impl std::fmt::Debug for CalculatedSize { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CalculatedSize").finish() + } +} + +/// A `Measure` is used to compute the size of a ui node +/// when the size of that node is based on its content. +pub trait Measure: Send + Sync + 'static { + /// Calculate the size of the node given the constraints. + fn measure( + &self, + width: Option, + height: Option, + available_width: AvailableSpace, + available_height: AvailableSpace, + ) -> Vec2; + + /// Clone and box self. + fn dyn_clone(&self) -> Box; +} + +/// A `FixedMeasure` is a `Measure` that ignores all constraints and +/// always returns the same size. +#[derive(Default, Clone)] +pub struct FixedMeasure { + size: Vec2, +} + +impl Measure for FixedMeasure { + fn measure( + &self, + _: Option, + _: Option, + _: AvailableSpace, + _: AvailableSpace, + ) -> Vec2 { + self.size + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +/// A node with a `CalculatedSize` component is a node where its size +/// is based on its content. +#[derive(Component, Reflect)] +pub struct CalculatedSize { + /// The `Measure` used to compute the intrinsic size + #[reflect(ignore)] + pub measure: Box, +} + +#[allow(clippy::derivable_impls)] +impl Default for CalculatedSize { + fn default() -> Self { + Self { + // Default `FixedMeasure` always returns zero size. + measure: Box::::default(), + } + } +} + +impl Clone for CalculatedSize { + fn clone(&self) -> Self { + Self { + measure: self.measure.dyn_clone(), + } + } +} diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index e0eee6a61062a..1219597269fd3 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -1,8 +1,8 @@ //! This module contains basic node bundles used to build UIs use crate::{ - widget::Button, BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, - UiImage, ZIndex, + widget::{Button, UiImageSize}, + BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex, }; use bevy_ecs::bundle::Bundle; use bevy_render::{ @@ -10,7 +10,7 @@ use bevy_render::{ view::Visibility, }; #[cfg(feature = "bevy_text")] -use bevy_text::{Text, TextAlignment, TextSection, TextStyle}; +use bevy_text::{Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle}; use bevy_transform::prelude::{GlobalTransform, Transform}; /// The basic UI node @@ -76,6 +76,10 @@ pub struct ImageBundle { pub background_color: BackgroundColor, /// The image of the node pub image: UiImage, + /// The size of the image in pixels + /// + /// This field is set automatically + pub image_size: UiImageSize, /// Whether this node should block interaction with lower nodes pub focus_policy: FocusPolicy, /// The transform of the node @@ -106,6 +110,8 @@ pub struct TextBundle { pub style: Style, /// Contains the text of the node pub text: Text, + /// Text layout information + pub text_layout_info: TextLayoutInfo, /// The calculated size based on the given image pub calculated_size: CalculatedSize, /// Whether this node should block interaction with lower nodes @@ -135,6 +141,7 @@ impl Default for TextBundle { fn default() -> Self { Self { text: Default::default(), + text_layout_info: Default::default(), calculated_size: Default::default(), // Transparent background background_color: BackgroundColor(Color::NONE), diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index d51e2c8a75f34..a4e2668f00312 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -682,29 +682,6 @@ impl Default for FlexWrap { } } -/// The calculated size of the node -#[derive(Component, Copy, Clone, Debug, Reflect)] -#[reflect(Component)] -pub struct CalculatedSize { - /// The size of the node in logical pixels - pub size: Vec2, - /// Whether to attempt to preserve the aspect ratio when determining the layout for this item - pub preserve_aspect_ratio: bool, -} - -impl CalculatedSize { - const DEFAULT: Self = Self { - size: Vec2::ZERO, - preserve_aspect_ratio: false, - }; -} - -impl Default for CalculatedSize { - fn default() -> Self { - Self::DEFAULT - } -} - /// The background color of the node /// /// This serves as the "fill" color. diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 4b5777c841dd4..09ddeceebc6c5 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -1,29 +1,91 @@ -use crate::{CalculatedSize, UiImage}; +use crate::{measurement::AvailableSpace, CalculatedSize, Measure, Node, UiImage}; use bevy_asset::Assets; #[cfg(feature = "bevy_text")] use bevy_ecs::query::Without; -use bevy_ecs::system::{Query, Res}; +use bevy_ecs::{ + prelude::Component, + query::With, + system::{Query, Res}, +}; use bevy_math::Vec2; use bevy_render::texture::Image; #[cfg(feature = "bevy_text")] use bevy_text::Text; +/// The size of the image in pixels +/// +/// This field is set automatically +#[derive(Component, Copy, Clone, Debug, Default)] +pub struct UiImageSize { + size: Vec2, +} + +impl UiImageSize { + pub fn size(&self) -> Vec2 { + self.size + } +} + +#[derive(Clone)] +pub struct ImageMeasure { + // target size of the image + size: Vec2, +} + +impl Measure for ImageMeasure { + fn measure( + &self, + width: Option, + height: Option, + _: AvailableSpace, + _: AvailableSpace, + ) -> Vec2 { + let mut size = self.size; + match (width, height) { + (None, None) => {} + (Some(width), None) => { + size.y = width * size.y / size.x; + size.x = width; + } + (None, Some(height)) => { + size.x = height * size.x / size.y; + size.y = height; + } + (Some(width), Some(height)) => { + size.x = width; + size.y = height; + } + } + size + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } +} + /// Updates calculated size of the node based on the image provided pub fn update_image_calculated_size_system( textures: Res>, - #[cfg(feature = "bevy_text")] mut query: Query<(&mut CalculatedSize, &UiImage), Without>, - #[cfg(not(feature = "bevy_text"))] mut query: Query<(&mut CalculatedSize, &UiImage)>, + #[cfg(feature = "bevy_text")] mut query: Query< + (&mut CalculatedSize, &UiImage, &mut UiImageSize), + (With, Without), + >, + #[cfg(not(feature = "bevy_text"))] mut query: Query< + (&mut CalculatedSize, &UiImage, &mut UiImageSize), + With, + >, ) { - for (mut calculated_size, image) in &mut query { + for (mut calculated_size, image, mut image_size) in &mut query { if let Some(texture) = textures.get(&image.texture) { let size = Vec2::new( texture.texture_descriptor.size.width as f32, texture.texture_descriptor.size.height as f32, ); // Update only if size has changed to avoid needless layout calculations - if size != calculated_size.size { - calculated_size.size = size; - calculated_size.preserve_aspect_ratio = true; + if size != image_size.size { + image_size.size = size; + calculated_size.measure = Box::new(ImageMeasure { size }); } } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 97a7c7b82137f..0181a8a579720 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -1,33 +1,135 @@ -use crate::{CalculatedSize, Node, Style, UiScale, Val}; +use crate::{CalculatedSize, Measure, Node, UiScale}; use bevy_asset::Assets; use bevy_ecs::{ entity::Entity, query::{Changed, Or, With}, - system::{Commands, Local, ParamSet, Query, Res, ResMut}, + system::{Local, ParamSet, Query, Res, ResMut}, }; use bevy_math::Vec2; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_text::{ - Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, - TextSettings, YAxisOrientation, + Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextMeasureInfo, + TextPipeline, TextSettings, YAxisOrientation, }; use bevy_window::{PrimaryWindow, Window}; +use taffy::style::AvailableSpace; fn scale_value(value: f32, factor: f64) -> f32 { (value as f64 * factor) as f32 } -/// Defines how `min_size`, `size`, and `max_size` affects the bounds of a text -/// block. -pub fn text_constraint(min_size: Val, size: Val, max_size: Val, scale_factor: f64) -> f32 { - // Needs support for percentages - match (min_size, size, max_size) { - (_, _, Val::Px(max)) => scale_value(max, scale_factor), - (Val::Px(min), _, _) => scale_value(min, scale_factor), - (Val::Auto, Val::Px(size), Val::Auto) => scale_value(size, scale_factor), - _ => f32::MAX, +#[derive(Clone)] +pub struct TextMeasure { + pub info: TextMeasureInfo, +} + +impl Measure for TextMeasure { + fn measure( + &self, + width: Option, + height: Option, + available_width: AvailableSpace, + available_height: AvailableSpace, + ) -> Vec2 { + let x = width.unwrap_or_else(|| match available_width { + AvailableSpace::Definite(x) => x.clamp( + self.info.min_width_content_size.x, + self.info.max_width_content_size.x, + ), + AvailableSpace::MinContent => self.info.min_width_content_size.x, + AvailableSpace::MaxContent => self.info.max_width_content_size.x, + }); + + height + .map_or_else( + || match available_height { + AvailableSpace::Definite(y) => { + let y = y.clamp( + self.info.max_width_content_size.y, + self.info.min_width_content_size.y, + ); + self.info.compute_size(Vec2::new(x, y)) + } + AvailableSpace::MinContent => Vec2::new(x, self.info.max_width_content_size.y), + AvailableSpace::MaxContent => Vec2::new(x, self.info.min_width_content_size.y), + }, + |y| Vec2::new(x, y), + ) + .ceil() + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Creates a `Measure` for text nodes that allows the UI to determine the appropriate amount of space +/// to provide for the text given the fonts, the text itself and the constraints of the layout. +pub fn measure_text_system( + mut queued_text: Local>, + mut last_scale_factor: Local, + fonts: Res>, + windows: Query<&Window, With>, + ui_scale: Res, + mut text_pipeline: ResMut, + mut text_queries: ParamSet<( + Query>, + Query, With)>, + Query<(&Text, &mut CalculatedSize)>, + )>, +) { + let window_scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor()) + .unwrap_or(1.); + + let scale_factor = ui_scale.scale * window_scale_factor; + + #[allow(clippy::float_cmp)] + if *last_scale_factor == scale_factor { + // Adds all entities where the text or the style has changed to the local queue + for entity in text_queries.p0().iter() { + if !queued_text.contains(&entity) { + queued_text.push(entity); + } + } + } else { + // If the scale factor has changed, queue all text + for entity in text_queries.p1().iter() { + queued_text.push(entity); + } + *last_scale_factor = scale_factor; + } + + if queued_text.is_empty() { + return; } + + let mut new_queue = Vec::new(); + let mut query = text_queries.p2(); + for entity in queued_text.drain(..) { + if let Ok((text, mut calculated_size)) = query.get_mut(entity) { + match text_pipeline.create_text_measure( + &fonts, + &text.sections, + scale_factor, + text.alignment, + text.linebreak_behavior, + ) { + Ok(measure) => { + calculated_size.measure = Box::new(TextMeasure { info: measure }); + } + Err(TextError::NoSuchFont) => { + new_queue.push(entity); + } + Err(e @ TextError::FailedToAddGlyph(_)) => { + panic!("Fatal error when processing text: {e}."); + } + }; + } + } + *queued_text = new_queue; } /// Updates the layout and size information whenever the text or style is changed. @@ -39,10 +141,9 @@ pub fn text_constraint(min_size: Val, size: Val, max_size: Val, scale_factor: f6 /// It does not modify or observe existing ones. #[allow(clippy::too_many_arguments)] pub fn text_system( - mut commands: Commands, - mut queued_text_ids: Local>, - mut last_scale_factor: Local, + mut queued_text: Local>, mut textures: ResMut>, + mut last_scale_factor: Local, fonts: Res>, windows: Query<&Window, With>, text_settings: Res, @@ -52,14 +153,9 @@ pub fn text_system( mut font_atlas_set_storage: ResMut>, mut text_pipeline: ResMut, mut text_queries: ParamSet<( - Query, Changed, Changed