From e782612e0bc3927e16560882035004b373ba7b8e Mon Sep 17 00:00:00 2001 From: koe Date: Tue, 1 Oct 2024 16:32:01 -0500 Subject: [PATCH 01/41] text rework WIP --- crates/bevy_text/src/text.rs | 73 ++++++++++++-- crates/bevy_text/src/text2d.rs | 21 +++- crates/bevy_ui/src/node_bundles.rs | 103 ------------------- crates/bevy_ui/src/widget/text.rs | 155 +++++++++++++++++++++++++++-- 4 files changed, 226 insertions(+), 126 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 5f6215ea0ad6f..38feae1304e8b 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -14,7 +14,7 @@ pub use cosmic_text::{ }; /// Wrapper for [`cosmic_text::Buffer`] -#[derive(Component, Deref, DerefMut, Debug, Clone)] +#[derive(Deref, DerefMut, Debug, Clone)] pub struct CosmicBuffer(pub Buffer); impl Default for CosmicBuffer { @@ -23,12 +23,63 @@ impl Default for CosmicBuffer { } } +/// Computed information for a [`TextBlock`]. +/// +/// Automatically updated. +#[derive(Component, Debug, Clone)] +pub struct ComputedTextBlock { + /// Buffer for managing text layout and creating [`TextLayoutInfo`]. + pub(crate) buffer: CosmicBuffer, + /// Entities for all text spans in the block, including the root-level text. + pub(crate) entities: SmallVec<[TextEntity; 1]>, + /// Flag set when any spans in this block have changed. + pub(crate) is_changed: bool, +} + +impl ComputedTextBlock { + pub fn iter_entities(&self) -> impl Iterator { + self.entities.iter() + } +} + +impl Default for ComputedTextBlock { + fn default() -> Self { + Self { + buffer: CosmicBuffer::default(), + entities: SmallVec::default(), + is_changed: true, + } + } +} + +/// Component with text format settings for a block of text. +/// +/// A block of text is composed of text spans, which each have a separate [`TextStyle`]. Text spans associated +/// with a text block are collected into [`ComputedTextBlock`] for layout, and then inserted to [`TextLayoutInfo`] +/// for rendering. +/// +/// See [`Text2d`] for the core component of 2d text, and `Text` in `bevy_ui` for UI text. +#[derive(Component)] +#[requires(ComputedTextBlock, TextLayoutInfo)] +pub struct TextBlock +{ + /// The text's internal alignment. + /// Should not affect its position within a container. + pub justify: JustifyText, + /// How the text should linebreak when running out of the bounds determined by `max_size`. + pub line_break: LineBreak, + /// The antialiasing method to use when rendering text. + pub font_smoothing: FontSmoothing, +} + + + /// A component that is the entry point for rendering text. /// /// It contains all of the text value and styling information. #[derive(Component, Debug, Clone, Default, Reflect)] #[reflect(Component, Default, Debug)] -pub struct Text { +pub struct OldText { /// The text's sections pub sections: Vec, /// The text's internal alignment. @@ -40,7 +91,7 @@ pub struct Text { pub font_smoothing: FontSmoothing, } -impl Text { +impl OldText { /// Constructs a [`Text`] with a single section. /// /// ``` @@ -137,15 +188,15 @@ impl Text { /// Contains the value of the text in a section and how it should be styled. #[derive(Debug, Default, Clone, Reflect)] #[reflect(Default)] -pub struct TextSection { +pub struct OldTextSection { /// The content (in `String` form) of the text in the section. pub value: String, /// The style of the text in the section, including the font face, font size, and color. pub style: TextStyle, } -impl TextSection { - /// Create a new [`TextSection`]. +impl OldTextSection { + /// Create a new [`OldTextSection`]. pub fn new(value: impl Into, style: TextStyle) -> Self { Self { value: value.into(), @@ -153,7 +204,7 @@ impl TextSection { } } - /// Create an empty [`TextSection`] from a style. Useful when the value will be set dynamically. + /// Create an empty [`OldTextSection`] from a style. Useful when the value will be set dynamically. pub const fn from_style(style: TextStyle) -> Self { Self { value: String::new(), @@ -162,7 +213,7 @@ impl TextSection { } } -impl From<&str> for TextSection { +impl From<&str> for OldTextSection { fn from(value: &str) -> Self { Self { value: value.into(), @@ -171,7 +222,7 @@ impl From<&str> for TextSection { } } -impl From for TextSection { +impl From for OldTextSection { fn from(value: String) -> Self { Self { value, @@ -216,9 +267,9 @@ impl From for cosmic_text::Align { } } -#[derive(Clone, Debug, Reflect)] -/// `TextStyle` determines the style of the text in a section, specifically +/// `TextStyle` determines the style of a text span within a [`TextBlock`], specifically /// the font face, the font size, and the color. +#[derive(Component, Clone, Debug, Reflect)] pub struct TextStyle { /// The specific font face to use, as a `Handle` to a [`Font`] asset. /// diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index db175fd09d436..a4088a00014bd 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -27,10 +27,27 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; -/// The bundle of components needed to draw text in a 2D scene via a `Camera2d`. + +/// The top-level 2D text component. +/// +/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into +/// a [`TextBlock`]. See [`TextSpan2d`] for the component used by children of entities with [`Text2d`]. +#[derive(Component, Clone, Debug, Default)] +#[require(TextBlock, TextStyle, TextBounds, Anchor, SpriteSource, Visibility, Transform)] +pub struct Text2d(pub String); + +/// A span of text in a tree of spans under an entity with [`Text2d`]. +/// +/// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. +#[derive(Component, Clone, Debug, Default)] +#[require(TextStyle, Visibility = Visibility::Hidden, Transform)] +pub struct TextSpan2d(pub String); + + +/// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) #[derive(Bundle, Clone, Debug, Default)] -pub struct Text2dBundle { +pub struct Text2dBundleOLD { /// Contains the text. /// /// With `Text2dBundle` the alignment field of `Text` only affects the internal alignment of a block of text and not its diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 957a93024be85..eb1f4dc21da7f 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -109,109 +109,6 @@ pub struct ImageBundle { pub z_index: ZIndex, } -#[cfg(feature = "bevy_text")] -/// A UI node that is text -/// -/// The positioning of this node is controlled by the UI layout system. If you need manual control, -/// use [`Text2dBundle`](bevy_text::Text2dBundle). -#[derive(Bundle, Debug, Default)] -pub struct TextBundle { - /// Describes the logical size of the node - pub node: Node, - /// Styles which control the layout (size and position) of the node and its children - /// In some cases these styles also affect how the node drawn/painted. - pub style: Style, - /// Contains the text of the node - pub text: Text, - /// Cached cosmic buffer for layout - pub buffer: CosmicBuffer, - /// Text layout information - pub text_layout_info: TextLayoutInfo, - /// Text system flags - pub text_flags: TextFlags, - /// The calculated size based on the given image - pub calculated_size: ContentSize, - /// Whether this node should block interaction with lower nodes - pub focus_policy: FocusPolicy, - /// The transform of the node - /// - /// This component is automatically managed by the UI layout system. - /// To alter the position of the `TextBundle`, use the properties of the [`Style`] component. - pub transform: Transform, - /// The global transform of the node - /// - /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. - pub global_transform: GlobalTransform, - /// Describes the visibility properties of the node - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, - /// Indicates the depth at which the node should appear in the UI - pub z_index: ZIndex, - /// The background color that will fill the containing node - pub background_color: BackgroundColor, -} - -#[cfg(feature = "bevy_text")] -impl TextBundle { - /// Create a [`TextBundle`] from a single section. - /// - /// See [`Text::from_section`] for usage. - pub fn from_section(value: impl Into, style: TextStyle) -> Self { - Self { - text: Text::from_section(value, style), - ..Default::default() - } - } - - /// Create a [`TextBundle`] from a list of sections. - /// - /// See [`Text::from_sections`] for usage. - pub fn from_sections(sections: impl IntoIterator) -> Self { - Self { - text: Text::from_sections(sections), - ..Default::default() - } - } - - /// Returns this [`TextBundle`] with a new [`JustifyText`] on [`Text`]. - pub const fn with_text_justify(mut self, justify: JustifyText) -> Self { - self.text.justify = justify; - self - } - - /// Returns this [`TextBundle`] with a new [`Style`]. - pub fn with_style(mut self, style: Style) -> Self { - self.style = style; - self - } - - /// Returns this [`TextBundle`] with a new [`BackgroundColor`]. - pub const fn with_background_color(mut self, color: Color) -> Self { - 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 = LineBreak::NoWrap; - self - } -} - -#[cfg(feature = "bevy_text")] -impl From for TextBundle -where - I: Into, -{ - fn from(value: I) -> Self { - Self::from_sections(vec![value.into()]) - } -} - /// A UI node that is a button /// /// # Extra behaviours diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 45a54dac98c8f..bc87e36942f78 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -23,27 +23,162 @@ use bevy_text::{ use bevy_utils::{tracing::error, Entry}; use taffy::style::AvailableSpace; -/// Text system flags +/// UI text system flags. /// /// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default, Debug)] -pub struct TextFlags { - /// If set a new measure function for the text node will be created - needs_new_measure_func: bool, +pub struct TextNodeFlags +{ + /// If set a new measure function for the text node will be created. + needs_measure_fn: bool, /// If set the text will be recomputed needs_recompute: bool, } -impl Default for TextFlags { +impl Default for TextNodeFlags { fn default() -> Self { Self { - needs_new_measure_func: true, + needs_measure_fn: true, needs_recompute: true, } } } +/// The top-level UI text component. +/// +/// If a string is specified, it behaves as if it has a "first" TextSpan child. +#[derive(Component)] +#[require( + TextBlock, + TextStyle, + Node, + Style, // TODO: Remove when Node uses required components. + ContentSize, // TODO: Remove when Node uses required components. + FocusPolicy, // TODO: Remove when Node uses required components. + ZIndex, // TODO: Remove when Node uses required components. + BackgroundColor, // TODO: Remove when Node uses required components. + TextNodeFlags, + Visibility, // TODO: Remove when Node uses required components. + Transform // TODO: Remove when Node uses required components. +)] +pub struct TextNEW(pub String); + +/// A span of text in a tree of spans under an entity with [`Text`]. +/// +/// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. +#[derive(Component)] +#[require(TextStyle, GhostNode, Visibility = Visibility::Hidden)] +pub struct TextSpan(pub String); + + + +#[cfg(feature = "bevy_text")] +/// A UI node that is text +/// +/// The positioning of this node is controlled by the UI layout system. If you need manual control, +/// use [`Text2dBundle`](bevy_text::Text2dBundle). +#[derive(Bundle, Debug, Default)] +pub struct TextBundle { + /// Describes the logical size of the node + pub node: Node, + /// Styles which control the layout (size and position) of the node and its children + /// In some cases these styles also affect how the node drawn/painted. + pub style: Style, + /// Contains the text of the node + pub text: Text, + /// Cached cosmic buffer for layout + pub buffer: CosmicBuffer, + /// Text layout information + pub text_layout_info: TextLayoutInfo, + /// Text system flags + pub text_flags: TextFlags, + /// The calculated size based on the given image + pub calculated_size: ContentSize, + /// Whether this node should block interaction with lower nodes + pub focus_policy: FocusPolicy, + /// The transform of the node + /// + /// This component is automatically managed by the UI layout system. + /// To alter the position of the `TextBundle`, use the properties of the [`Style`] component. + pub transform: Transform, + /// The global transform of the node + /// + /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. + pub global_transform: GlobalTransform, + /// Describes the visibility properties of the node + pub visibility: Visibility, + /// Inherited visibility of an entity. + pub inherited_visibility: InheritedVisibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub view_visibility: ViewVisibility, + /// Indicates the depth at which the node should appear in the UI + pub z_index: ZIndex, + /// The background color that will fill the containing node + pub background_color: BackgroundColor, +} + +#[cfg(feature = "bevy_text")] +impl TextBundle { + /// Create a [`TextBundle`] from a single section. + /// + /// See [`Text::from_section`] for usage. + pub fn from_section(value: impl Into, style: TextStyle) -> Self { + Self { + text: Text::from_section(value, style), + ..Default::default() + } + } + + /// Create a [`TextBundle`] from a list of sections. + /// + /// See [`Text::from_sections`] for usage. + pub fn from_sections(sections: impl IntoIterator) -> Self { + Self { + text: Text::from_sections(sections), + ..Default::default() + } + } + + /// Returns this [`TextBundle`] with a new [`JustifyText`] on [`Text`]. + pub const fn with_text_justify(mut self, justify: JustifyText) -> Self { + self.text.justify = justify; + self + } + + /// Returns this [`TextBundle`] with a new [`Style`]. + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + + /// Returns this [`TextBundle`] with a new [`BackgroundColor`]. + pub const fn with_background_color(mut self, color: Color) -> Self { + 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 = LineBreak::NoWrap; + self + } +} + +#[cfg(feature = "bevy_text")] +impl From for TextBundle +where + I: Into, +{ + fn from(value: I) -> Self { + Self::from_sections(vec![value.into()]) + } +} + + + +/// Text measurement for UI layout. See [`NodeMeasure`]. pub struct TextMeasure { pub info: TextMeasureInfo, } @@ -133,12 +268,12 @@ fn create_text_measure( } // Text measure func created successfully, so set `TextFlags` to schedule a recompute - text_flags.needs_new_measure_func = false; + text_flags.needs_measure_fn = false; text_flags.needs_recompute = true; } Err(TextError::NoSuchFont) => { // Try again next frame - text_flags.needs_new_measure_func = true; + text_flags.needs_measure_fn = true; } Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); @@ -197,8 +332,8 @@ pub fn measure_text_system( ), }; if last_scale_factors.get(&camera_entity) != Some(&scale_factor) - || text.is_changed() - || text_flags.needs_new_measure_func + || text.is_changed() // TODO: get needs_recompute from ComputedTextBlock + || text_flags.needs_measure_fn || content_size.is_added() { let text_alignment = text.justify; From 88ac373620bbcb682d2aa6fdebca9877f9b09b36 Mon Sep 17 00:00:00 2001 From: koe Date: Tue, 1 Oct 2024 19:34:51 -0500 Subject: [PATCH 02/41] rework WIP --- crates/bevy_text/src/pipeline.rs | 12 +- crates/bevy_text/src/text.rs | 60 +++++---- crates/bevy_text/src/text2d.rs | 25 +++- crates/bevy_ui/src/lib.rs | 4 +- crates/bevy_ui/src/node_bundles.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 189 +++++++++-------------------- 6 files changed, 122 insertions(+), 170 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index e63cd2d76de89..ce838b1804393 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -193,7 +193,7 @@ impl TextPipeline { &mut self, layout_info: &mut TextLayoutInfo, fonts: &Assets, - sections: &[TextSection], + text_spans: impl Iterator, scale_factor: f64, text_alignment: JustifyText, linebreak: LineBreak, @@ -216,9 +216,7 @@ impl TextPipeline { self.update_buffer( fonts, - sections - .iter() - .map(|section| (section.value.as_str(), §ion.style)), + text_spans, linebreak, bounds, scale_factor, @@ -314,7 +312,7 @@ impl TextPipeline { &mut self, entity: Entity, fonts: &Assets, - sections: &[TextSection], + text_spans: impl Iterator, scale_factor: f64, linebreak: LineBreak, buffer: &mut CosmicBuffer, @@ -325,9 +323,7 @@ impl TextPipeline { self.update_buffer( fonts, - sections - .iter() - .map(|section| (section.value.as_str(), §ion.style)), + text_spans, linebreak, MIN_WIDTH_CONTENT_BOUNDS, scale_factor, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 38feae1304e8b..38386bdf59fbb 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -25,10 +25,15 @@ impl Default for CosmicBuffer { /// Computed information for a [`TextBlock`]. /// -/// Automatically updated. +/// Automatically updated by 2d and UI text systems. #[derive(Component, Debug, Clone)] pub struct ComputedTextBlock { /// Buffer for managing text layout and creating [`TextLayoutInfo`]. + /// + /// This is private because buffer contents are always refreshed from ECS state when writing glyphs to + /// `TextLayoutInfo`. If you want to control the buffer contents manually or use the `cosmic-text` + /// editor, then you need to not use `TextBlock` and instead manually implement the conversion to + /// `TextLayoutInfo`. pub(crate) buffer: CosmicBuffer, /// Entities for all text spans in the block, including the root-level text. pub(crate) entities: SmallVec<[TextEntity; 1]>, @@ -40,6 +45,10 @@ impl ComputedTextBlock { pub fn iter_entities(&self) -> impl Iterator { self.entities.iter() } + + pub fn is_changed(&self) -> bool { + self.is_changed + } } impl Default for ComputedTextBlock { @@ -54,12 +63,13 @@ impl Default for ComputedTextBlock { /// Component with text format settings for a block of text. /// -/// A block of text is composed of text spans, which each have a separate [`TextStyle`]. Text spans associated -/// with a text block are collected into [`ComputedTextBlock`] for layout, and then inserted to [`TextLayoutInfo`] -/// for rendering. +/// A block of text is composed of text spans, which each have a separate string value and [`TextStyle`]. Text +/// spans associated with a text block are collected into [`ComputedTextBlock`] for layout, and then inserted +/// to [`TextLayoutInfo`] for rendering. /// /// See [`Text2d`] for the core component of 2d text, and `Text` in `bevy_ui` for UI text. -#[derive(Component)] +#[derive(Component, Debug, Clone, Default, Reflect)] +#[reflect(Component, Default, Debug)] #[requires(ComputedTextBlock, TextLayoutInfo)] pub struct TextBlock { @@ -72,6 +82,27 @@ pub struct TextBlock pub font_smoothing: FontSmoothing, } +impl TextBlock { + /// Returns this [`TextBlock`] with a new [`JustifyText`]. + pub const fn with_justify(mut self, justify: JustifyText) -> Self { + self.justify = justify; + self + } + + /// Returns this [`TextBlock`] 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 = LineBreak::NoWrap; + self + } + + /// Returns this [`TextBlock`] with the specified [`FontSmoothing`]. + pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self { + self.font_smoothing = font_smoothing; + self + } +} + /// A component that is the entry point for rendering text. @@ -164,25 +195,6 @@ impl OldText { ..default() } } - - /// Returns this [`Text`] with a new [`JustifyText`]. - pub const fn with_justify(mut self, justify: JustifyText) -> Self { - self.justify = justify; - 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 = LineBreak::NoWrap; - self - } - - /// Returns this [`Text`] with the specified [`FontSmoothing`]. - pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self { - self.font_smoothing = font_smoothing; - self - } } /// Contains the value of the text in a section and how it should be styled. diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index a4088a00014bd..e2d994c3f848a 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -27,16 +27,35 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; - /// The top-level 2D text component. /// +/// Adding `Text2d` to an entity will pull in required components for setting up 2d text. +/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) +/// /// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into /// a [`TextBlock`]. See [`TextSpan2d`] for the component used by children of entities with [`Text2d`]. -#[derive(Component, Clone, Debug, Default)] +/// +/// With `Text2d` the `justify` field of [`TextBlock`] only affects the internal alignment of a block of text and not its +/// relative position which is controlled by the [`Anchor`] component. +/// This means that for a block of text consisting of only one line that doesn't wrap, the `justify` field will have no effect. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)] #[require(TextBlock, TextStyle, TextBounds, Anchor, SpriteSource, Visibility, Transform)] +#[reflect(Component, Default, Debug)] pub struct Text2d(pub String); -/// A span of text in a tree of spans under an entity with [`Text2d`]. +impl From<&str> for Text2d { + fn from(value: &str) -> Self { + Self(String::from(value)) + } +} + +impl From for Text2d { + fn from(value: String) -> Self { + Self(value) + } +} + +/// A span of 2d text in a tree of spans under an entity with [`Text2d`]. /// /// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. #[derive(Component, Clone, Debug, Default)] diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index f85b4b3a5454d..82e2d211b0913 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -217,11 +217,11 @@ impl Plugin for UiPlugin { /// A function that should be called from [`UiPlugin::build`] when [`bevy_text`] is enabled. #[cfg(feature = "bevy_text")] fn build_text_interop(app: &mut App) { - use crate::widget::TextFlags; + use crate::widget::TextNodeFlags; use bevy_text::TextLayoutInfo; app.register_type::() - .register_type::(); + .register_type::(); app.add_systems( PostUpdate, diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index eb1f4dc21da7f..a01eed12b10f4 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -11,7 +11,7 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; #[cfg(feature = "bevy_text")] use { - crate::widget::TextFlags, + crate::widget::TextNodeFlags, bevy_color::Color, bevy_text::{ CosmicBuffer, JustifyText, LineBreak, Text, TextLayoutInfo, TextSection, TextStyle, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index bc87e36942f78..96a8099b58e4a 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -47,136 +47,48 @@ impl Default for TextNodeFlags { /// The top-level UI text component. /// -/// If a string is specified, it behaves as if it has a "first" TextSpan child. -#[derive(Component)] +/// Adding `TextNEW` to an entity will pull in required components for setting up a UI text node. +/// +/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into +/// a [`TextBlock`]. See [`TextSpan`] for the component used by children of entities with `TextNEW`. +/// +/// Note that [`Transform`] on this entity is managed automatically by the UI layout system. +#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)] +#[reflect(Component, Default, Debug)] #[require( TextBlock, TextStyle, + TextNodeFlags, Node, Style, // TODO: Remove when Node uses required components. ContentSize, // TODO: Remove when Node uses required components. FocusPolicy, // TODO: Remove when Node uses required components. ZIndex, // TODO: Remove when Node uses required components. BackgroundColor, // TODO: Remove when Node uses required components. - TextNodeFlags, Visibility, // TODO: Remove when Node uses required components. Transform // TODO: Remove when Node uses required components. )] pub struct TextNEW(pub String); -/// A span of text in a tree of spans under an entity with [`Text`]. -/// -/// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. -#[derive(Component)] -#[require(TextStyle, GhostNode, Visibility = Visibility::Hidden)] -pub struct TextSpan(pub String); - - - -#[cfg(feature = "bevy_text")] -/// A UI node that is text -/// -/// The positioning of this node is controlled by the UI layout system. If you need manual control, -/// use [`Text2dBundle`](bevy_text::Text2dBundle). -#[derive(Bundle, Debug, Default)] -pub struct TextBundle { - /// Describes the logical size of the node - pub node: Node, - /// Styles which control the layout (size and position) of the node and its children - /// In some cases these styles also affect how the node drawn/painted. - pub style: Style, - /// Contains the text of the node - pub text: Text, - /// Cached cosmic buffer for layout - pub buffer: CosmicBuffer, - /// Text layout information - pub text_layout_info: TextLayoutInfo, - /// Text system flags - pub text_flags: TextFlags, - /// The calculated size based on the given image - pub calculated_size: ContentSize, - /// Whether this node should block interaction with lower nodes - pub focus_policy: FocusPolicy, - /// The transform of the node - /// - /// This component is automatically managed by the UI layout system. - /// To alter the position of the `TextBundle`, use the properties of the [`Style`] component. - pub transform: Transform, - /// The global transform of the node - /// - /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. - pub global_transform: GlobalTransform, - /// Describes the visibility properties of the node - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, - /// Indicates the depth at which the node should appear in the UI - pub z_index: ZIndex, - /// The background color that will fill the containing node - pub background_color: BackgroundColor, -} - -#[cfg(feature = "bevy_text")] -impl TextBundle { - /// Create a [`TextBundle`] from a single section. - /// - /// See [`Text::from_section`] for usage. - pub fn from_section(value: impl Into, style: TextStyle) -> Self { - Self { - text: Text::from_section(value, style), - ..Default::default() - } - } - - /// Create a [`TextBundle`] from a list of sections. - /// - /// See [`Text::from_sections`] for usage. - pub fn from_sections(sections: impl IntoIterator) -> Self { - Self { - text: Text::from_sections(sections), - ..Default::default() - } - } - - /// Returns this [`TextBundle`] with a new [`JustifyText`] on [`Text`]. - pub const fn with_text_justify(mut self, justify: JustifyText) -> Self { - self.text.justify = justify; - self - } - - /// Returns this [`TextBundle`] with a new [`Style`]. - pub fn with_style(mut self, style: Style) -> Self { - self.style = style; - self - } - - /// Returns this [`TextBundle`] with a new [`BackgroundColor`]. - pub const fn with_background_color(mut self, color: Color) -> Self { - 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 = LineBreak::NoWrap; - self +impl From<&str> for TextNEW { + fn from(value: &str) -> Self { + Self(String::from(value)) } } -#[cfg(feature = "bevy_text")] -impl From for TextBundle -where - I: Into, -{ - fn from(value: I) -> Self { - Self::from_sections(vec![value.into()]) +impl From for TextNEW { + fn from(value: String) -> Self { + Self(value) } } - +/// A span of UI text in a tree of spans under an entity with [`Text`]. +/// +/// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. +#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)] +#[reflect(Component, Default, Debug)] +#[require(TextStyle, GhostNode, Visibility = Visibility::Hidden)] +pub struct TextSpan(pub String); /// Text measurement for UI layout. See [`NodeMeasure`]. pub struct TextMeasure { @@ -238,36 +150,36 @@ impl Measure for TextMeasure { #[allow(clippy::too_many_arguments)] #[inline] -fn create_text_measure( +fn create_text_measure<'a>( entity: Entity, fonts: &Assets, scale_factor: f64, - text: Ref, + spans: impl Iterator<(&'a str, &'a TextStyle)>, + block: Ref, text_pipeline: &mut TextPipeline, mut content_size: Mut, - mut text_flags: Mut, - buffer: &mut CosmicBuffer, - text_alignment: JustifyText, + mut text_flags: Mut, + mut computed: Mut, font_system: &mut CosmicFontSystem, ) { match text_pipeline.create_text_measure( entity, fonts, - &text.sections, + spans, scale_factor, - text.linebreak, - buffer, - text_alignment, + block.linebreak, + computed, + block.justify, font_system, ) { Ok(measure) => { - if text.linebreak == LineBreak::NoWrap { + if block.linebreak == LineBreak::NoWrap { content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); } else { content_size.set(NodeMeasure::Text(TextMeasure { info: measure })); } - // Text measure func created successfully, so set `TextFlags` to schedule a recompute + // Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute text_flags.needs_measure_fn = false; text_flags.needs_recompute = true; } @@ -303,20 +215,34 @@ pub fn measure_text_system( ( Entity, Ref, + Ref, + Ref, &mut ContentSize, - &mut TextFlags, + &mut TextNodeFlags, + &mut ComputedTextBlock, Option<&TargetCamera>, - &mut CosmicBuffer, + Option<&Children>, ), With, >, + spans: TextSpans, mut text_pipeline: ResMut, mut font_system: ResMut, ) { scale_factors_buffer.clear(); - for (entity, text, content_size, text_flags, camera, mut buffer) in &mut text_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + for ( + entity, + text, + text_style, + text_block, + content_size, + text_flags, + computed, + maybe_camera, + maybe_children + ) in &mut text_query { + let Some(camera_entity) = maybe_camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; @@ -336,17 +262,16 @@ pub fn measure_text_system( || text_flags.needs_measure_fn || content_size.is_added() { - let text_alignment = text.justify; create_text_measure( entity, &fonts, scale_factor.into(), - text, + spans.iter_from_base(text.as_str(), text_style, maybe_children), + text_block, &mut text_pipeline, content_size, text_flags, - buffer.as_mut(), - text_alignment, + computed, &mut font_system, ); } @@ -366,7 +291,7 @@ fn queue_text( inverse_scale_factor: f32, text: &Text, node: Ref, - mut text_flags: Mut, + mut text_flags: Mut, text_layout_info: Mut, buffer: &mut CosmicBuffer, font_system: &mut CosmicFontSystem, @@ -422,7 +347,7 @@ fn queue_text( } /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, -/// or when the `needs_recompute` field of [`TextFlags`] is set to true. +/// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true. /// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`]. /// /// ## World Resources @@ -445,7 +370,7 @@ pub fn text_system( Ref, &Text, &mut TextLayoutInfo, - &mut TextFlags, + &mut TextNodeFlags, Option<&TargetCamera>, &mut CosmicBuffer, )>, From 91be8cb5ee4270c33c1165604c515cd455f13d72 Mon Sep 17 00:00:00 2001 From: koe Date: Wed, 2 Oct 2024 01:55:39 -0500 Subject: [PATCH 03/41] text rework WIP --- crates/bevy_text/src/glyph.rs | 13 +-- crates/bevy_text/src/lib.rs | 20 ++-- crates/bevy_text/src/pipeline.rs | 96 +++++++++++------- crates/bevy_text/src/text.rs | 44 +++++++-- crates/bevy_text/src/text2d.rs | 106 ++++++++++---------- crates/bevy_text/src/text_span.rs | 158 ++++++++++++++++++++++++++++++ crates/bevy_ui/src/lib.rs | 10 +- crates/bevy_ui/src/render/mod.rs | 32 +++++- crates/bevy_ui/src/widget/text.rs | 151 ++++++++++++++++------------ 9 files changed, 448 insertions(+), 182 deletions(-) create mode 100644 crates/bevy_text/src/text_span.rs diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 3efd1ac8afe90..9db1072242b7f 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -19,8 +19,8 @@ pub struct PositionedGlyph { pub size: Vec2, /// Information about the glyph's atlas. pub atlas_info: GlyphAtlasInfo, - /// The index of the glyph in the [`Text`](crate::Text)'s sections. - pub section_index: usize, + /// The index of the glyph in the [`ComputedTextBlock`](crate::ComputedTextBlock)'s tracked spans. + pub span_index: usize, /// TODO: In order to do text editing, we need access to the size of glyphs and their index in the associated String. /// For example, to figure out where to place the cursor in an input box from the mouse's position. /// Without this, it's only possible in texts where each glyph is one byte. Cosmic text has methods for this @@ -30,17 +30,12 @@ pub struct PositionedGlyph { impl PositionedGlyph { /// Creates a new [`PositionedGlyph`] - pub fn new( - position: Vec2, - size: Vec2, - atlas_info: GlyphAtlasInfo, - section_index: usize, - ) -> Self { + pub fn new(position: Vec2, size: Vec2, atlas_info: GlyphAtlasInfo, span_index: usize) -> Self { Self { position, size, atlas_info, - section_index, + span_index, byte_index: 0, } } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index ee2c28a8e1166..e09416f4cc499 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -98,6 +98,10 @@ pub enum YAxisOrientation { BottomToTop, } +/// System set in [`PostUpdate`] where all 2d text update systems are executed. +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub struct Update2dText; + /// A convenient alias for `With`, for use with /// [`bevy_render::view::VisibleEntities`]. pub type WithText = With; @@ -105,28 +109,30 @@ pub type WithText = With; impl Plugin for TextPlugin { fn build(&self, app: &mut App) { app.init_asset::() - .register_type::() + .register_type::() + .register_type::() .register_type::() .init_asset_loader::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( PostUpdate, ( - calculate_bounds_text2d - .in_set(VisibilitySystems::CalculateBounds) - .after(update_text2d_layout), + remove_dropped_font_atlas_sets, + detect_text_needs_rerender::, update_text2d_layout - .after(remove_dropped_font_atlas_sets) // Potential conflict: `Assets` // In practice, they run independently since `bevy_render::camera_update_system` // will only ever observe its own render target, and `update_text2d_layout` // will never modify a pre-existing `Image` asset. .ambiguous_with(CameraUpdateSystem), - remove_dropped_font_atlas_sets, - ), + calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), + ) + .chain() + .in_set(Update2dText), ) .add_systems(Last, trim_cosmic_cache); diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index ce838b1804393..26775325fb3ab 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -71,6 +71,8 @@ pub struct TextPipeline { /// /// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10). spans_buffer: Vec<(usize, &'static str, &'static TextStyle, FontFaceInfo)>, + /// Buffered vec for collecting font ids for glyph assembly. + font_ids: Vec>, } impl TextPipeline { @@ -81,12 +83,12 @@ impl TextPipeline { pub fn update_buffer<'a>( &mut self, fonts: &Assets, - text_spans: impl Iterator, + mut text_spans: impl Iterator, linebreak: LineBreak, + justify: JustifyText, bounds: TextBounds, scale_factor: f64, - buffer: &mut CosmicBuffer, - alignment: JustifyText, + computed: &mut ComputedTextBlock, font_system: &mut CosmicFontSystem, ) -> Result<(), TextError> { let font_system = &mut font_system.0; @@ -100,7 +102,9 @@ impl TextPipeline { .map(|_| -> (usize, &str, &TextStyle, FontFaceInfo) { unreachable!() }) .collect(); - for (span_index, (span, style)) in text_spans.enumerate() { + computed.entities.clear(); + + for (span_index, (entity, depth, span, style)) in text_spans.enumerate() { // Return early if a font is not loaded yet. if !fonts.contains(style.font.id()) { spans.clear(); @@ -116,6 +120,9 @@ impl TextPipeline { return Err(TextError::NoSuchFont); } + // Save this span entity in the computed text block. + computed.entities.push(TextEntity { entity, depth }); + // Get max font size for use in cosmic Metrics. font_size = font_size.max(style.font_size); @@ -152,6 +159,7 @@ impl TextPipeline { }); // Update the buffer. + let buffer = &mut computed.buffer; buffer.set_metrics(font_system, metrics); buffer.set_size(font_system, bounds.width, bounds.height); @@ -170,7 +178,7 @@ impl TextPipeline { // PERF: https://github.com/pop-os/cosmic-text/issues/166: // Setting alignment afterwards appears to invalidate some layouting performed by `set_text` which is presumably not free? for buffer_line in buffer.lines.iter_mut() { - buffer_line.set_align(Some(alignment.into())); + buffer_line.set_align(Some(justify.into())); } buffer.shape_until_scroll(font_system, false); @@ -193,41 +201,50 @@ impl TextPipeline { &mut self, layout_info: &mut TextLayoutInfo, fonts: &Assets, - text_spans: impl Iterator, + text_spans: impl Iterator, scale_factor: f64, - text_alignment: JustifyText, - linebreak: LineBreak, - font_smoothing: FontSmoothing, + block: &TextBlock, bounds: TextBounds, font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, y_axis_orientation: YAxisOrientation, - buffer: &mut CosmicBuffer, + computed: &mut ComputedTextBlock, font_system: &mut CosmicFontSystem, swash_cache: &mut SwashCache, ) -> Result<(), TextError> { layout_info.glyphs.clear(); layout_info.size = Default::default(); - if sections.is_empty() { - return Ok(()); - } + // Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries. + computed.needs_rerender = false; - self.update_buffer( + // Extract font ids from the iterator while traversing it. + let mut font_ids = std::mem::take(&mut self.font_ids); + font_ids.clear(); + let text_spans = text_spans.inspect(|(_, _, _, style)| { + font_ids.push(style.font.id()); + }); + + let update_result = self.update_buffer( fonts, text_spans, - linebreak, + block.linebreak, + block.justify, bounds, scale_factor, - buffer, - text_alignment, + computed, font_system, - )?; + ); + if let Err(err) = update_result { + self.font_ids = font_ids; + return Err(err); + } + let buffer = &mut computed.buffer; let box_size = buffer_dimensions(buffer); - buffer + let result = buffer .layout_runs() .flat_map(|run| { run.glyphs @@ -237,7 +254,7 @@ impl TextPipeline { .try_for_each(|(layout_glyph, line_y)| { let mut temp_glyph; - let layout_glyph = if font_smoothing == FontSmoothing::None { + let layout_glyph = if block.font_smoothing == FontSmoothing::None { // If font smoothing is disabled, round the glyph positions and sizes, // effectively discarding all subpixel layout. temp_glyph = layout_glyph.clone(); @@ -253,15 +270,17 @@ impl TextPipeline { layout_glyph }; - let section_index = layout_glyph.metadata; + let span_index = layout_glyph.metadata; - let font_handle = sections[section_index].style.font.clone_weak(); - let font_atlas_set = font_atlas_sets.sets.entry(font_handle.id()).or_default(); + let font_atlas_set = font_atlas_sets + .sets + .entry(font_ids[span_index]) + .or_default(); let physical_glyph = layout_glyph.physical((0., 0.), 1.); let atlas_info = font_atlas_set - .get_glyph_atlas_info(physical_glyph.cache_key, font_smoothing) + .get_glyph_atlas_info(physical_glyph.cache_key, block.font_smoothing) .map(Ok) .unwrap_or_else(|| { font_atlas_set.add_glyph_to_atlas( @@ -270,7 +289,7 @@ impl TextPipeline { &mut font_system.0, &mut swash_cache.0, layout_glyph, - font_smoothing, + block.font_smoothing, ) })?; @@ -294,10 +313,16 @@ impl TextPipeline { // TODO: recreate the byte index, that keeps track of where a cursor is, // when glyphs are not limited to single byte representation, relevant for #1319 let pos_glyph = - PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, section_index); + PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, span_index); layout_info.glyphs.push(pos_glyph); Ok(()) - })?; + }); + + // Return the scratch vec. + self.font_ids = font_ids; + + // Check result. + result?; layout_info.size = box_size; Ok(()) @@ -312,23 +337,26 @@ impl TextPipeline { &mut self, entity: Entity, fonts: &Assets, - text_spans: impl Iterator, + text_spans: impl Iterator, scale_factor: f64, - linebreak: LineBreak, - buffer: &mut CosmicBuffer, - text_alignment: JustifyText, + block: &TextBlock, + computed: &mut ComputedTextBlock, font_system: &mut CosmicFontSystem, ) -> Result { const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0); + // Clear this here at the focal point of measured text rendering to ensure the field's lifecycle has + // strong boundaries. + computed.needs_rerender = false; + self.update_buffer( fonts, text_spans, - linebreak, + block.linebreak, + block.justify, MIN_WIDTH_CONTENT_BOUNDS, scale_factor, - buffer, - text_alignment, + computed, font_system, )?; diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 38386bdf59fbb..dc64990904cdb 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -23,6 +23,16 @@ impl Default for CosmicBuffer { } } +/// A sub-entity of a [`TextBlock`]. +/// +/// Returned by [`ComputedTextBlock::entities`]. +#[derive(Default, Debug, Copy, Clone)] +pub struct TextEntity { + pub entity: Entity, + /// Records the hierarchy depth of the entity within a `TextBlock`. + pub depth: usize, +} + /// Computed information for a [`TextBlock`]. /// /// Automatically updated by 2d and UI text systems. @@ -37,17 +47,33 @@ pub struct ComputedTextBlock { pub(crate) buffer: CosmicBuffer, /// Entities for all text spans in the block, including the root-level text. pub(crate) entities: SmallVec<[TextEntity; 1]>, - /// Flag set when any spans in this block have changed. - pub(crate) is_changed: bool, + /// Flag set when any change has been made to this block that should cause it to be rerendered. + /// + /// Includes: + /// - [`TextBlock`] changes. + /// - [`TextStyle`] or `Text2d`/`Text`/`TextSpan2d`/`TextSpan` changes anywhere in the block's entity hierarchy. + // TODO: This encompasses both structural changes like font size or justification and non-structural + // changes like text color and font smoothing. This field currently causes UI to 'remeasure' text, even if + // the actual changes are non-structural and can be handled by only rerendering and not remeasuring. A full + // solution would probably require splitting TextBlock and TextStyle into structural/non-structural + // components for more granular change detection. A cost/benefit analysis is needed. + pub(crate) needs_rerender: bool, } impl ComputedTextBlock { - pub fn iter_entities(&self) -> impl Iterator { + /// Accesses entities in this block. + /// + /// Can be used to look up [`TextStyle`] components for glyphs in [`TextLayoutInfo`] using the `span_index` + /// stored there. + pub fn entities(&self) -> &[TextEntity] { self.entities.iter() } - pub fn is_changed(&self) -> bool { - self.is_changed + /// Indicates if the text needs to be refreshed in [`TextLayoutInfo`]. + /// + /// Updated automatically by [`detect_text_needs_rerender`](crate::detect_text_needs_rerender). + pub fn needs_rerender(&self) -> bool { + self.needs_rerender } } @@ -56,7 +82,7 @@ impl Default for ComputedTextBlock { Self { buffer: CosmicBuffer::default(), entities: SmallVec::default(), - is_changed: true, + needs_rerender: true, } } } @@ -71,8 +97,7 @@ impl Default for ComputedTextBlock { #[derive(Component, Debug, Clone, Default, Reflect)] #[reflect(Component, Default, Debug)] #[requires(ComputedTextBlock, TextLayoutInfo)] -pub struct TextBlock -{ +pub struct TextBlock { /// The text's internal alignment. /// Should not affect its position within a container. pub justify: JustifyText, @@ -103,8 +128,6 @@ impl TextBlock { } } - - /// A component that is the entry point for rendering text. /// /// It contains all of the text value and styling information. @@ -282,6 +305,7 @@ impl From for cosmic_text::Align { /// `TextStyle` determines the style of a text span within a [`TextBlock`], specifically /// the font face, the font size, and the color. #[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Default, Debug)] pub struct TextStyle { /// The specific font face to use, as a `Handle` to a [`Font`] asset. /// diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index e2d994c3f848a..0a5ef6b416d04 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -39,8 +39,16 @@ use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; /// relative position which is controlled by the [`Anchor`] component. /// This means that for a block of text consisting of only one line that doesn't wrap, the `justify` field will have no effect. #[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)] -#[require(TextBlock, TextStyle, TextBounds, Anchor, SpriteSource, Visibility, Transform)] #[reflect(Component, Default, Debug)] +#[require( + TextBlock, + TextStyle, + TextBounds, + Anchor, + SpriteSource, + Visibility, + Transform +)] pub struct Text2d(pub String); impl From<&str> for Text2d { @@ -58,46 +66,15 @@ impl From for Text2d { /// A span of 2d text in a tree of spans under an entity with [`Text2d`]. /// /// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. -#[derive(Component, Clone, Debug, Default)] +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default, Debug)] #[require(TextStyle, Visibility = Visibility::Hidden, Transform)] pub struct TextSpan2d(pub String); - -/// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. -/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) -#[derive(Bundle, Clone, Debug, Default)] -pub struct Text2dBundleOLD { - /// Contains the text. - /// - /// With `Text2dBundle` the alignment field of `Text` only affects the internal alignment of a block of text and not its - /// relative position which is controlled by the `Anchor` component. - /// This means that for a block of text consisting of only one line that doesn't wrap, the `alignment` field will have no effect. - pub text: Text, - /// Cached buffer for layout with cosmic-text - pub buffer: CosmicBuffer, - /// How the text is positioned relative to its transform. - /// - /// `text_anchor` does not affect the internal alignment of the block of text, only - /// its position. - pub text_anchor: Anchor, - /// The maximum width and height of the text. - pub text_2d_bounds: TextBounds, - /// The transform of the text. - pub transform: Transform, - /// The global transform of the text. - pub global_transform: GlobalTransform, - /// The visibility properties of the text. - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, - /// Contains the size of the text and its glyph's position and scale data. Generated via [`TextPipeline::queue_text`] - pub text_layout_info: TextLayoutInfo, - /// Marks that this is a [`SpriteSource`]. - /// - /// This is needed for visibility computation to work properly. - pub sprite_source: SpriteSource, +impl TextSpanReader for TextSpan2d { + fn read_span(&self) -> &str { + self.as_str() + } } /// This system extracts the sprites from the 2D text components and adds them to the @@ -111,12 +88,13 @@ pub fn extract_text2d_sprite( Query<( Entity, &ViewVisibility, - &Text, + &ComputedTextBlock, &TextLayoutInfo, &Anchor, &GlobalTransform, )>, >, + text_styles: Extract>, ) { // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows @@ -125,8 +103,14 @@ pub fn extract_text2d_sprite( .unwrap_or(1.0); let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.)); - for (original_entity, view_visibility, text, text_layout_info, anchor, global_transform) in - text2d_query.iter() + for ( + original_entity, + view_visibility, + computed_block, + text_layout_info, + anchor, + global_transform, + ) in text2d_query.iter() { if !view_visibility.get() { continue; @@ -138,17 +122,25 @@ pub fn extract_text2d_sprite( * GlobalTransform::from_translation(alignment_translation.extend(0.)) * scaling; let mut color = LinearRgba::WHITE; - let mut current_section = usize::MAX; + let mut current_span = usize::MAX; for PositionedGlyph { position, atlas_info, - section_index, + span_index, .. } in &text_layout_info.glyphs { - if *section_index != current_section { - color = LinearRgba::from(text.sections[*section_index].style.color); - current_section = *section_index; + if *span_index != current_span { + color = text_styles + .get( + computed_block + .entities() + .get(*span_index) + .unwrap_or(Entity::PLACEHOLDER), + ) + .map(|style| LinearRgba::from(style.color)) + .unwrap_or_default(); + current_span = *span_index; } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); @@ -190,11 +182,13 @@ pub fn update_text2d_layout( mut text_pipeline: ResMut, mut text_query: Query<( Entity, - Ref, + Ref, + Ref, Ref, &mut TextLayoutInfo, - &mut CosmicBuffer, + &mut ComputedTextBlock, )>, + mut spans: TextSpans, mut font_system: ResMut, mut swash_cache: ResMut, ) { @@ -209,8 +203,12 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, bounds, text_layout_info, mut buffer) in &mut text_query { - if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) { + for (entity, text, block, bounds, text_layout_info, mut computed) in &mut text_query { + if factor_changed + || computed.needs_rerender() + || bounds.is_changed() + || queue.remove(&entity) + { let text_bounds = TextBounds { width: if text.linebreak == LineBreak::NoWrap { None @@ -226,17 +224,15 @@ pub fn update_text2d_layout( match text_pipeline.queue_text( text_layout_info, &fonts, - &text.sections, + spans.iter_from_base(entity, text.as_str(), text_style, maybe_children), scale_factor.into(), - text.justify, - text.linebreak, - text.font_smoothing, + block, text_bounds, &mut font_atlas_sets, &mut texture_atlases, &mut textures, YAxisOrientation::BottomToTop, - buffer.as_mut(), + computed.as_mut(), &mut font_system, &mut swash_cache, ) { diff --git a/crates/bevy_text/src/text_span.rs b/crates/bevy_text/src/text_span.rs new file mode 100644 index 0000000000000..78177eff8592c --- /dev/null +++ b/crates/bevy_text/src/text_span.rs @@ -0,0 +1,158 @@ + +/// Helper trait for setting up [`TextSpanPlugin`]. +pub trait TextSpanReader: Component { + /// Gets the text span's string. + fn read_span(&self) -> &str; +} + +#[derive(Resource, Default)] +pub(crate) struct TextSpansScratch { + stack: Vec<(&'static Children, usize)> +} + +#[derive(SystemParam)] +pub struct TextSpans<'w, 's, T: TextSpanReader> { + scratch: ResMut<'w, TextSpansScratch>, + spans: Query<'w, 's, (Entity, &'static T, &'static TextStyle, Option<&'static Children>)> +} + +impl<'w, 's, T: TextSpanReader> TextSpans<'w, 's, T> { + pub fn iter_from_base( + &mut self, + root_entity: Entity, + root_text: &str, + root_style: &TextStyle, + root_children: Option<&Children> + ) -> TextSpanIter<'w, 's> { + let mut stack = core::mem::take(&mut self.scratch.stack) + .into_iter() + .map(|_| -> (&Children, usize) { unreachable!() }) + .collect(); + + TextSpanIter{ + scratch: &mut self.scratch, + root: Some((root_entity, root_text, root_style, root_children)), + stack, + spans: &self.spans, + } + } +} + +// TODO: Use this iterator design in UiChildrenIter to reduce allocations. +pub struct TextSpanIter<'w, 's> { + scratch: &'s mut TextSpansScratch, + root: Option<(Entity, &'s str, &'s TextStyle, Option<&'s Children>)>, + /// Stack of (children, next index into children). + stack: Vec<(&'s Children, usize)>, + spans: &'s Query<'w, 's, (Entity, &'static T, &'static TextStyle, Option<&'static Children>)>, +} + +impl<'w, 's> Iterator for TextSpanIter<'w, 's> { + /// Item = (entity in text block, depth in the block, span text, span style). + type Item = (Entity, usize, &str, &TextStyle>); + fn next(&mut self) -> Option { + // Root + if let Some((entity, text, style, maybe_children)) = self.root.take() { + if let Some(children) = maybe_children { + self.stack.push((children, 0)); + } + return Some((entity, 0, text, style)); + } + + // Span + loop { + let Some((children, idx)) = self.stack.last_mut() else { return None }; + + loop { + let Some(child) = children.get(idx) else { break }; + + // Increment to prep the next entity in this stack level. + *idx += 1; + + let Some((entity, span, style, maybe_children)) = self.spans.get(*child) else { continue }; + + let depth = self.stack.len(); + if let Some(children) = maybe_children { + self.stack.push((children, 0)); + } + return Some((entity, depth, span.read_span(), style)); + } + + // All children at this stack entry have been iterated. + self.stack.pop(); + } + } +} + +impl<'w, 's> Drop for TextSpanIter { + fn drop(&mut self) { + // Return the internal stack. + self.stack.clear(); + self.scratch.stack = self.stack + .into_iter() + .map( + |_| -> (&'static Children, usize) { + unreachable!() + }, + ) + .collect(); + } +} + +/// Detects changes to text blocks and sets `ComputedTextBlock::should_rerender`. +pub fn detect_text_needs_rerender( + changed_roots: Query, Changed, Changed, Changed)>, + )>, + changed_roots_leaf: Query, Changed, Changed)>, + Without + )>, + changed_spans: Query< + &Parent, + Or<(Changed, Changed, Changed)>, + >, + changed_spans_leaf: Query< + &Parent, + ( + Or<(Changed, Changed)>, + Without + ) + >, + mut computed: Query<(Option<&Parent>, Option<&mut ComputedTextBlock>)>, +) +{ + // Root entity: + // - TextBlock changed. + // - TextStyle on root changed. + // - Root component changed. + // - Root children changed (can include additions and removals). + for root in changed_roots.iter().chain(changed_roots_leaf.iter()) { + // TODO: ComputedTextBlock *should* be here, log a warning? + let Ok((_, Some(mut computed))) = computed.get_mut(root) else { continue }; + computed.needs_rerender = true; + } + + // Span entity: + // - Span component changed. + // - Span TextStyle changed. + // - Span children changed (can include additions and removals). + for span_parent in changed_spans.iter().chain(changed_spans_leaf.iter()) { + let mut parent = span_parent; + + // Search for the nearest ancestor with ComputedTextBlock. + // Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited + // is outweighed by the expense of tracking visited spans. + loop { + // TODO: If this lookup fails then there is a hierarchy error. Log a warning? + let Ok((maybe_parent, maybe_computed)) = computed.get_mut(parent) else { break }; + if let Some(computed) = maybe_computed { + computed.needs_rerender = true; + break; + } + // TODO: If there is no parent then a span is floating without an owning TextBlock. Log a warning? + let Some(next_parent) = maybe_parent else { break }; + parent = next_parent; + } + } +} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 82e2d211b0913..ceeba29a884b8 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -221,12 +221,18 @@ fn build_text_interop(app: &mut App) { use bevy_text::TextLayoutInfo; app.register_type::() - .register_type::(); + .register_type::() + .register_type::() + .register_type::(); app.add_systems( PostUpdate, ( - widget::measure_text_system + ( + bevy_text::detect_text_needs_rerender::, + widget::measure_text_system, + ) + .chain() .in_set(UiSystem::Prepare) // Potential conflict: `Assets` // Since both systems will only ever insert new [`Image`] assets, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 76b6c1d1173bc..79e2b88deb6ad 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -593,18 +593,26 @@ pub fn extract_text_sections( &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, - &Text, + &ComputedTextBlock, &TextLayoutInfo, )>, >, + text_styles: Extract>, mapping: Extract>, ) { let mut start = 0; let mut end = 1; let default_ui_camera = default_ui_camera.get(); - for (uinode, global_transform, view_visibility, clip, camera, text, text_layout_info) in - &uinode_query + for ( + uinode, + global_transform, + view_visibility, + clip, + camera, + computed_block, + text_layout_info, + ) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else { continue; @@ -642,16 +650,30 @@ pub fn extract_text_sections( transform.translation = transform.translation.round(); transform.translation *= inverse_scale_factor; + let mut color = LinearRgba::WHITE; + let mut current_span = usize::MAX; for ( i, PositionedGlyph { position, atlas_info, - section_index, + span_index, .. - }, + } ) in text_layout_info.glyphs.iter().enumerate() { + if *span_index != current_span { + color = text_styles + .get( + computed_block + .entities() + .get(*span_index) + .unwrap_or(Entity::PLACEHOLDER), + ) + .map(|style| LinearRgba::from(style.color)) + .unwrap_or_default(); + current_span = *span_index; + } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect(); diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 96a8099b58e4a..9d19fc009022b 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -28,11 +28,10 @@ use taffy::style::AvailableSpace; /// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default, Debug)] -pub struct TextNodeFlags -{ +pub struct TextNodeFlags { /// If set a new measure function for the text node will be created. needs_measure_fn: bool, - /// If set the text will be recomputed + /// If set the text will be recomputed. needs_recompute: bool, } @@ -90,6 +89,12 @@ impl From for TextNEW { #[require(TextStyle, GhostNode, Visibility = Visibility::Hidden)] pub struct TextSpan(pub String); +impl TextSpanReader for TextSpan { + fn read_span(&self) -> &str { + self.as_str() + } +} + /// Text measurement for UI layout. See [`NodeMeasure`]. pub struct TextMeasure { pub info: TextMeasureInfo, @@ -167,9 +172,8 @@ fn create_text_measure<'a>( fonts, spans, scale_factor, - block.linebreak, + block, computed, - block.justify, font_system, ) { Ok(measure) => { @@ -225,7 +229,7 @@ pub fn measure_text_system( ), With, >, - spans: TextSpans, + mut spans: TextSpans, mut text_pipeline: ResMut, mut font_system: ResMut, ) { @@ -235,14 +239,17 @@ pub fn measure_text_system( entity, text, text_style, - text_block, + block, content_size, text_flags, computed, maybe_camera, - maybe_children - ) in &mut text_query { - let Some(camera_entity) = maybe_camera.map(TargetCamera::entity).or(default_ui_camera.get()) + maybe_children, + ) in &mut text_query + { + let Some(camera_entity) = maybe_camera + .map(TargetCamera::entity) + .or(default_ui_camera.get()) else { continue; }; @@ -257,8 +264,9 @@ pub fn measure_text_system( * ui_scale.0, ), }; + // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). if last_scale_factors.get(&camera_entity) != Some(&scale_factor) - || text.is_changed() // TODO: get needs_recompute from ComputedTextBlock + || computed.needs_rerender() || text_flags.needs_measure_fn || content_size.is_added() { @@ -267,7 +275,7 @@ pub fn measure_text_system( &fonts, scale_factor.into(), spans.iter_from_base(text.as_str(), text_style, maybe_children), - text_block, + block, &mut text_pipeline, content_size, text_flags, @@ -290,58 +298,60 @@ fn queue_text( scale_factor: f32, inverse_scale_factor: f32, text: &Text, + text_style: &TextStyle, + block: &TextBlock, + maybe_children: Option<&Children>, node: Ref, mut text_flags: Mut, text_layout_info: Mut, - buffer: &mut CosmicBuffer, + computed: &mut ComputedTextBlock, + spans: &mut TextSpans, font_system: &mut CosmicFontSystem, swash_cache: &mut SwashCache, ) { // Skip the text node if it is waiting for a new measure func - if !text_flags.needs_new_measure_func { - let physical_node_size = if text.linebreak == LineBreak::NoWrap { - // With `NoWrap` set, no constraints are placed on the width of the text. - TextBounds::UNBOUNDED - } else { - // `scale_factor` is already multiplied by `UiScale` - TextBounds::new( - node.unrounded_size.x * scale_factor, - node.unrounded_size.y * scale_factor, - ) - }; + if text_flags.needs_measure_fn { + return; + } - let text_layout_info = text_layout_info.into_inner(); - match text_pipeline.queue_text( - text_layout_info, - fonts, - &text.sections, - scale_factor.into(), - text.justify, - text.linebreak, - text.font_smoothing, - physical_node_size, - font_atlas_sets, - texture_atlases, - textures, - YAxisOrientation::TopToBottom, - buffer, - font_system, - swash_cache, - ) { - Err(TextError::NoSuchFont) => { - // There was an error processing the text layout, try again next frame - text_flags.needs_recompute = true; - } - Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.size.x = - scale_value(text_layout_info.size.x, inverse_scale_factor); - text_layout_info.size.y = - scale_value(text_layout_info.size.y, inverse_scale_factor); - text_flags.needs_recompute = false; - } + let physical_node_size = if text.linebreak == LineBreak::NoWrap { + // With `NoWrap` set, no constraints are placed on the width of the text. + TextBounds::UNBOUNDED + } else { + // `scale_factor` is already multiplied by `UiScale` + TextBounds::new( + node.unrounded_size.x * scale_factor, + node.unrounded_size.y * scale_factor, + ) + }; + + let text_layout_info = text_layout_info.into_inner(); + match text_pipeline.queue_text( + text_layout_info, + fonts, + spans.iter_from_base(text.as_str(), text_style, maybe_children), + scale_factor.into(), + block, + physical_node_size, + font_atlas_sets, + texture_atlases, + textures, + YAxisOrientation::TopToBottom, + computed, + font_system, + swash_cache, + ) { + Err(TextError::NoSuchFont) => { + // There was an error processing the text layout, try again next frame + text_flags.needs_recompute = true; + } + Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { + panic!("Fatal error when processing text: {e}."); + } + Ok(()) => { + text_layout_info.size.x = scale_value(text_layout_info.size.x, inverse_scale_factor); + text_layout_info.size.y = scale_value(text_layout_info.size.y, inverse_scale_factor); + text_flags.needs_recompute = false; } } } @@ -369,18 +379,35 @@ pub fn text_system( mut text_query: Query<( Ref, &Text, + &TextStyle, + &TextBlock, &mut TextLayoutInfo, &mut TextNodeFlags, + &mut ComputedTextBlock, Option<&TargetCamera>, - &mut CosmicBuffer, + Option<&Children>, )>, + mut spans: TextSpans, mut font_system: ResMut, mut swash_cache: ResMut, ) { scale_factors_buffer.clear(); - for (node, text, text_layout_info, text_flags, camera, mut buffer) in &mut text_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + for ( + node, + text, + text_style, + block, + text_layout_info, + text_flags, + mut computed, + maybe_camera, + maybe_children, + ) in &mut text_query + { + let Some(camera_entity) = maybe_camera + .map(TargetCamera::entity) + .or(default_ui_camera.get()) else { continue; }; @@ -410,10 +437,14 @@ pub fn text_system( scale_factor, inverse_scale_factor, text, + text_style, + block, + maybe_children, node, text_flags, text_layout_info, - buffer.as_mut(), + computed.as_mut(), + &mut spans, &mut font_system, &mut swash_cache, ); From 94b1b138dfc6ee3817895b1521150b041b96bf52 Mon Sep 17 00:00:00 2001 From: koe Date: Wed, 2 Oct 2024 15:08:13 -0500 Subject: [PATCH 04/41] cleanup WIP --- crates/bevy_text/src/text.rs | 179 +++++++----------------------- crates/bevy_text/src/text2d.rs | 55 ++++++++- crates/bevy_text/src/text_span.rs | 31 ++---- crates/bevy_ui/src/widget/text.rs | 55 +++++++++ 4 files changed, 158 insertions(+), 162 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index dc64990904cdb..9f7d7708a8b10 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -46,6 +46,8 @@ pub struct ComputedTextBlock { /// `TextLayoutInfo`. pub(crate) buffer: CosmicBuffer, /// Entities for all text spans in the block, including the root-level text. + /// + /// The [`TextEntity::depth`] field can be used to reconstruct the hierarchy. pub(crate) entities: SmallVec<[TextEntity; 1]>, /// Flag set when any change has been made to this block that should cause it to be rerendered. /// @@ -71,7 +73,8 @@ impl ComputedTextBlock { /// Indicates if the text needs to be refreshed in [`TextLayoutInfo`]. /// - /// Updated automatically by [`detect_text_needs_rerender`](crate::detect_text_needs_rerender). + /// Updated automatically by [`detect_text_needs_rerender`](crate::detect_text_needs_rerender) and cleared + /// by [`TextPipeline`] methods. pub fn needs_rerender(&self) -> bool { self.needs_rerender } @@ -102,167 +105,61 @@ pub struct TextBlock { /// Should not affect its position within a container. pub justify: JustifyText, /// How the text should linebreak when running out of the bounds determined by `max_size`. - pub line_break: LineBreak, + pub linebreak: LineBreak, /// The antialiasing method to use when rendering text. pub font_smoothing: FontSmoothing, } impl TextBlock { - /// Returns this [`TextBlock`] with a new [`JustifyText`]. - pub const fn with_justify(mut self, justify: JustifyText) -> Self { - self.justify = justify; - self + /// Makes a new [`TextBlock`]. + pub const fn new(justify: JustifyText, linebreak: LineBreak, font_smoothing: FontSmoothing) -> Self { + Self{ justify, linebreak, font_smoothing } } - /// Returns this [`TextBlock`] 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 = LineBreak::NoWrap; - self + /// Makes a new [`TextBlock`] with the specified [`JustifyText`]. + pub const fn new_with_justify(justify: JustifyText) -> Self { + Self::default().with_justify(justify) } - /// Returns this [`TextBlock`] with the specified [`FontSmoothing`]. - pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self { - self.font_smoothing = font_smoothing; - self + /// Makes a new [`TextBlock`] with the specified [`LineBreak`]. + pub const fn new_with_linebreak(linebreak: LineBreak) -> Self { + Self::default().with_linebreak(linebreak) } -} -/// A component that is the entry point for rendering text. -/// -/// It contains all of the text value and styling information. -#[derive(Component, Debug, Clone, Default, Reflect)] -#[reflect(Component, Default, Debug)] -pub struct OldText { - /// The text's sections - pub sections: Vec, - /// The text's internal alignment. - /// Should not affect its position within a container. - pub justify: JustifyText, - /// How the text should linebreak when running out of the bounds determined by `max_size` - pub linebreak: LineBreak, - /// The antialiasing method to use when rendering text. - pub font_smoothing: FontSmoothing, -} - -impl OldText { - /// Constructs a [`Text`] with a single section. - /// - /// ``` - /// # use bevy_asset::Handle; - /// # use bevy_color::Color; - /// # use bevy_text::{Font, Text, TextStyle, JustifyText}; - /// # - /// # let font_handle: Handle = Default::default(); - /// # - /// // Basic usage. - /// let hello_world = Text::from_section( - /// // Accepts a String or any type that converts into a String, such as &str. - /// "hello world!", - /// TextStyle { - /// font: font_handle.clone().into(), - /// font_size: 60.0, - /// color: Color::WHITE, - /// }, - /// ); - /// - /// let hello_bevy = Text::from_section( - /// "hello world\nand bevy!", - /// TextStyle { - /// font: font_handle.into(), - /// font_size: 60.0, - /// color: Color::WHITE, - /// }, - /// ) // You can still add text justifaction. - /// .with_justify(JustifyText::Center); - /// ``` - pub fn from_section(value: impl Into, style: TextStyle) -> Self { - Self { - sections: vec![TextSection::new(value, style)], - ..default() - } + /// Makes a new [`TextBlock`] with the specified [`FontSmoothing`]. + pub const fn new_with_font_smoothing(font_smoothing: FontSmoothing) -> Self { + Self::default().with_font_smoothing(font_smoothing) } - /// Constructs a [`Text`] from a list of sections. - /// - /// ``` - /// # use bevy_asset::Handle; - /// # use bevy_color::Color; - /// # use bevy_color::palettes::basic::{RED, BLUE}; - /// # use bevy_text::{Font, Text, TextStyle, TextSection}; - /// # - /// # let font_handle: Handle = Default::default(); - /// # - /// let hello_world = Text::from_sections([ - /// TextSection::new( - /// "Hello, ", - /// TextStyle { - /// font: font_handle.clone().into(), - /// font_size: 60.0, - /// color: BLUE.into(), - /// }, - /// ), - /// TextSection::new( - /// "World!", - /// TextStyle { - /// font: font_handle.into(), - /// font_size: 60.0, - /// color: RED.into(), - /// }, - /// ), - /// ]); - /// ``` - pub fn from_sections(sections: impl IntoIterator) -> Self { - Self { - sections: sections.into_iter().collect(), - ..default() - } + /// Makes a new [`TextBlock`] with soft wrapping disabled. + /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur. + pub const fn new_with_no_wrap() -> Self { + Self::default().with_no_wrap(linebreak) } -} - -/// Contains the value of the text in a section and how it should be styled. -#[derive(Debug, Default, Clone, Reflect)] -#[reflect(Default)] -pub struct OldTextSection { - /// The content (in `String` form) of the text in the section. - pub value: String, - /// The style of the text in the section, including the font face, font size, and color. - pub style: TextStyle, -} -impl OldTextSection { - /// Create a new [`OldTextSection`]. - pub fn new(value: impl Into, style: TextStyle) -> Self { - Self { - value: value.into(), - style, - } + /// Returns this [`TextBlock`] with the specified [`JustifyText`]. + pub const fn with_justify(mut self, justify: JustifyText) -> Self { + self.justify = justify; + self } - /// Create an empty [`OldTextSection`] from a style. Useful when the value will be set dynamically. - pub const fn from_style(style: TextStyle) -> Self { - Self { - value: String::new(), - style, - } + /// Returns this [`TextBlock`] with the specified [`LineBreak`]. + pub const fn with_linebreak(mut self, linebreak: LineBreak) -> Self { + self.linebreak = linebreak; + self } -} -impl From<&str> for OldTextSection { - fn from(value: &str) -> Self { - Self { - value: value.into(), - ..default() - } + /// Returns this [`TextBlock`] with the specified [`FontSmoothing`]. + pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self { + self.font_smoothing = font_smoothing; + self } -} -impl From for OldTextSection { - fn from(value: String) -> Self { - Self { - value, - ..Default::default() - } + /// Returns this [`TextBlock`] 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 = LineBreak::NoWrap; + self } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 0a5ef6b416d04..e702c251823a4 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -36,8 +36,25 @@ use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; /// a [`TextBlock`]. See [`TextSpan2d`] for the component used by children of entities with [`Text2d`]. /// /// With `Text2d` the `justify` field of [`TextBlock`] only affects the internal alignment of a block of text and not its -/// relative position which is controlled by the [`Anchor`] component. +/// relative position, which is controlled by the [`Anchor`] component. /// This means that for a block of text consisting of only one line that doesn't wrap, the `justify` field will have no effect. +/* +``` +# use bevy_ecs::World; +# use bevy_text::{Text2d, TextBlock, JustifyText}; +# +# let mut world = World::default(); +# +// Basic usage. +world.spawn(Text2d::new("hello world!")); + +// With text justification. +world.spawn(( + Text2d::new("hello world\nand bevy!"), + TextBlock::new_with_justify(JustifyText::Center) +)); +``` +*/ #[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug)] #[require( @@ -51,6 +68,13 @@ use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; )] pub struct Text2d(pub String); +impl Text2d { + /// Makes a new 2d text component. + pub fn new(text: impl Into) -> Self { + Self(text.into()) + } +} + impl From<&str> for Text2d { fn from(value: &str) -> Self { Self(String::from(value)) @@ -66,6 +90,35 @@ impl From for Text2d { /// A span of 2d text in a tree of spans under an entity with [`Text2d`]. /// /// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. +/* +``` +# use bevy_asset::Handle; +# use bevy_color::Color; +# use bevy_color::palettes::basic::{RED, BLUE}; +# use bevy_ecs::World; +# use bevy_text::{Font, Text2d, TextSpan2d, TextStyle, TextSection}; +# +# let font_handle: Handle = Default::default(); +# let mut world = World::default(); +# +world.spawn(( + Text2d::new("Hello, "), + TextStyle { + font: font_handle.clone().into(), + font_size: 60.0, + color: BLUE.into(), + } +)) +.with_child(( + TextSpan2d::new("World!"), + TextStyle { + font: font_handle.into(), + font_size: 60.0, + color: RED.into(), + } +)); +``` +*/ #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Default, Debug)] #[require(TextStyle, Visibility = Visibility::Hidden, Transform)] diff --git a/crates/bevy_text/src/text_span.rs b/crates/bevy_text/src/text_span.rs index 78177eff8592c..9f86753c335ae 100644 --- a/crates/bevy_text/src/text_span.rs +++ b/crates/bevy_text/src/text_span.rs @@ -23,7 +23,7 @@ impl<'w, 's, T: TextSpanReader> TextSpans<'w, 's, T> { root_text: &str, root_style: &TextStyle, root_children: Option<&Children> - ) -> TextSpanIter<'w, 's> { + ) -> TextSpanIter<'w, 's, T> { let mut stack = core::mem::take(&mut self.scratch.stack) .into_iter() .map(|_| -> (&Children, usize) { unreachable!() }) @@ -39,7 +39,7 @@ impl<'w, 's, T: TextSpanReader> TextSpans<'w, 's, T> { } // TODO: Use this iterator design in UiChildrenIter to reduce allocations. -pub struct TextSpanIter<'w, 's> { +pub struct TextSpanIter<'w, 's, T: TextSpanReader> { scratch: &'s mut TextSpansScratch, root: Option<(Entity, &'s str, &'s TextStyle, Option<&'s Children>)>, /// Stack of (children, next index into children). @@ -47,7 +47,7 @@ pub struct TextSpanIter<'w, 's> { spans: &'s Query<'w, 's, (Entity, &'static T, &'static TextStyle, Option<&'static Children>)>, } -impl<'w, 's> Iterator for TextSpanIter<'w, 's> { +impl<'w, 's, T: TextSpanReader> Iterator for TextSpanIter<'w, 's, T> { /// Item = (entity in text block, depth in the block, span text, span style). type Item = (Entity, usize, &str, &TextStyle>); fn next(&mut self) -> Option { @@ -103,31 +103,22 @@ impl<'w, 's> Drop for TextSpanIter { pub fn detect_text_needs_rerender( changed_roots: Query, Changed, Changed, Changed)>, - )>, - changed_roots_leaf: Query, Changed, Changed)>, - Without + With, With, With, )>, changed_spans: Query< &Parent, Or<(Changed, Changed, Changed)>, - >, - changed_spans_leaf: Query< - &Parent, - ( - Or<(Changed, Changed)>, - Without - ) + With, With, >, mut computed: Query<(Option<&Parent>, Option<&mut ComputedTextBlock>)>, ) { // Root entity: - // - TextBlock changed. - // - TextStyle on root changed. // - Root component changed. + // - TextStyle on root changed. + // - TextBlock changed. // - Root children changed (can include additions and removals). - for root in changed_roots.iter().chain(changed_roots_leaf.iter()) { + for root in changed_roots.iter() { // TODO: ComputedTextBlock *should* be here, log a warning? let Ok((_, Some(mut computed))) = computed.get_mut(root) else { continue }; computed.needs_rerender = true; @@ -137,8 +128,8 @@ pub fn detect_text_needs_rerender( // - Span component changed. // - Span TextStyle changed. // - Span children changed (can include additions and removals). - for span_parent in changed_spans.iter().chain(changed_spans_leaf.iter()) { - let mut parent = span_parent; + for span_parent in changed_spans.iter() { + let mut parent: Entity = **span_parent; // Search for the nearest ancestor with ComputedTextBlock. // Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited @@ -152,7 +143,7 @@ pub fn detect_text_needs_rerender( } // TODO: If there is no parent then a span is floating without an owning TextBlock. Log a warning? let Some(next_parent) = maybe_parent else { break }; - parent = next_parent; + parent = **next_parent; } } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 9d19fc009022b..93ced34bbdd99 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -52,6 +52,24 @@ impl Default for TextNodeFlags { /// a [`TextBlock`]. See [`TextSpan`] for the component used by children of entities with `TextNEW`. /// /// Note that [`Transform`] on this entity is managed automatically by the UI layout system. +/* +``` +# use bevy_ecs::World; +# use bevy_text::{JustifyText, TextBlock}; +# use bevy_ui::Text; +# +# let mut world = World::default(); +# +// Basic usage. +world.spawn(Text::new("hello world!")); + +// With text justification. +world.spawn(( + Text::new("hello world\nand bevy!"), + TextBlock::new_with_justify(JustifyText::Center) +)); +``` +*/ #[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug)] #[require( @@ -69,6 +87,13 @@ impl Default for TextNodeFlags { )] pub struct TextNEW(pub String); +impl TextNEW { + /// Makes a new text component. + pub fn new(text: impl Into) -> Self { + Self(text.into()) + } +} + impl From<&str> for TextNEW { fn from(value: &str) -> Self { Self(String::from(value)) @@ -84,6 +109,36 @@ impl From for TextNEW { /// A span of UI text in a tree of spans under an entity with [`Text`]. /// /// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. +/* +``` +# use bevy_asset::Handle; +# use bevy_color::Color; +# use bevy_color::palettes::basic::{RED, BLUE}; +# use bevy_ecs::World; +# use bevy_text::{Font, TextStyle, TextSection}; +# use bevy_ui::{Text, TextSpan}; +# +# let font_handle: Handle = Default::default(); +# let mut world = World::default(); +# +world.spawn(( + Text::new("Hello, "), + TextStyle { + font: font_handle.clone().into(), + font_size: 60.0, + color: BLUE.into(), + } +)) +.with_child(( + TextSpan::new("World!"), + TextStyle { + font: font_handle.into(), + font_size: 60.0, + color: RED.into(), + } +)); +``` +*/ #[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug)] #[require(TextStyle, GhostNode, Visibility = Visibility::Hidden)] From a1cc06d0362f5d603b3e57c25cefe81acff78c61 Mon Sep 17 00:00:00 2001 From: koe Date: Wed, 2 Oct 2024 17:26:49 -0500 Subject: [PATCH 05/41] cleanup WIP --- crates/bevy_text/src/lib.rs | 6 +- crates/bevy_text/src/text.rs | 82 +++++++++++++++- crates/bevy_text/src/text2d.rs | 18 +++- crates/bevy_text/src/text_span.rs | 149 ----------------------------- crates/bevy_text/src/text_spans.rs | 125 ++++++++++++++++++++++++ crates/bevy_ui/src/lib.rs | 8 +- crates/bevy_ui/src/widget/text.rs | 32 +++++-- 7 files changed, 258 insertions(+), 162 deletions(-) delete mode 100644 crates/bevy_text/src/text_span.rs create mode 100644 crates/bevy_text/src/text_spans.rs diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index e09416f4cc499..08f8b0cac1dd5 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -43,6 +43,7 @@ mod glyph; mod pipeline; mod text; mod text2d; +mod text_spans; pub use cosmic_text; @@ -56,13 +57,16 @@ pub use glyph::*; pub use pipeline::*; pub use text::*; pub use text2d::*; +pub use text_spans::*; /// The text prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] - pub use crate::{Font, JustifyText, Text, Text2dBundle, TextError, TextSection, TextStyle}; + pub use crate::{ + Font, JustifyText, LineBreak, Text2d, TextBlock, TextError, TextSpan2d, TextStyle, + }; } use bevy_app::prelude::*; diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 9f7d7708a8b10..32bf95047dade 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -68,7 +68,7 @@ impl ComputedTextBlock { /// Can be used to look up [`TextStyle`] components for glyphs in [`TextLayoutInfo`] using the `span_index` /// stored there. pub fn entities(&self) -> &[TextEntity] { - self.entities.iter() + &self.entities } /// Indicates if the text needs to be refreshed in [`TextLayoutInfo`]. @@ -112,8 +112,16 @@ pub struct TextBlock { impl TextBlock { /// Makes a new [`TextBlock`]. - pub const fn new(justify: JustifyText, linebreak: LineBreak, font_smoothing: FontSmoothing) -> Self { - Self{ justify, linebreak, font_smoothing } + pub const fn new( + justify: JustifyText, + linebreak: LineBreak, + font_smoothing: FontSmoothing, + ) -> Self { + Self { + justify, + linebreak, + font_smoothing, + } } /// Makes a new [`TextBlock`] with the specified [`JustifyText`]. @@ -277,3 +285,71 @@ pub enum FontSmoothing { // TODO: Add subpixel antialias support // SubpixelAntiAliased, } + +/// System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`. +/// +/// Generic over the root text component and text span component. For example, [`Text2d`]/[`TextSpan2d`] for 2d or +/// `Text`/`TextSpan` for UI. +pub fn detect_text_needs_rerender( + changed_roots: Query< + Entity, + ( + Or<( + Changed, + Changed, + Changed, + Changed, + )>, + With, + With, + With, + ), + >, + changed_spans: Query< + &Parent, + Or<(Changed, Changed, Changed)>, + With, + With, + >, + mut computed: Query<(Option<&Parent>, Option<&mut ComputedTextBlock>)>, +) { + // Root entity: + // - Root component changed. + // - TextStyle on root changed. + // - TextBlock changed. + // - Root children changed (can include additions and removals). + for root in changed_roots.iter() { + // TODO: ComputedTextBlock *should* be here. Log a warning? + let Ok((_, Some(mut computed))) = computed.get_mut(root) else { + continue; + }; + computed.needs_rerender = true; + } + + // Span entity: + // - Span component changed. + // - Span TextStyle changed. + // - Span children changed (can include additions and removals). + for span_parent in changed_spans.iter() { + let mut parent: Entity = **span_parent; + + // Search for the nearest ancestor with ComputedTextBlock. + // Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited + // is outweighed by the expense of tracking visited spans. + loop { + // TODO: If this lookup fails then there is a hierarchy error. Log a warning? + let Ok((maybe_parent, maybe_computed)) = computed.get_mut(parent) else { + break; + }; + if let Some(computed) = maybe_computed { + computed.needs_rerender = true; + break; + } + // TODO: If there is no parent then a span is floating without an owning TextBlock. Log a warning? + let Some(next_parent) = maybe_parent else { + break; + }; + parent = **next_parent; + } + } +} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index e702c251823a4..6dd999a4b8326 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -38,16 +38,31 @@ use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; /// With `Text2d` the `justify` field of [`TextBlock`] only affects the internal alignment of a block of text and not its /// relative position, which is controlled by the [`Anchor`] component. /// This means that for a block of text consisting of only one line that doesn't wrap, the `justify` field will have no effect. +/// /* ``` +# use bevy_asset::Handle; +# use bevy_color::Color; +# use bevy_color::palettes::basic::BLUE; # use bevy_ecs::World; -# use bevy_text::{Text2d, TextBlock, JustifyText}; +# use bevy_text::{Font, JustifyText, Text2d, TextBlock, TextStyle}; # +# let font_handle: Handle = Default::default(); # let mut world = World::default(); # // Basic usage. world.spawn(Text2d::new("hello world!")); +// With non-default style. +world.spawn(( + Text2d::new("hello world!"), + TextStyle { + font: font_handle.clone().into(), + font_size: 60.0, + color: BLUE.into(), + } +)); + // With text justification. world.spawn(( Text2d::new("hello world\nand bevy!"), @@ -90,6 +105,7 @@ impl From for Text2d { /// A span of 2d text in a tree of spans under an entity with [`Text2d`]. /// /// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. +/// /* ``` # use bevy_asset::Handle; diff --git a/crates/bevy_text/src/text_span.rs b/crates/bevy_text/src/text_span.rs deleted file mode 100644 index 9f86753c335ae..0000000000000 --- a/crates/bevy_text/src/text_span.rs +++ /dev/null @@ -1,149 +0,0 @@ - -/// Helper trait for setting up [`TextSpanPlugin`]. -pub trait TextSpanReader: Component { - /// Gets the text span's string. - fn read_span(&self) -> &str; -} - -#[derive(Resource, Default)] -pub(crate) struct TextSpansScratch { - stack: Vec<(&'static Children, usize)> -} - -#[derive(SystemParam)] -pub struct TextSpans<'w, 's, T: TextSpanReader> { - scratch: ResMut<'w, TextSpansScratch>, - spans: Query<'w, 's, (Entity, &'static T, &'static TextStyle, Option<&'static Children>)> -} - -impl<'w, 's, T: TextSpanReader> TextSpans<'w, 's, T> { - pub fn iter_from_base( - &mut self, - root_entity: Entity, - root_text: &str, - root_style: &TextStyle, - root_children: Option<&Children> - ) -> TextSpanIter<'w, 's, T> { - let mut stack = core::mem::take(&mut self.scratch.stack) - .into_iter() - .map(|_| -> (&Children, usize) { unreachable!() }) - .collect(); - - TextSpanIter{ - scratch: &mut self.scratch, - root: Some((root_entity, root_text, root_style, root_children)), - stack, - spans: &self.spans, - } - } -} - -// TODO: Use this iterator design in UiChildrenIter to reduce allocations. -pub struct TextSpanIter<'w, 's, T: TextSpanReader> { - scratch: &'s mut TextSpansScratch, - root: Option<(Entity, &'s str, &'s TextStyle, Option<&'s Children>)>, - /// Stack of (children, next index into children). - stack: Vec<(&'s Children, usize)>, - spans: &'s Query<'w, 's, (Entity, &'static T, &'static TextStyle, Option<&'static Children>)>, -} - -impl<'w, 's, T: TextSpanReader> Iterator for TextSpanIter<'w, 's, T> { - /// Item = (entity in text block, depth in the block, span text, span style). - type Item = (Entity, usize, &str, &TextStyle>); - fn next(&mut self) -> Option { - // Root - if let Some((entity, text, style, maybe_children)) = self.root.take() { - if let Some(children) = maybe_children { - self.stack.push((children, 0)); - } - return Some((entity, 0, text, style)); - } - - // Span - loop { - let Some((children, idx)) = self.stack.last_mut() else { return None }; - - loop { - let Some(child) = children.get(idx) else { break }; - - // Increment to prep the next entity in this stack level. - *idx += 1; - - let Some((entity, span, style, maybe_children)) = self.spans.get(*child) else { continue }; - - let depth = self.stack.len(); - if let Some(children) = maybe_children { - self.stack.push((children, 0)); - } - return Some((entity, depth, span.read_span(), style)); - } - - // All children at this stack entry have been iterated. - self.stack.pop(); - } - } -} - -impl<'w, 's> Drop for TextSpanIter { - fn drop(&mut self) { - // Return the internal stack. - self.stack.clear(); - self.scratch.stack = self.stack - .into_iter() - .map( - |_| -> (&'static Children, usize) { - unreachable!() - }, - ) - .collect(); - } -} - -/// Detects changes to text blocks and sets `ComputedTextBlock::should_rerender`. -pub fn detect_text_needs_rerender( - changed_roots: Query, Changed, Changed, Changed)>, - With, With, With, - )>, - changed_spans: Query< - &Parent, - Or<(Changed, Changed, Changed)>, - With, With, - >, - mut computed: Query<(Option<&Parent>, Option<&mut ComputedTextBlock>)>, -) -{ - // Root entity: - // - Root component changed. - // - TextStyle on root changed. - // - TextBlock changed. - // - Root children changed (can include additions and removals). - for root in changed_roots.iter() { - // TODO: ComputedTextBlock *should* be here, log a warning? - let Ok((_, Some(mut computed))) = computed.get_mut(root) else { continue }; - computed.needs_rerender = true; - } - - // Span entity: - // - Span component changed. - // - Span TextStyle changed. - // - Span children changed (can include additions and removals). - for span_parent in changed_spans.iter() { - let mut parent: Entity = **span_parent; - - // Search for the nearest ancestor with ComputedTextBlock. - // Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited - // is outweighed by the expense of tracking visited spans. - loop { - // TODO: If this lookup fails then there is a hierarchy error. Log a warning? - let Ok((maybe_parent, maybe_computed)) = computed.get_mut(parent) else { break }; - if let Some(computed) = maybe_computed { - computed.needs_rerender = true; - break; - } - // TODO: If there is no parent then a span is floating without an owning TextBlock. Log a warning? - let Some(next_parent) = maybe_parent else { break }; - parent = **next_parent; - } - } -} diff --git a/crates/bevy_text/src/text_spans.rs b/crates/bevy_text/src/text_spans.rs new file mode 100644 index 0000000000000..7793552d53c2d --- /dev/null +++ b/crates/bevy_text/src/text_spans.rs @@ -0,0 +1,125 @@ +/// Helper trait for using the [`TextSpans`] system param. +pub trait TextSpanReader: Component { + /// Gets the text span's string. + fn read_span(&self) -> &str; +} + +#[derive(Resource, Default)] +pub(crate) struct TextSpansScratch { + stack: Vec<(&'static Children, usize)>, +} + +#[derive(SystemParam)] +pub struct TextSpans<'w, 's, T: TextSpanReader> { + scratch: ResMut<'w, TextSpansScratch>, + spans: Query< + 'w, + 's, + ( + Entity, + &'static T, + &'static TextStyle, + Option<&'static Children>, + ), + >, +} + +impl<'w, 's, T: TextSpanReader> TextSpans<'w, 's, T> { + /// Returns an iterator over text spans in a text block, starting with the root entity. + pub fn iter_from_base( + &'s mut self, + root_entity: Entity, + root_text: &str, + root_style: &TextStyle, + root_children: Option<&Children>, + ) -> TextSpanIter<'w, 's, T> { + let mut stack = core::mem::take(&mut self.scratch.stack) + .into_iter() + .map(|_| -> (&Children, usize) { unreachable!() }) + .collect(); + + TextSpanIter { + scratch: &mut self.scratch, + root: Some((root_entity, root_text, root_style, root_children)), + stack, + spans: &self.spans, + } + } +} + +/// Iterator returned by [`TextSpans::iter_from_base`]. +/// +/// Iterates all spans in a text block according to hierarchy traversal order. +/// Does *not* flatten interspersed ghost nodes. Only contiguous spans are traversed. +// TODO: Use this iterator design in UiChildrenIter to reduce allocations. +pub struct TextSpanIter<'w, 's, T: TextSpanReader> { + scratch: &'s mut TextSpansScratch, + root: Option<(Entity, &'s str, &'s TextStyle, Option<&'s Children>)>, + /// Stack of (children, next index into children). + stack: Vec<(&'s Children, usize)>, + spans: &'s Query< + 'w, + 's, + ( + Entity, + &'static T, + &'static TextStyle, + Option<&'static Children>, + ), + >, +} + +impl<'w, 's, T: TextSpanReader> Iterator for TextSpanIter<'w, 's, T> { + /// Item = (entity in text block, hierarchy depth in the block, span text, span style). + type Item = (Entity, usize, &str, &TextStyle); + fn next(&mut self) -> Option { + // Root + if let Some((entity, text, style, maybe_children)) = self.root.take() { + if let Some(children) = maybe_children { + self.stack.push((children, 0)); + } + return Some((entity, 0, text, style)); + } + + // Span + loop { + let Some((children, idx)) = self.stack.last_mut() else { + return None; + }; + + loop { + let Some(child) = children.get(idx) else { + break; + }; + + // Increment to prep the next entity in this stack level. + *idx += 1; + + let Some((entity, span, style, maybe_children)) = self.spans.get(*child) else { + continue; + }; + + let depth = self.stack.len(); + if let Some(children) = maybe_children { + self.stack.push((children, 0)); + } + return Some((entity, depth, span.read_span(), style)); + } + + // All children at this stack entry have been iterated. + self.stack.pop(); + } + } +} + +impl<'w, 's> Drop for TextSpanIter { + fn drop(&mut self) { + // Return the internal stack. + let mut stack = std::mem::take(&mut self.stack); + stack.clear(); + self.scratch.stack = stack + .into_iter() + .map(|_| -> (&'static Children, usize) { unreachable!() }) + .collect(); + } +} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index ceeba29a884b8..e943a06d8fb4a 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -49,8 +49,12 @@ pub mod prelude { #[doc(hidden)] pub use { crate::{ - geometry::*, node_bundles::*, ui_material::*, ui_node::*, widget::Button, - widget::Label, Interaction, UiMaterialHandle, UiMaterialPlugin, UiScale, + geometry::*, + node_bundles::*, + ui_material::*, + ui_node::*, + widget::{Button, Label, TextNEW, TextSpan}, + Interaction, UiMaterialHandle, UiMaterialPlugin, UiScale, }, // `bevy_sprite` re-exports for texture slicing bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer}, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 93ced34bbdd99..839c3152f6e6d 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -29,9 +29,9 @@ use taffy::style::AvailableSpace; #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default, Debug)] pub struct TextNodeFlags { - /// If set a new measure function for the text node will be created. + /// If set then a new measure function for the text node will be created. needs_measure_fn: bool, - /// If set the text will be recomputed. + /// If set then the text will be recomputed. needs_recompute: bool, } @@ -52,17 +52,32 @@ impl Default for TextNodeFlags { /// a [`TextBlock`]. See [`TextSpan`] for the component used by children of entities with `TextNEW`. /// /// Note that [`Transform`] on this entity is managed automatically by the UI layout system. +/// /* ``` +# use bevy_asset::Handle; +# use bevy_color::Color; +# use bevy_color::palettes::basic::BLUE; # use bevy_ecs::World; -# use bevy_text::{JustifyText, TextBlock}; +# use bevy_text::{Font, JustifyText, TextBlock, TextStyle}; # use bevy_ui::Text; # +# let font_handle: Handle = Default::default(); # let mut world = World::default(); # // Basic usage. world.spawn(Text::new("hello world!")); +// With non-default style. +world.spawn(( + Text::new("hello world!"), + TextStyle { + font: font_handle.clone().into(), + font_size: 60.0, + color: BLUE.into(), + } +)); + // With text justification. world.spawn(( Text::new("hello world\nand bevy!"), @@ -109,6 +124,7 @@ impl From for TextNEW { /// A span of UI text in a tree of spans under an entity with [`Text`]. /// /// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout. +/// /* ``` # use bevy_asset::Handle; @@ -214,7 +230,7 @@ fn create_text_measure<'a>( entity: Entity, fonts: &Assets, scale_factor: f64, - spans: impl Iterator<(&'a str, &'a TextStyle)>, + spans: impl Iterator<(Entity, usize, &'a str, &'a TextStyle)>, block: Ref, text_pipeline: &mut TextPipeline, mut content_size: Mut, @@ -329,7 +345,7 @@ pub fn measure_text_system( entity, &fonts, scale_factor.into(), - spans.iter_from_base(text.as_str(), text_style, maybe_children), + spans.iter_from_base(entity, text.as_str(), text_style, maybe_children), block, &mut text_pipeline, content_size, @@ -345,6 +361,7 @@ pub fn measure_text_system( #[allow(clippy::too_many_arguments)] #[inline] fn queue_text( + entity: Entity, fonts: &Assets, text_pipeline: &mut TextPipeline, font_atlas_sets: &mut FontAtlasSets, @@ -384,7 +401,7 @@ fn queue_text( match text_pipeline.queue_text( text_layout_info, fonts, - spans.iter_from_base(text.as_str(), text_style, maybe_children), + spans.iter_from_base(entity, text.as_str(), text_style, maybe_children), scale_factor.into(), block, physical_node_size, @@ -432,6 +449,7 @@ pub fn text_system( mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, mut text_query: Query<( + Entity, Ref, &Text, &TextStyle, @@ -449,6 +467,7 @@ pub fn text_system( scale_factors_buffer.clear(); for ( + entity, node, text, text_style, @@ -484,6 +503,7 @@ pub fn text_system( || text_flags.needs_recompute { queue_text( + entity, &fonts, &mut text_pipeline, &mut font_atlas_sets, From 120c4a402bad034584dc0f999814febdc7d6b178 Mon Sep 17 00:00:00 2001 From: koe Date: Wed, 2 Oct 2024 19:15:13 -0500 Subject: [PATCH 06/41] crates compile --- crates/bevy_text/Cargo.toml | 2 + crates/bevy_text/src/lib.rs | 8 +- crates/bevy_text/src/pipeline.rs | 19 ++-- crates/bevy_text/src/text.rs | 32 ++++--- crates/bevy_text/src/text2d.rs | 43 ++++++--- .../src/{text_spans.rs => text_blocks.rs} | 71 ++++++++------- crates/bevy_ui/src/accessibility.rs | 39 ++++---- crates/bevy_ui/src/layout/mod.rs | 4 +- crates/bevy_ui/src/layout/ui_surface.rs | 10 +-- crates/bevy_ui/src/lib.rs | 5 +- crates/bevy_ui/src/measurement.rs | 2 +- crates/bevy_ui/src/node_bundles.rs | 9 -- crates/bevy_ui/src/render/mod.rs | 7 +- crates/bevy_ui/src/widget/image.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 88 +++++++------------ 15 files changed, 171 insertions(+), 170 deletions(-) rename crates/bevy_text/src/{text_spans.rs => text_blocks.rs} (55%) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 57cea37b52a07..ef3d235113007 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -18,6 +18,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", @@ -36,6 +37,7 @@ derive_more = { version = "1", default-features = false, features = [ "display", ] } serde = { version = "1", features = ["derive"] } +smallvec = "1.13" unicode-bidi = "0.3.13" sys-locale = "0.3.0" diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 08f8b0cac1dd5..15b58303d4291 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -43,7 +43,7 @@ mod glyph; mod pipeline; mod text; mod text2d; -mod text_spans; +mod text_blocks; pub use cosmic_text; @@ -57,7 +57,7 @@ pub use glyph::*; pub use pipeline::*; pub use text::*; pub use text2d::*; -pub use text_spans::*; +pub use text_blocks::*; /// The text prelude. /// @@ -106,10 +106,6 @@ pub enum YAxisOrientation { #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub struct Update2dText; -/// A convenient alias for `With`, for use with -/// [`bevy_render::view::VisibleEntities`]. -pub type WithText = With; - impl Plugin for TextPlugin { fn build(&self, app: &mut App) { app.init_asset::() diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 26775325fb3ab..9983fd3242603 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -16,8 +16,8 @@ use bevy_utils::HashMap; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, CosmicBuffer, Font, FontAtlasSets, FontSmoothing, JustifyText, LineBreak, - PositionedGlyph, TextBounds, TextSection, TextStyle, YAxisOrientation, + error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText, + LineBreak, PositionedGlyph, TextBlock, TextBounds, TextEntity, TextStyle, YAxisOrientation, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -83,7 +83,7 @@ impl TextPipeline { pub fn update_buffer<'a>( &mut self, fonts: &Assets, - mut text_spans: impl Iterator, + text_spans: impl Iterator, linebreak: LineBreak, justify: JustifyText, bounds: TextBounds, @@ -197,7 +197,7 @@ impl TextPipeline { /// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s /// which contain information for rendering the text. #[allow(clippy::too_many_arguments)] - pub fn queue_text( + pub fn queue_text<'a>( &mut self, layout_info: &mut TextLayoutInfo, fonts: &Assets, @@ -333,7 +333,7 @@ impl TextPipeline { /// Produces a [`TextMeasureInfo`] which can be used by a layout system /// to measure the text area on demand. #[allow(clippy::too_many_arguments)] - pub fn create_text_measure( + pub fn create_text_measure<'a>( &mut self, entity: Entity, fonts: &Assets, @@ -360,6 +360,7 @@ impl TextPipeline { font_system, )?; + let buffer = &mut computed.buffer; let min_width_content_size = buffer_dimensions(buffer); let max_width_content_size = { @@ -414,13 +415,15 @@ impl TextMeasureInfo { pub fn compute_size( &mut self, bounds: TextBounds, - buffer: &mut Buffer, + computed: &mut ComputedTextBlock, font_system: &mut cosmic_text::FontSystem, ) -> Vec2 { // Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed' // whenever a canonical state is required. - buffer.set_size(font_system, bounds.width, bounds.height); - buffer_dimensions(buffer) + computed + .buffer + .set_size(font_system, bounds.width, bounds.height); + buffer_dimensions(&computed.buffer) } } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 32bf95047dade..33597fd6cc92c 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,13 +1,14 @@ use bevy_asset::Handle; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; +use bevy_ecs::{prelude::*, reflect::ReflectComponent}; +use bevy_hierarchy::{Children, Parent}; use bevy_reflect::prelude::*; -use bevy_utils::default; use cosmic_text::{Buffer, Metrics}; use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; -use crate::Font; +use crate::{Font, TextLayoutInfo}; pub use cosmic_text::{ self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, @@ -26,8 +27,9 @@ impl Default for CosmicBuffer { /// A sub-entity of a [`TextBlock`]. /// /// Returned by [`ComputedTextBlock::entities`]. -#[derive(Default, Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub struct TextEntity { + /// The entity. pub entity: Entity, /// Records the hierarchy depth of the entity within a `TextBlock`. pub depth: usize, @@ -99,7 +101,7 @@ impl Default for ComputedTextBlock { /// See [`Text2d`] for the core component of 2d text, and `Text` in `bevy_ui` for UI text. #[derive(Component, Debug, Clone, Default, Reflect)] #[reflect(Component, Default, Debug)] -#[requires(ComputedTextBlock, TextLayoutInfo)] +#[require(ComputedTextBlock, TextLayoutInfo)] pub struct TextBlock { /// The text's internal alignment. /// Should not affect its position within a container. @@ -125,24 +127,24 @@ impl TextBlock { } /// Makes a new [`TextBlock`] with the specified [`JustifyText`]. - pub const fn new_with_justify(justify: JustifyText) -> Self { + pub fn new_with_justify(justify: JustifyText) -> Self { Self::default().with_justify(justify) } /// Makes a new [`TextBlock`] with the specified [`LineBreak`]. - pub const fn new_with_linebreak(linebreak: LineBreak) -> Self { + pub fn new_with_linebreak(linebreak: LineBreak) -> Self { Self::default().with_linebreak(linebreak) } /// Makes a new [`TextBlock`] with the specified [`FontSmoothing`]. - pub const fn new_with_font_smoothing(font_smoothing: FontSmoothing) -> Self { + pub fn new_with_font_smoothing(font_smoothing: FontSmoothing) -> Self { Self::default().with_font_smoothing(font_smoothing) } /// Makes a new [`TextBlock`] with soft wrapping disabled. /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur. - pub const fn new_with_no_wrap() -> Self { - Self::default().with_no_wrap(linebreak) + pub fn new_with_no_wrap() -> Self { + Self::default().with_no_wrap() } /// Returns this [`TextBlock`] with the specified [`JustifyText`]. @@ -307,9 +309,11 @@ pub fn detect_text_needs_rerender( >, changed_spans: Query< &Parent, - Or<(Changed, Changed, Changed)>, - With, - With, + ( + Or<(Changed, Changed, Changed)>, + With, + With, + ), >, mut computed: Query<(Option<&Parent>, Option<&mut ComputedTextBlock>)>, ) { @@ -341,7 +345,7 @@ pub fn detect_text_needs_rerender( let Ok((maybe_parent, maybe_computed)) = computed.get_mut(parent) else { break; }; - if let Some(computed) = maybe_computed { + if let Some(mut computed) = maybe_computed { computed.needs_rerender = true; break; } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 6dd999a4b8326..e33ff11cdfe09 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,29 +1,34 @@ use crate::pipeline::CosmicFontSystem; use crate::{ - CosmicBuffer, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, Text, TextBounds, - TextError, TextLayoutInfo, TextPipeline, YAxisOrientation, + ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBlock, + TextBlocks, TextBounds, TextError, TextLayoutInfo, TextPipeline, TextSpanReader, TextStyle, + YAxisOrientation, }; use bevy_asset::Assets; use bevy_color::LinearRgba; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Component; use bevy_ecs::{ - bundle::Bundle, change_detection::{DetectChanges, Ref}, entity::Entity, event::EventReader, - prelude::With, + prelude::{ReflectComponent, With}, query::{Changed, Without}, system::{Commands, Local, Query, Res, ResMut}, }; use bevy_math::Vec2; use bevy_render::sync_world::TemporaryRenderEntity; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::view::Visibility; use bevy_render::{ primitives::Aabb, texture::Image, - view::{InheritedVisibility, NoFrustumCulling, ViewVisibility, Visibility}, + view::{NoFrustumCulling, ViewVisibility}, Extract, }; use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, SpriteSource, TextureAtlasLayout}; -use bevy_transform::prelude::{GlobalTransform, Transform}; +use bevy_transform::components::Transform; +use bevy_transform::prelude::GlobalTransform; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; @@ -90,6 +95,12 @@ impl Text2d { } } +impl TextSpanReader for Text2d { + fn read_span(&self) -> &str { + self.as_str() + } +} + impl From<&str> for Text2d { fn from(value: &str) -> Self { Self(String::from(value)) @@ -135,9 +146,9 @@ world.spawn(( )); ``` */ -#[derive(Component, Clone, Debug, Default, Reflect)] +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug)] -#[require(TextStyle, Visibility = Visibility::Hidden, Transform)] +#[require(TextStyle, Visibility(visibility_hidden), Transform)] pub struct TextSpan2d(pub String); impl TextSpanReader for TextSpan2d { @@ -146,6 +157,10 @@ impl TextSpanReader for TextSpan2d { } } +fn visibility_hidden() -> Visibility { + Visibility::Hidden +} + /// This system extracts the sprites from the 2D text components and adds them to the /// "render world". pub fn extract_text2d_sprite( @@ -205,6 +220,7 @@ pub fn extract_text2d_sprite( computed_block .entities() .get(*span_index) + .map(|t| t.entity) .unwrap_or(Entity::PLACEHOLDER), ) .map(|style| LinearRgba::from(style.color)) @@ -251,13 +267,12 @@ pub fn update_text2d_layout( mut text_pipeline: ResMut, mut text_query: Query<( Entity, - Ref, Ref, Ref, &mut TextLayoutInfo, &mut ComputedTextBlock, )>, - mut spans: TextSpans, + mut blocks: TextBlocks, mut font_system: ResMut, mut swash_cache: ResMut, ) { @@ -272,14 +287,14 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, block, bounds, text_layout_info, mut computed) in &mut text_query { + for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query { if factor_changed || computed.needs_rerender() || bounds.is_changed() || queue.remove(&entity) { let text_bounds = TextBounds { - width: if text.linebreak == LineBreak::NoWrap { + width: if block.linebreak == LineBreak::NoWrap { None } else { bounds.width.map(|width| scale_value(width, scale_factor)) @@ -293,9 +308,9 @@ pub fn update_text2d_layout( match text_pipeline.queue_text( text_layout_info, &fonts, - spans.iter_from_base(entity, text.as_str(), text_style, maybe_children), + blocks.iter(entity), scale_factor.into(), - block, + &block, text_bounds, &mut font_atlas_sets, &mut texture_atlases, diff --git a/crates/bevy_text/src/text_spans.rs b/crates/bevy_text/src/text_blocks.rs similarity index 55% rename from crates/bevy_text/src/text_spans.rs rename to crates/bevy_text/src/text_blocks.rs index 7793552d53c2d..b02f2653178d7 100644 --- a/crates/bevy_text/src/text_spans.rs +++ b/crates/bevy_text/src/text_blocks.rs @@ -1,4 +1,9 @@ -/// Helper trait for using the [`TextSpans`] system param. +use bevy_ecs::{prelude::*, system::SystemParam}; +use bevy_hierarchy::Children; + +use crate::TextStyle; + +/// Helper trait for using the [`TextBlocks`] system param. pub trait TextSpanReader: Component { /// Gets the text span's string. fn read_span(&self) -> &str; @@ -9,76 +14,80 @@ pub(crate) struct TextSpansScratch { stack: Vec<(&'static Children, usize)>, } +/// System parameter for iterating over text spans in a [`TextBlock`]. +/// +/// `R` is the root text component, and `S` is the text span component on children. #[derive(SystemParam)] -pub struct TextSpans<'w, 's, T: TextSpanReader> { +pub struct TextBlocks<'w, 's, R: TextSpanReader, S: TextSpanReader> { scratch: ResMut<'w, TextSpansScratch>, + roots: Query<'w, 's, (&'static R, &'static TextStyle, Option<&'static Children>)>, spans: Query< 'w, 's, ( Entity, - &'static T, + &'static S, &'static TextStyle, Option<&'static Children>, ), >, } -impl<'w, 's, T: TextSpanReader> TextSpans<'w, 's, T> { +impl<'w, 's, R: TextSpanReader, S: TextSpanReader> TextBlocks<'w, 's, R, S> { /// Returns an iterator over text spans in a text block, starting with the root entity. - pub fn iter_from_base( - &'s mut self, - root_entity: Entity, - root_text: &str, - root_style: &TextStyle, - root_children: Option<&Children>, - ) -> TextSpanIter<'w, 's, T> { - let mut stack = core::mem::take(&mut self.scratch.stack) + pub fn iter<'a>(&'a mut self, root_entity: Entity) -> TextSpanIter<'a, R, S> { + let stack = core::mem::take(&mut self.scratch.stack) .into_iter() .map(|_| -> (&Children, usize) { unreachable!() }) .collect(); TextSpanIter { scratch: &mut self.scratch, - root: Some((root_entity, root_text, root_style, root_children)), + root_entity: Some(root_entity), stack, + roots: &self.roots, spans: &self.spans, } } } -/// Iterator returned by [`TextSpans::iter_from_base`]. +/// Iterator returned by [`TextBlocks::iter`]. /// /// Iterates all spans in a text block according to hierarchy traversal order. /// Does *not* flatten interspersed ghost nodes. Only contiguous spans are traversed. // TODO: Use this iterator design in UiChildrenIter to reduce allocations. -pub struct TextSpanIter<'w, 's, T: TextSpanReader> { - scratch: &'s mut TextSpansScratch, - root: Option<(Entity, &'s str, &'s TextStyle, Option<&'s Children>)>, +pub struct TextSpanIter<'a, R: TextSpanReader, S: TextSpanReader> { + scratch: &'a mut TextSpansScratch, + root_entity: Option, /// Stack of (children, next index into children). - stack: Vec<(&'s Children, usize)>, - spans: &'s Query< - 'w, - 's, + stack: Vec<(&'a Children, usize)>, + roots: &'a Query<'a, 'a, (&'static R, &'static TextStyle, Option<&'static Children>)>, + spans: &'a Query< + 'a, + 'a, ( Entity, - &'static T, + &'static S, &'static TextStyle, Option<&'static Children>, ), >, } -impl<'w, 's, T: TextSpanReader> Iterator for TextSpanIter<'w, 's, T> { +impl<'a, R: TextSpanReader, S: TextSpanReader> Iterator for TextSpanIter<'a, R, S> { /// Item = (entity in text block, hierarchy depth in the block, span text, span style). - type Item = (Entity, usize, &str, &TextStyle); + type Item = (Entity, usize, &'a str, &'a TextStyle); fn next(&mut self) -> Option { // Root - if let Some((entity, text, style, maybe_children)) = self.root.take() { - if let Some(children) = maybe_children { - self.stack.push((children, 0)); + if let Some(root_entity) = self.root_entity.take() { + if let Ok((text, style, maybe_children)) = self.roots.get(root_entity) { + if let Some(children) = maybe_children { + self.stack.push((children, 0)); + } + return Some((root_entity, 0, text.read_span(), style)); + } else { + return None; } - return Some((entity, 0, text, style)); } // Span @@ -88,14 +97,14 @@ impl<'w, 's, T: TextSpanReader> Iterator for TextSpanIter<'w, 's, T> { }; loop { - let Some(child) = children.get(idx) else { + let Some(child) = children.get(*idx) else { break; }; // Increment to prep the next entity in this stack level. *idx += 1; - let Some((entity, span, style, maybe_children)) = self.spans.get(*child) else { + let Ok((entity, span, style, maybe_children)) = self.spans.get(*child) else { continue; }; @@ -112,7 +121,7 @@ impl<'w, 's, T: TextSpanReader> Iterator for TextSpanIter<'w, 's, T> { } } -impl<'w, 's> Drop for TextSpanIter { +impl<'a, R: TextSpanReader, S: TextSpanReader> Drop for TextSpanIter<'a, R, S> { fn drop(&mut self) { // Return the internal stack. let mut stack = std::mem::take(&mut self.stack); diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index fb17415fe2eb4..7dcdbe963b815 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,5 +1,6 @@ use crate::{ prelude::{Button, Label}, + widget::{TextNEW, TextSpan}, Node, UiChildren, UiImage, }; use bevy_a11y::{ @@ -15,18 +16,20 @@ use bevy_ecs::{ world::Ref, }; use bevy_render::{camera::CameraUpdateSystem, prelude::Camera}; -use bevy_text::Text; +use bevy_text::TextBlocks; use bevy_transform::prelude::GlobalTransform; -fn calc_name(texts: &Query<&Text>, children: impl Iterator) -> Option> { +fn calc_name( + blocks: &mut TextBlocks, + children: impl Iterator, +) -> Option> { let mut name = None; for child in children { - if let Ok(text) = texts.get(child) { - let values = text - .sections - .iter() - .map(|v| v.value.to_string()) - .collect::>(); + let values = blocks + .iter(child) + .map(|(_, _, text, _)| text.into()) + .collect::>(); + if values.len() > 0 { name = Some(values.join(" ")); } } @@ -60,10 +63,10 @@ fn button_changed( mut commands: Commands, mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed