Files
galaxy/crates/galaxyui/src/windowing/winit/fonts.rs
T

1265 lines
46 KiB
Rust

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<FontHandle>,
}
#[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<FontFamily> {
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<Vec<FontHandle>, 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<FontFamily> {
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<Vec<FontHandle>> {
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<Vec<u8>>) -> Result<FontFamily> {
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::<Result<_>>()?;
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<FontId>,
}
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<FamilyId, Family>,
/// 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<cosmic_text::FontSystem>,
font_id_map: RwLock<BiMap<FontId, fontdb::ID>>,
font_selections: DashMap<(FamilyId, Properties), FontId>,
loaded_fonts: DashMap<FontKey, FontId>,
#[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<Vec<FontId>>,
#[cfg(not(target_os = "windows"))]
fallback_fonts: DashMap<FontId, Vec<FontId>>,
}
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<cosmic_text::SwashCache>,
}
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<FamilyId> {
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<dyn AsRef<[u8]>` to a
// `Vec<u8>`, 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, F: FnOnce(owned_ttf_parser::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, F: FnOnce(owned_ttf_parser::Face) -> T>(
&self,
font_id: FontId,
ttf_font_callback: F,
) -> Option<T> {
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<Option<T>> into an Option<T>,
// 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, F: FnOnce(&[u8], u32) -> 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, F: FnOnce(&[u8], u32) -> T>(
&self,
font_id: FontId,
font_data_callback: F,
) -> Option<T> {
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, F: FnOnce(Source, u32) -> T>(
&self,
font_id: FontId,
face_source_callback: F,
) -> Option<T> {
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<fontdb::ID>,
) -> 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<FontId> {
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<FontId> {
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<Item = (LayoutLine, bool)>,
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<ClipConfig>,
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<char> {
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<usize>, 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<Self>) -> Box<dyn Any> {
self as Box<dyn Any>
}
}
impl platform::FontDB for FontDB {
fn load_from_bytes(&mut self, name: &str, bytes: Vec<Vec<u8>>) -> Result<FamilyId> {
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<FamilyId> {
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<dyn platform::LoadedSystemFonts>> {
self.text_layout_system.load_all_system_fonts()
}
#[cfg(not(target_family = "wasm"))]
fn process_loaded_system_fonts(
&mut self,
loaded_system_fonts: Box<dyn platform::LoadedSystemFonts>,
) -> Vec<(Option<FamilyId>, crate::fonts::FontInfo)> {
let loaded_system_fonts: Box<LoadedSystemFonts> = 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<FamilyId> {
self.text_layout_system.family_id_for_name(name)
}
fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
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<FontId> {
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<Vector2I> {
self.text_layout_system.glyph_advance(font_id, glyph_id)
}
fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<RectI> {
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<RectI> {
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<RectI> {
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<RasterizedGlyph> {
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<RasterizedGlyph> {
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<GlyphId> {
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<usize>, 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<usize>, StyleAndFont)],
max_width: f32,
max_height: f32,
alignment: TextAlignment,
first_line_head_indent: Option<f32>,
) -> 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,
&current_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<dyn platform::LoadedSystemFonts>> {
use futures::FutureExt as _;
async { Box::new(loader::load_all_system_fonts()) as Box<dyn platform::LoadedSystemFonts> }
.boxed()
}
fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
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<FontId> {
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<FontId> {
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<Vector2I> {
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<RectI> {
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<GlyphId> {
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<FamilyId> {
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;