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 
This commit is contained in:
parent
a94830f0c9
commit
cef56a0d47
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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(§ions, bounds, text_alignment)?;
|
.compute_glyphs(§ions, bounds, text_alignment, linebreak_behaviour)?;
|
||||||
|
|
||||||
if section_glyphs.is_empty() {
|
if section_glyphs.is_empty() {
|
||||||
return Ok(TextLayoutInfo::default());
|
return Ok(TextLayoutInfo::default());
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user