From c2b47f7a46895ecfa63c4935d62bc82e08f468cf Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 30 Apr 2023 01:52:29 +0800 Subject: [PATCH 01/32] make text update lazier in tonemapping example --- examples/3d/tonemapping.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 9ae3c84dc8fc6..f34fbac89f40f 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -492,9 +492,16 @@ fn update_ui( } text.clear(); if *hide_ui { + if !text_query.single().sections[0].value.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 mut text = String::with_capacity(text_query.single().sections[0].value.len()); + let scn = current_scene.0; text.push_str("(H) Hide UI\n\n"); text.push_str("Test Scene: \n"); @@ -598,6 +605,12 @@ fn update_ui( if current_scene.0 == 1 { text.push_str("(Enter) Reset all to scene recommendation\n"); } + + if text != text_query.single().sections[0].value { + // single_mut() always triggers change detection, + // so only access if text actually changed + text_query.single_mut().sections[0].value = text; + } } // ---------------------------------------------------------------------------- From c6c49aadb72805e7b2e32486d74638c415ac93f6 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 2 May 2023 23:05:45 +0800 Subject: [PATCH 02/32] implemented cosmic_text side-by-side --- crates/bevy_text/Cargo.toml | 3 + crates/bevy_text/src/error.rs | 4 +- crates/bevy_text/src/font.rs | 58 +++- crates/bevy_text/src/font_atlas.rs | 49 +++- crates/bevy_text/src/font_atlas_set.rs | 153 ++++++++++- crates/bevy_text/src/glyph_brush.rs | 14 +- crates/bevy_text/src/pipeline.rs | 363 ++++++++++++++++++++++++- crates/bevy_text/src/text2d.rs | 3 + crates/bevy_ui/src/widget/text.rs | 6 + 9 files changed, 621 insertions(+), 32 deletions(-) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 7e17ab8c5640e..a725a4d5a1972 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -28,6 +28,9 @@ bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } # other anyhow = "1.0.4" ab_glyph = "0.2.6" +cosmic-text = "0.8.0" glyph_brush_layout = "0.2.1" thiserror = "1.0" serde = {version = "1", features = ["derive"]} +sys-locale = "0.3.0" +unicode-bidi = "0.3.13" diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 1bb7cf1253581..0367bdbb415e6 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -6,5 +6,7 @@ pub enum TextError { #[error("font not found")] NoSuchFont, #[error("failed to add glyph to newly-created atlas {0:?}")] - FailedToAddGlyph(GlyphId), + FailedToAddGlyphOld(GlyphId), + #[error("failed to add glyph to newly-created atlas {0:?}")] + FailedToAddGlyph(u16), } diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 1d8a465a76ea2..322cc64c4c8b9 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -9,13 +9,67 @@ use bevy_render::{ #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] pub struct Font { pub font: FontArc, + pub data: std::sync::Arc>, } impl Font { pub fn try_from_bytes(font_data: Vec) -> Result { - let font = FontVec::try_from_vec(font_data)?; + // DEBUGGING: + // eprintln!("loading font {}", font_data.len()); + let font = FontVec::try_from_vec(font_data.clone())?; let font = FontArc::new(font); - Ok(Font { font }) + // TODO: validate font + Ok(Font { + font, + data: std::sync::Arc::new(font_data), + }) + } + + // TODO: consider moving to pipeline.rs + pub fn get_outlined_glyph_texture_new( + font_system: &mut cosmic_text::FontSystem, + swash_cache: &mut cosmic_text::SwashCache, + layout_glyph: &cosmic_text::LayoutGlyph, + ) -> (Image, i32, i32, u32, u32) { + // TODO: consider using cosmic_text's own caching mechanism + let image = swash_cache + .get_image_uncached(font_system, layout_glyph.cache_key) + // TODO: don't unwrap + .unwrap(); + + let width = image.placement.width; + let height = image.placement.height; + + 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 + todo!() + } + }; + + // TODO: make this texture grayscale + ( + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + data, + TextureFormat::Rgba8UnormSrgb, + ), + image.placement.left, + image.placement.top, + width, + height, + ) } pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image { diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index be1903c121b36..fb6dff183af35 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -41,7 +41,8 @@ impl From for SubpixelOffset { pub struct FontAtlas { pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - pub glyph_to_atlas_index: HashMap<(GlyphId, SubpixelOffset), usize>, + pub glyph_to_atlas_index_old: HashMap<(GlyphId, SubpixelOffset), usize>, + pub glyph_to_atlas_index_new: HashMap, pub texture_atlas: Handle, } @@ -64,27 +65,63 @@ impl FontAtlas { let texture_atlas = TextureAtlas::new_empty(atlas_texture, size); Self { texture_atlas: texture_atlases.add(texture_atlas), - glyph_to_atlas_index: HashMap::default(), + glyph_to_atlas_index_old: HashMap::default(), + glyph_to_atlas_index_new: HashMap::default(), dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1), } } + pub fn get_glyph_index_new( + &self, + cache_key: cosmic_text::CacheKey, + ) -> Option<(usize, i32, i32, u32, u32)> { + self.glyph_to_atlas_index_new.get(&cache_key).copied() + } + pub fn get_glyph_index( &self, glyph_id: GlyphId, subpixel_offset: SubpixelOffset, ) -> Option { - self.glyph_to_atlas_index + self.glyph_to_atlas_index_old .get(&(glyph_id, subpixel_offset)) .copied() } + pub fn has_glyph_new(&self, cache_key: cosmic_text::CacheKey) -> bool { + self.glyph_to_atlas_index_new.contains_key(&cache_key) + } + pub fn has_glyph(&self, glyph_id: GlyphId, subpixel_offset: SubpixelOffset) -> bool { - self.glyph_to_atlas_index + self.glyph_to_atlas_index_old .contains_key(&(glyph_id, subpixel_offset)) } - pub fn add_glyph( + pub fn add_glyph_new( + &mut self, + textures: &mut Assets, + texture_atlases: &mut Assets, + cache_key: cosmic_text::CacheKey, + texture: &Image, + left: i32, + top: i32, + width: u32, + height: u32, + ) -> bool { + let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); + if let Some(index) = + self.dynamic_texture_atlas_builder + .add_texture(texture_atlas, textures, texture) + { + self.glyph_to_atlas_index_new + .insert(cache_key, (index, left, top, width, height)); + true + } else { + false + } + } + + pub fn add_glyph_old( &mut self, textures: &mut Assets, texture_atlases: &mut Assets, @@ -97,7 +134,7 @@ impl FontAtlas { self.dynamic_texture_atlas_builder .add_texture(texture_atlas, textures, texture) { - self.glyph_to_atlas_index + self.glyph_to_atlas_index_old .insert((glyph_id, subpixel_offset), index); true } else { diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index f28b4138ad8d0..49915b71b9426 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -9,14 +9,28 @@ use bevy_sprite::TextureAtlas; use bevy_utils::FloatOrd; use bevy_utils::HashMap; -type FontSizeKey = FloatOrd; +type FontSizeKeyOld = FloatOrd; +type FontSizeKey = u32; +// TODO: FontAtlasSet is an asset tied to a Handle cast weakly +// This won't work for "font queries" (non-vendored fonts) #[derive(TypeUuid, TypePath)] #[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] pub struct FontAtlasSet { + font_atlases_old: HashMap>, font_atlases: HashMap>, } +#[derive(Debug, Clone)] +pub struct GlyphAtlasInfoNew { + pub texture_atlas: Handle, + pub glyph_index: usize, + pub left: i32, + pub top: i32, + pub width: u32, + pub height: u32, +} + #[derive(Debug, Clone)] pub struct GlyphAtlasInfo { pub texture_atlas: Handle, @@ -26,18 +40,21 @@ pub struct GlyphAtlasInfo { impl Default for FontAtlasSet { fn default() -> Self { FontAtlasSet { + font_atlases_old: HashMap::with_capacity_and_hasher(1, Default::default()), font_atlases: HashMap::with_capacity_and_hasher(1, Default::default()), } } } impl FontAtlasSet { - pub fn iter(&self) -> impl Iterator)> { - self.font_atlases.iter() + // TODO: update for cosmic_text + pub fn iter(&self) -> impl Iterator)> { + self.font_atlases_old.iter() } + // TODO: update for cosmic_text pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool { - self.font_atlases + self.font_atlases_old .get(&FloatOrd(font_size)) .map_or(false, |font_atlas| { font_atlas @@ -46,7 +63,77 @@ impl FontAtlasSet { }) } - pub fn add_glyph_to_atlas( + pub fn add_glyph_to_atlas_new( + &mut self, + texture_atlases: &mut Assets, + textures: &mut Assets, + font_system: &mut cosmic_text::FontSystem, + swash_cache: &mut cosmic_text::SwashCache, + layout_glyph: &cosmic_text::LayoutGlyph, + ) -> Result { + // let glyph = layout_glyph.glyph(); + // let glyph_id = glyph.id; + // let glyph_position = glyph.position; + // let font_size = glyph.scale.y; + let font_atlases = self + .font_atlases + .entry(layout_glyph.cache_key.font_size_bits) + .or_insert_with(|| { + vec![FontAtlas::new( + textures, + texture_atlases, + Vec2::splat(512.0), + )] + }); + + let (glyph_texture, left, top, w, h) = + Font::get_outlined_glyph_texture_new(font_system, swash_cache, layout_glyph); + let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { + atlas.add_glyph_new( + textures, + texture_atlases, + layout_glyph.cache_key, + &glyph_texture, + left, + top, + w, + h, + ) + }; + if !font_atlases.iter_mut().any(add_char_to_font_atlas) { + // Find the largest dimension of the glyph, either its width or its height + let glyph_max_size: u32 = glyph_texture + .texture_descriptor + .size + .height + .max(glyph_texture.texture_descriptor.size.width); + // Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size + let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512) as f32; + font_atlases.push(FontAtlas::new( + textures, + texture_atlases, + Vec2::new(containing, containing), + )); + if !font_atlases.last_mut().unwrap().add_glyph_new( + textures, + texture_atlases, + layout_glyph.cache_key, + &glyph_texture, + left, + top, + w, + h, + ) { + return Err(TextError::FailedToAddGlyph(layout_glyph.cache_key.glyph_id)); + } + } + + Ok(self + .get_glyph_atlas_info_new(layout_glyph.cache_key) + .unwrap()) + } + + pub fn add_glyph_to_atlas_old( &mut self, texture_atlases: &mut Assets, textures: &mut Assets, @@ -57,7 +144,7 @@ impl FontAtlasSet { let glyph_position = glyph.position; let font_size = glyph.scale.y; let font_atlases = self - .font_atlases + .font_atlases_old .entry(FloatOrd(font_size)) .or_insert_with(|| { vec![FontAtlas::new( @@ -69,7 +156,7 @@ impl FontAtlasSet { let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph); let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { - atlas.add_glyph( + atlas.add_glyph_old( textures, texture_atlases, glyph_id, @@ -91,29 +178,56 @@ impl FontAtlasSet { texture_atlases, Vec2::new(containing, containing), )); - if !font_atlases.last_mut().unwrap().add_glyph( + if !font_atlases.last_mut().unwrap().add_glyph_old( textures, texture_atlases, glyph_id, glyph_position.into(), &glyph_texture, ) { - return Err(TextError::FailedToAddGlyph(glyph_id)); + return Err(TextError::FailedToAddGlyphOld(glyph_id)); } } Ok(self - .get_glyph_atlas_info(font_size, glyph_id, glyph_position) + .get_glyph_atlas_info_old(font_size, glyph_id, glyph_position) .unwrap()) } - pub fn get_glyph_atlas_info( + pub fn get_glyph_atlas_info_new( + &mut self, + cache_key: cosmic_text::CacheKey, + ) -> Option { + self.font_atlases + .get(&cache_key.font_size_bits) + .and_then(|font_atlases| { + font_atlases + .iter() + .find_map(|atlas| { + atlas + .get_glyph_index_new(cache_key) + .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) + }) + .map( + |((glyph_index, left, top, w, h), texture_atlas)| GlyphAtlasInfoNew { + texture_atlas, + glyph_index, + left, + top, + width: w, + height: h, + }, + ) + }) + } + + pub fn get_glyph_atlas_info_old( &mut self, font_size: f32, glyph_id: GlyphId, position: Point, ) -> Option { - self.font_atlases + self.font_atlases_old .get(&FloatOrd(font_size)) .and_then(|font_atlases| { font_atlases @@ -131,6 +245,19 @@ impl FontAtlasSet { } pub fn num_font_atlases(&self) -> usize { - self.font_atlases.len() + self.font_atlases_old.len() } } + +#[derive(Debug, Clone)] +pub struct PositionedGlyph { + pub position: Vec2, + pub size: Vec2, + pub atlas_info: GlyphAtlasInfoNew, + pub section_index: usize, + /// 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. + // TODO: re-implement this or equivalent + pub byte_index: usize, +} diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index 9b17f09a7b910..559caf3fd743d 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -64,7 +64,7 @@ impl GlyphBrush { text_settings: &TextSettings, font_atlas_warning: &mut FontAtlasWarning, y_axis_orientation: YAxisOrientation, - ) -> Result, TextError> { + ) -> Result, TextError> { if glyphs.is_empty() { return Ok(Vec::new()); } @@ -118,10 +118,14 @@ impl GlyphBrush { .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); let atlas_info = font_atlas_set - .get_glyph_atlas_info(section_data.2, glyph_id, glyph_position) + .get_glyph_atlas_info_old(section_data.2, glyph_id, glyph_position) .map(Ok) .unwrap_or_else(|| { - font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph) + font_atlas_set.add_glyph_to_atlas_old( + texture_atlases, + textures, + outlined_glyph, + ) })?; if !text_settings.allow_dynamic_font_size @@ -145,7 +149,7 @@ impl GlyphBrush { let position = adjust.position(Vec2::new(x, y)); - positioned_glyphs.push(PositionedGlyph { + positioned_glyphs.push(PositionedGlyphOld { position, size, atlas_info, @@ -167,7 +171,7 @@ impl GlyphBrush { } #[derive(Debug, Clone)] -pub struct PositionedGlyph { +pub struct PositionedGlyphOld { pub position: Vec2, pub size: Vec2, pub atlas_info: GlyphAtlasInfo, diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index e71e49c030e67..b3970411c0bd9 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -5,19 +5,79 @@ use bevy_ecs::system::Resource; use bevy_math::Vec2; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; -use bevy_utils::HashMap; +use bevy_utils::{tracing::warn, HashMap}; +use cosmic_text::{Attrs, AttrsList, Buffer, BufferLine, Family, Metrics, Wrap}; use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText}; use crate::{ - error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, - FontAtlasWarning, PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation, + error::TextError, font, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, + FontAtlasWarning, PositionedGlyph, PositionedGlyphOld, TextAlignment, TextSection, + TextSettings, YAxisOrientation, }; +pub struct FontSystem(cosmic_text::FontSystem); + +impl Default for FontSystem { + 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)) + } +} + +impl FontSystem { + /// Attempts to load system fonts. + /// + /// Supports Windows, Linux and macOS. + /// + /// System fonts loading is a surprisingly complicated task, + /// mostly unsolvable without interacting with system libraries. + /// And since `fontdb` tries to be small and portable, this method + /// will simply scan some predefined directories. + /// Which means that fonts that are not in those directories must + /// be added manually. + /// + /// This allows access to any installed system fonts + /// + /// # Timing + /// + /// This function takes some time to run. On the release build, it can take up to a second, + /// while debug builds can take up to ten times longer. For this reason, it should only be + /// called once, and the resulting [`FontSystem`] should be shared. + /// + /// This should ideally run in a background thread. + // TODO: This should run in a background thread. + pub fn load_system_fonts(&mut self) { + self.0.db_mut().load_system_fonts(); + } +} + +pub struct SwashCache(cosmic_text::SwashCache); + +impl Default for SwashCache { + fn default() -> Self { + Self(cosmic_text::SwashCache::new()) + } +} + #[derive(Default, Resource)] pub struct TextPipeline { brush: GlyphBrush, map_font_id: HashMap, + map_handle_to_font_id: HashMap, + font_system: FontSystem, + swash_cache: SwashCache, +} + +/// 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)] +pub struct TextLayoutInfoOld { + pub glyphs: Vec, + pub size: Vec2, } /// Render information for a corresponding [`Text`](crate::Text) component. @@ -42,8 +102,10 @@ impl TextPipeline { pub fn queue_text( &mut self, fonts: &Assets, + // TODO: TextSection should support referencing fonts via "Font Query" (Family, Stretch, Weight and Style) sections: &[TextSection], scale_factor: f64, + // TODO: Implement text alignment text_alignment: TextAlignment, linebreak_behavior: BreakLineOn, bounds: Vec2, @@ -54,6 +116,245 @@ impl TextPipeline { font_atlas_warning: &mut FontAtlasWarning, y_axis_orientation: YAxisOrientation, ) -> Result { + if sections.is_empty() { + return Ok(TextLayoutInfo::default()); + } + + // TODO: Support loading fonts without cloning the already loaded data (they are cloned in `add_span`) + let font_system = &mut self.font_system.0; + let swash_cache = &mut self.swash_cache.0; + + // TODO: Support multiple section font sizes, pending upstream implementation in cosmic_text + // For now, just use the first section's size or a default + let font_size = sections + .get(0) + .map(|s| s.style.font_size) + .unwrap_or_else(|| crate::TextStyle::default().font_size) + as f64 + * scale_factor; + // TODO: Support line height as an option. Unitless `1.2` is the default used in browsers (1.2x font size). + let line_height = font_size * 1.2; + let (font_size, line_height) = (font_size as f32, line_height as f32); + let metrics = Metrics::new(font_size, line_height); + + // TODO: cache buffers (see Iced / glyphon) + let mut buffer = Buffer::new(font_system, metrics); + + let mut buffer_lines = Vec::new(); + let mut attrs_list = AttrsList::new(Attrs::new()); + let mut line_text = String::new(); + // all sections need to be combined and broken up into lines + // e.g. + // style0"Lorem ipsum\ndolor sit amet," + // style1" consectetur adipiscing\nelit," + // style2" sed do eiusmod tempor\nincididunt" + // style3" ut labore et dolore\nmagna aliqua." + // becomes: + // line0: style0"Lorem ipsum" + // line1: style0"dolor sit amet," + // style1" consectetur adipiscing," + // line2: style1"elit," + // style2" sed do eiusmod tempor" + // line3: style2"incididunt" + // style3"ut labore et dolore" + // line4: style3"magna aliqua." + for (section_index, section) in sections.iter().enumerate() { + // can't simply use `let mut lines = section.value.lines()` because + // unicode-bidi used by cosmic text doesn't have the same newline behaviour. + // when using exotic chars, such as in example font_atlas_debug, there is a panic in shaping + let bidi = unicode_bidi::BidiInfo::new(§ion.value, None); + let mut lines = bidi.paragraphs.into_iter().map(|para| { + // `para.range` includes the newline at the end, and in an unusual way. + // we can remove it by taking the first line + section.value[para.range].lines().next().unwrap() + }); + + // continue the current line, adding spans + if let Some(line) = lines.next() { + add_span( + &mut line_text, + &mut attrs_list, + section, + section_index, + line, + font_system, + &mut self.map_handle_to_font_id, + fonts, + ); + } + // for any remaining lines in this section + for line in lines { + // finalise this line and start a new line + let prev_attrs_list = + std::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); + let prev_line_text = std::mem::take(&mut line_text); + buffer_lines.push(BufferLine::new(prev_line_text, prev_attrs_list)); + add_span( + &mut line_text, + &mut attrs_list, + section, + section_index, + line, + font_system, + &mut self.map_handle_to_font_id, + fonts, + ); + } + } + // finalise last line + buffer_lines.push(BufferLine::new(line_text, attrs_list)); + + // node size (bounds) is already scaled by the systems that call queue_text + let buffer_height = bounds.y; + buffer.set_size(font_system, bounds.x, buffer_height); + buffer.lines = buffer_lines; + + buffer.set_wrap( + font_system, + match linebreak_behavior { + BreakLineOn::WordBoundary => Wrap::Word, + BreakLineOn::AnyCharacter => Wrap::Glyph, + }, + ); + + // TODO: other shaping methods? + buffer.shape_until_scroll(font_system); + + if buffer.visible_lines() == 0 { + // Presumably the font(s) are not available yet + return Err(TextError::NoSuchFont); + } + + // DEBUGGING: + // let a = buffer.lines.iter().map(|l| l.text()).collect::(); + // let b = sections + // .iter() + // .map(|s| s.value.lines().collect::()) + // .collect::(); + // println!(); + // dbg!(a, b); + + // TODO: check height and width logic + let width = buffer + .layout_runs() + .map(|run| run.line_w) + .reduce(|max_w, w| max_w.max(w)) + .map(|max_w| max_w as u32) + .unwrap(); + let height = (buffer.layout_runs().count() as f32 * line_height) + .ceil() + .min(bounds.y) as u32; + let size = Vec2::new(width as f32, height as f32); + + let glyphs = buffer + .layout_runs() + .flat_map(|run| { + run.glyphs + .iter() + .map(move |g| (g, run.line_i, run.line_w, run.line_y, run.rtl, run.text)) + }) + .collect::>(); + + if glyphs.is_empty() { + return Ok(TextLayoutInfo::default()); + } + + // DEBUGGING: + // for sg in glyphs.iter() { + // let (glyph, line_i, line_w, line_y, rtl, text) = sg; + // dbg!(*line_i as i32 * line_height as i32 + glyph.y_int); + // } + + // DEBUGGING: + // dbg!(glyphs.first().unwrap()); + + let glyphs_results = glyphs + .iter() + .map(|(layout_glyph, line_i, line_w, line_y, rtl, text)| { + let section_index = layout_glyph.metadata; + + let handle_font_atlas: Handle = sections[section_index].style.font.cast_weak(); + let font_atlas_set = font_atlas_set_storage + .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); + + let mut cache_key = layout_glyph.cache_key; + let position_x = cache_key.x_bin.as_float(); + let position_y = cache_key.y_bin.as_float(); + let (position_x, subpixel_x) = cosmic_text::SubpixelBin::new(position_x); + let (position_y, subpixel_y) = cosmic_text::SubpixelBin::new(position_y); + cache_key.x_bin = subpixel_x; + cache_key.y_bin = subpixel_y; + assert_eq!(layout_glyph.cache_key, cache_key); + assert_eq!([position_x, position_y], [0, 0]); + + let atlas_info = font_atlas_set + .get_glyph_atlas_info_new(layout_glyph.cache_key) + .map(Ok) + .unwrap_or_else(|| { + font_atlas_set.add_glyph_to_atlas_new(texture_atlases, textures, font_system, swash_cache, layout_glyph) + })?; + + if !text_settings.allow_dynamic_font_size + && !font_atlas_warning.warned + && font_atlas_set.num_font_atlases() > text_settings.max_font_atlases.get() + { + warn!("warning[B0005]: Number of font atlases has exceeded the maximum of {}. Performance and memory usage may suffer.", text_settings.max_font_atlases.get()); + font_atlas_warning.warned = true; + } + + let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); + let glyph_rect = texture_atlas.textures[atlas_info.glyph_index]; + let left = atlas_info.left as f32; + let top = atlas_info.top as f32; + let size = Vec2::new(glyph_rect.width(), glyph_rect.height()); + assert_eq!(atlas_info.width as f32, size.x); + assert_eq!(atlas_info.height as f32, size.y); + + // offset by half the size because the origin is center + let x = size.x / 2.0 + left + layout_glyph.x_int as f32; + let y = *line_y + layout_glyph.y_int as f32 - top + size.y / 2.0; + let y = match y_axis_orientation { + YAxisOrientation::TopToBottom => y, + YAxisOrientation::BottomToTop => height as f32 - y, + }; + + let position = Vec2::new(x, y); + + let pos_glyph = PositionedGlyph { + position, + size, + atlas_info, + section_index, + // TODO: recreate the byte index, relevant for #1319 + byte_index: 0, + }; + Ok::<_, TextError>(pos_glyph) + }); + + let mut glyphs = Vec::with_capacity(glyphs_results.len()); + for glyph_result in glyphs_results { + glyphs.push(glyph_result?); + } + + Ok(TextLayoutInfo { glyphs, size }) + } + + #[allow(clippy::too_many_arguments)] + pub fn queue_text_old( + &mut self, + fonts: &Assets, + sections: &[TextSection], + scale_factor: f64, + text_alignment: TextAlignment, + linebreak_behavior: BreakLineOn, + bounds: Vec2, + font_atlas_set_storage: &mut Assets, + texture_atlases: &mut Assets, + textures: &mut Assets, + text_settings: &TextSettings, + font_atlas_warning: &mut FontAtlasWarning, + y_axis_orientation: YAxisOrientation, + ) -> Result { let mut scaled_fonts = Vec::with_capacity(sections.len()); let sections = sections .iter() @@ -81,7 +382,7 @@ impl TextPipeline { .compute_glyphs(§ions, bounds, text_alignment, linebreak_behavior)?; if section_glyphs.is_empty() { - return Ok(TextLayoutInfo::default()); + return Ok(TextLayoutInfoOld::default()); } let mut min_x: f32 = std::f32::MAX; @@ -115,7 +416,7 @@ impl TextPipeline { y_axis_orientation, )?; - Ok(TextLayoutInfo { glyphs, size }) + Ok(TextLayoutInfoOld { glyphs, size }) } pub fn create_text_measure( @@ -254,3 +555,55 @@ impl TextMeasureInfo { self.compute_size_from_section_texts(§ions, bounds) } } + +/// Adds a span to the attributes list, +/// loading fonts into the DB if required. +#[allow(clippy::too_many_arguments)] +fn add_span( + line_text: &mut String, + attrs_list: &mut AttrsList, + section: &TextSection, + section_index: usize, + line: &str, + font_system: &mut cosmic_text::FontSystem, + map_handle_to_font_id: &mut HashMap, + fonts: &Assets, +) { + let start = line_text.len(); + line_text.push_str(line); + let end = line_text.len(); + + let font_handle = §ion.style.font; + let font_handle_id = font_handle.id(); + let face_id = map_handle_to_font_id + .entry(font_handle_id) + .or_insert_with(|| { + let font = fonts.get(font_handle).unwrap(); + font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(font.data.clone())); + // TODO: validate this is the right font face. alternatively, + // 1. parse the face info using ttf-parser + // 2. push this face info into the db + // 3. query the db from the same face info we just pushed in to get the id + let face_id = font_system.db().faces().last().unwrap().id; + // let font = font_system.get_font(face_id).unwrap(); + // map_font_id_to_metrics + // .entry(face_id) + // .or_insert_with(|| font.as_swash().metrics(&[])); + face_id + }); + let face = font_system.db().face(*face_id).unwrap(); + + // TODO: validate this is the correct string to extract + let family_name = &face.families[0].0; + let attrs = Attrs::new() + // TODO: validate that we can use metadata + .metadata(section_index) + .family(Family::Name(family_name)) + .stretch(face.stretch) + .style(face.style) + .weight(face.weight) + .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())); + attrs_list.add_span(start..end, attrs); +} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index a17a1c670950e..c28237112c394 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -197,6 +197,9 @@ pub fn update_text2d_layout( // queue for further processing queue.insert(entity); } + Err(e @ TextError::FailedToAddGlyphOld(_)) => { + panic!("Fatal error when processing text: {e}."); + } Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c56ae98bc5997..12be382e7bdf8 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -104,6 +104,9 @@ fn create_text_measure( Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } + Err(e @ TextError::FailedToAddGlyphOld(_)) => { + panic!("Fatal error when processing text: {e}."); + } }; } @@ -197,6 +200,9 @@ fn queue_text( Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } + Err(e @ TextError::FailedToAddGlyphOld(_)) => { + panic!("Fatal error when processing text: {e}."); + } Ok(info) => { *text_layout_info = info; text_flags.needs_recompute = false; From 643fc6a72f93994ab8b88a0c5ca948a3b2729297 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 8 May 2023 17:21:48 +0800 Subject: [PATCH 03/32] make text update lazier in tonemapping ex. try 2 --- examples/3d/tonemapping.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index f34fbac89f40f..9af1d4952ed57 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -474,25 +474,21 @@ fn update_color_grading_settings( } fn update_ui( - mut text: Query<&mut Text, Without>, + mut text_query: Query<&mut Text, Without>, settings: Query<(&Tonemapping, &ColorGrading)>, current_scene: Res, selected_parameter: Res, mut hide_ui: Local, keys: Res>, ) { - 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::H) { *hide_ui = !*hide_ui; } - text.clear(); + + let old_text = &text_query.single().sections[0].value; + if *hide_ui { - if !text_query.single().sections[0].value.is_empty() { + 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(); @@ -500,7 +496,10 @@ fn update_ui( return; } - let mut text = String::with_capacity(text_query.single().sections[0].value.len()); + 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"); @@ -606,7 +605,7 @@ fn update_ui( text.push_str("(Enter) Reset all to scene recommendation\n"); } - if text != text_query.single().sections[0].value { + 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; From 9118acc0d36a4fbf81895625544153aa2f995d32 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 8 May 2023 18:50:08 +0800 Subject: [PATCH 04/32] introduce `BidiParagraphs` iterator --- crates/bevy_text/src/pipeline.rs | 50 ++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index b3970411c0bd9..a02b1ceab472f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -159,15 +159,10 @@ impl TextPipeline { // style3"ut labore et dolore" // line4: style3"magna aliqua." for (section_index, section) in sections.iter().enumerate() { - // can't simply use `let mut lines = section.value.lines()` because - // unicode-bidi used by cosmic text doesn't have the same newline behaviour. - // when using exotic chars, such as in example font_atlas_debug, there is a panic in shaping - let bidi = unicode_bidi::BidiInfo::new(§ion.value, None); - let mut lines = bidi.paragraphs.into_iter().map(|para| { - // `para.range` includes the newline at the end, and in an unusual way. - // we can remove it by taking the first line - section.value[para.range].lines().next().unwrap() - }); + // We can't simply use `let mut lines = section.value.lines()` because + // `unicode-bidi` used by `cosmic_text` doesn't have the same newline behaviour: it breaks on `\r` for example. + // In example `font_atlas_debug`, eventually a `\r` character is inserted and there is a panic in shaping. + let mut lines = BidiParagraphs::new(§ion.value); // continue the current line, adding spans if let Some(line) = lines.next() { @@ -607,3 +602,40 @@ fn add_span( .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())); attrs_list.add_span(start..end, attrs); } + +/// An iterator over the paragraphs in the input text. +/// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour. +// TODO: upstream to cosmic_text, see https://github.com/pop-os/cosmic-text/pull/124 +pub struct BidiParagraphs<'text> { + text: &'text str, + info: std::vec::IntoIter, +} + +impl<'text> BidiParagraphs<'text> { + /// Create an iterator to split the input text into paragraphs + /// in accordance with `unicode-bidi` behaviour. + pub fn new(text: &'text str) -> Self { + let info = unicode_bidi::BidiInfo::new(text, None); + let info = info.paragraphs.into_iter(); + Self { text, info } + } +} + +impl<'text> Iterator for BidiParagraphs<'text> { + type Item = &'text str; + + fn next(&mut self) -> Option { + let para = self.info.next()?; + let paragraph = &self.text[para.range]; + // `para.range` includes the newline that splits the line, so remove it if present + let mut char_indices = paragraph.char_indices(); + if let Some(i) = char_indices.next_back().and_then(|(i, c)| { + // `BidiClass::B` is a Paragraph_Separator (various newline characters) + (unicode_bidi::BidiClass::B == unicode_bidi::bidi_class(c)).then_some(i) + }) { + Some(¶graph[0..i]) + } else { + Some(paragraph) + } + } +} From fbba4c74362c9a9b8f269a28db7c07ca7ef9277d Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 12 May 2023 16:47:53 +0800 Subject: [PATCH 05/32] update `FontAtlasSet` `iter`, `has_glyph` methods --- crates/bevy_text/src/font_atlas_set.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 49915b71b9426..b9f7fc61d0660 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -47,15 +47,13 @@ impl Default for FontAtlasSet { } impl FontAtlasSet { - // TODO: update for cosmic_text - pub fn iter(&self) -> impl Iterator)> { - self.font_atlases_old.iter() + pub fn iter(&self) -> impl Iterator)> { + self.font_atlases.iter() } - // TODO: update for cosmic_text pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool { - self.font_atlases_old - .get(&FloatOrd(font_size)) + self.font_atlases + .get(&font_size.to_bits()) .map_or(false, |font_atlas| { font_atlas .iter() From 7036d0570cff2800c6da297842cc55f61b988371 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 12 May 2023 17:18:34 +0800 Subject: [PATCH 06/32] collect positioned glyphs rather than push vec --- crates/bevy_text/src/pipeline.rs | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index a02b1ceab472f..2bd7f8ec457d5 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -241,18 +241,11 @@ impl TextPipeline { .min(bounds.y) as u32; let size = Vec2::new(width as f32, height as f32); - let glyphs = buffer - .layout_runs() - .flat_map(|run| { - run.glyphs - .iter() - .map(move |g| (g, run.line_i, run.line_w, run.line_y, run.rtl, run.text)) - }) - .collect::>(); - - if glyphs.is_empty() { - return Ok(TextLayoutInfo::default()); - } + let glyphs = buffer.layout_runs().flat_map(|run| { + run.glyphs + .iter() + .map(move |g| (g, run.line_i, run.line_w, run.line_y, run.rtl, run.text)) + }); // DEBUGGING: // for sg in glyphs.iter() { @@ -263,8 +256,7 @@ impl TextPipeline { // DEBUGGING: // dbg!(glyphs.first().unwrap()); - let glyphs_results = glyphs - .iter() + let glyphs = glyphs .map(|(layout_glyph, line_i, line_w, line_y, rtl, text)| { let section_index = layout_glyph.metadata; @@ -307,7 +299,7 @@ impl TextPipeline { // offset by half the size because the origin is center let x = size.x / 2.0 + left + layout_glyph.x_int as f32; - let y = *line_y + layout_glyph.y_int as f32 - top + size.y / 2.0; + let y = line_y + layout_glyph.y_int as f32 - top + size.y / 2.0; let y = match y_axis_orientation { YAxisOrientation::TopToBottom => y, YAxisOrientation::BottomToTop => height as f32 - y, @@ -323,13 +315,8 @@ impl TextPipeline { // TODO: recreate the byte index, relevant for #1319 byte_index: 0, }; - Ok::<_, TextError>(pos_glyph) - }); - - let mut glyphs = Vec::with_capacity(glyphs_results.len()); - for glyph_result in glyphs_results { - glyphs.push(glyph_result?); - } + Ok(pos_glyph) + }).collect::, _>>()?; Ok(TextLayoutInfo { glyphs, size }) } From 6c5b506ef0ef9a0e67fd1cacef80ba1b2dff0caf Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 12 May 2023 17:23:12 +0800 Subject: [PATCH 07/32] remove unnecessary position calcs --- crates/bevy_text/src/pipeline.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 2bd7f8ec457d5..3600d72b50298 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -264,16 +264,6 @@ impl TextPipeline { let font_atlas_set = font_atlas_set_storage .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); - let mut cache_key = layout_glyph.cache_key; - let position_x = cache_key.x_bin.as_float(); - let position_y = cache_key.y_bin.as_float(); - let (position_x, subpixel_x) = cosmic_text::SubpixelBin::new(position_x); - let (position_y, subpixel_y) = cosmic_text::SubpixelBin::new(position_y); - cache_key.x_bin = subpixel_x; - cache_key.y_bin = subpixel_y; - assert_eq!(layout_glyph.cache_key, cache_key); - assert_eq!([position_x, position_y], [0, 0]); - let atlas_info = font_atlas_set .get_glyph_atlas_info_new(layout_glyph.cache_key) .map(Ok) From b759c85c7b0a6cfd69e06bae81c261e01b69f233 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 12 May 2023 17:27:56 +0800 Subject: [PATCH 08/32] implement horizontal text alignment --- crates/bevy_text/src/pipeline.rs | 62 ++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 3600d72b50298..b9390f77095a4 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -230,16 +230,25 @@ impl TextPipeline { // dbg!(a, b); // TODO: check height and width logic - let width = buffer - .layout_runs() - .map(|run| run.line_w) - .reduce(|max_w, w| max_w.max(w)) - .map(|max_w| max_w as u32) - .unwrap(); - let height = (buffer.layout_runs().count() as f32 * line_height) - .ceil() - .min(bounds.y) as u32; - let size = Vec2::new(width as f32, height as f32); + // TODO: move this to text measurement + let box_size = { + let width = buffer + .layout_runs() + .map(|run| run.line_w) + .reduce(|max_w, w| max_w.max(w)) + .unwrap(); + let height = buffer + .layout_runs() + .map(|run| run.glyphs) + .flat_map(|glyphs| { + glyphs + .iter() + .map(|g| f32::from_bits(g.cache_key.font_size_bits)) + .reduce(|max_h, w| max_h.max(w)) + }) + .sum::(); + Vec2::new(width, height) + }; let glyphs = buffer.layout_runs().flat_map(|run| { run.glyphs @@ -283,32 +292,47 @@ impl TextPipeline { let glyph_rect = texture_atlas.textures[atlas_info.glyph_index]; let left = atlas_info.left as f32; let top = atlas_info.top as f32; - let size = Vec2::new(glyph_rect.width(), glyph_rect.height()); - assert_eq!(atlas_info.width as f32, size.x); - assert_eq!(atlas_info.height as f32, size.y); + let glyph_size = Vec2::new(glyph_rect.width(), glyph_rect.height()); + assert_eq!(atlas_info.width as f32, glyph_size.x); + assert_eq!(atlas_info.height as f32, glyph_size.y); // offset by half the size because the origin is center - let x = size.x / 2.0 + left + layout_glyph.x_int as f32; - let y = line_y + layout_glyph.y_int as f32 - top + size.y / 2.0; + let x = glyph_size.x / 2.0 + left + layout_glyph.x_int as f32; + let y = line_y + layout_glyph.y_int as f32 - top + glyph_size.y / 2.0; + // TODO: cosmic text may handle text alignment in future + let x = x + match text_alignment { + TextAlignment::Left => 0.0, + TextAlignment::Center => (box_size.x - line_w) / 2.0, + TextAlignment::Right => box_size.x - line_w, + }; let y = match y_axis_orientation { YAxisOrientation::TopToBottom => y, - YAxisOrientation::BottomToTop => height as f32 - y, + YAxisOrientation::BottomToTop => box_size.y as f32 - y, }; + // TODO: confirm whether we need to offset by glyph baseline + // (this should be testable with a single line of text with + // fonts of different sizes and/or baselines) + let position = Vec2::new(x, y); let pos_glyph = PositionedGlyph { position, - size, + size: glyph_size, atlas_info, section_index, // TODO: recreate the byte index, relevant for #1319 + // alternatively, reimplement cosmic-text's own hit tests for text byte_index: 0, }; Ok(pos_glyph) - }).collect::, _>>()?; + }) + .collect::, _>>()?; - Ok(TextLayoutInfo { glyphs, size }) + Ok(TextLayoutInfo { + glyphs, + size: box_size, + }) } #[allow(clippy::too_many_arguments)] From aa37e0d7a1cf28a320ca134aab2676ad3ce5d57a Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 12 May 2023 17:31:15 +0800 Subject: [PATCH 09/32] split up `TextPipeline::queue_text` --- crates/bevy_text/src/pipeline.rs | 62 ++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index b9390f77095a4..4d312757eb6b0 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -82,7 +82,7 @@ pub struct TextLayoutInfoOld { /// Render information for a corresponding [`Text`](crate::Text) component. /// -/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`]. +/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`]. #[derive(Component, Clone, Default, Debug)] pub struct TextLayoutInfo { pub glyphs: Vec, @@ -98,32 +98,14 @@ impl TextPipeline { .or_insert_with(|| brush.add_font(handle.clone(), font.font.clone())) } - #[allow(clippy::too_many_arguments)] - pub fn queue_text( + pub fn create_buffer( &mut self, fonts: &Assets, - // TODO: TextSection should support referencing fonts via "Font Query" (Family, Stretch, Weight and Style) sections: &[TextSection], - scale_factor: f64, - // TODO: Implement text alignment - text_alignment: TextAlignment, linebreak_behavior: BreakLineOn, bounds: Vec2, - font_atlas_set_storage: &mut Assets, - texture_atlases: &mut Assets, - textures: &mut Assets, - text_settings: &TextSettings, - font_atlas_warning: &mut FontAtlasWarning, - y_axis_orientation: YAxisOrientation, - ) -> Result { - if sections.is_empty() { - return Ok(TextLayoutInfo::default()); - } - - // TODO: Support loading fonts without cloning the already loaded data (they are cloned in `add_span`) - let font_system = &mut self.font_system.0; - let swash_cache = &mut self.swash_cache.0; - + scale_factor: f64, + ) -> Result { // TODO: Support multiple section font sizes, pending upstream implementation in cosmic_text // For now, just use the first section's size or a default let font_size = sections @@ -137,6 +119,8 @@ impl TextPipeline { let (font_size, line_height) = (font_size as f32, line_height as f32); let metrics = Metrics::new(font_size, line_height); + let font_system = &mut self.font_system.0; + // TODO: cache buffers (see Iced / glyphon) let mut buffer = Buffer::new(font_system, metrics); @@ -220,6 +204,39 @@ impl TextPipeline { return Err(TextError::NoSuchFont); } + Ok(buffer) + } + + #[allow(clippy::too_many_arguments)] + pub fn queue_text( + &mut self, + fonts: &Assets, + // TODO: TextSection should support referencing fonts via "Font Query" (Family, Stretch, Weight and Style) + sections: &[TextSection], + scale_factor: f64, + // TODO: Implement text alignment + text_alignment: TextAlignment, + linebreak_behavior: BreakLineOn, + bounds: Vec2, + font_atlas_set_storage: &mut Assets, + texture_atlases: &mut Assets, + textures: &mut Assets, + text_settings: &TextSettings, + font_atlas_warning: &mut FontAtlasWarning, + y_axis_orientation: YAxisOrientation, + ) -> Result { + if sections.is_empty() { + return Ok(TextLayoutInfo::default()); + } + + // TODO: Support loading fonts without cloning the already loaded data (they are cloned in `add_span`) + + let buffer = + self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; + + let font_system = &mut self.font_system.0; + let swash_cache = &mut self.swash_cache.0; + // DEBUGGING: // let a = buffer.lines.iter().map(|l| l.text()).collect::(); // let b = sections @@ -607,6 +624,7 @@ fn add_span( /// An iterator over the paragraphs in the input text. /// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour. // TODO: upstream to cosmic_text, see https://github.com/pop-os/cosmic-text/pull/124 +// TODO: create separate iterator that keeps the ranges, or simply use memory address introspection (as_ptr()) pub struct BidiParagraphs<'text> { text: &'text str, info: std::vec::IntoIter, From 1cf10aeca780ce965cf0a0f72f435c5537331b0f Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 15 May 2023 19:43:30 +0800 Subject: [PATCH 10/32] implement text measure --- crates/bevy_text/src/error.rs | 2 + crates/bevy_text/src/pipeline.rs | 199 ++++++++++++++++++++++++------ crates/bevy_text/src/text2d.rs | 3 + crates/bevy_ui/src/widget/text.rs | 7 +- 4 files changed, 174 insertions(+), 37 deletions(-) diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index 0367bdbb415e6..cdfabe03d7570 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -9,4 +9,6 @@ pub enum TextError { FailedToAddGlyphOld(GlyphId), #[error("failed to add glyph to newly-created atlas {0:?}")] FailedToAddGlyph(u16), + #[error("font system mutex could not be acquired or is poisoned")] + FailedToAcquireMutex, } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 4d312757eb6b0..4053607a18037 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Mutex}; + use ab_glyph::{PxScale, ScaleFont}; use bevy_asset::{Assets, Handle, HandleId}; use bevy_ecs::component::Component; @@ -5,7 +7,10 @@ use bevy_ecs::system::Resource; use bevy_math::Vec2; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; -use bevy_utils::{tracing::warn, HashMap}; +use bevy_utils::{ + tracing::{error, warn}, + HashMap, +}; use cosmic_text::{Attrs, AttrsList, Buffer, BufferLine, Family, Metrics, Wrap}; use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText}; @@ -16,14 +21,30 @@ use crate::{ TextSettings, YAxisOrientation, }; -pub struct FontSystem(cosmic_text::FontSystem); +// TODO: introduce FontQuery enum instead of Handle +// TODO: cache buffers / store buffers on the entity +// TODO: remove old implementation +// TODO: reconstruct byte indices +// TODO: rescale font sizes in all examples +// TODO: fix any broken examples +// TODO: solve spans with different font sizes +// TODO: (future work) split text entities into section entities + +const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); +const MAX_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(f32::INFINITY, f32::INFINITY); + +// TODO: the only reason we need a mutex is due to TextMeasure +// - is there a way to do this without it? +pub struct FontSystem(Arc>); impl Default for FontSystem { 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)) + Self(Arc::new(Mutex::new( + cosmic_text::FontSystem::new_with_locale_and_db(locale, db), + ))) } } @@ -50,7 +71,14 @@ impl FontSystem { /// This should ideally run in a background thread. // TODO: This should run in a background thread. pub fn load_system_fonts(&mut self) { - self.0.db_mut().load_system_fonts(); + match self.0.try_lock() { + Ok(mut font_system) => { + font_system.db_mut().load_system_fonts(); + } + Err(err) => { + error!("Failed to acquire mutex: {:?}", err); + } + }; } } @@ -119,7 +147,11 @@ impl TextPipeline { let (font_size, line_height) = (font_size as f32, line_height as f32); let metrics = Metrics::new(font_size, line_height); - let font_system = &mut self.font_system.0; + let font_system = &mut self + .font_system + .0 + .try_lock() + .map_err(|_| TextError::FailedToAcquireMutex)?; // TODO: cache buffers (see Iced / glyphon) let mut buffer = Buffer::new(font_system, metrics); @@ -184,7 +216,10 @@ impl TextPipeline { buffer_lines.push(BufferLine::new(line_text, attrs_list)); // node size (bounds) is already scaled by the systems that call queue_text - let buffer_height = bounds.y; + // TODO: cosmic text does not shape/layout text outside the buffer height + // consider a better way to do this + // let buffer_height = bounds.y; + let buffer_height = f32::INFINITY; buffer.set_size(font_system, bounds.x, buffer_height); buffer.lines = buffer_lines; @@ -234,7 +269,11 @@ impl TextPipeline { let buffer = self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; - let font_system = &mut self.font_system.0; + let font_system = &mut self + .font_system + .0 + .try_lock() + .map_err(|_| TextError::FailedToAcquireMutex)?; let swash_cache = &mut self.swash_cache.0; // DEBUGGING: @@ -248,24 +287,7 @@ impl TextPipeline { // TODO: check height and width logic // TODO: move this to text measurement - let box_size = { - let width = buffer - .layout_runs() - .map(|run| run.line_w) - .reduce(|max_w, w| max_w.max(w)) - .unwrap(); - let height = buffer - .layout_runs() - .map(|run| run.glyphs) - .flat_map(|glyphs| { - glyphs - .iter() - .map(|g| f32::from_bits(g.cache_key.font_size_bits)) - .reduce(|max_h, w| max_h.max(w)) - }) - .sum::(); - Vec2::new(width, height) - }; + let box_size = buffer_dimensions(&buffer); let glyphs = buffer.layout_runs().flat_map(|run| { run.glyphs @@ -438,8 +460,77 @@ impl TextPipeline { sections: &[TextSection], scale_factor: f64, text_alignment: TextAlignment, - linebreak_behaviour: BreakLineOn, + linebreak_behavior: BreakLineOn, ) -> Result { + let mut buffer = self.create_buffer( + fonts, + sections, + linebreak_behavior, + MIN_WIDTH_CONTENT_BOUNDS, + scale_factor, + )?; + + let min_width_content_size = buffer_dimensions(&buffer); + + let max_width_content_size = { + let font_system = &mut self + .font_system + .0 + .try_lock() + .map_err(|_| TextError::FailedToAcquireMutex)?; + + buffer.set_size( + font_system, + MAX_WIDTH_CONTENT_BOUNDS.x, + MAX_WIDTH_CONTENT_BOUNDS.y, + ); + + let max_width_content_size = buffer_dimensions(&buffer); + + max_width_content_size + }; + + Ok(TextMeasureInfo { + min_width_content_size, + max_width_content_size, + font_system: Arc::clone(&self.font_system.0), + buffer: Mutex::new(buffer), + }) + } + + pub fn create_text_measure_old( + &mut self, + fonts: &Assets, + sections: &[TextSection], + scale_factor: f64, + text_alignment: TextAlignment, + linebreak_behavior: BreakLineOn, + ) -> Result { + let mut buffer = self.create_buffer( + fonts, + sections, + linebreak_behavior, + MIN_WIDTH_CONTENT_BOUNDS, + scale_factor, + )?; + let min_width_content_size = buffer_dimensions(&buffer); + + let font_system = &mut self + .font_system + .0 + .try_lock() + .map_err(|_| TextError::FailedToAcquireMutex)?; + + buffer.set_size( + font_system, + MAX_WIDTH_CONTENT_BOUNDS.x, + MAX_WIDTH_CONTENT_BOUNDS.y, + ); + + let max_width_content_size = buffer_dimensions(&buffer); + + dbg!(min_width_content_size, max_width_content_size); + let mut auto_fonts = Vec::with_capacity(sections.len()); let mut scaled_fonts = Vec::with_capacity(sections.len()); let sections = sections @@ -464,12 +555,12 @@ impl TextPipeline { }) .collect::, _>>()?; - Ok(TextMeasureInfo::new( + Ok(TextMeasureInfoOld::new( auto_fonts, scaled_fonts, sections, text_alignment, - linebreak_behaviour.into(), + linebreak_behavior.into(), )) } } @@ -481,8 +572,36 @@ pub struct TextMeasureSection { pub font_id: FontId, } -#[derive(Debug, Clone)] +// TODO: is there a way to do this without mutexes? pub struct TextMeasureInfo { + pub min_width_content_size: Vec2, + pub max_width_content_size: Vec2, + buffer: Mutex, + font_system: Arc>, +} + +impl std::fmt::Debug for TextMeasureInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TextMeasureInfo") + .field("min_width_content_size", &self.min_width_content_size) + .field("max_width_content_size", &self.max_width_content_size) + .field("buffer", &"_") + .field("font_system", &"_") + .finish() + } +} + +impl TextMeasureInfo { + pub fn compute_size(&self, bounds: Vec2) -> Vec2 { + let font_system = &mut self.font_system.try_lock().expect("Failed to acquire lock"); + let mut buffer = self.buffer.lock().expect("Failed to acquire the lock"); + buffer.set_size(font_system, bounds.x, bounds.y); + buffer_dimensions(&buffer) + } +} + +#[derive(Debug, Clone)] +pub struct TextMeasureInfoOld { pub fonts: Vec, pub scaled_fonts: Vec>, pub sections: Vec, @@ -492,7 +611,7 @@ pub struct TextMeasureInfo { pub max_width_content_size: Vec2, } -impl TextMeasureInfo { +impl TextMeasureInfoOld { fn new( fonts: Vec, scaled_fonts: Vec>, @@ -511,12 +630,8 @@ impl TextMeasureInfo { }; let section_texts = info.prepare_section_texts(); - let min = - info.compute_size_from_section_texts(§ion_texts, Vec2::new(0.0, f32::INFINITY)); - let max = info.compute_size_from_section_texts( - §ion_texts, - Vec2::new(f32::INFINITY, f32::INFINITY), - ); + let min = info.compute_size_from_section_texts(§ion_texts, MIN_WIDTH_CONTENT_BOUNDS); + let max = info.compute_size_from_section_texts(§ion_texts, MAX_WIDTH_CONTENT_BOUNDS); info.min_width_content_size = min; info.max_width_content_size = max; info @@ -621,6 +736,18 @@ fn add_span( attrs_list.add_span(start..end, attrs); } +fn buffer_dimensions(buffer: &Buffer) -> Vec2 { + let width = buffer + .layout_runs() + .map(|run| run.line_w) + .reduce(|max_w, w| max_w.max(w)) + .unwrap(); + // TODO: support multiple line heights / font sizes (once supported by cosmic text) + let line_height = buffer.metrics().line_height; + let height = buffer.layout_runs().count() as f32 * line_height; + Vec2::new(width.ceil(), height.ceil()) +} + /// An iterator over the paragraphs in the input text. /// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour. // TODO: upstream to cosmic_text, see https://github.com/pop-os/cosmic-text/pull/124 diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index c28237112c394..5064d2e339671 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -203,6 +203,9 @@ pub fn update_text2d_layout( Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } + Err(e @ TextError::FailedToAcquireMutex) => { + panic!("Fatal error when processing text: {e}."); + } Ok(info) => *text_layout_info = info, } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 12be382e7bdf8..fe0f177df6dff 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -39,7 +39,6 @@ impl Default for TextFlags { } } -#[derive(Clone)] pub struct TextMeasure { pub info: TextMeasureInfo, } @@ -107,6 +106,9 @@ fn create_text_measure( Err(e @ TextError::FailedToAddGlyphOld(_)) => { panic!("Fatal error when processing text: {e}."); } + Err(e @ TextError::FailedToAcquireMutex) => { + panic!("Fatal error when processing text: {e}."); + } }; } @@ -203,6 +205,9 @@ fn queue_text( Err(e @ TextError::FailedToAddGlyphOld(_)) => { panic!("Fatal error when processing text: {e}."); } + Err(e @ TextError::FailedToAcquireMutex) => { + panic!("Fatal error when processing text: {e}."); + } Ok(info) => { *text_layout_info = info; text_flags.needs_recompute = false; From ff2aeab823d7d124f5101022dba20644c4ed2474 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 15 May 2023 21:00:47 +0800 Subject: [PATCH 11/32] remove old implementation, dependencies --- Cargo.toml | 3 - crates/bevy_internal/Cargo.toml | 3 - crates/bevy_text/Cargo.toml | 3 - crates/bevy_text/src/error.rs | 3 - crates/bevy_text/src/font.rs | 42 +--- crates/bevy_text/src/font_atlas.rs | 86 +------- crates/bevy_text/src/font_atlas_set.rs | 113 +---------- crates/bevy_text/src/font_loader.rs | 2 +- crates/bevy_text/src/glyph_brush.rs | 215 -------------------- crates/bevy_text/src/lib.rs | 4 +- crates/bevy_text/src/pipeline.rs | 271 +------------------------ crates/bevy_text/src/text.rs | 19 -- crates/bevy_text/src/text2d.rs | 8 +- crates/bevy_ui/src/widget/text.rs | 16 +- docs/cargo_features.md | 1 - 15 files changed, 35 insertions(+), 754 deletions(-) delete mode 100644 crates/bevy_text/src/glyph_brush.rs diff --git a/Cargo.toml b/Cargo.toml index 4168bcbb78083..f8f12c99a1ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -205,9 +205,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"] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index d3e9a532a44cf..e1610e0e2fa11 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -71,9 +71,6 @@ serialize = ["bevy_core/serialize", "bevy_input/serialize", "bevy_time/serialize wayland = ["bevy_winit/wayland"] x11 = ["bevy_winit/x11"] -# enable rendering of font glyphs using subpixel accuracy -subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] - # Optimise for WebGL2 webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"] diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index a725a4d5a1972..1561fa21a25d3 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -subpixel_glyph_atlas = [] default_font = [] [dependencies] @@ -27,9 +26,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } # other anyhow = "1.0.4" -ab_glyph = "0.2.6" cosmic-text = "0.8.0" -glyph_brush_layout = "0.2.1" thiserror = "1.0" serde = {version = "1", features = ["derive"]} sys-locale = "0.3.0" diff --git a/crates/bevy_text/src/error.rs b/crates/bevy_text/src/error.rs index cdfabe03d7570..33f8f12647e58 100644 --- a/crates/bevy_text/src/error.rs +++ b/crates/bevy_text/src/error.rs @@ -1,4 +1,3 @@ -use ab_glyph::GlyphId; use thiserror::Error; #[derive(Debug, PartialEq, Eq, Error)] @@ -6,8 +5,6 @@ pub enum TextError { #[error("font not found")] NoSuchFont, #[error("failed to add glyph to newly-created atlas {0:?}")] - FailedToAddGlyphOld(GlyphId), - #[error("failed to add glyph to newly-created atlas {0:?}")] FailedToAddGlyph(u16), #[error("font system mutex could not be acquired or is poisoned")] FailedToAcquireMutex, diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 322cc64c4c8b9..470fec53c08dd 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,4 +1,3 @@ -use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph}; use bevy_reflect::{TypePath, TypeUuid}; use bevy_render::{ render_resource::{Extent3d, TextureDimension, TextureFormat}, @@ -8,25 +7,19 @@ use bevy_render::{ #[derive(Debug, TypeUuid, TypePath, Clone)] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] pub struct Font { - pub font: FontArc, pub data: std::sync::Arc>, } impl Font { - pub fn try_from_bytes(font_data: Vec) -> Result { - // DEBUGGING: - // eprintln!("loading font {}", font_data.len()); - let font = FontVec::try_from_vec(font_data.clone())?; - let font = FontArc::new(font); - // TODO: validate font - Ok(Font { - font, + pub fn from_bytes(font_data: Vec) -> Self { + // TODO: validate font, restore `try_from_bytes` + Self { data: std::sync::Arc::new(font_data), - }) + } } // TODO: consider moving to pipeline.rs - pub fn get_outlined_glyph_texture_new( + pub fn get_outlined_glyph_texture( font_system: &mut cosmic_text::FontSystem, swash_cache: &mut cosmic_text::SwashCache, layout_glyph: &cosmic_text::LayoutGlyph, @@ -71,29 +64,4 @@ impl Font { height, ) } - - pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image { - let bounds = outlined_glyph.px_bounds(); - let width = bounds.width() as usize; - let height = bounds.height() as usize; - let mut alpha = vec![0.0; width * height]; - outlined_glyph.draw(|x, y, v| { - alpha[y as usize * width + x as usize] = 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::>(), - TextureFormat::Rgba8UnormSrgb, - ) - } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index fb6dff183af35..64fb96c76cd78 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,4 +1,3 @@ -use ab_glyph::{GlyphId, Point}; use bevy_asset::{Assets, Handle}; use bevy_math::Vec2; use bevy_render::{ @@ -8,41 +7,9 @@ use bevy_render::{ use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; 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 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 for SubpixelOffset { - fn from(_: Point) -> Self { - Self - } -} - pub struct FontAtlas { pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - pub glyph_to_atlas_index_old: HashMap<(GlyphId, SubpixelOffset), usize>, - pub glyph_to_atlas_index_new: HashMap, + pub glyph_to_atlas_index: HashMap, pub texture_atlas: Handle, } @@ -65,39 +32,23 @@ impl FontAtlas { let texture_atlas = TextureAtlas::new_empty(atlas_texture, size); Self { texture_atlas: texture_atlases.add(texture_atlas), - glyph_to_atlas_index_old: HashMap::default(), - glyph_to_atlas_index_new: HashMap::default(), + glyph_to_atlas_index: HashMap::default(), dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1), } } - pub fn get_glyph_index_new( + pub fn get_glyph_index( &self, cache_key: cosmic_text::CacheKey, ) -> Option<(usize, i32, i32, u32, u32)> { - self.glyph_to_atlas_index_new.get(&cache_key).copied() + self.glyph_to_atlas_index.get(&cache_key).copied() } - pub fn get_glyph_index( - &self, - glyph_id: GlyphId, - subpixel_offset: SubpixelOffset, - ) -> Option { - self.glyph_to_atlas_index_old - .get(&(glyph_id, subpixel_offset)) - .copied() - } - - pub fn has_glyph_new(&self, cache_key: cosmic_text::CacheKey) -> bool { - self.glyph_to_atlas_index_new.contains_key(&cache_key) + pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey) -> bool { + self.glyph_to_atlas_index.contains_key(&cache_key) } - pub fn has_glyph(&self, glyph_id: GlyphId, subpixel_offset: SubpixelOffset) -> bool { - self.glyph_to_atlas_index_old - .contains_key(&(glyph_id, subpixel_offset)) - } - - pub fn add_glyph_new( + pub fn add_glyph( &mut self, textures: &mut Assets, texture_atlases: &mut Assets, @@ -113,32 +64,11 @@ impl FontAtlas { self.dynamic_texture_atlas_builder .add_texture(texture_atlas, textures, texture) { - self.glyph_to_atlas_index_new + self.glyph_to_atlas_index .insert(cache_key, (index, left, top, width, height)); true } else { false } } - - pub fn add_glyph_old( - &mut self, - textures: &mut Assets, - texture_atlases: &mut Assets, - glyph_id: GlyphId, - subpixel_offset: SubpixelOffset, - texture: &Image, - ) -> bool { - let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); - if let Some(index) = - self.dynamic_texture_atlas_builder - .add_texture(texture_atlas, textures, texture) - { - self.glyph_to_atlas_index_old - .insert((glyph_id, subpixel_offset), index); - true - } else { - false - } - } } diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index b9f7fc61d0660..f34d5dae7afca 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,15 +1,12 @@ use crate::{error::TextError, Font, FontAtlas}; -use ab_glyph::{GlyphId, OutlinedGlyph, Point}; use bevy_asset::{Assets, Handle}; use bevy_math::Vec2; use bevy_reflect::TypePath; use bevy_reflect::TypeUuid; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; -use bevy_utils::FloatOrd; use bevy_utils::HashMap; -type FontSizeKeyOld = FloatOrd; type FontSizeKey = u32; // TODO: FontAtlasSet is an asset tied to a Handle cast weakly @@ -17,7 +14,6 @@ type FontSizeKey = u32; #[derive(TypeUuid, TypePath)] #[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] pub struct FontAtlasSet { - font_atlases_old: HashMap>, font_atlases: HashMap>, } @@ -40,7 +36,6 @@ pub struct GlyphAtlasInfo { impl Default for FontAtlasSet { fn default() -> Self { FontAtlasSet { - font_atlases_old: HashMap::with_capacity_and_hasher(1, Default::default()), font_atlases: HashMap::with_capacity_and_hasher(1, Default::default()), } } @@ -51,17 +46,15 @@ impl FontAtlasSet { self.font_atlases.iter() } - pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool { + pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: f32) -> bool { self.font_atlases .get(&font_size.to_bits()) .map_or(false, |font_atlas| { - font_atlas - .iter() - .any(|atlas| atlas.has_glyph(glyph_id, glyph_position.into())) + font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)) }) } - pub fn add_glyph_to_atlas_new( + pub fn add_glyph_to_atlas( &mut self, texture_atlases: &mut Assets, textures: &mut Assets, @@ -85,9 +78,9 @@ impl FontAtlasSet { }); let (glyph_texture, left, top, w, h) = - Font::get_outlined_glyph_texture_new(font_system, swash_cache, layout_glyph); + Font::get_outlined_glyph_texture(font_system, swash_cache, layout_glyph); let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { - atlas.add_glyph_new( + atlas.add_glyph( textures, texture_atlases, layout_glyph.cache_key, @@ -112,7 +105,7 @@ impl FontAtlasSet { texture_atlases, Vec2::new(containing, containing), )); - if !font_atlases.last_mut().unwrap().add_glyph_new( + if !font_atlases.last_mut().unwrap().add_glyph( textures, texture_atlases, layout_glyph.cache_key, @@ -126,73 +119,10 @@ impl FontAtlasSet { } } - Ok(self - .get_glyph_atlas_info_new(layout_glyph.cache_key) - .unwrap()) - } - - pub fn add_glyph_to_atlas_old( - &mut self, - texture_atlases: &mut Assets, - textures: &mut Assets, - outlined_glyph: OutlinedGlyph, - ) -> Result { - let glyph = outlined_glyph.glyph(); - let glyph_id = glyph.id; - let glyph_position = glyph.position; - let font_size = glyph.scale.y; - let font_atlases = self - .font_atlases_old - .entry(FloatOrd(font_size)) - .or_insert_with(|| { - vec![FontAtlas::new( - textures, - texture_atlases, - Vec2::splat(512.0), - )] - }); - - let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph); - let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { - atlas.add_glyph_old( - textures, - texture_atlases, - glyph_id, - glyph_position.into(), - &glyph_texture, - ) - }; - if !font_atlases.iter_mut().any(add_char_to_font_atlas) { - // Find the largest dimension of the glyph, either its width or its height - let glyph_max_size: u32 = glyph_texture - .texture_descriptor - .size - .height - .max(glyph_texture.texture_descriptor.size.width); - // Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size - let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512) as f32; - font_atlases.push(FontAtlas::new( - textures, - texture_atlases, - Vec2::new(containing, containing), - )); - if !font_atlases.last_mut().unwrap().add_glyph_old( - textures, - texture_atlases, - glyph_id, - glyph_position.into(), - &glyph_texture, - ) { - return Err(TextError::FailedToAddGlyphOld(glyph_id)); - } - } - - Ok(self - .get_glyph_atlas_info_old(font_size, glyph_id, glyph_position) - .unwrap()) + Ok(self.get_glyph_atlas_info(layout_glyph.cache_key).unwrap()) } - pub fn get_glyph_atlas_info_new( + pub fn get_glyph_atlas_info( &mut self, cache_key: cosmic_text::CacheKey, ) -> Option { @@ -203,7 +133,7 @@ impl FontAtlasSet { .iter() .find_map(|atlas| { atlas - .get_glyph_index_new(cache_key) + .get_glyph_index(cache_key) .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) }) .map( @@ -219,31 +149,8 @@ impl FontAtlasSet { }) } - pub fn get_glyph_atlas_info_old( - &mut self, - font_size: f32, - glyph_id: GlyphId, - position: Point, - ) -> Option { - self.font_atlases_old - .get(&FloatOrd(font_size)) - .and_then(|font_atlases| { - font_atlases - .iter() - .find_map(|atlas| { - atlas - .get_glyph_index(glyph_id, position.into()) - .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) - }) - .map(|(glyph_index, texture_atlas)| GlyphAtlasInfo { - texture_atlas, - glyph_index, - }) - }) - } - pub fn num_font_atlases(&self) -> usize { - self.font_atlases_old.len() + self.font_atlases.len() } } diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index e179ec9ccf82e..540a954590a92 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -13,7 +13,7 @@ impl AssetLoader for FontLoader { load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<()>> { Box::pin(async move { - let font = Font::try_from_bytes(bytes.into())?; + let font = Font::from_bytes(bytes.into()); load_context.set_default_asset(LoadedAsset::new(font)); Ok(()) }) diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs deleted file mode 100644 index 559caf3fd743d..0000000000000 --- a/crates/bevy_text/src/glyph_brush.rs +++ /dev/null @@ -1,215 +0,0 @@ -use ab_glyph::{Font as _, FontArc, Glyph, ScaleFont as _}; -use bevy_asset::{Assets, Handle}; -use bevy_math::Vec2; -use bevy_render::texture::Image; -use bevy_sprite::TextureAtlas; -use bevy_utils::tracing::warn; -use glyph_brush_layout::{ - BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, - SectionText, ToSectionText, -}; - -use crate::{ - error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo, - TextAlignment, TextSettings, YAxisOrientation, -}; - -pub struct GlyphBrush { - fonts: Vec, - handles: Vec>, - latest_font_id: FontId, -} - -impl Default for GlyphBrush { - fn default() -> Self { - GlyphBrush { - fonts: Vec::new(), - handles: Vec::new(), - latest_font_id: FontId(0), - } - } -} - -impl GlyphBrush { - pub fn compute_glyphs( - &self, - sections: &[S], - bounds: Vec2, - text_alignment: TextAlignment, - linebreak_behavior: BreakLineOn, - ) -> Result, 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, - sections: &[SectionText], - font_atlas_set_storage: &mut Assets, - fonts: &Assets, - texture_atlases: &mut Assets, - textures: &mut Assets, - text_settings: &TextSettings, - font_atlas_warning: &mut FontAtlasWarning, - y_axis_orientation: YAxisOrientation, - ) -> Result, TextError> { - if glyphs.is_empty() { - return Ok(Vec::new()); - } - - let sections_data = sections - .iter() - .map(|section| { - let handle = &self.handles[section.font_id.0]; - let font = fonts.get(handle).ok_or(TextError::NoSuchFont)?; - let font_size = section.scale.y; - Ok(( - handle, - font, - font_size, - ab_glyph::Font::as_scaled(&font.font, font_size), - )) - }) - .collect::, _>>()?; - - let mut min_x = std::f32::MAX; - let mut min_y = std::f32::MAX; - let mut max_y = std::f32::MIN; - for sg in &glyphs { - let glyph = &sg.glyph; - - let scaled_font = sections_data[sg.section_index].3; - min_x = min_x.min(glyph.position.x); - min_y = min_y.min(glyph.position.y - scaled_font.ascent()); - max_y = max_y.max(glyph.position.y - scaled_font.descent()); - } - min_x = min_x.floor(); - min_y = min_y.floor(); - max_y = max_y.floor(); - - let mut positioned_glyphs = Vec::new(); - for sg in glyphs { - let SectionGlyph { - section_index: _, - byte_index, - mut glyph, - font_id: _, - } = sg; - let glyph_id = glyph.id; - let glyph_position = glyph.position; - 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 handle_font_atlas: Handle = section_data.0.cast_weak(); - let font_atlas_set = font_atlas_set_storage - .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); - - let atlas_info = font_atlas_set - .get_glyph_atlas_info_old(section_data.2, glyph_id, glyph_position) - .map(Ok) - .unwrap_or_else(|| { - font_atlas_set.add_glyph_to_atlas_old( - texture_atlases, - textures, - outlined_glyph, - ) - })?; - - if !text_settings.allow_dynamic_font_size - && !font_atlas_warning.warned - && font_atlas_set.num_font_atlases() > text_settings.max_font_atlases.get() - { - warn!("warning[B0005]: Number of font atlases has exceeded the maximum of {}. Performance and memory usage may suffer.", text_settings.max_font_atlases.get()); - font_atlas_warning.warned = true; - } - - let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let glyph_rect = texture_atlas.textures[atlas_info.glyph_index]; - let size = Vec2::new(glyph_rect.width(), glyph_rect.height()); - - let x = bounds.min.x + size.x / 2.0 - min_x; - - let y = match y_axis_orientation { - YAxisOrientation::BottomToTop => max_y - bounds.max.y + size.y / 2.0, - YAxisOrientation::TopToBottom => bounds.min.y + size.y / 2.0 - min_y, - }; - - let position = adjust.position(Vec2::new(x, y)); - - positioned_glyphs.push(PositionedGlyphOld { - position, - size, - atlas_info, - section_index: sg.section_index, - byte_index, - }); - } - } - Ok(positioned_glyphs) - } - - pub fn add_font(&mut self, handle: Handle, font: FontArc) -> FontId { - self.fonts.push(font); - self.handles.push(handle); - let font_id = self.latest_font_id; - self.latest_font_id = FontId(font_id.0 + 1); - font_id - } -} - -#[derive(Debug, Clone)] -pub struct PositionedGlyphOld { - 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 - } -} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index ed7825adbbf4f..70e56a915e01a 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -5,7 +5,6 @@ mod font; mod font_atlas; mod font_atlas_set; mod font_loader; -mod glyph_brush; mod pipeline; mod text; mod text2d; @@ -15,7 +14,6 @@ pub use font::*; pub use font_atlas::*; pub use font_atlas_set::*; pub use font_loader::*; -pub use glyph_brush::*; pub use pipeline::*; pub use text::*; pub use text2d::*; @@ -111,7 +109,7 @@ impl Plugin for TextPlugin { app, DEFAULT_FONT_HANDLE, "FiraMono-subset.ttf", - |bytes: &[u8]| { Font::try_from_bytes(bytes.to_vec()).unwrap() } + |bytes: &[u8]| { Font::from_bytes(bytes.to_vec()) } ); } } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 4053607a18037..5c48164333dad 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,6 +1,5 @@ use std::sync::{Arc, Mutex}; -use ab_glyph::{PxScale, ScaleFont}; use bevy_asset::{Assets, Handle, HandleId}; use bevy_ecs::component::Component; use bevy_ecs::system::Resource; @@ -13,22 +12,22 @@ use bevy_utils::{ }; use cosmic_text::{Attrs, AttrsList, Buffer, BufferLine, Family, Metrics, Wrap}; -use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText}; use crate::{ - error::TextError, font, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, - FontAtlasWarning, PositionedGlyph, PositionedGlyphOld, TextAlignment, TextSection, - TextSettings, YAxisOrientation, + error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, PositionedGlyph, + TextAlignment, TextSection, TextSettings, YAxisOrientation, }; // TODO: introduce FontQuery enum instead of Handle // TODO: cache buffers / store buffers on the entity -// TODO: remove old implementation // TODO: reconstruct byte indices // TODO: rescale font sizes in all examples // TODO: fix any broken examples // TODO: solve spans with different font sizes // TODO: (future work) split text entities into section entities +// TODO: (future work) support emojis +// TODO: (future work) text editing +// TODO: font validation const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); const MAX_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(f32::INFINITY, f32::INFINITY); @@ -92,22 +91,11 @@ impl Default for SwashCache { #[derive(Default, Resource)] pub struct TextPipeline { - brush: GlyphBrush, - map_font_id: HashMap, map_handle_to_font_id: HashMap, font_system: FontSystem, swash_cache: SwashCache, } -/// 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)] -pub struct TextLayoutInfoOld { - pub glyphs: Vec, - pub size: Vec2, -} - /// Render information for a corresponding [`Text`](crate::Text) component. /// /// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`]. @@ -118,14 +106,6 @@ pub struct TextLayoutInfo { } impl TextPipeline { - pub fn get_or_insert_font_id(&mut self, handle: &Handle, font: &Font) -> FontId { - let brush = &mut self.brush; - *self - .map_font_id - .entry(handle.id()) - .or_insert_with(|| brush.add_font(handle.clone(), font.font.clone())) - } - pub fn create_buffer( &mut self, fonts: &Assets, @@ -313,10 +293,10 @@ impl TextPipeline { .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); let atlas_info = font_atlas_set - .get_glyph_atlas_info_new(layout_glyph.cache_key) + .get_glyph_atlas_info(layout_glyph.cache_key) .map(Ok) .unwrap_or_else(|| { - font_atlas_set.add_glyph_to_atlas_new(texture_atlases, textures, font_system, swash_cache, layout_glyph) + font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, font_system, swash_cache, layout_glyph) })?; if !text_settings.allow_dynamic_font_size @@ -374,86 +354,6 @@ impl TextPipeline { }) } - #[allow(clippy::too_many_arguments)] - pub fn queue_text_old( - &mut self, - fonts: &Assets, - sections: &[TextSection], - scale_factor: f64, - text_alignment: TextAlignment, - linebreak_behavior: BreakLineOn, - bounds: Vec2, - font_atlas_set_storage: &mut Assets, - texture_atlases: &mut Assets, - textures: &mut Assets, - text_settings: &TextSettings, - font_atlas_warning: &mut FontAtlasWarning, - y_axis_orientation: YAxisOrientation, - ) -> Result { - let mut scaled_fonts = Vec::with_capacity(sections.len()); - let sections = sections - .iter() - .map(|section| { - let font = fonts - .get(§ion.style.font) - .ok_or(TextError::NoSuchFont)?; - let font_id = self.get_or_insert_font_id(§ion.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: §ion.value, - }; - - Ok(section) - }) - .collect::, _>>()?; - - let section_glyphs = - self.brush - .compute_glyphs(§ions, bounds, text_alignment, linebreak_behavior)?; - - if section_glyphs.is_empty() { - return Ok(TextLayoutInfoOld::default()); - } - - let mut min_x: f32 = std::f32::MAX; - let mut min_y: f32 = std::f32::MAX; - let mut max_x: f32 = std::f32::MIN; - let mut max_y: f32 = std::f32::MIN; - - for sg in §ion_glyphs { - let scaled_font = scaled_fonts[sg.section_index]; - let glyph = &sg.glyph; - // The fonts use a coordinate system increasing upwards so ascent is a positive value - // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, - // so we have to subtract from the baseline position to get the minimum and maximum values. - min_x = min_x.min(glyph.position.x); - min_y = min_y.min(glyph.position.y - scaled_font.ascent()); - max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); - max_y = max_y.max(glyph.position.y - scaled_font.descent()); - } - - let size = Vec2::new(max_x - min_x, max_y - min_y); - - let glyphs = self.brush.process_glyphs( - section_glyphs, - §ions, - font_atlas_set_storage, - fonts, - texture_atlases, - textures, - text_settings, - font_atlas_warning, - y_axis_orientation, - )?; - - Ok(TextLayoutInfoOld { glyphs, size }) - } - pub fn create_text_measure( &mut self, fonts: &Assets, @@ -497,79 +397,6 @@ impl TextPipeline { buffer: Mutex::new(buffer), }) } - - pub fn create_text_measure_old( - &mut self, - fonts: &Assets, - sections: &[TextSection], - scale_factor: f64, - text_alignment: TextAlignment, - linebreak_behavior: BreakLineOn, - ) -> Result { - let mut buffer = self.create_buffer( - fonts, - sections, - linebreak_behavior, - MIN_WIDTH_CONTENT_BOUNDS, - scale_factor, - )?; - let min_width_content_size = buffer_dimensions(&buffer); - - let font_system = &mut self - .font_system - .0 - .try_lock() - .map_err(|_| TextError::FailedToAcquireMutex)?; - - buffer.set_size( - font_system, - MAX_WIDTH_CONTENT_BOUNDS.x, - MAX_WIDTH_CONTENT_BOUNDS.y, - ); - - let max_width_content_size = buffer_dimensions(&buffer); - - dbg!(min_width_content_size, max_width_content_size); - - let mut auto_fonts = Vec::with_capacity(sections.len()); - let mut scaled_fonts = Vec::with_capacity(sections.len()); - let sections = sections - .iter() - .enumerate() - .map(|(i, section)| { - let font = fonts - .get(§ion.style.font) - .ok_or(TextError::NoSuchFont)?; - let font_size = scale_value(section.style.font_size, scale_factor); - auto_fonts.push(font.font.clone()); - let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size); - scaled_fonts.push(px_scale_font); - - let section = TextMeasureSection { - font_id: FontId(i), - scale: PxScale::from(font_size), - text: section.value.clone(), - }; - - Ok(section) - }) - .collect::, _>>()?; - - Ok(TextMeasureInfoOld::new( - auto_fonts, - scaled_fonts, - sections, - text_alignment, - linebreak_behavior.into(), - )) - } -} - -#[derive(Debug, Clone)] -pub struct TextMeasureSection { - pub text: String, - pub scale: PxScale, - pub font_id: FontId, } // TODO: is there a way to do this without mutexes? @@ -600,90 +427,6 @@ impl TextMeasureInfo { } } -#[derive(Debug, Clone)] -pub struct TextMeasureInfoOld { - pub fonts: Vec, - pub scaled_fonts: Vec>, - pub sections: Vec, - pub text_alignment: TextAlignment, - pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, - pub min_width_content_size: Vec2, - pub max_width_content_size: Vec2, -} - -impl TextMeasureInfoOld { - fn new( - fonts: Vec, - scaled_fonts: Vec>, - sections: Vec, - text_alignment: TextAlignment, - linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, - ) -> Self { - let mut info = Self { - fonts, - scaled_fonts, - sections, - text_alignment, - linebreak_behaviour, - min_width_content_size: Vec2::ZERO, - max_width_content_size: Vec2::ZERO, - }; - - let section_texts = info.prepare_section_texts(); - let min = info.compute_size_from_section_texts(§ion_texts, MIN_WIDTH_CONTENT_BOUNDS); - let max = info.compute_size_from_section_texts(§ion_texts, MAX_WIDTH_CONTENT_BOUNDS); - info.min_width_content_size = min; - info.max_width_content_size = max; - info - } - - fn prepare_section_texts(&self) -> Vec { - self.sections - .iter() - .map(|section| SectionText { - font_id: section.font_id, - scale: section.scale, - text: §ion.text, - }) - .collect::>() - } - - fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 { - let geom = SectionGeometry { - bounds: (bounds.x, bounds.y), - ..Default::default() - }; - let section_glyphs = glyph_brush_layout::Layout::default() - .h_align(self.text_alignment.into()) - .line_breaker(self.linebreak_behaviour) - .calculate_glyphs(&self.fonts, &geom, sections); - - let mut min_x: f32 = std::f32::MAX; - let mut min_y: f32 = std::f32::MAX; - let mut max_x: f32 = std::f32::MIN; - let mut max_y: f32 = std::f32::MIN; - - for sg in section_glyphs { - let scaled_font = &self.scaled_fonts[sg.section_index]; - let glyph = &sg.glyph; - // The fonts use a coordinate system increasing upwards so ascent is a positive value - // and descent is negative, but Bevy UI uses a downwards increasing coordinate system, - // so we have to subtract from the baseline position to get the minimum and maximum values. - min_x = min_x.min(glyph.position.x); - min_y = min_y.min(glyph.position.y - scaled_font.ascent()); - max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id)); - max_y = max_y.max(glyph.position.y - scaled_font.descent()); - } - - Vec2::new(max_x - min_x, max_y - min_y) - } - - pub fn compute_size(&self, bounds: Vec2) -> Vec2 { - let sections = self.prepare_section_texts(); - self.compute_size_from_section_texts(§ions, bounds) - } -} - /// Adds a span to the attributes list, /// loading fonts into the DB if required. #[allow(clippy::too_many_arguments)] diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index eaaf58d2b5273..99fa391fb4b6c 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -147,16 +147,6 @@ pub enum TextAlignment { Right, } -impl From for glyph_brush_layout::HorizontalAlign { - fn from(val: TextAlignment) -> Self { - match val { - TextAlignment::Left => glyph_brush_layout::HorizontalAlign::Left, - TextAlignment::Center => glyph_brush_layout::HorizontalAlign::Center, - TextAlignment::Right => glyph_brush_layout::HorizontalAlign::Right, - } - } -} - #[derive(Clone, Debug, Reflect, FromReflect)] pub struct TextStyle { pub font: Handle, @@ -187,12 +177,3 @@ pub enum BreakLineOn { /// However it may lead to words being broken up across linebreaks. AnyCharacter, } - -impl From for glyph_brush_layout::BuiltInLineBreaker { - fn from(val: BreakLineOn) -> Self { - match val { - BreakLineOn::WordBoundary => glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker, - BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker, - } - } -} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5064d2e339671..bad25698d1124 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -197,13 +197,7 @@ pub fn update_text2d_layout( // queue for further processing queue.insert(entity); } - Err(e @ TextError::FailedToAddGlyphOld(_)) => { - panic!("Fatal error when processing text: {e}."); - } - Err(e @ TextError::FailedToAddGlyph(_)) => { - panic!("Fatal error when processing text: {e}."); - } - Err(e @ TextError::FailedToAcquireMutex) => { + Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { panic!("Fatal error when processing text: {e}."); } Ok(info) => *text_layout_info = info, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index fe0f177df6dff..c89b8714ff88e 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -100,13 +100,7 @@ fn create_text_measure( // Try again next frame text_flags.needs_new_measure_func = true; } - Err(e @ TextError::FailedToAddGlyph(_)) => { - panic!("Fatal error when processing text: {e}."); - } - Err(e @ TextError::FailedToAddGlyphOld(_)) => { - panic!("Fatal error when processing text: {e}."); - } - Err(e @ TextError::FailedToAcquireMutex) => { + Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { panic!("Fatal error when processing text: {e}."); } }; @@ -199,13 +193,7 @@ fn queue_text( // There was an error processing the text layout, try again next frame text_flags.needs_recompute = true; } - Err(e @ TextError::FailedToAddGlyph(_)) => { - panic!("Fatal error when processing text: {e}."); - } - Err(e @ TextError::FailedToAddGlyphOld(_)) => { - panic!("Fatal error when processing text: {e}."); - } - Err(e @ TextError::FailedToAcquireMutex) => { + Err(e @ TextError::FailedToAddGlyph(_) | e @ TextError::FailedToAcquireMutex) => { panic!("Fatal error when processing text: {e}."); } Ok(info) => { diff --git a/docs/cargo_features.md b/docs/cargo_features.md index bf6a5e55fc643..0b4fc0f9f5ec0 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -61,7 +61,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)| From 79632d6aa4c89b02ccdb6ee0d7801181037f0708 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 16 May 2023 20:56:56 +0800 Subject: [PATCH 12/32] tidy up --- crates/bevy_text/src/font_atlas_set.rs | 4 -- crates/bevy_text/src/pipeline.rs | 96 +++++++++++--------------- 2 files changed, 39 insertions(+), 61 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index f34d5dae7afca..d78e70c3c0076 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -62,10 +62,6 @@ impl FontAtlasSet { swash_cache: &mut cosmic_text::SwashCache, layout_glyph: &cosmic_text::LayoutGlyph, ) -> Result { - // let glyph = layout_glyph.glyph(); - // let glyph_id = glyph.id; - // let glyph_position = glyph.position; - // let font_size = glyph.scale.y; let font_atlases = self .font_atlases .entry(layout_glyph.cache_key.font_size_bits) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 5c48164333dad..8fc319ff69f54 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -29,9 +29,6 @@ use crate::{ // TODO: (future work) text editing // TODO: font validation -const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); -const MAX_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(f32::INFINITY, f32::INFINITY); - // TODO: the only reason we need a mutex is due to TextMeasure // - is there a way to do this without it? pub struct FontSystem(Arc>); @@ -96,15 +93,6 @@ pub struct TextPipeline { swash_cache: SwashCache, } -/// 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)] -pub struct TextLayoutInfo { - pub glyphs: Vec, - pub size: Vec2, -} - impl TextPipeline { pub fn create_buffer( &mut self, @@ -136,7 +124,7 @@ impl TextPipeline { // TODO: cache buffers (see Iced / glyphon) let mut buffer = Buffer::new(font_system, metrics); - let mut buffer_lines = Vec::new(); + buffer.lines.clear(); let mut attrs_list = AttrsList::new(Attrs::new()); let mut line_text = String::new(); // all sections need to be combined and broken up into lines @@ -179,7 +167,9 @@ impl TextPipeline { let prev_attrs_list = std::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); let prev_line_text = std::mem::take(&mut line_text); - buffer_lines.push(BufferLine::new(prev_line_text, prev_attrs_list)); + buffer + .lines + .push(BufferLine::new(prev_line_text, prev_attrs_list)); add_span( &mut line_text, &mut attrs_list, @@ -193,15 +183,14 @@ impl TextPipeline { } } // finalise last line - buffer_lines.push(BufferLine::new(line_text, attrs_list)); + buffer.lines.push(BufferLine::new(line_text, attrs_list)); // node size (bounds) is already scaled by the systems that call queue_text // TODO: cosmic text does not shape/layout text outside the buffer height // consider a better way to do this // let buffer_height = bounds.y; let buffer_height = f32::INFINITY; - buffer.set_size(font_system, bounds.x, buffer_height); - buffer.lines = buffer_lines; + buffer.set_size(font_system, bounds.x.ceil(), buffer_height); buffer.set_wrap( font_system, @@ -244,8 +233,6 @@ impl TextPipeline { return Ok(TextLayoutInfo::default()); } - // TODO: Support loading fonts without cloning the already loaded data (they are cloned in `add_span`) - let buffer = self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; @@ -256,36 +243,14 @@ impl TextPipeline { .map_err(|_| TextError::FailedToAcquireMutex)?; let swash_cache = &mut self.swash_cache.0; - // DEBUGGING: - // let a = buffer.lines.iter().map(|l| l.text()).collect::(); - // let b = sections - // .iter() - // .map(|s| s.value.lines().collect::()) - // .collect::(); - // println!(); - // dbg!(a, b); - - // TODO: check height and width logic - // TODO: move this to text measurement let box_size = buffer_dimensions(&buffer); let glyphs = buffer.layout_runs().flat_map(|run| { run.glyphs .iter() - .map(move |g| (g, run.line_i, run.line_w, run.line_y, run.rtl, run.text)) - }); - - // DEBUGGING: - // for sg in glyphs.iter() { - // let (glyph, line_i, line_w, line_y, rtl, text) = sg; - // dbg!(*line_i as i32 * line_height as i32 + glyph.y_int); - // } - - // DEBUGGING: - // dbg!(glyphs.first().unwrap()); - - let glyphs = glyphs - .map(|(layout_glyph, line_i, line_w, line_y, rtl, text)| { + .map(move |layout_glyph| (layout_glyph, run.line_w, run.line_y)) + }) + .map(|(layout_glyph, line_w, line_y)| { let section_index = layout_glyph.metadata; let handle_font_atlas: Handle = sections[section_index].style.font.cast_weak(); @@ -326,7 +291,7 @@ impl TextPipeline { }; let y = match y_axis_orientation { YAxisOrientation::TopToBottom => y, - YAxisOrientation::BottomToTop => box_size.y as f32 - y, + YAxisOrientation::BottomToTop => box_size.y - y, }; // TODO: confirm whether we need to offset by glyph baseline @@ -359,9 +324,13 @@ impl TextPipeline { fonts: &Assets, sections: &[TextSection], scale_factor: f64, - text_alignment: TextAlignment, + // TODO: not currently required + _text_alignment: TextAlignment, linebreak_behavior: BreakLineOn, ) -> Result { + const MIN_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(0.0, f32::INFINITY); + const MAX_WIDTH_CONTENT_BOUNDS: Vec2 = Vec2::new(f32::INFINITY, f32::INFINITY); + let mut buffer = self.create_buffer( fonts, sections, @@ -385,9 +354,7 @@ impl TextPipeline { MAX_WIDTH_CONTENT_BOUNDS.y, ); - let max_width_content_size = buffer_dimensions(&buffer); - - max_width_content_size + buffer_dimensions(&buffer) }; Ok(TextMeasureInfo { @@ -399,6 +366,15 @@ impl TextPipeline { } } +/// 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)] +pub struct TextLayoutInfo { + pub glyphs: Vec, + pub size: Vec2, +} + // TODO: is there a way to do this without mutexes? pub struct TextMeasureInfo { pub min_width_content_size: Vec2, @@ -422,7 +398,7 @@ impl TextMeasureInfo { pub fn compute_size(&self, bounds: Vec2) -> Vec2 { let font_system = &mut self.font_system.try_lock().expect("Failed to acquire lock"); let mut buffer = self.buffer.lock().expect("Failed to acquire the lock"); - buffer.set_size(font_system, bounds.x, bounds.y); + buffer.set_size(font_system, bounds.x.ceil(), bounds.y.ceil()); buffer_dimensions(&buffer) } } @@ -450,14 +426,16 @@ fn add_span( .entry(font_handle_id) .or_insert_with(|| { let font = fonts.get(font_handle).unwrap(); + let data = Arc::clone(&font.data); font_system .db_mut() - .load_font_source(cosmic_text::fontdb::Source::Binary(font.data.clone())); - // TODO: validate this is the right font face. alternatively, - // 1. parse the face info using ttf-parser - // 2. push this face info into the db - // 3. query the db from the same face info we just pushed in to get the id + .load_font_source(cosmic_text::fontdb::Source::Binary(data)); + // TODO: it is assumed this is the right font face + // see https://github.com/pop-os/cosmic-text/issues/125 + // fontdb 0.14 returns the font ids from `load_font_source` let face_id = font_system.db().faces().last().unwrap().id; + // TODO: below may be required if we need to offset by the baseline (TBC) + // see https://github.com/pop-os/cosmic-text/issues/123 // let font = font_system.get_font(face_id).unwrap(); // map_font_id_to_metrics // .entry(face_id) @@ -480,15 +458,19 @@ fn add_span( } fn buffer_dimensions(buffer: &Buffer) -> Vec2 { + // TODO: see https://github.com/pop-os/cosmic-text/issues/70 Let a Buffer figure out its height during set_size + // TODO: see https://github.com/pop-os/cosmic-text/issues/42 Request: Allow buffer dimensions to be undefined + // TODO: debug tonemapping example let width = buffer .layout_runs() .map(|run| run.line_w) .reduce(|max_w, w| max_w.max(w)) .unwrap(); // TODO: support multiple line heights / font sizes (once supported by cosmic text) - let line_height = buffer.metrics().line_height; + let line_height = buffer.metrics().line_height.ceil(); let height = buffer.layout_runs().count() as f32 * line_height; - Vec2::new(width.ceil(), height.ceil()) + + Vec2::new(width, height).ceil() } /// An iterator over the paragraphs in the input text. From 85063cff9d9d6af841da01b7ab8fc989d3b07fe4 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 3 Jun 2023 21:19:52 +0800 Subject: [PATCH 13/32] fix tonemapping example layout bug --- crates/bevy_text/src/pipeline.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 8fc319ff69f54..4d8f00786f799 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -470,7 +470,9 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { let line_height = buffer.metrics().line_height.ceil(); let height = buffer.layout_runs().count() as f32 * line_height; - Vec2::new(width, height).ceil() + // `width.ceil() + 0.001` gets around a rare text layout bug in the tonemapping example. + // See https://github.com/pop-os/cosmic-text/issues/134 + Vec2::new(width.ceil() + 0.001, height).ceil() } /// An iterator over the paragraphs in the input text. From 86a90b3349a406fba580b6bc3f509036e924dd50 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 3 Jun 2023 21:47:58 +0800 Subject: [PATCH 14/32] update todos --- crates/bevy_text/src/pipeline.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 4d8f00786f799..c780b6e0167e2 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -283,7 +283,8 @@ impl TextPipeline { // offset by half the size because the origin is center let x = glyph_size.x / 2.0 + left + layout_glyph.x_int as f32; let y = line_y + layout_glyph.y_int as f32 - top + glyph_size.y / 2.0; - // TODO: cosmic text may handle text alignment in future + // TODO: use cosmic text's implementation (per-BufferLine alignment) as it will be editor aware + // see https://github.com/pop-os/cosmic-text/issues/130 (currently bugged) let x = x + match text_alignment { TextAlignment::Left => 0.0, TextAlignment::Center => (box_size.x - line_w) / 2.0, @@ -460,7 +461,6 @@ fn add_span( fn buffer_dimensions(buffer: &Buffer) -> Vec2 { // TODO: see https://github.com/pop-os/cosmic-text/issues/70 Let a Buffer figure out its height during set_size // TODO: see https://github.com/pop-os/cosmic-text/issues/42 Request: Allow buffer dimensions to be undefined - // TODO: debug tonemapping example let width = buffer .layout_runs() .map(|run| run.line_w) From da8cb97dbc868d1dc8a4f096bb8dfd67c77b3abb Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 3 Jun 2023 21:32:48 +0800 Subject: [PATCH 15/32] implement font query --- crates/bevy_text/src/pipeline.rs | 167 ++++++++++++++++++++----------- crates/bevy_text/src/text.rs | 34 ++++++- 2 files changed, 138 insertions(+), 63 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index c780b6e0167e2..00afb34e863ac 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -7,14 +7,14 @@ use bevy_math::Vec2; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::{ - tracing::{error, warn}, + tracing::{error, info, warn}, HashMap, }; use cosmic_text::{Attrs, AttrsList, Buffer, BufferLine, Family, Metrics, Wrap}; use crate::{ - error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, PositionedGlyph, + error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, FontRef, PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation, }; @@ -45,28 +45,7 @@ impl Default for FontSystem { } impl FontSystem { - /// Attempts to load system fonts. - /// - /// Supports Windows, Linux and macOS. - /// - /// System fonts loading is a surprisingly complicated task, - /// mostly unsolvable without interacting with system libraries. - /// And since `fontdb` tries to be small and portable, this method - /// will simply scan some predefined directories. - /// Which means that fonts that are not in those directories must - /// be added manually. - /// - /// This allows access to any installed system fonts - /// - /// # Timing - /// - /// This function takes some time to run. On the release build, it can take up to a second, - /// while debug builds can take up to ten times longer. For this reason, it should only be - /// called once, and the resulting [`FontSystem`] should be shared. - /// - /// This should ideally run in a background thread. - // TODO: This should run in a background thread. - pub fn load_system_fonts(&mut self) { + fn load_system_fonts(&mut self) { match self.0.try_lock() { Ok(mut font_system) => { font_system.db_mut().load_system_fonts(); @@ -88,7 +67,10 @@ impl Default for SwashCache { #[derive(Default, Resource)] pub struct TextPipeline { + /// Identifies a font ID by its font asset handle map_handle_to_font_id: HashMap, + /// Identifies a font atlas set handle by its font iD + map_font_id_to_handle: HashMap, font_system: FontSystem, swash_cache: SwashCache, } @@ -253,9 +235,31 @@ impl TextPipeline { .map(|(layout_glyph, line_w, line_y)| { let section_index = layout_glyph.metadata; - let handle_font_atlas: Handle = sections[section_index].style.font.cast_weak(); - let font_atlas_set = font_atlas_set_storage - .get_or_insert_with(handle_font_atlas, FontAtlasSet::default); + let font_atlas_set = match sections[section_index].style.font { + FontRef::Asset(ref font_handle) => { + let handle: Handle = font_handle.cast_weak(); + font_atlas_set_storage + .get_or_insert_with(handle, FontAtlasSet::default) + } + FontRef::Query(ref query) => { + // get the id from the database + // TODO: error handling + // TODO: font may not yet be available, but may be available in future + let font_id = font_system.get_font_matches(cosmic_text::Attrs { + color_opt: None, + family: query.family.as_family(), + stretch: query.stretch, + style: query.style, + weight: query.weight, + metadata: 0, + })[0]; + let handle_id = self.map_font_id_to_handle.entry(font_id).or_insert_with(|| { + let handle = font_atlas_set_storage.add(FontAtlasSet::default()); + handle.id() + }); + font_atlas_set_storage.get_mut(&Handle::weak(*handle_id)).unwrap() + } + }; let atlas_info = font_atlas_set .get_glyph_atlas_info(layout_glyph.cache_key) @@ -365,6 +369,33 @@ impl TextPipeline { buffer: Mutex::new(buffer), }) } + + /// Attempts to load system fonts. + /// + /// Supports Windows, Linux and macOS. + /// + /// System fonts loading is a surprisingly complicated task, + /// mostly unsolvable without interacting with system libraries. + /// And since `fontdb` tries to be small and portable, this method + /// will simply scan some predefined directories. + /// Which means that fonts that are not in those directories must + /// be added manually. + /// + /// This allows access to any installed system fonts + /// + /// # Timing + /// + /// This function takes some time to run. On the release build, it can take up to a second, + /// while debug builds can take up to ten times longer. For this reason, it should only be + /// called once, and the resulting [`FontSystem`] should be shared. + /// + /// This should ideally run in a background thread. + // TODO: This should run in a background thread. + pub fn load_system_fonts(&mut self) { + info!("Loading system fonts"); + self.font_system.load_system_fonts(); + info!("Loaded system fonts"); + } } /// Render information for a corresponding [`Text`](crate::Text) component. @@ -421,40 +452,54 @@ fn add_span( line_text.push_str(line); let end = line_text.len(); - let font_handle = §ion.style.font; - let font_handle_id = font_handle.id(); - let face_id = map_handle_to_font_id - .entry(font_handle_id) - .or_insert_with(|| { - let font = fonts.get(font_handle).unwrap(); - let data = Arc::clone(&font.data); - font_system - .db_mut() - .load_font_source(cosmic_text::fontdb::Source::Binary(data)); - // TODO: it is assumed this is the right font face - // see https://github.com/pop-os/cosmic-text/issues/125 - // fontdb 0.14 returns the font ids from `load_font_source` - let face_id = font_system.db().faces().last().unwrap().id; - // TODO: below may be required if we need to offset by the baseline (TBC) - // see https://github.com/pop-os/cosmic-text/issues/123 - // let font = font_system.get_font(face_id).unwrap(); - // map_font_id_to_metrics - // .entry(face_id) - // .or_insert_with(|| font.as_swash().metrics(&[])); - face_id - }); - let face = font_system.db().face(*face_id).unwrap(); - - // TODO: validate this is the correct string to extract - let family_name = &face.families[0].0; - let attrs = Attrs::new() - // TODO: validate that we can use metadata - .metadata(section_index) - .family(Family::Name(family_name)) - .stretch(face.stretch) - .style(face.style) - .weight(face.weight) - .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())); + let attrs = match section.style.font { + FontRef::Asset(ref font_handle) => { + let font_handle_id = font_handle.id(); + let face_id = map_handle_to_font_id + .entry(font_handle_id) + .or_insert_with(|| { + let font = fonts.get(font_handle).unwrap(); + let data = Arc::clone(&font.data); + font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(data)); + // TODO: it is assumed this is the right font face + // see https://github.com/pop-os/cosmic-text/issues/125 + // fontdb 0.14 returns the font ids from `load_font_source` + let face_id = font_system.db().faces().last().unwrap().id; + // TODO: below may be required if we need to offset by the baseline (TBC) + // see https://github.com/pop-os/cosmic-text/issues/123 + // let font = font_system.get_font(face_id).unwrap(); + // map_font_id_to_metrics + // .entry(face_id) + // .or_insert_with(|| font.as_swash().metrics(&[])); + face_id + }); + let face = font_system.db().face(*face_id).unwrap(); + + // TODO: validate this is the correct string to extract + let family_name = &face.families[0].0; + Attrs::new() + // TODO: validate that we can use metadata + .metadata(section_index) + .family(Family::Name(family_name)) + .stretch(face.stretch) + .style(face.style) + .weight(face.weight) + .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())) + } + FontRef::Query(ref query) => { + Attrs::new() + // TODO: validate that we can use metadata + .metadata(section_index) + .family(query.family.as_family()) + .stretch(query.stretch) + .style(query.style) + .weight(query.weight) + .color(cosmic_text::Color(section.style.color.as_linear_rgba_u32())) + } + }; + attrs_list.add_span(start..end, attrs); } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 99fa391fb4b6c..289dd4444528d 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -6,6 +6,7 @@ use bevy_utils::default; use serde::{Deserialize, Serialize}; use crate::{Font, DEFAULT_FONT_HANDLE}; +pub use cosmic_text::{FamilyOwned, Stretch, Style, Weight}; #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] @@ -149,7 +150,7 @@ pub enum TextAlignment { #[derive(Clone, Debug, Reflect, FromReflect)] pub struct TextStyle { - pub font: Handle, + pub font: FontRef, pub font_size: f32, pub color: Color, } @@ -157,7 +158,7 @@ pub struct TextStyle { impl Default for TextStyle { fn default() -> Self { Self { - font: DEFAULT_FONT_HANDLE.typed(), + font: FontRef::Asset(DEFAULT_FONT_HANDLE.typed()), font_size: 12.0, color: Color::WHITE, } @@ -177,3 +178,32 @@ pub enum BreakLineOn { /// However it may lead to words being broken up across linebreaks. AnyCharacter, } + +#[derive(Clone, Debug, Reflect, FromReflect)] +pub enum FontRef { + /// A reference to a font loaded as a bevy asset. + Asset(Handle), + /// A reference to a font queried by font family and attributes. + /// This is useful for example for fonts that are not loaded as a bevy asset, + /// such as system fonts. + Query(#[reflect(ignore)] FontQuery), +} + +#[derive(Clone, Debug)] +pub struct FontQuery { + pub family: FamilyOwned, + pub stretch: Stretch, + pub style: Style, + pub weight: Weight, +} + +impl Default for FontQuery { + fn default() -> Self { + Self { + family: FamilyOwned::SansSerif, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } +} From 297a7a5b71a69054a17a5ab51d3c943d3816a32b Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 3 Jun 2023 21:41:03 +0800 Subject: [PATCH 16/32] add system fonts example --- Cargo.toml | 10 ++++++++++ examples/README.md | 1 + examples/ui/system_fonts.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 examples/ui/system_fonts.rs diff --git a/Cargo.toml b/Cargo.toml index f8f12c99a1ec4..936c16606bde8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1829,6 +1829,16 @@ description = "Demonstrates how the to use the size constraints to control the s category = "UI (User Interface)" wasm = true +[[example]] +name = "system_fonts" +path = "examples/ui/system_fonts.rs" + +[package.metadata.example.system_fonts] +name = "System Fonts" +description = "Demonstrates using system fonts." +category = "UI (User Interface)" +wasm = false + [[example]] name = "text" path = "examples/ui/text.rs" diff --git a/examples/README.md b/examples/README.md index 4751f537cfabf..86e43ebe46489 100644 --- a/examples/README.md +++ b/examples/README.md @@ -343,6 +343,7 @@ Example | Description [Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior [Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. +[System Fonts](../examples/ui/system_fonts.rs) | Demonstrates using system fonts. [Text](../examples/ui/text.rs) | Illustrates creating and updating text [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping diff --git a/examples/ui/system_fonts.rs b/examples/ui/system_fonts.rs new file mode 100644 index 0000000000000..8482bbddaa223 --- /dev/null +++ b/examples/ui/system_fonts.rs @@ -0,0 +1,29 @@ +//! This example demonstrates using system fonts. + +use bevy::{prelude::*, text::TextPipeline}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(PostStartup, setup2) + .run() +} + +fn setup(mut commands: Commands, mut text_pipeline: ResMut) { + bevy::log::info!("setup"); + text_pipeline.load_system_fonts(); + commands.spawn(Camera2dBundle::default()); +} + +fn setup2(mut commands: Commands) { + bevy::log::info!("setup2"); + commands.spawn(TextBundle::from_sections([TextSection { + value: "Test".into(), + style: TextStyle { + font: bevy::text::FontRef::Query(bevy::text::FontQuery::default()), + font_size: 50.0, + color: Color::WHITE, + }, + }])); +} From 02e317e33aced2bd898b96db86cedeb17e9337f0 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 3 Jun 2023 22:13:36 +0800 Subject: [PATCH 17/32] promote GlyphAtlasInfoNew to sole GlyphAtlasInfo --- crates/bevy_text/src/font_atlas_set.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index d78e70c3c0076..d18c729899e42 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -18,7 +18,7 @@ pub struct FontAtlasSet { } #[derive(Debug, Clone)] -pub struct GlyphAtlasInfoNew { +pub struct GlyphAtlasInfo { pub texture_atlas: Handle, pub glyph_index: usize, pub left: i32, @@ -27,12 +27,6 @@ pub struct GlyphAtlasInfoNew { pub height: u32, } -#[derive(Debug, Clone)] -pub struct GlyphAtlasInfo { - pub texture_atlas: Handle, - pub glyph_index: usize, -} - impl Default for FontAtlasSet { fn default() -> Self { FontAtlasSet { @@ -61,7 +55,7 @@ impl FontAtlasSet { font_system: &mut cosmic_text::FontSystem, swash_cache: &mut cosmic_text::SwashCache, layout_glyph: &cosmic_text::LayoutGlyph, - ) -> Result { + ) -> Result { let font_atlases = self .font_atlases .entry(layout_glyph.cache_key.font_size_bits) @@ -121,7 +115,7 @@ impl FontAtlasSet { pub fn get_glyph_atlas_info( &mut self, cache_key: cosmic_text::CacheKey, - ) -> Option { + ) -> Option { self.font_atlases .get(&cache_key.font_size_bits) .and_then(|font_atlases| { @@ -133,7 +127,7 @@ impl FontAtlasSet { .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) }) .map( - |((glyph_index, left, top, w, h), texture_atlas)| GlyphAtlasInfoNew { + |((glyph_index, left, top, w, h), texture_atlas)| GlyphAtlasInfo { texture_atlas, glyph_index, left, @@ -154,7 +148,7 @@ impl FontAtlasSet { pub struct PositionedGlyph { pub position: Vec2, pub size: Vec2, - pub atlas_info: GlyphAtlasInfoNew, + pub atlas_info: GlyphAtlasInfo, pub section_index: usize, /// 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. From 8694d48bbbff27285ec3e8fb3233b2f54522e4d4 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 4 Jun 2023 21:45:16 +0800 Subject: [PATCH 18/32] fix crash from dropped `Handle` --- crates/bevy_text/src/font_atlas_set.rs | 2 -- crates/bevy_text/src/pipeline.rs | 13 +++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index d18c729899e42..663244e3edbaf 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -9,8 +9,6 @@ use bevy_utils::HashMap; type FontSizeKey = u32; -// TODO: FontAtlasSet is an asset tied to a Handle cast weakly -// This won't work for "font queries" (non-vendored fonts) #[derive(TypeUuid, TypePath)] #[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] pub struct FontAtlasSet { diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 00afb34e863ac..a3b3cbf57eb7c 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -70,7 +70,7 @@ pub struct TextPipeline { /// Identifies a font ID by its font asset handle map_handle_to_font_id: HashMap, /// Identifies a font atlas set handle by its font iD - map_font_id_to_handle: HashMap, + map_font_id_to_handle: HashMap>, font_system: FontSystem, swash_cache: SwashCache, } @@ -253,11 +253,11 @@ impl TextPipeline { weight: query.weight, metadata: 0, })[0]; - let handle_id = self.map_font_id_to_handle.entry(font_id).or_insert_with(|| { - let handle = font_atlas_set_storage.add(FontAtlasSet::default()); - handle.id() - }); - font_atlas_set_storage.get_mut(&Handle::weak(*handle_id)).unwrap() + let handle = self + .map_font_id_to_handle + .entry(font_id) + .or_insert_with(|| font_atlas_set_storage.add(FontAtlasSet::default())); + font_atlas_set_storage.get_mut(handle).unwrap() } }; @@ -524,6 +524,7 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { /// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour. // TODO: upstream to cosmic_text, see https://github.com/pop-os/cosmic-text/pull/124 // TODO: create separate iterator that keeps the ranges, or simply use memory address introspection (as_ptr()) +// TODO: this breaks for lines ending in newlines, e.g. "foo\n" should split into ["foo", ""] but we actually get ["foo"] pub struct BidiParagraphs<'text> { text: &'text str, info: std::vec::IntoIter, From a4241108bebf728d5a096de52c9f98fab0af4a38 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 4 Jun 2023 21:46:56 +0800 Subject: [PATCH 19/32] expand `FontRef`-related APIs --- crates/bevy_text/src/text.rs | 81 +++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 289dd4444528d..0a9da0b038fa7 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -6,6 +6,7 @@ use bevy_utils::default; use serde::{Deserialize, Serialize}; use crate::{Font, DEFAULT_FONT_HANDLE}; +// TODO: reexport cosmic_text and these types in the prelude pub use cosmic_text::{FamilyOwned, Stretch, Style, Weight}; #[derive(Component, Debug, Clone, Reflect)] @@ -186,9 +187,16 @@ pub enum FontRef { /// A reference to a font queried by font family and attributes. /// This is useful for example for fonts that are not loaded as a bevy asset, /// such as system fonts. + // TODO: Support Reflect? Query(#[reflect(ignore)] FontQuery), } +impl From> for FontRef { + fn from(handle: Handle) -> Self { + Self::Asset(handle) + } +} + #[derive(Clone, Debug)] pub struct FontQuery { pub family: FamilyOwned, @@ -197,8 +205,8 @@ pub struct FontQuery { pub weight: Weight, } -impl Default for FontQuery { - fn default() -> Self { +impl FontQuery { + pub fn sans_serif() -> Self { Self { family: FamilyOwned::SansSerif, stretch: Default::default(), @@ -206,4 +214,73 @@ impl Default for FontQuery { weight: Default::default(), } } + + pub fn serif() -> Self { + Self { + family: FamilyOwned::Serif, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn fantasy() -> Self { + Self { + family: FamilyOwned::Fantasy, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn cursive() -> Self { + Self { + family: FamilyOwned::Cursive, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn monospace() -> Self { + Self { + family: FamilyOwned::Monospace, + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn family>(name: S) -> Self { + Self { + family: FamilyOwned::Name(name.as_ref().to_string()), + stretch: Default::default(), + style: Default::default(), + weight: Default::default(), + } + } + + pub fn stretch(self, stretch: Stretch) -> Self { + Self { stretch, ..self } + } + + pub fn style(self, style: Style) -> Self { + Self { style, ..self } + } + + pub fn weight(self, weight: Weight) -> Self { + Self { weight, ..self } + } +} + +impl Default for FontQuery { + fn default() -> Self { + Self::sans_serif() + } +} + +impl From for FontRef { + fn from(query: FontQuery) -> Self { + Self::Query(query) + } } From ff1590adc30cb570fe6bc3d5e3a84b82baf66eee Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 4 Jun 2023 22:13:06 +0800 Subject: [PATCH 20/32] improve `system_fonts` example --- examples/ui/system_fonts.rs | 95 ++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/examples/ui/system_fonts.rs b/examples/ui/system_fonts.rs index 8482bbddaa223..d380c7b778e6a 100644 --- a/examples/ui/system_fonts.rs +++ b/examples/ui/system_fonts.rs @@ -1,29 +1,98 @@ //! This example demonstrates using system fonts. -use bevy::{prelude::*, text::TextPipeline}; +use bevy::{ + prelude::*, + text::{FontQuery, TextPipeline}, +}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(PostStartup, setup2) - .run() + .run(); } fn setup(mut commands: Commands, mut text_pipeline: ResMut) { - bevy::log::info!("setup"); text_pipeline.load_system_fonts(); commands.spawn(Camera2dBundle::default()); -} -fn setup2(mut commands: Commands) { - bevy::log::info!("setup2"); - commands.spawn(TextBundle::from_sections([TextSection { - value: "Test".into(), + let text_style = TextStyle { + font_size: 50.0, + color: Color::WHITE, + ..default() + }; + let mut sections = vec![]; + + // the default font is sans-serif + sections.push(TextSection { + value: "Default font".into(), + style: TextStyle { + font: FontQuery::default().into(), + ..text_style + }, + }); + + // sans-serif + sections.push(TextSection { + value: "\nsans-serif".into(), + style: TextStyle { + font: FontQuery::sans_serif().into(), + ..text_style + }, + }); + + // serif + sections.push(TextSection { + value: "\nserif".into(), + style: TextStyle { + font: FontQuery::serif().into(), + ..text_style + }, + }); + + // fantasy + sections.push(TextSection { + value: "\nfantasy".into(), + style: TextStyle { + font: FontQuery::fantasy().into(), + ..text_style + }, + }); + + // cursive + sections.push(TextSection { + value: "\ncursive".into(), style: TextStyle { - font: bevy::text::FontRef::Query(bevy::text::FontQuery::default()), - font_size: 50.0, - color: Color::WHITE, + font: FontQuery::cursive().into(), + ..text_style }, - }])); + }); + + // monospace + sections.push(TextSection { + value: "\nmonospace".into(), + style: TextStyle { + font: FontQuery::monospace().into(), + ..text_style + }, + }); + + // you can also refer to families by name + for family in [ + "Arial", + "Comic Sans MS", + "Impact", + "Courier New", + "Times New Roman", + "A fallback font for fonts that can't be found", + ] { + sections.push(TextSection { + value: "\n".to_string() + family, + style: TextStyle { + font: FontQuery::family(family).into(), + ..text_style + }, + }) + } + commands.spawn(TextBundle::from_sections(sections)); } From 193611dac812eda99535e43dcde0c270936e36b1 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 4 Jun 2023 22:14:10 +0800 Subject: [PATCH 21/32] update examples with `FontRef` not `Handle` --- examples/2d/text2d.rs | 4 ++-- examples/games/contributors.rs | 4 ++-- examples/input/text_input.rs | 14 +++++++------- examples/stress_tests/text_pipeline.rs | 4 ++-- examples/ui/button.rs | 2 +- examples/ui/flex_layout.rs | 2 +- examples/ui/font_atlas_debug.rs | 2 +- examples/ui/grid.rs | 6 +++--- examples/ui/overflow.rs | 2 +- examples/ui/overflow_debug.rs | 4 ++-- examples/ui/relative_cursor_position.rs | 2 +- examples/ui/size_constraints.rs | 2 +- examples/ui/text.rs | 6 +++--- examples/ui/text_debug.rs | 18 +++++++++--------- examples/ui/text_wrap_debug.rs | 2 +- examples/ui/transparency_ui.rs | 4 ++-- examples/ui/ui.rs | 7 ++++--- examples/ui/ui_scaling.rs | 2 +- examples/ui/window_fallthrough.rs | 2 +- 19 files changed, 45 insertions(+), 44 deletions(-) diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 8fe1b4b05b642..78b8ca0fd657b 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -34,7 +34,7 @@ struct AnimateScale; fn setup(mut commands: Commands, asset_server: Res) { let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let text_style = TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 60.0, color: Color::WHITE, }; @@ -68,7 +68,7 @@ fn setup(mut commands: Commands, asset_server: Res) { )); // Demonstrate text wrapping let slightly_smaller_text_style = TextStyle { - font, + font: font.into(), font_size: 42.0, color: Color::WHITE, }; diff --git a/examples/games/contributors.rs b/examples/games/contributors.rs index 1ca223c35d0b7..d4bb43adf066c 100644 --- a/examples/games/contributors.rs +++ b/examples/games/contributors.rs @@ -144,13 +144,13 @@ fn setup(mut commands: Commands, asset_server: Res) { TextSection::new( "Contributor showcase", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 60.0, color: Color::WHITE, }, ), TextSection::from_style(TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 60.0, color: Color::WHITE, }), diff --git a/examples/input/text_input.rs b/examples/input/text_input.rs index 8140325094df2..4d7f0cdfe6efd 100644 --- a/examples/input/text_input.rs +++ b/examples/input/text_input.rs @@ -33,7 +33,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "IME Enabled: ".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 20.0, color: Color::WHITE, }, @@ -41,7 +41,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "false\n".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 30.0, color: Color::WHITE, }, @@ -49,7 +49,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "IME Active: ".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 20.0, color: Color::WHITE, }, @@ -57,7 +57,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "false\n".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 30.0, color: Color::WHITE, }, @@ -65,7 +65,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "click to toggle IME, press return to start a new line\n\n".to_string(), style: TextStyle { - font: font.clone_weak(), + font: font.clone_weak().into(), font_size: 18.0, color: Color::WHITE, }, @@ -73,7 +73,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { TextSection { value: "".to_string(), style: TextStyle { - font, + font: font.into(), font_size: 25.0, color: Color::WHITE, }, @@ -91,7 +91,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { text: Text::from_section( "".to_string(), TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 100.0, color: Color::WHITE, }, diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index 64ffe57ae76df..7c6e974636085 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -35,7 +35,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { TextSection { value: "text".repeat(i), style: TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: (4 + i % 10) as f32, color: Color::BLUE, }, @@ -43,7 +43,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { TextSection { value: "pipeline".repeat(i), style: TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: (4 + i % 11) as f32, color: Color::YELLOW, }, diff --git a/examples/ui/button.rs b/examples/ui/button.rs index aed15cb1142fe..39e71095078d8 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -75,7 +75,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 40.0, color: Color::rgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/flex_layout.rs b/examples/ui/flex_layout.rs index 6e7c736ee24e1..c37be6c398a61 100644 --- a/examples/ui/flex_layout.rs +++ b/examples/ui/flex_layout.rs @@ -177,7 +177,7 @@ fn spawn_nested_text_bundle( builder.spawn(TextBundle::from_section( text, TextStyle { - font, + font: font.into(), font_size: 24.0, color: Color::BLACK, }, diff --git a/examples/ui/font_atlas_debug.rs b/examples/ui/font_atlas_debug.rs index 54f1fe42f975d..51a2b7714390d 100644 --- a/examples/ui/font_atlas_debug.rs +++ b/examples/ui/font_atlas_debug.rs @@ -92,7 +92,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut state: ResM parent.spawn(TextBundle::from_section( "a", TextStyle { - font: font_handle, + font: font_handle.into(), font_size: 60.0, color: Color::YELLOW, }, diff --git a/examples/ui/grid.rs b/examples/ui/grid.rs index 9012e198d8708..e9f1421c8e914 100644 --- a/examples/ui/grid.rs +++ b/examples/ui/grid.rs @@ -142,7 +142,7 @@ fn spawn_layout(mut commands: Commands, asset_server: Res) { builder.spawn(TextBundle::from_section( "Sidebar", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 24.0, color: Color::WHITE, }, @@ -150,7 +150,7 @@ fn spawn_layout(mut commands: Commands, asset_server: Res) { builder.spawn(TextBundle::from_section( "A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely. A paragraph of text which ought to wrap nicely.", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 16.0, color: Color::WHITE, }, @@ -226,7 +226,7 @@ fn spawn_nested_text_bundle(builder: &mut ChildBuilder, font: Handle, text builder.spawn(TextBundle::from_section( text, TextStyle { - font, + font: font.into(), font_size: 24.0, color: Color::BLACK, }, diff --git a/examples/ui/overflow.rs b/examples/ui/overflow.rs index 12c922c92d993..5e036206e77ef 100644 --- a/examples/ui/overflow.rs +++ b/examples/ui/overflow.rs @@ -15,7 +15,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 20.0, color: Color::WHITE, }; diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index 8b71856616895..7ba4a484e101f 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -109,7 +109,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ] .join(" · "), TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 18.0, color: Color::WHITE, }, @@ -184,7 +184,7 @@ fn spawn_text( parent.spawn(TextBundle::from_section( "Bevy", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 120.0, color: Color::WHITE, }, diff --git a/examples/ui/relative_cursor_position.rs b/examples/ui/relative_cursor_position.rs index 57eba52064ed9..45061a586b79e 100644 --- a/examples/ui/relative_cursor_position.rs +++ b/examples/ui/relative_cursor_position.rs @@ -45,7 +45,7 @@ fn setup(mut commands: Commands, asset_server: Res) { text: Text::from_section( "(0.0, 0.0)", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 40.0, color: Color::rgb(0.9, 0.9, 0.9), }, diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index 562a892bd93c1..701e8c66f5479 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -43,7 +43,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 40.0, color: Color::rgb(0.9, 0.9, 0.9), }; diff --git a/examples/ui/text.rs b/examples/ui/text.rs index 841358c2bd3b2..863ac986aa681 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -35,7 +35,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // Accepts a `String` or any type that converts into a `String`, such as `&str` "hello\nbevy!", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 100.0, color: Color::WHITE, }, @@ -57,13 +57,13 @@ fn setup(mut commands: Commands, asset_server: Res) { TextSection::new( "FPS: ", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 60.0, color: Color::WHITE, }, ), TextSection::from_style(TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 60.0, color: Color::GOLD, }), diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index e1756d3792074..053fbe9638c16 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -31,7 +31,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "This is\ntext with\nline breaks\nin the top left", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 50.0, color: Color::WHITE, }, @@ -46,7 +46,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { commands.spawn(TextBundle::from_section( "This text is very long, has a limited width, is centered, is positioned in the top right and is also colored pink.", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 50.0, color: Color::rgb(0.8, 0.2, 0.7), }, @@ -65,7 +65,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextSection::new( "This text changes in the bottom right", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::WHITE, }, @@ -73,33 +73,33 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextSection::new( "\nThis text changes in the bottom right - ", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::RED, }, ), TextSection::from_style(TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::ORANGE_RED, }), TextSection::new( " fps, ", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::YELLOW, }, ), TextSection::from_style(TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::GREEN, }), TextSection::new( " ms/frame", TextStyle { - font: font.clone(), + font: font.clone().into(), font_size: 30.0, color: Color::BLUE, }, @@ -117,7 +117,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "This\ntext has\nline breaks and also a set width in the bottom left", TextStyle { - font, + font: font.into(), font_size: 50.0, color: Color::WHITE, }, diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 0ef93cf6c25cd..dd4ccc1c6c0a4 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -16,7 +16,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 14.0, color: Color::WHITE, }; diff --git a/examples/ui/transparency_ui.rs b/examples/ui/transparency_ui.rs index 2a6fe7f9b93d4..e8301b01d7ded 100644 --- a/examples/ui/transparency_ui.rs +++ b/examples/ui/transparency_ui.rs @@ -43,7 +43,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button 1", TextStyle { - font: font_handle.clone(), + font: font_handle.clone().into(), font_size: 40.0, // Alpha channel of the color controls transparency. color: Color::rgba(1.0, 1.0, 1.0, 0.2), @@ -69,7 +69,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(TextBundle::from_section( "Button 2", TextStyle { - font: font_handle.clone(), + font: font_handle.clone().into(), font_size: 40.0, // Alpha channel of the color controls transparency. color: Color::rgba(1.0, 1.0, 1.0, 0.2), diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index d338446f9136b..e3963e8c6bd71 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -64,7 +64,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "Text Example", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 30.0, color: Color::WHITE, }, @@ -99,7 +99,7 @@ fn setup(mut commands: Commands, asset_server: Res) { TextBundle::from_section( "Scrolling list", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 25., color: Color::WHITE, }, @@ -142,7 +142,8 @@ fn setup(mut commands: Commands, asset_server: Res) { format!("Item {i}"), TextStyle { font: asset_server - .load("fonts/FiraSans-Bold.ttf"), + .load("fonts/FiraSans-Bold.ttf") + .into(), font_size: 20., color: Color::WHITE, }, diff --git a/examples/ui/ui_scaling.rs b/examples/ui/ui_scaling.rs index 2b4c8551e6e83..6c02c40524515 100644 --- a/examples/ui/ui_scaling.rs +++ b/examples/ui/ui_scaling.rs @@ -28,7 +28,7 @@ fn setup(mut commands: Commands, asset_server: ResMut) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: 16., color: Color::BLACK, }; diff --git a/examples/ui/window_fallthrough.rs b/examples/ui/window_fallthrough.rs index daf744aeae5ee..4136901d37783 100644 --- a/examples/ui/window_fallthrough.rs +++ b/examples/ui/window_fallthrough.rs @@ -32,7 +32,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // Accepts a `String` or any type that converts into a `String`, such as `&str` "Hit 'P' then scroll/click around!", TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: 100.0, // Nice and big so you can see it! color: Color::WHITE, }, From cbfbfdf14cecc1b59fd0aa3a8f1d3002bb781ad4 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 10 Jun 2023 19:36:02 +0800 Subject: [PATCH 22/32] fix lines not breaking when sections end with \n --- crates/bevy_text/src/pipeline.rs | 102 +++++++++++++++++++------------ examples/ui/system_fonts.rs | 14 ++--- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index a3b3cbf57eb7c..81f495dc81faf 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -18,14 +18,12 @@ use crate::{ TextAlignment, TextSection, TextSettings, YAxisOrientation, }; -// TODO: introduce FontQuery enum instead of Handle // TODO: cache buffers / store buffers on the entity // TODO: reconstruct byte indices // TODO: rescale font sizes in all examples // TODO: fix any broken examples -// TODO: solve spans with different font sizes +// TODO: solve spans with different font sizes, see https://github.com/pop-os/cosmic-text/issues/64 // TODO: (future work) split text entities into section entities -// TODO: (future work) support emojis // TODO: (future work) text editing // TODO: font validation @@ -108,7 +106,7 @@ impl TextPipeline { buffer.lines.clear(); let mut attrs_list = AttrsList::new(Attrs::new()); - let mut line_text = String::new(); + let mut line_string = String::new(); // all sections need to be combined and broken up into lines // e.g. // style0"Lorem ipsum\ndolor sit amet," @@ -124,48 +122,75 @@ impl TextPipeline { // line3: style2"incididunt" // style3"ut labore et dolore" // line4: style3"magna aliqua." - for (section_index, section) in sections.iter().enumerate() { - // We can't simply use `let mut lines = section.value.lines()` because - // `unicode-bidi` used by `cosmic_text` doesn't have the same newline behaviour: it breaks on `\r` for example. - // In example `font_atlas_debug`, eventually a `\r` character is inserted and there is a panic in shaping. - let mut lines = BidiParagraphs::new(§ion.value); - - // continue the current line, adding spans - if let Some(line) = lines.next() { + + // combine all sections into a string + // as well as metadata that links those sections to that string + let mut end = 0; + let (string, sections_data): (String, Vec<_>) = sections + .iter() + .enumerate() + .map(|(section_index, section)| { + let start = end; + end += section.value.len(); + (section.value.as_str(), (section, section_index, start..end)) + }) + .unzip(); + + let mut sections_iter = sections_data.into_iter(); + let mut maybe_section = sections_iter.next(); + + // split the string into lines, as ranges + let string_start = string.as_ptr() as usize; + let mut lines_iter = BidiParagraphs::new(&string).map(|line: &str| { + let start = line.as_ptr() as usize - string_start; + let end = start + line.len(); + start..end + }); + let mut maybe_line = lines_iter.next(); + + loop { + let (Some(line_range), Some((section, section_index, section_range))) = (&maybe_line, &maybe_section) else { + // if there are no more lines or no more sections we're done. + buffer.lines.push(BufferLine::new(line_string, attrs_list)); + break; + }; + + // start..end is the intersection of this line and this section + let start = line_range.start.max(section_range.start); + let end = line_range.end.min(section_range.end); + if start < end { + let text = &string[start..end]; add_span( - &mut line_text, + &mut line_string, &mut attrs_list, section, - section_index, - line, + *section_index, + text, font_system, &mut self.map_handle_to_font_id, fonts, ); } - // for any remaining lines in this section - for line in lines { + + // we know that at the end of a line, + // section text's end index is always >= line text's end index + // so if this section ends before this line ends, + // there is another section in this line. + // otherwise, we move on to the next line. + if section_range.end < line_range.end { + maybe_section = sections_iter.next(); + } else { // finalise this line and start a new line let prev_attrs_list = std::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); - let prev_line_text = std::mem::take(&mut line_text); + let prev_line_string = std::mem::take(&mut line_string); buffer .lines - .push(BufferLine::new(prev_line_text, prev_attrs_list)); - add_span( - &mut line_text, - &mut attrs_list, - section, - section_index, - line, - font_system, - &mut self.map_handle_to_font_id, - fonts, - ); + .push(BufferLine::new(prev_line_string, prev_attrs_list)); + + maybe_line = lines_iter.next(); } } - // finalise last line - buffer.lines.push(BufferLine::new(line_text, attrs_list)); // node size (bounds) is already scaled by the systems that call queue_text // TODO: cosmic text does not shape/layout text outside the buffer height @@ -197,10 +222,9 @@ impl TextPipeline { pub fn queue_text( &mut self, fonts: &Assets, - // TODO: TextSection should support referencing fonts via "Font Query" (Family, Stretch, Weight and Style) sections: &[TextSection], scale_factor: f64, - // TODO: Implement text alignment + // TODO: Implement text alignment properly text_alignment: TextAlignment, linebreak_behavior: BreakLineOn, bounds: Vec2, @@ -439,18 +463,18 @@ impl TextMeasureInfo { /// loading fonts into the DB if required. #[allow(clippy::too_many_arguments)] fn add_span( - line_text: &mut String, + line_string: &mut String, attrs_list: &mut AttrsList, section: &TextSection, section_index: usize, - line: &str, + text: &str, font_system: &mut cosmic_text::FontSystem, map_handle_to_font_id: &mut HashMap, fonts: &Assets, ) { - let start = line_text.len(); - line_text.push_str(line); - let end = line_text.len(); + let start = line_string.len(); + line_string.push_str(text); + let end = line_string.len(); let attrs = match section.style.font { FontRef::Asset(ref font_handle) => { @@ -511,7 +535,7 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { .map(|run| run.line_w) .reduce(|max_w, w| max_w.max(w)) .unwrap(); - // TODO: support multiple line heights / font sizes (once supported by cosmic text) + // TODO: support multiple line heights / font sizes (once supported by cosmic text), see https://github.com/pop-os/cosmic-text/issues/64 let line_height = buffer.metrics().line_height.ceil(); let height = buffer.layout_runs().count() as f32 * line_height; diff --git a/examples/ui/system_fonts.rs b/examples/ui/system_fonts.rs index d380c7b778e6a..78f44ed4be8b1 100644 --- a/examples/ui/system_fonts.rs +++ b/examples/ui/system_fonts.rs @@ -25,7 +25,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { // the default font is sans-serif sections.push(TextSection { - value: "Default font".into(), + value: "Default font\n".to_string(), style: TextStyle { font: FontQuery::default().into(), ..text_style @@ -34,7 +34,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { // sans-serif sections.push(TextSection { - value: "\nsans-serif".into(), + value: "sans-serif\n".to_string(), style: TextStyle { font: FontQuery::sans_serif().into(), ..text_style @@ -43,7 +43,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { // serif sections.push(TextSection { - value: "\nserif".into(), + value: "serif\n".to_string(), style: TextStyle { font: FontQuery::serif().into(), ..text_style @@ -52,7 +52,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { // fantasy sections.push(TextSection { - value: "\nfantasy".into(), + value: "fantasy\n".to_string(), style: TextStyle { font: FontQuery::fantasy().into(), ..text_style @@ -61,7 +61,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { // cursive sections.push(TextSection { - value: "\ncursive".into(), + value: "cursive\n".to_string(), style: TextStyle { font: FontQuery::cursive().into(), ..text_style @@ -70,7 +70,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { // monospace sections.push(TextSection { - value: "\nmonospace".into(), + value: "monospace\n".to_string(), style: TextStyle { font: FontQuery::monospace().into(), ..text_style @@ -87,7 +87,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { "A fallback font for fonts that can't be found", ] { sections.push(TextSection { - value: "\n".to_string() + family, + value: family.to_string() + "\n", style: TextStyle { font: FontQuery::family(family).into(), ..text_style From 4b1f6f18107be3f5c2b4a75a596df391bba74232 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 10 Jun 2023 19:39:55 +0800 Subject: [PATCH 23/32] enhance system_fonts example --- examples/ui/system_fonts.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/examples/ui/system_fonts.rs b/examples/ui/system_fonts.rs index 78f44ed4be8b1..fa2e092bdf46d 100644 --- a/examples/ui/system_fonts.rs +++ b/examples/ui/system_fonts.rs @@ -17,7 +17,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { commands.spawn(Camera2dBundle::default()); let text_style = TextStyle { - font_size: 50.0, + font_size: 42.0, color: Color::WHITE, ..default() }; @@ -25,7 +25,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { // the default font is sans-serif sections.push(TextSection { - value: "Default font\n".to_string(), + value: "(The default font)\n".to_string(), style: TextStyle { font: FontQuery::default().into(), ..text_style @@ -84,7 +84,7 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { "Impact", "Courier New", "Times New Roman", - "A fallback font for fonts that can't be found", + "(A fallback when not found)", ] { sections.push(TextSection { value: family.to_string() + "\n", @@ -94,5 +94,24 @@ fn setup(mut commands: Commands, mut text_pipeline: ResMut) { }, }) } + + // bidirectional text + sections.push(TextSection { + value: "We can even render اللغة العربية and\n".to_string(), + style: TextStyle { + font: FontQuery::serif().into(), + ..text_style + }, + }); + + // and emojis + sections.push(TextSection { + value: "emojis: 🐣🐤🐥🐔🐓🦃🐦🐧🕊️🦅🦆🦢🦉🦩🦚🦜\n".to_string(), + style: TextStyle { + font: FontQuery::cursive().into(), + ..text_style + }, + }); + commands.spawn(TextBundle::from_sections(sections)); } From d8b1afe99b586e0ef075238a741506d2c655cf1d Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 10 Jun 2023 20:42:54 +0800 Subject: [PATCH 24/32] fix extra line bug --- crates/bevy_text/src/pipeline.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 81f495dc81faf..27f45dbc6939a 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -150,8 +150,7 @@ impl TextPipeline { loop { let (Some(line_range), Some((section, section_index, section_range))) = (&maybe_line, &maybe_section) else { - // if there are no more lines or no more sections we're done. - buffer.lines.push(BufferLine::new(line_string, attrs_list)); + // this is reached only if this text is empty break; }; @@ -180,15 +179,20 @@ impl TextPipeline { if section_range.end < line_range.end { maybe_section = sections_iter.next(); } else { - // finalise this line and start a new line - let prev_attrs_list = - std::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); - let prev_line_string = std::mem::take(&mut line_string); - buffer - .lines - .push(BufferLine::new(prev_line_string, prev_attrs_list)); - maybe_line = lines_iter.next(); + if maybe_line.is_some() { + // finalize this line and start a new line + let prev_attrs_list = + std::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); + let prev_line_string = std::mem::take(&mut line_string); + buffer + .lines + .push(BufferLine::new(prev_line_string, prev_attrs_list)); + } else { + // finalize the final line + buffer.lines.push(BufferLine::new(line_string, attrs_list)); + break; + } } } @@ -545,7 +549,7 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { } /// An iterator over the paragraphs in the input text. -/// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour. +/// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behavior. // TODO: upstream to cosmic_text, see https://github.com/pop-os/cosmic-text/pull/124 // TODO: create separate iterator that keeps the ranges, or simply use memory address introspection (as_ptr()) // TODO: this breaks for lines ending in newlines, e.g. "foo\n" should split into ["foo", ""] but we actually get ["foo"] @@ -556,7 +560,7 @@ pub struct BidiParagraphs<'text> { impl<'text> BidiParagraphs<'text> { /// Create an iterator to split the input text into paragraphs - /// in accordance with `unicode-bidi` behaviour. + /// in accordance with `unicode-bidi` behavior. pub fn new(text: &'text str) -> Self { let info = unicode_bidi::BidiInfo::new(text, None); let info = info.paragraphs.into_iter(); From ce44ab450ce6f61b660ddc80f8ad9ff46e448a79 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 11 Jun 2023 09:53:42 +0800 Subject: [PATCH 25/32] replace `(i32, i32, u32, u32)` with `Placement` --- crates/bevy_text/src/font.rs | 8 +++---- crates/bevy_text/src/font_atlas.rs | 15 ++++-------- crates/bevy_text/src/font_atlas_set.rs | 33 ++++++++------------------ crates/bevy_text/src/pipeline.rs | 6 ++--- 4 files changed, 20 insertions(+), 42 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 470fec53c08dd..17eef6712c45f 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -3,6 +3,7 @@ use bevy_render::{ render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::Image, }; +use cosmic_text::Placement; #[derive(Debug, TypeUuid, TypePath, Clone)] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] @@ -23,7 +24,7 @@ impl Font { font_system: &mut cosmic_text::FontSystem, swash_cache: &mut cosmic_text::SwashCache, layout_glyph: &cosmic_text::LayoutGlyph, - ) -> (Image, i32, i32, u32, u32) { + ) -> (Image, Placement) { // TODO: consider using cosmic_text's own caching mechanism let image = swash_cache .get_image_uncached(font_system, layout_glyph.cache_key) @@ -58,10 +59,7 @@ impl Font { data, TextureFormat::Rgba8UnormSrgb, ), - image.placement.left, - image.placement.top, - width, - height, + image.placement, ) } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 64fb96c76cd78..2145c568df26f 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -6,10 +6,11 @@ use bevy_render::{ }; use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; use bevy_utils::HashMap; +use cosmic_text::Placement; pub struct FontAtlas { pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - pub glyph_to_atlas_index: HashMap, + pub glyph_to_atlas_index: HashMap, pub texture_atlas: Handle, } @@ -37,10 +38,7 @@ impl FontAtlas { } } - pub fn get_glyph_index( - &self, - cache_key: cosmic_text::CacheKey, - ) -> Option<(usize, i32, i32, u32, u32)> { + pub fn get_glyph_index(&self, cache_key: cosmic_text::CacheKey) -> Option<(usize, Placement)> { self.glyph_to_atlas_index.get(&cache_key).copied() } @@ -54,10 +52,7 @@ impl FontAtlas { texture_atlases: &mut Assets, cache_key: cosmic_text::CacheKey, texture: &Image, - left: i32, - top: i32, - width: u32, - height: u32, + placement: Placement, ) -> bool { let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); if let Some(index) = @@ -65,7 +60,7 @@ impl FontAtlas { .add_texture(texture_atlas, textures, texture) { self.glyph_to_atlas_index - .insert(cache_key, (index, left, top, width, height)); + .insert(cache_key, (index, placement)); true } else { false diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 663244e3edbaf..9a6265512d256 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -6,6 +6,7 @@ use bevy_reflect::TypeUuid; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::HashMap; +use cosmic_text::Placement; type FontSizeKey = u32; @@ -19,10 +20,7 @@ pub struct FontAtlasSet { pub struct GlyphAtlasInfo { pub texture_atlas: Handle, pub glyph_index: usize, - pub left: i32, - pub top: i32, - pub width: u32, - pub height: u32, + pub placement: Placement, } impl Default for FontAtlasSet { @@ -65,7 +63,7 @@ impl FontAtlasSet { )] }); - let (glyph_texture, left, top, w, h) = + let (glyph_texture, placement) = Font::get_outlined_glyph_texture(font_system, swash_cache, layout_glyph); let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { atlas.add_glyph( @@ -73,10 +71,7 @@ impl FontAtlasSet { texture_atlases, layout_glyph.cache_key, &glyph_texture, - left, - top, - w, - h, + placement, ) }; if !font_atlases.iter_mut().any(add_char_to_font_atlas) { @@ -98,10 +93,7 @@ impl FontAtlasSet { texture_atlases, layout_glyph.cache_key, &glyph_texture, - left, - top, - w, - h, + placement, ) { return Err(TextError::FailedToAddGlyph(layout_glyph.cache_key.glyph_id)); } @@ -124,16 +116,11 @@ impl FontAtlasSet { .get_glyph_index(cache_key) .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) }) - .map( - |((glyph_index, left, top, w, h), texture_atlas)| GlyphAtlasInfo { - texture_atlas, - glyph_index, - left, - top, - width: w, - height: h, - }, - ) + .map(|((glyph_index, placement), texture_atlas)| GlyphAtlasInfo { + texture_atlas, + glyph_index, + placement, + }) }) } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 27f45dbc6939a..269e4086332f7 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -306,11 +306,9 @@ impl TextPipeline { let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); let glyph_rect = texture_atlas.textures[atlas_info.glyph_index]; - let left = atlas_info.left as f32; - let top = atlas_info.top as f32; + let left = atlas_info.placement.left as f32; + let top = atlas_info.placement.top as f32; let glyph_size = Vec2::new(glyph_rect.width(), glyph_rect.height()); - assert_eq!(atlas_info.width as f32, glyph_size.x); - assert_eq!(atlas_info.height as f32, glyph_size.y); // offset by half the size because the origin is center let x = glyph_size.x / 2.0 + left + layout_glyph.x_int as f32; From ab8df892798442cf912717f310171b154da25860 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 11 Jun 2023 11:17:01 +0800 Subject: [PATCH 26/32] extract `(glyph_index, placement)` to `*Location` --- crates/bevy_text/src/font_atlas.rs | 24 +++++++++++++++++++----- crates/bevy_text/src/font_atlas_set.rs | 12 +++++------- crates/bevy_text/src/pipeline.rs | 7 ++++--- crates/bevy_text/src/text2d.rs | 2 +- crates/bevy_ui/src/render/mod.rs | 2 +- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 2145c568df26f..51fd5bdca40cd 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -8,9 +8,18 @@ use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; use bevy_utils::HashMap; use cosmic_text::Placement; +/// The location of a glyph in an atlas, and how it is positioned +#[derive(Debug, Clone, Copy)] +pub struct GlyphAtlasLocation { + /// The index of the glyph in the atlas + pub glyph_index: usize, + /// The positioning (offset), and size of the glyph + pub placement: Placement, +} + pub struct FontAtlas { pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, - pub glyph_to_atlas_index: HashMap, + pub glyph_to_atlas_index: HashMap, pub texture_atlas: Handle, } @@ -38,7 +47,7 @@ impl FontAtlas { } } - pub fn get_glyph_index(&self, cache_key: cosmic_text::CacheKey) -> Option<(usize, Placement)> { + pub fn get_glyph_index(&self, cache_key: cosmic_text::CacheKey) -> Option { self.glyph_to_atlas_index.get(&cache_key).copied() } @@ -55,12 +64,17 @@ impl FontAtlas { placement: Placement, ) -> bool { let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); - if let Some(index) = + if let Some(glyph_index) = self.dynamic_texture_atlas_builder .add_texture(texture_atlas, textures, texture) { - self.glyph_to_atlas_index - .insert(cache_key, (index, placement)); + self.glyph_to_atlas_index.insert( + cache_key, + GlyphAtlasLocation { + glyph_index, + placement, + }, + ); true } else { false diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 9a6265512d256..4ed4851fe9381 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,3 +1,4 @@ +use crate::GlyphAtlasLocation; use crate::{error::TextError, Font, FontAtlas}; use bevy_asset::{Assets, Handle}; use bevy_math::Vec2; @@ -6,7 +7,6 @@ use bevy_reflect::TypeUuid; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::HashMap; -use cosmic_text::Placement; type FontSizeKey = u32; @@ -19,8 +19,7 @@ pub struct FontAtlasSet { #[derive(Debug, Clone)] pub struct GlyphAtlasInfo { pub texture_atlas: Handle, - pub glyph_index: usize, - pub placement: Placement, + pub location: GlyphAtlasLocation, } impl Default for FontAtlasSet { @@ -114,12 +113,11 @@ impl FontAtlasSet { .find_map(|atlas| { atlas .get_glyph_index(cache_key) - .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) + .map(|location| (location, atlas.texture_atlas.clone_weak())) }) - .map(|((glyph_index, placement), texture_atlas)| GlyphAtlasInfo { + .map(|(location, texture_atlas)| GlyphAtlasInfo { texture_atlas, - glyph_index, - placement, + location, }) }) } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 269e4086332f7..3650f69e0423f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -305,9 +305,10 @@ impl TextPipeline { } let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let glyph_rect = texture_atlas.textures[atlas_info.glyph_index]; - let left = atlas_info.placement.left as f32; - let top = atlas_info.placement.top as f32; + let location = atlas_info.location; + let glyph_rect = texture_atlas.textures[location.glyph_index]; + let left = location.placement.left as f32; + let top = location.placement.top as f32; let glyph_size = Vec2::new(glyph_rect.width(), glyph_rect.height()); // offset by half the size because the origin is center diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index bad25698d1124..2e24abaf518c1 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -129,7 +129,7 @@ pub fn extract_text2d_sprite( entity, transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, - rect: Some(atlas.textures[atlas_info.glyph_index]), + rect: Some(atlas.textures[atlas_info.location.glyph_index]), custom_size: None, image_handle_id: atlas.texture.id(), flip_x: false, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index f203c4b227565..42a2a4e85bdcd 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -323,7 +323,7 @@ pub fn extract_text_uinodes( } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let mut rect = atlas.textures[atlas_info.glyph_index]; + let mut rect = atlas.textures[atlas_info.location.glyph_index]; rect.min *= inverse_scale_factor; rect.max *= inverse_scale_factor; extracted_uinodes.uinodes.push(ExtractedUiNode { From 117b6c387935c67cbb67feb189f88476750e4987 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 11 Jun 2023 12:20:29 +0800 Subject: [PATCH 27/32] replace `Placement` with `IVec2` offset --- crates/bevy_text/src/font.rs | 14 +++++++++----- crates/bevy_text/src/font_atlas.rs | 14 +++++++------- crates/bevy_text/src/font_atlas_set.rs | 9 ++++----- crates/bevy_text/src/pipeline.rs | 4 ++-- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 17eef6712c45f..1441661993a3d 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,9 +1,9 @@ +use bevy_math::IVec2; use bevy_reflect::{TypePath, TypeUuid}; use bevy_render::{ render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::Image, }; -use cosmic_text::Placement; #[derive(Debug, TypeUuid, TypePath, Clone)] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] @@ -24,15 +24,19 @@ impl Font { font_system: &mut cosmic_text::FontSystem, swash_cache: &mut cosmic_text::SwashCache, layout_glyph: &cosmic_text::LayoutGlyph, - ) -> (Image, Placement) { + ) -> (Image, IVec2) { // TODO: consider using cosmic_text's own caching mechanism let image = swash_cache .get_image_uncached(font_system, layout_glyph.cache_key) // TODO: don't unwrap .unwrap(); - let width = image.placement.width; - let height = image.placement.height; + let cosmic_text::Placement { + left, + top, + width, + height, + } = image.placement; let data = match image.content { cosmic_text::SwashContent::Mask => image @@ -59,7 +63,7 @@ impl Font { data, TextureFormat::Rgba8UnormSrgb, ), - image.placement, + IVec2::new(left, top), ) } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 51fd5bdca40cd..0314bac4fb7ca 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,20 +1,20 @@ use bevy_asset::{Assets, Handle}; -use bevy_math::Vec2; +use bevy_math::{IVec2, Vec2}; use bevy_render::{ render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::Image, }; use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; use bevy_utils::HashMap; -use cosmic_text::Placement; -/// The location of a glyph in an atlas, and how it is positioned +/// The location of a glyph in an atlas, +/// and how it is positioned when placed #[derive(Debug, Clone, Copy)] pub struct GlyphAtlasLocation { /// The index of the glyph in the atlas pub glyph_index: usize, - /// The positioning (offset), and size of the glyph - pub placement: Placement, + /// The required offset (relative positioning) when placed + pub offset: IVec2, } pub struct FontAtlas { @@ -61,7 +61,7 @@ impl FontAtlas { texture_atlases: &mut Assets, cache_key: cosmic_text::CacheKey, texture: &Image, - placement: Placement, + offset: IVec2, ) -> bool { let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); if let Some(glyph_index) = @@ -72,7 +72,7 @@ impl FontAtlas { cache_key, GlyphAtlasLocation { glyph_index, - placement, + offset, }, ); true diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 4ed4851fe9381..c28370f14e70e 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,5 +1,4 @@ -use crate::GlyphAtlasLocation; -use crate::{error::TextError, Font, FontAtlas}; +use crate::{error::TextError, Font, FontAtlas, GlyphAtlasLocation}; use bevy_asset::{Assets, Handle}; use bevy_math::Vec2; use bevy_reflect::TypePath; @@ -62,7 +61,7 @@ impl FontAtlasSet { )] }); - let (glyph_texture, placement) = + let (glyph_texture, offset) = Font::get_outlined_glyph_texture(font_system, swash_cache, layout_glyph); let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { atlas.add_glyph( @@ -70,7 +69,7 @@ impl FontAtlasSet { texture_atlases, layout_glyph.cache_key, &glyph_texture, - placement, + offset, ) }; if !font_atlases.iter_mut().any(add_char_to_font_atlas) { @@ -92,7 +91,7 @@ impl FontAtlasSet { texture_atlases, layout_glyph.cache_key, &glyph_texture, - placement, + offset, ) { return Err(TextError::FailedToAddGlyph(layout_glyph.cache_key.glyph_id)); } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 3650f69e0423f..10ebb5515e2eb 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -307,8 +307,8 @@ impl TextPipeline { 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.placement.left as f32; - let top = location.placement.top as f32; + let left = location.offset.x as f32; + let top = location.offset.y as f32; let glyph_size = Vec2::new(glyph_rect.width(), glyph_rect.height()); // offset by half the size because the origin is center From 8696b4e1fe5c90f2a751c12f7ab084c5d69ea567 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 11 Jun 2023 13:17:21 +0800 Subject: [PATCH 28/32] extract and use `acquire_font_system` --- crates/bevy_text/src/pipeline.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 10ebb5515e2eb..826ea0b8754b7 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -95,11 +95,7 @@ impl TextPipeline { let (font_size, line_height) = (font_size as f32, line_height as f32); let metrics = Metrics::new(font_size, line_height); - let font_system = &mut self - .font_system - .0 - .try_lock() - .map_err(|_| TextError::FailedToAcquireMutex)?; + let font_system = &mut acquire_font_system(&mut self.font_system)?; // TODO: cache buffers (see Iced / glyphon) let mut buffer = Buffer::new(font_system, metrics); @@ -246,11 +242,7 @@ impl TextPipeline { let buffer = self.create_buffer(fonts, sections, linebreak_behavior, bounds, scale_factor)?; - let font_system = &mut self - .font_system - .0 - .try_lock() - .map_err(|_| TextError::FailedToAcquireMutex)?; + let font_system = &mut acquire_font_system(&mut self.font_system)?; let swash_cache = &mut self.swash_cache.0; let box_size = buffer_dimensions(&buffer); @@ -374,11 +366,7 @@ impl TextPipeline { let min_width_content_size = buffer_dimensions(&buffer); let max_width_content_size = { - let font_system = &mut self - .font_system - .0 - .try_lock() - .map_err(|_| TextError::FailedToAcquireMutex)?; + let font_system = &mut acquire_font_system(&mut self.font_system)?; buffer.set_size( font_system, @@ -585,3 +573,13 @@ impl<'text> Iterator for BidiParagraphs<'text> { } } } + +#[inline(always)] +fn acquire_font_system( + font_system: &mut FontSystem, +) -> Result, TextError> { + font_system + .0 + .try_lock() + .map_err(|_| TextError::FailedToAcquireMutex) +} From ab56e132a316dec79375a87883227c17fab5e250 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 11 Jun 2023 21:11:54 +0800 Subject: [PATCH 29/32] reorganize code and add more docs --- crates/bevy_text/src/font.rs | 57 ++-------------- crates/bevy_text/src/font_atlas.rs | 16 ++--- crates/bevy_text/src/font_atlas_set.rs | 93 +++++++++++++++++++------- crates/bevy_text/src/glyph.rs | 51 ++++++++++++++ crates/bevy_text/src/lib.rs | 32 +++++++++ crates/bevy_text/src/pipeline.rs | 44 +++++++++--- crates/bevy_text/src/text.rs | 15 +++++ 7 files changed, 212 insertions(+), 96 deletions(-) create mode 100644 crates/bevy_text/src/glyph.rs diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 1441661993a3d..c51b7cd8dded0 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,12 +1,11 @@ -use bevy_math::IVec2; use bevy_reflect::{TypePath, TypeUuid}; -use bevy_render::{ - render_resource::{Extent3d, TextureDimension, TextureFormat}, - texture::Image, -}; #[derive(Debug, TypeUuid, TypePath, Clone)] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] + +/// An asset that contains the data for a loaded font, if loaded as an asset. +/// +/// Loaded by [`FontLoader`](crate::FontLoader). pub struct Font { pub data: std::sync::Arc>, } @@ -18,52 +17,4 @@ impl Font { data: std::sync::Arc::new(font_data), } } - - // TODO: consider moving to pipeline.rs - pub fn get_outlined_glyph_texture( - font_system: &mut cosmic_text::FontSystem, - swash_cache: &mut cosmic_text::SwashCache, - layout_glyph: &cosmic_text::LayoutGlyph, - ) -> (Image, IVec2) { - // TODO: consider using cosmic_text's own caching mechanism - let image = swash_cache - .get_image_uncached(font_system, layout_glyph.cache_key) - // TODO: don't unwrap - .unwrap(); - - 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 - todo!() - } - }; - - // TODO: make this texture grayscale - ( - Image::new( - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - data, - TextureFormat::Rgba8UnormSrgb, - ), - IVec2::new(left, top), - ) - } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 0314bac4fb7ca..a52541841dc76 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -7,19 +7,17 @@ use bevy_render::{ use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; use bevy_utils::HashMap; -/// The location of a glyph in an atlas, -/// and how it is positioned when placed -#[derive(Debug, Clone, Copy)] -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, -} +use crate::GlyphAtlasLocation; +/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`. +/// +/// A [`FontAtlasSet`](crate::FontAtlasSet) contains one or more `FontAtlas`es. pub struct FontAtlas { + /// Used to update the [`TextureAtlas`]. pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, + /// A mapping between subpixel-binned glyphs and their [`GlyphAtlasLocation`]. pub glyph_to_atlas_index: HashMap, + /// The handle to the [`TextureAtlas`] that holds the rasterized glyphs. pub texture_atlas: Handle, } diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index c28370f14e70e..b587f5e070495 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -1,26 +1,36 @@ -use crate::{error::TextError, Font, FontAtlas, GlyphAtlasLocation}; -use bevy_asset::{Assets, Handle}; -use bevy_math::Vec2; -use bevy_reflect::TypePath; -use bevy_reflect::TypeUuid; -use bevy_render::texture::Image; +use bevy_asset::Assets; +use bevy_math::{IVec2, Vec2}; +use bevy_reflect::{TypePath, TypeUuid}; +use bevy_render::{ + render_resource::{Extent3d, TextureDimension, TextureFormat}, + texture::Image, +}; use bevy_sprite::TextureAtlas; use bevy_utils::HashMap; +use crate::{error::TextError, FontAtlas, GlyphAtlasInfo}; + type FontSizeKey = u32; +/// 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`](crate::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`. +/// +/// A `FontAtlasSet` contains one or more [`FontAtlas`]es for each font size. +/// +/// It is used by [`TextPipeline::queue_text`](crate::TextPipeline::queue_text). #[derive(TypeUuid, TypePath)] #[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] pub struct FontAtlasSet { font_atlases: HashMap>, } -#[derive(Debug, Clone)] -pub struct GlyphAtlasInfo { - pub texture_atlas: Handle, - pub location: GlyphAtlasLocation, -} - impl Default for FontAtlasSet { fn default() -> Self { FontAtlasSet { @@ -62,7 +72,7 @@ impl FontAtlasSet { }); let (glyph_texture, offset) = - Font::get_outlined_glyph_texture(font_system, swash_cache, layout_glyph); + Self::get_outlined_glyph_texture(font_system, swash_cache, layout_glyph); let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { atlas.add_glyph( textures, @@ -124,17 +134,50 @@ impl FontAtlasSet { pub fn num_font_atlases(&self) -> usize { self.font_atlases.len() } -} -#[derive(Debug, Clone)] -pub struct PositionedGlyph { - pub position: Vec2, - pub size: Vec2, - pub atlas_info: GlyphAtlasInfo, - pub section_index: usize, - /// 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. - // TODO: re-implement this or equivalent - pub byte_index: usize, + /// 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, + layout_glyph: &cosmic_text::LayoutGlyph, + ) -> (Image, IVec2) { + let image = swash_cache + .get_image_uncached(font_system, layout_glyph.cache_key) + // TODO: don't unwrap + .unwrap(); + + 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 + todo!() + } + }; + + ( + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + data, + TextureFormat::Rgba8UnormSrgb, + ), + IVec2::new(left, top), + ) + } } diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs new file mode 100644 index 0000000000000..5883d4451d563 --- /dev/null +++ b/crates/bevy_text/src/glyph.rs @@ -0,0 +1,51 @@ +//! This module exports types related to rendering glyphs. + +use bevy_asset::Handle; +use bevy_math::{IVec2, Vec2}; +use bevy_sprite::TextureAtlas; + +/// 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)] +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, + /// 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. + // TODO: re-implement this or equivalent + pub byte_index: usize, +} + +/// Information about a glyph in an atlas. +/// +/// Rasterized glyphs are stored as rectangles +/// in one or more [`FontAtlas`](crate::FontAtlas)es. +/// +/// Used in [`PositionedGlyph`] and [`FontAtlasSet`](crate::FontAtlasSet). +#[derive(Debug, Clone)] +pub struct GlyphAtlasInfo { + /// A handle to the texture atlas this glyph was placed in. + pub texture_atlas: Handle, + /// Location and offset of a glyph. + 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)] +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, +} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 70e56a915e01a..43dde3aabc06d 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -1,3 +1,33 @@ +//! 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`](FontLoader) as assets, +//! or they can be loaded as system fonts through [`TextPipeline::load_system_fonts`]. +//! +//! # `TextPipeline` +//! +//! The [`TextPipeline`] resource does all of the heavy lifting for rendering text. +//! +//! [`Text`](Text) is first measured by creating a [`TextMeasureInfo`] in [`TextPipeline::create_text_measure`], +//! which is called by a system. +//! +//! Note that text measurement is only relevant in a UI context. +//! +//! With the actual text bounds defined, another system 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`](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 error; @@ -5,6 +35,7 @@ mod font; mod font_atlas; mod font_atlas_set; mod font_loader; +mod glyph; mod pipeline; mod text; mod text2d; @@ -14,6 +45,7 @@ pub use font::*; pub use font_atlas::*; pub use font_atlas_set::*; pub use font_loader::*; +pub use glyph::*; pub use pipeline::*; pub use text::*; pub use text2d::*; diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 826ea0b8754b7..ed2560cd6db34 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,8 +1,7 @@ use std::sync::{Arc, Mutex}; use bevy_asset::{Assets, Handle, HandleId}; -use bevy_ecs::component::Component; -use bevy_ecs::system::Resource; +use bevy_ecs::{component::Component, system::Resource}; use bevy_math::Vec2; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; @@ -29,6 +28,7 @@ use crate::{ // TODO: the only reason we need a mutex is due to TextMeasure // - is there a way to do this without it? +/// A wrapper around a [`cosmic_text::FontSystem`] pub struct FontSystem(Arc>); impl Default for FontSystem { @@ -55,6 +55,7 @@ impl FontSystem { } } +/// A wrapper around a [`cosmic_text::SwashCache`] pub struct SwashCache(cosmic_text::SwashCache); impl Default for SwashCache { @@ -63,13 +64,24 @@ impl Default for SwashCache { } } +/// The `TextPipeline` is used to layout and render [`Text`](crate::Text). +/// +/// See the [crate-level documentation](crate) for more information. #[derive(Default, Resource)] pub struct TextPipeline { - /// Identifies a font ID by its font asset handle + /// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its font asset handle ID. map_handle_to_font_id: HashMap, - /// Identifies a font atlas set handle by its font iD + /// Identifies a [`FontAtlasSet`] handle by its font [`ID`](cosmic_text::fontdb::ID). + /// + /// Note that this is a strong handle, so that textures are not dropped. map_font_id_to_handle: HashMap>, + /// The font system is used to retrieve fonts and their information, including glyph outlines. + /// + /// See [`cosmic_text::FontSystem`] for more information. font_system: FontSystem, + /// The swash cache rasterizer is used to rasterize glyphs + /// + /// See [`cosmic_text::SwashCache`] for more information. swash_cache: SwashCache, } @@ -218,6 +230,10 @@ impl TextPipeline { Ok(buffer) } + /// 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, @@ -343,6 +359,10 @@ impl TextPipeline { }) } + /// 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, @@ -391,7 +411,7 @@ impl TextPipeline { /// /// System fonts loading is a surprisingly complicated task, /// mostly unsolvable without interacting with system libraries. - /// And since `fontdb` tries to be small and portable, this method + /// And since [`fontdb`](cosmic_text::fontdb) tries to be small and portable, this method /// will simply scan some predefined directories. /// Which means that fonts that are not in those directories must /// be added manually. @@ -423,6 +443,9 @@ pub struct TextLayoutInfo { } // TODO: is there a way to do this without mutexes? +/// Size information for a corresponding [`Text`](crate::Text) component. +/// +/// Generated via [`TextPipeline::create_text_measure`]. pub struct TextMeasureInfo { pub min_width_content_size: Vec2, pub max_width_content_size: Vec2, @@ -450,8 +473,9 @@ impl TextMeasureInfo { } } -/// Adds a span to the attributes list, -/// loading fonts into the DB if required. +/// For the current line, +/// adds a span to the attributes list and pushes the text into the line string, +/// loading fonts into the [`Database`](cosmic_text::fontdb::Database) if required. #[allow(clippy::too_many_arguments)] fn add_span( line_string: &mut String, @@ -518,6 +542,7 @@ fn add_span( attrs_list.add_span(start..end, attrs); } +/// Calculate the size of the text area for the given buffer. fn buffer_dimensions(buffer: &Buffer) -> Vec2 { // TODO: see https://github.com/pop-os/cosmic-text/issues/70 Let a Buffer figure out its height during set_size // TODO: see https://github.com/pop-os/cosmic-text/issues/42 Request: Allow buffer dimensions to be undefined @@ -536,7 +561,7 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 { } /// An iterator over the paragraphs in the input text. -/// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behavior. +/// It is equivalent to [`core::str::Lines`] but follows [`unicode_bidi`] behavior. // TODO: upstream to cosmic_text, see https://github.com/pop-os/cosmic-text/pull/124 // TODO: create separate iterator that keeps the ranges, or simply use memory address introspection (as_ptr()) // TODO: this breaks for lines ending in newlines, e.g. "foo\n" should split into ["foo", ""] but we actually get ["foo"] @@ -547,7 +572,7 @@ pub struct BidiParagraphs<'text> { impl<'text> BidiParagraphs<'text> { /// Create an iterator to split the input text into paragraphs - /// in accordance with `unicode-bidi` behavior. + /// in accordance with [`unicode_bidi`] behavior. pub fn new(text: &'text str) -> Self { let info = unicode_bidi::BidiInfo::new(text, None); let info = info.paragraphs.into_iter(); @@ -574,6 +599,7 @@ impl<'text> Iterator for BidiParagraphs<'text> { } } +/// Helper method to acquire a font system mutex. #[inline(always)] fn acquire_font_system( font_system: &mut FontSystem, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 0a9da0b038fa7..ad1cfd59ca85a 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -9,9 +9,13 @@ use crate::{Font, DEFAULT_FONT_HANDLE}; // TODO: reexport cosmic_text and these types in the prelude pub use cosmic_text::{FamilyOwned, Stretch, Style, Weight}; +/// A component that is the entry point for rendering text. +/// +/// It contains all of the text value and styling information. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct Text { + /// The text's sections pub sections: Vec, /// The text's internal alignment. /// Should not affect its position within a container. @@ -110,6 +114,7 @@ impl Text { } } +/// Contains the value of the text in a section and how it should be styled. #[derive(Debug, Default, Clone, FromReflect, Reflect)] pub struct TextSection { pub value: String, @@ -149,6 +154,7 @@ pub enum TextAlignment { Right, } +/// Describes the style of a [`TextSection`]. #[derive(Clone, Debug, Reflect, FromReflect)] pub struct TextStyle { pub font: FontRef, @@ -180,6 +186,7 @@ pub enum BreakLineOn { AnyCharacter, } +/// A reference to a font. #[derive(Clone, Debug, Reflect, FromReflect)] pub enum FontRef { /// A reference to a font loaded as a bevy asset. @@ -197,11 +204,19 @@ impl From> for FontRef { } } +/// Queries for a font from those already loaded. #[derive(Clone, Debug)] pub struct FontQuery { + /// The font family. See [`cosmic_text::fontdb::Family`] for details. pub family: FamilyOwned, + /// The stretch (or width) of the font face in this family, e.g. condensed. + /// See [`cosmic_text::fontdb::Stretch`] for details. pub stretch: Stretch, + /// The style of the font face in this family, e.g. italic. + /// See [`cosmic_text::fontdb::Style`] for details. pub style: Style, + /// The weight of the font face in this family, e.g. bold. + /// See [`cosmic_text::fontdb::Weight`] for details. pub weight: Weight, } From b25d8e73c682527ce749c6b9af483ae684beb4e1 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 11 Jun 2023 21:39:06 +0800 Subject: [PATCH 30/32] renamed cosmic text re-exports --- crates/bevy_text/src/text.rs | 39 +++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index ad1cfd59ca85a..8920630443906 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize}; use crate::{Font, DEFAULT_FONT_HANDLE}; // TODO: reexport cosmic_text and these types in the prelude -pub use cosmic_text::{FamilyOwned, Stretch, Style, Weight}; +pub use cosmic_text::{ + FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, +}; /// A component that is the entry point for rendering text. /// @@ -205,25 +207,34 @@ impl From> for FontRef { } /// Queries for a font from those already loaded. +/// +/// ``` +/// let fira_sans_bold = FontQuery::family("FiraSans").weight(FontWeight::Bold); +/// +/// let text_style = TextStyle { +/// font: fira_sans_bold.into(), +/// ..Default::default() +/// } +/// ``` #[derive(Clone, Debug)] pub struct FontQuery { /// The font family. See [`cosmic_text::fontdb::Family`] for details. - pub family: FamilyOwned, + pub family: FontFamily, /// The stretch (or width) of the font face in this family, e.g. condensed. /// See [`cosmic_text::fontdb::Stretch`] for details. - pub stretch: Stretch, + pub stretch: FontStretch, /// The style of the font face in this family, e.g. italic. /// See [`cosmic_text::fontdb::Style`] for details. - pub style: Style, + pub style: FontStyle, /// The weight of the font face in this family, e.g. bold. /// See [`cosmic_text::fontdb::Weight`] for details. - pub weight: Weight, + pub weight: FontWeight, } impl FontQuery { pub fn sans_serif() -> Self { Self { - family: FamilyOwned::SansSerif, + family: FontFamily::SansSerif, stretch: Default::default(), style: Default::default(), weight: Default::default(), @@ -232,7 +243,7 @@ impl FontQuery { pub fn serif() -> Self { Self { - family: FamilyOwned::Serif, + family: FontFamily::Serif, stretch: Default::default(), style: Default::default(), weight: Default::default(), @@ -241,7 +252,7 @@ impl FontQuery { pub fn fantasy() -> Self { Self { - family: FamilyOwned::Fantasy, + family: FontFamily::Fantasy, stretch: Default::default(), style: Default::default(), weight: Default::default(), @@ -250,7 +261,7 @@ impl FontQuery { pub fn cursive() -> Self { Self { - family: FamilyOwned::Cursive, + family: FontFamily::Cursive, stretch: Default::default(), style: Default::default(), weight: Default::default(), @@ -259,7 +270,7 @@ impl FontQuery { pub fn monospace() -> Self { Self { - family: FamilyOwned::Monospace, + family: FontFamily::Monospace, stretch: Default::default(), style: Default::default(), weight: Default::default(), @@ -268,22 +279,22 @@ impl FontQuery { pub fn family>(name: S) -> Self { Self { - family: FamilyOwned::Name(name.as_ref().to_string()), + family: FontFamily::Name(name.as_ref().to_string()), stretch: Default::default(), style: Default::default(), weight: Default::default(), } } - pub fn stretch(self, stretch: Stretch) -> Self { + pub fn stretch(self, stretch: FontStretch) -> Self { Self { stretch, ..self } } - pub fn style(self, style: Style) -> Self { + pub fn style(self, style: FontStyle) -> Self { Self { style, ..self } } - pub fn weight(self, weight: Weight) -> Self { + pub fn weight(self, weight: FontWeight) -> Self { Self { weight, ..self } } } From da0a81f5bde1b011f862df06151839144c8cad60 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sun, 11 Jun 2023 21:48:27 +0800 Subject: [PATCH 31/32] incorporate docs suggestions --- crates/bevy_text/src/font.rs | 2 +- crates/bevy_text/src/font_atlas_set.rs | 2 +- crates/bevy_text/src/glyph.rs | 4 +++- crates/bevy_text/src/pipeline.rs | 2 +- crates/bevy_text/src/text.rs | 2 +- examples/ui/system_fonts.rs | 6 +++++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index c51b7cd8dded0..52b528eb999e6 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -3,7 +3,7 @@ use bevy_reflect::{TypePath, TypeUuid}; #[derive(Debug, TypeUuid, TypePath, Clone)] #[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"] -/// An asset that contains the data for a loaded font, if loaded as an asset. +/// An [`Asset`](bevy_asset::Asset) that contains the data for a loaded font, if loaded as an asset. /// /// Loaded by [`FontLoader`](crate::FontLoader). pub struct Font { diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index b587f5e070495..5b03c08002d60 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -14,7 +14,7 @@ type FontSizeKey = u32; /// Provides the interface for adding and retrieving rasterized glyphs, and manages the [`FontAtlas`]es. /// -/// A `FontAtlasSet` is an asset. +/// A `FontAtlasSet` is an [`Asset`](bevy_asset::Asset). /// /// There is one `FontAtlasSet` for each font: /// - When a [`Font`](crate::Font) is loaded as an asset and then used in [`Text`](crate::Text), diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 5883d4451d563..7ba9bc4c3a8c8 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -4,7 +4,9 @@ use bevy_asset::Handle; use bevy_math::{IVec2, Vec2}; use bevy_sprite::TextureAtlas; -/// Information about how and where to render a glyph. +/// 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)] diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index ed2560cd6db34..957d59faf7105 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -69,7 +69,7 @@ impl Default for SwashCache { /// 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 handle ID. + /// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its [`Font`] [`Asset`](bevy_asset::Asset) [`HandleId`]. map_handle_to_font_id: HashMap, /// Identifies a [`FontAtlasSet`] handle by its font [`ID`](cosmic_text::fontdb::ID). /// diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 8920630443906..1a71308375732 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -188,7 +188,7 @@ pub enum BreakLineOn { AnyCharacter, } -/// A reference to a font. +/// Identifies a font to use, which is either stored as an [`Asset`](bevy_asset::Asset) or loaded directly from the user's system. #[derive(Clone, Debug, Reflect, FromReflect)] pub enum FontRef { /// A reference to a font loaded as a bevy asset. diff --git a/examples/ui/system_fonts.rs b/examples/ui/system_fonts.rs index fa2e092bdf46d..6543dbef7912f 100644 --- a/examples/ui/system_fonts.rs +++ b/examples/ui/system_fonts.rs @@ -1,4 +1,8 @@ -//! This example demonstrates using system fonts. +//! This example demonstrates using system fonts, which are already installed on the user's computer. +//! +//! System fonts won't always be present: if they're not found, a fallback will be used instead. +//! +//! If you need consistent font rendering for aesthetic reasons, you should package and ship your own font as an asset instead. use bevy::{ prelude::*, From 0e59b115a6680446f731c0ef1ff87b8eb40075d9 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 27 Jun 2023 22:53:54 +0800 Subject: [PATCH 32/32] fix doctests --- crates/bevy_text/src/text.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 1a71308375732..f5db886244408 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -51,7 +51,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, /// }, @@ -60,7 +60,7 @@ impl Text { /// let hello_bevy = Text::from_section( /// "hello bevy!", /// TextStyle { - /// font: font_handle, + /// font: font_handle.into(), /// font_size: 60.0, /// color: Color::WHITE, /// }, @@ -87,7 +87,7 @@ impl Text { /// TextSection::new( /// "Hello, ", /// TextStyle { - /// font: font_handle.clone(), + /// font: font_handle.clone().into(), /// font_size: 60.0, /// color: Color::BLUE, /// }, @@ -95,7 +95,7 @@ impl Text { /// TextSection::new( /// "World!", /// TextStyle { - /// font: font_handle, + /// font: font_handle.into(), /// font_size: 60.0, /// color: Color::RED, /// }, @@ -209,12 +209,14 @@ impl From> for FontRef { /// Queries for a font from those already loaded. /// /// ``` -/// let fira_sans_bold = FontQuery::family("FiraSans").weight(FontWeight::Bold); +/// # use bevy_text::{FontQuery, FontWeight, TextStyle}; +/// +/// let fira_sans_bold = FontQuery::family("FiraSans").weight(FontWeight::BOLD); /// /// let text_style = TextStyle { /// font: fira_sans_bold.into(), /// ..Default::default() -/// } +/// }; /// ``` #[derive(Clone, Debug)] pub struct FontQuery {