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

View file

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

View file

@ -14,6 +14,8 @@ pub struct Text {
/// 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 {
@ -21,6 +23,7 @@ impl Default for Text {
Self {
sections: Default::default(),
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,
scale_factor,
text.alignment,
text.linebreak_behaviour,
text_bounds,
&mut font_atlas_set_storage,
&mut texture_atlases,

View file

@ -120,6 +120,7 @@ pub fn text_system(
&text.sections,
scale_factor,
text.alignment,
text.linebreak_behaviour,
node_size,
&mut font_atlas_set_storage,
&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
//! 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() {
App::new()
@ -29,7 +32,7 @@ struct AnimateScale;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
let text_style = TextStyle {
font,
font: font.clone(),
font_size: 60.0,
color: Color::WHITE,
};
@ -56,12 +59,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Demonstrate changing scale
commands.spawn((
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()
},
AnimateScale,
));
// 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_position = Vec2::new(0.0, -250.0);
commands
@ -76,8 +84,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
})
.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text::from_section("this text wraps in the box", text_style)
.with_alignment(TextAlignment::Left),
text: Text {
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 {
// Wrap text in the rectangle
size: box_size,
@ -87,6 +101,38 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..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(