Improve ergonomics and reduce boilerplate around creating text elements. (#5343)

# Objective

Creating UI elements is very boilerplate-y with lots of indentation.
This PR aims to reduce boilerplate around creating text elements.

## Changelog

* Renamed `Text::with_section` to `from_section`.
  It no longer takes a `TextAlignment` as argument, as the vast majority of cases left it `Default::default()`.
* Added `Text::from_sections` which creates a `Text` from a list of `TextSections`.
  Reduces line-count and reduces indentation by one level.
* Added `Text::with_alignment`.
  A builder style method for setting the `TextAlignment` of a `Text`.
* Added `TextSection::new`.
  Does not reduce line count, but reduces character count and made it easier to read. No more `.to_string()` calls!
* Added `TextSection::from_style` which creates an empty `TextSection` with a style.
  No more empty strings! Reduces indentation.
* Added `TextAlignment::CENTER` and friends.
* Added methods to `TextBundle`. `from_section`, `from_sections`, `with_text_alignment` and `with_style`.

## Note for reviewers.
Because of the nature of these changes I recommend setting diff view to 'split'.
~~Look for the book icon~~ cog in the top-left of the Files changed tab.

Have fun reviewing ❤️
<sup> >:D </sup>

## Migration Guide

`Text::with_section` was renamed to `from_section` and no longer takes a `TextAlignment` as argument.
Use `with_alignment` to set the alignment instead.

Co-authored-by: devil-ira <justthecooldude@gmail.com>
This commit is contained in:
ira 2022-07-20 14:14:29 +00:00
parent 35f99a6ccc
commit 9f906fdc8b
22 changed files with 618 additions and 677 deletions

View File

@ -522,7 +522,7 @@ mod tests {
Err(error) => error,
};
assert_eq!(
format!("{}", error),
error.to_string(),
"cannot convert VertexAttributeValues::Uint32x4 to alloc::vec::Vec<u32>"
);
assert_eq!(format!("{:?}", error),

View File

@ -14,54 +14,83 @@ pub struct Text {
}
impl Text {
/// Constructs a [`Text`] with (initially) one section.
/// Constructs a [`Text`] with a single section.
///
/// ```
/// # use bevy_asset::{AssetServer, Handle};
/// # use bevy_asset::Handle;
/// # use bevy_render::color::Color;
/// # use bevy_text::{Font, Text, TextAlignment, TextStyle, HorizontalAlign, VerticalAlign};
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// #
/// // basic usage
/// let hello_world = Text::with_section(
/// "hello world!".to_string(),
/// // Basic usage.
/// let hello_world = Text::from_section(
/// // Accepts a String or any type that converts into a String, such as &str.
/// "hello world!",
/// TextStyle {
/// font: font_handle.clone(),
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// TextAlignment {
/// vertical: VerticalAlign::Center,
/// horizontal: HorizontalAlign::Center,
/// },
/// );
///
/// let hello_bevy = Text::with_section(
/// // accepts a String or any type that converts into a String, such as &str
/// let hello_bevy = Text::from_section(
/// "hello bevy!",
/// TextStyle {
/// font: font_handle,
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// // you can still use Default
/// Default::default(),
/// );
/// ) // You can still add an alignment.
/// .with_alignment(TextAlignment::CENTER);
/// ```
pub fn with_section<S: Into<String>>(
value: S,
style: TextStyle,
alignment: TextAlignment,
) -> Self {
pub fn from_section(value: impl Into<String>, style: TextStyle) -> Self {
Self {
sections: vec![TextSection {
value: value.into(),
style,
}],
alignment,
sections: vec![TextSection::new(value, style)],
alignment: Default::default(),
}
}
/// Constructs a [`Text`] from a list of sections.
///
/// ```
/// # use bevy_asset::Handle;
/// # use bevy_render::color::Color;
/// # use bevy_text::{Font, Text, TextStyle, TextSection};
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// #
/// let hello_world = Text::from_sections([
/// TextSection::new(
/// "Hello, ",
/// TextStyle {
/// font: font_handle.clone(),
/// font_size: 60.0,
/// color: Color::BLUE,
/// },
/// ),
/// TextSection::new(
/// "World!",
/// TextStyle {
/// font: font_handle,
/// font_size: 60.0,
/// color: Color::RED,
/// },
/// ),
/// ]);
/// ```
pub fn from_sections(sections: impl IntoIterator<Item = TextSection>) -> Self {
Self {
sections: sections.into_iter().collect(),
alignment: Default::default(),
}
}
/// Returns this [`Text`] with a new [`TextAlignment`].
pub const fn with_alignment(mut self, alignment: TextAlignment) -> Self {
self.alignment = alignment;
self
}
}
#[derive(Debug, Default, Clone, FromReflect, Reflect)]
@ -70,18 +99,89 @@ pub struct TextSection {
pub style: TextStyle,
}
impl TextSection {
/// Create a new [`TextSection`].
pub fn new(value: impl Into<String>, style: TextStyle) -> Self {
Self {
value: value.into(),
style,
}
}
/// Create an empty [`TextSection`] from a style. Useful when the value will be set dynamically.
pub const fn from_style(style: TextStyle) -> Self {
Self {
value: String::new(),
style,
}
}
}
#[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 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 {
vertical: VerticalAlign::Top,
horizontal: HorizontalAlign::Left,
}
TextAlignment::TOP_LEFT
}
}

