pub const PRIMARY_FONT: &str = "Intel One Mono";
pub const EXTENDED_TEXT_FONT: &str = "Noto Sans";
pub const MONOSPACE_FONT: &str = "Noto Sans Mono";
pub const DINGBATS_FONT: &str = "Noto Sans Symbols 2";
pub const ARABIC_FONT: &str = "Noto Naskh Arabic";
pub const SVG_FONT_FAMILY: &str = "'Intel One Mono', 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', 'Consolas', 'Menlo', monospace";
pub const fn font_family_for_slot(slot: FontSlot) -> &'static str {
match slot {
FontSlot::Primary => PRIMARY_FONT,
FontSlot::ExtendedText => EXTENDED_TEXT_FONT,
FontSlot::Monospace => MONOSPACE_FONT,
FontSlot::Dingbats => DINGBATS_FONT,
FontSlot::Arabic => ARABIC_FONT,
FontSlot::Cjk => "Noto Sans SC",
FontSlot::Emoji => "Noto Color Emoji",
}
}
pub const fn embedded_font_file(slot: FontSlot) -> Option<&'static str> {
match slot {
FontSlot::Primary => Some("IntelOneMono-Regular.ttf"),
FontSlot::ExtendedText => Some("NotoSans-Regular.ttf"),
FontSlot::Monospace => Some("NotoSansMono-Regular.ttf"),
FontSlot::Dingbats => Some("NotoSansSymbols2-Regular.ttf"),
FontSlot::Arabic => Some("NotoNaskhArabic-Regular.ttf"),
FontSlot::Cjk => None, FontSlot::Emoji => None, }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FontSlot {
Primary,
ExtendedText,
Monospace,
Dingbats,
Arabic,
Cjk,
Emoji,
}
impl FontSlot {
#[inline]
pub const fn is_embedded(self) -> bool {
matches!(
self,
Self::Primary | Self::ExtendedText | Self::Monospace | Self::Dingbats | Self::Arabic
)
}
#[inline]
pub const fn is_external(self) -> bool {
matches!(self, Self::Cjk | Self::Emoji)
}
}
#[inline]
pub const fn font_for_char(ch: char) -> FontSlot {
let cp = ch as u32;
match cp {
0x0000..=0x024F => FontSlot::Primary,
0x1E00..=0x1EFF => FontSlot::Primary,
0x0370..=0x03FF | 0x1F00..=0x1FFF => FontSlot::ExtendedText,
0x0400..=0x04FF | 0x0500..=0x052F | 0x2DE0..=0x2DFF | 0xA640..=0xA69F => FontSlot::ExtendedText,
0x0600..=0x06FF | 0x0750..=0x077F | 0x08A0..=0x08FF |
0xFB50..=0xFDFF | 0xFE70..=0xFEFF => FontSlot::Arabic,
0x0590..=0x05FF | 0xFB1D..=0xFB4F => FontSlot::Arabic,
0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0x20000..=0x2A6DF |
0xF900..=0xFAFF |
0x3040..=0x30FF | 0x31F0..=0x31FF |
0xAC00..=0xD7AF | 0x1100..=0x11FF | 0x3130..=0x318F |
0x3000..=0x303F |
0x3100..=0x312F |
0xFF01..=0xFF60 => FontSlot::Cjk,
0x1F300..=0x1F9FF | 0x1FA00..=0x1FA6F | 0x1FA70..=0x1FAFF | 0xFE00..=0xFE0F | 0x200D => FontSlot::Emoji,
0x2000..=0x20CF => FontSlot::Primary,
0x2100..=0x214F => FontSlot::Primary,
0x2190..=0x21FF => FontSlot::Monospace,
0x2200..=0x22FF => FontSlot::Monospace,
0x2300..=0x23FF => FontSlot::Monospace,
0x2460..=0x24FF => FontSlot::Monospace,
0x2500..=0x25FF => FontSlot::Monospace,
0x2600..=0x27BF => FontSlot::Dingbats,
0x27C0..=0x27EF | 0x2980..=0x29FF | 0x2B00..=0x2BFF => FontSlot::Dingbats,
0x0900..=0x0DFF => FontSlot::ExtendedText,
0x0E00..=0x0E7F | 0x0E80..=0x0EFF => FontSlot::ExtendedText,
_ => FontSlot::ExtendedText,
}
}
#[inline]
pub fn detect_external_fonts(text: &str) -> (bool, bool) {
let mut cjk = false;
let mut emoji = false;
for ch in text.chars() {
match font_for_char(ch) {
FontSlot::Cjk => cjk = true,
FontSlot::Emoji => emoji = true,
_ => {}
}
if cjk && emoji {
break;
} }
(cjk, emoji)
}
pub const NOTO_SANS_SC_URL: &str =
"https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf";
pub const NOTO_COLOR_EMOJI_URL: &str =
"https://cdn.jsdelivr.net/gh/googlefonts/noto-emoji@v2.047/fonts/NotoColorEmoji.ttf";
pub fn system_font_dirs() -> &'static [&'static str] {
if cfg!(target_os = "macos") {
&[
"/System/Library/Fonts/",
"/System/Library/Fonts/Supplemental/",
"/Library/Fonts/",
]
} else if cfg!(target_os = "linux") {
&["/usr/share/fonts/", "/usr/local/share/fonts/"]
} else if cfg!(target_os = "windows") {
&["C:\\Windows\\Fonts\\"]
} else {
&[]
}
}
pub fn find_system_font(family: &str) -> Option<std::path::PathBuf> {
for dir in system_font_dirs() {
let dir_path = std::path::Path::new(dir);
if !dir_path.exists() {
continue;
}
for ext in &["ttf", "ttc", "otf"] {
let path = dir_path.join(format!("{family}.{ext}"));
if path.exists() {
return Some(path);
}
let path = dir_path.join(format!("{}.{ext}", family.replace(' ', "")));
if path.exists() {
return Some(path);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_is_primary() {
assert_eq!(font_for_char('A'), FontSlot::Primary);
assert_eq!(font_for_char('z'), FontSlot::Primary);
assert_eq!(font_for_char('0'), FontSlot::Primary);
assert_eq!(font_for_char(' '), FontSlot::Primary);
}
#[test]
fn latin_extended_is_primary() {
assert_eq!(font_for_char('é'), FontSlot::Primary);
assert_eq!(font_for_char('ñ'), FontSlot::Primary);
assert_eq!(font_for_char('ü'), FontSlot::Primary);
}
#[test]
fn greek_is_extended_text() {
assert_eq!(font_for_char('α'), FontSlot::ExtendedText);
assert_eq!(font_for_char('β'), FontSlot::ExtendedText);
assert_eq!(font_for_char('γ'), FontSlot::ExtendedText);
}
#[test]
fn cyrillic_is_extended_text() {
assert_eq!(font_for_char('П'), FontSlot::ExtendedText);
assert_eq!(font_for_char('р'), FontSlot::ExtendedText);
}
#[test]
fn arabic_is_arabic() {
assert_eq!(font_for_char('م'), FontSlot::Arabic);
assert_eq!(font_for_char('ر'), FontSlot::Arabic);
}
#[test]
fn cjk_is_cjk() {
assert_eq!(font_for_char('你'), FontSlot::Cjk);
assert_eq!(font_for_char('好'), FontSlot::Cjk);
assert_eq!(font_for_char('世'), FontSlot::Cjk);
}
#[test]
fn symbols_are_dingbats() {
assert_eq!(font_for_char('★'), FontSlot::Dingbats);
assert_eq!(font_for_char('☆'), FontSlot::Dingbats);
assert_eq!(font_for_char('→'), FontSlot::Monospace);
assert_eq!(font_for_char('✔'), FontSlot::Dingbats);
assert_eq!(font_for_char('✘'), FontSlot::Dingbats);
}
#[test]
fn coffee_is_dingbats() {
assert_eq!(font_for_char('☕'), FontSlot::Dingbats);
}
#[test]
fn detect_external_ascii_only() {
assert_eq!(detect_external_fonts("Hello World"), (false, false));
}
#[test]
fn detect_external_cjk() {
assert_eq!(detect_external_fonts("Hello 你好"), (true, false));
}
#[test]
fn detect_external_emoji() {
let (_, emoji) = detect_external_fonts("Hello 😀");
assert!(emoji);
}
#[test]
fn font_for_char_is_const() {
const _: FontSlot = font_for_char('A');
const _B: FontSlot = font_for_char('你');
}
}