bevy/crates/bevy_text/src/text.rs
Molot2032 cef56a0d47 Allow users of Text/TextBundle to choose from glyph_brush_layout's BuiltInLineBreaker options. (#7283)
# Objective
Currently, Text always uses the default linebreaking behaviour in glyph_brush_layout `BuiltInLineBreaker::Unicode` which breaks lines at word boundaries. However, glyph_brush_layout also supports breaking lines at any character by setting the linebreaker to `BuiltInLineBreaker::AnyChar`. Having text wrap character-by-character instead of at word boundaries is desirable in some cases - consider that consoles/terminals usually wrap this way.

As a side note, the default Unicode linebreaker does not seem to handle emergency cases, where there is no word boundary on a line to break at. In that case, the text runs out of bounds. Issue #1867 shows an example of this.

## Solution
Basically just copies how TextAlignment is exposed, but for a new enum TextLineBreakBehaviour.
This PR exposes glyph_brush_layout's two simple linebreaking options (Unicode, AnyChar) to users of Text via the enum TextLineBreakBehaviour (which just translates those 2 aforementioned options), plus a method 'with_linebreak_behaviour' on Text and TextBundle. 

## Changelog

Added `Text::with_linebreak_behaviour`
Added `TextBundle::with_linebreak_behaviour` 
`TextPipeline::queue_text` and `GlyphBrush::compute_glyphs` now need a TextLineBreakBehaviour argument, in order to pass through the new field.
Modified the `text2d` example to show both linebreaking behaviours. 


## Example
Here's what the modified example looks like
![image](https://user-images.githubusercontent.com/117271367/213589184-b1a54bf3-116c-4721-8cb6-1cb69edb3070.png)
2023-01-21 00:17:11 +00:00

199 lines
6.3 KiB
Rust

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, 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,
/// How the text should linebreak when running out of the bounds determined by max_size
pub linebreak_behaviour: BreakLineOn,
}
impl Default for Text {
fn default() -> Self {
Self {
sections: Default::default(),
alignment: TextAlignment::Left,
linebreak_behaviour: BreakLineOn::WordBoundary,
}
}
}
impl Text {
/// Constructs a [`Text`] with a single section.
///
/// ```
/// # use bevy_asset::Handle;
/// # use bevy_render::color::Color;
/// # use bevy_text::{Font, Text, TextStyle, TextAlignment};
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// #
/// // 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,
/// },
/// );
///
/// let hello_bevy = Text::from_section(
/// "hello bevy!",
/// TextStyle {
/// font: font_handle,
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// ) // You can still add an alignment.
/// .with_alignment(TextAlignment::Center);
/// ```
pub fn from_section(value: impl Into<String>, style: TextStyle) -> Self {
Self {
sections: vec![TextSection::new(value, style)],
..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(),
..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)]
pub struct TextSection {
pub value: String,
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,
}
}
}
/// Describes horizontal alignment preference for positioning & bounds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize)]
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,
/// Leftmost & rightmost characters are equidistant to the render position.<br/>
/// Bounds start from the render position and advance equally left & right.
Center,
/// Rightmost character is immediately to the left of the render position.<br/>
/// Bounds start from the render position and advance leftwards.
Right,
}
impl From<TextAlignment> for glyph_brush_layout::HorizontalAlign {
fn from(val: TextAlignment) -> Self {
match val {
TextAlignment::Left => glyph_brush_layout::HorizontalAlign::Left,
TextAlignment::Center => glyph_brush_layout::HorizontalAlign::Center,
TextAlignment::Right => glyph_brush_layout::HorizontalAlign::Right,
}
}
}
#[derive(Clone, Debug, Reflect, FromReflect)]
pub struct TextStyle {
pub font: Handle<Font>,
pub font_size: f32,
pub color: Color,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font: Default::default(),
font_size: 12.0,
color: Color::WHITE,
}
}
}
/// Determines how lines will be broken when preventing text from running out of bounds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize)]
pub enum BreakLineOn {
/// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/).
/// Lines will be broken up at the nearest suitable word boundary, usually a space.
/// This behaviour suits most cases, as it keeps words intact across linebreaks.
WordBoundary,
/// Lines will be broken without discrimination on any character that would leave bounds.
/// This is closer to the behaviour one might expect from text in a terminal.
/// However it may lead to words being broken up across linebreaks.
AnyCharacter,
}
impl From<BreakLineOn> for glyph_brush_layout::BuiltInLineBreaker {
fn from(val: BreakLineOn) -> Self {
match val {
BreakLineOn::WordBoundary => glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker,
BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker,
}
}
}