Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

BIN
vendor/bevy_text/src/FiraMono-subset.ttf vendored Normal file

Binary file not shown.

71
vendor/bevy_text/src/bounds.rs vendored Normal file
View File

@@ -0,0 +1,71 @@
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// The maximum width and height of text. The text will wrap according to the specified size.
///
/// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the
/// specified [`JustifyText`](crate::text::JustifyText).
///
/// Note: only characters that are completely out of the bounds will be truncated, so this is not a
/// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this
/// component is mainly useful for text wrapping only.
#[derive(Component, Copy, Clone, Debug, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextBounds {
/// The maximum width of text in logical pixels.
/// If `None`, the width is unbounded.
pub width: Option<f32>,
/// The maximum height of text in logical pixels.
/// If `None`, the height is unbounded.
pub height: Option<f32>,
}
impl Default for TextBounds {
#[inline]
fn default() -> Self {
Self::UNBOUNDED
}
}
impl TextBounds {
/// Unbounded text will not be truncated or wrapped.
pub const UNBOUNDED: Self = Self {
width: None,
height: None,
};
/// Creates a new `TextBounds`, bounded with the specified width and height values.
#[inline]
pub const fn new(width: f32, height: f32) -> Self {
Self {
width: Some(width),
height: Some(height),
}
}
/// Creates a new `TextBounds`, bounded with the specified width value and unbounded on height.
#[inline]
pub const fn new_horizontal(width: f32) -> Self {
Self {
width: Some(width),
height: None,
}
}
/// Creates a new `TextBounds`, bounded with the specified height value and unbounded on width.
#[inline]
pub const fn new_vertical(height: f32) -> Self {
Self {
width: None,
height: Some(height),
}
}
}
impl From<Vec2> for TextBounds {
#[inline]
fn from(v: Vec2) -> Self {
Self::new(v.x, v.y)
}
}

17
vendor/bevy_text/src/error.rs vendored Normal file
View File

@@ -0,0 +1,17 @@
use cosmic_text::CacheKey;
use thiserror::Error;
#[derive(Debug, PartialEq, Eq, Error)]
/// Errors related to the textsystem
pub enum TextError {
/// Font was not found, this could be that the font has not yet been loaded, or
/// that the font failed to load for some other reason
#[error("font not found")]
NoSuchFont,
/// Failed to add glyph to a newly created atlas for some reason
#[error("failed to add glyph to newly-created atlas {0:?}")]
FailedToAddGlyph(u16),
/// Failed to get scaled glyph image for cache key
#[error("failed to get scaled glyph image for cache key: {0:?}")]
FailedToGetGlyphImage(CacheKey),
}

35
vendor/bevy_text/src/font.rs vendored Normal file
View File

@@ -0,0 +1,35 @@
use alloc::sync::Arc;
use bevy_asset::Asset;
use bevy_reflect::TypePath;
/// An [`Asset`] that contains the data for a loaded font, if loaded as an asset.
///
/// Loaded by [`FontLoader`](crate::FontLoader).
///
/// # A note on fonts
///
/// `Font` may differ from the everyday notion of what a "font" is.
/// A font *face* (e.g. Fira Sans Semibold Italic) is part of a font *family* (e.g. Fira Sans),
/// and is distinguished from other font faces in the same family
/// by its style (e.g. italic), its weight (e.g. bold) and its stretch (e.g. condensed).
///
/// Bevy currently loads a single font face as a single `Font` asset.
#[derive(Debug, TypePath, Clone, Asset)]
pub struct Font {
/// Content of a font file as bytes
pub data: Arc<Vec<u8>>,
}
impl Font {
/// Creates a [`Font`] from bytes
pub fn try_from_bytes(
font_data: Vec<u8>,
) -> Result<Self, cosmic_text::ttf_parser::FaceParsingError> {
use cosmic_text::ttf_parser;
ttf_parser::Face::parse(&font_data, 0)?;
Ok(Self {
data: Arc::new(font_data),
})
}
}

127
vendor/bevy_text/src/font_atlas.rs vendored Normal file
View File

