mod font_handle; mod str_index_map; #[cfg(not(feature = "fontkit-rasterizer"))] mod swash_rasterizer; mod text_layout; #[cfg(any(target_os = "linux", target_os = "freebsd"))] mod linux; #[cfg(target_os = "windows")] mod windows; use std::any::Any; use std::collections::HashMap; use std::ops::{DerefMut, Range}; use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use bimap::BiMap; use cosmic_text::{ Align, Attrs, AttrsList, BidiParagraphs, LayoutGlyph, LayoutLine, ShapeLine, Shaping, Wrap, }; use dashmap::mapref::entry::Entry; use dashmap::DashMap; use fontdb::Source; use galaxyui_core::fonts::{Style, Weight}; use itertools::Itertools; use parking_lot::RwLock; use pathfinder_geometry::rect::{RectF, RectI}; use pathfinder_geometry::vector::{vec2f, vec2i, Vector2F, Vector2I}; use resvg::usvg::fontdb; use resvg::usvg::fontdb::Query; use vec1::Vec1; #[cfg(target_os = "windows")] use windows::loader; use self::font_handle::{FontData, FontHandle}; use self::str_index_map::StrIndexMap; use self::text_layout::{RunBuilder, TextStylesMap}; use crate::fonts::canvas::RasterFormat; use crate::fonts::{ FamilyId, FontId, GlyphId, Metrics, Properties, RasterizedGlyph, SubpixelAlignment, }; use crate::platform::{self, LineStyle}; use crate::rendering::GlyphConfig; use crate::text_layout::{CaretPosition, ClipConfig, Line, StyleAndFont, TextAlignment, TextFrame}; struct FontFamily { name: String, fonts: Vec, } #[cfg(any(target_os = "linux", target_os = "freebsd"))] mod loader { use anyhow::Result; use super::*; use crate::windowing::winit::fonts::linux::{Error, FontconfigLoader}; pub fn load_all_system_fonts() -> LoadedSystemFonts { let manager = match FontconfigLoader::new() { Ok(x) => x, Err(e) => { log::error!("Failed to load system fonts: {e:?}"); return LoadedSystemFonts(vec![]); } }; let handles = match manager.get_all_families() { Ok(x) => x, Err(e) => { log::error!("Failed to load system fonts: {e:?}"); return LoadedSystemFonts(vec![]); } }; let mut families = vec![]; for handle in handles.into_iter() { let name = handle.name().to_string(); // Note: this call will do a validation check when we load match handle.into_info_and_family() { Ok((info, family)) => families.push((info, family)), Err(e) => { // Making sure this is just a debug message, since this is not necessarily // a bug if this fails to load (ex: could be an unsupported font format) log::debug!("Failed to load system fonts for family {name}: {e:?}") } }; } log::info!("Loaded {} font families", families.len()); LoadedSystemFonts(families) } pub fn load_system_font(font_family: &str) -> Result { let manager = FontconfigLoader::new()?; let handle = manager.get_family(font_family)?; Ok(handle.into_family()?) } pub fn fallback_fonts( family_name: &str, properties: Properties, ) -> Result, Error> { let loader = FontconfigLoader::new()?; loader.fallback_fonts(family_name, properties) } } #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))] mod loader { #[cfg(not(target_family = "wasm"))] use crate::fonts::FontInfo; #[cfg(not(target_family = "wasm"))] pub fn load_system_font(_font_family: &str) -> Result { anyhow::bail!("have not yet implemented loading system fonts") } #[cfg(not(target_family = "wasm"))] pub fn load_all_system_fonts() -> Vec<(FontInfo, FontFamily)> { vec![] } pub fn fallback_fonts(_family_name: &str, _properties: Properties) -> Result> { anyhow::bail!("Fallback fonts are not yet implemented on wasm") } } // We use font-kit's family handle to load fonts that come with Warp as // these binaries are already in memory and won't increase our memory load. fn load_font_family_from_bytes(name: &str, font_bytes: Vec>) -> Result { use owned_ttf_parser::OwnedFace; let fonts = font_bytes .into_iter() .map(|font| { // We use index 0 here since the each set of bytes are assumed to be a single font // face. let face = OwnedFace::from_vec(font, 0)?; let handle = FontHandle::from(face); Ok(handle) }) .collect::>()?; Ok(FontFamily { fonts, name: name.to_string(), }) } /// Enum indicating whether font validation should enforce that the font supports the english language. #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))] #[derive(Copy, Clone)] enum ValidateFontSupportsEn { Yes, No, } struct Family { name: String, font_ids: Vec1, } fn next_family_id() -> FamilyId { static FAMILY_ID: AtomicUsize = AtomicUsize::new(0); let next = FAMILY_ID.fetch_add(1, Ordering::Relaxed); FamilyId(next) } fn next_font_id() -> FontId { static FONT_ID: AtomicUsize = AtomicUsize::new(0); let next = FONT_ID.fetch_add(1, Ordering::Relaxed); FontId(next) } /// Identifier of a system font we've loaded into the database. #[derive(PartialEq, Eq, Hash, Debug, Clone)] struct FontKey { /// The path to the font on the user's filesystem. path: PathBuf, /// The index within the font file that was loaded. index: u32, } pub struct TextLayoutSystem { families: HashMap, /// The internal font database that stores all of our loaded fonts. Since internally, /// `cosmic_text` caches font selection, all of its functions to layout text are `&mut`. /// However, the `FontDB` trait for text layout is _immutable_ and cannot be easily changed to /// mutable. Therefore, we use an [`RwLock`] for internal mutability. `FontDB` is Sync and Send, /// so we can't use a `RefCell`. font_store: RwLock, font_id_map: RwLock>, font_selections: DashMap<(FamilyId, Properties), FontId>, loaded_fonts: DashMap, #[cfg(feature = "fontkit-rasterizer")] /// The set of loaded fonts since the last time we tried to rasterize. There's an unfortunate /// dependency where we need to load fonts within the TextLayoutSystem since we load fallback /// fonts while doing text layout. This means we need cache the fonts we loaded so we can read /// it out at raster time and load any necessary fonts. loaded_font_ids_since_last_raster: RwLock>, #[cfg(not(target_os = "windows"))] fallback_fonts: DashMap>, } pub struct FontDB { text_layout_system: TextLayoutSystem, #[cfg(feature = "fontkit-rasterizer")] font_kit_rasterizer: crate::fonts::font_kit::Rasterizer, #[cfg(not(feature = "fontkit-rasterizer"))] swash_cache: RwLock, } impl FontDB { pub fn new() -> Self { Self { text_layout_system: TextLayoutSystem::new(), #[cfg(feature = "fontkit-rasterizer")] font_kit_rasterizer: crate::fonts::font_kit::Rasterizer::new(), #[cfg(not(feature = "fontkit-rasterizer"))] swash_cache: RwLock::new(cosmic_text::SwashCache::new()), } } /// Inserts the given font family into the DB, returning a [`FamilyId`] /// for the inserted family. fn insert_font_family(&mut self, font_family: FontFamily) -> Result { let mut font_ids = Vec::with_capacity(font_family.fonts.len()); for font in font_family.fonts { let font_id = self.text_layout_system.insert_font(font)?; #[cfg(feature = "fontkit-rasterizer")] self.load_font_kit_font(font_id)?; font_ids.push(font_id); } let font_ids = Vec1::try_from_vec(font_ids)?; let family_id = next_family_id(); self.text_layout_system.families.insert( family_id, Family { name: font_family.name, font_ids, }, ); Ok(family_id) } #[cfg(feature = "fontkit-rasterizer")] fn load_font_kit_font(&self, font_id: FontId) -> Result<()> { let font_kit_font = self .text_layout_system .try_read_face_source(font_id, |source, index| { match source { Source::Binary(bytes) => { // There's no easy way to convert from an `Arc` to a // `Vec`, which means we need to clone here. This is unfortunate // since internally the font source is actually represented as a `Vec`, // but there's no way of downcasting back to the concrete type. // TODO(alokedesai): Make refactors to fontdb and/or font-kit to make // this conversion more performant. font_kit::loader::Loader::from_bytes( Arc::new(bytes.as_ref().as_ref().to_vec()), index, ) } Source::File(path) => font_kit::loader::Loader::from_path(path, index), Source::SharedFile(path, _) => font_kit::loader::Loader::from_path(path, index), } }) .ok_or_else(|| anyhow!("Unable to find font with given font ID"))?; // We have to use an `Arc` with a non send / sync type since // the font kit rasterizer is actually send + sync on Mac, which results // in it being wrapped in an Arc. #[allow(clippy::arc_with_non_send_sync)] self.font_kit_rasterizer .insert(font_id, Arc::new(font_kit_font?)); Ok(()) } } impl Default for TextLayoutSystem { fn default() -> Self { Self::new() } } impl TextLayoutSystem { pub fn new() -> Self { Self { families: Default::default(), font_store: RwLock::new(cosmic_text::FontSystem::new_with_locale_and_db( // Locale is needed for font fallback. For now, we hardcode this to "en" to match // our mac implementation https://github.com/warpdotdev/warp-internal/blob/bf33d651a9fcece70df8eac35f89b0393ca5189a/ui/src/platform/mac/fonts.rs#L383. "en".into(), Default::default(), )), font_id_map: Default::default(), font_selections: Default::default(), loaded_fonts: Default::default(), #[cfg(not(target_os = "windows"))] fallback_fonts: Default::default(), #[cfg(feature = "fontkit-rasterizer")] loaded_font_ids_since_last_raster: Default::default(), } } /// Helper function to read a Font Face ([`owned_ttf_parser::Face`]) identified by a [`FontId`]. /// /// ## Panics /// This function can panic under a few conditions: /// 1) If there is no font identified by [`FontId`] stored in the [`TextLayoutSystem`]. /// 2) The underlying [`fontdb::Database`] does not hold a font identified by [`ID`]. We only /// store an [`ID`] in the `font_id_map` upon inserting it in the [`fontdb::Database`]. /// 3) The font can't be parsed by [`ttf_parser`]. We validate fonts can be parsed by /// `owned_ttf_parser` when we initially load a font. fn read_font_face T>( &self, font_id: FontId, ttf_font_callback: F, ) -> T { self.read_font_face_data(font_id, |data, index| { // SAFETY: We've already validated that we can parse the font when loading it. let font_face = owned_ttf_parser::Face::parse(data, index).expect("Must be able to parse font"); ttf_font_callback(font_face) }) } /// Safe version of [`Self::read_font_face`]. Instead of panicking, will return None fn try_read_font_face T>( &self, font_id: FontId, ttf_font_callback: F, ) -> Option { let result = self.try_read_font_face_data(font_id, |data, index| { let Ok(font_face) = owned_ttf_parser::Face::parse(data, index) else { return None; }; Some(ttf_font_callback(font_face)) }); match result { // Flatten out the Option> into an Option, // to keep the API in line with try_read_font_face_data None => None, Some(None) => None, Some(Some(x)) => Some(x), } } /// Helper function to read the font data of a font identified by a [`FontId`]. /// /// ## Panics /// This function can panic under a few conditions: /// 1) If there is no font identified by [`FontId`] stored in the [`TextLayoutSystem`]. /// 2) The underlying [`fontdb::Database`] does not hold a font identified by [`ID`]. We only /// store an [`ID`] in the `font_id_map` upon inserting it in the [`fontdb::Database`]. fn read_font_face_data T>( &self, font_id: FontId, font_data_callback: F, ) -> T { match self.try_read_font_face_data(font_id, font_data_callback) { Some(x) => x, None => { match self.font_id_map.read().get_by_left(&font_id) { Some(internal_font_id) => { let internal_font_id = *internal_font_id; let panic_font_data = self.read_font_face_panic_data(font_id, Some(internal_font_id)); let panic_message = match self.font_store.read().db().face_source(internal_font_id) { None => { format!("does not have an internal font source for id {internal_font_id}") } Some((source, idx)) => format!( "was unable to load font source ({source:?}, {idx}) for id {internal_font_id}" ) }; log::warn!( "Tried to load font data {panic_font_data}, but {panic_message}" ); } None => { let panic_font_data = self.read_font_face_panic_data(font_id, None); log::warn!("Tried to load font data {panic_font_data}, but no corresponding internal id exists"); } }; panic!("Tried to load font data. No font source"); } } } /// Safe version of [`Self::read_font_face_data`]. Instead of panicking, will return None fn try_read_font_face_data T>( &self, font_id: FontId, font_data_callback: F, ) -> Option { let internal_font_id = *self.font_id_map.read().get_by_left(&font_id)?; self.font_store .read() .db() .with_face_data(internal_font_id, |font_data, index| { font_data_callback(font_data, index) }) } /// Returns the [`Source`] and corresponding font index of a font identified by /// [`FontId`]. #[allow(dead_code)] fn try_read_face_source T>( &self, font_id: FontId, face_source_callback: F, ) -> Option { let internal_font_id = *self.font_id_map.read().get_by_left(&font_id)?; let font_store = self.font_store.read(); let (source, index) = font_store.db().face_source(internal_font_id)?; Some(face_source_callback(source, index)) } fn read_font_face_panic_data( &self, font_id: FontId, internal_id: Option, ) -> String { let internal_id = internal_id .map(|id| format!("{id}")) .unwrap_or("None".to_string()); let family_name = self .families .values() .find(|family| family.font_ids.contains(&font_id)) .map(|f| f.name.as_str()) .unwrap_or("Unavailable"); let font_loaded = self.loaded_fonts.iter().any(|z| z.value() == &font_id); format!( "Font(id={font_id:?}, internal={internal_id}, family={family_name}, loaded={font_loaded}" ) } #[cfg(feature = "fontkit-rasterizer")] fn take_fonts_loaded_since_last_raster(&self) -> Vec { let mut new_vec = vec![]; let mut loaded_fonts = self.loaded_font_ids_since_last_raster.write(); std::mem::swap(loaded_fonts.as_mut(), &mut new_vec); new_vec } /// Inserts the font identified by the given [`FontHandle`] into the DB, returning the [`FontId`] identified by that /// font. /// If the font has already been loaded, the same [`FontId`] from the initial time the font was loaded is returned. fn insert_font(&self, font_handle: FontHandle) -> Result { let (source, index) = match font_handle.into_data() { FontData::Bytes(face) => (fontdb::Source::Binary(Arc::new(face.into_vec())), 0), FontData::Path { path, index, .. } => (fontdb::Source::File(path.clone()), index), }; if let Source::File(path) = &source { // The font we're trying to load has already been loaded into the DB. if let Some(id) = self.loaded_fonts.get(&FontKey { path: path.clone(), index, }) { return Ok(*id); } } // TODO(alokedesai): Consider using FontDB's `make_shared_font_data` here. FontDB creates a temporary memory // mapped file every time data is read via a call to `with_face_data`. When rendering text this can // repeatedly cause allocation of a large amount of virtual address space, especially when trying to load // large fonts. See https://github.com/RazrFalcon/fontdb/issues/18 for more details. let fontdb_ids = self.font_store.write().db_mut().load_font_source(source); if fontdb_ids.is_empty() { bail!("Loaded a font source that corresponds to 0 font faces") } let mut font_id_to_return = None; for id in fontdb_ids { let font_store = self.font_store.read(); let Some(face_info) = font_store.db().face(id) else { continue; }; // Update our FontID map for every font that was loaded into the internal fontDB. cosmic_text may return a // `fontdb::ID` for a glyph based on the state of the fontDB, so we need to make sure we have a matching // FontID or else we will panic. let font_id = next_font_id(); self.font_id_map.write().insert(font_id, id); #[cfg(feature = "fontkit-rasterizer")] self.loaded_font_ids_since_last_raster.write().push(font_id); if let Source::File(path) = &face_info.source { self.loaded_fonts.insert( FontKey { path: path.clone(), index: face_info.index, }, font_id, ); } if face_info.index == index { font_id_to_return = Some(font_id); } } font_id_to_return .ok_or_else(|| anyhow!("Requested index for the font handle was unable to be loaded")) } /// Loads all of the fallback fonts for the `font_id` with a given `family_name` and set of `properties`. /// Noops if fallback fonts for the font are already loaded. #[cfg(not(target_os = "windows"))] fn load_fallback_fonts(&self, font_id: FontId, family_name: &str, properties: Properties) { if self.fallback_fonts.contains_key(&font_id) { return; } if let Ok(fallback_fonts) = loader::fallback_fonts(family_name, properties) { let fallback_fonts = fallback_fonts .into_iter() .filter_map(|font| self.insert_font(font).ok()) .collect_vec(); self.fallback_fonts.insert(font_id, fallback_fonts); } } /// On Windows, this is a no-op. We retrieve fallback fonts dynamically as needed. #[cfg(target_os = "windows")] fn load_fallback_fonts(&self, _font_id: FontId, _family_name: &str, _properties: Properties) {} #[allow(clippy::too_many_arguments)] fn create_text_frame( &self, layout_lines: impl Iterator, line_style: LineStyle, text_styles_map: &TextStylesMap, max_height: f32, alignment: TextAlignment, str_index_map: &StrIndexMap, text: &str, ) -> TextFrame { let (_, upper_bound) = layout_lines.size_hint(); let mut lines = match upper_bound { None => vec![], Some(size) => Vec::with_capacity(size), }; let mut max_line_width: f32 = 0.; let mut total_height = 0.; let mut line_glyph_start_index: usize = 0; let mut layout_lines = layout_lines.peekable(); while let Some((line, has_trailing_newline)) = layout_lines.next() { max_line_width = max_line_width.max(line.w); let is_last_line = layout_lines.peek().is_none(); let line = self.create_line( line, line_style, text_styles_map, is_last_line.then_some(ClipConfig::default()), str_index_map, text, line_glyph_start_index, has_trailing_newline, ); total_height += line.height(); // Only update line_glyph_start_index at paragraph boundaries (when there's a trailing // newline). For soft-wrapped lines within the same paragraph, glyph.start values from // cosmic-text are always byte offsets relative to the paragraph start, so // line_glyph_start_index must stay at the paragraph's starting byte offset in the full // text. if has_trailing_newline { // Convert the last caret's char index back to a byte index, then advance past the // newline separator to get the next paragraph's byte offset. line_glyph_start_index = line .caret_positions .last() .and_then(|position| str_index_map.byte_index(position.last_offset + 1)) .unwrap_or(line_glyph_start_index); } // TODO(alokedesai): Properly clip multi-line text using the same strategy we use on mac. // See https://github.com/warpdotdev/warp-internal/blob/91dfe429074c6129a6b5c1c57c55c1daf6d274a9/ui/src/platform/mac/text_layout.rs#L318-L359. if total_height > max_height { break; } lines.push(line); } match Vec1::try_from_vec(lines) { Ok(lines) => TextFrame::new(lines, max_line_width, alignment), Err(_) => TextFrame::empty(line_style.font_size, line_style.line_height_ratio), } } #[allow(clippy::too_many_arguments)] fn create_line( &self, layout_line: LayoutLine, line_style: LineStyle, text_styles_map: &TextStylesMap, clip_config: Option, str_index_map: &StrIndexMap, text: &str, line_glyph_start_index: usize, has_trailing_newline: bool, ) -> Line { let Some(first_glyph) = layout_line.glyphs.first() else { return Line::empty( line_style.font_size, line_style.line_height_ratio, line_glyph_start_index, ); }; let width = layout_line.w; let initial_font_id = *self .font_id_map .read() .get_by_right(&first_glyph.font_id) .expect("Font must exist in map"); let mut run_builder = RunBuilder::new(text_styles_map, initial_font_id, str_index_map); let mut caret_positions = vec![]; let mut chars_with_missing_glyphs = vec![]; let mut last_glyph_offset: usize = 0; run_builder.reserve_capacity(layout_line.glyphs.len()); for glyph in layout_line.glyphs { // TODO(daprahamian): when we have time, we should investigate pulling // caret position data out of font gdef table. Right now, this impl does // not work for certain ligatures if glyph.w > 0.0 { let start_offset = str_index_map .char_index(line_glyph_start_index + glyph.start) .unwrap_or(0); let last_offset = str_index_map .char_index(line_glyph_start_index + glyph.end) .unwrap_or_else(|| str_index_map.num_chars()) - 1; caret_positions.push(CaretPosition { position_in_line: glyph.x, start_offset, last_offset, }); last_glyph_offset = last_offset; } // A glyph_id of 0 implies that no glyph was found for this character. if glyph.glyph_id == 0 { if let Some(ch) = Self::char_for_glyph(&glyph, text) { chars_with_missing_glyphs.push(ch); } } run_builder.push_glyph(glyph, |id| { *self .font_id_map .read() .get_by_right(id) .expect("Font must exist in map") }); } if has_trailing_newline { caret_positions.push(CaretPosition { position_in_line: layout_line.w, start_offset: last_glyph_offset + 1, last_offset: last_glyph_offset + 1, }); } Line { width, // TODO(vorporeal): See if we need to compute this (and if so, how to). trailing_whitespace_width: 0.0, runs: run_builder.build(), font_size: line_style.font_size, line_height_ratio: line_style.line_height_ratio, baseline_ratio: line_style.baseline_ratio, ascent: layout_line.max_ascent, descent: layout_line.max_descent, clip_config, caret_positions, chars_with_missing_glyphs, } } fn char_for_glyph(glyph: &LayoutGlyph, text: &str) -> Option { if !text.is_char_boundary(glyph.start) { log::warn!("Expected glyph start to be a char boundary"); return None; } text[glyph.start..].chars().next() } /// Produces an [`AttrsList`] to layout text given a list of `style_runs` and the `text` the /// runs correspond to. fn build_attrs_list( &self, text: &str, style_runs: &[(Range, StyleAndFont)], text_styles_map: &mut TextStylesMap, str_index_map: &StrIndexMap, ) -> AttrsList { let attrs = Attrs::new(); let mut attrs_list = AttrsList::new(attrs); for (range, style_and_font) in style_runs { let start_byte_index = str_index_map.byte_index(range.start).unwrap_or(text.len()); let end_byte_index = str_index_map.byte_index(range.end).unwrap_or(text.len()); // Perform font selection using font-db before passing the font to cosmic text. Since it // does not use the CSS3 font selection algorithm, it will panic if we pass a font // weight or style that isn't available for the font. See https://github.com/pop-os/cosmic-text/issues/58. let selected_font = self.select_font(style_and_font.font_family, style_and_font.properties); let id = *self .font_id_map .read() .get_by_left(&selected_font) .expect("Selected font must exist in font_id_map"); let font_store = self.font_store.read(); let face = match font_store.db().face(id) { None => continue, Some(face) => face, }; let Some((family, _)) = face.families.first() else { continue; }; let style_index = text_styles_map.insert(style_and_font.style); attrs_list.add_span( start_byte_index..end_byte_index, Attrs { color_opt: None, family: cosmic_text::Family::Name(family), stretch: Default::default(), style: face.style, weight: face.weight, metadata: style_index, cache_key_flags: cosmic_text::CacheKeyFlags::empty(), metrics_opt: None, }, ); } attrs_list } } #[cfg_attr(target_family = "wasm", expect(dead_code))] struct LoadedSystemFonts(Vec<(crate::fonts::FontInfo, FontFamily)>); impl platform::LoadedSystemFonts for LoadedSystemFonts { fn as_any(self: Box) -> Box { self as Box } } impl platform::FontDB for FontDB { fn load_from_bytes(&mut self, name: &str, bytes: Vec>) -> Result { let family = load_font_family_from_bytes(name, bytes)?; self.insert_font_family(family) } #[cfg(not(target_family = "wasm"))] fn load_from_system(&mut self, font_family: &str) -> Result { let family = loader::load_system_font(font_family)?; self.insert_font_family(family) } #[cfg(not(target_family = "wasm"))] fn load_all_system_fonts( &self, ) -> futures::future::BoxFuture<'static, Box> { self.text_layout_system.load_all_system_fonts() } #[cfg(not(target_family = "wasm"))] fn process_loaded_system_fonts( &mut self, loaded_system_fonts: Box, ) -> Vec<(Option, crate::fonts::FontInfo)> { let loaded_system_fonts: Box = loaded_system_fonts .as_any() .downcast() .expect("should not fail to downcast to concrete type"); loaded_system_fonts .0 .into_iter() .map(|(font_info, family)| { let family_id = self.insert_font_family(family).ok(); (family_id, font_info) }) .collect_vec() } fn family_id_for_name(&self, name: &str) -> Option { self.text_layout_system.family_id_for_name(name) } fn load_family_name_from_id(&self, id: FamilyId) -> Option { self.text_layout_system.load_family_name_from_id(id) } fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId { self.text_layout_system.select_font(family_id, properties) } fn fallback_fonts(&self, character: char, font_id: FontId) -> Vec { self.text_layout_system.fallback_fonts(character, font_id) } fn font_metrics(&self, font_id: FontId) -> Metrics { self.text_layout_system.font_metrics(font_id) } fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result { self.text_layout_system.glyph_advance(font_id, glyph_id) } fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result { self.text_layout_system .glyph_typographic_bounds(font_id, glyph_id) } #[cfg(feature = "fontkit-rasterizer")] fn glyph_raster_bounds( &self, font_id: FontId, size: f32, glyph_id: GlyphId, scale: Vector2F, glyph_config: &GlyphConfig, ) -> Result { let fonts = self .text_layout_system .take_fonts_loaded_since_last_raster(); for font in fonts { self.load_font_kit_font(font)? } self.font_kit_rasterizer .glyph_raster_bounds(font_id, size, glyph_id, scale, glyph_config) } #[cfg(not(feature = "fontkit-rasterizer"))] fn glyph_raster_bounds( &self, font_id: FontId, size: f32, glyph_id: GlyphId, scale: Vector2F, glyph_config: &GlyphConfig, ) -> Result { Self::glyph_raster_bounds(self, font_id, size, glyph_id, scale, glyph_config) } #[cfg(feature = "fontkit-rasterizer")] fn rasterize_glyph( &self, font_id: FontId, size: f32, glyph_id: GlyphId, scale: Vector2F, subpixel_alignment: SubpixelAlignment, glyph_config: &GlyphConfig, format: RasterFormat, ) -> Result { let fonts = self .text_layout_system .take_fonts_loaded_since_last_raster(); for font in fonts { self.load_font_kit_font(font)? } self.font_kit_rasterizer.rasterize_glyph( font_id, size, glyph_id, scale, subpixel_alignment, glyph_config, format, ) } #[cfg(not(feature = "fontkit-rasterizer"))] fn rasterize_glyph( &self, font_id: FontId, size: f32, glyph_id: GlyphId, scale: Vector2F, subpixel_alignment: SubpixelAlignment, glyph_config: &GlyphConfig, format: RasterFormat, ) -> Result { Self::rasterize_glyph( self, font_id, size, glyph_id, scale, subpixel_alignment, glyph_config, format, ) } fn glyph_for_char(&self, font_id: FontId, char: char) -> Option { self.text_layout_system.glyph_for_char(font_id, char) } fn text_layout_system(&self) -> &dyn platform::TextLayoutSystem { &self.text_layout_system } } impl platform::TextLayoutSystem for TextLayoutSystem { fn layout_line( &self, text: &str, line_style: LineStyle, style_runs: &[(Range, StyleAndFont)], max_width: f32, clip_config: ClipConfig, ) -> Line { // cosmic_text panics if we pass text that is multiple paragraphs. Since we are laying out // a line (which is by definition _not_ wrapped), combine all of the paragraphs together // with a zero-width space (U+200B). We do this to ensure that all of the text renders on a // single line, while also ensuring the style runs (which previously included the separator) // are still respected properly. let text = BidiParagraphs::new(text).join("\u{200B}"); let mut text_styles_map = TextStylesMap::new(); let str_index_map = StrIndexMap::new(&text); let attrs_list = self.build_attrs_list( text.as_str(), style_runs, &mut text_styles_map, &str_index_map, ); let tab_width = line_style.fixed_width_tab_size.unwrap_or(4).into(); let shape_line = ShapeLine::new( self.font_store.write().deref_mut(), text.as_str(), &attrs_list, Shaping::Advanced, tab_width, ); // Layout the line, passing `Wrap::None` here since we want to render all of the text on a // single line. let layout = shape_line.layout( line_style.font_size, Some(max_width), Wrap::None, Some(Align::Left), None, None, ); // Since we passed `Wrap::None`, cosmic text will only produce a single laid out line. debug_assert_eq!( layout.len(), 1, "Expected a single laid out line but there were {} lines instead", layout.len() ); let first_line = match layout.into_iter().next() { Some(line) => line, None => return Line::empty(line_style.font_size, line_style.line_height_ratio, 0), }; self.create_line( first_line, line_style, &text_styles_map, Some(clip_config), &str_index_map, &text, 0, false, ) } fn layout_text( &self, text: &str, line_style: LineStyle, style_runs: &[(Range, StyleAndFont)], max_width: f32, max_height: f32, alignment: TextAlignment, first_line_head_indent: Option, ) -> TextFrame { let mut text_styles_map = TextStylesMap::new(); let str_index_map = StrIndexMap::new(text); let mut attrs_list = self.build_attrs_list(text, style_runs, &mut text_styles_map, &str_index_map); let mut font_store = self.font_store.write(); let tab_width = line_style.fixed_width_tab_size.unwrap_or(4).into(); let mut num_bytes_seen = 0; let layouts = BidiParagraphs::new(text).flat_map(|paragraph| { let following_paragraph_text = &text[num_bytes_seen + paragraph.len()..]; let mut char_indices = following_paragraph_text.char_indices(); // Determine the byte length of the separator. The `BidiParagraphs` iterator does not // coalesce multiple paragraph separators so we know there is at most one separator. let (separator_byte_length, has_trailing_separator) = match char_indices.next() { Some((start_byte_index, _char)) => { // Determine the end byte index of the character by checking the start character // index of the following character. If the current character is the last // character, the end byte index is the total length of the string. let end_byte_index = char_indices .next() .map(|(index, _)| index) .unwrap_or(following_paragraph_text.len()); (end_byte_index - start_byte_index, true) } None => (0, false), }; num_bytes_seen += paragraph.len() + separator_byte_length; // Remove the attributes from the attrs list that correspond to this specific paragraph. // We also remove the paragraph's separator--this won't impact styling (since the length // of the text is smaller than that of the attribute list) but ensures all of the styles // related to this line of text are correctly removed. let mut current_attrs_list = attrs_list.split_off(paragraph.len() + separator_byte_length); std::mem::swap(&mut current_attrs_list, &mut attrs_list); let shape_line = ShapeLine::new( font_store.deref_mut(), paragraph, ¤t_attrs_list, Shaping::Advanced, tab_width, ); let layout_lines = shape_line.layout( line_style.font_size, Some(max_width), Wrap::WordOrGlyph, Some(Align::Left), first_line_head_indent, None, ); let layout_line_count = layout_lines.len(); layout_lines .into_iter() .enumerate() .map(|(line_idx, line)| { // Each ShapeLine may get wrapped to multiple LayoutLines. // We should only add the trailing separator to the last LayoutLine. let is_last = line_idx == layout_line_count.saturating_sub(1); let include_trailing_separator = is_last && has_trailing_separator; (line, include_trailing_separator) }) .collect_vec() }); self.create_text_frame( layouts, line_style, &text_styles_map, max_height, alignment, &str_index_map, text, ) } } impl TextLayoutSystem { #[cfg(not(target_family = "wasm"))] fn load_all_system_fonts( &self, ) -> futures::future::BoxFuture<'static, Box> { use futures::FutureExt as _; async { Box::new(loader::load_all_system_fonts()) as Box } .boxed() } fn load_family_name_from_id(&self, id: FamilyId) -> Option { self.families.get(&id).map(|family| family.name.to_owned()) } fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId { match self.font_selections.entry((family_id, properties)) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { let family = self .families .get(&family_id) .expect("Font family must exist"); let weight = match properties.weight { Weight::Thin => fontdb::Weight::THIN, Weight::ExtraLight => fontdb::Weight::EXTRA_LIGHT, Weight::Light => fontdb::Weight::LIGHT, Weight::Normal => fontdb::Weight::NORMAL, Weight::Medium => fontdb::Weight::MEDIUM, Weight::Semibold => fontdb::Weight::SEMIBOLD, Weight::Bold => fontdb::Weight::BOLD, Weight::ExtraBold => fontdb::Weight::EXTRA_BOLD, Weight::Black => fontdb::Weight::BLACK, }; let style = match properties.style { Style::Normal => fontdb::Style::Normal, Style::Italic => fontdb::Style::Italic, }; let best_match = self.font_store.read().db().query(&Query { families: &[fontdb::Family::Name(family.name.as_str())], weight, stretch: Default::default(), style, }); let best_match = best_match.and_then(|id| self.font_id_map.read().get_by_right(&id).copied()); let best_match = best_match.unwrap_or(*family.font_ids.first()); self.load_fallback_fonts(best_match, family.name.as_str(), properties); *entry.insert(best_match) } } } #[cfg(not(target_os = "windows"))] fn fallback_fonts(&self, _ch: char, font_id: FontId) -> Vec { self.fallback_fonts .get(&font_id) .map(|fallbacks| fallbacks.clone()) .unwrap_or_default() } #[cfg(target_os = "windows")] fn fallback_fonts(&self, character: char, font_id: FontId) -> Vec { self.get_fallback_fonts_for_character(character, font_id) .map_err(|err| { log::warn!("Unable to fetch fallback fonts for character {character:?}: {err:?}"); err }) .unwrap_or_default() } fn font_metrics(&self, font_id: FontId) -> crate::fonts::Metrics { self.read_font_face(font_id, |font_face| crate::fonts::Metrics { units_per_em: font_face.units_per_em().into(), ascent: font_face.ascender(), descent: font_face.descender(), line_gap: font_face.line_gap(), }) } fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result { self.read_font_face(font_id, |font_face| { let glyph_id = owned_ttf_parser::GlyphId(glyph_id as u16); let horizontal_advance = font_face.glyph_hor_advance(glyph_id).unwrap_or(0); let vertical_advance = font_face.glyph_ver_advance(glyph_id).unwrap_or(0); Ok(vec2i(horizontal_advance.into(), vertical_advance.into())) }) } fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result { self.read_font_face(font_id, |font_face| { let ttf_parser_glyph_id = owned_ttf_parser::GlyphId::from_glyph_id(glyph_id); let raster_image = font_face.glyph_raster_image(ttf_parser_glyph_id, u16::MAX); if let Some(raster_image) = raster_image { // The raster image's bounds is in pixels. Determine the scale factor to convert the raster image's // bounds back to font units. let scale = if raster_image.pixels_per_em != 0 { font_face.units_per_em() as f32 / raster_image.pixels_per_em as f32 } else { 1.0 }; let bounding_box = RectF::new( vec2f(raster_image.x as f32, raster_image.y as f32), vec2f(raster_image.width as f32, raster_image.height as f32), ); return Ok((bounding_box * scale).to_i32()); } let bounding_box = font_face .glyph_bounding_box(ttf_parser_glyph_id) .ok_or_else(|| anyhow!("No bounding box for glyph id {glyph_id:?}"))?; Ok(bounding_box.to_recti()) }) } fn glyph_for_char(&self, font_id: FontId, char: char) -> Option { self.try_read_font_face(font_id, |font_face| { font_face.glyph_index(char).map(GlyphIdExt::to_glyph_id) })? } fn family_id_for_name(&self, name: &str) -> Option { self.families .iter() .find_map(|(family_id, family)| (family.name == name).then_some(*family_id)) } } /// Helper extension trait to convert to a [`RectI`]. trait ToRectI { fn to_recti(self) -> RectI; } impl ToRectI for owned_ttf_parser::Rect { fn to_recti(self) -> RectI { RectI::new( vec2i(self.x_min.into(), self.y_min.into()), vec2i(self.width().into(), self.height().into()), ) } } /// Helper extension trait to convert to a [`GlyphId`] trait GlyphIdExt { fn to_glyph_id(self) -> GlyphId; fn from_glyph_id(glyph_id: GlyphId) -> Self; } impl GlyphIdExt for owned_ttf_parser::GlyphId { fn to_glyph_id(self) -> GlyphId { self.0.into() } fn from_glyph_id(glyph_id: GlyphId) -> Self { Self(glyph_id as u16) } } #[cfg(test)] #[path = "text_layout_tests.rs"] mod layout_tests;