View File

@ -13,7 +13,7 @@ use bevy_render::{
camera::Camera, extract_component::ExtractComponent, prelude::ComputedVisibility,
view::Visibility,
};
use bevy_text::Text;
use bevy_text::{Text, TextAlignment, TextSection, TextStyle};
use bevy_transform::prelude::{GlobalTransform, Transform};
/// The basic UI node
@ -89,6 +89,40 @@ pub struct TextBundle {
pub computed_visibility: ComputedVisibility,
}
impl TextBundle {
/// Create a [`TextBundle`] from a single section.
///
/// See [`Text::from_section`] for usage.
pub fn from_section(value: impl Into<String>, style: TextStyle) -> Self {
Self {
text: Text::from_section(value, style),
..Default::default()
}
}
/// Create a [`TextBundle`] from a list of sections.
///
/// See [`Text::from_sections`] for usage.
pub fn from_sections(sections: impl IntoIterator<Item = TextSection>) -> Self {
Self {
text: Text::from_sections(sections),
..Default::default()
}
}
/// Returns this [`TextBundle`] with a new [`TextAlignment`] on [`Text`].
pub const fn with_text_alignment(mut self, alignment: TextAlignment) -> Self {
self.text.alignment = alignment;
self
}
/// Returns this [`TextBundle`] with a new [`Style`].
pub const fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl Default for TextBundle {
fn default() -> Self {
TextBundle {

View File

@ -31,30 +31,28 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
font_size: 60.0,
color: Color::WHITE,
};
let text_alignment = TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
};
let text_alignment = TextAlignment::CENTER;
// 2d camera
commands.spawn_bundle(Camera2dBundle::default());
// Demonstrate changing translation
commands
.spawn_bundle(Text2dBundle {
text: Text::with_section("translation", text_style.clone(), text_alignment),
text: Text::from_section("translation", text_style.clone())
.with_alignment(text_alignment),
..default()
})
.insert(AnimateTranslation);
// Demonstrate changing rotation
commands
.spawn_bundle(Text2dBundle {
text: Text::with_section("rotation", text_style.clone(), text_alignment),
text: Text::from_section("rotation", text_style.clone()).with_alignment(text_alignment),
..default()
})
.insert(AnimateRotation);
// Demonstrate changing scale
commands
.spawn_bundle(Text2dBundle {
text: Text::with_section("scale", text_style.clone(), text_alignment),
text: Text::from_section("scale", text_style.clone()).with_alignment(text_alignment),
..default()
})
.insert(AnimateScale);
@ -70,16 +68,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
transform: Transform::from_translation(box_position.extend(0.0)),
..default()
});
let text_alignment_topleft = TextAlignment {
vertical: VerticalAlign::Top,
horizontal: HorizontalAlign::Left,
};
commands.spawn_bundle(Text2dBundle {
text: Text::with_section(
"this text wraps in the box",
text_style,
text_alignment_topleft,
),
text: Text::from_section("this text wraps in the box", text_style),
text_2d_bounds: Text2dBounds {
// Wrap text in the rectangle
size: box_size,

View File

@ -62,13 +62,11 @@ fn spawn_text(
font_size: 20.0,
color: Color::WHITE,
};
let text_alignment = TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
};
for (per_frame, event) in reader.iter().enumerate() {
commands.spawn_bundle(Text2dBundle {
text: Text::with_section(format!("{}", event.0), text_style.clone(), text_alignment),
text: Text::from_section(event.0.to_string(), text_style.clone())
.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

@ -55,18 +55,14 @@ fn setup_menu(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Play",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
},
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"Play",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
},
));
})
.id();
commands.insert_resource(MenuData { button_entity });

