Add text wrapping support to Text2d (#4347)

# Objective

Fixes #4344.

## Solution

Add a new component `Text2dBounds` to `Text2dBundle` that specifies the maximum width and height of text. Text will wrap according to this size.
This commit is contained in:
Yutao Yuan 2022-03-29 23:03:20 +00:00
parent c7c08f95cb
commit 954022c799
3 changed files with 82 additions and 29 deletions

View file

@ -1,6 +1,5 @@
use bevy_asset::Handle;
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_math::Size;
use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize};
use bevy_render::color::Color;
use serde::{Deserialize, Serialize};
@ -150,9 +149,3 @@ impl Default for TextStyle {
}
}
}
#[derive(Component, Default, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct Text2dSize {
pub size: Size,
}

View file

@ -1,46 +1,63 @@
use bevy_asset::Assets;
use bevy_ecs::{
bundle::Bundle,
component::Component,
entity::Entity,
query::{Changed, QueryState, With},
reflect::ReflectComponent,
system::{Local, Query, QuerySet, Res, ResMut},
};
use bevy_math::{Size, Vec3};
use bevy_reflect::Reflect;
use bevy_render::{texture::Image, view::Visibility, RenderWorld};
use bevy_sprite::{ExtractedSprite, ExtractedSprites, TextureAtlas};
use bevy_transform::prelude::{GlobalTransform, Transform};
use bevy_window::{WindowId, Windows};
use crate::{
DefaultTextPipeline, Font, FontAtlasSet, HorizontalAlign, Text, Text2dSize, TextError,
VerticalAlign,
DefaultTextPipeline, Font, FontAtlasSet, HorizontalAlign, Text, TextError, VerticalAlign,
};
/// The calculated size of text drawn in 2D scene.
#[derive(Component, Default, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct Text2dSize {
pub size: 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
/// specified `TextAlignment`.
///
/// Note: only characters that are completely out of the bounds will be truncated, so this is not a
/// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this
/// component is mainly useful for text wrapping only.
#[derive(Component, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct Text2dBounds {
pub size: Size,
}
impl Default for Text2dBounds {
fn default() -> Self {
Self {
size: Size::new(f32::MAX, f32::MAX),
}
}
}
/// The bundle of components needed to draw text in a 2D scene via a 2D `OrthographicCameraBundle`.
/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs)
#[derive(Bundle, Clone, Debug)]
#[derive(Bundle, Clone, Debug, Default)]
pub struct Text2dBundle {
pub text: Text,
pub transform: Transform,
pub global_transform: GlobalTransform,
pub text_2d_size: Text2dSize,
pub text_2d_bounds: Text2dBounds,
pub visibility: Visibility,
}
impl Default for Text2dBundle {
fn default() -> Self {
Self {
text: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
text_2d_size: Text2dSize {
size: Size::default(),
},
visibility: Default::default(),
}
}
}
pub fn extract_text2d_sprite(
mut render_world: ResMut<RenderWorld>,
texture_atlases: Res<Assets<TextureAtlas>>,
@ -123,7 +140,7 @@ pub fn text2d_system(
mut text_pipeline: ResMut<DefaultTextPipeline>,
mut text_queries: QuerySet<(
QueryState<Entity, (With<Text2dSize>, Changed<Text>)>,
QueryState<(&Text, &mut Text2dSize), With<Text2dSize>>,
QueryState<(&Text, Option<&Text2dBounds>, &mut Text2dSize), With<Text2dSize>>,
)>,
) {
// Adds all entities where the text or the style has changed to the local queue
@ -141,14 +158,21 @@ pub fn text2d_system(
let mut new_queue = Vec::new();
let mut query = text_queries.q1();
for entity in queued_text.entities.drain(..) {
if let Ok((text, mut calculated_size)) = query.get_mut(entity) {
if let Ok((text, bounds, mut calculated_size)) = query.get_mut(entity) {
let text_bounds = match bounds {
Some(bounds) => Size {
width: scale_value(bounds.size.width, scale_factor),
height: scale_value(bounds.size.height, scale_factor),
},
None => Size::new(f32::MAX, f32::MAX),
};
match text_pipeline.queue_text(
entity,
&fonts,
&text.sections,
scale_factor,
text.alignment,
Size::new(f32::MAX, f32::MAX),
text_bounds,
&mut *font_atlas_set_storage,
&mut *texture_atlases,
&mut *textures,

View file

@ -1,4 +1,4 @@
use bevy::prelude::*;
use bevy::{prelude::*, text::Text2dBounds};
fn main() {
App::new()
@ -47,10 +47,46 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Demonstrate changing scale
commands
.spawn_bundle(Text2dBundle {
text: Text::with_section("scale", text_style, text_alignment),
text: Text::with_section("scale", text_style.clone(), text_alignment),
..default()
})
.insert(AnimateScale);
// Demonstrate text wrapping
let box_size = Size::new(300.0, 200.0);
let box_position = Vec2::new(0.0, -250.0);
commands.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: Color::rgb(0.25, 0.25, 0.75),
custom_size: Some(Vec2::new(box_size.width, box_size.height)),
..default()
},
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_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.width / 2.0,
box_position.y + box_size.height / 2.0,
1.0,
),
..default()
});
}
fn animate_translation(