mod metrics;
use crate::resolve::{AttrMap, ResolvedValue};
pub use metrics::CHARSET;
pub struct Face {
pub upem: u16,
#[allow(dead_code)]
pub ascent: i16,
#[allow(dead_code)]
pub descent: i16,
pub cap_height: i16,
pub advances: &'static [u16],
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Kind {
#[default]
Mono,
Prop,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Font {
pub kind: Kind,
weight: usize,
}
const KNOWN_MONO: &[&str] = &[
"google sans code",
"menlo",
"consolas",
"courier",
"courier new",
"cascadia code",
"fira code",
"source code pro",
];
pub(crate) const SUBSTITUTES: &[(char, char)] = &[('⌀', 'Ø')];
fn fallback_advance_em(ch: char) -> f64 {
let cp = ch as u32;
let wide = matches!(
cp,
0x1100..=0x115F | 0x2E80..=0x9FFF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF | 0xFF00..=0xFF60 | 0x20000..=0x3FFFD, );
if wide { 1.0 } else { 0.6 }
}
fn is_mono_name(family: &str) -> bool {
let f = family.trim().trim_matches(['"', '\'']).to_ascii_lowercase();
f.contains("mono") || KNOWN_MONO.contains(&f.as_str())
}
impl Kind {
pub fn of_family(value: Option<&ResolvedValue>) -> Kind {
let name = match value {
Some(ResolvedValue::String(s))
| Some(ResolvedValue::Ident(s))
| Some(ResolvedValue::RawCss(s)) => s,
_ => return Kind::Mono,
};
let first = name.split(',').next().unwrap_or(name);
if is_mono_name(first) {
Kind::Mono
} else {
Kind::Prop
}
}
}
impl Font {
#[allow(dead_code)] pub const MONO_REGULAR: Font = Font {
kind: Kind::Mono,
weight: 0,
};
pub fn regular(kind: Kind) -> Font {
Font { kind, weight: 0 }
}
pub fn bold(kind: Kind) -> Font {
Font { kind, weight: 3 }
}
pub fn medium(kind: Kind) -> Font {
Font { kind, weight: 1 }
}
pub fn semibold(kind: Kind) -> Font {
Font { kind, weight: 2 }
}
pub fn with_kind(self, kind: Kind) -> Font {
Font { kind, ..self }
}
pub fn of(attrs: &AttrMap) -> Font {
Font {
kind: Kind::of_family(attrs.get("font-family")),
weight: weight_index(attrs.get("font-weight")),
}
}
pub fn face(&self) -> &'static Face {
match self.kind {
Kind::Mono => metrics::MONO[self.weight],
Kind::Prop => metrics::PROP[self.weight],
}
}
pub fn advance_em(&self, ch: char) -> f64 {
let face = self.face();
let lookup = |c: char| {
charset_index(c)
.map(|i| face.advances[i])
.filter(|&a| a != 0)
};
let sub = || {
SUBSTITUTES
.iter()
.find(|&&(from, _)| from == ch)
.and_then(|&(_, to)| lookup(to))
};
match lookup(ch).or_else(sub) {
Some(units) => units as f64 / face.upem as f64,
None => fallback_advance_em(ch),
}
}
pub fn cap_height_em(&self) -> f64 {
let face = self.face();
face.cap_height as f64 / face.upem as f64
}
pub fn weight(&self) -> u16 {
[400, 500, 600, 700][self.weight]
}
}
fn weight_index(value: Option<&ResolvedValue>) -> usize {
match value {
None => 1,
Some(ResolvedValue::Ident(w)) => match w.as_str() {
"medium" => 1,
"semibold" => 2,
"bold" => 3,
_ => 0,
},
Some(ResolvedValue::Number(n)) => match *n as u16 {
500 => 1,
600 => 2,
700 => 3,
_ => 0,
},
_ => 0,
}
}
fn charset_index(ch: char) -> Option<usize> {
let cp = ch as u32;
let mut base = 0usize;
for &(start, end) in CHARSET {
if (start..=end).contains(&cp) {
return Some(base + (cp - start) as usize);
}
base += (end - start + 1) as usize;
}
None
}
pub const ENABLED: bool = cfg!(feature = "font");
#[cfg(feature = "font")]
pub fn subset_bytes(kind: Kind, weight: u16) -> &'static [u8] {
let w = match weight {
500 => 1,
600 => 2,
700 => 3,
_ => 0,
};
match kind {
Kind::Mono => [
include_bytes!("../../assets/fonts/subset/GoogleSansCode-Regular.ttf").as_slice(),
include_bytes!("../../assets/fonts/subset/GoogleSansCode-Medium.ttf"),
include_bytes!("../../assets/fonts/subset/GoogleSansCode-SemiBold.ttf"),
include_bytes!("../../assets/fonts/subset/GoogleSansCode-Bold.ttf"),
][w],
Kind::Prop => [
include_bytes!("../../assets/fonts/subset/GoogleSans-Regular.ttf").as_slice(),
include_bytes!("../../assets/fonts/subset/GoogleSans-Medium.ttf"),
include_bytes!("../../assets/fonts/subset/GoogleSans-SemiBold.ttf"),
include_bytes!("../../assets/fonts/subset/GoogleSans-Bold.ttf"),
][w],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mono_advances_are_uniformly_point_six_em() {
for face in metrics::MONO {
for &adv in face.advances {
assert!(adv == 0 || (adv as f64 / face.upem as f64 - 0.6).abs() < 1e-12);
}
}
}
#[test]
fn kind_follows_the_name_not_the_table() {
let kind = |s: &str| Kind::of_family(Some(&ResolvedValue::String(s.into())));
assert_eq!(kind("Google Sans Code"), Kind::Mono);
assert_eq!(kind("JetBrains Mono"), Kind::Mono);
assert_eq!(kind("ui-monospace, SF Mono"), Kind::Mono);
assert_eq!(kind("Google Sans"), Kind::Prop);
assert_eq!(kind("Inter, system-ui"), Kind::Prop);
assert_eq!(Kind::of_family(None), Kind::Mono);
}
#[test]
fn advances_fall_back_for_unknown_glyphs() {
let mono = Font::default();
assert!((mono.advance_em('⌀') - 0.6).abs() < 1e-12);
assert!((mono.advance_em('你') - 1.0).abs() < 1e-12);
assert!((mono.advance_em('Ж') - 0.6).abs() < 1e-12);
}
#[test]
fn proportional_glyphs_differ() {
let prop = Font {
kind: Kind::Prop,
weight: 0,
};
let (i, m) = (prop.advance_em('i'), prop.advance_em('M'));
assert!(i < m, "i {i} vs M {m}");
}
#[test]
fn every_face_has_a_cap_height() {
for face in metrics::MONO.into_iter().chain(metrics::PROP) {
assert!(face.cap_height > 0);
assert!(face.ascent > 0 && face.descent < 0);
}
}
}