View File

@ -150,17 +150,16 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMu
game.bonus.handle = asset_server.load("models/AlienCake/cakeBirthday.glb#Scene0");
// scoreboard
commands.spawn_bundle(TextBundle {
text: Text::with_section(
commands.spawn_bundle(
TextBundle::from_section(
"Score:",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.5, 0.5, 1.0),
},
Default::default(),
),
style: Style {
)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
top: Val::Px(5.0),
@ -168,9 +167,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMu
..default()
},
..default()
},
..default()
});
}),
);
}
// remove all entities that are not a camera
@ -383,17 +381,13 @@ fn display_score(mut commands: Commands, asset_server: Res<AssetServer>, game: R
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
format!("Cake eaten: {}", game.cake_eaten),
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 80.0,
color: Color::rgb(0.5, 0.5, 1.0),
},
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
format!("Cake eaten: {}", game.cake_eaten),
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 80.0,
color: Color::rgb(0.5, 0.5, 1.0),
},
));
});
}

View File

@ -216,29 +216,23 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.insert(Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED));
// Scoreboard
commands.spawn_bundle(TextBundle {
text: Text {
sections: vec![
TextSection {
value: "Score: ".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: SCOREBOARD_FONT_SIZE,
color: TEXT_COLOR,
},
commands.spawn_bundle(
TextBundle::from_sections([
TextSection::new(
"Score: ",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: SCOREBOARD_FONT_SIZE,
color: TEXT_COLOR,
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: SCOREBOARD_FONT_SIZE,
color: SCORE_COLOR,
},
},
],
..default()
},
style: Style {
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: SCOREBOARD_FONT_SIZE,
color: SCORE_COLOR,
}),
])
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
top: SCOREBOARD_TEXT_PADDING,
@ -246,9 +240,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
..default()
});
}),
);
// Walls
commands.spawn_bundle(WallBundle::new(WallLocation::Left));
@ -351,7 +344,7 @@ fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) {
fn update_scoreboard(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
let mut text = query.single_mut();
text.sections[1].value = format!("{}", scoreboard.score);
text.sections[1].value = scoreboard.score.to_string();
}
fn check_for_collisions(

View File

@ -134,37 +134,27 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(Camera2dBundle::default());
commands
.spawn()
.insert(ContributorDisplay)
.insert_bundle(TextBundle {
style: Style {
align_self: AlignSelf::FlexEnd,
..default()
},
text: Text {
sections: vec![
TextSection {
value: "Contributor showcase".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
},
},
],
..default()
},
commands.spawn().insert(ContributorDisplay).insert_bundle(
TextBundle::from_sections([
TextSection::new(
"Contributor showcase",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
},
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
}),
])
.with_style(Style {
align_self: AlignSelf::FlexEnd,
..default()
});
}),
);
}
/// Finds the next contributor to display and selects the entity

View File

