Cosmic text (#10193)

# Replace ab_glyph with the more capable cosmic-text

Fixes #7616.

Cosmic-text is a more mature text-rendering library that handles scripts
and ligatures better than ab_glyph, it can also handle system fonts
which can be implemented in bevy in the future

Rebase of https://github.com/bevyengine/bevy/pull/8808

## Changelog

Replaces text renderer ab_glyph with cosmic-text

The definition of the font size has changed with the migration to cosmic
text. The behavior is now consistent with other platforms (e.g. the
web), where the font size in pixels measures the height of the font (the
distance between the top of the highest ascender and the bottom of the
lowest descender). Font sizes in your app need to be rescaled to
approximately 1.2x smaller; for example, if you were using a font size
of 60.0, you should now use a font size of 50.0.

## Migration guide

- `Text2dBounds` has been replaced with `TextBounds`, and it now accepts
`Option`s to the bounds, instead of using `f32::INFINITY` to inidicate
lack of bounds
- Textsizes should be changed, dividing the current size with 1.2 will
result in the same size as before.
- `TextSettings` struct is removed
- Feature `subpixel_alignment` has been removed since cosmic-text
already does this automatically
- TextBundles and things rendering texts requires the `CosmicBuffer`
Component on them as well

## Suggested followups:

- TextPipeline: reconstruct byte indices for keeping track of eventual
cursors in text input
- TextPipeline: (future work) split text entities into section entities
- TextPipeline: (future work) text editing
- Support line height as an option. Unitless `1.2` is the default used
in browsers (1.2x font size).
- Support System Fonts and font families
- Example showing of animated text styles. Eg. throbbing hyperlinks

---------

Co-authored-by: tigregalis <anak.harimau@gmail.com>
Co-authored-by: Nico Burns <nico@nicoburns.com>
Co-authored-by: sam edelsten <samedelsten1@gmail.com>
Co-authored-by: Dimchikkk <velo.app1@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Rob Parrett <robparrett@gmail.com>
This commit is contained in:
TotalKrill 2024-07-04 22:41:08 +02:00 committed by GitHub
parent 1c2f687202
commit 5986d5d309
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1084 additions and 856 deletions

View file

@ -271,9 +271,6 @@ wayland = ["bevy_internal/wayland"]
# X11 display server support
x11 = ["bevy_internal/x11"]
# Enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_internal/subpixel_glyph_atlas"]
# Enable systems that allow for automated testing on CI
bevy_ci_testing = ["bevy_internal/bevy_ci_testing"]

View file

@ -96,9 +96,6 @@ async-io = ["bevy_tasks/async-io"]
wayland = ["bevy_winit/wayland"]
x11 = ["bevy_winit/x11"]
# enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]
# Transmission textures in `StandardMaterial`:
pbr_transmission_textures = [
"bevy_pbr?/pbr_transmission_textures",

View file

@ -375,14 +375,15 @@ pub fn extract_sprites(
.map(|e| (commands.spawn_empty().id(), e)),
);
} else {
let atlas_rect = sheet.and_then(|s| s.texture_rect(&texture_atlases));
let atlas_rect =
sheet.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));
let rect = match (atlas_rect, sprite.rect) {
(None, None) => None,
(None, Some(sprite_rect)) => Some(sprite_rect),
(Some(atlas_rect), None) => Some(atlas_rect.as_rect()),
(Some(atlas_rect), None) => Some(atlas_rect),
(Some(atlas_rect), Some(mut sprite_rect)) => {
sprite_rect.min += atlas_rect.min.as_vec2();
sprite_rect.max += atlas_rect.min.as_vec2();
sprite_rect.min += atlas_rect.min;
sprite_rect.max += atlas_rect.min;
Some(sprite_rect)
}

View file

@ -31,26 +31,6 @@ pub struct TextureAtlasLayout {
pub(crate) texture_handles: Option<HashMap<AssetId<Image>, usize>>,
}
/// Component used to draw a specific section of a texture.
///
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
/// image file for either sprite animation or global mapping.
/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
/// for efficient rendering of related game objects.
///
/// Check the following examples for usage:
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Component, Default, Debug, Clone, Reflect)]
pub struct TextureAtlas {
/// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>,
/// Texture atlas section index
pub index: usize,
}
impl TextureAtlasLayout {
/// Create a new empty layout with custom `dimensions`
pub fn new_empty(dimensions: UVec2) -> Self {
@ -149,6 +129,26 @@ impl TextureAtlasLayout {
}
}
/// Component used to draw a specific section of a texture.
///
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
/// image file for either sprite animation or global mapping.
/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
/// for efficient rendering of related game objects.
///
/// Check the following examples for usage:
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Component, Default, Debug, Clone, Reflect)]
pub struct TextureAtlas {
/// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>,
/// Texture atlas section index
pub index: usize,
}
impl TextureAtlas {
/// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index`
pub fn texture_rect(&self, texture_atlases: &Assets<TextureAtlasLayout>) -> Option<URect> {

View file

@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[features]
subpixel_glyph_atlas = []
default_font = []
[dependencies]
@ -17,6 +16,7 @@ default_font = []
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
@ -29,10 +29,11 @@ bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
# other
ab_glyph = "0.2.6"
glyph_brush_layout = "0.2.1"
cosmic-text = "0.12"
thiserror = "1.0"
serde = { version = "1", features = ["derive"] }
unicode-bidi = "0.3.13"
sys-locale = "0.3.0"
[dev-dependencies]
approx = "0.5.1"

View file

@ -0,0 +1,70 @@
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::Vec2;
use bevy_reflect::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)]
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)
}
}

View file

@ -1,10 +1,17 @@
use ab_glyph::GlyphId;
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(GlyphId),
FailedToAddGlyph(u16),
/// Failed to get scaled glyph image for cache key
#[error("failed to get scaled glyph image for cache key: {0:?}")]
FailedToGetGlyphImage(CacheKey),
}

View file

@ -1,53 +1,35 @@
use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph};
use std::sync::Arc;
use bevy_asset::Asset;
use bevy_reflect::TypePath;
use bevy_render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::Image,
};
#[derive(Asset, TypePath, Debug, Clone)]
/// 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 {
pub font: FontArc,
/// Content of a font file as bytes
pub data: Arc<Vec<u8>>,
}
impl Font {
pub fn try_from_bytes(font_data: Vec<u8>) -> Result<Self, InvalidFont> {
let font = FontVec::try_from_vec(font_data)?;
let font = FontArc::new(font);
Ok(Font { font })
}
pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image {
let bounds = outlined_glyph.px_bounds();
// Increase the length of the glyph texture by 2-pixels on each axis to make space
// for a pixel wide transparent border along its edges.
let width = bounds.width() as usize + 2;
let height = bounds.height() as usize + 2;
let mut alpha = vec![0.0; width * height];
outlined_glyph.draw(|x, y, v| {
// Displace the glyph by 1 pixel on each axis so that it is drawn in the center of the texture.
// This leaves a pixel wide transparent border around the glyph.
alpha[(y + 1) as usize * width + x as usize + 1] = v;
});
// TODO: make this texture grayscale
Image::new(
Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
alpha
.iter()
.flat_map(|a| vec![255, 255, 255, (*a * 255.0) as u8])
.collect::<Vec<u8>>(),
TextureFormat::Rgba8UnormSrgb,
// This glyph image never needs to reach the render world because it's placed
// into a font texture atlas that'll be used for rendering.
RenderAssetUsages::MAIN_WORLD,
)
/// 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),
})
}
}

View file