@@ -0,0 +1,127 @@
use bevy_asset::{Assets, Handle};
use bevy_image::{prelude::*, ImageSampler};
use bevy_math::{IVec2, UVec2};
use bevy_platform::collections::HashMap;
use bevy_render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
};
use crate::{FontSmoothing, GlyphAtlasLocation, TextError};
/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`.
///
/// A `FontAtlas` contains one or more textures, each of which contains one or more glyphs packed into them.
///
/// A [`FontAtlasSet`](crate::FontAtlasSet) contains a `FontAtlas` for each font size in the same font face.
///
/// For the same font face and font size, a glyph will be rasterized differently for different subpixel offsets.
/// In practice, ranges of subpixel offsets are grouped into subpixel bins to limit the number of rasterized glyphs,
/// providing a trade-off between visual quality and performance.
///
/// A [`CacheKey`](cosmic_text::CacheKey) encodes all of the information of a subpixel-offset glyph and is used to
/// find that glyphs raster in a [`TextureAtlas`] through its corresponding [`GlyphAtlasLocation`].
pub struct FontAtlas {
/// Used to update the [`TextureAtlasLayout`].
pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
/// A mapping between subpixel-offset glyphs and their [`GlyphAtlasLocation`].
pub glyph_to_atlas_index: HashMap<cosmic_text::CacheKey, GlyphAtlasLocation>,
/// The handle to the [`TextureAtlasLayout`] that holds the rasterized glyphs.
pub texture_atlas: Handle<TextureAtlasLayout>,
/// The texture where this font atlas is located
pub texture: Handle<Image>,
}
impl FontAtlas {
/// Create a new [`FontAtlas`] with the given size, adding it to the appropriate asset collections.
pub fn new(
textures: &mut Assets<Image>,
texture_atlases_layout: &mut Assets<TextureAtlasLayout>,
size: UVec2,
font_smoothing: FontSmoothing,
) -> FontAtlas {
let mut image = Image::new_fill(
Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Rgba8UnormSrgb,
// Need to keep this image CPU persistent in order to add additional glyphs later on
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
);
if font_smoothing == FontSmoothing::None {
image.sampler = ImageSampler::nearest();
}
let texture = textures.add(image);
let texture_atlas = texture_atlases_layout.add(TextureAtlasLayout::new_empty(size));
Self {
texture_atlas,
glyph_to_atlas_index: HashMap::default(),
dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1),
texture,
}
}
/// Get the [`GlyphAtlasLocation`] for a subpixel-offset glyph.
pub fn get_glyph_index(&self, cache_key: cosmic_text::CacheKey) -> Option<GlyphAtlasLocation> {
self.glyph_to_atlas_index.get(&cache_key).copied()
}
/// Checks if the given subpixel-offset glyph is contained in this [`FontAtlas`].
pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey) -> bool {
self.glyph_to_atlas_index.contains_key(&cache_key)
}
/// Add a glyph to the atlas, updating both its texture and layout.
///
/// The glyph is represented by `glyph`, and its image content is `glyph_texture`.
/// This content is copied into the atlas texture, and the atlas layout is updated
/// to store the location of that glyph into the atlas.
///
/// # Returns
///
/// Returns `()` if the glyph is successfully added, or [`TextError::FailedToAddGlyph`] otherwise.
/// In that case, neither the atlas texture nor the atlas layout are
/// modified.
pub fn add_glyph(
&mut self,
textures: &mut Assets<Image>,
atlas_layouts: &mut Assets<TextureAtlasLayout>,
cache_key: cosmic_text::CacheKey,
texture: &Image,
offset: IVec2,
) -> Result<(), TextError> {
let atlas_layout = atlas_layouts.get_mut(&self.texture_atlas).unwrap();
let atlas_texture = textures.get_mut(&self.texture).unwrap();
if let Ok(glyph_index) =
self.dynamic_texture_atlas_builder
.add_texture(atlas_layout, texture, atlas_texture)
{
self.glyph_to_atlas_index.insert(
cache_key,
GlyphAtlasLocation {
glyph_index,
offset,
},
);
Ok(())
} else {
Err(TextError::FailedToAddGlyph(cache_key.glyph_id))
}
}
}
impl core::fmt::Debug for FontAtlas {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("FontAtlas")
.field("glyph_to_atlas_index", &self.glyph_to_atlas_index)
.field("texture_atlas", &self.texture_atlas)
.field("texture", &self.texture)
.field("dynamic_texture_atlas_builder", &"[...]")
.finish()
}
}

265
vendor/bevy_text/src/font_atlas_set.rs vendored Normal file
View File

@@ -0,0 +1,265 @@
use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
use bevy_ecs::{event::EventReader, resource::Resource, system::ResMut};
use bevy_image::prelude::*;
use bevy_math::{IVec2, UVec2};
use bevy_platform::collections::HashMap;
use bevy_reflect::TypePath;
use bevy_render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
};
use crate::{error::TextError, Font, FontAtlas, FontSmoothing, GlyphAtlasInfo};
/// A map of font faces to their corresponding [`FontAtlasSet`]s.
#[derive(Debug, Default, Resource)]
pub struct FontAtlasSets {
// PERF: in theory this could be optimized with Assets storage ... consider making some fast "simple" AssetMap
pub(crate) sets: HashMap<AssetId<Font>, FontAtlasSet>,
}
impl FontAtlasSets {
/// Get a reference to the [`FontAtlasSet`] with the given font asset id.
pub fn get(&self, id: impl Into<AssetId<Font>>) -> Option<&FontAtlasSet> {
let id: AssetId<Font> = id.into();
self.sets.get(&id)
}
/// Get a mutable reference to the [`FontAtlasSet`] with the given font asset id.
pub fn get_mut(&mut self, id: impl Into<AssetId<Font>>) -> Option<&mut FontAtlasSet> {
let id: AssetId<Font> = id.into();
self.sets.get_mut(&id)
}
}
/// A system that cleans up [`FontAtlasSet`]s for removed [`Font`]s
pub fn remove_dropped_font_atlas_sets(
mut font_atlas_sets: ResMut<FontAtlasSets>,
mut font_events: EventReader<AssetEvent<Font>>,
) {
for event in font_events.read() {
if let AssetEvent::Removed { id } = event {
font_atlas_sets.sets.remove(id);
}
}
}
/// Identifies a font size and smoothing method in a [`FontAtlasSet`].
///
/// Allows an `f32` font size to be used as a key in a `HashMap`, by its binary representation.
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct FontAtlasKey(pub u32, pub FontSmoothing);
/// A map of font sizes to their corresponding [`FontAtlas`]es, for a given font face.
///
/// Provides the interface for adding and retrieving rasterized glyphs, and manages the [`FontAtlas`]es.
///
/// A `FontAtlasSet` is an [`Asset`].
///
/// There is one `FontAtlasSet` for each font:
/// - When a [`Font`] is loaded as an asset and then used in [`TextFont`](crate::TextFont),
/// a `FontAtlasSet` asset is created from a weak handle to the `Font`.
/// - ~When a font is loaded as a system font, and then used in [`TextFont`](crate::TextFont),
/// a `FontAtlasSet` asset is created and stored with a strong handle to the `FontAtlasSet`.~
/// (*Note that system fonts are not currently supported by the `TextPipeline`.*)
///
/// A `FontAtlasSet` contains one or more [`FontAtlas`]es for each font size.
///
/// It is used by [`TextPipeline::queue_text`](crate::TextPipeline::queue_text).
#[derive(Debug, TypePath, Asset)]
pub struct FontAtlasSet {
font_atlases: HashMap<FontAtlasKey, Vec<FontAtlas>>,
}
impl Default for FontAtlasSet {
fn default() -> Self {
FontAtlasSet {
font_atlases: HashMap::with_capacity_and_hasher(1, Default::default()),
}
}
}
impl FontAtlasSet {
/// Returns an iterator over the [`FontAtlas`]es in this set
pub fn iter(&self) -> impl Iterator<Item = (&FontAtlasKey, &Vec<FontAtlas>)> {
self.font_atlases.iter()
}
/// Checks if the given subpixel-offset glyph is contained in any of the [`FontAtlas`]es in this set
pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: &FontAtlasKey) -> bool {
self.font_atlases
.get(font_size)
.is_some_and(|font_atlas| font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)))
}
/// Adds the given subpixel-offset glyph to the [`FontAtlas`]es in this set
pub fn add_glyph_to_atlas(
&mut self,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
font_system: &mut cosmic_text::FontSystem,
swash_cache: &mut cosmic_text::SwashCache,
layout_glyph: &cosmic_text::LayoutGlyph,
font_smoothing: FontSmoothing,
) -> Result<GlyphAtlasInfo, TextError> {
let physical_glyph = layout_glyph.physical((0., 0.), 1.0);
let font_atlases = self
.font_atlases
.entry(FontAtlasKey(
physical_glyph.cache_key.font_size_bits,
font_smoothing,
))
.or_insert_with(|| {
vec![FontAtlas::new(
textures,
texture_atlases,
UVec2::splat(512),
font_smoothing,
)]
});
let (glyph_texture, offset) = Self::get_outlined_glyph_texture(
font_system,
swash_cache,
&physical_glyph,
font_smoothing,
)?;
let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> {
atlas.add_glyph(
textures,
texture_atlases,
physical_glyph.cache_key,
&glyph_texture,
offset,
)
};
if !font_atlases
.iter_mut()
.any(|atlas| add_char_to_font_atlas(atlas).is_ok())
{
// Find the largest dimension of the glyph, either its width or its height
let glyph_max_size: u32 = glyph_texture
.texture_descriptor
.size
.height
.max(glyph_texture.width());
// Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size
let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512);
font_atlases.push(FontAtlas::new(
textures,
texture_atlases,
UVec2::splat(containing),
font_smoothing,
));
font_atlases.last_mut().unwrap().add_glyph(
textures,
texture_atlases,
physical_glyph.cache_key,
&glyph_texture,
offset,
)?;
}
Ok(self
.get_glyph_atlas_info(physical_glyph.cache_key, font_smoothing)
.unwrap())
}
/// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph.
pub fn get_glyph_atlas_info(
&mut self,
cache_key: cosmic_text::CacheKey,
font_smoothing: FontSmoothing,
) -> Option<GlyphAtlasInfo> {
self.font_atlases
.get(&FontAtlasKey(cache_key.font_size_bits, font_smoothing))
.and_then(|font_atlases| {
font_atlases.iter().find_map(|atlas| {
atlas
.get_glyph_index(cache_key)
.map(|location| GlyphAtlasInfo {
location,
texture_atlas: atlas.texture_atlas.clone_weak(),
texture: atlas.texture.clone_weak(),
})
})
})
}
/// Returns the number of font atlases in this set.
pub fn len(&self) -> usize {
self.font_atlases.len()
}
/// Returns `true` if the set has no font atlases.
pub fn is_empty(&self) -> bool {
self.font_atlases.len() == 0
}
/// Get the texture of the glyph as a rendered image, and its offset
pub fn get_outlined_glyph_texture(
font_system: &mut cosmic_text::FontSystem,
swash_cache: &mut cosmic_text::SwashCache,
physical_glyph: &cosmic_text::PhysicalGlyph,
font_smoothing: FontSmoothing,
) -> Result<(Image, IVec2), TextError> {
// NOTE: Ideally, we'd ask COSMIC Text to honor the font smoothing setting directly.
// However, since it currently doesn't support that, we render the glyph with antialiasing
// and apply a threshold to the alpha channel to simulate the effect.
//
// This has the side effect of making regular vector fonts look quite ugly when font smoothing
// is turned off, but for fonts that are specifically designed for pixel art, it works well.
//
// See: https://github.com/pop-os/cosmic-text/issues/279
let image = swash_cache
.get_image_uncached(font_system, physical_glyph.cache_key)
.ok_or(TextError::FailedToGetGlyphImage(physical_glyph.cache_key))?;
let cosmic_text::Placement {
left,
top,
width,
height,
} = image.placement;
let data = match image.content {
cosmic_text::SwashContent::Mask => {
if font_smoothing == FontSmoothing::None {
image
.data
.iter()
// Apply a 50% threshold to the alpha channel
.flat_map(|a| [255, 255, 255, if *a > 127 { 255 } else { 0 }])
.collect()
} else {
image
.data
.iter()
.flat_map(|a| [255, 255, 255, *a])
.collect()
}
}
cosmic_text::SwashContent::Color => image.data,
cosmic_text::SwashContent::SubpixelMask => {
// TODO: implement
todo!()
}
};
Ok((
Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::MAIN_WORLD,
),
IVec2::new(left, top),
))
}
}

40
vendor/bevy_text/src/font_loader.rs vendored Normal file
View File

@@ -0,0 +1,40 @@
use crate::Font;
use bevy_asset::{io::Reader, AssetLoader, LoadContext};
use thiserror::Error;
#[derive(Default)]
/// An [`AssetLoader`] for [`Font`]s, for use by the [`AssetServer`](bevy_asset::AssetServer)
pub struct FontLoader;
/// Possible errors that can be produced by [`FontLoader`]
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum FontLoaderError {
/// The contents that could not be parsed
#[error(transparent)]
Content(#[from] cosmic_text::ttf_parser::FaceParsingError),
/// An [IO](std::io) Error
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl AssetLoader for FontLoader {
type Asset = Font;
type Settings = ();
type Error = FontLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<Font, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let font = Font::try_from_bytes(bytes)?;
Ok(font)
}
fn extensions(&self) -> &[&str] {
&["ttf", "otf"]
}
}

64
vendor/bevy_text/src/glyph.rs vendored Normal file
View File

@@ -0,0 +1,64 @@
//! This module exports types related to rendering glyphs.
use bevy_asset::Handle;
use bevy_image::prelude::*;
use bevy_math::{IVec2, Vec2};
use bevy_reflect::Reflect;
/// A glyph of a font, typically representing a single character, positioned in screen space.
///
/// Contains information about how and where to render a glyph.
///
/// Used in [`TextPipeline::queue_text`](crate::TextPipeline::queue_text) and [`crate::TextLayoutInfo`] for rendering glyphs.
#[derive(Debug, Clone, Reflect)]
#[reflect(Clone)]
pub struct PositionedGlyph {
/// The position of the glyph in the text block's bounding box.
pub position: Vec2,
/// The width and height of the glyph in logical pixels.
pub size: Vec2,
/// Information about the glyph's atlas.
pub atlas_info: GlyphAtlasInfo,
/// The index of the glyph in the [`ComputedTextBlock`](crate::ComputedTextBlock)'s tracked spans.
pub span_index: usize,
/// The index of the glyph's line.
pub line_index: usize,
/// The byte index of the glyph in it's line.
pub byte_index: usize,
/// The byte length of the glyph.
pub byte_length: usize,
}
/// Information about a glyph in an atlas.
///
/// Rasterized glyphs are stored as rectangles
/// in one or more [`FontAtlas`](crate::FontAtlas)es.
///
/// Used in [`PositionedGlyph`] and [`FontAtlasSet`](crate::FontAtlasSet).
#[derive(Debug, Clone, Reflect)]
#[reflect(Clone)]
pub struct GlyphAtlasInfo {
/// A handle to the [`Image`] data for the texture atlas this glyph was placed in.
///
/// A (weak) clone of the handle held by the [`FontAtlas`](crate::FontAtlas).
pub texture: Handle<Image>,
/// A handle to the [`TextureAtlasLayout`] map for the texture atlas this glyph was placed in.
///
/// A (weak) clone of the handle held by the [`FontAtlas`](crate::FontAtlas).
pub texture_atlas: Handle<TextureAtlasLayout>,
/// Location and offset of a glyph within the texture atlas.
pub location: GlyphAtlasLocation,
}
/// The location of a glyph in an atlas,
/// and how it should be positioned when placed.
///
/// Used in [`GlyphAtlasInfo`] and [`FontAtlas`](crate::FontAtlas).
#[derive(Debug, Clone, Copy, Reflect)]
#[reflect(Clone)]
pub struct GlyphAtlasLocation {
/// The index of the glyph in the atlas
pub glyph_index: usize,
/// The required offset (relative positioning) when placed
pub offset: IVec2,
}

158
vendor/bevy_text/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,158 @@
//! This crate provides the tools for positioning and rendering text in Bevy.
//!
//! # `Font`
//!
//! Fonts contain information for drawing glyphs, which are shapes that typically represent a single character,
//! but in some cases part of a "character" (grapheme clusters) or more than one character (ligatures).
//!
//! A font *face* is part of a font family,
//! and is distinguished by its style (e.g. italic), its weight (e.g. bold) and its stretch (e.g. condensed).
//!
//! In Bevy, [`Font`]s are loaded by the [`FontLoader`] as [assets](bevy_asset::AssetPlugin).
//!
//! # `TextPipeline`
//!
//! The [`TextPipeline`] resource does all of the heavy lifting for rendering text.
//!
//! UI `Text` is first measured by creating a [`TextMeasureInfo`] in [`TextPipeline::create_text_measure`],
//! which is called by the `measure_text_system` system of `bevy_ui`.
//!
//! Note that text measurement is only relevant in a UI context.
//!
//! With the actual text bounds defined, the `bevy_ui::widget::text::text_system` system (in a UI context)
//! or [`text2d::update_text2d_layout`] system (in a 2d world space context)
//! passes it into [`TextPipeline::queue_text`], which:
//!
//! 1. updates a [`Buffer`](cosmic_text::Buffer) from the [`TextSpan`]s, generating new [`FontAtlasSet`]s if necessary.
//! 2. iterates over each glyph in the [`Buffer`](cosmic_text::Buffer) to create a [`PositionedGlyph`],
//! retrieving glyphs from the cache, or rasterizing to a [`FontAtlas`] if necessary.
//! 3. [`PositionedGlyph`]s are stored in a [`TextLayoutInfo`],
//! which contains all the information that downstream systems need for rendering.
extern crate alloc;
mod bounds;
mod error;
mod font;
mod font_atlas;
mod font_atlas_set;
mod font_loader;
mod glyph;
mod pipeline;
mod text;
mod text2d;
mod text_access;
pub use bounds::*;
pub use error::*;
pub use font::*;
pub use font_atlas::*;
pub use font_atlas_set::*;
pub use font_loader::*;
pub use glyph::*;
pub use pipeline::*;
pub use text::*;
pub use text2d::*;
pub use text_access::*;
/// 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, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError,
TextFont, TextLayout, TextSpan,
};
}
use bevy_app::{prelude::*, Animation};
#[cfg(feature = "default_font")]
use bevy_asset::{load_internal_binary_asset, Handle};
use bevy_asset::{AssetApp, AssetEvents};
use bevy_ecs::prelude::*;
use bevy_render::{
camera::CameraUpdateSystem, view::VisibilitySystems, ExtractSchedule, RenderApp,
};
use bevy_sprite::SpriteSystem;
/// The raw data for the default font used by `bevy_text`
#[cfg(feature = "default_font")]
pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf");
/// Adds text rendering support to an app.
///
/// When the `bevy_text` feature is enabled with the `bevy` crate, this
/// plugin is included by default in the `DefaultPlugins`.
#[derive(Default)]
pub struct TextPlugin;
/// Text is rendered for two different view projections;
/// 2-dimensional text ([`Text2d`]) is rendered in "world space" with a `BottomToTop` Y-axis,
/// while UI is rendered with a `TopToBottom` Y-axis.
/// This matters for text because the glyph positioning is different in either layout.
/// For `TopToBottom`, 0 is the top of the text, while for `BottomToTop` 0 is the bottom.
pub enum YAxisOrientation {
/// Top to bottom Y-axis orientation, for UI
TopToBottom,
/// Bottom to top Y-axis orientation, for 2d world space
BottomToTop,
}
/// System set in [`PostUpdate`] where all 2d text update systems are executed.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub struct Update2dText;
impl Plugin for TextPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<Font>()
.register_type::<Text2d>()
.register_type::<TextFont>()
.register_type::<LineHeight>()
.register_type::<TextColor>()
.register_type::<TextSpan>()
.register_type::<TextBounds>()
.register_type::<TextLayout>()
.register_type::<ComputedTextBlock>()
.register_type::<TextEntity>()
.init_asset_loader::<FontLoader>()
.init_resource::<FontAtlasSets>()
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.init_resource::<SwashCache>()
.init_resource::<TextIterScratch>()
.add_systems(
PostUpdate,
(
remove_dropped_font_atlas_sets.before(AssetEvents),
detect_text_needs_rerender::<Text2d>,
update_text2d_layout
// Potential conflict: `Assets<Image>`
// 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),
calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
)
.chain()
.in_set(Update2dText)
.after(Animation),
)
.add_systems(Last, trim_cosmic_cache);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(
ExtractSchedule,
extract_text2d_sprite.after(SpriteSystem::ExtractSprites),
);
}
#[cfg(feature = "default_font")]
load_internal_binary_asset!(
app,
Handle::default(),
"FiraMono-subset.ttf",
|bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
);
}
}

534
vendor/bevy_text/src/pipeline.rs vendored Normal file
View File

@@ -0,0 +1,534 @@
use alloc::sync::Arc;
use bevy_asset::{AssetId, Assets};
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource,
system::ResMut,
};
use bevy_image::prelude::*;
use bevy_log::{once, warn};
use bevy_math::{UVec2, Vec2};
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
use crate::{
error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText,
LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, YAxisOrientation,
};
/// A wrapper resource around a [`cosmic_text::FontSystem`]
///
/// The font system is used to retrieve fonts and their information, including glyph outlines.
///
/// This resource is updated by the [`TextPipeline`] resource.
#[derive(Resource, Deref, DerefMut)]
pub struct CosmicFontSystem(pub cosmic_text::FontSystem);
impl Default for CosmicFontSystem {
fn default() -> Self {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
let db = cosmic_text::fontdb::Database::new();
// TODO: consider using `cosmic_text::FontSystem::new()` (load system fonts by default)
Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db))
}
}
/// A wrapper resource around a [`cosmic_text::SwashCache`]
///
/// The swash cache rasterizer is used to rasterize glyphs
///
/// This resource is updated by the [`TextPipeline`] resource.
#[derive(Resource)]
pub struct SwashCache(pub cosmic_text::SwashCache);
impl Default for SwashCache {
fn default() -> Self {
Self(cosmic_text::SwashCache::new())
}
}
/// Information about a font collected as part of preparing for text layout.
#[derive(Clone)]
struct FontFaceInfo {
stretch: cosmic_text::fontdb::Stretch,
style: cosmic_text::fontdb::Style,
weight: cosmic_text::fontdb::Weight,
family_name: Arc<str>,
}
/// The `TextPipeline` is used to layout and render text blocks (see `Text`/[`Text2d`](crate::Text2d)).
///
/// See the [crate-level documentation](crate) for more information.
#[derive(Default, Resource)]
pub struct TextPipeline {
/// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its [`Font`] [`Asset`](bevy_asset::Asset).
map_handle_to_font_id: HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
/// Buffered vec for collecting spans.
///
/// 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 TextFont, FontFaceInfo)>,
/// Buffered vec for collecting info for glyph assembly.
glyph_info: Vec<(AssetId<Font>, FontSmoothing)>,
}
impl TextPipeline {
/// Utilizes [`cosmic_text::Buffer`] to shape and layout text
///
/// Negative or 0.0 font sizes will not be laid out.
pub fn update_buffer<'a>(
&mut self,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
linebreak: LineBreak,
justify: JustifyText,
bounds: TextBounds,
scale_factor: f64,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
) -> Result<(), TextError> {
let font_system = &mut font_system.0;
// Collect span information into a vec. This is necessary because font loading requires mut access
// to FontSystem, which the cosmic-text Buffer also needs.
let mut font_size: f32 = 0.;
let mut line_height: f32 = 0.0;
let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color)> =
core::mem::take(&mut self.spans_buffer)
.into_iter()
.map(|_| -> (usize, &str, &TextFont, FontFaceInfo, Color) { unreachable!() })
.collect();
computed.entities.clear();
for (span_index, (entity, depth, span, text_font, color)) in text_spans.enumerate() {
// Save this span entity in the computed text block.
computed.entities.push(TextEntity { entity, depth });
if span.is_empty() {
continue;
}
// Return early if a font is not loaded yet.
if !fonts.contains(text_font.font.id()) {
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(
|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) {
unreachable!()
},
)
.collect();
return Err(TextError::NoSuchFont);
}
// Get max font size for use in cosmic Metrics.
font_size = font_size.max(text_font.font_size);
line_height = line_height.max(text_font.line_height.eval(text_font.font_size));
// Load Bevy fonts into cosmic-text's font system.
let face_info = load_font_to_fontdb(
text_font,
font_system,
&mut self.map_handle_to_font_id,
fonts,
);
// Save spans that aren't zero-sized.
if scale_factor <= 0.0 || text_font.font_size <= 0.0 {
once!(warn!(
"Text span {entity} has a font size <= 0.0. Nothing will be displayed.",
));
continue;
}
spans.push((span_index, span, text_font, face_info, color));
}
let mut metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32);
// Metrics of 0.0 cause `Buffer::set_metrics` to panic. We hack around this by 'falling
// through' to call `Buffer::set_rich_text` with zero spans so any cached text will be cleared without
// deallocating the buffer.
metrics.font_size = metrics.font_size.max(0.000001);
metrics.line_height = metrics.line_height.max(0.000001);
// Map text sections to cosmic-text spans, and ignore sections with negative or zero fontsizes,
// since they cannot be rendered by cosmic-text.
//
// The section index is stored in the metadata of the spans, and could be used
// to look up the section the span came from and is not used internally
// in cosmic-text.
let spans_iter = spans
.iter()
.map(|(span_index, span, text_font, font_info, color)| {
(
*span,
get_attrs(*span_index, text_font, *color, font_info, scale_factor),
)
});
// Update the buffer.
let buffer = &mut computed.buffer;
buffer.set_metrics_and_size(font_system, metrics, bounds.width, bounds.height);
buffer.set_wrap(
font_system,
match linebreak {
LineBreak::WordBoundary => Wrap::Word,
LineBreak::AnyCharacter => Wrap::Glyph,
LineBreak::WordOrCharacter => Wrap::WordOrGlyph,
LineBreak::NoWrap => Wrap::None,
},
);
buffer.set_rich_text(
font_system,
spans_iter,
Attrs::new(),
Shaping::Advanced,
Some(justify.into()),
);
buffer.shape_until_scroll(font_system, false);
// Workaround for alignment not working for unbounded text.
// See https://github.com/pop-os/cosmic-text/issues/343
if bounds.width.is_none() && justify != JustifyText::Left {
let dimensions = buffer_dimensions(buffer);
// `set_size` causes a re-layout to occur.
buffer.set_size(font_system, Some(dimensions.x), bounds.height);
}
// Recover the spans buffer.
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) { unreachable!() })
.collect();
Ok(())
}
/// Queues text for rendering
///
/// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s
/// which contain information for rendering the text.
pub fn queue_text<'a>(
&mut self,
layout_info: &mut TextLayoutInfo,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
scale_factor: f64,
layout: &TextLayout,
bounds: TextBounds,
font_atlas_sets: &mut FontAtlasSets,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
y_axis_orientation: YAxisOrientation,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
swash_cache: &mut SwashCache,
) -> Result<(), TextError> {
layout_info.glyphs.clear();
layout_info.size = Default::default();
// Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries.
computed.needs_rerender = false;
// Extract font ids from the iterator while traversing it.
let mut glyph_info = core::mem::take(&mut self.glyph_info);
glyph_info.clear();
let text_spans = text_spans.inspect(|(_, _, _, text_font, _)| {
glyph_info.push((text_font.font.id(), text_font.font_smoothing));
});
let update_result = self.update_buffer(
fonts,
text_spans,
layout.linebreak,
layout.justify,
bounds,
scale_factor,
computed,
font_system,
);
if let Err(err) = update_result {
self.glyph_info = glyph_info;
return Err(err);
}
let buffer = &mut computed.buffer;
let box_size = buffer_dimensions(buffer);
let result = buffer.layout_runs().try_for_each(|run| {
let result = run
.glyphs
.iter()
.map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i))
.try_for_each(|(layout_glyph, line_y, line_i)| {
let mut temp_glyph;
let span_index = layout_glyph.metadata;
let font_id = glyph_info[span_index].0;
let font_smoothing = glyph_info[span_index].1;
let layout_glyph = if 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();
temp_glyph.x = temp_glyph.x.round();
temp_glyph.y = temp_glyph.y.round();
temp_glyph.w = temp_glyph.w.round();
temp_glyph.x_offset = temp_glyph.x_offset.round();
temp_glyph.y_offset = temp_glyph.y_offset.round();
temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round);
&temp_glyph
} else {
layout_glyph
};
let font_atlas_set = font_atlas_sets.sets.entry(font_id).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)
.map(Ok)
.unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(
texture_atlases,
textures,
&mut font_system.0,
&mut swash_cache.0,
layout_glyph,
font_smoothing,
)
})?;
let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
let location = atlas_info.location;
let glyph_rect = texture_atlas.textures[location.glyph_index];
let left = location.offset.x as f32;
let top = location.offset.y as f32;
let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height());
// offset by half the size because the origin is center
let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32;
let y =
line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0;
let y = match y_axis_orientation {
YAxisOrientation::TopToBottom => y,
YAxisOrientation::BottomToTop => box_size.y - y,
};
let position = Vec2::new(x, y);
let pos_glyph = PositionedGlyph {
position,
size: glyph_size.as_vec2(),
atlas_info,
span_index,
byte_index: layout_glyph.start,
byte_length: layout_glyph.end - layout_glyph.start,
line_index: line_i,
};
layout_info.glyphs.push(pos_glyph);
Ok(())
});
result
});
// Return the scratch vec.
self.glyph_info = glyph_info;
// Check result.
result?;
layout_info.size = box_size;
Ok(())
}
/// Queues text for measurement
///
/// Produces a [`TextMeasureInfo`] which can be used by a layout system
/// to measure the text area on demand.
pub fn create_text_measure<'a>(
&mut self,
entity: Entity,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
scale_factor: f64,
layout: &TextLayout,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
) -> Result<TextMeasureInfo, TextError> {
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,
layout.linebreak,
layout.justify,
MIN_WIDTH_CONTENT_BOUNDS,
scale_factor,
computed,
font_system,
)?;
let buffer = &mut computed.buffer;
let min_width_content_size = buffer_dimensions(buffer);
let max_width_content_size = {
let font_system = &mut font_system.0;
buffer.set_size(font_system, None, None);
buffer_dimensions(buffer)
};
Ok(TextMeasureInfo {
min: min_width_content_size,
max: max_width_content_size,
entity,
})
}
/// Returns the [`cosmic_text::fontdb::ID`] for a given [`Font`] asset.
pub fn get_font_id(&self, asset_id: AssetId<Font>) -> Option<cosmic_text::fontdb::ID> {
self.map_handle_to_font_id
.get(&asset_id)
.cloned()
.map(|(id, _)| id)
}
}
/// Render information for a corresponding text block.
///
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`] when an entity has
/// [`TextLayout`] and [`ComputedTextBlock`] components.
#[derive(Component, Clone, Default, Debug, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextLayoutInfo {
/// Scaled and positioned glyphs in screenspace
pub glyphs: Vec<PositionedGlyph>,
/// The glyphs resulting size
pub size: Vec2,
}
/// Size information for a corresponding [`ComputedTextBlock`] component.
///
/// Generated via [`TextPipeline::create_text_measure`].
#[derive(Debug)]
pub struct TextMeasureInfo {
/// Minimum size for a text area in pixels, to be used when laying out widgets with taffy
pub min: Vec2,
/// Maximum size for a text area in pixels, to be used when laying out widgets with taffy
pub max: Vec2,
/// The entity that is measured.
pub entity: Entity,
}
impl TextMeasureInfo {
/// Computes the size of the text area within the provided bounds.
pub fn compute_size(
&mut self,
bounds: TextBounds,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
) -> Vec2 {
// Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed'
// whenever a canonical state is required.
computed
.buffer
.set_size(&mut font_system.0, bounds.width, bounds.height);
buffer_dimensions(&computed.buffer)
}
}
fn load_font_to_fontdb(
text_font: &TextFont,
font_system: &mut cosmic_text::FontSystem,
map_handle_to_font_id: &mut HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
fonts: &Assets<Font>,
) -> FontFaceInfo {
let font_handle = text_font.font.clone();
let (face_id, family_name) = map_handle_to_font_id
.entry(font_handle.id())
.or_insert_with(|| {
let font = fonts.get(font_handle.id()).expect(
"Tried getting a font that was not available, probably due to not being loaded yet",
);
let data = Arc::clone(&font.data);
let ids = font_system
.db_mut()
.load_font_source(cosmic_text::fontdb::Source::Binary(data));
// TODO: it is assumed this is the right font face
let face_id = *ids.last().unwrap();
let face = font_system.db().face(face_id).unwrap();
let family_name = Arc::from(face.families[0].0.as_str());
(face_id, family_name)
});
let face = font_system.db().face(*face_id).unwrap();
FontFaceInfo {
stretch: face.stretch,
style: face.style,
weight: face.weight,
family_name: family_name.clone(),
}
}
/// Translates [`TextFont`] to [`Attrs`].
fn get_attrs<'a>(
span_index: usize,
text_font: &TextFont,
color: Color,
face_info: &'a FontFaceInfo,
scale_factor: f64,
) -> Attrs<'a> {
let attrs = Attrs::new()
.metadata(span_index)
.family(Family::Name(&face_info.family_name))
.stretch(face_info.stretch)
.style(face_info.style)
.weight(face_info.weight)
.metrics(
Metrics {
font_size: text_font.font_size,
line_height: text_font.line_height.eval(text_font.font_size),
}
.scale(scale_factor as f32),
)
.color(cosmic_text::Color(color.to_linear().as_u32()));
attrs
}
/// Calculate the size of the text area for the given buffer.
fn buffer_dimensions(buffer: &Buffer) -> Vec2 {
let (width, height) = buffer
.layout_runs()
.map(|run| (run.line_w, run.line_height))
.reduce(|(w1, h1), (w2, h2)| (w1.max(w2), h1 + h2))
.unwrap_or((0.0, 0.0));
Vec2::new(width, height).ceil()
}
/// Discards stale data cached in `FontSystem`.
pub(crate) fn trim_cosmic_cache(mut font_system: ResMut<CosmicFontSystem>) {
// A trim age of 2 was found to reduce frame time variance vs age of 1 when tested with dynamic text.
// See https://github.com/bevyengine/bevy/pull/15037
//
// We assume only text updated frequently benefits from the shape cache (e.g. animated text, or
// text that is dynamically measured for UI).
font_system.0.shape_run_cache.trim(2);
}