@ -165,58 +165,52 @@ mod game {
.insert(OnGameScreen)
.with_children(|parent| {
// Display two lines of text, the second one with the current settings
parent.spawn_bundle(TextBundle {
style: Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
},
text: Text::with_section(
parent.spawn_bundle(
TextBundle::from_section(
"Will be back to the menu shortly...",
TextStyle {
font: font.clone(),
font_size: 80.0,
color: TEXT_COLOR,
},
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle {
style: Style {
)
.with_style(Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
},
text: Text {
sections: vec![
TextSection {
value: format!("quality: {:?}", *display_quality),
style: TextStyle {
font: font.clone(),
font_size: 60.0,
color: Color::BLUE,
},
}),
);
parent.spawn_bundle(
TextBundle::from_sections([
TextSection::new(
format!("quality: {:?}", *display_quality),
TextStyle {
font: font.clone(),
font_size: 60.0,
color: Color::BLUE,
},
TextSection {
value: " - ".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 60.0,
color: TEXT_COLOR,
},
),
TextSection::new(
" - ",
TextStyle {
font: font.clone(),
font_size: 60.0,
color: TEXT_COLOR,
},
TextSection {
value: format!("volume: {:?}", *volume),
style: TextStyle {
font: font.clone(),
font_size: 60.0,
color: Color::GREEN,
},
),
TextSection::new(
format!("volume: {:?}", *volume),
TextStyle {
font: font.clone(),
font_size: 60.0,
color: Color::GREEN,
},
],
),
])
.with_style(Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
},
..default()
});
}),
);
});
// Spawn a 5 seconds timer to trigger going back to the menu
commands.insert_resource(GameTimer(Timer::from_seconds(5.0, false)));
@ -432,22 +426,20 @@ mod menu {
.insert(OnMainMenuScreen)
.with_children(|parent| {
// Display the game name
parent.spawn_bundle(TextBundle {
style: Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
},
text: Text::with_section(
parent.spawn_bundle(
TextBundle::from_section(
"Bevy Game Menu UI",
TextStyle {
font: font.clone(),
font_size: 80.0,
color: TEXT_COLOR,
},
Default::default(),
),
..default()
});
)
.with_style(Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
}),
);
// Display three buttons for each action available from the main menu:
// - new game
@ -467,14 +459,10 @@ mod menu {
image: UiImage(icon),
..default()
});
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"New Game",
button_text_style.clone(),
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"New Game",
button_text_style.clone(),
));
});
parent
.spawn_bundle(ButtonBundle {
@ -490,14 +478,10 @@ mod menu {
image: UiImage(icon),
..default()
});
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Settings",
button_text_style.clone(),
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"Settings",
button_text_style.clone(),
));
});
parent
.spawn_bundle(ButtonBundle {
@ -513,10 +497,7 @@ mod menu {
image: UiImage(icon),
..default()
});
parent.spawn_bundle(TextBundle {
text: Text::with_section("Quit", button_text_style, Default::default()),
..default()
});
parent.spawn_bundle(TextBundle::from_section("Quit", button_text_style));
});
});
}
@ -529,6 +510,7 @@ mod menu {
align_items: AlignItems::Center,
..default()
};
let button_text_style = TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
@ -548,55 +530,25 @@ mod menu {
})
.insert(OnSettingsMenuScreen)
.with_children(|parent| {
// Display two buttons for the submenus
parent
.spawn_bundle(ButtonBundle {
style: button_style.clone(),
color: NORMAL_BUTTON.into(),
..default()
})
.insert(MenuButtonAction::SettingsDisplay)
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Display",
for (action, text) in [
(MenuButtonAction::SettingsDisplay, "Display"),
(MenuButtonAction::SettingsSound, "Sound"),
(MenuButtonAction::BackToMainMenu, "Back"),
] {
parent
.spawn_bundle(ButtonBundle {
style: button_style.clone(),
color: NORMAL_BUTTON.into(),
..default()
})
.insert(action)
.with_children(|parent| {
parent.spawn_bundle(TextBundle::from_section(
text,
button_text_style.clone(),
Default::default(),
),
..default()
));
});
});
parent
.spawn_bundle(ButtonBundle {
style: button_style.clone(),
color: NORMAL_BUTTON.into(),
..default()
})
.insert(MenuButtonAction::SettingsSound)
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Sound",
button_text_style.clone(),
Default::default(),
),
..default()
});
});
// Display the back button to return to the main menu screen
parent
.spawn_bundle(ButtonBundle {
style: button_style,
color: NORMAL_BUTTON.into(),
..default()
})
.insert(MenuButtonAction::BackToMainMenu)
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section("Back", button_text_style, Default::default()),
..default()
});
});
}
});
}
@ -644,14 +596,10 @@ mod menu {
})
.with_children(|parent| {
// Display a label for the current setting
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Display Quality",
button_text_style.clone(),
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"Display Quality",
button_text_style.clone(),
));
// Display a button for each possible value
for quality_setting in [
DisplayQuality::Low,
@ -667,14 +615,10 @@ mod menu {
..default()
});
entity.insert(quality_setting).with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
format!("{:?}", quality_setting),
button_text_style.clone(),
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
format!("{quality_setting:?}"),
button_text_style.clone(),
));
});
if *display_quality == quality_setting {
entity.insert(SelectedOption);
@ -690,10 +634,7 @@ mod menu {
})
.insert(MenuButtonAction::BackToSettings)
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section("Back", button_text_style, Default::default()),
..default()
});
parent.spawn_bundle(TextBundle::from_section("Back", button_text_style));
});
});
}
@ -739,14 +680,10 @@ mod menu {
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Volume",
button_text_style.clone(),
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"Volume",
button_text_style.clone(),
));
for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
let mut entity = parent.spawn_bundle(ButtonBundle {
style: Style {
@ -770,10 +707,7 @@ mod menu {
})
.insert(MenuButtonAction::BackToSettings)
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section("Back", button_text_style, Default::default()),
..default()
});
parent.spawn_bundle(TextBundle::from_section("Back", button_text_style));
});
});
}

