diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 2f2b37ab9a19e..95a9fa7ee54ff 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -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, -} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 0119bffde7b6e..3b4ff0c3e74fb 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -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, texture_atlases: Res>, @@ -123,7 +140,7 @@ pub fn text2d_system( mut text_pipeline: ResMut, mut text_queries: QuerySet<( QueryState, Changed)>, - QueryState<(&Text, &mut Text2dSize), With>, + QueryState<(&Text, Option<&Text2dBounds>, &mut Text2dSize), With>, )>, ) { // 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, diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index c083bc2cd7b34..9a41e7912c246 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -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) { // 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(