561
vendor/bevy_text/src/text.rs vendored Normal file
View File

@@ -0,0 +1,561 @@
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<Font> = 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<String>) -> 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<String> 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<JustifyText> 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<Font>,
/// 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<Font>) -> 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<Font>) -> 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<T: Into<Color>> From<T> 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 cant 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<Root: Component>(
changed_roots: Query<
Entity,
(
Or<(
Changed<Root>,
Changed<TextFont>,
Changed<TextLayout>,
Changed<Children>,
)>,
With<Root>,
With<TextFont>,
With<TextLayout>,
),
>,
changed_spans: Query<
(Entity, Option<&ChildOf>, Has<TextLayout>),
(
Or<(
Changed<TextSpan>,
Changed<TextFont>,
Changed<Children>,
Changed<ChildOf>, // Included to detect broken text block hierarchies.
Added<TextLayout>,
)>,
With<TextSpan>,
With<TextFont>,
),
>,
mut computed: Query<(
Option<&ChildOf>,
Option<&mut ComputedTextBlock>,
Has<TextSpan>,
)>,
) {
// 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::<Root>()));
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::<Root>()));
}
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::<Root>()
));
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::<Root>()
));
break;
};
parent = next_child_of.parent();
}
}
}

502
vendor/bevy_text/src/text2d.rs vendored Normal file
View File

