From 61a81e12b906d687211c685ec97e244f855bca6f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 10 Jul 2024 17:02:51 +0100 Subject: [PATCH 1/8] try translating by bounds --- crates/bevy_text/src/text2d.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 8c28fe3079aca..f3d2fb741ffaf 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -13,7 +13,7 @@ use bevy_ecs::{ query::{Changed, Without}, system::{Commands, Local, Query, Res, ResMut}, }; -use bevy_math::Vec2; +use bevy_math::{Vec2, Vec3}; use bevy_render::{ primitives::Aabb, texture::Image, @@ -76,6 +76,7 @@ pub fn extract_text2d_sprite( &Text, &TextLayoutInfo, &Anchor, + &TextBounds, &GlobalTransform, )>, >, @@ -87,17 +88,19 @@ pub fn extract_text2d_sprite( .unwrap_or(1.0); let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.)); - for (original_entity, view_visibility, text, text_layout_info, anchor, global_transform) in + for (original_entity, view_visibility, text, text_layout_info, anchor, bounds, global_transform) in text2d_query.iter() { if !view_visibility.get() { continue; } + let b = Vec3::new(bounds.width.unwrap_or_default(), bounds.height.unwrap_or_default(), 0.); + let text_anchor = -(anchor.as_vec() + 0.5); let alignment_translation = text_layout_info.size * text_anchor; let transform = *global_transform - * GlobalTransform::from_translation(alignment_translation.extend(0.)) + * GlobalTransform::from_translation(alignment_translation.extend(0.) - 0.5 * b) * scaling; let mut color = LinearRgba::WHITE; let mut current_section = usize::MAX; @@ -113,7 +116,6 @@ pub fn extract_text2d_sprite( current_section = *section_index; } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let entity = commands.spawn_empty().id(); extracted_sprites.sprites.insert( entity, From ea6a786fdb214ce11b74ef2cc6bad12e8dda6faa Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 10 Jul 2024 17:08:07 +0100 Subject: [PATCH 2/8] negate y bounds offset --- crates/bevy_text/src/text2d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index f3d2fb741ffaf..27779ebca1d3a 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -95,7 +95,7 @@ pub fn extract_text2d_sprite( continue; } - let b = Vec3::new(bounds.width.unwrap_or_default(), bounds.height.unwrap_or_default(), 0.); + let b = Vec3::new(bounds.width.unwrap_or_default(), -bounds.height.unwrap_or_default(), 0.); let text_anchor = -(anchor.as_vec() + 0.5); let alignment_translation = text_layout_info.size * text_anchor; From 9cbe1e6cf8d90687429e2c8ee539b8c9d5dd93d5 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 10 Jul 2024 17:12:04 +0100 Subject: [PATCH 3/8] removed translation --- crates/bevy_text/src/text2d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 27779ebca1d3a..d6e43fe32389f 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -100,7 +100,7 @@ pub fn extract_text2d_sprite( let text_anchor = -(anchor.as_vec() + 0.5); let alignment_translation = text_layout_info.size * text_anchor; let transform = *global_transform - * GlobalTransform::from_translation(alignment_translation.extend(0.) - 0.5 * b) + * GlobalTransform::from_translation(alignment_translation.extend(0.)) * scaling; let mut color = LinearRgba::WHITE; let mut current_section = usize::MAX; From acac549d923f3a094f618431dcb6174327da594a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 10 Jul 2024 21:13:05 +0100 Subject: [PATCH 4/8] In `update_text2d_layout` after generating the layout translate all the glpyhs horizontally so that the left edge of the leftmost glyph is at 0. --- crates/bevy_text/src/text2d.rs | 102 +++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index d6e43fe32389f..be0e8f6dddf97 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,19 +1,22 @@ use crate::{ - BreakLineOn, CosmicBuffer, Font, FontAtlasSets, PositionedGlyph, Text, TextBounds, TextError, - TextLayoutInfo, TextPipeline, YAxisOrientation, + BreakLineOn, Font, FontAtlasSets, PositionedGlyph, Text, TextError, TextLayoutInfo, + TextPipeline, TextSettings, YAxisOrientation, }; use bevy_asset::Assets; use bevy_color::LinearRgba; use bevy_ecs::{ bundle::Bundle, change_detection::{DetectChanges, Ref}, + component::Component, entity::Entity, event::EventReader, prelude::With, query::{Changed, Without}, + reflect::ReflectComponent, system::{Commands, Local, Query, Res, ResMut}, }; -use bevy_math::{Vec2, Vec3}; +use bevy_math::{FloatOrd, Vec2}; +use bevy_reflect::Reflect; use bevy_render::{ primitives::Aabb, texture::Image, @@ -25,6 +28,34 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; +/// The maximum width and height of text. The text will wrap according to the specified size. +/// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the +/// specified [`JustifyText`](crate::text::JustifyText). +/// +/// Note: only characters that are completely out of the bounds will be truncated, so this is not a +/// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this +/// component is mainly useful for text wrapping only. +#[derive(Component, Copy, Clone, Debug, Reflect)] +#[reflect(Component)] +pub struct Text2dBounds { + /// The maximum width and height of text in logical pixels. + pub size: Vec2, +} + +impl Default for Text2dBounds { + #[inline] + fn default() -> Self { + Self::UNBOUNDED + } +} + +impl Text2dBounds { + /// Unbounded text will not be truncated or wrapped. + pub const UNBOUNDED: Self = Self { + size: Vec2::splat(f32::INFINITY), + }; +} + /// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) #[derive(Bundle, Clone, Debug, Default)] @@ -35,15 +66,13 @@ pub struct Text2dBundle { /// relative position which is controlled by the `Anchor` component. /// This means that for a block of text consisting of only one line that doesn't wrap, the `alignment` field will have no effect. pub text: Text, - /// Cached buffer for layout with cosmic-text - pub buffer: CosmicBuffer, /// How the text is positioned relative to its transform. /// /// `text_anchor` does not affect the internal alignment of the block of text, only /// its position. pub text_anchor: Anchor, /// The maximum width and height of the text. - pub text_2d_bounds: TextBounds, + pub text_2d_bounds: Text2dBounds, /// The transform of the text. pub transform: Transform, /// The global transform of the text. @@ -76,7 +105,6 @@ pub fn extract_text2d_sprite( &Text, &TextLayoutInfo, &Anchor, - &TextBounds, &GlobalTransform, )>, >, @@ -88,17 +116,15 @@ pub fn extract_text2d_sprite( .unwrap_or(1.0); let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.)); - for (original_entity, view_visibility, text, text_layout_info, anchor, bounds, global_transform) in + for (original_entity, view_visibility, text, text_layout_info, anchor, global_transform) in text2d_query.iter() { if !view_visibility.get() { continue; } - let b = Vec3::new(bounds.width.unwrap_or_default(), -bounds.height.unwrap_or_default(), 0.); - let text_anchor = -(anchor.as_vec() + 0.5); - let alignment_translation = text_layout_info.size * text_anchor; + let alignment_translation = text_layout_info.logical_size * text_anchor; let transform = *global_transform * GlobalTransform::from_translation(alignment_translation.extend(0.)) * scaling; @@ -116,13 +142,14 @@ pub fn extract_text2d_sprite( current_section = *section_index; } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); + let entity = commands.spawn_empty().id(); extracted_sprites.sprites.insert( entity, ExtractedSprite { transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, - rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()), + rect: Some(atlas.textures[atlas_info.glyph_index].as_rect()), custom_size: None, image_handle_id: atlas_info.texture.id(), flip_x: false, @@ -148,18 +175,13 @@ pub fn update_text2d_layout( mut queue: Local>, mut textures: ResMut>, fonts: Res>, + text_settings: Res, windows: Query<&Window, With>, mut scale_factor_changed: EventReader, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, - mut text_query: Query<( - Entity, - Ref, - Ref, - &mut TextLayoutInfo, - &mut CosmicBuffer, - )>, + mut text_query: Query<(Entity, Ref, Ref, &mut TextLayoutInfo)>, ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.read().last().is_some(); @@ -172,43 +194,48 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, bounds, mut text_layout_info, mut buffer) in &mut text_query { + 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 = TextBounds { - width: if text.linebreak_behavior == BreakLineOn::NoWrap { - None + let text_bounds = Vec2::new( + if text.linebreak_behavior == BreakLineOn::NoWrap { + f32::INFINITY } else { - bounds.width.map(|width| scale_value(width, scale_factor)) + scale_value(bounds.size.x, scale_factor) }, - height: bounds - .height - .map(|height| scale_value(height, scale_factor)), - }; - + scale_value(bounds.size.y, scale_factor), + ); match text_pipeline.queue_text( &fonts, &text.sections, - scale_factor.into(), + scale_factor, text.justify, text.linebreak_behavior, text_bounds, &mut font_atlas_sets, &mut texture_atlases, &mut textures, + text_settings.as_ref(), YAxisOrientation::BottomToTop, - buffer.as_mut(), ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, let's add this entity to the // queue for further processing queue.insert(entity); } - Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { + Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { - info.size.x = scale_value(info.size.x, inverse_scale_factor); - info.size.y = scale_value(info.size.y, inverse_scale_factor); + if let Some(min_x) = info.glyphs.iter() + .map(|glyph| FloatOrd(glyph.position.x - 0.5 * glyph.size.x)) + .min() { + for glyph in info.glyphs.iter_mut() { + glyph.position.x -= min_x.0; + } + } + + info.logical_size.x = scale_value(info.logical_size.x, inverse_scale_factor); + info.logical_size.y = scale_value(info.logical_size.y, inverse_scale_factor); *text_layout_info = info; } } @@ -235,9 +262,11 @@ pub fn calculate_bounds_text2d( for (entity, layout_info, anchor, aabb) in &mut text_to_update_aabb { // `Anchor::as_vec` gives us an offset relative to the text2d bounds, by negating it and scaling // by the logical size we compensate the transform offset in local space to get the center. - let center = (-anchor.as_vec() * layout_info.size).extend(0.0).into(); + let center = (-anchor.as_vec() * layout_info.logical_size) + .extend(0.0) + .into(); // Distance in local space from the center to the x and y limits of the text2d bounds. - let half_extents = (layout_info.size / 2.0).extend(0.0).into(); + let half_extents = (layout_info.logical_size / 2.0).extend(0.0).into(); if let Some(mut aabb) = aabb { *aabb = Aabb { center, @@ -270,6 +299,7 @@ mod tests { app.init_resource::>() .init_resource::>() .init_resource::>() + .init_resource::() .init_resource::() .init_resource::>() .insert_resource(TextPipeline::default()) From 3c011e6a3f7e76f02ca0f76fb386ed644dc0ed9e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 10 Jul 2024 21:54:59 +0100 Subject: [PATCH 5/8] cargo fmt --- crates/bevy_text/src/text2d.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index be0e8f6dddf97..88eab258bf94a 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -226,14 +226,17 @@ pub fn update_text2d_layout( panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { - if let Some(min_x) = info.glyphs.iter() + if let Some(min_x) = info + .glyphs + .iter() .map(|glyph| FloatOrd(glyph.position.x - 0.5 * glyph.size.x)) - .min() { + .min() + { for glyph in info.glyphs.iter_mut() { glyph.position.x -= min_x.0; } } - + info.logical_size.x = scale_value(info.logical_size.x, inverse_scale_factor); info.logical_size.y = scale_value(info.logical_size.y, inverse_scale_factor); *text_layout_info = info; From 17dae6dbefae9159e97a91b982622dde4d04351b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 10 Jul 2024 22:16:02 +0100 Subject: [PATCH 6/8] Fixed merge --- crates/bevy_text/src/text2d.rs | 86 ++++++++++++---------------------- 1 file changed, 31 insertions(+), 55 deletions(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 88eab258bf94a..3347602169040 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,22 +1,19 @@ use crate::{ - BreakLineOn, Font, FontAtlasSets, PositionedGlyph, Text, TextError, TextLayoutInfo, - TextPipeline, TextSettings, YAxisOrientation, + BreakLineOn, CosmicBuffer, Font, FontAtlasSets, PositionedGlyph, Text, TextBounds, TextError, + TextLayoutInfo, TextPipeline, YAxisOrientation, }; use bevy_asset::Assets; use bevy_color::LinearRgba; use bevy_ecs::{ bundle::Bundle, change_detection::{DetectChanges, Ref}, - component::Component, entity::Entity, event::EventReader, prelude::With, query::{Changed, Without}, - reflect::ReflectComponent, system::{Commands, Local, Query, Res, ResMut}, }; use bevy_math::{FloatOrd, Vec2}; -use bevy_reflect::Reflect; use bevy_render::{ primitives::Aabb, texture::Image, @@ -28,34 +25,6 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; -/// The maximum width and height of text. The text will wrap according to the specified size. -/// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the -/// specified [`JustifyText`](crate::text::JustifyText). -/// -/// Note: only characters that are completely out of the bounds will be truncated, so this is not a -/// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this -/// component is mainly useful for text wrapping only. -#[derive(Component, Copy, Clone, Debug, Reflect)] -#[reflect(Component)] -pub struct Text2dBounds { - /// The maximum width and height of text in logical pixels. - pub size: Vec2, -} - -impl Default for Text2dBounds { - #[inline] - fn default() -> Self { - Self::UNBOUNDED - } -} - -impl Text2dBounds { - /// Unbounded text will not be truncated or wrapped. - pub const UNBOUNDED: Self = Self { - size: Vec2::splat(f32::INFINITY), - }; -} - /// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) #[derive(Bundle, Clone, Debug, Default)] @@ -66,13 +35,15 @@ pub struct Text2dBundle { /// relative position which is controlled by the `Anchor` component. /// This means that for a block of text consisting of only one line that doesn't wrap, the `alignment` field will have no effect. pub text: Text, + /// Cached buffer for layout with cosmic-text + pub buffer: CosmicBuffer, /// How the text is positioned relative to its transform. /// /// `text_anchor` does not affect the internal alignment of the block of text, only /// its position. pub text_anchor: Anchor, /// The maximum width and height of the text. - pub text_2d_bounds: Text2dBounds, + pub text_2d_bounds: TextBounds, /// The transform of the text. pub transform: Transform, /// The global transform of the text. @@ -124,7 +95,7 @@ pub fn extract_text2d_sprite( } let text_anchor = -(anchor.as_vec() + 0.5); - let alignment_translation = text_layout_info.logical_size * text_anchor; + let alignment_translation = text_layout_info.size * text_anchor; let transform = *global_transform * GlobalTransform::from_translation(alignment_translation.extend(0.)) * scaling; @@ -149,7 +120,7 @@ pub fn extract_text2d_sprite( ExtractedSprite { transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, - rect: Some(atlas.textures[atlas_info.glyph_index].as_rect()), + rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()), custom_size: None, image_handle_id: atlas_info.texture.id(), flip_x: false, @@ -175,13 +146,18 @@ pub fn update_text2d_layout( mut queue: Local>, mut textures: ResMut>, fonts: Res>, - text_settings: Res, windows: Query<&Window, With>, mut scale_factor_changed: EventReader, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, - mut text_query: Query<(Entity, Ref, Ref, &mut TextLayoutInfo)>, + mut text_query: Query<( + Entity, + Ref, + Ref, + &mut TextLayoutInfo, + &mut CosmicBuffer, + )>, ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.read().last().is_some(); @@ -194,35 +170,38 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, bounds, mut text_layout_info) in &mut text_query { + for (entity, text, bounds, mut text_layout_info, mut buffer) in &mut text_query { if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) { - let text_bounds = Vec2::new( - if text.linebreak_behavior == BreakLineOn::NoWrap { - f32::INFINITY + let text_bounds = TextBounds { + width: if text.linebreak_behavior == BreakLineOn::NoWrap { + None } else { - scale_value(bounds.size.x, scale_factor) + bounds.width.map(|width| scale_value(width, scale_factor)) }, - scale_value(bounds.size.y, scale_factor), - ); + height: bounds + .height + .map(|height| scale_value(height, scale_factor)), + }; + match text_pipeline.queue_text( &fonts, &text.sections, - scale_factor, + scale_factor.into(), text.justify, text.linebreak_behavior, text_bounds, &mut font_atlas_sets, &mut texture_atlases, &mut textures, - text_settings.as_ref(), YAxisOrientation::BottomToTop, + buffer.as_mut(), ) { Err(TextError::NoSuchFont) => { // There was an error processing the text layout, let's add this entity to the // queue for further processing queue.insert(entity); } - Err(e @ TextError::FailedToAddGlyph(_)) => { + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } Ok(mut info) => { @@ -237,8 +216,8 @@ pub fn update_text2d_layout( } } - info.logical_size.x = scale_value(info.logical_size.x, inverse_scale_factor); - info.logical_size.y = scale_value(info.logical_size.y, inverse_scale_factor); + info.size.x = scale_value(info.size.x, inverse_scale_factor); + info.size.y = scale_value(info.size.y, inverse_scale_factor); *text_layout_info = info; } } @@ -265,11 +244,9 @@ pub fn calculate_bounds_text2d( for (entity, layout_info, anchor, aabb) in &mut text_to_update_aabb { // `Anchor::as_vec` gives us an offset relative to the text2d bounds, by negating it and scaling // by the logical size we compensate the transform offset in local space to get the center. - let center = (-anchor.as_vec() * layout_info.logical_size) - .extend(0.0) - .into(); + let center = (-anchor.as_vec() * layout_info.size).extend(0.0).into(); // Distance in local space from the center to the x and y limits of the text2d bounds. - let half_extents = (layout_info.logical_size / 2.0).extend(0.0).into(); + let half_extents = (layout_info.size / 2.0).extend(0.0).into(); if let Some(mut aabb) = aabb { *aabb = Aabb { center, @@ -302,7 +279,6 @@ mod tests { app.init_resource::>() .init_resource::>() .init_resource::>() - .init_resource::() .init_resource::() .init_resource::>() .insert_resource(TextPipeline::default()) From b5407b420ca44634f48be60a53582aaed9f5b05b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 5 Sep 2024 00:30:22 +0100 Subject: [PATCH 7/8] Added brief comment --- crates/bevy_text/src/text2d.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index af492ac66aaed..d20bbf059bcb4 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -208,6 +208,7 @@ pub fn update_text2d_layout( } Ok(()) => { + // Translate all the glyphs horizontally so that the left edge of the leftmost glyph is at 0. if let Some(min_x) = text_layout_info .glyphs .iter() From d75e16d93e92ad1a2a722e143a9094d6879956fc Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 5 Sep 2024 00:36:52 +0100 Subject: [PATCH 8/8] more comment --- crates/bevy_text/src/text2d.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index d20bbf059bcb4..aa67da944b0a9 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -209,6 +209,7 @@ pub fn update_text2d_layout( Ok(()) => { // Translate all the glyphs horizontally so that the left edge of the leftmost glyph is at 0. + // This a temporary fix for layout issues with the `Text2d` API (https://github.com/bevyengine/bevy/issues/14266) if let Some(min_x) = text_layout_info .glyphs .iter()