View File

@ -108,23 +108,17 @@ fn setup_scene(
..default()
})
.with_children(|b| {
b.spawn_bundle(TextBundle {
text: Text {
sections: vec![TextSection {
value: "Test Button".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::BLACK,
},
}],
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
b.spawn_bundle(
TextBundle::from_section(
"Test Button",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::BLACK,
},
},
..default()
});
)
.with_text_alignment(TextAlignment::CENTER),
);
});
}

View File

@ -105,20 +105,18 @@ fn save_scene_system(world: &mut World) {
// text example.
fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(Camera2dBundle::default());
commands.spawn_bundle(TextBundle {
style: Style {
align_self: AlignSelf::FlexEnd,
..default()
},
text: Text::with_section(
commands.spawn_bundle(
TextBundle::from_section(
"Nothing to see in this window! Check the console output!",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::WHITE,
},
Default::default(),
),
..default()
});
)
.with_style(Style {
align_self: AlignSelf::FlexEnd,
..default()
}),
);
}

View File

@ -96,45 +96,36 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(Camera2dBundle::default());
commands
.spawn_bundle(TextBundle {
text: Text {
sections: vec![
TextSection {
value: "Bird Count: ".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 0.0),
},
.spawn_bundle(
TextBundle::from_sections([
TextSection::new(
"Bird Count: ",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 0.0),
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
},
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
}),
TextSection::new(
"\nAverage FPS: ",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 0.0),
},
TextSection {
value: "\nAverage FPS: ".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 0.0),
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
},
},
],
..default()
},
style: Style {
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
}),
])
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
top: Val::Px(5.0),
@ -142,9 +133,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
..default()
})
}),
)
.insert(StatsText);
commands.insert_resource(BirdTexture(texture));
@ -265,12 +255,12 @@ fn counter_system(
let mut text = query.single_mut();
if counter.is_changed() {
text.sections[1].value = format!("{}", counter.count);
text.sections[1].value = counter.count.to_string();
}
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
text.sections[3].value = format!("{:.2}", average);
text.sections[3].value = format!("{average:.2}");
}
};
}

View File

@ -101,25 +101,18 @@ fn setup(
});
// Add text to explain inputs and what is happening.
commands.spawn_bundle(TextBundle {
text: Text::with_section(
"Press the arrow keys to move the cubes. Toggle movement for yellow (1), red (2) and green (3) cubes via number keys.
commands.spawn_bundle(TextBundle::from_section(
"Press the arrow keys to move the cubes. Toggle movement for yellow (1), red (2) and green (3) cubes via number keys.
Notice how the green cube will translate further in respect to the yellow in contrast to the red cube.
This is due to the use of its LocalTransform that is relative to the yellow cubes transform instead of the GlobalTransform as in the case of the red cube.
The red cube is moved through its GlobalTransform and thus is unaffected by the yellows transform.",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 22.0,
color: Color::WHITE,
},
TextAlignment {
horizontal: HorizontalAlign::Left,
..default()
},
),
..default()
});
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 22.0,
color: Color::WHITE,
},
));
}
// This system will move all cubes that are marked as ChangeGlobal according to their global transform.

