use std::path::PathBuf;
use std::sync::Arc;
use cosmic_text::{Attrs, FontSystem};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct FontId(pub fontdb::ID);
impl FontId {
#[inline(always)]
pub fn new(id: fontdb::ID) -> Self {
Self(id)
}
#[inline(always)]
pub fn dummy() -> Self {
Self(fontdb::ID::dummy())
}
#[inline(always)]
pub fn raw(self) -> fontdb::ID {
self.0
}
}
impl From<fontdb::ID> for FontId {
#[inline(always)]
fn from(id: fontdb::ID) -> Self {
Self(id)
}
}
impl From<FontId> for fontdb::ID {
#[inline(always)]
fn from(id: FontId) -> Self {
id.0
}
}
#[derive(Clone, Debug)]
pub enum FontSource {
File(PathBuf),
Binary(Arc<Vec<u8>>),
}
impl FontSource {
#[inline(always)]
pub fn file(path: impl Into<PathBuf>) -> Self {
Self::File(path.into())
}
#[inline(always)]
pub fn binary(data: impl Into<Vec<u8>>) -> Self {
Self::Binary(Arc::new(data.into()))
}
}
impl From<PathBuf> for FontSource {
#[inline(always)]
fn from(path: PathBuf) -> Self {
Self::File(path)
}
}
impl From<Vec<u8>> for FontSource {
#[inline(always)]
fn from(data: Vec<u8>) -> Self {
Self::Binary(Arc::new(data))
}
}
#[derive(Clone, Debug)]
pub struct FontFaceInfo {
pub id: FontId,
pub family: String,
pub monospaced: bool,
pub weight: u16,
pub style: FontStyle,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum FontStyle {
Normal,
Italic,
Oblique,
}
impl From<fontdb::Style> for FontStyle {
#[inline]
fn from(style: fontdb::Style) -> Self {
match style {
fontdb::Style::Normal => Self::Normal,
fontdb::Style::Italic => Self::Italic,
fontdb::Style::Oblique => Self::Oblique,
}
}
}
pub struct FontManager {
system: FontSystem,
}
impl FontManager {
pub fn new() -> Self {
Self {
system: FontSystem::new(),
}
}
pub fn with_fonts(fonts: impl IntoIterator<Item = FontSource>) -> Self {
let sources: Vec<fontdb::Source> = fonts
.into_iter()
.map(|src| match src {
FontSource::File(path) => fontdb::Source::File(path),
FontSource::Binary(data) => fontdb::Source::Binary(data),
})
.collect();
Self {
system: FontSystem::new_with_fonts(sources),
}
}
pub fn from_system(system: FontSystem) -> Self {
Self { system }
}
pub fn load_font_file(&mut self, path: impl Into<PathBuf>) -> Vec<FontId> {
self.load_font_file_result(path).unwrap_or_default()
}
pub fn load_font_file_result(
&mut self,
path: impl Into<PathBuf>,
) -> Result<Vec<FontId>, std::io::Error> {
let path = path.into();
let before: std::collections::HashSet<fontdb::ID> =
self.system.db().faces().map(|f| f.id).collect();
self.system.db_mut().load_font_file(&path).map_err(|e| {
std::io::Error::other(format!("{e:?}"))
})?;
Ok(self
.system
.db()
.faces()
.filter(|f| !before.contains(&f.id))
.map(|f| FontId(f.id))
.collect())
}
pub fn load_font_data(
&mut self,
data: impl AsRef<[u8]> + Sync + Send + 'static,
) -> Vec<FontId> {
let source = fontdb::Source::Binary(Arc::new(data));
let face_ids = self.system.db_mut().load_font_source(source);
face_ids.into_iter().map(FontId).collect()
}
pub fn faces(&self) -> Vec<FontFaceInfo> {
self.system
.db()
.faces()
.map(|face| FontFaceInfo {
id: FontId(face.id),
family: face
.families
.first()
.map(|(name, _)| name.clone())
.unwrap_or_default(),
monospaced: face.monospaced,
weight: face.weight.0,
style: face.style.into(),
})
.collect()
}
pub fn find_by_family(&self, family: &str) -> Vec<FontFaceInfo> {
self.faces()
.into_iter()
.filter(|f| f.family.eq_ignore_ascii_case(family))
.collect()
}
pub fn locale(&self) -> &str {
self.system.locale()
}
#[inline(always)]
pub fn system(&self) -> &FontSystem {
&self.system
}
#[inline(always)]
pub fn system_mut(&mut self) -> &mut FontSystem {
&mut self.system
}
#[inline(always)]
pub fn into_system(self) -> FontSystem {
self.system
}
pub fn attrs_for_family<'a>(&self, family: &'a str) -> Attrs<'a> {
Attrs::new().family(cosmic_text::Family::Name(family))
}
}
impl Default for FontManager {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for FontManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FontManager")
.field("locale", &self.system.locale())
.field("face_count", &self.system.db().faces().count())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn font_id_roundtrip() {
let raw = fontdb::ID::dummy();
let id = FontId::new(raw);
assert_eq!(id.raw(), raw);
let id2: FontId = raw.into();
assert_eq!(id, id2);
let back: fontdb::ID = id.into();
assert_eq!(back, raw);
}
#[test]
fn font_source_file_from_path() {
let src = FontSource::file("/tmp/font.ttf");
assert!(matches!(src, FontSource::File(_)));
}
#[test]
fn font_source_binary_from_vec() {
let src = FontSource::binary(vec![0u8, 1, 2, 3]);
assert!(matches!(src, FontSource::Binary(_)));
}
#[test]
fn font_source_from_pathbuf() {
let src: FontSource = PathBuf::from("/tmp/font.otf").into();
assert!(matches!(src, FontSource::File(_)));
}
#[test]
fn font_source_from_vec() {
let src: FontSource = vec![0u8, 1, 2].into();
assert!(matches!(src, FontSource::Binary(_)));
}
#[test]
fn font_style_from_fontdb() {
assert_eq!(FontStyle::from(fontdb::Style::Normal), FontStyle::Normal);
assert_eq!(FontStyle::from(fontdb::Style::Italic), FontStyle::Italic);
assert_eq!(FontStyle::from(fontdb::Style::Oblique), FontStyle::Oblique);
}
#[test]
fn font_manager_new_discovers_system_fonts() {
let manager = FontManager::new();
let _locale = manager.locale();
let _faces = manager.faces();
}
#[test]
fn font_manager_with_custom_fonts_only() {
let manager = FontManager::with_fonts(std::iter::empty());
let _faces = manager.faces();
}
#[test]
fn font_manager_debug_format() {
let manager = FontManager::with_fonts(std::iter::empty());
let debug = format!("{:?}", manager);
assert!(debug.contains("FontManager"));
assert!(debug.contains("locale"));
}
#[test]
fn font_manager_attrs_for_family() {
let manager = FontManager::with_fonts(std::iter::empty());
let attrs = manager.attrs_for_family("Helvetica");
let _ = attrs;
}
#[test]
fn font_manager_find_by_family_empty() {
let manager = FontManager::with_fonts(std::iter::empty());
let results = manager.find_by_family("NonExistentFont12345");
assert!(results.is_empty());
}
}