pub use cosmic_text::{ self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, }; use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_reflect::prelude::*; use bevy_utils::once; use cosmic_text::{Buffer, Metrics}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use tracing::warn; /// Wrapper for [`cosmic_text::Buffer`] #[derive(Deref, DerefMut, Debug, Clone)] pub struct CosmicBuffer(pub Buffer); impl Default for CosmicBuffer { fn default() -> Self { Self(Buffer::new_empty(Metrics::new(0.0, 0.000001))) } } /// A sub-entity of a [`ComputedTextBlock`]. /// /// Returned by [`ComputedTextBlock::entities`]. #[derive(Debug, Copy, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct TextEntity { /// The entity. pub entity: Entity, /// Records the hierarchy depth of the entity within a `TextLayout`. pub depth: usize, } /// Computed information for a text block. /// /// See [`TextLayout`]. /// /// Automatically updated by 2d and UI text systems. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Debug, Default, 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 `TextLayout` and instead manually implement the conversion to /// `TextLayoutInfo`. #[reflect(ignore, clone)] 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. /// /// Includes: /// - [`TextLayout`] changes. /// - [`TextFont`] or `Text2d`/`Text`/`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 TextLayout and TextFont into structural/non-structural // components for more granular change detection. A cost/benefit analysis is needed. pub(crate) needs_rerender: bool, } impl ComputedTextBlock { /// Accesses entities in this block. /// /// Can be used to look up [`TextFont`] components for glyphs in [`TextLayoutInfo`] using the `span_index` /// stored there. pub fn entities(&self) -> &[TextEntity] { &self.entities } /// Indicates if the text needs to be refreshed in [`TextLayoutInfo`]. /// /// Updated automatically by [`detect_text_needs_rerender`] and cleared /// by [`TextPipeline`](crate::TextPipeline) methods. pub fn needs_rerender(&self) -> bool { self.needs_rerender } /// Accesses the underlying buffer which can be used for `cosmic-text` APIs such as accessing layout information /// or calculating a cursor position. /// /// Mutable access is not offered because changes would be overwritten during the automated layout calculation. /// If you want to control the buffer contents manually or use the `cosmic-text` /// editor, then you need to not use `TextLayout` and instead manually implement the conversion to /// `TextLayoutInfo`. pub fn buffer(&self) -> &CosmicBuffer { &self.buffer } } impl Default for ComputedTextBlock { fn default() -> Self { Self { buffer: CosmicBuffer::default(), entities: SmallVec::default(), needs_rerender: 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 string value and [`TextFont`]. Text /// spans associated with a text block are collected into [`ComputedTextBlock`] for layout, and then inserted /// to [`TextLayoutInfo`] for rendering. /// /// See [`Text2d`](crate::Text2d) for the core component of 2d text, and `Text` in `bevy_ui` for UI text. #[derive(Component, Debug, Copy, Clone, Default, Reflect)] #[reflect(Component, Default, Debug, Clone)] #[require(ComputedTextBlock, TextLayoutInfo)] pub struct TextLayout { /// 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, } impl TextLayout { /// Makes a new [`TextLayout`]. pub const fn new(justify: JustifyText, linebreak: LineBreak) -> Self { Self { justify, linebreak } } /// Makes a new [`TextLayout`] with the specified [`JustifyText`]. pub fn new_with_justify(justify: JustifyText) -> Self { Self::default().with_justify(justify) } /// Makes a new [`TextLayout`] with the specified [`LineBreak`]. pub fn new_with_linebreak(linebreak: LineBreak) -> Self { Self::default().with_linebreak(linebreak) } /// Makes a new [`TextLayout`] with soft wrapping disabled. /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur. pub fn new_with_no_wrap() -> Self { Self::default().with_no_wrap() } /// Returns this [`TextLayout`] with the specified [`JustifyText`]. pub const fn with_justify(mut self, justify: JustifyText) -> Self { self.justify = justify; self } /// Returns this [`TextLayout`] with the specified [`LineBreak`]. pub const fn with_linebreak(mut self, linebreak: LineBreak) -> Self { self.linebreak = linebreak; self } /// Returns this [`TextLayout`] 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 } } /// A span of text in a tree of spans. /// /// `TextSpan` is only valid as a child of an entity with [`TextLayout`], which is provided by `Text` /// for text in `bevy_ui` or `Text2d` for text in 2d world-space. /// /// 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::World; /// # use bevy_text::{Font, TextLayout, TextFont, TextSpan, TextColor}; /// /// # let font_handle: Handle = Default::default(); /// # let mut world = World::default(); /// # /// world.spawn(( /// // `Text` or `Text2d` are needed, and will provide default instances /// // of the following components. /// TextLayout::default(), /// TextFont { /// font: font_handle.clone().into(), /// font_size: 60.0, /// ..Default::default() /// }, /// TextColor(BLUE.into()), /// )) /// .with_child(( /// // Children must be `TextSpan`, not `Text` or `Text2d`. /// TextSpan::new("Hello!"), /// TextFont { /// font: font_handle.into(), /// font_size: 60.0, /// ..Default::default() /// }, /// TextColor(RED.into()), /// )); /// ``` #[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug, Clone)] #[require(TextFont, TextColor)] pub struct TextSpan(pub String); impl TextSpan { /// Makes a new text span component. pub fn new(text: impl Into) -> Self { Self(text.into()) } } impl TextSpanComponent for TextSpan {} impl TextSpanAccess for TextSpan { fn read_span(&self) -> &str { self.as_str() } fn write_span(&mut self) -> &mut String { &mut *self } } impl From<&str> for TextSpan { fn from(value: &str) -> Self { Self(String::from(value)) } } impl From for TextSpan { fn from(value: String) -> Self { Self(value) } } /// Describes the horizontal alignment of multiple lines of text relative to each other. /// /// This only affects the internal positioning of the lines of text within a text entity and /// does not affect the text entity's position. /// /// _Has no affect on a single line text entity_, unless used together with a /// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] pub enum JustifyText { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] Left, /// Leftmost & rightmost characters are equidistant to the render position. /// Bounds start from the render position and advance equally left & right. Center, /// Rightmost character is immediately to the left of the render position. /// Bounds start from the render position and advance leftwards. Right, /// Words are spaced so that leftmost & rightmost characters /// align with their margins. /// Bounds start from the render position and advance equally left & right. Justified, } impl From for cosmic_text::Align { fn from(justify: JustifyText) -> Self { match justify { JustifyText::Left => cosmic_text::Align::Left, JustifyText::Center => cosmic_text::Align::Center, JustifyText::Right => cosmic_text::Align::Right, JustifyText::Justified => cosmic_text::Align::Justified, } } } /// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically /// the font face, the font size, and the color. #[derive(Component, Clone, Debug, Reflect)] #[reflect(Component, Default, Debug, Clone)] pub struct TextFont { /// The specific font face to use, as a `Handle` to a [`Font`] asset. /// /// If the `font` is not specified, then /// * if `default_font` feature is enabled (enabled by default in `bevy` crate), /// `FiraMono-subset.ttf` compiled into the library is used. /// * otherwise no text will be rendered, unless a custom font is loaded into the default font /// handle. pub font: Handle, /// The vertical height of rasterized glyphs in the font atlas in pixels. /// /// This is multiplied by the window scale factor and `UiScale`, but not the text entity /// transform or camera projection. /// /// A new font atlas is generated for every combination of font handle and scaled font size /// which can have a strong performance impact. pub font_size: f32, /// The vertical height of a line of text, from the top of one line to the top of the /// next. /// /// Defaults to `LineHeight::RelativeToFont(1.2)` pub line_height: LineHeight, /// The antialiasing method to use when rendering text. pub font_smoothing: FontSmoothing, } impl TextFont { /// Returns a new [`TextFont`] with the specified font face handle. pub fn from_font(font: Handle) -> Self { Self::default().with_font(font) } /// Returns a new [`TextFont`] with the specified font size. pub fn from_font_size(font_size: f32) -> Self { Self::default().with_font_size(font_size) } /// Returns this [`TextFont`] with the specified font face handle. pub fn with_font(mut self, font: Handle) -> Self { self.font = font; self } /// Returns this [`TextFont`] with the specified font size. pub const fn with_font_size(mut self, font_size: f32) -> Self { self.font_size = font_size; self } /// Returns this [`TextFont`] with the specified [`FontSmoothing`]. pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self { self.font_smoothing = font_smoothing; self } /// Returns this [`TextFont`] with the specified [`LineHeight`]. pub const fn with_line_height(mut self, line_height: LineHeight) -> Self { self.line_height = line_height; self } } impl Default for TextFont { fn default() -> Self { Self { font: Default::default(), font_size: 20.0, line_height: LineHeight::default(), font_smoothing: Default::default(), } } } /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size #[derive(Debug, Clone, Copy, Reflect)] #[reflect(Debug, Clone)] pub enum LineHeight { /// Set line height to a specific number of pixels Px(f32), /// Set line height to a multiple of the font size RelativeToFont(f32), } impl LineHeight { pub(crate) fn eval(self, font_size: f32) -> f32 { match self { LineHeight::Px(px) => px, LineHeight::RelativeToFont(scale) => scale * font_size, } } } impl Default for LineHeight { fn default() -> Self { LineHeight::RelativeToFont(1.2) } } /// The color of the text for this section. #[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)] #[reflect(Component, Default, Debug, PartialEq, Clone)] pub struct TextColor(pub Color); impl Default for TextColor { fn default() -> Self { Self::WHITE } } impl> From for TextColor { fn from(color: T) -> Self { Self(color.into()) } } impl TextColor { /// Black colored text pub const BLACK: Self = TextColor(Color::BLACK); /// White colored text pub const WHITE: Self = TextColor(Color::WHITE); } /// Determines how lines will be broken when preventing text from running out of bounds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)] pub enum LineBreak { /// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/). /// Lines will be broken up at the nearest suitable word boundary, usually a space. /// This behavior suits most cases, as it keeps words intact across linebreaks. #[default] WordBoundary, /// Lines will be broken without discrimination on any character that would leave bounds. /// This is closer to the behavior one might expect from text in a terminal. /// However it may lead to words being broken up across linebreaks. AnyCharacter, /// Wraps at the word level, or fallback to character level if a word can’t fit on a line by itself WordOrCharacter, /// No soft wrapping, where text is automatically broken up into separate lines when it overflows a boundary, will ever occur. /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, is still enabled. NoWrap, } /// Determines which antialiasing method to use when rendering text. By default, text is /// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look. /// /// **Note:** Subpixel antialiasing is not currently supported. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)] #[doc(alias = "antialiasing")] #[doc(alias = "pixelated")] pub enum FontSmoothing { /// No antialiasing. Useful for when you want to render text with a pixel art aesthetic. /// /// Combine this with `UiAntiAlias::Off` and `Msaa::Off` on your 2D camera for a fully pixelated look. /// /// **Note:** Due to limitations of the underlying text rendering library, /// this may require specially-crafted pixel fonts to look good, especially at small sizes. None, /// The default grayscale antialiasing. Produces text that looks smooth, /// even at small font sizes and low resolutions with modern vector fonts. #[default] AntiAliased, // 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`](crate::Text2d)/[`TextSpan`] 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< (Entity, Option<&ChildOf>, Has), ( Or<( Changed, Changed, Changed, Changed, // Included to detect broken text block hierarchies. Added, )>, With, With, ), >, mut computed: Query<( Option<&ChildOf>, Option<&mut ComputedTextBlock>, Has, )>, ) { // Root entity: // - Root component changed. // - TextFont on root changed. // - TextLayout changed. // - Root children changed (can include additions and removals). for root in changed_roots.iter() { let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else { once!(warn!("found entity {} with a root text component ({}) but no ComputedTextBlock; this warning only \ prints once", root, core::any::type_name::())); continue; }; computed.needs_rerender = true; } // Span entity: // - Span component changed. // - Span TextFont changed. // - Span children changed (can include additions and removals). for (entity, maybe_span_child_of, has_text_block) in changed_spans.iter() { if has_text_block { once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \ text entities (that have {}); this warning only prints once", entity, core::any::type_name::())); } let Some(span_child_of) = maybe_span_child_of else { once!(warn!( "found entity {} with a TextSpan that has no parent; it should have an ancestor \ with a root text component ({}); this warning only prints once", entity, core::any::type_name::() )); continue; }; let mut parent: Entity = span_child_of.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 { let Ok((maybe_child_of, maybe_computed, has_span)) = computed.get_mut(parent) else { once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a ChildOf \ component that points at non-existent entity {}; this warning only prints once", entity, parent)); break; }; if let Some(mut computed) = maybe_computed { computed.needs_rerender = true; break; } if !has_span { once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \ span component or a ComputedTextBlock component; this warning only prints once", entity, parent)); break; } let Some(next_child_of) = maybe_child_of else { once!(warn!( "found entity {} with a TextSpan that has no ancestor with the root text \ component ({}); this warning only prints once", entity, core::any::type_name::() )); break; }; parent = next_child_of.parent; } } }