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).
/// It defaults to `Anchor::Center`.
#[derive(Debug, Clone, Default, Reflect)]
#[derive(Component, Debug, Clone, Default, Reflect)]
#[doc(alias = "pivot")]
pub enum Anchor {
#[default]

View File

@ -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)
}

View File

@ -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::<TextSection>()
.register_type::<Vec<TextSection>>()
.register_type::<TextStyle>()
.register_type::<Text>()
.register_type::<TextAlignment>()
.register_type::<VerticalAlign>()
.register_type::<HorizontalAlign>()
.init_asset_loader::<FontLoader>()
.init_resource::<TextSettings>()
.init_resource::<FontAtlasWarning>()

View File

@ -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<TextSection>,
/// 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<Font> = 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<String>, 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<Item = TextSection>) -> 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.<br/>
/// Bounds start from the render position and advance rightwards.
Left,
@ -200,35 +144,12 @@ pub enum HorizontalAlign {
Right,
}
impl From<HorizontalAlign> for glyph_brush_layout::HorizontalAlign {
fn from(val: HorizontalAlign) -> Self {
impl From<TextAlignment> for glyph_brush_layout::HorizontalAlign {
fn from(val: TextAlignment) -> 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<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,
TextAlignment::Left => glyph_brush_layout::HorizontalAlign::Left,
TextAlignment::Center => glyph_brush_layout::HorizontalAlign::Center,
TextAlignment::Right => glyph_brush_layout::HorizontalAlign::Right,
}
}
}

View File

@ -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<Text>,
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);
}
}
},
}
}
}

View File

@ -33,7 +33,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
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<AssetServer>) {
// 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(

View File

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

View File

@ -121,7 +121,7 @@ fn setup_scene(
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,
},
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),

View File

@ -41,7 +41,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
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,

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),
},
)
.with_text_alignment(TextAlignment::CENTER)
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {