Skip to content

Commit

Permalink
Allow users of Text/TextBundle to choose from glyph_brush_layout's Bu…
Browse files Browse the repository at this point in the history
…iltInLineBreaker 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)
  • Loading branch information
Molot2032 committed Jan 21, 2023
1 parent a94830f commit cef56a0
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 13 deletions.
12 changes: 9 additions & 3 deletions crates/bevy_text/src/glyph_brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
11 changes: 6 additions & 5 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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());
Expand Down
26 changes: 26 additions & 0 deletions crates/bevy_text/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ 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 {
fn default() -> Self {
Self {
sections: Default::default(),
alignment: TextAlignment::Left,
linebreak_behaviour: BreakLineOn::WordBoundary,
}
}
}
Expand Down Expand Up @@ -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,
}
}
}
1 change: 1 addition & 0 deletions crates/bevy_text/src/text2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ui/src/widget/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
56 changes: 51 additions & 5 deletions examples/2d/text2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
};
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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(
Expand Down

0 comments on commit cef56a0

Please sign in to comment.