From ffc62c1a81bc2d4a50e61c3f5919bc3424305234 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 17 Apr 2023 16:23:21 +0100 Subject: [PATCH] `text_system` split (#7779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective `text_system` runs before the UI layout is calculated and the size of the text node is determined, so it cannot correctly shape the text to fit the layout, and has no way of determining if the text needs to be wrapped. The function `text_constraint` attempts to determine the size of the node from the local size constraints in the `Style` component. It can't be made to work, you have to compute the whole layout to get the correct size. A simple example of where this fails completely is a text node set to stretch to fill the empty space adjacent to a node with size constraints set to `Val::Percent(50.)`. The text node will take up half the space, even though its size constraints are `Val::Auto` Also because the `text_system` queries for changes to the `Style` component, when a style value is changed that doesn't affect the node's geometry the text is recomputed unnecessarily. Querying on changes to `Node` is not much better. The UI layout is changed to fit the `CalculatedSize` of the text, so the size of the node is changed and so the text and UI layout get recalculated multiple times from a single change to a `Text`. Also, the `MeasureFunc` doesn't work at all, it doesn't have enough information to fit the text correctly and makes no attempt. Fixes #7663, #6717, #5834, #1490, ## Solution Split the `text_system` into two functions: * `measure_text_system` which calculates the size constraints for the text node and runs before `UiSystem::Flex` * `text_system` which runs after `UiSystem::Flex` and generates the actual text. * Fix the `MeasureFunc` calculations. --- Text wrapping in main: Capturemain With this PR: captured_wrap ## Changelog * Removed the previous fields from `CalculatedSize`. `CalculatedSize` now contains a boxed `Measure`. * Added `measurement` module to `bevy_ui`. * Added the method `create_text_measure` to `TextPipeline`. * Added a new system `measure_text_system` that runs before `UiSystem::Flex` that creates a `MeasureFunc` for the text. * Rescheduled `text_system` to run after `UiSystem::Flex`. * Added a trait `Measure`. A `Measure` is used to compute the size of a UI node when the size of that node is based on its content. * Added `ImageMeasure` and `TextMeasure` which implement `Measure`. * Added a new component `UiImageSize` which is used by `update_image_calculated_size_system` to track image size changes. * Added a `UiImageSize` component to `ImageBundle`. ## Migration Guide `ImageBundle` has a new component `UiImageSize` which contains the size of the image bundle's texture and is updated automatically by `update_image_calculated_size_system` --------- Co-authored-by: François --- crates/bevy_text/src/pipeline.rs | 143 ++++++++++++++++++++- crates/bevy_text/src/text2d.rs | 21 +--- crates/bevy_ui/Cargo.toml | 2 +- crates/bevy_ui/src/flex/mod.rs | 52 ++++---- crates/bevy_ui/src/lib.rs | 11 +- crates/bevy_ui/src/measurement.rs | 77 ++++++++++++ crates/bevy_ui/src/node_bundles.rs | 13 +- crates/bevy_ui/src/ui_node.rs | 23 ---- crates/bevy_ui/src/widget/image.rs | 78 ++++++++++-- crates/bevy_ui/src/widget/text.rs | 191 ++++++++++++++++++++--------- 10 files changed, 464 insertions(+), 147 deletions(-) create mode 100644 crates/bevy_ui/src/measurement.rs diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index b6e9c3a699..e71e49c030 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 750d0edb72..fdabddab07 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 cfe142b088..b927f278fe 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 d59d8c5441..748b0450e6 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 fe63b84670..983130d555 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 0000000000..1fffffeacb --- /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 e0eee6a610..1219597269 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 d51e2c8a75..a4e2668f00 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 4b5777c841..09ddeceebc 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 97a7c7b821..0181a8a579 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