@@ -0,0 +1,502 @@
use crate::pipeline::CosmicFontSystem;
use crate::{
ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBounds,
TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot,
TextSpanAccess, TextWriter, YAxisOrientation,
};
use bevy_asset::Assets;
use bevy_color::LinearRgba;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::entity::EntityHashSet;
use bevy_ecs::{
change_detection::{DetectChanges, Ref},
component::Component,
entity::Entity,
prelude::{ReflectComponent, With},
query::{Changed, Without},
system::{Commands, Local, Query, Res, ResMut},
};
use bevy_image::prelude::*;
use bevy_math::Vec2;
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_render::view::{self, Visibility, VisibilityClass};
use bevy_render::{
primitives::Aabb,
view::{NoFrustumCulling, ViewVisibility},
Extract,
};
use bevy_sprite::{
Anchor, ExtractedSlice, ExtractedSlices, ExtractedSprite, ExtractedSprites, Sprite,
};
use bevy_transform::components::Transform;
use bevy_transform::prelude::GlobalTransform;
use bevy_window::{PrimaryWindow, Window};
/// 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 [`ComputedTextBlock`]. See [`TextSpan`](crate::TextSpan) for the component used by children of entities with [`Text2d`].
///
/// With `Text2d` the `justify` field of [`TextLayout`] 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::World;
/// # use bevy_text::{Font, JustifyText, Text2d, TextLayout, TextFont, TextColor, TextSpan};
/// #
/// # let font_handle: Handle<Font> = 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!"),
/// TextFont {
/// font: font_handle.clone().into(),
/// font_size: 60.0,
/// ..Default::default()
/// },
/// TextColor(BLUE.into()),
/// ));
///
/// // With text justification.
/// world.spawn((
/// Text2d::new("hello world\nand bevy!"),
/// TextLayout::new_with_justify(JustifyText::Center)
/// ));
///
/// // With spans
/// world.spawn(Text2d::new("hello ")).with_children(|parent| {
/// parent.spawn(TextSpan::new("world"));
/// parent.spawn((TextSpan::new("!"), TextColor(BLUE.into())));
/// });
/// ```
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(
TextLayout,
TextFont,
TextColor,
TextBounds,
Anchor,
Visibility,
VisibilityClass,
Transform
)]
#[component(on_add = view::add_visibility_class::<Sprite>)]
pub struct Text2d(pub String);
impl Text2d {
/// Makes a new 2d text component.
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
impl TextRoot for Text2d {}
impl TextSpanAccess for Text2d {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}
impl From<&str> for Text2d {
fn from(value: &str) -> Self {
Self(String::from(value))
}
}
impl From<String> for Text2d {
fn from(value: String) -> Self {
Self(value)
}
}
/// 2d alias for [`TextReader`].
pub type Text2dReader<'w, 's> = TextReader<'w, 's, Text2d>;
/// 2d alias for [`TextWriter`].
pub type Text2dWriter<'w, 's> = TextWriter<'w, 's, Text2d>;
/// This system extracts the sprites from the 2D text components and adds them to the
/// "render world".
pub fn extract_text2d_sprite(
mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>,
mut extracted_slices: ResMut<ExtractedSlices>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
text2d_query: Extract<
Query<(
Entity,
&ViewVisibility,
&ComputedTextBlock,
&TextLayoutInfo,
&TextBounds,
&Anchor,
&GlobalTransform,
)>,
>,
text_colors: Extract<Query<&TextColor>>,
) {
let mut start = extracted_slices.slices.len();
let mut end = start + 1;
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let scale_factor = windows
.single()
.map(|window| window.resolution.scale_factor())
.unwrap_or(1.0);
let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.));
for (
main_entity,
view_visibility,
computed_block,
text_layout_info,
text_bounds,
anchor,
global_transform,
) in text2d_query.iter()
{
if !view_visibility.get() {
continue;
}
let size = Vec2::new(
text_bounds.width.unwrap_or(text_layout_info.size.x),
text_bounds.height.unwrap_or(text_layout_info.size.y),
);
let bottom_left =
-(anchor.as_vec() + 0.5) * size + (size.y - text_layout_info.size.y) * Vec2::Y;
let transform =
*global_transform * GlobalTransform::from_translation(bottom_left.extend(0.)) * scaling;
let mut color = LinearRgba::WHITE;
let mut current_span = usize::MAX;
for (
i,
PositionedGlyph {
position,
atlas_info,
span_index,
..
},
) in text_layout_info.glyphs.iter().enumerate()
{
if *span_index != current_span {
color = text_colors
.get(
computed_block
.entities()
.get(*span_index)
.map(|t| t.entity)
.unwrap_or(Entity::PLACEHOLDER),
)
.map(|text_color| LinearRgba::from(text_color.0))
.unwrap_or_default();
current_span = *span_index;
}
let rect = texture_atlases
.get(&atlas_info.texture_atlas)
.unwrap()
.textures[atlas_info.location.glyph_index]
.as_rect();
extracted_slices.slices.push(ExtractedSlice {
offset: *position,
rect,
size: rect.size(),
});
if text_layout_info.glyphs.get(i + 1).is_none_or(|info| {
info.span_index != current_span || info.atlas_info.texture != atlas_info.texture
}) {
let render_entity = commands.spawn(TemporaryRenderEntity).id();
extracted_sprites.sprites.push(ExtractedSprite {
main_entity,
render_entity,
transform,
color,
image_handle_id: atlas_info.texture.id(),
flip_x: false,
flip_y: false,
kind: bevy_sprite::ExtractedSpriteKind::Slices {
indices: start..end,
},
});
start = end;
}
end += 1;
}
}
}
/// Updates the layout and size information whenever the text or style is changed.
/// This information is computed by the [`TextPipeline`] on insertion, then stored.
///
/// ## World Resources
///
/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
/// It does not modify or observe existing ones.
pub fn update_text2d_layout(
mut last_scale_factor: Local<Option<f32>>,
// Text items which should be reprocessed again, generally when the font hasn't loaded yet.
mut queue: Local<EntityHashSet>,
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_sets: ResMut<FontAtlasSets>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Entity,
Ref<TextLayout>,
Ref<TextBounds>,
&mut TextLayoutInfo,
&mut ComputedTextBlock,
)>,
mut text_reader: Text2dReader,
mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>,
) {
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let scale_factor = windows
.single()
.ok()
.map(|window| window.resolution.scale_factor())
.or(*last_scale_factor)
.unwrap_or(1.);
let inverse_scale_factor = scale_factor.recip();
let factor_changed = *last_scale_factor != Some(scale_factor);
*last_scale_factor = Some(scale_factor);
for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query {
if factor_changed
|| computed.needs_rerender()
|| bounds.is_changed()
|| (!queue.is_empty() && queue.remove(&entity))
{
let text_bounds = TextBounds {
width: if block.linebreak == LineBreak::NoWrap {
None
} else {
bounds.width.map(|width| scale_value(width, scale_factor))
},
height: bounds
.height
.map(|height| scale_value(height, scale_factor)),
};
let text_layout_info = text_layout_info.into_inner();
match text_pipeline.queue_text(
text_layout_info,
&fonts,
text_reader.iter(entity),
scale_factor.into(),
&block,
text_bounds,
&mut font_atlas_sets,
&mut texture_atlases,
&mut textures,
YAxisOrientation::BottomToTop,
computed.as_mut(),
&mut font_system,
&mut swash_cache,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, let's add this entity to the
// queue for further processing
queue.insert(entity);
}
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);
}
}
}
}
}
/// Scales `value` by `factor`.
pub fn scale_value(value: f32, factor: f32) -> f32 {
value * factor
}
/// System calculating and inserting an [`Aabb`] component to entities with some
/// [`TextLayoutInfo`] and [`Anchor`] components, and without a [`NoFrustumCulling`] component.
///
/// Used in system set [`VisibilitySystems::CalculateBounds`](bevy_render::view::VisibilitySystems::CalculateBounds).
pub fn calculate_bounds_text2d(
mut commands: Commands,
mut text_to_update_aabb: Query<
(
Entity,
&TextLayoutInfo,
&Anchor,
&TextBounds,
Option<&mut Aabb>,
),
(Changed<TextLayoutInfo>, Without<NoFrustumCulling>),
>,
) {
for (entity, layout_info, anchor, text_bounds, aabb) in &mut text_to_update_aabb {
let size = Vec2::new(
text_bounds.width.unwrap_or(layout_info.size.x),
text_bounds.height.unwrap_or(layout_info.size.y),
);
let center = (-anchor.as_vec() * size + (size.y - layout_info.size.y) * Vec2::Y)
.extend(0.)
.into();
let half_extents = (0.5 * layout_info.size).extend(0.0).into();
if let Some(mut aabb) = aabb {
*aabb = Aabb {
center,
half_extents,
};
} else {
commands.entity(entity).try_insert(Aabb {
center,
half_extents,
});
}
}
}
#[cfg(test)]
mod tests {
use bevy_app::{App, Update};
use bevy_asset::{load_internal_binary_asset, Handle};
use bevy_ecs::schedule::IntoScheduleConfigs;
use crate::{detect_text_needs_rerender, TextIterScratch};
use super::*;
const FIRST_TEXT: &str = "Sample text.";
const SECOND_TEXT: &str = "Another, longer sample text.";
fn setup() -> (App, Entity) {
let mut app = App::new();
app.init_resource::<Assets<Font>>()
.init_resource::<Assets<Image>>()
.init_resource::<Assets<TextureAtlasLayout>>()
.init_resource::<FontAtlasSets>()
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.init_resource::<SwashCache>()
.init_resource::<TextIterScratch>()
.add_systems(
Update,
(
detect_text_needs_rerender::<Text2d>,
update_text2d_layout,
calculate_bounds_text2d,
)
.chain(),
);
// A font is needed to ensure the text is laid out with an actual size.
load_internal_binary_asset!(
app,
Handle::default(),
"FiraMono-subset.ttf",
|bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
);
let entity = app.world_mut().spawn(Text2d::new(FIRST_TEXT)).id();
(app, entity)
}
#[test]
fn calculate_bounds_text2d_create_aabb() {
let (mut app, entity) = setup();
assert!(!app
.world()
.get_entity(entity)
.expect("Could not find entity")
.contains::<Aabb>());
// Creates the AABB after text layouting.
app.update();
let aabb = app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Text should have an AABB");
// Text2D AABB does not have a depth.
assert_eq!(aabb.center.z, 0.0);
assert_eq!(aabb.half_extents.z, 0.0);
// AABB has an actual size.
assert!(aabb.half_extents.x > 0.0 && aabb.half_extents.y > 0.0);
}
#[test]
fn calculate_bounds_text2d_update_aabb() {
let (mut app, entity) = setup();
// Creates the initial AABB after text layouting.
app.update();
let first_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find initial AABB");
let mut entity_ref = app
.world_mut()
.get_entity_mut(entity)
.expect("Could not find entity");
*entity_ref
.get_mut::<Text2d>()
.expect("Missing Text2d on entity") = Text2d::new(SECOND_TEXT);
// Recomputes the AABB.
app.update();
let second_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find second AABB");
// Check that the height is the same, but the width is greater.
approx::assert_abs_diff_eq!(first_aabb.half_extents.y, second_aabb.half_extents.y);
assert!(FIRST_TEXT.len() < SECOND_TEXT.len());
assert!(first_aabb.half_extents.x < second_aabb.half_extents.x);
}
}

