From bad4159b970a7a3c60aae4c5335d2ee517b3305e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 18 Jan 2023 02:19:17 +0000 Subject: [PATCH] Remove VerticalAlign from TextAlignment (#6807) # Objective Remove the `VerticalAlign` enum. Text's alignment field should only affect the text's internal text alignment, not its position. The only way to control a `TextBundle`'s position and bounds should be through the manipulation of the constraints in the `Style` components of the nodes in the Bevy UI's layout tree. `Text2dBundle` should have a separate `Anchor` component that sets its position relative to its transform. Related issues: #676, #1490, #5502, #5513, #5834, #6717, #6724, #6741, #6748 ## Changelog * Changed `TextAlignment` into an enum with `Left`, `Center`, and `Right` variants. * Removed the `HorizontalAlign` and `VerticalAlign` types. * Added an `Anchor` component to `Text2dBundle` * Added `Component` derive to `Anchor` * Use `f32::INFINITY` instead of `f32::MAX` to represent unbounded text in Text2dBounds ## Migration Guide The `alignment` field of `Text` now only affects the text's internal alignment. ### Change `TextAlignment` to TextAlignment` which is now an enum. Replace: * `TextAlignment::TOP_LEFT`, `TextAlignment::CENTER_LEFT`, `TextAlignment::BOTTOM_LEFT` with `TextAlignment::Left` * `TextAlignment::TOP_CENTER`, `TextAlignment::CENTER_LEFT`, `TextAlignment::BOTTOM_CENTER` with `TextAlignment::Center` * `TextAlignment::TOP_RIGHT`, `TextAlignment::CENTER_RIGHT`, `TextAlignment::BOTTOM_RIGHT` with `TextAlignment::Right` ### Changes for `Text2dBundle` `Text2dBundle` has a new field 'text_anchor' that takes an `Anchor` component that controls its position relative to its transform. --- crates/bevy_sprite/src/sprite.rs | 2 +- crates/bevy_text/src/glyph_brush.rs | 3 +- crates/bevy_text/src/lib.rs | 8 +- crates/bevy_text/src/text.rs | 125 ++++-------------- crates/bevy_text/src/text2d.rs | 81 +++++------- examples/2d/text2d.rs | 48 ++++--- .../external_source_external_thread.rs | 2 +- examples/ios/src/lib.rs | 2 +- examples/tools/gamepad_viewer.rs | 9 +- examples/ui/text.rs | 2 +- examples/ui/text_debug.rs | 2 +- 11 files changed, 88 insertions(+), 196 deletions(-) diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index da2a6de1c2cf1b..3a3e85a4caf8a8 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -24,7 +24,7 @@ pub struct Sprite { /// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). /// It defaults to `Anchor::Center`. -#[derive(Debug, Clone, Default, Reflect)] +#[derive(Component, Debug, Clone, Default, Reflect)] #[doc(alias = "pivot")] pub enum Anchor { #[default] diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index b3b8e3d79f0187..8f88a33f56467f 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -41,8 +41,7 @@ impl GlyphBrush { ..Default::default() }; let section_glyphs = Layout::default() - .h_align(text_alignment.horizontal.into()) - .v_align(text_alignment.vertical.into()) + .h_align(text_alignment.into()) .calculate_glyphs(&self.fonts, &geom, sections); Ok(section_glyphs) } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 79f6909289a5ea..84844ec5024ef3 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -20,10 +20,7 @@ pub use text2d::*; pub mod prelude { #[doc(hidden)] - pub use crate::{ - Font, HorizontalAlign, Text, Text2dBundle, TextAlignment, TextError, TextSection, - TextStyle, VerticalAlign, - }; + pub use crate::{Font, Text, Text2dBundle, TextAlignment, TextError, TextSection, TextStyle}; } use bevy_app::prelude::*; @@ -77,9 +74,8 @@ impl Plugin for TextPlugin { .register_type::() .register_type::>() .register_type::() + .register_type::() .register_type::() - .register_type::() - .register_type::() .init_asset_loader::() .init_resource::() .init_resource::() diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 7a8dc176c6349d..719b59106b7004 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -2,24 +2,36 @@ use bevy_asset::Handle; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_reflect::{prelude::*, FromReflect}; use bevy_render::color::Color; +use bevy_utils::default; use serde::{Deserialize, Serialize}; use crate::Font; -#[derive(Component, Debug, Default, Clone, Reflect)] +#[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct Text { pub sections: Vec, + /// The text's internal alignment. + /// Should not affect its position within a container. pub alignment: TextAlignment, } +impl Default for Text { + fn default() -> Self { + Self { + sections: Default::default(), + alignment: TextAlignment::Left, + } + } +} + impl Text { /// Constructs a [`Text`] with a single section. /// /// ``` /// # use bevy_asset::Handle; /// # use bevy_render::color::Color; - /// # use bevy_text::{Font, Text, TextAlignment, TextStyle, HorizontalAlign, VerticalAlign}; + /// # use bevy_text::{Font, Text, TextStyle, TextAlignment}; /// # /// # let font_handle: Handle = Default::default(); /// # @@ -42,12 +54,12 @@ impl Text { /// color: Color::WHITE, /// }, /// ) // You can still add an alignment. - /// .with_alignment(TextAlignment::CENTER); + /// .with_alignment(TextAlignment::Center); /// ``` pub fn from_section(value: impl Into, style: TextStyle) -> Self { Self { sections: vec![TextSection::new(value, style)], - alignment: Default::default(), + ..default() } } @@ -82,7 +94,7 @@ impl Text { pub fn from_sections(sections: impl IntoIterator) -> Self { Self { sections: sections.into_iter().collect(), - alignment: Default::default(), + ..default() } } @@ -117,78 +129,10 @@ impl TextSection { } } -#[derive(Debug, Clone, Copy, Reflect)] -pub struct TextAlignment { - pub vertical: VerticalAlign, - pub horizontal: HorizontalAlign, -} - -impl TextAlignment { - /// A [`TextAlignment`] set to the top-left. - pub const TOP_LEFT: Self = TextAlignment { - vertical: VerticalAlign::Top, - horizontal: HorizontalAlign::Left, - }; - - /// A [`TextAlignment`] set to the top-center. - pub const TOP_CENTER: Self = TextAlignment { - vertical: VerticalAlign::Top, - horizontal: HorizontalAlign::Center, - }; - - /// A [`TextAlignment`] set to the top-right. - pub const TOP_RIGHT: Self = TextAlignment { - vertical: VerticalAlign::Top, - horizontal: HorizontalAlign::Right, - }; - - /// A [`TextAlignment`] set to center the center-left. - pub const CENTER_LEFT: Self = TextAlignment { - vertical: VerticalAlign::Center, - horizontal: HorizontalAlign::Left, - }; - - /// A [`TextAlignment`] set to center on both axes. - pub const CENTER: Self = TextAlignment { - vertical: VerticalAlign::Center, - horizontal: HorizontalAlign::Center, - }; - - /// A [`TextAlignment`] set to the center-right. - pub const CENTER_RIGHT: Self = TextAlignment { - vertical: VerticalAlign::Center, - horizontal: HorizontalAlign::Right, - }; - - /// A [`TextAlignment`] set to the bottom-left. - pub const BOTTOM_LEFT: Self = TextAlignment { - vertical: VerticalAlign::Bottom, - horizontal: HorizontalAlign::Left, - }; - - /// A [`TextAlignment`] set to the bottom-center. - pub const BOTTOM_CENTER: Self = TextAlignment { - vertical: VerticalAlign::Bottom, - horizontal: HorizontalAlign::Center, - }; - - /// A [`TextAlignment`] set to the bottom-right. - pub const BOTTOM_RIGHT: Self = TextAlignment { - vertical: VerticalAlign::Bottom, - horizontal: HorizontalAlign::Right, - }; -} - -impl Default for TextAlignment { - fn default() -> Self { - TextAlignment::TOP_LEFT - } -} - /// Describes horizontal alignment preference for positioning & bounds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] -pub enum HorizontalAlign { +pub enum TextAlignment { /// Leftmost character is immediately to the right of the render position.
/// Bounds start from the render position and advance rightwards. Left, @@ -200,35 +144,12 @@ pub enum HorizontalAlign { Right, } -impl From for glyph_brush_layout::HorizontalAlign { - fn from(val: HorizontalAlign) -> Self { - match val { - HorizontalAlign::Left => glyph_brush_layout::HorizontalAlign::Left, - HorizontalAlign::Center => glyph_brush_layout::HorizontalAlign::Center, - HorizontalAlign::Right => glyph_brush_layout::HorizontalAlign::Right, - } - } -} - -/// Describes vertical alignment preference for positioning & bounds. Currently a placeholder -/// for future functionality. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[reflect(Serialize, Deserialize)] -pub enum VerticalAlign { - /// Characters/bounds start underneath the render position and progress downwards. - Top, - /// Characters/bounds center at the render position and progress outward equally. - Center, - /// Characters/bounds start above the render position and progress upward. - Bottom, -} - -impl From for glyph_brush_layout::VerticalAlign { - fn from(val: VerticalAlign) -> Self { +impl From for glyph_brush_layout::HorizontalAlign { + fn from(val: TextAlignment) -> Self { match val { - VerticalAlign::Top => glyph_brush_layout::VerticalAlign::Top, - VerticalAlign::Center => glyph_brush_layout::VerticalAlign::Center, - VerticalAlign::Bottom => glyph_brush_layout::VerticalAlign::Bottom, + TextAlignment::Left => glyph_brush_layout::HorizontalAlign::Left, + TextAlignment::Center => glyph_brush_layout::HorizontalAlign::Center, + TextAlignment::Right => glyph_brush_layout::HorizontalAlign::Right, } } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 956c52644fef5b..15a4038ad87e5a 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -22,17 +22,10 @@ use bevy_utils::HashSet; use bevy_window::{WindowId, WindowScaleFactorChanged, Windows}; use crate::{ - Font, FontAtlasSet, FontAtlasWarning, HorizontalAlign, Text, TextError, TextLayoutInfo, - TextPipeline, TextSettings, VerticalAlign, YAxisOrientation, + Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, + TextSettings, YAxisOrientation, }; -/// The calculated size of text drawn in 2D scene. -#[derive(Component, Default, Copy, Clone, Debug, Reflect)] -#[reflect(Component)] -pub struct Text2dSize { - pub size: Vec2, -} - /// 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 `TextAlignment`. @@ -47,21 +40,27 @@ pub struct Text2dBounds { } impl Default for Text2dBounds { + #[inline] fn default() -> Self { - Self { - size: Vec2::new(f32::MAX, f32::MAX), - } + 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)] pub struct Text2dBundle { pub text: Text, + pub text_anchor: Anchor, pub transform: Transform, pub global_transform: GlobalTransform, - pub text_2d_size: Text2dSize, pub text_2d_bounds: Text2dBounds, pub visibility: Visibility, pub computed_visibility: ComputedVisibility, @@ -77,32 +76,23 @@ pub fn extract_text2d_sprite( &ComputedVisibility, &Text, &TextLayoutInfo, + &Anchor, &GlobalTransform, - &Text2dSize, )>, >, ) { let scale_factor = windows.scale_factor(WindowId::primary()) as f32; - for (entity, computed_visibility, text, text_layout_info, text_transform, calculated_size) in + for (entity, computed_visibility, text, text_layout_info, anchor, text_transform) in text2d_query.iter() { if !computed_visibility.is_visible() { continue; } - let (width, height) = (calculated_size.size.x, calculated_size.size.y); let text_glyphs = &text_layout_info.glyphs; - let alignment_offset = match text.alignment.vertical { - VerticalAlign::Top => Vec3::new(0.0, -height, 0.0), - VerticalAlign::Center => Vec3::new(0.0, -height * 0.5, 0.0), - VerticalAlign::Bottom => Vec3::ZERO, - } + match text.alignment.horizontal { - HorizontalAlign::Left => Vec3::ZERO, - HorizontalAlign::Center => Vec3::new(-width * 0.5, 0.0, 0.0), - HorizontalAlign::Right => Vec3::new(-width, 0.0, 0.0), - }; - + let text_anchor = anchor.as_vec() * Vec2::new(1., -1.) - 0.5; + let alignment_offset = text_layout_info.size * text_anchor; let mut color = Color::WHITE; let mut current_section = usize::MAX; for text_glyph in text_glyphs { @@ -120,10 +110,9 @@ pub fn extract_text2d_sprite( let index = text_glyph.atlas_info.glyph_index; let rect = Some(atlas.textures[index]); - let glyph_transform = Transform::from_translation( - alignment_offset * scale_factor + text_glyph.position.extend(0.), - ); - // NOTE: Should match `bevy_ui::render::extract_text_uinodes` + let glyph_transform = + Transform::from_translation((alignment_offset + text_glyph.position).extend(0.)); + let transform = *text_transform * GlobalTransform::from_scale(Vec3::splat(scale_factor.recip())) * glyph_transform; @@ -167,8 +156,7 @@ pub fn update_text2d_layout( mut text_query: Query<( Entity, Ref, - Option<&Text2dBounds>, - &mut Text2dSize, + &Text2dBounds, Option<&mut TextLayoutInfo>, )>, ) { @@ -176,15 +164,12 @@ pub fn update_text2d_layout( let factor_changed = scale_factor_changed.iter().last().is_some(); let scale_factor = windows.scale_factor(WindowId::primary()); - for (entity, text, maybe_bounds, mut calculated_size, text_layout_info) in &mut text_query { + for (entity, text, bounds, text_layout_info) in &mut text_query { if factor_changed || text.is_changed() || queue.remove(&entity) { - let text_bounds = match maybe_bounds { - Some(bounds) => Vec2::new( - scale_value(bounds.size.x, scale_factor), - scale_value(bounds.size.y, scale_factor), - ), - None => Vec2::new(f32::MAX, f32::MAX), - }; + let text_bounds = Vec2::new( + scale_value(bounds.size.x, scale_factor), + scale_value(bounds.size.y, scale_factor), + ); match text_pipeline.queue_text( &fonts, @@ -207,18 +192,12 @@ pub fn update_text2d_layout( Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } - Ok(info) => { - calculated_size.size = Vec2::new( - scale_value(info.size.x, 1. / scale_factor), - scale_value(info.size.y, 1. / scale_factor), - ); - match text_layout_info { - Some(mut t) => *t = info, - None => { - commands.entity(entity).insert(info); - } + Ok(info) => match text_layout_info { + Some(mut t) => *t = info, + None => { + commands.entity(entity).insert(info); } - } + }, } } } diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 176c89b91e0f70..2f4f53df183ec4 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -33,7 +33,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 60.0, color: Color::WHITE, }; - let text_alignment = TextAlignment::CENTER; + let text_alignment = TextAlignment::Center; // 2d camera commands.spawn(Camera2dBundle::default()); // Demonstrate changing translation @@ -64,31 +64,29 @@ fn setup(mut commands: Commands, asset_server: Res) { // Demonstrate text wrapping let box_size = Vec2::new(300.0, 200.0); let box_position = Vec2::new(0.0, -250.0); - commands.spawn(SpriteBundle { - sprite: Sprite { - color: Color::rgb(0.25, 0.25, 0.75), - custom_size: Some(Vec2::new(box_size.x, box_size.y)), + commands + .spawn(SpriteBundle { + sprite: Sprite { + color: Color::rgb(0.25, 0.25, 0.75), + custom_size: Some(Vec2::new(box_size.x, box_size.y)), + ..default() + }, + transform: Transform::from_translation(box_position.extend(0.0)), ..default() - }, - transform: Transform::from_translation(box_position.extend(0.0)), - ..default() - }); - commands.spawn(Text2dBundle { - text: Text::from_section("this text wraps in the box", text_style), - text_2d_bounds: Text2dBounds { - // Wrap text in the rectangle - size: box_size, - }, - // We align text to the top-left, so this transform is the top-left corner of our text. The - // box is centered at box_position, so it is necessary to move by half of the box size to - // keep the text in the box. - transform: Transform::from_xyz( - box_position.x - box_size.x / 2.0, - box_position.y + box_size.y / 2.0, - 1.0, - ), - ..default() - }); + }) + .with_children(|builder| { + builder.spawn(Text2dBundle { + text: Text::from_section("this text wraps in the box", text_style) + .with_alignment(TextAlignment::Left), + text_2d_bounds: Text2dBounds { + // Wrap text in the rectangle + size: box_size, + }, + // ensure the text is drawn on top of the box + transform: Transform::from_translation(Vec3::Z), + ..default() + }); + }); } fn animate_translation( diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 7e370879a2d90e..3a174713904fa4 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -66,7 +66,7 @@ fn spawn_text( for (per_frame, event) in reader.iter().enumerate() { commands.spawn(Text2dBundle { text: Text::from_section(event.0.to_string(), text_style.clone()) - .with_alignment(TextAlignment::CENTER), + .with_alignment(TextAlignment::Center), transform: Transform::from_xyz( per_frame as f32 * 100.0 + rand::thread_rng().gen_range(-40.0..40.0), 300.0, diff --git a/examples/ios/src/lib.rs b/examples/ios/src/lib.rs index e77085cd6737b3..98c493af8619ed 100644 --- a/examples/ios/src/lib.rs +++ b/examples/ios/src/lib.rs @@ -121,7 +121,7 @@ fn setup_scene( color: Color::BLACK, }, ) - .with_text_alignment(TextAlignment::CENTER), + .with_text_alignment(TextAlignment::Center), ); }); } diff --git a/examples/tools/gamepad_viewer.rs b/examples/tools/gamepad_viewer.rs index 2f7a77a57c04b4..ae912192cbfdd3 100644 --- a/examples/tools/gamepad_viewer.rs +++ b/examples/tools/gamepad_viewer.rs @@ -7,7 +7,7 @@ use bevy::{ GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent, GamepadSettings, }, prelude::*, - sprite::{MaterialMesh2dBundle, Mesh2dHandle}, + sprite::{Anchor, MaterialMesh2dBundle, Mesh2dHandle}, }; const BUTTON_RADIUS: f32 = 25.; @@ -342,8 +342,8 @@ fn setup_sticks( value: format!("{:.3}", 0.), style, }, - ]) - .with_alignment(TextAlignment::BOTTOM_CENTER), + ]), + text_anchor: Anchor::BottomCenter, ..default() }, TextWithAxes { x_axis, y_axis }, @@ -409,8 +409,7 @@ fn setup_triggers( font_size: 16., color: TEXT_COLOR, }, - ) - .with_alignment(TextAlignment::CENTER), + ), ..default() }, TextWithButtonValue(button_type), diff --git a/examples/ui/text.rs b/examples/ui/text.rs index 014929cf7a63e5..a25da20132d027 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -41,7 +41,7 @@ fn setup(mut commands: Commands, asset_server: Res) { color: Color::WHITE, }, ) // Set the alignment of the Text - .with_text_alignment(TextAlignment::TOP_CENTER) + .with_text_alignment(TextAlignment::Center) // Set the style of the TextBundle itself. .with_style(Style { position_type: PositionType::Absolute, diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 040f2344c05262..6436f64720eee1 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -54,7 +54,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { color: Color::rgb(0.8, 0.2, 0.7), }, ) - .with_text_alignment(TextAlignment::CENTER) + .with_text_alignment(TextAlignment::Center) .with_style(Style { position_type: PositionType::Absolute, position: UiRect {