text_system split (#7779)

# 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:
<img width="961" alt="Capturemain"
src="https://user-images.githubusercontent.com/27962798/220425740-4fe4bf46-24fb-4685-a1cf-bc01e139e72d.PNG">

With this PR:
<img width="961" alt="captured_wrap"
src="https://user-images.githubusercontent.com/27962798/220425807-949996b0-f127-4637-9f33-56a6da944fb0.PNG">

## 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 <mockersf@gmail.com>
This commit is contained in:
ickshonpe 2023-04-17 16:23:21 +01:00 committed by GitHub
parent 328347f44c
commit ffc62c1a81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 464 additions and 147 deletions

View file

@ -7,7 +7,7 @@ use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas; use bevy_sprite::TextureAtlas;
use bevy_utils::HashMap; use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, SectionText}; use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText};
use crate::{ use crate::{
error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet,
@ -54,7 +54,7 @@ impl TextPipeline {
font_atlas_warning: &mut FontAtlasWarning, font_atlas_warning: &mut FontAtlasWarning,
y_axis_orientation: YAxisOrientation, y_axis_orientation: YAxisOrientation,
) -> Result<TextLayoutInfo, TextError> { ) -> Result<TextLayoutInfo, TextError> {
let mut scaled_fonts = Vec::new(); let mut scaled_fonts = Vec::with_capacity(sections.len());
let sections = sections let sections = sections
.iter() .iter()
.map(|section| { .map(|section| {
@ -92,6 +92,9 @@ impl TextPipeline {
for sg in &section_glyphs { for sg in &section_glyphs {
let scaled_font = scaled_fonts[sg.section_index]; let scaled_font = scaled_fonts[sg.section_index];
let glyph = &sg.glyph; 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_x = min_x.min(glyph.position.x);
min_y = min_y.min(glyph.position.y - scaled_font.ascent()); 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_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
@ -114,4 +117,140 @@ impl TextPipeline {
Ok(TextLayoutInfo { glyphs, size }) Ok(TextLayoutInfo { glyphs, size })
} }
pub fn create_text_measure(
&mut self,
fonts: &Assets<Font>,
sections: &[TextSection],
scale_factor: f64,
text_alignment: TextAlignment,
linebreak_behaviour: BreakLineOn,
) -> Result<TextMeasureInfo, TextError> {
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(&section.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::<Result<Vec<_>, _>>()?;
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<ab_glyph::FontArc>,
pub scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
pub sections: Vec<TextMeasureSection>,
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<ab_glyph::FontArc>,
scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
sections: Vec<TextMeasureSection>,
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(&section_texts, Vec2::new(0.0, f32::INFINITY));
let max = info.compute_size_from_section_texts(
&section_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<SectionText> {
self.sections
.iter()
.map(|section| SectionText {
font_id: section.font_id,
scale: section.scale,
text: &section.text,
})
.collect::<Vec<_>>()
}
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(&sections, bounds)
}
} }

View file

@ -7,7 +7,7 @@ use bevy_ecs::{
event::EventReader, event::EventReader,
prelude::With, prelude::With,
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Commands, Local, Query, Res, ResMut}, system::{Local, Query, Res, ResMut},
}; };
use bevy_math::{Vec2, Vec3}; use bevy_math::{Vec2, Vec3};
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
@ -72,6 +72,8 @@ pub struct Text2dBundle {
pub visibility: Visibility, pub visibility: Visibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering. /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
pub computed_visibility: ComputedVisibility, 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( pub fn extract_text2d_sprite(
@ -147,7 +149,6 @@ pub fn extract_text2d_sprite(
/// It does not modify or observe existing ones. /// It does not modify or observe existing ones.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn update_text2d_layout( pub fn update_text2d_layout(
mut commands: Commands,
// Text items which should be reprocessed again, generally when the font hasn't loaded yet. // Text items which should be reprocessed again, generally when the font hasn't loaded yet.
mut queue: Local<HashSet<Entity>>, mut queue: Local<HashSet<Entity>>,
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
@ -159,12 +160,7 @@ pub fn update_text2d_layout(
mut texture_atlases: ResMut<Assets<TextureAtlas>>, mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>, mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<TextPipeline>, mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<( mut text_query: Query<(Entity, Ref<Text>, Ref<Text2dBounds>, &mut TextLayoutInfo)>,
Entity,
Ref<Text>,
Ref<Text2dBounds>,
Option<&mut TextLayoutInfo>,
)>,
) { ) {
// We need to consume the entire iterator, hence `last` // We need to consume the entire iterator, hence `last`
let factor_changed = scale_factor_changed.iter().last().is_some(); 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()) .map(|window| window.resolution.scale_factor())
.unwrap_or(1.0); .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) { if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) {
let text_bounds = Vec2::new( let text_bounds = Vec2::new(
scale_value(bounds.size.x, scale_factor), scale_value(bounds.size.x, scale_factor),
@ -204,12 +200,7 @@ pub fn update_text2d_layout(
Err(e @ TextError::FailedToAddGlyph(_)) => { Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {e}."); panic!("Fatal error when processing text: {e}.");
} }
Ok(info) => match text_layout_info { Ok(info) => *text_layout_info = info,
Some(mut t) => *t = info,
None => {
commands.entity(entity).insert(info);
}
},
} }
} }
} }

View file

@ -31,7 +31,7 @@ bevy_window = { path = "../bevy_window", version = "0.11.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
# other # 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"] } serde = { version = "1", features = ["derive"] }
smallvec = { version = "1.6", features = ["union", "const_generics"] } smallvec = { version = "1.6", features = ["union", "const_generics"] }
bytemuck = { version = "1.5", features = ["derive"] } bytemuck = { version = "1.5", features = ["derive"] }

View file

@ -97,45 +97,35 @@ impl FlexSurface {
&mut self, &mut self,
entity: Entity, entity: Entity,
style: &Style, style: &Style,
calculated_size: CalculatedSize, calculated_size: &CalculatedSize,
context: &LayoutContext, context: &LayoutContext,
) { ) {
let taffy = &mut self.taffy; let taffy = &mut self.taffy;
let taffy_style = convert::from_style(context, style); let taffy_style = convert::from_style(context, style);
let scale_factor = context.scale_factor; let measure = calculated_size.measure.dyn_clone();
let measure = taffy::node::MeasureFunc::Boxed(Box::new( let measure_func = taffy::node::MeasureFunc::Boxed(Box::new(
move |constraints: Size<Option<f32>>, _available: Size<AvailableSpace>| { move |constraints: Size<Option<f32>>, available: Size<AvailableSpace>| {
let mut size = Size { let size = measure.measure(
width: (scale_factor * calculated_size.size.x as f64) as f32, constraints.width,
height: (scale_factor * calculated_size.size.y as f64) as f32, constraints.height,
}; available.width,
match (constraints.width, constraints.height) { available.height,
(None, None) => {} );
(Some(width), None) => { taffy::geometry::Size {
if calculated_size.preserve_aspect_ratio { width: size.x,
size.height = width * size.height / size.width; height: size.y,
}
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;
}
} }
size
}, },
)); ));
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) { if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy.set_style(*taffy_node, taffy_style).unwrap(); 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 { } 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); self.entity_to_taffy.insert(entity, taffy_node);
} }
} }
@ -307,7 +297,7 @@ pub fn flex_node_system(
for (entity, style, calculated_size) in &query { for (entity, style, calculated_size) in &query {
// TODO: remove node from old hierarchy if its root has changed // TODO: remove node from old hierarchy if its root has changed
if let Some(calculated_size) = calculated_size { 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 { } else {
flex_surface.upsert_node(entity, style, viewport_values); 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 { 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 // clean up removed nodes

View file

@ -14,6 +14,7 @@ mod ui_node;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
mod accessibility; mod accessibility;
pub mod camera_config; pub mod camera_config;
pub mod measurement;
pub mod node_bundles; pub mod node_bundles;
pub mod update; pub mod update;
pub mod widget; pub mod widget;
@ -24,6 +25,7 @@ use bevy_render::extract_component::ExtractComponentPlugin;
pub use flex::*; pub use flex::*;
pub use focus::*; pub use focus::*;
pub use geometry::*; pub use geometry::*;
pub use measurement::*;
pub use render::*; pub use render::*;
pub use ui_node::*; pub use ui_node::*;
@ -31,10 +33,12 @@ pub use ui_node::*;
pub mod prelude { pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
pub use crate::{ 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_app::prelude::*;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_input::InputSystem; use bevy_input::InputSystem;
@ -43,8 +47,6 @@ use stack::ui_stack_system;
pub use stack::UiStack; pub use stack::UiStack;
use update::update_clipping_system; use update::update_clipping_system;
use crate::prelude::UiCameraConfig;
/// The basic plugin for Bevy UI /// The basic plugin for Bevy UI
#[derive(Default)] #[derive(Default)]
pub struct UiPlugin; pub struct UiPlugin;
@ -114,7 +116,7 @@ impl Plugin for UiPlugin {
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
app.add_systems( app.add_systems(
PostUpdate, PostUpdate,
widget::text_system widget::measure_text_system
.before(UiSystem::Flex) .before(UiSystem::Flex)
// Potential conflict: `Assets<Image>` // Potential conflict: `Assets<Image>`
// In practice, they run independently since `bevy_render::camera_update_system` // In practice, they run independently since `bevy_render::camera_update_system`
@ -149,6 +151,7 @@ impl Plugin for UiPlugin {
.before(TransformSystem::TransformPropagate), .before(TransformSystem::TransformPropagate),
ui_stack_system.in_set(UiSystem::Stack), ui_stack_system.in_set(UiSystem::Stack),
update_clipping_system.after(TransformSystem::TransformPropagate), update_clipping_system.after(TransformSystem::TransformPropagate),
widget::text_system.after(UiSystem::Flex),
), ),
); );

View file

@ -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<f32>,
height: Option<f32>,
available_width: AvailableSpace,
available_height: AvailableSpace,
) -> Vec2;
/// Clone and box self.
fn dyn_clone(&self) -> Box<dyn Measure>;
}
/// 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<f32>,
_: Option<f32>,
_: AvailableSpace,
_: AvailableSpace,
) -> Vec2 {
self.size
}
fn dyn_clone(&self) -> Box<dyn Measure> {
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<dyn Measure>,
}
#[allow(clippy::derivable_impls)]
impl Default for CalculatedSize {
fn default() -> Self {
Self {
// Default `FixedMeasure` always returns zero size.
measure: Box::<FixedMeasure>::default(),
}
}
}
impl Clone for CalculatedSize {
fn clone(&self) -> Self {
Self {
measure: self.measure.dyn_clone(),
}
}
}

View file

@ -1,8 +1,8 @@
//! This module contains basic node bundles used to build UIs //! This module contains basic node bundles used to build UIs
use crate::{ use crate::{
widget::Button, BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, widget::{Button, UiImageSize},
UiImage, ZIndex, BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex,
}; };
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::Bundle;
use bevy_render::{ use bevy_render::{
@ -10,7 +10,7 @@ use bevy_render::{
view::Visibility, view::Visibility,
}; };
#[cfg(feature = "bevy_text")] #[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}; use bevy_transform::prelude::{GlobalTransform, Transform};
/// The basic UI node /// The basic UI node
@ -76,6 +76,10 @@ pub struct ImageBundle {
pub background_color: BackgroundColor, pub background_color: BackgroundColor,
/// The image of the node /// The image of the node
pub image: UiImage, 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 /// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy, pub focus_policy: FocusPolicy,
/// The transform of the node /// The transform of the node
@ -106,6 +110,8 @@ pub struct TextBundle {
pub style: Style, pub style: Style,
/// Contains the text of the node /// Contains the text of the node
pub text: Text, pub text: Text,
/// Text layout information
pub text_layout_info: TextLayoutInfo,
/// The calculated size based on the given image /// The calculated size based on the given image
pub calculated_size: CalculatedSize, pub calculated_size: CalculatedSize,
/// Whether this node should block interaction with lower nodes /// Whether this node should block interaction with lower nodes
@ -135,6 +141,7 @@ impl Default for TextBundle {
fn default() -> Self { fn default() -> Self {
Self { Self {
text: Default::default(), text: Default::default(),
text_layout_info: Default::default(),
calculated_size: Default::default(), calculated_size: Default::default(),
// Transparent background // Transparent background
background_color: BackgroundColor(Color::NONE), background_color: BackgroundColor(Color::NONE),

View file

@ -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 /// The background color of the node
/// ///
/// This serves as the "fill" color. /// This serves as the "fill" color.

View file

@ -1,29 +1,91 @@
use crate::{CalculatedSize, UiImage}; use crate::{measurement::AvailableSpace, CalculatedSize, Measure, Node, UiImage};
use bevy_asset::Assets; use bevy_asset::Assets;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_ecs::query::Without; 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_math::Vec2;
use bevy_render::texture::Image; use bevy_render::texture::Image;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_text::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<f32>,
height: Option<f32>,
_: 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<dyn Measure> {
Box::new(self.clone())
}
}
/// Updates calculated size of the node based on the image provided /// Updates calculated size of the node based on the image provided
pub fn update_image_calculated_size_system( pub fn update_image_calculated_size_system(
textures: Res<Assets<Image>>, textures: Res<Assets<Image>>,
#[cfg(feature = "bevy_text")] mut query: Query<(&mut CalculatedSize, &UiImage), Without<Text>>, #[cfg(feature = "bevy_text")] mut query: Query<
#[cfg(not(feature = "bevy_text"))] mut query: Query<(&mut CalculatedSize, &UiImage)>, (&mut CalculatedSize, &UiImage, &mut UiImageSize),
(With<Node>, Without<Text>),
>,
#[cfg(not(feature = "bevy_text"))] mut query: Query<
(&mut CalculatedSize, &UiImage, &mut UiImageSize),
With<Node>,
>,
) { ) {
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) { if let Some(texture) = textures.get(&image.texture) {
let size = Vec2::new( let size = Vec2::new(
texture.texture_descriptor.size.width as f32, texture.texture_descriptor.size.width as f32,
texture.texture_descriptor.size.height as f32, texture.texture_descriptor.size.height as f32,
); );
// Update only if size has changed to avoid needless layout calculations // Update only if size has changed to avoid needless layout calculations
if size != calculated_size.size { if size != image_size.size {
calculated_size.size = size; image_size.size = size;
calculated_size.preserve_aspect_ratio = true; calculated_size.measure = Box::new(ImageMeasure { size });
} }
} }
} }

View file

@ -1,33 +1,135 @@
use crate::{CalculatedSize, Node, Style, UiScale, Val}; use crate::{CalculatedSize, Measure, Node, UiScale};
use bevy_asset::Assets; use bevy_asset::Assets;
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, entity::Entity,
query::{Changed, Or, With}, query::{Changed, Or, With},
system::{Commands, Local, ParamSet, Query, Res, ResMut}, system::{Local, ParamSet, Query, Res, ResMut},
}; };
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_render::texture::Image; use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas; use bevy_sprite::TextureAtlas;
use bevy_text::{ use bevy_text::{
Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextMeasureInfo,
TextSettings, YAxisOrientation, TextPipeline, TextSettings, YAxisOrientation,
}; };
use bevy_window::{PrimaryWindow, Window}; use bevy_window::{PrimaryWindow, Window};
use taffy::style::AvailableSpace;
fn scale_value(value: f32, factor: f64) -> f32 { fn scale_value(value: f32, factor: f64) -> f32 {
(value as f64 * factor) as f32 (value as f64 * factor) as f32
} }
/// Defines how `min_size`, `size`, and `max_size` affects the bounds of a text #[derive(Clone)]
/// block. pub struct TextMeasure {
pub fn text_constraint(min_size: Val, size: Val, max_size: Val, scale_factor: f64) -> f32 { pub info: TextMeasureInfo,
// Needs support for percentages }
match (min_size, size, max_size) {
(_, _, Val::Px(max)) => scale_value(max, scale_factor), impl Measure for TextMeasure {
(Val::Px(min), _, _) => scale_value(min, scale_factor), fn measure(
(Val::Auto, Val::Px(size), Val::Auto) => scale_value(size, scale_factor), &self,
_ => f32::MAX, width: Option<f32>,
height: Option<f32>,
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<dyn Measure> {
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<Vec<Entity>>,
mut last_scale_factor: Local<f64>,
fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>,
ui_scale: Res<UiScale>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_queries: ParamSet<(
Query<Entity, Changed<Text>>,
Query<Entity, (With<Text>, With<Node>)>,
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. /// 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. /// It does not modify or observe existing ones.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn text_system( pub fn text_system(
mut commands: Commands, mut queued_text: Local<Vec<Entity>>,
mut queued_text_ids: Local<Vec<Entity>>,
mut last_scale_factor: Local<f64>,
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
mut last_scale_factor: Local<f64>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>, windows: Query<&Window, With<PrimaryWindow>>,
text_settings: Res<TextSettings>, text_settings: Res<TextSettings>,
@ -52,14 +153,9 @@ pub fn text_system(
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>, mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<TextPipeline>, mut text_pipeline: ResMut<TextPipeline>,
mut text_queries: ParamSet<( mut text_queries: ParamSet<(
Query<Entity, Or<(Changed<Text>, Changed<Node>, Changed<Style>)>>, Query<Entity, Or<(Changed<Text>, Changed<Node>)>>,
Query<Entity, (With<Text>, With<Style>)>, Query<Entity, (With<Text>, With<Node>)>,
Query<( Query<(&Node, &Text, &mut TextLayoutInfo)>,
&Text,
&Style,
&mut CalculatedSize,
Option<&mut TextLayoutInfo>,
)>,
)>, )>,
) { ) {
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
@ -70,44 +166,29 @@ pub fn text_system(
let scale_factor = ui_scale.scale * window_scale_factor; let scale_factor = ui_scale.scale * window_scale_factor;
let inv_scale_factor = 1. / scale_factor;
#[allow(clippy::float_cmp)] #[allow(clippy::float_cmp)]
if *last_scale_factor == scale_factor { if *last_scale_factor == scale_factor {
// Adds all entities where the text or the style has changed to the local queue // Adds all entities where the text or the style has changed to the local queue
for entity in text_queries.p0().iter() { for entity in text_queries.p0().iter() {
queued_text_ids.push(entity); if !queued_text.contains(&entity) {
queued_text.push(entity);
}
} }
} else { } else {
// If the scale factor has changed, queue all text // If the scale factor has changed, queue all text
for entity in text_queries.p1().iter() { for entity in text_queries.p1().iter() {
queued_text_ids.push(entity); queued_text.push(entity);
} }
*last_scale_factor = scale_factor; *last_scale_factor = scale_factor;
} }
if queued_text_ids.is_empty() {
return;
}
// Computes all text in the local queue
let mut new_queue = Vec::new(); let mut new_queue = Vec::new();
let mut query = text_queries.p2(); let mut text_query = text_queries.p2();
for entity in queued_text_ids.drain(..) { for entity in queued_text.drain(..) {
if let Ok((text, style, mut calculated_size, text_layout_info)) = query.get_mut(entity) { if let Ok((node, text, mut text_layout_info)) = text_query.get_mut(entity) {
let node_size = Vec2::new( let node_size = Vec2::new(
text_constraint( scale_value(node.size().x, scale_factor),
style.min_size.width, scale_value(node.size().y, scale_factor),
style.size.width,
style.max_size.width,
scale_factor,
),
text_constraint(
style.min_size.height,
style.size.height,
style.max_size.height,
scale_factor,
),
); );
match text_pipeline.queue_text( match text_pipeline.queue_text(
@ -133,20 +214,10 @@ pub fn text_system(
panic!("Fatal error when processing text: {e}."); panic!("Fatal error when processing text: {e}.");
} }
Ok(info) => { Ok(info) => {
calculated_size.size = Vec2::new( *text_layout_info = info;
scale_value(info.size.x, inv_scale_factor),
scale_value(info.size.y, inv_scale_factor),
);
match text_layout_info {
Some(mut t) => *t = info,
None => {
commands.entity(entity).insert(info);
}
}
} }
} }
} }
} }
*queued_text = new_queue;
*queued_text_ids = new_queue;
} }