@ -1,6 +1,5 @@
use ab_glyph::{GlyphId, Point};
use bevy_asset::{Assets, Handle};
use bevy_math::UVec2;
use bevy_math::{IVec2, UVec2};
use bevy_render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
@ -9,57 +8,36 @@ use bevy_render::{
use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlasLayout};
use bevy_utils::HashMap;
#[cfg(feature = "subpixel_glyph_atlas")]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct SubpixelOffset {
x: u16,
y: u16,
}
#[cfg(feature = "subpixel_glyph_atlas")]
impl From<Point> for SubpixelOffset {
fn from(p: Point) -> Self {
fn f(v: f32) -> u16 {
((v % 1.) * (u16::MAX as f32)) as u16
}
Self {
x: f(p.x),
y: f(p.y),
}
}
}
#[cfg(not(feature = "subpixel_glyph_atlas"))]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct SubpixelOffset;
#[cfg(not(feature = "subpixel_glyph_atlas"))]
impl From<Point> for SubpixelOffset {
fn from(_: Point) -> Self {
Self
}
}
/// A font glyph placed at a specific sub-pixel offset.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PlacedGlyph {
/// The font glyph ID.
pub glyph_id: GlyphId,
/// The sub-pixel offset of the placed glyph.
pub subpixel_offset: SubpixelOffset,
}
use crate::{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,
pub glyph_to_atlas_index: HashMap<PlacedGlyph, usize>,
/// 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: &mut Assets<TextureAtlasLayout>,
texture_atlases_layout: &mut Assets<TextureAtlasLayout>,
size: UVec2,
) -> FontAtlas {
let texture = textures.add(Image::new_fill(
@ -74,21 +52,23 @@ impl FontAtlas {
// Need to keep this image CPU persistent in order to add additional glyphs later on
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
));
let texture_atlas = TextureAtlasLayout::new_empty(size);
let texture_atlas = texture_atlases_layout.add(TextureAtlasLayout::new_empty(size));
Self {
texture_atlas: texture_atlases.add(texture_atlas),
texture_atlas,
glyph_to_atlas_index: HashMap::default(),
dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 0),
dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1),
texture,
}
}
pub fn get_glyph_index(&self, glyph: &PlacedGlyph) -> Option<usize> {
self.glyph_to_atlas_index.get(glyph).copied()
/// 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()
}
pub fn has_glyph(&self, glyph: &PlacedGlyph) -> bool {
self.glyph_to_atlas_index.contains_key(glyph)
/// 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.
@ -99,31 +79,45 @@ impl FontAtlas {
///
/// # Returns
///
/// Returns `true` if the glyph is successfully added, or `false` otherwise.
/// 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>,
glyph: &PlacedGlyph,
glyph_texture: &Image,
) -> bool {
let Some(atlas_layout) = atlas_layouts.get_mut(&self.texture_atlas) else {
return false;
};
let Some(atlas_texture) = textures.get_mut(&self.texture) else {
return false;
};
if let Some(index) = self.dynamic_texture_atlas_builder.add_texture(
atlas_layout,
glyph_texture,
atlas_texture,
) {
self.glyph_to_atlas_index.insert(*glyph, index);
true
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 Some(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 {
false
Err(TextError::FailedToAddGlyph(cache_key.glyph_id))
}
}
}
impl std::fmt::Debug for FontAtlas {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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()
}
}

View file

@ -1,34 +1,45 @@
use crate::{error::TextError, Font, FontAtlas, PlacedGlyph};
use ab_glyph::{GlyphId, OutlinedGlyph, Point};
use bevy_asset::{AssetEvent, AssetId};
use bevy_asset::{Assets, Handle};
use bevy_ecs::prelude::*;
use bevy_math::{FloatOrd, UVec2};
use bevy_reflect::Reflect;
use bevy_render::texture::Image;
use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
use bevy_ecs::{
event::EventReader,
system::{ResMut, Resource},
};
use bevy_math::{IVec2, UVec2};
use bevy_reflect::TypePath;
use bevy_render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::Image,
};
use bevy_sprite::TextureAtlasLayout;
use bevy_utils::HashMap;
type FontSizeKey = FloatOrd;
use crate::{error::TextError, Font, FontAtlas, GlyphAtlasInfo};
#[derive(Default, Resource)]
/// 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>>,
) {
// Clean up font atlas sets for removed fonts
for event in font_events.read() {
if let AssetEvent::Removed { id } = event {
font_atlas_sets.sets.remove(id);
@ -36,15 +47,37 @@ pub fn remove_dropped_font_atlas_sets(
}
}
pub struct FontAtlasSet {
font_atlases: HashMap<FontSizeKey, Vec<FontAtlas>>,
/// Identifies a font size 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 FontSizeKey(pub u32);
impl From<u32> for FontSizeKey {
fn from(val: u32) -> FontSizeKey {
Self(val)
}
}
#[derive(Debug, Clone, Reflect)]
pub struct GlyphAtlasInfo {
pub texture_atlas: Handle<TextureAtlasLayout>,
pub texture: Handle<Image>,
pub glyph_index: usize,
/// 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 [`Text`](crate::Text),
/// 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 [`Text`](crate::Text),
/// 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<FontSizeKey, Vec<FontAtlas>>,
}
impl Default for FontAtlasSet {
@ -56,46 +89,51 @@ impl Default for FontAtlasSet {
}
impl FontAtlasSet {
/// Returns an iterator over the [`FontAtlas`]es in this set
pub fn iter(&self) -> impl Iterator<Item = (&FontSizeKey, &Vec<FontAtlas>)> {
self.font_atlases.iter()
}
pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool {
/// 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: &FontSizeKey) -> bool {
self.font_atlases
.get(&FloatOrd(font_size))
.get(font_size)
.map_or(false, |font_atlas| {
let placed_glyph = PlacedGlyph {
glyph_id,
subpixel_offset: glyph_position.into(),
};
font_atlas
.iter()
.any(|atlas| atlas.has_glyph(&placed_glyph))
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>,
outlined_glyph: OutlinedGlyph,
font_system: &mut cosmic_text::FontSystem,
swash_cache: &mut cosmic_text::SwashCache,
layout_glyph: &cosmic_text::LayoutGlyph,
) -> Result<GlyphAtlasInfo, TextError> {
let glyph = outlined_glyph.glyph();
let placed_glyph = PlacedGlyph {
glyph_id: glyph.id,
subpixel_offset: glyph.position.into(),
};
let font_size = glyph.scale.y;
let physical_glyph = layout_glyph.physical((0., 0.), 1.0);
let font_atlases = self
.font_atlases
.entry(FloatOrd(font_size))
.entry(physical_glyph.cache_key.font_size_bits.into())
.or_insert_with(|| vec![FontAtlas::new(textures, texture_atlases, UVec2::splat(512))]);
let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph);
let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool {
atlas.add_glyph(textures, texture_atlases, &placed_glyph, &glyph_texture)
let (glyph_texture, offset) =
Self::get_outlined_glyph_texture(font_system, swash_cache, &physical_glyph)?;
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(add_char_to_font_atlas) {
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
@ -109,42 +147,42 @@ impl FontAtlasSet {
texture_atlases,
UVec2::splat(containing),
));
if !font_atlases.last_mut().unwrap().add_glyph(
font_atlases.last_mut().unwrap().add_glyph(
textures,
texture_atlases,
&placed_glyph,
physical_glyph.cache_key,
&glyph_texture,
) {
return Err(TextError::FailedToAddGlyph(placed_glyph.glyph_id));
}
offset,
)?;
}
Ok(self.get_glyph_atlas_info(font_size, &placed_glyph).unwrap())
Ok(self.get_glyph_atlas_info(physical_glyph.cache_key).unwrap())
}
/// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph.
pub fn get_glyph_atlas_info(
&mut self,
font_size: f32,
placed_glyph: &PlacedGlyph,
cache_key: cosmic_text::CacheKey,
) -> Option<GlyphAtlasInfo> {
self.font_atlases
.get(&FloatOrd(font_size))
.get(&FontSizeKey(cache_key.font_size_bits))
.and_then(|font_atlases| {
font_atlases
.iter()
.find_map(|atlas| {
atlas.get_glyph_index(placed_glyph).map(|glyph_index| {
atlas.get_glyph_index(cache_key).map(|location| {
(
glyph_index,
location,
atlas.texture_atlas.clone_weak(),
atlas.texture.clone_weak(),
)
})
})
.map(|(glyph_index, texture_atlas, texture)| GlyphAtlasInfo {
.map(|(location, texture_atlas, texture)| GlyphAtlasInfo {
texture_atlas,
location,
texture,
glyph_index,
})
})
}
@ -153,9 +191,54 @@ impl FontAtlasSet {
pub fn len(&self) -> usize {
self.font_atlases.len()
}
/// Returns `true` if the font atlas set contains no elements
/// Returns the number of font atlases in this set
pub fn is_empty(&self) -> bool {
self.font_atlases.is_empty()
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,
) -> Result<(Image, IVec2), TextError> {
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 => 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),
))
}
}

View file

@ -3,18 +3,19 @@ use bevy_asset::{io::Reader, AssetLoader, LoadContext};
use thiserror::Error;
#[derive(Default)]
/// An [`AssetLoader`] for [`Font`]s, for use by the [`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),
/// An [`InvalidFont`](ab_glyph::InvalidFont) Error
#[error(transparent)]
FontInvalid(#[from] ab_glyph::InvalidFont),
}
impl AssetLoader for FontLoader {
@ -29,7 +30,8 @@ impl AssetLoader for FontLoader {
) -> Result<Font, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
Ok(Font::try_from_bytes(bytes)?)
let font = Font::try_from_bytes(bytes)?;
Ok(font)
}
fn extensions(&self) -> &[&str] {

View file

@ -0,0 +1,79 @@
//! This module exports types related to rendering glyphs.
use bevy_asset::Handle;
use bevy_math::{IVec2, Vec2};
use bevy_reflect::Reflect;
use bevy_render::texture::Image;
use bevy_sprite::TextureAtlasLayout;
/// 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)]
pub struct PositionedGlyph {
/// The position of the glyph in the [`Text`](crate::Text)'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 [`Text`](crate::Text)'s sections.
pub section_index: usize,
/// TODO: In order to do text editing, we need access to the size of glyphs and their index in the associated String.
/// For example, to figure out where to place the cursor in an input box from the mouse's position.
/// Without this, it's only possible in texts where each glyph is one byte. Cosmic text has methods for this
/// cosmic-texts [hit detection](https://pop-os.github.io/cosmic-text/cosmic_text/struct.Buffer.html#method.hit)
byte_index: usize,
}
impl PositionedGlyph {
/// Creates a new [`PositionedGlyph`]
pub fn new(
position: Vec2,
size: Vec2,
atlas_info: GlyphAtlasInfo,
section_index: usize,
) -> Self {
Self {
position,
size,
atlas_info,
section_index,
byte_index: 0,
}
}
}
/// 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)]
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`].
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`].
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)]
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,
}

View file

@ -1,237 +0,0 @@
use ab_glyph::{Font as _, FontArc, Glyph, PxScaleFont, ScaleFont as _};
use bevy_asset::{AssetId, Assets};
use bevy_math::{Rect, Vec2};
use bevy_reflect::Reflect;
use bevy_render::texture::Image;
use bevy_sprite::TextureAtlasLayout;
use bevy_utils::warn_once;
use glyph_brush_layout::{
BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph,
SectionText, ToSectionText,
};
use crate::{
error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasSets, GlyphAtlasInfo, JustifyText,
PlacedGlyph, TextSettings, YAxisOrientation,
};
pub struct GlyphBrush {
fonts: Vec<FontArc>,
asset_ids: Vec<AssetId<Font>>,
latest_font_id: FontId,
}
impl Default for GlyphBrush {
fn default() -> Self {
GlyphBrush {
fonts: Vec::new(),
asset_ids: Vec::new(),
latest_font_id: FontId(0),
}
}
}
impl GlyphBrush {
pub fn compute_glyphs<S: ToSectionText>(
&self,
sections: &[S],
bounds: Vec2,
text_alignment: JustifyText,
linebreak_behavior: BreakLineOn,
) -> Result<Vec<SectionGlyph>, TextError> {
let geom = SectionGeometry {
bounds: (bounds.x, bounds.y),
..Default::default()
};
let lbb: BuiltInLineBreaker = linebreak_behavior.into();
let section_glyphs = Layout::default()
.h_align(text_alignment.into())
.line_breaker(lbb)
.calculate_glyphs(&self.fonts, &geom, sections);
Ok(section_glyphs)
}
#[allow(clippy::too_many_arguments)]
pub fn process_glyphs(
&self,
glyphs: Vec<SectionGlyph>,
sections: &[SectionText],
font_atlas_sets: &mut FontAtlasSets,
fonts: &Assets<Font>,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
text_settings: &TextSettings,
y_axis_orientation: YAxisOrientation,
h_anchor: f32,
) -> Result<Vec<PositionedGlyph>, TextError> {
if glyphs.is_empty() {
return Ok(Vec::new());
}
let sections_data = sections
.iter()
.map(|section| {
let asset_id = &self.asset_ids[section.font_id.0];
let font = fonts.get(*asset_id).ok_or(TextError::NoSuchFont)?;
let font_size = section.scale.y;
Ok((
asset_id,
font,
font_size,
ab_glyph::Font::as_scaled(&font.font, font_size),
))
})
.collect::<Result<Vec<_>, _>>()?;
let text_bounds = compute_text_bounds(&glyphs, |index| sections_data[index].3);
let mut positioned_glyphs = Vec::new();
for sg in glyphs {
let SectionGlyph {
section_index: _,
byte_index,
mut glyph,
font_id: _,
} = sg;
let placed_glyph = PlacedGlyph {
glyph_id: glyph.id,
subpixel_offset: glyph.position.into(),
};
let adjust = GlyphPlacementAdjuster::new(&mut glyph);
let section_data = sections_data[sg.section_index];
if let Some(outlined_glyph) = section_data.1.font.outline_glyph(glyph) {
let bounds = outlined_glyph.px_bounds();
let font_atlas_set = font_atlas_sets
.sets
.entry(*section_data.0)
.or_insert_with(FontAtlasSet::default);
let atlas_info = font_atlas_set
.get_glyph_atlas_info(section_data.2, &placed_glyph)
.map(Ok)
.unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph)
})?;
if !text_settings.allow_dynamic_font_size
&& font_atlas_set.len() > text_settings.soft_max_font_atlases.get()
{
warn_once!(
"warning[B0005]: Number of font atlases has exceeded the maximum of {}. Performance and memory usage may suffer. See: https://bevyengine.org/learn/errors/#b0005",
text_settings.soft_max_font_atlases.get());
}
let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
let glyph_rect = texture_atlas.textures[atlas_info.glyph_index];
let size = glyph_rect.size().as_vec2();
let x = bounds.min.x + size.x / 2.0 + h_anchor;
let y = match y_axis_orientation {
YAxisOrientation::BottomToTop => {
text_bounds.max.y - bounds.max.y + size.y / 2.0
}
YAxisOrientation::TopToBottom => {
bounds.min.y + size.y / 2.0 - text_bounds.min.y
}
};
// We must offset by 1 to account for glyph texture padding.
// See https://github.com/bevyengine/bevy/pull/11662
let position = adjust.position(Vec2::new(x, y) - 1.);
positioned_glyphs.push(PositionedGlyph {
position,
size,
atlas_info,
section_index: sg.section_index,
byte_index,
});
}
}
Ok(positioned_glyphs)
}
pub fn add_font(&mut self, asset_id: AssetId<Font>, font: FontArc) -> FontId {
self.fonts.push(font);
self.asset_ids.push(asset_id);
let font_id = self.latest_font_id;
self.latest_font_id = FontId(font_id.0 + 1);
font_id
}
}
#[derive(Debug, Clone, Reflect)]
pub struct PositionedGlyph {
pub position: Vec2,
pub size: Vec2,
pub atlas_info: GlyphAtlasInfo,
pub section_index: usize,
pub byte_index: usize,
}
#[cfg(feature = "subpixel_glyph_atlas")]
struct GlyphPlacementAdjuster;
#[cfg(feature = "subpixel_glyph_atlas")]
impl GlyphPlacementAdjuster {
#[inline(always)]
pub fn new(_: &mut Glyph) -> Self {
Self
}
#[inline(always)]
pub fn position(&self, p: Vec2) -> Vec2 {
p
}
}
#[cfg(not(feature = "subpixel_glyph_atlas"))]
struct GlyphPlacementAdjuster(f32);
#[cfg(not(feature = "subpixel_glyph_atlas"))]
impl GlyphPlacementAdjuster {
#[inline(always)]
pub fn new(glyph: &mut Glyph) -> Self {
let v = glyph.position.x.round();
glyph.position.x = 0.;
glyph.position.y = glyph.position.y.ceil();
Self(v)
}
#[inline(always)]
pub fn position(&self, v: Vec2) -> Vec2 {
Vec2::new(self.0, 0.) + v
}
}
/// Computes the minimal bounding rectangle for a block of text.
/// Ignores empty trailing lines.
pub(crate) fn compute_text_bounds<T>(
section_glyphs: &[SectionGlyph],
get_scaled_font: impl Fn(usize) -> PxScaleFont<T>,
) -> Rect
where
T: ab_glyph::Font,
{
let mut text_bounds = Rect {
min: Vec2::splat(f32::MAX),
max: Vec2::splat(f32::MIN),
};
for sg in section_glyphs {
let scaled_font = get_scaled_font(sg.section_index);
let glyph = &sg.glyph;
text_bounds = text_bounds.union(Rect {
min: Vec2::new(glyph.position.x, 0.),
max: Vec2::new(
glyph.position.x + scaled_font.h_advance(glyph.id),
glyph.position.y - scaled_font.descent(),
),
});
}
text_bounds
}

View file

@ -1,32 +1,61 @@
// FIXME(3492): remove once docs are ready
#![allow(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
//! 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.
//!
//! [`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 [`bevy_text::text2d::update_text2d_layout`] system (in a 2d world space context)
//! passes it into [`TextPipeline::queue_text`], which:
//!
//! 1. creates a [`Buffer`](cosmic_text::Buffer) from the [`TextSection`]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.
#![allow(clippy::type_complexity)]
mod bounds;
mod error;
mod font;
mod font_atlas;
mod font_atlas_set;
mod font_loader;
mod glyph_brush;
mod glyph;
mod pipeline;
mod text;
mod text2d;
pub use cosmic_text;
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_brush::*;
pub use glyph::*;
pub use pipeline::*;
pub use text::*;
pub use text2d::*;
/// Most commonly used re-exported types.
pub mod prelude {
#[doc(hidden)]
pub use crate::{Font, JustifyText, Text, Text2dBundle, TextError, TextSection, TextStyle};
@ -41,7 +70,6 @@ use bevy_render::{
camera::CameraUpdateSystem, view::VisibilitySystems, ExtractSchedule, RenderApp,
};
use bevy_sprite::SpriteSystem;
use std::num::NonZeroUsize;
/// Adds text rendering support to an app.
///
@ -50,31 +78,15 @@ use std::num::NonZeroUsize;
#[derive(Default)]
pub struct TextPlugin;
/// Settings used to configure the [`TextPlugin`].
#[derive(Resource)]
pub struct TextSettings {
/// Soft maximum number of font atlases supported in a [`FontAtlasSet`]. When this is exceeded,
/// a warning will be emitted a single time.
pub soft_max_font_atlases: NonZeroUsize,
/// Allows font size to be set dynamically exceeding the amount set in `soft_max_font_atlases`.
/// Note each font size has to be generated which can have a strong performance impact.
pub allow_dynamic_font_size: bool,
}
impl Default for TextSettings {
fn default() -> Self {
Self {
soft_max_font_atlases: NonZeroUsize::new(16).unwrap(),
allow_dynamic_font_size: false,
}
}
}
/// Text is rendered for two different view projections, a [`Text2dBundle`] is rendered 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.
/// Text is rendered for two different view projections;
/// 2-dimensional text ([`Text2dBundle`]) 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,
}
@ -86,9 +98,8 @@ impl Plugin for TextPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<Font>()
.register_type::<Text>()
.register_type::<Text2dBounds>()
.register_type::<TextBounds>()
.init_asset_loader::<FontLoader>()
.init_resource::<TextSettings>()
.init_resource::<FontAtlasSets>()
.insert_resource(TextPipeline::default())
.add_systems(

View file

@ -1,219 +1,397 @@
use crate::{
compute_text_bounds, error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font,
FontAtlasSets, JustifyText, PositionedGlyph, Text, TextSection, TextSettings, YAxisOrientation,
};
use ab_glyph::PxScale;
use bevy_asset::{AssetId, Assets, Handle};
use bevy_ecs::component::Component;
use bevy_ecs::prelude::ReflectComponent;
use bevy_ecs::system::Resource;
use bevy_math::Vec2;
use bevy_reflect::prelude::ReflectDefault;
use bevy_reflect::Reflect;
use std::sync::Arc;
use bevy_asset::{AssetId, Assets};
use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource};
use bevy_math::{UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::Image;
use bevy_sprite::TextureAtlasLayout;
use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText, ToSectionText};
#[derive(Default, Resource)]
pub struct TextPipeline {
brush: GlyphBrush,
map_font_id: HashMap<AssetId<Font>, FontId>,
use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
use crate::{
error::TextError, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, PositionedGlyph,
TextBounds, TextSection, YAxisOrientation,
};
/// A wrapper around a [`cosmic_text::FontSystem`]
struct CosmicFontSystem(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))
}
}
/// Render information for a corresponding [`Text`] component.
/// A wrapper around a [`cosmic_text::SwashCache`]
struct SwashCache(cosmic_text::SwashCache);
impl Default for SwashCache {
fn default() -> Self {
Self(cosmic_text::SwashCache::new())
}
}
/// The `TextPipeline` is used to layout and render [`Text`](crate::Text).
///
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`].
#[derive(Component, Clone, Default, Debug, Reflect)]
#[reflect(Component, Default)]
pub struct TextLayoutInfo {
pub glyphs: Vec<PositionedGlyph>,
pub logical_size: Vec2,
/// 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, String)>,
/// The font system is used to retrieve fonts and their information, including glyph outlines.
///
/// See [`cosmic_text::FontSystem`] for more information.
font_system: CosmicFontSystem,
/// The swash cache rasterizer is used to rasterize glyphs
///
/// See [`cosmic_text::SwashCache`] for more information.
swash_cache: SwashCache,
}
impl TextPipeline {
pub fn get_or_insert_font_id(&mut self, handle: &Handle<Font>, font: &Font) -> FontId {
let brush = &mut self.brush;
*self
.map_font_id
.entry(handle.id())
.or_insert_with(|| brush.add_font(handle.id(), font.font.clone()))
/// Utilizes [`cosmic_text::Buffer`] to shape and layout text
///
/// Negative or 0.0 font sizes will not be laid out.
#[allow(clippy::too_many_arguments)]
pub fn update_buffer(
&mut self,
fonts: &Assets<Font>,
sections: &[TextSection],
linebreak_behavior: BreakLineOn,
bounds: TextBounds,
scale_factor: f64,
buffer: &mut CosmicBuffer,
alignment: JustifyText,
) -> Result<(), TextError> {
let font_system = &mut self.font_system.0;
// return early if the fonts are not loaded yet
let mut font_size = 0.;
for section in sections {
if section.style.font_size > font_size {
font_size = section.style.font_size;
}
fonts
.get(section.style.font.id())
.ok_or(TextError::NoSuchFont)?;
}
let line_height = font_size * 1.2;
let metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32);
// Load Bevy fonts into cosmic-text's font system.
// This is done as as separate pre-pass to avoid borrow checker issues
for section in sections.iter() {
load_font_to_fontdb(section, font_system, &mut self.map_handle_to_font_id, fonts);
}
// 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: Vec<(&str, Attrs)> = sections
.iter()
.enumerate()
.filter(|(_section_index, section)| section.style.font_size > 0.0)
.map(|(section_index, section)| {
(
&section.value[..],
get_attrs(
section,
section_index,
font_system,
&self.map_handle_to_font_id,
scale_factor,
),
)
})
.collect();
buffer.set_metrics(font_system, metrics);
buffer.set_size(font_system, bounds.width, bounds.height);
buffer.set_wrap(
font_system,
match linebreak_behavior {
BreakLineOn::WordBoundary => Wrap::Word,
BreakLineOn::AnyCharacter => Wrap::Glyph,
BreakLineOn::WordOrCharacter => Wrap::WordOrGlyph,
BreakLineOn::NoWrap => Wrap::None,
},
);
buffer.set_rich_text(font_system, spans, Attrs::new(), Shaping::Advanced);
// PERF: https://github.com/pop-os/cosmic-text/issues/166:
// Setting alignment afterwards appears to invalidate some layouting performed by `set_text` which is presumably not free?
for buffer_line in buffer.lines.iter_mut() {
buffer_line.set_align(Some(alignment.into()));
}
buffer.shape_until_scroll(font_system, false);
Ok(())
}
/// Queues text for rendering
///
/// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s
/// which contain information for rendering the text.
#[allow(clippy::too_many_arguments)]
pub fn queue_text(
&mut self,
fonts: &Assets<Font>,
sections: &[TextSection],
scale_factor: f32,
scale_factor: f64,
text_alignment: JustifyText,
linebreak_behavior: BreakLineOn,
bounds: Vec2,
bounds: TextBounds,
font_atlas_sets: &mut FontAtlasSets,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
text_settings: &TextSettings,
y_axis_orientation: YAxisOrientation,
buffer: &mut CosmicBuffer,
) -> Result<TextLayoutInfo, TextError> {
let mut scaled_fonts = Vec::with_capacity(sections.len());
let sections = sections
.iter()
.map(|section| {
let font = fonts
.get(&section.style.font)
.ok_or(TextError::NoSuchFont)?;
let font_id = self.get_or_insert_font_id(&section.style.font, font);
let font_size = scale_value(section.style.font_size, scale_factor);
scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size));
let section = SectionText {
font_id,
scale: PxScale::from(font_size),
text: &section.value,
};
Ok(section)
})
.collect::<Result<Vec<_>, _>>()?;
let section_glyphs =
self.brush
.compute_glyphs(&sections, bounds, text_alignment, linebreak_behavior)?;
if section_glyphs.is_empty() {
if sections.is_empty() {
return Ok(TextLayoutInfo::default());
}
let size = compute_text_bounds(&section_glyphs, |index| scaled_fonts[index]).size();
let h_limit = if bounds.x.is_finite() {
bounds.x
} else {
size.x
};
let h_anchor = match text_alignment {
JustifyText::Left => 0.0,
JustifyText::Center => h_limit * 0.5,
JustifyText::Right => h_limit * 1.0,
}
.floor();
let glyphs = self.brush.process_glyphs(
section_glyphs,
&sections,
font_atlas_sets,
self.update_buffer(
fonts,
texture_atlases,
textures,
text_settings,
y_axis_orientation,
h_anchor,
sections,
linebreak_behavior,
bounds,
scale_factor,
buffer,
text_alignment,
)?;
let box_size = buffer_dimensions(buffer);
let font_system = &mut self.font_system.0;
let swash_cache = &mut self.swash_cache.0;
let glyphs = buffer
.layout_runs()
.flat_map(|run| {
run.glyphs
.iter()
.map(move |layout_glyph| (layout_glyph, run.line_y))
})
.map(|(layout_glyph, line_y)| {
let section_index = layout_glyph.metadata;
let font_handle = sections[section_index].style.font.clone_weak();
let font_atlas_set = font_atlas_sets.sets.entry(font_handle.id()).or_default();
let physical_glyph = layout_glyph.physical((0., 0.), 1.);
let atlas_info = font_atlas_set
.get_glyph_atlas_info(physical_glyph.cache_key)
.map(Ok)
.unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(
texture_atlases,
textures,
font_system,
swash_cache,
layout_glyph,
)
})?;
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);
// TODO: recreate the byte index, that keeps track of where a cursor is,
// when glyphs are not limited to single byte representation, relevant for #1319
let pos_glyph =
PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, section_index);
Ok(pos_glyph)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(TextLayoutInfo {
glyphs,
logical_size: size,
size: box_size,
})
}
/// 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(
&mut self,
fonts: &Assets<Font>,
sections: &[TextSection],
scale_factor: f64,
linebreak_behavior: BreakLineOn,
buffer: &mut CosmicBuffer,
text_alignment: JustifyText,
) -> Result<TextMeasureInfo, TextError> {
const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0);
self.update_buffer(
fonts,
sections,
linebreak_behavior,
MIN_WIDTH_CONTENT_BOUNDS,
scale_factor,
buffer,
text_alignment,
)?;
let min_width_content_size = buffer_dimensions(buffer);
let max_width_content_size = {
let font_system = &mut self.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,
// TODO: This clone feels wasteful, is there another way to structure TextMeasureInfo
// that it doesn't need to own a buffer? - bytemunch
buffer: buffer.0.clone(),
})
}
/// Get a mutable reference to the [`cosmic_text::FontSystem`].
///
/// Used internally.
pub fn font_system_mut(&mut self) -> &mut cosmic_text::FontSystem {
&mut self.font_system.0
}
}
#[derive(Debug, Clone)]
pub struct TextMeasureSection {
pub text: Box<str>,
pub scale: f32,
pub font_id: FontId,
/// Render information for a corresponding [`Text`](crate::Text) component.
///
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`].
#[derive(Component, Clone, Default, Debug, Reflect)]
#[reflect(Component, Default)]
pub struct TextLayoutInfo {
/// Scaled and positioned glyphs in screenspace
pub glyphs: Vec<PositionedGlyph>,
/// The glyphs resulting size
pub size: Vec2,
}
#[derive(Debug, Clone, Default)]
/// Size information for a corresponding [`Text`](crate::Text) component.
///
/// Generated via [`TextPipeline::create_text_measure`].
pub struct TextMeasureInfo {
pub fonts: Box<[ab_glyph::FontArc]>,
pub sections: Box<[TextMeasureSection]>,
pub justification: JustifyText,
pub linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker,
/// 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,
buffer: cosmic_text::Buffer,
}
impl std::fmt::Debug for TextMeasureInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextMeasureInfo")
.field("min", &self.min)
.field("max", &self.max)
.field("buffer", &"_")
.field("font_system", &"_")
.finish()
}
}
impl TextMeasureInfo {
pub fn from_text(
text: &Text,
fonts: &Assets<Font>,
scale_factor: f32,
) -> Result<TextMeasureInfo, TextError> {
let sections = &text.sections;
let mut auto_fonts = Vec::with_capacity(sections.len());
let mut out_sections = Vec::with_capacity(sections.len());
for (i, section) in sections.iter().enumerate() {
match fonts.get(&section.style.font) {
Some(font) => {
auto_fonts.push(font.font.clone());
out_sections.push(TextMeasureSection {
font_id: FontId(i),
scale: scale_value(section.style.font_size, scale_factor),
text: section.value.clone().into_boxed_str(),
});
}
None => return Err(TextError::NoSuchFont),
}
}
Ok(Self::new(
auto_fonts,
out_sections,
text.justify,
text.linebreak_behavior.into(),
))
}
fn new(
fonts: Vec<ab_glyph::FontArc>,
sections: Vec<TextMeasureSection>,
justification: JustifyText,
linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker,
) -> Self {
let mut info = Self {
fonts: fonts.into_boxed_slice(),
sections: sections.into_boxed_slice(),
justification,
linebreak_behavior,
min: Vec2::ZERO,
max: Vec2::ZERO,
};
let min = info.compute_size(Vec2::new(0.0, f32::INFINITY));
let max = info.compute_size(Vec2::INFINITY);
info.min = min;
info.max = max;
info
}
pub fn compute_size(&self, bounds: Vec2) -> Vec2 {
let sections = &self.sections;
let geom = SectionGeometry {
bounds: (bounds.x, bounds.y),
..Default::default()
};
let section_glyphs = glyph_brush_layout::Layout::default()
.h_align(self.justification.into())
.line_breaker(self.linebreak_behavior)
.calculate_glyphs(&self.fonts, &geom, sections);
compute_text_bounds(&section_glyphs, |index| {
let font = &self.fonts[index];
let font_size = self.sections[index].scale;
ab_glyph::Font::into_scaled(font, font_size)
})
.size()
/// Computes the size of the text area within the provided bounds.
pub fn compute_size(
&mut self,
bounds: TextBounds,
font_system: &mut cosmic_text::FontSystem,
) -> Vec2 {
self.buffer
.set_size(font_system, bounds.width, bounds.height);
buffer_dimensions(&self.buffer)
}
}
impl ToSectionText for TextMeasureSection {
#[inline(always)]
fn to_section_text(&self) -> SectionText<'_> {
SectionText {
text: &self.text,
scale: PxScale::from(self.scale),
font_id: self.font_id,
}
}
fn load_font_to_fontdb(
section: &TextSection,
font_system: &mut cosmic_text::FontSystem,
map_handle_to_font_id: &mut HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, String)>,
fonts: &Assets<Font>,
) {
let font_handle = section.style.font.clone();
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 = face.families[0].0.to_owned();
(face_id, family_name)
});
}
/// Translates [`TextSection`] to [`Attrs`](cosmic_text::attrs::Attrs),
/// loading fonts into the [`Database`](cosmic_text::fontdb::Database) if required.
fn get_attrs<'a>(
section: &TextSection,
section_index: usize,
font_system: &mut cosmic_text::FontSystem,
map_handle_to_font_id: &'a HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, String)>,
scale_factor: f64,
) -> Attrs<'a> {
let (face_id, family_name) = map_handle_to_font_id
.get(&section.style.font.id())
.expect("Already loaded with load_font_to_fontdb");
let face = font_system.db().face(*face_id).unwrap();
let attrs = Attrs::new()
.metadata(section_index)
.family(Family::Name(family_name))
.stretch(face.stretch)
.style(face.style)
.weight(face.weight)
.metrics(Metrics::relative(section.style.font_size, 1.2).scale(scale_factor as f32))
.color(cosmic_text::Color(section.style.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 = buffer
.layout_runs()
.map(|run| run.line_w)
.reduce(f32::max)
.unwrap_or(0.0);
let line_height = buffer.metrics().line_height.ceil();
let height = buffer.layout_runs().count() as f32 * line_height;
Vec2::new(width.ceil(), height).ceil()
}

View file

@ -1,15 +1,35 @@
use bevy_asset::Handle;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_reflect::prelude::*;
use bevy_utils::default;
use cosmic_text::{Buffer, Metrics};
use serde::{Deserialize, Serialize};
use crate::Font;
pub use cosmic_text::{
self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle,
Weight as FontWeight,
};
/// Wrapper for [`cosmic_text::Buffer`]
#[derive(Component, 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 component that is the entry point for rendering text.
///
/// It contains all of the text value and styling information.
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component, Default)]
pub struct Text {
/// The text's sections
pub sections: Vec<TextSection>,
/// The text's internal alignment.
/// Should not affect its position within a container.
@ -33,7 +53,7 @@ impl Text {
/// // Accepts a String or any type that converts into a String, such as &str.
/// "hello world!",
/// TextStyle {
/// font: font_handle.clone(),
/// font: font_handle.clone().into(),
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
@ -42,7 +62,7 @@ impl Text {
/// let hello_bevy = Text::from_section(
/// "hello world\nand bevy!",
/// TextStyle {
/// font: font_handle,
/// font: font_handle.into(),
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
@ -70,7 +90,7 @@ impl Text {
/// TextSection::new(
/// "Hello, ",
/// TextStyle {
/// font: font_handle.clone(),
/// font: font_handle.clone().into(),
/// font_size: 60.0,
/// color: BLUE.into(),
/// },
@ -78,7 +98,7 @@ impl Text {
/// TextSection::new(
/// "World!",
/// TextStyle {
/// font: font_handle,
/// font: font_handle.into(),
/// font_size: 60.0,
/// color: RED.into(),
/// },
@ -106,9 +126,12 @@ impl Text {
}
}
/// Contains the value of the text in a section and how it should be styled.
#[derive(Debug, Default, Clone, Reflect)]
pub struct TextSection {
/// The content (in `String` form) of the text in the section.
pub value: String,
/// The style of the text in the section, including the font face, font size, and color.
pub style: TextStyle,
}
@ -168,23 +191,32 @@ pub enum JustifyText {
/// 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 glyph_brush_layout::HorizontalAlign {
fn from(val: JustifyText) -> Self {
match val {
JustifyText::Left => glyph_brush_layout::HorizontalAlign::Left,
JustifyText::Center => glyph_brush_layout::HorizontalAlign::Center,
JustifyText::Right => glyph_brush_layout::HorizontalAlign::Right,
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,
}
}
}
#[derive(Clone, Debug, Reflect)]
/// `TextStyle` determines the style of the text in a section, specifically
/// the font face, the font size, and the color.
pub struct TextStyle {
/// If this is not specified, then
/// 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.
/// `FiraMono-subset.ttf` compiled into the library is used.
/// * otherwise no text will be rendered.
pub font: Handle<Font>,
/// The vertical height of rasterized glyphs in the font atlas in pixels.
@ -195,6 +227,7 @@ pub struct TextStyle {
/// 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 color of the text for this section.
pub color: Color,
}
@ -202,7 +235,7 @@ impl Default for TextStyle {
fn default() -> Self {
Self {
font: Default::default(),
font_size: 24.0,
font_size: 20.0,
color: Color::WHITE,
}
}
@ -221,20 +254,9 @@ pub enum BreakLineOn {
/// This is closer to the behavior one might expect from text in a terminal.
/// However it may lead to words being broken up across linebreaks.
AnyCharacter,
/// 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,
}
impl From<BreakLineOn> for glyph_brush_layout::BuiltInLineBreaker {
fn from(val: BreakLineOn) -> Self {
match val {
// If `NoWrap` is set the choice of `BuiltInLineBreaker` doesn't matter as the text is given unbounded width and soft wrapping will never occur.
// But `NoWrap` does not disable hard breaks where a [`Text`] contains a newline character.
BreakLineOn::WordBoundary | BreakLineOn::NoWrap => {
glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker
}
BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker,
}
}
}

View file

@ -1,22 +1,19 @@
use crate::{
BreakLineOn, Font, FontAtlasSets, PositionedGlyph, Text, TextError, TextLayoutInfo,
TextPipeline, TextSettings, YAxisOrientation,
BreakLineOn, CosmicBuffer, Font, FontAtlasSets, PositionedGlyph, Text, TextBounds, TextError,
TextLayoutInfo, TextPipeline, YAxisOrientation,
};
use bevy_asset::Assets;
use bevy_color::LinearRgba;
use bevy_ecs::{
bundle::Bundle,
change_detection::{DetectChanges, Ref},
component::Component,
entity::Entity,
event::EventReader,
prelude::With,
query::{Changed, Without},
reflect::ReflectComponent,
system::{Commands, Local, Query, Res, ResMut},
};
use bevy_math::Vec2;
use bevy_reflect::Reflect;
use bevy_render::{
primitives::Aabb,
texture::Image,
@ -28,34 +25,6 @@ use bevy_transform::prelude::{GlobalTransform, Transform};
use bevy_utils::HashSet;
use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged};
/// 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)]
pub struct Text2dBounds {
/// The maximum width and height of text in logical pixels.
pub size: Vec2,
}
impl Default for Text2dBounds {
#[inline]
fn default() -> Self {
Self::UNBOUNDED
}
}
impl Text2dBounds {
/// Unbounded text will not be truncated or wrapped.
pub const UNBOUNDED: Self = Self {
size: Vec2::splat(f32::INFINITY),
};
}
/// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`.
/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs)
#[derive(Bundle, Clone, Debug, Default)]
@ -66,13 +35,15 @@ pub struct Text2dBundle {
/// relative position which is controlled by the `Anchor` component.
/// This means that for a block of text consisting of only one line that doesn't wrap, the `alignment` field will have no effect.
pub text: Text,
/// Cached buffer for layout with cosmic-text
pub buffer: CosmicBuffer,
/// How the text is positioned relative to its transform.
///
/// `text_anchor` does not affect the internal alignment of the block of text, only
/// its position.
pub text_anchor: Anchor,
/// The maximum width and height of the text.
pub text_2d_bounds: Text2dBounds,
pub text_2d_bounds: TextBounds,
/// The transform of the text.
pub transform: Transform,
/// The global transform of the text.
@ -124,7 +95,7 @@ pub fn extract_text2d_sprite(
}
let text_anchor = -(anchor.as_vec() + 0.5);
let alignment_translation = text_layout_info.logical_size * text_anchor;
let alignment_translation = text_layout_info.size * text_anchor;
let transform = *global_transform
* GlobalTransform::from_translation(alignment_translation.extend(0.))
* scaling;
@ -149,7 +120,7 @@ pub fn extract_text2d_sprite(
ExtractedSprite {
transform: transform * GlobalTransform::from_translation(position.extend(0.)),
color,
rect: Some(atlas.textures[atlas_info.glyph_index].as_rect()),
rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()),
custom_size: None,
image_handle_id: atlas_info.texture.id(),
flip_x: false,
@ -175,13 +146,18 @@ pub fn update_text2d_layout(
mut queue: Local<HashSet<Entity>>,
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
text_settings: Res<TextSettings>,
windows: Query<&Window, With<PrimaryWindow>>,
mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_sets: ResMut<FontAtlasSets>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(Entity, Ref<Text>, Ref<Text2dBounds>, &mut TextLayoutInfo)>,
mut text_query: Query<(
Entity,
Ref<Text>,
Ref<TextBounds>,
&mut TextLayoutInfo,
&mut CosmicBuffer,
)>,
) {
// We need to consume the entire iterator, hence `last`
let factor_changed = scale_factor_changed.read().last().is_some();
@ -194,40 +170,43 @@ pub fn update_text2d_layout(
let inverse_scale_factor = scale_factor.recip();
for (entity, text, bounds, mut text_layout_info) in &mut text_query {
for (entity, text, bounds, mut text_layout_info, mut buffer) in &mut text_query {
if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) {
let text_bounds = Vec2::new(
if text.linebreak_behavior == BreakLineOn::NoWrap {
f32::INFINITY
let text_bounds = TextBounds {
width: if text.linebreak_behavior == BreakLineOn::NoWrap {
None
} else {
scale_value(bounds.size.x, scale_factor)
bounds.width.map(|width| scale_value(width, scale_factor))
},
scale_value(bounds.size.y, scale_factor),
);
height: bounds
.height
.map(|height| scale_value(height, scale_factor)),
};
match text_pipeline.queue_text(
&fonts,
&text.sections,
scale_factor,
scale_factor.into(),
text.justify,
text.linebreak_behavior,
text_bounds,
&mut font_atlas_sets,
&mut texture_atlases,
&mut textures,
text_settings.as_ref(),
YAxisOrientation::BottomToTop,
buffer.as_mut(),
) {
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(_)) => {
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(mut info) => {
info.logical_size.x = scale_value(info.logical_size.x, inverse_scale_factor);
info.logical_size.y = scale_value(info.logical_size.y, inverse_scale_factor);
info.size.x = scale_value(info.size.x, inverse_scale_factor);
info.size.y = scale_value(info.size.y, inverse_scale_factor);
*text_layout_info = info;
}
}
@ -254,11 +233,9 @@ pub fn calculate_bounds_text2d(
for (entity, layout_info, anchor, aabb) in &mut text_to_update_aabb {
// `Anchor::as_vec` gives us an offset relative to the text2d bounds, by negating it and scaling
// by the logical size we compensate the transform offset in local space to get the center.
let center = (-anchor.as_vec() * layout_info.logical_size)
.extend(0.0)
.into();
let center = (-anchor.as_vec() * layout_info.size).extend(0.0).into();
// Distance in local space from the center to the x and y limits of the text2d bounds.
let half_extents = (layout_info.logical_size / 2.0).extend(0.0).into();
let half_extents = (layout_info.size / 2.0).extend(0.0).into();
if let Some(mut aabb) = aabb {
*aabb = Aabb {
center,
@ -291,7 +268,6 @@ mod tests {
app.init_resource::<Assets<Font>>()
.init_resource::<Assets<Image>>()
.init_resource::<Assets<TextureAtlasLayout>>()
.init_resource::<TextSettings>()
.init_resource::<FontAtlasSets>()
.init_resource::<Events<WindowScaleFactorChanged>>()
.insert_resource(TextPipeline::default())

View file

@ -1,3 +1,4 @@
use bevy_text::TextPipeline;
use thiserror::Error;
use crate::{ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale};
@ -94,6 +95,7 @@ pub fn ui_layout_system(
just_children_query: Query<&Children>,
mut removed_components: UiLayoutSystemRemovedComponentParam,
mut node_transform_query: Query<(&mut Node, &mut Transform)>,
#[cfg(feature = "bevy_text")] mut text_pipeline: ResMut<TextPipeline>,
) {
struct CameraLayoutInfo {
size: UVec2,
@ -214,13 +216,20 @@ pub fn ui_layout_system(
}
}
#[cfg(feature = "bevy_text")]
let font_system = text_pipeline.font_system_mut();
// clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used)
ui_surface.remove_entities(removed_components.removed_nodes.read());
for (camera_id, camera) in &camera_layout_info {
let inverse_target_scale_factor = camera.scale_factor.recip();
ui_surface.compute_camera_layout(*camera_id, camera.size);
ui_surface.compute_camera_layout(
*camera_id,
camera.size,
#[cfg(feature = "bevy_text")]
font_system,
);
for root in &camera.root_nodes {
update_uinode_geometry_recursive(
*root,
@ -399,6 +408,8 @@ mod tests {
world.init_resource::<Events<AssetEvent<Image>>>();
world.init_resource::<Assets<Image>>();
world.init_resource::<ManualTextureViews>();
#[cfg(feature = "bevy_text")]
world.init_resource::<bevy_text::TextPipeline>();
// spawn a dummy primary window and camera
world.spawn((
@ -1031,6 +1042,8 @@ mod tests {
world.init_resource::<Events<AssetEvent<Image>>>();
world.init_resource::<Assets<Image>>();
world.init_resource::<ManualTextureViews>();
#[cfg(feature = "bevy_text")]
world.init_resource::<bevy_text::TextPipeline>();
// spawn a dummy primary window and camera
world.spawn((

View file

@ -10,7 +10,7 @@ use bevy_utils::default;
use bevy_utils::tracing::warn;
use crate::layout::convert;
use crate::{LayoutContext, LayoutError, Measure, NodeMeasure, Style};
use crate::{LayoutContext, LayoutError, Measure, MeasureArgs, NodeMeasure, Style};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootNodePair {
@ -196,7 +196,12 @@ without UI components as a child of an entity with UI components, results may be
}
/// Compute the layout for each window entity's corresponding root node in the layout.
pub fn compute_camera_layout(&mut self, camera: Entity, render_target_resolution: UVec2) {
pub fn compute_camera_layout(
&mut self,
camera: Entity,
render_target_resolution: UVec2,
#[cfg(feature = "bevy_text")] font_system: &mut bevy_text::cosmic_text::FontSystem,
) {
let Some(camera_root_nodes) = self.camera_roots.get(&camera) else {
return;
};
@ -219,10 +224,14 @@ without UI components as a child of an entity with UI components, results may be
context
.map(|ctx| {
let size = ctx.measure(
known_dimensions.width,
known_dimensions.height,
available_space.width,
available_space.height,
MeasureArgs {
width: known_dimensions.width,
height: known_dimensions.height,
available_width: available_space.width,
available_height: available_space.height,
#[cfg(feature = "bevy_text")]
font_system,
},
style,
);
taffy::Size {

View file

@ -16,18 +16,20 @@ impl std::fmt::Debug for ContentSize {
}
}
pub struct MeasureArgs<'a> {
pub width: Option<f32>,
pub height: Option<f32>,
pub available_width: AvailableSpace,
pub available_height: AvailableSpace,
#[cfg(feature = "bevy_text")]
pub font_system: &'a mut bevy_text::cosmic_text::FontSystem,
}
/// A `Measure` is used to compute the size of a ui node
/// when the size of that node is based on its content.
pub trait Measure: Send + Sync + 'static {
/// Calculate the size of the node given the constraints.
fn measure(
&self,
width: Option<f32>,
height: Option<f32>,
available_width: AvailableSpace,
available_height: AvailableSpace,
style: &taffy::Style,
) -> Vec2;
fn measure(&mut self, measure_args: MeasureArgs<'_>, style: &taffy::Style) -> Vec2;
}
/// A type to serve as Taffy's node context (which allows the content size of leaf nodes to be computed)
@ -43,28 +45,13 @@ pub enum NodeMeasure {
}
impl Measure for NodeMeasure {
fn measure(
&self,
width: Option<f32>,
height: Option<f32>,
available_width: AvailableSpace,
available_height: AvailableSpace,
style: &taffy::Style,
) -> Vec2 {
fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 {
match self {
NodeMeasure::Fixed(fixed) => {
fixed.measure(width, height, available_width, available_height, style)
}
NodeMeasure::Fixed(fixed) => fixed.measure(measure_args, style),
#[cfg(feature = "bevy_text")]
NodeMeasure::Text(text) => {
text.measure(width, height, available_width, available_height, style)
}
NodeMeasure::Image(image) => {
image.measure(width, height, available_width, available_height, style)
}
NodeMeasure::Custom(custom) => {
custom.measure(width, height, available_width, available_height, style)
}
NodeMeasure::Text(text) => text.measure(measure_args, style),
NodeMeasure::Image(image) => image.measure(measure_args, style),
NodeMeasure::Custom(custom) => custom.measure(measure_args, style),
}
}
}
@ -77,14 +64,7 @@ pub struct FixedMeasure {
}
impl Measure for FixedMeasure {
fn measure(
&self,
_: Option<f32>,
_: Option<f32>,
_: AvailableSpace,
_: AvailableSpace,
_: &taffy::Style,
) -> Vec2 {
fn measure(&mut self, _: MeasureArgs, _: &taffy::Style) -> Vec2 {
self.size
}
}

View file

@ -15,7 +15,9 @@ use bevy_ecs::bundle::Bundle;
use bevy_render::view::{InheritedVisibility, ViewVisibility, Visibility};
use bevy_sprite::TextureAtlas;
#[cfg(feature = "bevy_text")]
use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle};
use bevy_text::{
BreakLineOn, CosmicBuffer, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle,
};
use bevy_transform::prelude::{GlobalTransform, Transform};
/// The basic UI node.
@ -169,6 +171,8 @@ pub struct TextBundle {
pub style: Style,
/// Contains the text of the node
pub text: Text,
/// Cached cosmic buffer for layout
pub buffer: CosmicBuffer,
/// Text layout information
pub text_layout_info: TextLayoutInfo,
/// Text system flags

View file

@ -861,7 +861,7 @@ pub fn extract_uinode_text(
}
let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
let mut rect = atlas.textures[atlas_info.glyph_index].as_rect();
let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect();
rect.min *= inverse_scale_factor;
rect.max *= inverse_scale_factor;
extracted_uinodes.uinodes.insert(

View file

@ -1,6 +1,4 @@
use crate::{
measurement::AvailableSpace, ContentSize, Measure, Node, NodeMeasure, UiImage, UiScale,
};
use crate::{ContentSize, Measure, MeasureArgs, Node, NodeMeasure, UiImage, UiScale};
use bevy_asset::Assets;
use bevy_ecs::prelude::*;
use bevy_math::{UVec2, Vec2};
@ -37,14 +35,15 @@ pub struct ImageMeasure {
}
impl Measure for ImageMeasure {
fn measure(
&self,
width: Option<f32>,
height: Option<f32>,
available_width: AvailableSpace,
available_height: AvailableSpace,
style: &taffy::Style,
) -> Vec2 {
fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 {
let MeasureArgs {
width,
height,
available_width,
available_height,
..
} = measure_args;
// Convert available width/height into an option
let parent_width = available_width.into_option();
let parent_height = available_height.into_option();

View file

@ -1,5 +1,6 @@
use crate::{
ContentSize, DefaultUiCamera, FixedMeasure, Measure, Node, NodeMeasure, TargetCamera, UiScale,
ContentSize, DefaultUiCamera, FixedMeasure, Measure, MeasureArgs, Node, NodeMeasure,
TargetCamera, UiScale,
};
use bevy_asset::Assets;
use bevy_ecs::{
@ -15,8 +16,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{camera::Camera, texture::Image};
use bevy_sprite::TextureAtlasLayout;
use bevy_text::{
scale_value, BreakLineOn, Font, FontAtlasSets, Text, TextError, TextLayoutInfo,
TextMeasureInfo, TextPipeline, TextSettings, YAxisOrientation,
scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, TextBounds,
TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation,
};
use bevy_utils::Entry;
use taffy::style::AvailableSpace;
@ -42,20 +43,19 @@ impl Default for TextFlags {
}
}
#[derive(Clone)]
pub struct TextMeasure {
pub info: TextMeasureInfo,
}
impl Measure for TextMeasure {
fn measure(
&self,
width: Option<f32>,
height: Option<f32>,
available_width: AvailableSpace,
_available_height: AvailableSpace,
_style: &taffy::Style,
) -> Vec2 {
fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
let MeasureArgs {
width,
height,
available_width,
font_system,
..
} = measure_args;
let x = width.unwrap_or_else(|| match available_width {
AvailableSpace::Definite(x) => {
// It is possible for the "min content width" to be larger than
@ -71,7 +71,9 @@ impl Measure for TextMeasure {
height
.map_or_else(
|| match available_width {
AvailableSpace::Definite(_) => self.info.compute_size(Vec2::new(x, f32::MAX)),
AvailableSpace::Definite(_) => self
.info
.compute_size(TextBounds::new_horizontal(x), font_system),
AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
},
@ -81,15 +83,26 @@ impl Measure for TextMeasure {
}
}
#[allow(clippy::too_many_arguments)]
#[inline]
fn create_text_measure(
fonts: &Assets<Font>,
scale_factor: f32,
scale_factor: f64,
text: Ref<Text>,
text_pipeline: &mut TextPipeline,
mut content_size: Mut<ContentSize>,
mut text_flags: Mut<TextFlags>,
buffer: &mut CosmicBuffer,
text_alignment: JustifyText,
) {
match TextMeasureInfo::from_text(&text, fonts, scale_factor) {
match text_pipeline.create_text_measure(
fonts,
&text.sections,
scale_factor,
text.linebreak_behavior,
buffer,
text_alignment,
) {
Ok(measure) => {
if text.linebreak_behavior == BreakLineOn::NoWrap {
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
@ -105,7 +118,7 @@ fn create_text_measure(
// Try again next frame
text_flags.needs_new_measure_func = true;
}
Err(e @ TextError::FailedToAddGlyph(_)) => {
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
}
};
@ -132,13 +145,15 @@ pub fn measure_text_system(
&mut ContentSize,
&mut TextFlags,
Option<&TargetCamera>,
&mut CosmicBuffer,
),
With<Node>,
>,
mut text_pipeline: ResMut<TextPipeline>,
) {
let mut scale_factors: EntityHashMap<f32> = EntityHashMap::default();
for (text, content_size, text_flags, camera) in &mut text_query {
for (text, content_size, text_flags, camera, mut buffer) in &mut text_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
@ -159,7 +174,17 @@ pub fn measure_text_system(
|| text_flags.needs_new_measure_func
|| content_size.is_added()
{
create_text_measure(&fonts, scale_factor, text, content_size, text_flags);
let text_alignment = text.justify;
create_text_measure(
&fonts,
scale_factor.into(),
text,
&mut text_pipeline,
content_size,
text_flags,
buffer.as_mut(),
text_alignment,
);
}
}
*last_scale_factors = scale_factors;
@ -173,22 +198,22 @@ fn queue_text(
font_atlas_sets: &mut FontAtlasSets,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
text_settings: &TextSettings,
scale_factor: f32,
inverse_scale_factor: f32,
text: &Text,
node: Ref<Node>,
mut text_flags: Mut<TextFlags>,
mut text_layout_info: Mut<TextLayoutInfo>,
buffer: &mut CosmicBuffer,
) {
// Skip the text node if it is waiting for a new measure func
if !text_flags.needs_new_measure_func {
let physical_node_size = if text.linebreak_behavior == BreakLineOn::NoWrap {
// With `NoWrap` set, no constraints are placed on the width of the text.
Vec2::splat(f32::INFINITY)
TextBounds::UNBOUNDED
} else {
// `scale_factor` is already multiplied by `UiScale`
Vec2::new(
TextBounds::new(
node.unrounded_size.x * scale_factor,
node.unrounded_size.y * scale_factor,
)
@ -197,26 +222,26 @@ fn queue_text(
match text_pipeline.queue_text(
fonts,
&text.sections,
scale_factor,
scale_factor.into(),
text.justify,
text.linebreak_behavior,
physical_node_size,
font_atlas_sets,
texture_atlases,
textures,
text_settings,
YAxisOrientation::TopToBottom,
buffer,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, try again next frame
text_flags.needs_recompute = true;
}
Err(e @ TextError::FailedToAddGlyph(_)) => {
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(mut info) => {
info.logical_size.x = scale_value(info.logical_size.x, inverse_scale_factor);
info.logical_size.y = scale_value(info.logical_size.y, inverse_scale_factor);
info.size.x = scale_value(info.size.x, inverse_scale_factor);
info.size.y = scale_value(info.size.y, inverse_scale_factor);
*text_layout_info = info;
text_flags.needs_recompute = false;
}
@ -239,7 +264,6 @@ pub fn text_system(
fonts: Res<Assets<Font>>,
camera_query: Query<(Entity, &Camera)>,
default_ui_camera: DefaultUiCamera,
text_settings: Res<TextSettings>,
ui_scale: Res<UiScale>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_sets: ResMut<FontAtlasSets>,
@ -250,11 +274,12 @@ pub fn text_system(
&mut TextLayoutInfo,
&mut TextFlags,
Option<&TargetCamera>,
&mut CosmicBuffer,
)>,
) {
let mut scale_factors: EntityHashMap<f32> = EntityHashMap::default();
for (node, text, text_layout_info, text_flags, camera) in &mut text_query {
for (node, text, text_layout_info, text_flags, camera, mut buffer) in &mut text_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
@ -282,13 +307,13 @@ pub fn text_system(
&mut font_atlas_sets,
&mut texture_atlases,
&mut textures,
&text_settings,
scale_factor,
inverse_scale_factor,
text,
node,
text_flags,
text_layout_info,
buffer.as_mut(),
);
}
}

View file

@ -78,7 +78,6 @@ The default feature set enables most of the expected features of a game engine,
|serialize|Enable serialization support through serde|
|shader_format_glsl|Enable support for shaders in GLSL|
|shader_format_spirv|Enable support for shaders in SPIR-V|
|subpixel_glyph_atlas|Enable rendering of font glyphs using subpixel accuracy|
|symphonia-aac|AAC audio format support (through symphonia)|
|symphonia-all|AAC, FLAC, MP3, MP4, OGG/VORBIS, and WAV audio formats support (through symphonia)|
|symphonia-flac|FLAC audio format support (through symphonia)|

View file

@ -9,7 +9,7 @@ use bevy::{
color::palettes::css::*,
prelude::*,
sprite::Anchor,
text::{BreakLineOn, Text2dBounds},
text::{BreakLineOn, TextBounds},
};
fn main() {
@ -97,10 +97,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
justify: JustifyText::Left,
linebreak_behavior: BreakLineOn::WordBoundary,
},
text_2d_bounds: Text2dBounds {
// Wrap text in the rectangle
size: box_size,
},
// Wrap text in the rectangle
text_2d_bounds: TextBounds::from(box_size),
// ensure the text is drawn on top of the box
transform: Transform::from_translation(Vec3::Z),
..default()
@ -129,10 +127,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
justify: JustifyText::Left,
linebreak_behavior: BreakLineOn::AnyCharacter,
},
text_2d_bounds: Text2dBounds {
// Wrap text in the rectangle
size: other_box_size,
},
// Wrap text in the rectangle
text_2d_bounds: TextBounds::from(other_box_size),
// ensure the text is drawn on top of the box
transform: Transform::from_translation(Vec3::Z),
..default()

View file

@ -411,27 +411,33 @@ fn update_color_grading_settings(
}
fn update_ui(
mut text: Query<&mut Text, Without<SceneNumber>>,
mut text_query: Query<&mut Text, Without<SceneNumber>>,
settings: Query<(&Tonemapping, &ColorGrading)>,
current_scene: Res<CurrentScene>,
selected_parameter: Res<SelectedParameter>,
mut hide_ui: Local<bool>,
keys: Res<ButtonInput<KeyCode>>,
) {
let (method, color_grading) = settings.single();
let method = *method;
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
if keys.just_pressed(KeyCode::KeyH) {
*hide_ui = !*hide_ui;
}
text.clear();
let old_text = &text_query.single().sections[0].value;
if *hide_ui {
if !old_text.is_empty() {
// single_mut() always triggers change detection,
// so only access if text actually needs changing
text_query.single_mut().sections[0].value.clear();
}
return;
}
let (method, color_grading) = settings.single();
let method = *method;
let mut text = String::with_capacity(old_text.len());
let scn = current_scene.0;
text.push_str("(H) Hide UI\n\n");
text.push_str("Test Scene: \n");
@ -535,6 +541,12 @@ fn update_ui(
if current_scene.0 == 1 {
text.push_str("(Enter) Reset all to scene recommendation\n");
}
if text != old_text.as_str() {
// single_mut() always triggers change detection,
// so only access if text actually changed
text_query.single_mut().sections[0].value = text;
}
}
// ----------------------------------------------------------------------------

View file

@ -9,7 +9,7 @@ use bevy::{
color::palettes::basic::RED,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
text::{BreakLineOn, Text2dBounds},
text::{BreakLineOn, TextBounds},
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
@ -83,9 +83,7 @@ fn setup(mut commands: Commands) {
commands.spawn(Text2dBundle {
text,
text_anchor: bevy::sprite::Anchor::Center,
text_2d_bounds: Text2dBounds {
size: Vec2::new(1000., f32::INFINITY),
},
text_2d_bounds: TextBounds::new_horizontal(1000.),
..Default::default()
});
}

View file

@ -6,7 +6,7 @@ use bevy::{
color::palettes::basic::{BLUE, YELLOW},
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
text::{BreakLineOn, Text2dBounds},
text::{BreakLineOn, TextBounds},
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
@ -72,9 +72,9 @@ fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
}
// changing the bounds of the text will cause a recomputation
fn update_text_bounds(time: Res<Time>, mut text_bounds_query: Query<&mut Text2dBounds>) {
fn update_text_bounds(time: Res<Time>, mut text_bounds_query: Query<&mut TextBounds>) {
let width = (1. + time.elapsed_seconds().sin()) * 600.0;
for mut text_bounds in text_bounds_query.iter_mut() {
text_bounds.size.x = width;
text_bounds.width = Some(width);
}
}

View file

@ -37,7 +37,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
TextStyle {
// This font is loaded and will be used instead of the default font.
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 100.0,
font_size: 80.0,
..default()
},
) // Set the justification of the Text
@ -61,13 +61,13 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
TextStyle {
// This font is loaded and will be used instead of the default font.
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0,
font_size: 50.0,
..default()
},
),
TextSection::from_style(if cfg!(feature = "default_font") {
TextStyle {
font_size: 60.0,
font_size: 40.0,
color: GOLD.into(),
// If no font is specified, the default font (a minimal subset of FiraMono) will be used.
..default()
@ -76,7 +76,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// "default_font" feature is unavailable, load a font to use instead.
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 60.0,
font_size: 40.0,
color: GOLD.into(),
}
}),

View file

@ -135,6 +135,22 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
}),
);
builder.spawn(
TextBundle::from_section(
"This text is fully justified and is positioned in the same way.",
TextStyle {
font: font.clone(),
font_size: 35.0,
color: GREEN_YELLOW.into(),
},
)
.with_text_justify(JustifyText::Justified)
.with_style(Style {
max_width: Val::Px(300.),
..default()
}),
);
builder.spawn((
TextBundle::from_sections([
TextSection::new(
@ -145,6 +161,14 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
),
TextSection::new(
" this text has zero fontsize",
TextStyle {
font: font.clone(),
font_size: 0.0,
color: BLUE.into(),
},
),
TextSection::new(
"\nThis text changes in the bottom right - ",
TextStyle {
@ -162,7 +186,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
" fps, ",
TextStyle {
font: font.clone(),
font_size: 25.0,
font_size: 12.0,
color: YELLOW.into(),
},
),
@ -175,7 +199,15 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
" ms/frame",
TextStyle {
font: font.clone(),
font_size: 25.0,
font_size: 50.0,
color: BLUE.into(),
},
),
TextSection::new(
" this text has negative fontsize",
TextStyle {
font: font.clone(),
font_size: -50.0,
color: BLUE.into(),
},
),
@ -216,8 +248,8 @@ fn change_text_system(
"This text changes in the bottom right - {fps:.1} fps, {frame_time:.3} ms/frame",
);
text.sections[2].value = format!("{fps:.1}");
text.sections[3].value = format!("{fps:.1}");
text.sections[4].value = format!("{frame_time:.3}");
text.sections[5].value = format!("{frame_time:.3}");
}
}

View file

@ -70,6 +70,7 @@ fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
for linebreak_behavior in [
BreakLineOn::AnyCharacter,
BreakLineOn::WordBoundary,
BreakLineOn::WordOrCharacter,
BreakLineOn::NoWrap,
] {
let row_id = commands
@ -115,8 +116,9 @@ fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
let messages = [
format!("JustifyContent::{justification:?}"),
format!("LineBreakOn::{linebreak_behavior:?}"),
"Line 1\nLine 2\nLine 3".to_string(),
"Line 1\nLine 2".to_string(),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor, nunc ac faucibus fringilla.".to_string(),
"pneumonoultramicroscopicsilicovolcanoconiosis".to_string()
];
for (j, message) in messages.into_iter().enumerate() {

View file

@ -1,16 +1,12 @@
//! This example illustrates the [`UiScale`] resource from `bevy_ui`.
use bevy::{color::palettes::css::*, prelude::*, text::TextSettings, utils::Duration};
use bevy::{color::palettes::css::*, prelude::*, utils::Duration};
const SCALE_TIME: u64 = 400;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(TextSettings {
allow_dynamic_font_size: true,
..default()
})
.insert_resource(TargetScale {
start_scale: 1.0,
target_scale: 1.0,