View File

@ -62,17 +62,13 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Button",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
},
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"Button",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
},
));
});
}

View File

@ -67,8 +67,9 @@ fn text_update_system(mut state: ResMut<State>, time: Res<Time>, mut query: Quer
if state.timer.tick(time.delta()).finished() {
for mut text in &mut query {
let c = rand::random::<u8>() as char;
if !text.sections[0].value.contains(c) {
text.sections[0].value.push(c);
let string = &mut text.sections[0].value;
if !string.contains(c) {
string.push(c);
}
}
@ -80,16 +81,12 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut state: ResM
let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");
state.handle = font_handle.clone();
commands.spawn_bundle(Camera2dBundle::default());
commands.spawn_bundle(TextBundle {
text: Text::with_section(
"a",
TextStyle {
font: font_handle,
font_size: 60.0,
color: Color::YELLOW,
},
Default::default(),
),
..default()
});
commands.spawn_bundle(TextBundle::from_section(
"a",
TextStyle {
font: font_handle,
font_size: 60.0,
color: Color::YELLOW,
},
));
}

View File

@ -31,8 +31,20 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(Camera2dBundle::default());
// Text with one section
commands
.spawn_bundle(TextBundle {
style: Style {
.spawn_bundle(
// Create a TextBundle that has a Text with a single section.
TextBundle::from_section(
// Accepts a `String` or any type that converts into a `String`, such as `&str`
"hello\nbevy!",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 100.0,
color: Color::WHITE,
},
) // Set the alignment of the Text
.with_text_alignment(TextAlignment::TOP_CENTER)
// Set the style of the TextBundle itself.
.with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: UiRect {
@ -41,76 +53,41 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
// Use the `Text::with_section` constructor
text: Text::with_section(
// Accepts a `String` or any type that converts into a `String`, such as `&str`
"hello\nbevy!",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 100.0,
color: Color::WHITE,
},
// Note: You can use `Default::default()` in place of the `TextAlignment`
TextAlignment {
horizontal: HorizontalAlign::Center,
..default()
},
),
..default()
})
}),
)
.insert(ColorText);
// Rich text with multiple sections
// Text with multiple sections
commands
.spawn_bundle(TextBundle {
style: Style {
.spawn_bundle(
// Create a TextBundle that has a Text with a list of sections.
TextBundle::from_sections([
TextSection::new(
"FPS: ",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
},
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 60.0,
color: Color::GOLD,
}),
])
.with_style(Style {
align_self: AlignSelf::FlexEnd,
..default()
},
// Use `Text` directly
text: Text {
// Construct a `Vec` of `TextSection`s
sections: vec![
TextSection {
value: "FPS: ".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
color: Color::WHITE,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 60.0,
color: Color::GOLD,
},
},
],
..default()
},
..default()
})
}),
)
.insert(FpsText);
}
fn text_update_system(diagnostics: Res<Diagnostics>, mut query: Query<&mut Text, With<FpsText>>) {
for mut text in &mut query {
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
// Update the value of the second section
text.sections[1].value = format!("{:.2}", average);
}
}
}
}
fn text_color_system(time: Res<Time>, mut query: Query<&mut Text, With<ColorText>>) {
for mut text in &mut query {
let seconds = time.seconds_since_startup() as f32;
// We used the `Text::with_section` helper method, but it is still just a `Text`,
// so to update it, we are still updating the one and only section
// Update the color of the first and only section.
text.sections[0].style.color = Color::Rgba {
red: (1.25 * seconds).sin() / 2.0 + 0.5,
green: (0.75 * seconds).sin() / 2.0 + 0.5,
@ -119,3 +96,14 @@ fn text_color_system(time: Res<Time>, mut query: Query<&mut Text, With<ColorText
};
}
}
fn text_update_system(diagnostics: Res<Diagnostics>, mut query: Query<&mut Text, With<FpsText>>) {
for mut text in &mut query {
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
// Update the value of the second section
text.sections[1].value = format!("{average:.2}");
}
}
}
}

View File

