NoWrap Text feature (#8947)

# Objective

In Bevy 10.1 and before, the only way to enable text wrapping was to set
a local `Val::Px` width constraint on the text node itself.
`Val::Percent` constraints and constraints on the text node's ancestors
did nothing.

#7779 fixed those problems. But perversely displaying unwrapped text is
really difficult now, and requires users to nest each `TextBundle` in a
`NodeBundle` and apply `min_width` and `max_width` constraints. Some
constructions may even need more than one layer of nesting. I've seen
several people already who have really struggled with this when porting
their projects to main in advance of 0.11.

## Solution

Add a `NoWrap` variant to the `BreakLineOn` enum.
If `NoWrap` is set, ignore any constraints on the width for the text and
call `TextPipeline::queue_text` with a width bound of `f32::INFINITY`.



---

## Changelog
* Added a `NoWrap` variant to the `BreakLineOn` enum.
* If `NoWrap` is set, any constraints on the width for the text are
ignored and `TextPipeline::queue_text` is called with a width bound of
`f32::INFINITY`.
* Changed the `size` field of `FixedMeasure` to `pub`. This shouldn't
have been private, it was always intended to have `pub` visibility.
* Added a `with_no_wrap` method to `TextBundle`.

## Migration Guide

`bevy_text::text::BreakLineOn` has a new variant `NoWrap` that disables
text wrapping for the `Text`.
Text wrapping can also be disabled using the `with_no_wrap` method of
`TextBundle`.
This commit is contained in:
ickshonpe 2023-06-26 17:23:00 +01:00 committed by GitHub
parent 4b1a502a49
commit aeea4b0344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 53 additions and 13 deletions

View File

@ -106,6 +106,13 @@ impl Text {
self.alignment = alignment;
self
}
/// Returns this [`Text`] with soft wrapping disabled.
/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.
pub const fn with_no_wrap(mut self) -> Self {
self.linebreak_behavior = BreakLineOn::NoWrap;
self
}
}
#[derive(Debug, Default, Clone, FromReflect, Reflect)]
@ -186,12 +193,19 @@ pub enum BreakLineOn {
/// This is closer to the behavior one might expect from text in a terminal.
/// However it may lead to words being broken up across linebreaks.
AnyCharacter,
/// No soft wrapping, where text is automatically broken up into separate lines when it overflows a boundary, will ever occur.
/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, is still enabled.
NoWrap,
}
impl From<BreakLineOn> for glyph_brush_layout::BuiltInLineBreaker {
fn from(val: BreakLineOn) -> Self {
match val {
BreakLineOn::WordBoundary => glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker,
// If `NoWrap` is set the choice of `BuiltInLineBreaker` doesn't matter as the text is given unbounded width and soft wrapping will never occur.
// But `NoWrap` does not disable hard breaks where a [`Text`] contains a newline character.
BreakLineOn::WordBoundary | BreakLineOn::NoWrap => {
glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker
}
BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker,
}
}

View File

@ -23,8 +23,8 @@ use bevy_utils::HashSet;
use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged};
use crate::{
Font, FontAtlasSet, FontAtlasWarning, PositionedGlyph, Text, TextError, TextLayoutInfo,
TextPipeline, TextSettings, YAxisOrientation,
BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, PositionedGlyph, Text, TextError,
TextLayoutInfo, TextPipeline, TextSettings, YAxisOrientation,
};
/// The maximum width and height of text. The text will wrap according to the specified size.
@ -174,7 +174,11 @@ pub fn update_text2d_layout(
for (entity, text, bounds, mut text_layout_info) in &mut text_query {
if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) {
let text_bounds = Vec2::new(
scale_value(bounds.size.x, scale_factor),
if text.linebreak_behavior == BreakLineOn::NoWrap {
f32::INFINITY
} else {
scale_value(bounds.size.x, scale_factor)
},
scale_value(bounds.size.y, scale_factor),
);

View File

@ -28,7 +28,7 @@ pub trait Measure: Send + Sync + 'static {
/// always returns the same size.
#[derive(Default, Clone)]
pub struct FixedMeasure {
size: Vec2,
pub size: Vec2,
}
impl Measure for FixedMeasure {

View File

@ -13,7 +13,7 @@ use bevy_render::{
};
use bevy_sprite::TextureAtlas;
#[cfg(feature = "bevy_text")]
use bevy_text::{Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle};
use bevy_text::{BreakLineOn, Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle};
use bevy_transform::prelude::{GlobalTransform, Transform};
/// The basic UI node
@ -256,6 +256,13 @@ impl TextBundle {
self.background_color = BackgroundColor(color);
self
}
/// Returns this [`TextBundle`] with soft wrapping disabled.
/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.
pub const fn with_no_wrap(mut self) -> Self {
self.text.linebreak_behavior = BreakLineOn::NoWrap;
self
}
}
/// A UI node that is a button

View File

@ -1,4 +1,4 @@
use crate::{ContentSize, Measure, Node, UiScale};
use crate::{ContentSize, FixedMeasure, Measure, Node, UiScale};
use bevy_asset::Assets;
use bevy_ecs::{
prelude::{Component, DetectChanges},
@ -12,8 +12,8 @@ use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, ReflectFrom
use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas;
use bevy_text::{
Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextMeasureInfo,
TextPipeline, TextSettings, YAxisOrientation,
BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo,
TextMeasureInfo, TextPipeline, TextSettings, YAxisOrientation,
};
use bevy_window::{PrimaryWindow, Window};
use taffy::style::AvailableSpace;
@ -91,7 +91,13 @@ fn create_text_measure(
text.linebreak_behavior,
) {
Ok(measure) => {
content_size.set(TextMeasure { info: measure });
if text.linebreak_behavior == BreakLineOn::NoWrap {
content_size.set(FixedMeasure {
size: measure.max_width_content_size,
});
} else {
content_size.set(TextMeasure { info: measure });
}
// Text measure func created succesfully, so set `TextFlags` to schedule a recompute
text_flags.needs_new_measure_func = false;
@ -174,7 +180,12 @@ fn queue_text(
) {
// Skip the text node if it is waiting for a new measure func
if !text_flags.needs_new_measure_func {
let physical_node_size = node.physical_size(scale_factor);
let physical_node_size = if text.linebreak_behavior == BreakLineOn::NoWrap {
// With `NoWrap` set, no constraints are placed on the width of the text.
Vec2::splat(f32::INFINITY)
} else {
node.physical_size(scale_factor)
};
match text_pipeline.queue_text(
fonts,

View File

@ -33,7 +33,11 @@ fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
})
.id();
for linebreak_behavior in [BreakLineOn::AnyCharacter, BreakLineOn::WordBoundary] {
for linebreak_behavior in [
BreakLineOn::AnyCharacter,
BreakLineOn::WordBoundary,
BreakLineOn::NoWrap,
] {
let row_id = commands
.spawn(NodeBundle {
style: Style {
@ -66,7 +70,7 @@ fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
flex_direction: FlexDirection::Column,
width: Val::Percent(16.),
height: Val::Percent(95.),
overflow: Overflow::clip(),
overflow: Overflow::clip_x(),
..Default::default()
},
background_color: Color::rgb(0.5, c, 1.0 - c).into(),