From 0cfb5cb631636326ad6b650231a280dbfeef4f05 Mon Sep 17 00:00:00 2001 From: Molot2032 <117271367+Molot2032@users.noreply.github.com> Date: Sat, 21 Jan 2023 00:17:11 +0000 Subject: [PATCH] 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) --- crates/bevy_text/src/glyph_brush.rs | 12 +++++-- crates/bevy_text/src/pipeline.rs | 11 +++--- crates/bevy_text/src/text.rs | 26 ++++++++++++++ crates/bevy_text/src/text2d.rs | 1 + crates/bevy_ui/src/widget/text.rs | 1 + examples/2d/text2d.rs | 56 ++++++++++++++++++++++++++--- 6 files changed, 94 insertions(+), 13 deletions(-) diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index 8f88a33f56467f..a96d25dd669230 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -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, 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) } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 3726f57fd40381..ac29c6b7c9f87e 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -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, texture_atlases: &mut Assets, @@ -75,9 +76,9 @@ impl TextPipeline { }) .collect::, _>>()?; - let section_glyphs = self - .brush - .compute_glyphs(§ions, bounds, text_alignment)?; + let section_glyphs = + self.brush + .compute_glyphs(§ions, bounds, text_alignment, linebreak_behaviour)?; if section_glyphs.is_empty() { return Ok(TextLayoutInfo::default()); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 719b59106b7004..a6ca996f828a0f 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -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 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, + } + } +} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 6a5ed740894869..56da32ade4e8b7 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -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, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c2694f3e142a16..28a3984edbc586 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -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, diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 2f4f53df183ec4..e920ab50625577 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -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) { 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) { // 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) { }) .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) { ..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(