use std::fmt;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, OnceLock};
use skrifa::instance::{LocationRef, Size};
use skrifa::{FontRef, MetadataProvider};
pub const BUNDLED_MONO: &[u8] = include_bytes!("../fonts/JetBrainsMono-Regular.ttf");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FontId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GlyphId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineMetrics {
pub ascent: f32,
pub descent: f32,
pub line_gap: f32,
}
impl LineMetrics {
pub fn line_height(&self) -> f32 {
self.ascent - self.descent + self.line_gap
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FontError {
Parse,
}
impl fmt::Display for FontError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Parse => f.write_str("the data is not a readable OpenType/TrueType font"),
}
}
}
impl std::error::Error for FontError {}
static NEXT_FONT_ID: AtomicU32 = AtomicU32::new(1);
pub type FontBytes = Arc<dyn AsRef<[u8]> + Send + Sync>;
#[derive(Clone)]
pub struct Font {
data: FontBytes,
index: u32,
id: FontId,
}
impl fmt::Debug for Font {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Font")
.field("id", &self.id)
.field("index", &self.index)
.field("bytes", &self.bytes().len())
.finish()
}
}
impl Font {
pub fn new(data: impl Into<Arc<[u8]>>) -> Result<Self, FontError> {
let data: Arc<[u8]> = data.into();
Self::from_shared(Arc::new(data), 0)
}
pub fn from_shared(data: FontBytes, index: u32) -> Result<Self, FontError> {
FontRef::from_index((*data).as_ref(), index).map_err(|_| FontError::Parse)?;
let id = FontId(NEXT_FONT_ID.fetch_add(1, Ordering::Relaxed));
Ok(Self { data, index, id })
}
pub fn bundled_mono() -> Result<Self, FontError> {
static BUNDLED: OnceLock<Result<Font, FontError>> = OnceLock::new();
BUNDLED
.get_or_init(|| Self::from_shared(Arc::new(BUNDLED_MONO), 0))
.clone()
}
pub fn id(&self) -> FontId {
self.id
}
pub fn index(&self) -> u32 {
self.index
}
pub fn bytes(&self) -> &[u8] {
(*self.data).as_ref()
}
pub fn shared_bytes(&self) -> FontBytes {
Arc::clone(&self.data)
}
pub(crate) fn font_ref(&self) -> Option<FontRef<'_>> {
FontRef::from_index(self.bytes(), self.index).ok()
}
pub fn glyph_id(&self, ch: char) -> Option<GlyphId> {
let glyph = self.font_ref()?.charmap().map(ch)?;
(glyph.to_u32() != 0).then_some(GlyphId(glyph.to_u32()))
}
pub fn advance(&self, glyph: GlyphId, px: f32) -> f32 {
self.font_ref()
.and_then(|font| {
font.glyph_metrics(Size::new(px), LocationRef::default())
.advance_width(skrifa::GlyphId::new(glyph.0))
})
.unwrap_or(0.0)
}
pub fn glyph_extent(&self, glyph: GlyphId, px: f32) -> Option<[f32; 2]> {
let font = self.font_ref()?;
let metrics = font.glyph_metrics(Size::new(px), LocationRef::default());
let bounds = metrics.bounds(skrifa::GlyphId::new(glyph.0))?;
Some([
(bounds.x_max - bounds.x_min).max(0.0),
(bounds.y_max - bounds.y_min).max(0.0),
])
}
pub fn line_metrics(&self, px: f32) -> LineMetrics {
let Some(font) = self.font_ref() else {
return LineMetrics {
ascent: 0.0,
descent: 0.0,
line_gap: 0.0,
};
};
let metrics = font.metrics(Size::new(px), LocationRef::default());
LineMetrics {
ascent: metrics.ascent,
descent: metrics.descent,
line_gap: metrics.leading,
}
}
}