use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent};1use bevy_asset::Handle;2use bevy_color::Color;3use bevy_derive::{Deref, DerefMut};4use bevy_ecs::{prelude::*, reflect::ReflectComponent};5use bevy_reflect::prelude::*;6use bevy_utils::{default, once};7use cosmic_text::{Buffer, Metrics};8use serde::{Deserialize, Serialize};9use smallvec::SmallVec;10use tracing::warn;1112/// Wrapper for [`cosmic_text::Buffer`]13#[derive(Deref, DerefMut, Debug, Clone)]14pub struct CosmicBuffer(pub Buffer);1516impl Default for CosmicBuffer {17fn default() -> Self {18Self(Buffer::new_empty(Metrics::new(0.0, 0.000001)))19}20}2122/// A sub-entity of a [`ComputedTextBlock`].23///24/// Returned by [`ComputedTextBlock::entities`].25#[derive(Debug, Copy, Clone, Reflect)]26#[reflect(Debug, Clone)]27pub struct TextEntity {28/// The entity.29pub entity: Entity,30/// Records the hierarchy depth of the entity within a `TextLayout`.31pub depth: usize,32}3334/// Computed information for a text block.35///36/// See [`TextLayout`].37///38/// Automatically updated by 2d and UI text systems.39#[derive(Component, Debug, Clone, Reflect)]40#[reflect(Component, Debug, Default, Clone)]41pub struct ComputedTextBlock {42/// Buffer for managing text layout and creating [`TextLayoutInfo`].43///44/// This is private because buffer contents are always refreshed from ECS state when writing glyphs to45/// `TextLayoutInfo`. If you want to control the buffer contents manually or use the `cosmic-text`46/// editor, then you need to not use `TextLayout` and instead manually implement the conversion to47/// `TextLayoutInfo`.48#[reflect(ignore, clone)]49pub(crate) buffer: CosmicBuffer,50/// Entities for all text spans in the block, including the root-level text.51///52/// The [`TextEntity::depth`] field can be used to reconstruct the hierarchy.53pub(crate) entities: SmallVec<[TextEntity; 1]>,54/// Flag set when any change has been made to this block that should cause it to be rerendered.55///56/// Includes:57/// - [`TextLayout`] changes.58/// - [`TextFont`] or `Text2d`/`Text`/`TextSpan` changes anywhere in the block's entity hierarchy.59// TODO: This encompasses both structural changes like font size or justification and non-structural60// changes like text color and font smoothing. This field currently causes UI to 'remeasure' text, even if61// the actual changes are non-structural and can be handled by only rerendering and not remeasuring. A full62// solution would probably require splitting TextLayout and TextFont into structural/non-structural63// components for more granular change detection. A cost/benefit analysis is needed.64pub(crate) needs_rerender: bool,65}6667impl ComputedTextBlock {68/// Accesses entities in this block.69///70/// Can be used to look up [`TextFont`] components for glyphs in [`TextLayoutInfo`] using the `span_index`71/// stored there.72pub fn entities(&self) -> &[TextEntity] {73&self.entities74}7576/// Indicates if the text needs to be refreshed in [`TextLayoutInfo`].77///78/// Updated automatically by [`detect_text_needs_rerender`] and cleared79/// by [`TextPipeline`](crate::TextPipeline) methods.80pub fn needs_rerender(&self) -> bool {81self.needs_rerender82}83/// Accesses the underlying buffer which can be used for `cosmic-text` APIs such as accessing layout information84/// or calculating a cursor position.85///86/// Mutable access is not offered because changes would be overwritten during the automated layout calculation.87/// If you want to control the buffer contents manually or use the `cosmic-text`88/// editor, then you need to not use `TextLayout` and instead manually implement the conversion to89/// `TextLayoutInfo`.90pub fn buffer(&self) -> &CosmicBuffer {91&self.buffer92}93}9495impl Default for ComputedTextBlock {96fn default() -> Self {97Self {98buffer: CosmicBuffer::default(),99entities: SmallVec::default(),100needs_rerender: true,101}102}103}104105/// Component with text format settings for a block of text.106///107/// A block of text is composed of text spans, which each have a separate string value and [`TextFont`]. Text108/// spans associated with a text block are collected into [`ComputedTextBlock`] for layout, and then inserted109/// to [`TextLayoutInfo`] for rendering.110///111/// See `Text2d` in `bevy_sprite` for the core component of 2d text, and `Text` in `bevy_ui` for UI text.112#[derive(Component, Debug, Copy, Clone, Default, Reflect)]113#[reflect(Component, Default, Debug, Clone)]114#[require(ComputedTextBlock, TextLayoutInfo)]115pub struct TextLayout {116/// The text's internal alignment.117/// Should not affect its position within a container.118pub justify: Justify,119/// How the text should linebreak when running out of the bounds determined by `max_size`.120pub linebreak: LineBreak,121}122123impl TextLayout {124/// Makes a new [`TextLayout`].125pub const fn new(justify: Justify, linebreak: LineBreak) -> Self {126Self { justify, linebreak }127}128129/// Makes a new [`TextLayout`] with the specified [`Justify`].130pub fn new_with_justify(justify: Justify) -> Self {131Self::default().with_justify(justify)132}133134/// Makes a new [`TextLayout`] with the specified [`LineBreak`].135pub fn new_with_linebreak(linebreak: LineBreak) -> Self {136Self::default().with_linebreak(linebreak)137}138139/// Makes a new [`TextLayout`] with soft wrapping disabled.140/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.141pub fn new_with_no_wrap() -> Self {142Self::default().with_no_wrap()143}144145/// Returns this [`TextLayout`] with the specified [`Justify`].146pub const fn with_justify(mut self, justify: Justify) -> Self {147self.justify = justify;148self149}150151/// Returns this [`TextLayout`] with the specified [`LineBreak`].152pub const fn with_linebreak(mut self, linebreak: LineBreak) -> Self {153self.linebreak = linebreak;154self155}156157/// Returns this [`TextLayout`] with soft wrapping disabled.158/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.159pub const fn with_no_wrap(mut self) -> Self {160self.linebreak = LineBreak::NoWrap;161self162}163}164165/// A span of text in a tree of spans.166///167/// A `TextSpan` is only valid when it exists as a child of a parent that has either `Text` or168/// `Text2d`. The parent's `Text` / `Text2d` component contains the base text content. Any children169/// with `TextSpan` extend this text by appending their content to the parent's text in sequence to170/// form a [`ComputedTextBlock`]. The parent's [`TextLayout`] determines the layout of the block171/// but each node has its own [`TextFont`] and [`TextColor`].172#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)]173#[reflect(Component, Default, Debug, Clone)]174#[require(TextFont, TextColor)]175pub struct TextSpan(pub String);176177impl TextSpan {178/// Makes a new text span component.179pub fn new(text: impl Into<String>) -> Self {180Self(text.into())181}182}183184impl TextSpanComponent for TextSpan {}185186impl TextSpanAccess for TextSpan {187fn read_span(&self) -> &str {188self.as_str()189}190fn write_span(&mut self) -> &mut String {191&mut *self192}193}194195impl From<&str> for TextSpan {196fn from(value: &str) -> Self {197Self(String::from(value))198}199}200201impl From<String> for TextSpan {202fn from(value: String) -> Self {203Self(value)204}205}206207/// Describes the horizontal alignment of multiple lines of text relative to each other.208///209/// This only affects the internal positioning of the lines of text within a text entity and210/// does not affect the text entity's position.211///212/// _Has no affect on a single line text entity_, unless used together with a213/// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value.214#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]215#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)]216#[doc(alias = "JustifyText")]217pub enum Justify {218/// Leftmost character is immediately to the right of the render position.219/// Bounds start from the render position and advance rightwards.220#[default]221Left,222/// Leftmost & rightmost characters are equidistant to the render position.223/// Bounds start from the render position and advance equally left & right.224Center,225/// Rightmost character is immediately to the left of the render position.226/// Bounds start from the render position and advance leftwards.227Right,228/// Words are spaced so that leftmost & rightmost characters229/// align with their margins.230/// Bounds start from the render position and advance equally left & right.231Justified,232}233234impl From<Justify> for cosmic_text::Align {235fn from(justify: Justify) -> Self {236match justify {237Justify::Left => cosmic_text::Align::Left,238Justify::Center => cosmic_text::Align::Center,239Justify::Right => cosmic_text::Align::Right,240Justify::Justified => cosmic_text::Align::Justified,241}242}243}244245/// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically246/// the font face, the font size, the line height, and the antialiasing method.247#[derive(Component, Clone, Debug, Reflect, PartialEq)]248#[reflect(Component, Default, Debug, Clone)]249pub struct TextFont {250/// The specific font face to use, as a `Handle` to a [`Font`] asset.251///252/// If the `font` is not specified, then253/// * if `default_font` feature is enabled (enabled by default in `bevy` crate),254/// `FiraMono-subset.ttf` compiled into the library is used.255/// * otherwise no text will be rendered, unless a custom font is loaded into the default font256/// handle.257pub font: Handle<Font>,258/// The vertical height of rasterized glyphs in the font atlas in pixels.259///260/// This is multiplied by the window scale factor and `UiScale`, but not the text entity261/// transform or camera projection.262///263/// A new font atlas is generated for every combination of font handle and scaled font size264/// which can have a strong performance impact.265pub font_size: f32,266/// The vertical height of a line of text, from the top of one line to the top of the267/// next.268///269/// Defaults to `LineHeight::RelativeToFont(1.2)`270pub line_height: LineHeight,271/// The antialiasing method to use when rendering text.272pub font_smoothing: FontSmoothing,273}274275impl TextFont {276/// Returns a new [`TextFont`] with the specified font size.277pub fn from_font_size(font_size: f32) -> Self {278Self::default().with_font_size(font_size)279}280281/// Returns this [`TextFont`] with the specified font face handle.282pub fn with_font(mut self, font: Handle<Font>) -> Self {283self.font = font;284self285}286287/// Returns this [`TextFont`] with the specified font size.288pub const fn with_font_size(mut self, font_size: f32) -> Self {289self.font_size = font_size;290self291}292293/// Returns this [`TextFont`] with the specified [`FontSmoothing`].294pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self {295self.font_smoothing = font_smoothing;296self297}298299/// Returns this [`TextFont`] with the specified [`LineHeight`].300pub const fn with_line_height(mut self, line_height: LineHeight) -> Self {301self.line_height = line_height;302self303}304}305306impl From<Handle<Font>> for TextFont {307fn from(font: Handle<Font>) -> Self {308Self { font, ..default() }309}310}311312impl From<LineHeight> for TextFont {313fn from(line_height: LineHeight) -> Self {314Self {315line_height,316..default()317}318}319}320321impl Default for TextFont {322fn default() -> Self {323Self {324font: Default::default(),325font_size: 20.0,326line_height: LineHeight::default(),327font_smoothing: Default::default(),328}329}330}331332/// Specifies the height of each line of text for `Text` and `Text2d`333///334/// Default is 1.2x the font size335#[derive(Debug, Clone, Copy, PartialEq, Reflect)]336#[reflect(Debug, Clone, PartialEq)]337pub enum LineHeight {338/// Set line height to a specific number of pixels339Px(f32),340/// Set line height to a multiple of the font size341RelativeToFont(f32),342}343344impl LineHeight {345pub(crate) fn eval(self, font_size: f32) -> f32 {346match self {347LineHeight::Px(px) => px,348LineHeight::RelativeToFont(scale) => scale * font_size,349}350}351}352353impl Default for LineHeight {354fn default() -> Self {355LineHeight::RelativeToFont(1.2)356}357}358359/// The color of the text for this section.360#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]361#[reflect(Component, Default, Debug, PartialEq, Clone)]362pub struct TextColor(pub Color);363364impl Default for TextColor {365fn default() -> Self {366Self::WHITE367}368}369370impl<T: Into<Color>> From<T> for TextColor {371fn from(color: T) -> Self {372Self(color.into())373}374}375376impl TextColor {377/// Black colored text378pub const BLACK: Self = TextColor(Color::BLACK);379/// White colored text380pub const WHITE: Self = TextColor(Color::WHITE);381}382383/// The background color of the text for this section.384#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]385#[reflect(Component, Default, Debug, PartialEq, Clone)]386pub struct TextBackgroundColor(pub Color);387388impl Default for TextBackgroundColor {389fn default() -> Self {390Self(Color::BLACK)391}392}393394impl<T: Into<Color>> From<T> for TextBackgroundColor {395fn from(color: T) -> Self {396Self(color.into())397}398}399400impl TextBackgroundColor {401/// Black background402pub const BLACK: Self = TextBackgroundColor(Color::BLACK);403/// White background404pub const WHITE: Self = TextBackgroundColor(Color::WHITE);405}406407/// Determines how lines will be broken when preventing text from running out of bounds.408#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]409#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]410pub enum LineBreak {411/// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/).412/// Lines will be broken up at the nearest suitable word boundary, usually a space.413/// This behavior suits most cases, as it keeps words intact across linebreaks.414#[default]415WordBoundary,416/// Lines will be broken without discrimination on any character that would leave bounds.417/// This is closer to the behavior one might expect from text in a terminal.418/// However it may lead to words being broken up across linebreaks.419AnyCharacter,420/// Wraps at the word level, or fallback to character level if a word can’t fit on a line by itself421WordOrCharacter,422/// No soft wrapping, where text is automatically broken up into separate lines when it overflows a boundary, will ever occur.423/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, is still enabled.424NoWrap,425}426427/// Determines which antialiasing method to use when rendering text. By default, text is428/// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look.429///430/// **Note:** Subpixel antialiasing is not currently supported.431#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]432#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]433#[doc(alias = "antialiasing")]434#[doc(alias = "pixelated")]435pub enum FontSmoothing {436/// No antialiasing. Useful for when you want to render text with a pixel art aesthetic.437///438/// Combine this with `UiAntiAlias::Off` and `Msaa::Off` on your 2D camera for a fully pixelated look.439///440/// **Note:** Due to limitations of the underlying text rendering library,441/// this may require specially-crafted pixel fonts to look good, especially at small sizes.442None,443/// The default grayscale antialiasing. Produces text that looks smooth,444/// even at small font sizes and low resolutions with modern vector fonts.445#[default]446AntiAliased,447// TODO: Add subpixel antialias support448// SubpixelAntiAliased,449}450451/// System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`.452///453/// Generic over the root text component and text span component. For example, `Text2d`/[`TextSpan`] for454/// 2d or `Text`/[`TextSpan`] for UI.455pub fn detect_text_needs_rerender<Root: Component>(456changed_roots: Query<457Entity,458(459Or<(460Changed<Root>,461Changed<TextFont>,462Changed<TextLayout>,463Changed<Children>,464)>,465With<Root>,466With<TextFont>,467With<TextLayout>,468),469>,470changed_spans: Query<471(Entity, Option<&ChildOf>, Has<TextLayout>),472(473Or<(474Changed<TextSpan>,475Changed<TextFont>,476Changed<Children>,477Changed<ChildOf>, // Included to detect broken text block hierarchies.478Added<TextLayout>,479)>,480With<TextSpan>,481With<TextFont>,482),483>,484mut computed: Query<(485Option<&ChildOf>,486Option<&mut ComputedTextBlock>,487Has<TextSpan>,488)>,489) {490// Root entity:491// - Root component changed.492// - TextFont on root changed.493// - TextLayout changed.494// - Root children changed (can include additions and removals).495for root in changed_roots.iter() {496let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else {497once!(warn!("found entity {} with a root text component ({}) but no ComputedTextBlock; this warning only \498prints once", root, core::any::type_name::<Root>()));499continue;500};501computed.needs_rerender = true;502}503504// Span entity:505// - Span component changed.506// - Span TextFont changed.507// - Span children changed (can include additions and removals).508for (entity, maybe_span_child_of, has_text_block) in changed_spans.iter() {509if has_text_block {510once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \511text entities (that have {}); this warning only prints once",512entity, core::any::type_name::<Root>()));513}514515let Some(span_child_of) = maybe_span_child_of else {516once!(warn!(517"found entity {} with a TextSpan that has no parent; it should have an ancestor \518with a root text component ({}); this warning only prints once",519entity,520core::any::type_name::<Root>()521));522continue;523};524let mut parent: Entity = span_child_of.parent();525526// Search for the nearest ancestor with ComputedTextBlock.527// Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited528// is outweighed by the expense of tracking visited spans.529loop {530let Ok((maybe_child_of, maybe_computed, has_span)) = computed.get_mut(parent) else {531once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a ChildOf \532component that points at non-existent entity {}; this warning only prints once",533entity, parent));534break;535};536if let Some(mut computed) = maybe_computed {537computed.needs_rerender = true;538break;539}540if !has_span {541once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \542span component or a ComputedTextBlock component; this warning only prints once",543entity, parent));544break;545}546let Some(next_child_of) = maybe_child_of else {547once!(warn!(548"found entity {} with a TextSpan that has no ancestor with the root text \549component ({}); this warning only prints once",550entity,551core::any::type_name::<Root>()552));553break;554};555parent = next_child_of.parent();556}557}558}559560561