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.
This commit is contained in:
ickshonpe 2023-01-18 02:19:17 +00:00
parent 4ff50f6b50
commit 9eefd7c022
11 changed files with 88 additions and 196 deletions

View File

@ -24,7 +24,7 @@ pub struct Sprite {
/// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). /// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform).
/// It defaults to `Anchor::Center`. /// It defaults to `Anchor::Center`.
#[derive(Debug, Clone, Default, Reflect)] #[derive(Component, Debug, Clone, Default, Reflect)]
#[doc(alias = "pivot")] #[doc(alias = "pivot")]
pub enum Anchor { pub enum Anchor {
#[default] #[default]

View File

@ -41,8 +41,7 @@ impl GlyphBrush {
..Default::default() ..Default::default()
}; };
let section_glyphs = Layout::default() let section_glyphs = Layout::default()
.h_align(text_alignment.horizontal.into()) .h_align(text_alignment.into())
.v_align(text_alignment.vertical.into())
.calculate_glyphs(&self.fonts, &geom, sections); .calculate_glyphs(&self.fonts, &geom, sections);
Ok(section_glyphs) Ok(section_glyphs)
} }

View File

@ -20,10 +20,7 @@ pub use text2d::*;
pub mod prelude { pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
pub use crate::{ pub use crate::{Font, Text, Text2dBundle, TextAlignment, TextError, TextSection, TextStyle};
Font, HorizontalAlign, Text, Text2dBundle, TextAlignment, TextError, TextSection,
TextStyle, VerticalAlign,
};
} }
use bevy_app::prelude::*; use bevy_app::prelude::*;
@ -77,9 +74,8 @@ impl Plugin for TextPlugin {
.register_type::<TextSection>() .register_type::<TextSection>()
.register_type::<Vec<TextSection>>() .register_type::<Vec<TextSection>>()
.register_type::<TextStyle>() .register_type::<TextStyle>()
.register_type::<Text>()
.register_type::<TextAlignment>() .register_type::<TextAlignment>()
.register_type::<VerticalAlign>()
.register_type::<HorizontalAlign>()
.init_asset_loader::<FontLoader>() .init_asset_loader::<FontLoader>()
.init_resource::<TextSettings>() .init_resource::<TextSettings>()
.init_resource::<FontAtlasWarning>() .init_resource::<FontAtlasWarning>()

View File

@ -2,24 +2,36 @@ use bevy_asset::Handle;
use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_reflect::{prelude::*, FromReflect}; use bevy_reflect::{prelude::*, FromReflect};
use bevy_render::color::Color; use bevy_render::color::Color;
use bevy_utils::default;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::Font; use crate::Font;
#[derive(Component, Debug, Default, Clone, Reflect)] #[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)] #[reflect(Component, Default)]
pub struct Text { pub struct Text {
pub sections: Vec<TextSection>, pub sections: Vec<TextSection>,
/// The text's internal alignment.
/// Should not affect its position within a container.
pub alignment: TextAlignment, pub alignment: TextAlignment,
} }
impl Default for Text {
fn default() -> Self {
Self {
sections: Default::default(),
alignment: TextAlignment::Left,
}
}
}
impl Text { impl Text {
/// Constructs a [`Text`] with a single section. /// Constructs a [`Text`] with a single section.
/// ///
/// ``` /// ```
/// # use bevy_asset::Handle; /// # use bevy_asset::Handle;
/// # use bevy_render::color::Color; /// # 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<Font> = Default::default(); /// # let font_handle: Handle<Font> = Default::default();
/// # /// #
@ -42,12 +54,12 @@ impl Text {
/// color: Color::WHITE, /// color: Color::WHITE,
/// }, /// },
/// ) // You can still add an alignment. /// ) // You can still add an alignment.
/// .with_alignment(TextAlignment::CENTER); /// .with_alignment(TextAlignment::Center);
/// ``` /// ```
pub fn from_section(value: impl Into<String>, style: TextStyle) -> Self { pub fn from_section(value: impl Into<String>, style: TextStyle) -> Self {
Self { Self {
sections: vec![TextSection::new(value, style)], sections: vec![TextSection::new(value, style)],
alignment: Default::default(), ..default()
} }
} }
@ -82,7 +94,7 @@ impl Text {
pub fn from_sections(sections: impl IntoIterator<Item = TextSection>) -> Self { pub fn from_sections(sections: impl IntoIterator<Item = TextSection>) -> Self {
Self { Self {
sections: sections.into_iter().collect(), 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. /// Describes horizontal alignment preference for positioning & bounds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize)] #[reflect(Serialize, Deserialize)]
pub enum HorizontalAlign { pub enum TextAlignment {
/// Leftmost character is immediately to the right of the render position.<br/> /// Leftmost character is immediately to the right of the render position.<br/>
/// Bounds start from the render position and advance rightwards. /// Bounds start from the render position and advance rightwards.
Left, Left,
@ -200,35 +144,12 @@ pub enum HorizontalAlign {
Right, Right,
} }
impl From<HorizontalAlign> for glyph_brush_layout::HorizontalAlign { impl From<TextAlignment> for glyph_brush_layout::HorizontalAlign {
fn from(val: HorizontalAlign) -> Self { fn from(val: TextAlignment) -> Self {
match val { match val {
HorizontalAlign::Left => glyph_brush_layout::HorizontalAlign::Left, TextAlignment::Left => glyph_brush_layout::HorizontalAlign::Left,
HorizontalAlign::Center => glyph_brush_layout::HorizontalAlign::Center, TextAlignment::Center => glyph_brush_layout::HorizontalAlign::Center,
HorizontalAlign::Right => glyph_brush_layout::HorizontalAlign::Right, TextAlignment::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<VerticalAlign> for glyph_brush_layout::VerticalAlign {
fn from(val: VerticalAlign) -> Self {
match val {
VerticalAlign::Top => glyph_brush_layout::VerticalAlign::Top,
VerticalAlign::Center => glyph_brush_layout::VerticalAlign::Center,
VerticalAlign::Bottom => glyph_brush_layout::VerticalAlign::Bottom,
} }
} }
} }

View File

@ -22,17 +22,10 @@ use bevy_utils::HashSet;
use bevy_window::{WindowId, WindowScaleFactorChanged, Windows}; use bevy_window::{WindowId, WindowScaleFactorChanged, Windows};
use crate::{ use crate::{
Font, FontAtlasSet, FontAtlasWarning, HorizontalAlign, Text, TextError, TextLayoutInfo, Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline,
TextPipeline, TextSettings, VerticalAlign, YAxisOrientation, 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. /// 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 /// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the
/// specified `TextAlignment`. /// specified `TextAlignment`.
@ -47,21 +40,27 @@ pub struct Text2dBounds {
} }
impl Default for Text2dBounds { impl Default for Text2dBounds {
#[inline]
fn default() -> Self { fn default() -> Self {
Self { Self::UNBOUNDED
size: Vec2::new(f32::MAX, f32::MAX),
}
} }
} }
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`. /// 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) /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs)
#[derive(Bundle, Clone, Debug, Default)] #[derive(Bundle, Clone, Debug, Default)]
pub struct Text2dBundle { pub struct Text2dBundle {
pub text: Text, pub text: Text,
pub text_anchor: Anchor,
pub transform: Transform, pub transform: Transform,
pub global_transform: GlobalTransform, pub global_transform: GlobalTransform,
pub text_2d_size: Text2dSize,
pub text_2d_bounds: Text2dBounds, pub text_2d_bounds: Text2dBounds,
pub visibility: Visibility, pub visibility: Visibility,
pub computed_visibility: ComputedVisibility, pub computed_visibility: ComputedVisibility,
@ -77,32 +76,23 @@ pub fn extract_text2d_sprite(
&ComputedVisibility, &ComputedVisibility,
&Text, &Text,
&TextLayoutInfo, &TextLayoutInfo,
&Anchor,
&GlobalTransform, &GlobalTransform,
&Text2dSize,
)>, )>,
>, >,
) { ) {
let scale_factor = windows.scale_factor(WindowId::primary()) as f32; 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() text2d_query.iter()
{ {
if !computed_visibility.is_visible() { if !computed_visibility.is_visible() {
continue; continue;
} }
let (width, height) = (calculated_size.size.x, calculated_size.size.y);
let text_glyphs = &text_layout_info.glyphs; let text_glyphs = &text_layout_info.glyphs;
let alignment_offset = match text.alignment.vertical { let text_anchor = anchor.as_vec() * Vec2::new(1., -1.) - 0.5;
VerticalAlign::Top => Vec3::new(0.0, -height, 0.0), let alignment_offset = text_layout_info.size * text_anchor;
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 mut color = Color::WHITE; let mut color = Color::WHITE;
let mut current_section = usize::MAX; let mut current_section = usize::MAX;
for text_glyph in text_glyphs { for text_glyph in text_glyphs {
@ -120,10 +110,9 @@ pub fn extract_text2d_sprite(
let index = text_glyph.atlas_info.glyph_index; let index = text_glyph.atlas_info.glyph_index;
let rect = Some(atlas.textures[index]); let rect = Some(atlas.textures[index]);
let glyph_transform = Transform::from_translation( let glyph_transform =
alignment_offset * scale_factor + text_glyph.position.extend(0.), Transform::from_translation((alignment_offset + text_glyph.position).extend(0.));
);
// NOTE: Should match `bevy_ui::render::extract_text_uinodes`
let transform = *text_transform let transform = *text_transform
* GlobalTransform::from_scale(Vec3::splat(scale_factor.recip())) * GlobalTransform::from_scale(Vec3::splat(scale_factor.recip()))
* glyph_transform; * glyph_transform;
@ -167,8 +156,7 @@ pub fn update_text2d_layout(
mut text_query: Query<( mut text_query: Query<(
Entity, Entity,
Ref<Text>, Ref<Text>,
Option<&Text2dBounds>, &Text2dBounds,
&mut Text2dSize,
Option<&mut TextLayoutInfo>, Option<&mut TextLayoutInfo>,
)>, )>,
) { ) {
@ -176,15 +164,12 @@ pub fn update_text2d_layout(
let factor_changed = scale_factor_changed.iter().last().is_some(); let factor_changed = scale_factor_changed.iter().last().is_some();
let scale_factor = windows.scale_factor(WindowId::primary()); 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) { if factor_changed || text.is_changed() || queue.remove(&entity) {
let text_bounds = match maybe_bounds { let text_bounds = Vec2::new(
Some(bounds) => Vec2::new( scale_value(bounds.size.x, scale_factor),
scale_value(bounds.size.x, scale_factor), scale_value(bounds.size.y, scale_factor),
scale_value(bounds.size.y, scale_factor), );
),
None => Vec2::new(f32::MAX, f32::MAX),
};
match text_pipeline.queue_text( match text_pipeline.queue_text(
&fonts, &fonts,
@ -207,18 +192,12 @@ 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) => { Ok(info) => match text_layout_info {
calculated_size.size = Vec2::new( Some(mut t) => *t = info,
scale_value(info.size.x, 1. / scale_factor), None => {
scale_value(info.size.y, 1. / scale_factor), commands.entity(entity).insert(info);
);
match text_layout_info {
Some(mut t) => *t = info,
None => {
commands.entity(entity).insert(info);
}
} }
} },
} }
} }
} }

View File

@ -33,7 +33,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::WHITE,
}; };
let text_alignment = TextAlignment::CENTER; let text_alignment = TextAlignment::Center;
// 2d camera // 2d camera
commands.spawn(Camera2dBundle::default()); commands.spawn(Camera2dBundle::default());
// Demonstrate changing translation // Demonstrate changing translation
@ -64,31 +64,29 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Demonstrate text wrapping // Demonstrate text wrapping
let box_size = Vec2::new(300.0, 200.0); let box_size = Vec2::new(300.0, 200.0);
let box_position = Vec2::new(0.0, -250.0); let box_position = Vec2::new(0.0, -250.0);
commands.spawn(SpriteBundle { commands
sprite: Sprite { .spawn(SpriteBundle {
color: Color::rgb(0.25, 0.25, 0.75), sprite: Sprite {
custom_size: Some(Vec2::new(box_size.x, box_size.y)), 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() ..default()
}, })
transform: Transform::from_translation(box_position.extend(0.0)), .with_children(|builder| {
..default() builder.spawn(Text2dBundle {
}); text: Text::from_section("this text wraps in the box", text_style)
commands.spawn(Text2dBundle { .with_alignment(TextAlignment::Left),
text: Text::from_section("this text wraps in the box", text_style), text_2d_bounds: Text2dBounds {
text_2d_bounds: Text2dBounds { // Wrap text in the rectangle
// Wrap text in the rectangle size: box_size,
size: box_size, },
}, // ensure the text is drawn on top of the box
// We align text to the top-left, so this transform is the top-left corner of our text. The transform: Transform::from_translation(Vec3::Z),
// box is centered at box_position, so it is necessary to move by half of the box size to ..default()
// 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()
});
} }
fn animate_translation( fn animate_translation(

View File

@ -66,7 +66,7 @@ fn spawn_text(
for (per_frame, event) in reader.iter().enumerate() { for (per_frame, event) in reader.iter().enumerate() {
commands.spawn(Text2dBundle { commands.spawn(Text2dBundle {
text: Text::from_section(event.0.to_string(), text_style.clone()) text: Text::from_section(event.0.to_string(), text_style.clone())
.with_alignment(TextAlignment::CENTER), .with_alignment(TextAlignment::Center),
transform: Transform::from_xyz( transform: Transform::from_xyz(
per_frame as f32 * 100.0 + rand::thread_rng().gen_range(-40.0..40.0), per_frame as f32 * 100.0 + rand::thread_rng().gen_range(-40.0..40.0),
300.0, 300.0,

View File

@ -121,7 +121,7 @@ fn setup_scene(
color: Color::BLACK, color: Color::BLACK,
}, },
) )
.with_text_alignment(TextAlignment::CENTER), .with_text_alignment(TextAlignment::Center),
); );
}); });
} }

View File

@ -7,7 +7,7 @@ use bevy::{
GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent, GamepadSettings, GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent, GamepadSettings,
}, },
prelude::*, prelude::*,
sprite::{MaterialMesh2dBundle, Mesh2dHandle}, sprite::{Anchor, MaterialMesh2dBundle, Mesh2dHandle},
}; };
const BUTTON_RADIUS: f32 = 25.; const BUTTON_RADIUS: f32 = 25.;
@ -342,8 +342,8 @@ fn setup_sticks(
value: format!("{:.3}", 0.), value: format!("{:.3}", 0.),
style, style,
}, },
]) ]),
.with_alignment(TextAlignment::BOTTOM_CENTER), text_anchor: Anchor::BottomCenter,
..default() ..default()
}, },
TextWithAxes { x_axis, y_axis }, TextWithAxes { x_axis, y_axis },
@ -409,8 +409,7 @@ fn setup_triggers(
font_size: 16., font_size: 16.,
color: TEXT_COLOR, color: TEXT_COLOR,
}, },
) ),
.with_alignment(TextAlignment::CENTER),
..default() ..default()
}, },
TextWithButtonValue(button_type), TextWithButtonValue(button_type),

View File

@ -41,7 +41,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
color: Color::WHITE, color: Color::WHITE,
}, },
) // Set the alignment of the Text ) // Set the alignment of the Text
.with_text_alignment(TextAlignment::TOP_CENTER) .with_text_alignment(TextAlignment::Center)
// Set the style of the TextBundle itself. // Set the style of the TextBundle itself.
.with_style(Style { .with_style(Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,

View File

@ -54,7 +54,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
color: Color::rgb(0.8, 0.2, 0.7), color: Color::rgb(0.8, 0.2, 0.7),
}, },
) )
.with_text_alignment(TextAlignment::CENTER) .with_text_alignment(TextAlignment::Center)
.with_style(Style { .with_style(Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
position: UiRect { position: UiRect {