From aeea4b034412a287e77a7d578ed76f4fe15cd85a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 26 Jun 2023 17:23:00 +0100 Subject: [PATCH] `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`. --- crates/bevy_text/src/text.rs | 16 +++++++++++++++- crates/bevy_text/src/text2d.rs | 10 +++++++--- crates/bevy_ui/src/measurement.rs | 2 +- crates/bevy_ui/src/node_bundles.rs | 9 ++++++++- crates/bevy_ui/src/widget/text.rs | 21 ++++++++++++++++----- examples/ui/text_wrap_debug.rs | 8 ++++++-- 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index eaaf58d2b5..9975d2d97e 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -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 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, } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 0cee62291d..61f0354b59 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -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), ); diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 8c5959f27e..896c9f6c05 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -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 { diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 468a155aa0..c42afda483 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -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 diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 25ada49e9a..72d2bbf0b3 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -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, diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 0ef93cf6c2..374799ec81 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -33,7 +33,11 @@ fn spawn(mut commands: Commands, asset_server: Res) { }) .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) { 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(),