467
vendor/bevy_text/src/text_access.rs vendored Normal file
View File

@@ -0,0 +1,467 @@
use bevy_color::Color;
use bevy_ecs::{
component::Mutable,
prelude::*,
system::{Query, SystemParam},
};
use crate::{TextColor, TextFont, TextSpan};
/// Helper trait for using the [`TextReader`] and [`TextWriter`] system params.
pub trait TextSpanAccess: Component<Mutability = Mutable> {
/// Gets the text span's string.
fn read_span(&self) -> &str;
/// Gets mutable reference to the text span's string.
fn write_span(&mut self) -> &mut String;
}
/// Helper trait for the root text component in a text block.
pub trait TextRoot: TextSpanAccess + From<String> {}
/// Helper trait for the text span components in a text block.
pub trait TextSpanComponent: TextSpanAccess + From<String> {}
#[derive(Resource, Default)]
pub(crate) struct TextIterScratch {
stack: Vec<(&'static Children, usize)>,
}
impl TextIterScratch {
fn take<'a>(&mut self) -> Vec<(&'a Children, usize)> {
core::mem::take(&mut self.stack)
.into_iter()
.map(|_| -> (&Children, usize) { unreachable!() })
.collect()
}
fn recover(&mut self, mut stack: Vec<(&Children, usize)>) {
stack.clear();
self.stack = stack
.into_iter()
.map(|_| -> (&'static Children, usize) { unreachable!() })
.collect();
}
}
/// System parameter for reading text spans in a text block.
///
/// `R` is the root text component.
#[derive(SystemParam)]
pub struct TextReader<'w, 's, R: TextRoot> {
// This is a local to avoid system ambiguities when TextReaders run in parallel.
scratch: Local<'s, TextIterScratch>,
roots: Query<
'w,
's,
(
&'static R,
&'static TextFont,
&'static TextColor,
Option<&'static Children>,
),
>,
spans: Query<
'w,
's,
(
&'static TextSpan,
&'static TextFont,
&'static TextColor,
Option<&'static Children>,
),
>,
}
impl<'w, 's, R: TextRoot> TextReader<'w, 's, R> {
/// Returns an iterator over text spans in a text block, starting with the root entity.
pub fn iter(&mut self, root_entity: Entity) -> TextSpanIter<R> {
let stack = self.scratch.take();
TextSpanIter {
scratch: &mut self.scratch,
root_entity: Some(root_entity),
stack,
roots: &self.roots,
spans: &self.spans,
}
}
/// Gets a text span within a text block at a specific index in the flattened span list.
pub fn get(
&mut self,
root_entity: Entity,
index: usize,
) -> Option<(Entity, usize, &str, &TextFont, Color)> {
self.iter(root_entity).nth(index)
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
pub fn get_text(&mut self, root_entity: Entity, index: usize) -> Option<&str> {
self.get(root_entity, index).map(|(_, _, text, _, _)| text)
}
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
pub fn get_font(&mut self, root_entity: Entity, index: usize) -> Option<&TextFont> {
self.get(root_entity, index).map(|(_, _, _, font, _)| font)
}
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
pub fn get_color(&mut self, root_entity: Entity, index: usize) -> Option<Color> {
self.get(root_entity, index)
.map(|(_, _, _, _, color)| color)
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn text(&mut self, root_entity: Entity, index: usize) -> &str {
self.get_text(root_entity, index).unwrap()
}
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn font(&mut self, root_entity: Entity, index: usize) -> &TextFont {
self.get_font(root_entity, index).unwrap()
}
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn color(&mut self, root_entity: Entity, index: usize) -> Color {
self.get_color(root_entity, index).unwrap()
}
}
/// Iterator returned by [`TextReader::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<'a, R: TextRoot> {
scratch: &'a mut TextIterScratch,
root_entity: Option<Entity>,
/// Stack of (children, next index into children).
stack: Vec<(&'a Children, usize)>,
roots: &'a Query<
'a,
'a,
(
&'static R,
&'static TextFont,
&'static TextColor,
Option<&'static Children>,
),
>,
spans: &'a Query<
'a,
'a,
(
&'static TextSpan,
&'static TextFont,
&'static TextColor,
Option<&'static Children>,
),
>,
}
impl<'a, R: TextRoot> Iterator for TextSpanIter<'a, R> {
/// Item = (entity in text block, hierarchy depth in the block, span text, span style).
type Item = (Entity, usize, &'a str, &'a TextFont, Color);
fn next(&mut self) -> Option<Self::Item> {
// Root
if let Some(root_entity) = self.root_entity.take() {
if let Ok((text, text_font, color, 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(), text_font, color.0));
}
return None;
}
// Span
loop {
let (children, idx) = self.stack.last_mut()?;
loop {
let Some(child) = children.get(*idx) else {
break;
};
// Increment to prep the next entity in this stack level.
*idx += 1;
let entity = *child;
let Ok((span, text_font, color, maybe_children)) = self.spans.get(entity) 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(), text_font, color.0));
}
// All children at this stack entry have been iterated.
self.stack.pop();
}
}
}
impl<'a, R: TextRoot> Drop for TextSpanIter<'a, R> {
fn drop(&mut self) {
// Return the internal stack.
let stack = core::mem::take(&mut self.stack);
self.scratch.recover(stack);
}
}
/// System parameter for reading and writing text spans in a text block.
///
/// `R` is the root text component, and `S` is the text span component on children.
#[derive(SystemParam)]
pub struct TextWriter<'w, 's, R: TextRoot> {
// This is a resource because two TextWriters can't run in parallel.
scratch: ResMut<'w, TextIterScratch>,
roots: Query<
'w,
's,
(
&'static mut R,
&'static mut TextFont,
&'static mut TextColor,
),
Without<TextSpan>,
>,
spans: Query<
'w,
's,
(
&'static mut TextSpan,
&'static mut TextFont,
&'static mut TextColor,
),
Without<R>,
>,
children: Query<'w, 's, &'static Children>,
}
impl<'w, 's, R: TextRoot> TextWriter<'w, 's, R> {
/// Gets a mutable reference to a text span within a text block at a specific index in the flattened span list.
pub fn get(
&mut self,
root_entity: Entity,
index: usize,
) -> Option<(Entity, usize, Mut<String>, Mut<TextFont>, Mut<TextColor>)> {
// Root
if index == 0 {
let (text, font, color) = self.roots.get_mut(root_entity).ok()?;
return Some((
root_entity,
0,
text.map_unchanged(|t| t.write_span()),
font,
color,
));
}
// Prep stack.
let mut stack: Vec<(&Children, usize)> = self.scratch.take();
if let Ok(children) = self.children.get(root_entity) {
stack.push((children, 0));
}
// Span
let mut count = 1;
let (depth, entity) = 'l: loop {
let Some((children, idx)) = stack.last_mut() else {
self.scratch.recover(stack);
return None;
};
loop {
let Some(child) = children.get(*idx) else {
// All children at this stack entry have been iterated.
stack.pop();
break;
};
// Increment to prep the next entity in this stack level.
*idx += 1;
if !self.spans.contains(*child) {
continue;
};
count += 1;
if count - 1 == index {
let depth = stack.len();
self.scratch.recover(stack);
break 'l (depth, *child);
}
if let Ok(children) = self.children.get(*child) {
stack.push((children, 0));
break;
}
}
};
// Note: We do this outside the loop due to borrow checker limitations.
let (text, font, color) = self.spans.get_mut(entity).unwrap();
Some((
entity,
depth,
text.map_unchanged(|t| t.write_span()),
font,
color,
))
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
pub fn get_text(&mut self, root_entity: Entity, index: usize) -> Option<Mut<String>> {
self.get(root_entity, index).map(|(_, _, text, ..)| text)
}
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
pub fn get_font(&mut self, root_entity: Entity, index: usize) -> Option<Mut<TextFont>> {
self.get(root_entity, index).map(|(_, _, _, font, _)| font)
}
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
pub fn get_color(&mut self, root_entity: Entity, index: usize) -> Option<Mut<TextColor>> {
self.get(root_entity, index)
.map(|(_, _, _, _, color)| color)
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn text(&mut self, root_entity: Entity, index: usize) -> Mut<String> {
self.get_text(root_entity, index).unwrap()
}
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn font(&mut self, root_entity: Entity, index: usize) -> Mut<TextFont> {
self.get_font(root_entity, index).unwrap()
}
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn color(&mut self, root_entity: Entity, index: usize) -> Mut<TextColor> {
self.get_color(root_entity, index).unwrap()
}
/// Invokes a callback on each span in a text block, starting with the root entity.
pub fn for_each(
&mut self,
root_entity: Entity,
mut callback: impl FnMut(Entity, usize, Mut<String>, Mut<TextFont>, Mut<TextColor>),
) {
self.for_each_until(root_entity, |a, b, c, d, e| {
(callback)(a, b, c, d, e);
true
});
}
/// Invokes a callback on each span's string value in a text block, starting with the root entity.
pub fn for_each_text(&mut self, root_entity: Entity, mut callback: impl FnMut(Mut<String>)) {
self.for_each(root_entity, |_, _, text, _, _| {
(callback)(text);
});
}
/// Invokes a callback on each span's [`TextFont`] in a text block, starting with the root entity.
pub fn for_each_font(&mut self, root_entity: Entity, mut callback: impl FnMut(Mut<TextFont>)) {
self.for_each(root_entity, |_, _, _, font, _| {
(callback)(font);
});
}
/// Invokes a callback on each span's [`TextColor`] in a text block, starting with the root entity.
pub fn for_each_color(
&mut self,
root_entity: Entity,
mut callback: impl FnMut(Mut<TextColor>),
) {
self.for_each(root_entity, |_, _, _, _, color| {
(callback)(color);
});
}
/// Invokes a callback on each span in a text block, starting with the root entity.
///
/// Traversal will stop when the callback returns `false`.
// TODO: find a way to consolidate get and for_each_until, or provide a real iterator. Lifetime issues are challenging here.
pub fn for_each_until(
&mut self,
root_entity: Entity,
mut callback: impl FnMut(Entity, usize, Mut<String>, Mut<TextFont>, Mut<TextColor>) -> bool,
) {
// Root
let Ok((text, font, color)) = self.roots.get_mut(root_entity) else {
return;
};
if !(callback)(
root_entity,
0,
text.map_unchanged(|t| t.write_span()),
font,
color,
) {
return;
}
// Prep stack.
let mut stack: Vec<(&Children, usize)> = self.scratch.take();
if let Ok(children) = self.children.get(root_entity) {
stack.push((children, 0));
}
// Span
loop {
let depth = stack.len();
let Some((children, idx)) = stack.last_mut() else {
self.scratch.recover(stack);
return;
};
loop {
let Some(child) = children.get(*idx) else {
// All children at this stack entry have been iterated.
stack.pop();
break;
};
// Increment to prep the next entity in this stack level.
*idx += 1;
let entity = *child;
let Ok((text, font, color)) = self.spans.get_mut(entity) else {
continue;
};
if !(callback)(
entity,
depth,
text.map_unchanged(|t| t.write_span()),
font,
color,
) {
self.scratch.recover(stack);
return;
}
if let Ok(children) = self.children.get(entity) {
stack.push((children, 0));
break;
}
}
}
}
}