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_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<TextLayoutInfo, TextError> {
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 &section_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<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,
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<HashSet<Entity>>,
mut textures: ResMut<Assets<Image>>,
@ -159,12 +160,7 @@ pub fn update_text2d_layout(
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Entity,
Ref<Text>,
Ref<Text2dBounds>,
Option<&mut TextLayoutInfo>,
)>,
mut text_query: Query<(Entity, Ref<Text>, Ref<Text2dBounds>, &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,
}
}
}

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" }
# 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"] }

View file

@ -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<Option<f32>>, _available: Size<AvailableSpace>| {
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;
let measure = calculated_size.measure.dyn_clone();
let measure_func = taffy::node::MeasureFunc::Boxed(Box::new(
move |constraints: Size<Option<f32>>, available: Size<AvailableSpace>| {
let size = measure.measure(
constraints.width,
constraints.height,
available.width,
available.height,
);
taffy::geometry::Size {
width: size.x,
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) {
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

View file

@ -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<Image>`
// 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),
),
);

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
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),

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
///
/// 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;
#[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<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
pub fn update_image_calculated_size_system(
textures: Res<Assets<Image>>,
#[cfg(feature = "bevy_text")] mut query: Query<(&mut CalculatedSize, &UiImage), Without<Text>>,
#[cfg(not(feature = "bevy_text"))] mut query: Query<(&mut CalculatedSize, &UiImage)>,
#[cfg(feature = "bevy_text")] mut query: Query<
(&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) {
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 });
}
}
}

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_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<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.
@ -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<Vec<Entity>>,
mut last_scale_factor: Local<f64>,
mut queued_text: Local<Vec<Entity>>,
mut textures: ResMut<Assets<Image>>,
mut last_scale_factor: Local<f64>,
fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>,
text_settings: Res<TextSettings>,
@ -52,14 +153,9 @@ pub fn text_system(
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_queries: ParamSet<(
Query<Entity, Or<(Changed<Text>, Changed<Node>, Changed<Style>)>>,
Query<Entity, (With<Text>, With<Style>)>,
Query<(
&Text,
&Style,
&mut CalculatedSize,
Option<&mut TextLayoutInfo>,
)>,
Query<Entity, Or<(Changed<Text>, Changed<Node>)>>,
Query<Entity, (With<Text>, With<Node>)>,
Query<(&Node, &Text, &mut TextLayoutInfo)>,
)>,
) {
// 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 inv_scale_factor = 1. / 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() {
queued_text_ids.push(entity);
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_ids.push(entity);
queued_text.push(entity);
}
*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 query = text_queries.p2();
for entity in queued_text_ids.drain(..) {
if let Ok((text, style, mut calculated_size, text_layout_info)) = query.get_mut(entity) {
let mut text_query = text_queries.p2();
for entity in queued_text.drain(..) {
if let Ok((node, text, mut text_layout_info)) = text_query.get_mut(entity) {
let node_size = Vec2::new(
text_constraint(
style.min_size.width,
style.size.width,
style.max_size.width,
scale_factor,
),
text_constraint(
style.min_size.height,
style.size.height,
style.max_size.height,
scale_factor,
),
scale_value(node.size().x, scale_factor),
scale_value(node.size().y, scale_factor),
);
match text_pipeline.queue_text(
@ -133,20 +214,10 @@ pub fn text_system(
panic!("Fatal error when processing text: {e}.");
}
Ok(info) => {
calculated_size.size = Vec2::new(
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);
*text_layout_info = info;
}
}
}
}
}
}
*queued_text_ids = new_queue;
*queued_text = new_queue;
}