Skip to content

Commit cef56a0

Browse files
committed
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)
1 parent a94830f commit cef56a0

File tree

6 files changed

+94
-13
lines changed

6 files changed

+94
-13
lines changed

crates/bevy_text/src/glyph_brush.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ use bevy_render::texture::Image;
55
use bevy_sprite::TextureAtlas;
66
use bevy_utils::tracing::warn;
77
use glyph_brush_layout::{
8-
FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, SectionText, ToSectionText,
8+
BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph,
9+
SectionText, ToSectionText,
910
};
1011

1112
use crate::{
12-
error::TextError, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo, TextAlignment,
13-
TextSettings, YAxisOrientation,
13+
error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo,
14+
TextAlignment, TextSettings, YAxisOrientation,
1415
};
1516

1617
pub struct GlyphBrush {
@@ -35,13 +36,18 @@ impl GlyphBrush {
3536
sections: &[S],
3637
bounds: Vec2,
3738
text_alignment: TextAlignment,
39+
linebreak_behaviour: BreakLineOn,
3840
) -> Result<Vec<SectionGlyph>, TextError> {
3941
let geom = SectionGeometry {
4042
bounds: (bounds.x, bounds.y),
4143
..Default::default()
4244
};
45+
46+
let lbb: BuiltInLineBreaker = linebreak_behaviour.into();
47+
4348
let section_glyphs = Layout::default()
4449
.h_align(text_alignment.into())
50+
.line_breaker(lbb)
4551
.calculate_glyphs(&self.fonts, &geom, sections);
4652
Ok(section_glyphs)
4753
}

crates/bevy_text/src/pipeline.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use bevy_utils::HashMap;
1010
use glyph_brush_layout::{FontId, SectionText};
1111

1212
use crate::{
13-
error::TextError, glyph_brush::GlyphBrush, scale_value, Font, FontAtlasSet, FontAtlasWarning,
14-
PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation,
13+
error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet,
14+
FontAtlasWarning, PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation,
1515
};
1616

1717
#[derive(Default, Resource)]
@@ -45,6 +45,7 @@ impl TextPipeline {
4545
sections: &[TextSection],
4646
scale_factor: f64,
4747
text_alignment: TextAlignment,
48+
linebreak_behaviour: BreakLineOn,
4849
bounds: Vec2,
4950
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
5051
texture_atlases: &mut Assets<TextureAtlas>,
@@ -75,9 +76,9 @@ impl TextPipeline {
7576
})
7677
.collect::<Result<Vec<_>, _>>()?;
7778

78-
let section_glyphs = self
79-
.brush
80-
.compute_glyphs(&sections, bounds, text_alignment)?;
79+
let section_glyphs =
80+
self.brush
81+
.compute_glyphs(&sections, bounds, text_alignment, linebreak_behaviour)?;
8182

8283
if section_glyphs.is_empty() {
8384
return Ok(TextLayoutInfo::default());

crates/bevy_text/src/text.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ pub struct Text {
1414
/// The text's internal alignment.
1515
/// Should not affect its position within a container.
1616
pub alignment: TextAlignment,
17+
/// How the text should linebreak when running out of the bounds determined by max_size
18+
pub linebreak_behaviour: BreakLineOn,
1719
}
1820

1921
impl Default for Text {
2022
fn default() -> Self {
2123
Self {
2224
sections: Default::default(),
2325
alignment: TextAlignment::Left,
26+
linebreak_behaviour: BreakLineOn::WordBoundary,
2427
}
2528
}
2629
}
@@ -170,3 +173,26 @@ impl Default for TextStyle {
170173
}
171174
}
172175
}
176+
177+
/// Determines how lines will be broken when preventing text from running out of bounds.
178+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
179+
#[reflect(Serialize, Deserialize)]
180+
pub enum BreakLineOn {
181+
/// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/).
182+
/// Lines will be broken up at the nearest suitable word boundary, usually a space.
183+
/// This behaviour suits most cases, as it keeps words intact across linebreaks.
184+
WordBoundary,
185+
/// Lines will be broken without discrimination on any character that would leave bounds.
186+
/// This is closer to the behaviour one might expect from text in a terminal.
187+
/// However it may lead to words being broken up across linebreaks.
188+
AnyCharacter,
189+
}
190+
191+
impl From<BreakLineOn> for glyph_brush_layout::BuiltInLineBreaker {
192+
fn from(val: BreakLineOn) -> Self {
193+
match val {
194+
BreakLineOn::WordBoundary => glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker,
195+
BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker,
196+
}
197+
}
198+
}