@ -25,8 +25,16 @@ struct TextChanges;
fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
commands.spawn_bundle(Camera2dBundle::default());
commands.spawn_bundle(TextBundle {
style: Style {
commands.spawn_bundle(
TextBundle::from_section(
"This is\ntext with\nline breaks\nin the top left",
TextStyle {
font: font.clone(),
font_size: 50.0,
color: Color::WHITE,
},
)
.with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: UiRect {
@ -35,20 +43,18 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
text: Text::with_section(
"This is\ntext with\nline breaks\nin the top left",
}),
);
commands.spawn_bundle(TextBundle::from_section(
"This text is very long, has a limited width, is centred, is positioned in the top right and is also coloured pink.",
TextStyle {
font: font.clone(),
font_size: 50.0,
color: Color::WHITE,
color: Color::rgb(0.8, 0.2, 0.7),
},
Default::default(),
),
..default()
});
commands.spawn_bundle(TextBundle {
style: Style {
)
.with_text_alignment(TextAlignment::CENTER)
.with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: UiRect {
@ -61,24 +67,55 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
height: Val::Undefined,
},
..default()
},
text: Text::with_section(
"This text is very long, has a limited width, is centred, is positioned in the top right and is also coloured pink.",
TextStyle {
font: font.clone(),
font_size: 50.0,
color: Color::rgb(0.8, 0.2, 0.7),
},
TextAlignment {
horizontal: HorizontalAlign::Center,
vertical: VerticalAlign::Center,
},
),
..default()
});
})
);
commands
.spawn_bundle(TextBundle {
style: Style {
.spawn_bundle(
TextBundle::from_sections([
TextSection::new(
"This text changes in the bottom right",
TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::WHITE,
},
),
TextSection::new(
"\nThis text changes in the bottom right - ",
TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::RED,
},
),
TextSection::from_style(TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::ORANGE_RED,
}),
TextSection::new(
" fps, ",
TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::YELLOW,
},
),
TextSection::from_style(TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::GREEN,
}),
TextSection::new(
" ms/frame",
TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::BLUE,
},
),
])
.with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: UiRect {
@ -87,65 +124,19 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
text: Text {
sections: vec![
TextSection {
value: "This text changes in the bottom right".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::WHITE,
},
},
TextSection {
value: "\nThis text changes in the bottom right - ".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::RED,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::ORANGE_RED,
},
},
TextSection {
value: " fps, ".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::YELLOW,
},
},
TextSection {
value: "".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::GREEN,
},
},
TextSection {
value: " ms/frame".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::BLUE,
},
},
],
alignment: Default::default(),
},
..default()
})
}),
)
.insert(TextChanges);
commands.spawn_bundle(TextBundle {
style: Style {
commands.spawn_bundle(
TextBundle::from_section(
"This\ntext has\nline breaks and also a set width in the bottom left",
TextStyle {
font,
font_size: 50.0,
color: Color::WHITE,
},
)
.with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
position: UiRect {
@ -158,18 +149,8 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
text: Text::with_section(
"This\ntext has\nline breaks and also a set width in the bottom left".to_string(),
TextStyle {
font,
font_size: 50.0,
color: Color::WHITE,
},
Default::default(),
),
..default()
});
}),
);
}
fn change_text_system(

View File

@ -29,19 +29,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Button 1",
TextStyle {
font: font_handle.clone(),
font_size: 40.0,
// Alpha channel of the color controls transparency.
color: Color::rgba(1.0, 1.0, 1.0, 0.2),
},
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"Button 1",
TextStyle {
font: font_handle.clone(),
font_size: 40.0,
// Alpha channel of the color controls transparency.
color: Color::rgba(1.0, 1.0, 1.0, 0.2),
},
));
});
// Button with a different color,
@ -59,18 +55,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section(
"Button 2",
TextStyle {
font: font_handle.clone(),
font_size: 40.0,
// Alpha channel of the color controls transparency.
color: Color::rgba(1.0, 1.0, 1.0, 0.2),
},
Default::default(),
),
..default()
});
parent.spawn_bundle(TextBundle::from_section(
"Button 2",
TextStyle {
font: font_handle.clone(),
font_size: 40.0,
// Alpha channel of the color controls transparency.
color: Color::rgba(1.0, 1.0, 1.0, 0.2),
},
));
});
}

