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)
This commit is contained in:
Molot2032 2023-01-21 00:17:11 +00:00
parent a94830f0c9
commit cef56a0d47
6 changed files with 94 additions and 13 deletions

View File

@ -5,12 +5,13 @@ use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas; use bevy_sprite::TextureAtlas;
use bevy_utils::tracing::warn; use bevy_utils::tracing::warn;
use glyph_brush_layout::{ use glyph_brush_layout::{
FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, SectionText, ToSectionText, BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph,
SectionText, ToSectionText,
}; };
use crate::{ use crate::{
error::TextError, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo, TextAlignment, error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo,
TextSettings, YAxisOrientation, TextAlignment, TextSettings, YAxisOrientation,
}; };
pub struct GlyphBrush { pub struct GlyphBrush {
@ -35,13 +36,18 @@ impl GlyphBrush {
sections: &[S], sections: &[S],
bounds: Vec2, bounds: Vec2,
text_alignment: TextAlignment, text_alignment: TextAlignment,
linebreak_behaviour: BreakLineOn,
) -> Result<Vec<SectionGlyph>, TextError> { ) -> Result<Vec<SectionGlyph>, TextError> {
let geom = SectionGeometry { let geom = SectionGeometry {
bounds: (bounds.x, bounds.y), bounds: (bounds.x, bounds.y),
..Default::default() ..Default::default()
}; };
let lbb: BuiltInLineBreaker = linebreak_behaviour.into();
let section_glyphs = Layout::default() let section_glyphs = Layout::default()
.h_align(text_alignment.into()) .h_align(text_alignment.into())
.line_breaker(lbb)
.calculate_glyphs(&self.fonts, &geom, sections); .calculate_glyphs(&self.fonts, &geom, sections);
Ok(section_glyphs) Ok(section_glyphs)
} }

View File

@ -10,8 +10,8 @@ use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, SectionText}; use glyph_brush_layout::{FontId, SectionText};
use crate::{ use crate::{
error::TextError, glyph_brush::GlyphBrush, scale_value, Font, FontAtlasSet, FontAtlasWarning, error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet,
PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation, FontAtlasWarning, PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation,
}; };
#[derive(Default, Resource)] #[derive(Default, Resource)]
@ -45,6 +45,7 @@ impl TextPipeline {
sections: &[TextSection], sections: &[TextSection],
scale_factor: f64, scale_factor: f64,
text_alignment: TextAlignment, text_alignment: TextAlignment,
linebreak_behaviour: BreakLineOn,
bounds: Vec2, bounds: Vec2,
font_atlas_set_storage: &mut Assets<FontAtlasSet>, font_atlas_set_storage: &mut Assets<FontAtlasSet>,
texture_atlases: &mut Assets<TextureAtlas>, texture_atlases: &mut Assets<TextureAtlas>,
@ -75,9 +76,9 @@ impl TextPipeline {
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let section_glyphs = self let section_glyphs =
.brush self.brush
.compute_glyphs(&sections, bounds, text_alignment)?; .compute_glyphs(&sections, bounds, text_alignment, linebreak_behaviour)?;
if section_glyphs.is_empty() { if section_glyphs.is_empty() {
return Ok(TextLayoutInfo::default()); return Ok(TextLayoutInfo::default());

View File

@ -14,6 +14,8 @@ pub struct Text {
/// The text's internal alignment. /// The text's internal alignment.
/// Should not affect its position within a container. /// Should not affect its position within a container.
pub alignment: TextAlignment, 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 { impl Default for Text {
@ -21,6 +23,7 @@ impl Default for Text {
Self { Self {
sections: Default::default(), sections: Default::default(),
alignment: TextAlignment::Left, alignment: TextAlignment::Left,
linebreak_behaviour: BreakLineOn::WordBoundary,
} }
} }
} }
@ -170,3 +173,26 @@ impl Default for TextStyle {
} }
} }
} }
/// 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,
}
}
}

View File