crates/bevy_text/src/text2d.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ pub fn update_text2d_layout(
186186
&text.sections,
187187
scale_factor,
188188
text.alignment,
189+
text.linebreak_behaviour,
189190
text_bounds,
190191
&mut font_atlas_set_storage,
191192
&mut texture_atlases,

crates/bevy_ui/src/widget/text.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ pub fn text_system(
120120
&text.sections,
121121
scale_factor,
122122
text.alignment,
123+
text.linebreak_behaviour,
123124
node_size,
124125
&mut font_atlas_set_storage,
125126
&mut texture_atlases,

examples/2d/text2d.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
//! For an example on how to render text as part of a user interface, independent from the world
66
//! viewport, you may want to look at `2d/contributors.rs` or `ui/text.rs`.
77
8-
use bevy::{prelude::*, text::Text2dBounds};
8+
use bevy::{
9+
prelude::*,
10+
text::{BreakLineOn, Text2dBounds},
11+
};
912

1013
fn main() {
1114
App::new()
@@ -29,7 +32,7 @@ struct AnimateScale;
2932
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
3033
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
3134
let text_style = TextStyle {
32-
font,
35+
font: font.clone(),
3336
font_size: 60.0,
3437
color: Color::WHITE,
3538
};
@@ -56,12 +59,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
5659
// Demonstrate changing scale
5760
commands.spawn((
5861
Text2dBundle {
59-
text: Text::from_section("scale", text_style.clone()).with_alignment(text_alignment),
62+
text: Text::from_section("scale", text_style).with_alignment(text_alignment),
6063
..default()
6164
},
6265
AnimateScale,
6366
));
6467
// Demonstrate text wrapping
68+
let slightly_smaller_text_style = TextStyle {
69+
font,
70+
font_size: 42.0,
71+
color: Color::WHITE,
72+
};
6573
let box_size = Vec2::new(300.0, 200.0);
6674
let box_position = Vec2::new(0.0, -250.0);
6775
commands
@@ -76,8 +84,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
7684
})
7785
.with_children(|builder| {
7886
builder.spawn(Text2dBundle {
79-
text: Text::from_section("this text wraps in the box", text_style)
80-
.with_alignment(TextAlignment::Left),
87+
text: Text {
88+
sections: vec![TextSection::new(
89+
"this text wraps in the box\n(Unicode linebreaks)",
90+
slightly_smaller_text_style.clone(),
91+
)],
92+
alignment: TextAlignment::Left,
93+
linebreak_behaviour: BreakLineOn::WordBoundary,
94+
},
8195
text_2d_bounds: Text2dBounds {
8296
// Wrap text in the rectangle
8397
size: box_size,
@@ -87,6 +101,38 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
87101
..default()
88102
});
89103
});
104+
105+
let other_box_size = Vec2::new(300.0, 200.0);
106+
let other_box_position = Vec2::new(320.0, -250.0);
107+
commands
108+
.spawn(SpriteBundle {
109+
sprite: Sprite {
110+
color: Color::rgb(0.20, 0.3, 0.70),
111+
custom_size: Some(Vec2::new(other_box_size.x, other_box_size.y)),
112+
..default()
113+
},
114+
transform: Transform::from_translation(other_box_position.extend(0.0)),
115+
..default()
116+
})
117+
.with_children(|builder| {
118+
builder.spawn(Text2dBundle {
119+
text: Text {
120+
sections: vec![TextSection::new(
121+
"this text wraps in the box\n(AnyCharacter linebreaks)",
122+
slightly_smaller_text_style.clone(),
123+
)],
124+
alignment: TextAlignment::Left,
125+
linebreak_behaviour: BreakLineOn::AnyCharacter,
126+
},
127+
text_2d_bounds: Text2dBounds {
128+
// Wrap text in the rectangle
129+
size: other_box_size,
130+
},
131+
// ensure the text is drawn on top of the box
132+
transform: Transform::from_translation(Vec3::Z),
133+
..default()
134+
});
135+
});
90136
}
91137

92138
fn animate_translation(

0 commit comments

Comments
 (0)