View File

@ -57,22 +57,20 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
})
.with_children(|parent| {
// text
parent.spawn_bundle(TextBundle {
style: Style {
margin: UiRect::all(Val::Px(5.0)),
..default()
},
text: Text::with_section(
parent.spawn_bundle(
TextBundle::from_section(
"Text Example",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::WHITE,
},
Default::default(),
),
..default()
});
)
.with_style(Style {
margin: UiRect::all(Val::Px(5.0)),
..default()
}),
);
});
});
// right vertical fill
@ -89,8 +87,16 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
})
.with_children(|parent| {
// Title
parent.spawn_bundle(TextBundle {
style: Style {
parent.spawn_bundle(
TextBundle::from_section(
"Scrolling list",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 25.,
color: Color::WHITE,
},
)
.with_style(Style {
size: Size::new(Val::Undefined, Val::Px(25.)),
margin: UiRect {
left: Val::Auto,
@ -98,18 +104,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
text: Text::with_section(
"Scrolling list",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 25.,
color: Color::WHITE,
},
Default::default(),
),
..default()
});
}),
);
// List with hidden overflow
parent
.spawn_bundle(NodeBundle {
@ -140,8 +136,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.with_children(|parent| {
// List items
for i in 0..30 {
parent.spawn_bundle(TextBundle {
style: Style {
parent.spawn_bundle(
TextBundle::from_section(
format!("Item {i}"),
TextStyle {
font: asset_server
.load("fonts/FiraSans-Bold.ttf"),
font_size: 20.,
color: Color::WHITE,
},
)
.with_style(Style {
flex_shrink: 0.,
size: Size::new(Val::Undefined, Val::Px(20.)),
margin: UiRect {
@ -150,19 +155,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
..default()
},
text: Text::with_section(
format!("Item {}", i),
TextStyle {
font: asset_server
.load("fonts/FiraSans-Bold.ttf"),
font_size: 20.,
color: Color::WHITE,
},
Default::default(),
),
..default()
});
}),
);
}
});
});

View File

@ -131,8 +131,9 @@ pub(crate) mod test_setup {
ExampleMode::Application => "desktop_app(), reactive",
ExampleMode::ApplicationWithRedraw => "desktop_app(), reactive, RequestRedraw sent",
};
query.get_single_mut().unwrap().sections[1].value = mode.to_string();
query.get_single_mut().unwrap().sections[3].value = format!("{}", *frame);
let mut text = query.single_mut();
text.sections[1].value = mode.to_string();
text.sections[3].value = frame.to_string();
}
/// Set up a scene with a cube and some text
@ -165,8 +166,36 @@ pub(crate) mod test_setup {
});
event.send(RequestRedraw);
commands
.spawn_bundle(TextBundle {
style: Style {
.spawn_bundle(
TextBundle::from_sections([
TextSection::new(
"Press spacebar to cycle modes\n",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::WHITE,
},
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::GREEN,
}),
TextSection::new(
"\nFrame: ",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::YELLOW,
},
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::YELLOW,
}),
])
.with_style(Style {
align_self: AlignSelf::FlexStart,
position_type: PositionType::Absolute,
position: UiRect {
@ -175,46 +204,8 @@ pub(crate) mod test_setup {
..default()
},
..default()
},
text: Text {
sections: vec![
TextSection {
value: "Press spacebar to cycle modes\n".into(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::WHITE,
},
},
TextSection {
value: "".into(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::GREEN,
},
},
TextSection {
value: "\nFrame: ".into(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::YELLOW,
},
},
TextSection {
value: "".into(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
color: Color::YELLOW,
},
},
],
alignment: TextAlignment::default(),
},
..default()
})
}),
)
.insert(ModeText);
}
}

View File

@ -44,22 +44,20 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
style: Style {
align_self: AlignSelf::FlexEnd,
..default()
},
text: Text::with_section(
parent.spawn_bundle(
TextBundle::from_section(
"Example text",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::WHITE,
},
Default::default(),
),
..default()
});
)
.with_style(Style {
align_self: AlignSelf::FlexEnd,
..default()
}),
);
});
});
}