@ -186,6 +186,7 @@ pub fn update_text2d_layout(
&text.sections, &text.sections,
scale_factor, scale_factor,
text.alignment, text.alignment,
text.linebreak_behaviour,
text_bounds, text_bounds,
&mut font_atlas_set_storage, &mut font_atlas_set_storage,
&mut texture_atlases, &mut texture_atlases,

View File

@ -120,6 +120,7 @@ pub fn text_system(
&text.sections, &text.sections,
scale_factor, scale_factor,
text.alignment, text.alignment,
text.linebreak_behaviour,
node_size, node_size,
&mut font_atlas_set_storage, &mut font_atlas_set_storage,
&mut texture_atlases, &mut texture_atlases,

View File

@ -5,7 +5,10 @@
//! For an example on how to render text as part of a user interface, independent from the world //! For an example on how to render text as part of a user interface, independent from the world
//! viewport, you may want to look at `2d/contributors.rs` or `ui/text.rs`. //! viewport, you may want to look at `2d/contributors.rs` or `ui/text.rs`.
use bevy::{prelude::*, text::Text2dBounds}; use bevy::{
prelude::*,
text::{BreakLineOn, Text2dBounds},
};
fn main() { fn main() {
App::new() App::new()
@ -29,7 +32,7 @@ struct AnimateScale;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let font = asset_server.load("fonts/FiraSans-Bold.ttf");
let text_style = TextStyle { let text_style = TextStyle {
font, font: font.clone(),
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::WHITE,
}; };
@ -56,12 +59,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Demonstrate changing scale // Demonstrate changing scale
commands.spawn(( commands.spawn((
Text2dBundle { Text2dBundle {
text: Text::from_section("scale", text_style.clone()).with_alignment(text_alignment), text: Text::from_section("scale", text_style).with_alignment(text_alignment),
..default() ..default()
}, },
AnimateScale, AnimateScale,
)); ));
// Demonstrate text wrapping // Demonstrate text wrapping
let slightly_smaller_text_style = TextStyle {
font,
font_size: 42.0,
color: Color::WHITE,
};
let box_size = Vec2::new(300.0, 200.0); let box_size = Vec2::new(300.0, 200.0);
let box_position = Vec2::new(0.0, -250.0); let box_position = Vec2::new(0.0, -250.0);
commands commands
@ -76,8 +84,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
}) })
.with_children(|builder| { .with_children(|builder| {
builder.spawn(Text2dBundle { builder.spawn(Text2dBundle {
text: Text::from_section("this text wraps in the box", text_style) text: Text {
.with_alignment(TextAlignment::Left), sections: vec![TextSection::new(
"this text wraps in the box\n(Unicode linebreaks)",
slightly_smaller_text_style.clone(),
)],
alignment: TextAlignment::Left,
linebreak_behaviour: BreakLineOn::WordBoundary,
},
text_2d_bounds: Text2dBounds { text_2d_bounds: Text2dBounds {
// Wrap text in the rectangle // Wrap text in the rectangle
size: box_size, size: box_size,
@ -87,6 +101,38 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default() ..default()
}); });
}); });
let other_box_size = Vec2::new(300.0, 200.0);
let other_box_position = Vec2::new(320.0, -250.0);
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: Color::rgb(0.20, 0.3, 0.70),
custom_size: Some(Vec2::new(other_box_size.x, other_box_size.y)),
..default()
},
transform: Transform::from_translation(other_box_position.extend(0.0)),
..default()
})
.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text {
sections: vec![TextSection::new(
"this text wraps in the box\n(AnyCharacter linebreaks)",
slightly_smaller_text_style.clone(),
)],
alignment: TextAlignment::Left,
linebreak_behaviour: BreakLineOn::AnyCharacter,
},
text_2d_bounds: Text2dBounds {
// Wrap text in the rectangle
size: other_box_size,
},
// ensure the text is drawn on top of the box
transform: Transform::from_translation(Vec3::Z),
..default()
});
});
} }
fn animate_translation( fn animate_translation(