pub mod constants;
pub mod fonts;
pub mod glyf_decode;
pub mod glyph_registry;
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))]
pub mod linux;
#[cfg(not(target_arch = "wasm32"))]
pub mod loader;
#[cfg(target_os = "macos")]
pub mod macos;
pub mod metrics;
pub mod nerd_font_attributes;
pub mod text_run_cache;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(test)]
mod cjk_metrics_tests;
pub const FONT_ID_REGULAR: usize = 0;
use crate::font::constants::*;
use crate::font::fonts::{parse_unicode, FontStyle};
use crate::font::metrics::{FaceMetrics, Metrics};
use crate::layout::SpanStyle;
use crate::SugarloafErrors;
use dashmap::DashMap;
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use swash::text::cluster::Parser;
use swash::text::cluster::Token;
use swash::text::cluster::{CharCluster, Status};
use swash::text::Codepoint;
use swash::text::Script;
use swash::{tag_from_bytes, CacheKey, FontRef, Synthesis};
pub use swash::{Style, Weight};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Slot {
Regular,
Bold,
Italic,
BoldItalic,
}
impl Slot {
#[inline]
pub fn is_bold(self) -> bool {
matches!(self, Slot::Bold | Slot::BoldItalic)
}
#[inline]
pub fn is_italic(self) -> bool {
matches!(self, Slot::Italic | Slot::BoldItalic)
}
}
type FontDataCache = Arc<DashMap<PathBuf, SharedData>>;
static FONT_DATA_CACHE: OnceLock<FontDataCache> = OnceLock::new();
fn get_font_data_cache() -> &'static FontDataCache {
FONT_DATA_CACHE.get_or_init(|| Arc::new(DashMap::default()))
}
pub fn clear_font_data_cache() {
if let Some(cache) = FONT_DATA_CACHE.get() {
cache.clear();
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LookupAttrs {
pub italic: bool,
pub bold: bool,
}
pub fn lookup_for_font_match(
cluster: &mut CharCluster,
synth: &mut Synthesis,
library: &FontLibraryData,
spec: Option<LookupAttrs>,
) -> Option<(usize, bool)> {
let mut search_result = None;
let fonts_len: usize = library.inner.len();
for font_id in 0..fonts_len {
let font = match library.inner.get(&font_id) {
Some(FontEntry::Owned(d)) => d,
Some(FontEntry::Alias(_)) | None => continue,
};
let is_emoji = font.is_emoji;
let font_synth = font.synth;
if let Some(spec) = spec {
if spec.italic && !font.is_italic() && !font.should_italicize {
continue;
}
if spec.bold && !font.is_bold() && !font.should_embolden {
continue;
}
}
#[cfg(target_os = "macos")]
let matched = {
let handle_opt = if let Some(path) = &font.path {
crate::font::macos::FontHandle::from_path(path)
} else if let Some(bytes) = &font.data {
crate::font::macos::FontHandle::from_bytes(bytes.as_ref())
} else {
None
};
if let Some(handle) = handle_opt {
let status = cluster.map(|ch| {
if crate::font::macos::font_has_char(&handle, ch) {
1
} else {
0
}
});
status != Status::Discard
} else {
false
}
};
#[cfg(not(target_os = "macos"))]
let matched = {
if let Some((shared_data, offset, key)) = library.get_data(&font_id) {
let font_ref = FontRef {
data: shared_data.as_ref(),
offset,
key,
};
let charmap = font_ref.charmap();
let status = cluster.map(|ch| charmap.map(ch));
status != Status::Discard
} else {
false
}
};
if matched {
*synth = font_synth;
search_result = Some((font_id, is_emoji));
break;
}
}
if search_result.is_none() && spec.is_some() {
return lookup_for_font_match(cluster, synth, library, None);
}
search_result
}
#[derive(Clone)]
pub struct FontLibrary {
pub inner: Arc<RwLock<FontLibraryData>>,
}
impl FontLibrary {
pub fn new(spec: SugarloafFonts) -> (Self, Option<SugarloafErrors>) {
let mut font_library = FontLibraryData::default();
let mut sugarloaf_errors = None;
let fonts_not_found = font_library.load(spec);
if !fonts_not_found.is_empty() {
sugarloaf_errors = Some(SugarloafErrors { fonts_not_found });
}
(
Self {
inner: Arc::new(RwLock::new(font_library)),
},
sugarloaf_errors,
)
}
#[cfg(target_os = "macos")]
pub fn ct_font(&self, font_id: usize) -> Option<crate::font::macos::FontHandle> {
self.inner
.read()
.try_get(&font_id)
.and_then(|f| f.handle().cloned())
}
pub fn font_id_for_postscript_name(&self, name: &str) -> Option<usize> {
self.inner.read().font_id_for_postscript_name(name)
}
pub fn resolve_font_for_char(
&self,
ch: char,
fragment_style: &SpanStyle,
route_id: Option<usize>,
) -> (usize, bool) {
if let Some(found) =
self.inner
.read()
.find_best_font_match_strict(ch, fragment_style, route_id)
{
return found;
}
self.cascade_discover(ch, fragment_style)
.unwrap_or((0, false))
}
#[cfg(target_os = "macos")]
fn cascade_discover(
&self,
ch: char,
_fragment_style: &SpanStyle,
) -> Option<(usize, bool)> {
let primary = self.ct_font(FONT_ID_REGULAR)?;
let discovered = crate::font::macos::discover_fallback(&primary, ch)?;
let ps_name = discovered.postscript_name();
if let Some(found) = self.dedupe_existing(&ps_name) {
return Some(found);
}
let mut lib = self.inner.write();
if let Some(existing) = lib.font_id_for_postscript_name(&ps_name) {
let is_emoji = lib
.try_get(&existing)
.map(|fd| fd.is_emoji)
.unwrap_or(false);
return Some((existing, is_emoji));
}
let font_data = FontData::from_ctfont_macos(discovered);
let is_emoji = font_data.is_emoji;
let new_id = lib.inner.len();
lib.insert(font_data);
tracing::debug!(
"CoreText cascade discovered {} for U+{:04X}, registered as font_id {}",
ps_name,
ch as u32,
new_id
);
Some((new_id, is_emoji))
}
#[cfg(any(
all(unix, not(target_os = "macos"), not(target_os = "android")),
target_os = "windows"
))]
fn cascade_discover(
&self,
ch: char,
fragment_style: &SpanStyle,
) -> Option<(usize, bool)> {
let primary_family = self.primary_family_name()?;
let want_bold = fragment_style.font_attrs.weight() == swash::Weight::BOLD;
let want_italic = fragment_style.font_attrs.style() == swash::Style::Italic;
let want_mono = true;
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))]
let discovered = crate::font::linux::discover_fallback(
&primary_family,
ch,
want_mono,
want_bold,
want_italic,
)?;
#[cfg(target_os = "windows")]
let discovered = crate::font::windows::discover_fallback(
&primary_family,
ch,
want_mono,
want_bold,
want_italic,
)?;
let (path, face_index) = discovered;
let font_data = FontData::from_discovered_path(path, face_index).ok()?;
let ps_name = font_data.postscript_name()?.to_string();
if let Some(found) = self.dedupe_existing(&ps_name) {
return Some(found);
}
let mut lib = self.inner.write();
if let Some(existing) = lib.font_id_for_postscript_name(&ps_name) {
let is_emoji = lib
.try_get(&existing)
.map(|fd| fd.is_emoji)
.unwrap_or(false);
return Some((existing, is_emoji));
}
let is_emoji = font_data.is_emoji;
let new_id = lib.inner.len();
lib.insert(font_data);
tracing::debug!(
"system cascade discovered {} for U+{:04X}, registered as font_id {}",
ps_name,
ch as u32,
new_id
);
Some((new_id, is_emoji))
}
fn dedupe_existing(&self, ps_name: &str) -> Option<(usize, bool)> {
let lib = self.inner.read();
let existing = lib.font_id_for_postscript_name(ps_name)?;
let is_emoji = lib
.try_get(&existing)
.map(|fd| fd.is_emoji)
.unwrap_or(false);
Some((existing, is_emoji))
}
#[cfg(any(
all(unix, not(target_os = "macos"), not(target_os = "android")),
target_os = "windows"
))]
fn primary_family_name(&self) -> Option<String> {
let lib = self.inner.read();
let primary = lib.try_get(&FONT_ID_REGULAR)?;
primary
.postscript_name()
.map(|s| s.to_string())
.or_else(|| Some(String::from("monospace")))
}
#[cfg(target_os = "macos")]
pub fn family_names(&self) -> Vec<String> {
crate::font::macos::all_families()
}
#[cfg(all(not(target_os = "macos"), not(target_arch = "wasm32")))]
pub fn family_names(&self) -> Vec<String> {
let source = font_kit::source::SystemSource::new();
let mut families = source.all_families().unwrap_or_default();
families.sort_unstable();
families.dedup();
families
}
#[cfg(target_arch = "wasm32")]
pub fn family_names(&self) -> Vec<String> {
Vec::new()
}
pub fn install_glyph_registry(
&self,
route_id: usize,
registry: glyph_registry::GlyphRegistry,
) {
self.inner
.write()
.glyph_registries
.insert(route_id, registry);
}
pub fn remove_glyph_registry(&self, route_id: usize) {
self.inner.write().glyph_registries.remove(&route_id);
}
#[inline]
pub fn glyph_registry_for(
&self,
route_id: usize,
) -> Option<glyph_registry::GlyphRegistry> {
self.inner.read().glyph_registries.get(&route_id).cloned()
}
pub fn covers_codepoint(&self, cp: u32) -> bool {
let Some(ch) = char::from_u32(cp) else {
return false;
};
self.inner
.read()
.find_best_font_match_strict(ch, &SpanStyle::default(), None)
.is_some_and(|(font_id, _)| font_id != glyph_registry::CUSTOM_GLYPH_FONT_ID)
}
}
impl Default for FontLibrary {
fn default() -> Self {
let mut font_library = FontLibraryData::default();
let _fonts_not_found = font_library.load(SugarloafFonts::default());
Self {
inner: Arc::new(RwLock::new(font_library)),
}
}
}
pub struct SymbolMap {
pub font_index: usize,
pub range: Range<char>,
}
#[derive(Clone)]
pub enum FontEntry {
Owned(FontData),
Alias(usize),
}
impl FontEntry {
#[inline]
pub fn as_owned(&self) -> Option<&FontData> {
match self {
FontEntry::Owned(d) => Some(d),
FontEntry::Alias(_) => None,
}
}
}
pub struct FontLibraryData {
pub inner: FxHashMap<usize, FontEntry>,
pub symbol_maps: Option<Vec<SymbolMap>>,
pub hinting: bool,
primary_metrics_cache: FxHashMap<u32, Metrics>,
postscript_to_id: FxHashMap<String, usize>,
glyph_registries: FxHashMap<usize, glyph_registry::GlyphRegistry>,
}
impl Default for FontLibraryData {
fn default() -> Self {
Self {
inner: FxHashMap::default(),
hinting: true,
symbol_maps: None,
primary_metrics_cache: FxHashMap::default(),
postscript_to_id: FxHashMap::default(),
glyph_registries: FxHashMap::default(),
}
}
}
impl FontLibraryData {
#[inline]
pub fn find_best_font_match(
&self,
ch: char,
fragment_style: &SpanStyle,
route_id: Option<usize>,
) -> Option<(usize, bool)> {
if let Some(route_id) = route_id {
if let Some(registry) = self.glyph_registries.get(&route_id) {
if registry.contains(ch as u32) {
return Some((glyph_registry::CUSTOM_GLYPH_FONT_ID, false));
}
}
}
let mut synth = Synthesis::default();
let mut char_cluster = CharCluster::new();
let mut parser = Parser::new(
Script::Latin,
std::iter::once(Token {
ch,
offset: 0,
len: ch.len_utf8() as u8,
info: ch.properties().into(),
data: 0,
}),
);
if !parser.next(&mut char_cluster) {
return Some((0, false));
}
if let Some(symbol_maps) = &self.symbol_maps {
for symbol_map in symbol_maps {
if symbol_map.range.contains(&ch) {
return Some((symbol_map.font_index, false));
}
}
}
let italic = fragment_style.font_attrs.style() == Style::Italic;
let bold = fragment_style.font_attrs.weight() == Weight::BOLD;
let spec = (italic || bold).then_some(LookupAttrs { italic, bold });
if let Some(result) =
lookup_for_font_match(&mut char_cluster, &mut synth, self, spec)
{
return Some(result);
}
Some((0, false))
}
#[inline]
pub fn find_best_font_match_strict(
&self,
ch: char,
fragment_style: &SpanStyle,
route_id: Option<usize>,
) -> Option<(usize, bool)> {
if let Some(route_id) = route_id {
if let Some(registry) = self.glyph_registries.get(&route_id) {
if registry.contains(ch as u32) {
return Some((glyph_registry::CUSTOM_GLYPH_FONT_ID, false));
}
}
}
let mut synth = Synthesis::default();
let mut char_cluster = CharCluster::new();
let mut parser = Parser::new(
Script::Latin,
std::iter::once(Token {
ch,
offset: 0,
len: ch.len_utf8() as u8,
info: ch.properties().into(),
data: 0,
}),
);
if !parser.next(&mut char_cluster) {
return None;
}
if let Some(symbol_maps) = &self.symbol_maps {
for symbol_map in symbol_maps {
if symbol_map.range.contains(&ch) {
return Some((symbol_map.font_index, false));
}
}
}
let italic = fragment_style.font_attrs.style() == Style::Italic;
let bold = fragment_style.font_attrs.weight() == Weight::BOLD;
let spec = (italic || bold).then_some(LookupAttrs { italic, bold });
lookup_for_font_match(&mut char_cluster, &mut synth, self, spec)
}
#[inline]
pub fn insert(&mut self, font_data: FontData) {
let id = self.inner.len();
if let Some(ps_name) = font_data.postscript_name() {
self.postscript_to_id
.entry(ps_name.to_string())
.or_insert(id);
}
self.inner.insert(id, FontEntry::Owned(font_data));
}
#[inline]
pub fn insert_alias(&mut self, target: usize) {
let id = self.inner.len();
let target = self.resolve_id(target);
self.inner.insert(id, FontEntry::Alias(target));
}
#[inline]
pub fn resolve_id(&self, font_id: usize) -> usize {
match self.inner.get(&font_id) {
Some(FontEntry::Alias(target)) => *target,
_ => font_id,
}
}
pub fn font_id_for_postscript_name(&self, name: &str) -> Option<usize> {
self.postscript_to_id.get(name).copied()
}
#[inline]
pub fn get(&self, font_id: &usize) -> &FontData {
let id = self.resolve_id(*font_id);
match &self.inner[&id] {
FontEntry::Owned(d) => d,
FontEntry::Alias(_) => {
unreachable!("alias must resolve to Owned in single hop")
}
}
}
#[inline]
pub fn try_get(&self, font_id: &usize) -> Option<&FontData> {
let id = self.resolve_id(*font_id);
self.inner.get(&id).and_then(FontEntry::as_owned)
}
pub fn get_data(&self, font_id: &usize) -> Option<(SharedData, u32, CacheKey)> {
if let Some(font) = self.try_get(font_id) {
if let Some(data) = &font.data {
return Some((data.clone(), font.offset, font.key));
} else if let Some(path) = &font.path {
if let Some(raw_data) = load_from_font_source(path) {
return Some((raw_data, font.offset, font.key));
}
}
}
None
}
#[inline]
pub fn get_mut(&mut self, font_id: &usize) -> Option<&mut FontData> {
let id = self.resolve_id(*font_id);
match self.inner.get_mut(&id)? {
FontEntry::Owned(d) => Some(d),
FontEntry::Alias(_) => None,
}
}
pub fn get_font_metrics(
&mut self,
font_id: &usize,
font_size: f32,
) -> Option<(f32, f32, f32)> {
let size_key = (font_size * 100.0) as u32;
let primary_metrics =
if let Some(cached) = self.primary_metrics_cache.get(&size_key) {
*cached
} else {
let primary_font = self.get_mut(&FONT_ID_REGULAR)?;
let primary_metrics = primary_font.get_metrics(font_size, None)?;
self.primary_metrics_cache.insert(size_key, primary_metrics);
primary_metrics
};
let resolved = self.resolve_id(*font_id);
match resolved {
FONT_ID_REGULAR => {
Some(primary_metrics.for_rich_text())
}
_ => {
let font = self.get_mut(&resolved)?;
font.get_rich_text_metrics(font_size, Some(&primary_metrics))
}
}
}
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load(&mut self, mut spec: SugarloafFonts) -> Vec<SugarloafFont> {
self.hinting = spec.hinting;
let mut fonts_not_fount: Vec<SugarloafFont> = vec![];
if let Some(font_family_overwrite) = spec.family {
font_family_overwrite.clone_into(&mut spec.regular.family);
font_family_overwrite.clone_into(&mut spec.bold.family);
font_family_overwrite.clone_into(&mut spec.bold_italic.family);
font_family_overwrite.clone_into(&mut spec.italic.family);
}
#[cfg(not(target_os = "macos"))]
let mut db = loader::Database::new();
let additional_dirs = spec.additional_dirs.unwrap_or_default();
for dir in additional_dirs.into_iter().map(PathBuf::from) {
#[cfg(target_os = "macos")]
crate::font::macos::register_fonts_in_dir(&dir);
#[cfg(not(target_os = "macos"))]
db.load_fonts_dir(dir);
}
#[cfg(target_os = "macos")]
let resolve = |spec: SugarloafFont, slot: Slot, evictable: bool| {
find_font(spec, slot, evictable)
};
#[cfg(not(target_os = "macos"))]
let resolve = |spec: SugarloafFont, slot: Slot, evictable: bool| {
find_font(&db, spec, slot, evictable)
};
let regular_index = self.len();
match resolve(spec.regular, Slot::Regular, false) {
FindResult::Found(data) => {
self.insert(data);
}
FindResult::NotFound(spec) => {
if !spec.is_default_family() {
fonts_not_fount.push(spec.to_owned());
}
self.insert(load_fallback_from_memory(Slot::Regular));
}
}
for (slot, slot_spec, evictable) in [
(Slot::Italic, spec.italic, false),
(Slot::Bold, spec.bold, false),
(Slot::BoldItalic, spec.bold_italic, true),
] {
if slot_spec.style.is_disabled() {
self.insert_alias(regular_index);
continue;
}
match resolve(slot_spec, slot, evictable) {
FindResult::Found(data) => {
self.insert(data);
}
FindResult::NotFound(spec) => {
if spec.is_default_family() {
self.insert(load_fallback_from_memory(slot));
} else {
warn!(
"Font family '{}' has no {:?} variant; falling back to regular",
spec.family, slot
);
self.insert_alias(regular_index);
}
}
}
}
#[cfg(target_os = "macos")]
{
let primary_handle = self.try_get(&FONT_ID_REGULAR).and_then(|f| {
if let Some(path) = &f.path {
crate::font::macos::FontHandle::from_path(path)
} else if let Some(bytes) = &f.data {
crate::font::macos::FontHandle::from_bytes(bytes.as_ref())
} else {
None
}
});
if let Some(primary_handle) = primary_handle {
let default_spec = SugarloafFont::default();
for path in crate::font::macos::default_cascade_list(&primary_handle) {
if let Ok(font_data) =
FontData::from_path_macos(path, Slot::Regular, &default_spec)
{
self.insert(font_data);
}
}
}
}
if let Some(symbol_map) = spec.symbol_map {
let mut symbol_maps = Vec::default();
for extra_font_from_symbol_map in symbol_map {
match resolve(
SugarloafFont {
family: extra_font_from_symbol_map.font_family,
..SugarloafFont::default()
},
Slot::Regular,
true,
) {
FindResult::Found(data) => {
if let Some(start) =
parse_unicode(&extra_font_from_symbol_map.start)
{
if let Some(end) =
parse_unicode(&extra_font_from_symbol_map.end)
{
self.insert(data);
symbol_maps.push(SymbolMap {
range: start..end,
font_index: self.len() - 1,
});
continue;
}
}
warn!("symbol-map: Failed to parse start and end values");
}
FindResult::NotFound(spec) => {
fonts_not_fount.push(spec);
}
}
}
self.symbol_maps = Some(symbol_maps);
}
if spec.disable_warnings_not_found {
vec![]
} else {
fonts_not_fount
}
}
#[cfg(target_arch = "wasm32")]
pub fn load(&mut self, _font_spec: SugarloafFonts) -> Vec<SugarloafFont> {
self.insert(FontData::from_slice(FONT_CASCADIA_CODE_NF).unwrap());
vec![]
}
}
#[derive(Clone, Debug)]
pub enum SharedData {
Heap(Arc<[u8]>),
Static(&'static [u8]),
#[cfg(not(target_arch = "wasm32"))]
Mmap(Arc<memmap2::Mmap>),
}
impl SharedData {
pub fn new(data: Vec<u8>) -> Self {
Self::Heap(Arc::from(data))
}
pub const fn from_static(data: &'static [u8]) -> Self {
Self::Static(data)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_mmap(mmap: memmap2::Mmap) -> Self {
Self::Mmap(Arc::new(mmap))
}
pub const fn is_static(&self) -> bool {
matches!(self, Self::Static(_))
}
}
impl std::ops::Deref for SharedData {
type Target = [u8];
fn deref(&self) -> &Self::Target {
match self {
Self::Heap(a) => a,
Self::Static(s) => s,
#[cfg(not(target_arch = "wasm32"))]
Self::Mmap(m) => m.as_ref(),
}
}
}
impl AsRef<[u8]> for SharedData {
fn as_ref(&self) -> &[u8] {
match self {
Self::Heap(a) => a,
Self::Static(s) => s,
#[cfg(not(target_arch = "wasm32"))]
Self::Mmap(m) => m.as_ref(),
}
}
}
#[derive(Clone)]
pub struct FontData {
data: Option<SharedData>,
path: Option<PathBuf>,
offset: u32,
pub key: CacheKey,
pub weight: swash::Weight,
pub style: swash::Style,
pub stretch: swash::Stretch,
pub synth: Synthesis,
pub should_embolden: bool,
pub should_italicize: bool,
pub wght_variation: Option<f32>,
pub is_emoji: bool,
metrics_cache: FxHashMap<u32, Metrics>,
#[cfg(target_os = "macos")]
handle: Option<crate::font::macos::FontHandle>,
postscript_name: Option<String>,
}
impl PartialEq for FontData {
fn eq(&self, other: &Self) -> bool {
self.key == other.key
}
}
impl FontData {
#[inline]
pub fn is_bold(&self) -> bool {
self.weight >= Weight(700)
}
#[inline]
pub fn is_italic(&self) -> bool {
self.style == Style::Italic
}
pub fn data(&self) -> &Option<SharedData> {
&self.data
}
pub fn path(&self) -> Option<&PathBuf> {
self.path.as_ref()
}
#[cfg(target_os = "macos")]
pub fn handle(&self) -> Option<&crate::font::macos::FontHandle> {
self.handle.as_ref()
}
pub fn postscript_name(&self) -> Option<&str> {
self.postscript_name.as_deref()
}
pub fn offset(&self) -> u32 {
self.offset
}
pub fn get_metrics(
&mut self,
font_size: f32,
primary_metrics: Option<&Metrics>,
) -> Option<Metrics> {
let size_key = (font_size * 100.0) as u32;
if let Some(cached) = self.metrics_cache.get(&size_key) {
return Some(*cached);
}
#[cfg(target_os = "macos")]
if self.data.is_none() {
let handle = if let Some(h) = self.handle.as_ref() {
h.clone()
} else {
self.path
.as_ref()
.and_then(|p| crate::font::macos::FontHandle::from_path(p))?
};
let font_metrics = crate::font::macos::design_unit_metrics(&handle);
let scaled_metrics = font_metrics.scale(font_size);
let face_metrics = FaceMetrics {
cell_width: scaled_metrics.max_width as f64,
ascent: scaled_metrics.ascent as f64,
descent: scaled_metrics.descent as f64,
line_gap: scaled_metrics.leading as f64,
underline_position: Some(scaled_metrics.underline_offset as f64),
underline_thickness: Some(scaled_metrics.stroke_size as f64),
strikethrough_position: Some(scaled_metrics.strikeout_offset as f64),
strikethrough_thickness: Some(scaled_metrics.stroke_size as f64),
cap_height: Some(scaled_metrics.cap_height as f64),
ex_height: Some(scaled_metrics.x_height as f64),
ic_width: crate::font::macos::cjk_ic_width(&handle).map(|u| {
u * font_size as f64 / scaled_metrics.units_per_em as f64
}),
};
let metrics = if let Some(primary) = primary_metrics {
Metrics::calc_with_primary_cell_dimensions(face_metrics, primary)
} else {
Metrics::calc(face_metrics)
};
self.metrics_cache.insert(size_key, metrics);
return Some(metrics);
}
if let Some(ref data) = self.data {
let font_ref = swash::FontRef {
data: data.as_ref(),
offset: self.offset,
key: self.key,
};
let scaled_metrics = font_ref.metrics(&[]).scale(font_size);
let face_metrics = FaceMetrics::from_font(&font_ref, &scaled_metrics);
let metrics = if let Some(primary) = primary_metrics {
Metrics::calc_with_primary_cell_dimensions(face_metrics, primary)
} else {
Metrics::calc(face_metrics)
};
self.metrics_cache.insert(size_key, metrics);
Some(metrics)
} else {
None
}
}
pub fn get_rich_text_metrics(
&mut self,
font_size: f32,
primary_metrics: Option<&Metrics>,
) -> Option<(f32, f32, f32)> {
self.get_metrics(font_size, primary_metrics)
.map(|m| m.for_rich_text())
}
#[inline]
pub fn from_data(
data: SharedData,
path: PathBuf,
evictable: bool,
slot: Slot,
font_spec: &SugarloafFont,
) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontRef::from_index(&data, 0)
.ok_or_else(|| format!("Failed to load font from path: {:?}", path))?;
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = attributes.weight();
let (should_embolden, should_italicize) = synth_decisions(
slot,
font_spec,
weight >= Weight(700),
style == Style::Italic,
);
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(&data);
let data = (!evictable).then_some(data);
Ok(Self {
data,
offset,
should_italicize,
should_embolden,
wght_variation: None,
key,
synth,
style,
weight,
stretch,
path: Some(path),
is_emoji,
metrics_cache: FxHashMap::default(),
#[cfg(target_os = "macos")]
handle: None,
postscript_name,
})
}
#[cfg(target_os = "macos")]
pub fn from_ctfont_macos(handle: crate::font::macos::FontHandle) -> Self {
let attrs = crate::font::macos::font_attributes(&handle);
let style = if attrs.is_italic {
swash::Style::Italic
} else {
swash::Style::Normal
};
let weight = swash::Weight(attrs.weight);
let postscript_name = Some(handle.postscript_name());
Self {
data: None,
path: None,
offset: 0,
key: CacheKey::new(),
weight,
style,
stretch: swash::Stretch::NORMAL,
synth: Synthesis::default(),
should_embolden: false,
should_italicize: false,
wght_variation: None,
is_emoji: attrs.is_color,
metrics_cache: FxHashMap::default(),
handle: Some(handle),
postscript_name,
}
}
#[cfg(target_os = "macos")]
pub fn from_path_macos(
path: PathBuf,
slot: Slot,
font_spec: &SugarloafFont,
) -> Result<Self, Box<dyn std::error::Error>> {
let handle = crate::font::macos::FontHandle::from_path(&path)
.ok_or_else(|| format!("CoreText refused {}", path.display()))?;
let attrs = crate::font::macos::font_attributes(&handle);
let style = if attrs.is_italic {
swash::Style::Italic
} else {
swash::Style::Normal
};
let weight = swash::Weight(attrs.weight);
let (should_embolden, should_italicize) =
synth_decisions(slot, font_spec, attrs.is_bold, attrs.is_italic);
let postscript_name = Some(handle.postscript_name());
Ok(Self {
data: None,
path: Some(path),
offset: 0,
key: CacheKey::new(),
weight,
style,
stretch: swash::Stretch::NORMAL,
synth: Synthesis::default(),
should_embolden,
should_italicize,
wght_variation: None,
is_emoji: attrs.is_color,
metrics_cache: FxHashMap::default(),
handle: Some(handle),
postscript_name,
})
}
#[inline]
pub fn from_static_slice(
data: &'static [u8],
) -> Result<Self, Box<dyn std::error::Error>> {
Self::from_static_slice_with_wght(data, None)
}
pub fn from_static_slice_with_wght(
data: &'static [u8],
wght: Option<f32>,
) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontRef::from_index(data, 0).unwrap();
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = match wght {
Some(v) => swash::Weight(v.round().clamp(0.0, u16::MAX as f32) as u16),
None => attributes.weight(),
};
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(data);
#[cfg(target_os = "macos")]
let handle = {
let base = crate::font::macos::FontHandle::from_static_bytes(data);
match (base, wght) {
(Some(h), Some(v)) => Some(h.clone().with_wght_variation(v).unwrap_or(h)),
(h, _) => h,
}
};
Ok(Self {
data: Some(SharedData::from_static(data)),
offset,
key,
synth,
style,
should_embolden: false,
should_italicize: false,
wght_variation: wght,
weight,
stretch,
path: None,
is_emoji,
metrics_cache: FxHashMap::default(),
#[cfg(target_os = "macos")]
handle,
postscript_name,
})
}
#[inline]
pub fn from_slice(data: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontRef::from_index(data, 0).unwrap();
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = attributes.weight();
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(data);
Ok(Self {
data: Some(SharedData::new(data.to_vec())),
offset,
key,
synth,
style,
should_embolden: false,
should_italicize: false,
wght_variation: None,
weight,
stretch,
path: None,
is_emoji,
metrics_cache: FxHashMap::default(),
#[cfg(target_os = "macos")]
handle: None,
postscript_name,
})
}
#[cfg(any(
all(unix, not(target_os = "macos"), not(target_os = "android")),
target_os = "windows"
))]
pub fn from_discovered_path(
path: PathBuf,
face_index: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
let data = load_from_font_source(&path).ok_or_else(|| {
format!("failed to load discovered font: {}", path.display())
})?;
let font = FontRef::from_index(&data, face_index as usize).ok_or_else(|| {
format!(
"failed to parse discovered font {} face {}",
path.display(),
face_index
)
})?;
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = attributes.weight();
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(&data);
Ok(Self {
data: Some(data),
offset,
should_italicize: false,
should_embolden: false,
wght_variation: None,
key,
synth,
style,
weight,
stretch,
path: Some(path),
is_emoji,
metrics_cache: FxHashMap::default(),
postscript_name,
})
}
}
fn parse_postscript_name(data: &[u8]) -> Option<String> {
let face = ttf_parser::Face::parse(data, 0).ok()?;
face.names()
.into_iter()
.find(|n| n.name_id == ttf_parser::name_id::POST_SCRIPT_NAME && n.is_unicode())
.and_then(|n| n.to_string())
.or_else(|| {
face.names()
.into_iter()
.find(|n| n.name_id == ttf_parser::name_id::FAMILY && n.is_unicode())
.and_then(|n| n.to_string())
})
}
fn has_color_tables(font: &FontRef<'_>) -> bool {
font.table(tag_from_bytes(b"COLR")).is_some()
|| font.table(tag_from_bytes(b"CBDT")).is_some()
|| font.table(tag_from_bytes(b"CBLC")).is_some()
|| font.table(tag_from_bytes(b"sbix")).is_some()
}
pub type SugarloafFont = fonts::SugarloafFont;
pub type SugarloafFonts = fonts::SugarloafFonts;
#[cfg(not(target_arch = "wasm32"))]
use tracing::{info, warn};
enum FindResult {
Found(FontData),
NotFound(SugarloafFont),
}
#[inline]
fn synth_decisions(
slot: Slot,
font_spec: &SugarloafFont,
matched_is_bold: bool,
matched_is_italic: bool,
) -> (bool, bool) {
let allowed = !matches!(font_spec.style, FontStyle::Named(_));
let embolden = allowed && slot.is_bold() && !matched_is_bold;
let italicize = allowed && slot.is_italic() && !matched_is_italic;
(embolden, italicize)
}
#[cfg(target_os = "macos")]
#[inline]
fn find_font(font_spec: SugarloafFont, slot: Slot, evictable: bool) -> FindResult {
if font_spec.is_default_family() {
return FindResult::NotFound(font_spec);
}
let family = font_spec.family.to_string();
let style_name = font_spec.style.name();
let bold = slot.is_bold();
let italic = slot.is_italic();
info!(
"Font search (CoreText): family='{family}' bold={bold} italic={italic} style={:?}",
style_name
);
let Some(path) =
crate::font::macos::find_font_path(&family, bold, italic, style_name)
else {
warn!("CoreText found no match for family='{family}'");
return FindResult::NotFound(font_spec);
};
let _ = evictable;
match FontData::from_path_macos(path.clone(), slot, &font_spec) {
Ok(d) => {
info!("Font '{family}' matched via CoreText at {}", path.display());
FindResult::Found(d)
}
Err(e) => {
warn!("Failed to open font '{family}' via CoreText: {e}");
FindResult::NotFound(font_spec)
}
}
}
#[cfg(all(not(target_os = "macos"), not(target_arch = "wasm32")))]
#[inline]
fn find_font(
db: &crate::font::loader::Database,
font_spec: SugarloafFont,
slot: Slot,
evictable: bool,
) -> FindResult {
if !font_spec.is_default_family() {
let family = font_spec.family.to_string();
let mut query = crate::font::loader::Query {
families: &[crate::font::loader::Family::Name(&family)],
..crate::font::loader::Query::default()
};
query.weight = if slot.is_bold() {
crate::font::loader::Weight::BOLD
} else {
crate::font::loader::Weight::NORMAL
};
query.style = if slot.is_italic() {
crate::font::loader::Style::Italic
} else {
crate::font::loader::Style::Normal
};
info!(
"Font search: '{query:?}' style_override={:?}",
font_spec.style.name()
);
match db.query(&query) {
Some(id) => {
match db.face_source(id) {
Some((crate::font::loader::Source::File(ref path), _index)) => {
if let Some(font_data_arc) =
load_from_font_source(&path.to_path_buf())
{
match FontData::from_data(
font_data_arc,
path.to_path_buf(),
evictable,
slot,
&font_spec,
) {
Ok(d) => {
tracing::info!(
"Font '{}' found in {}",
family,
path.display()
);
return FindResult::Found(d);
}
Err(err_message) => {
tracing::info!(
"Failed to load font '{query:?}', {err_message}"
);
return FindResult::NotFound(font_spec);
}
}
}
}
Some((crate::font::loader::Source::Binary(font_data), _index)) => {
tracing::debug!(
"Using binary font data, {} bytes",
font_data.len()
);
match FontData::from_data(
font_data,
std::path::PathBuf::from(&family),
evictable,
slot,
&font_spec,
) {
Ok(d) => {
tracing::info!("Font '{}' loaded from memory", family);
return FindResult::Found(d);
}
Err(err_message) => {
tracing::info!(
"Failed to load font '{query:?}' from memory, {err_message}"
);
return FindResult::NotFound(font_spec);
}
}
}
None => {
tracing::warn!("face_source returned None for font ID");
}
}
}
None => {
warn!("Failed to find font '{query:?}'");
}
}
}
FindResult::NotFound(font_spec)
}
fn load_fallback_from_memory(slot: Slot) -> FontData {
use constants::{FONT_CASCADIA_CODE_NF, FONT_CASCADIA_CODE_NF_ITALIC, WGHT_BOLD};
let (data, wght) = match slot {
Slot::Regular => (FONT_CASCADIA_CODE_NF, None),
Slot::Bold => (FONT_CASCADIA_CODE_NF, Some(WGHT_BOLD)),
Slot::Italic => (FONT_CASCADIA_CODE_NF_ITALIC, None),
Slot::BoldItalic => (FONT_CASCADIA_CODE_NF_ITALIC, Some(WGHT_BOLD)),
};
FontData::from_static_slice_with_wght(data, wght).unwrap()
}
#[cfg(test)]
mod alias_tests {
use super::*;
#[test]
fn insert_alias_resolves_to_target() {
let mut lib = FontLibraryData::default();
lib.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF)
.expect("load regular"),
);
lib.insert_alias(0);
assert_eq!(lib.len(), 2, "alias takes a slot");
assert_eq!(lib.resolve_id(1), 0, "alias resolves to its target");
let owned_key = lib.get(&0).key;
let aliased_key = lib.get(&1).key;
assert_eq!(
owned_key, aliased_key,
"aliased slot must surface the target FontData"
);
}
#[test]
fn alias_of_alias_collapses_to_root() {
let mut lib = FontLibraryData::default();
lib.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF)
.expect("load regular"),
);
lib.insert_alias(0);
lib.insert_alias(1);
assert_eq!(
lib.resolve_id(2),
0,
"alias pointing at an alias must collapse to the owning id"
);
assert!(matches!(lib.inner.get(&2), Some(FontEntry::Alias(0))));
}
#[test]
fn fallback_bold_slot_reports_is_bold() {
let regular = load_fallback_from_memory(Slot::Regular);
let bold = load_fallback_from_memory(Slot::Bold);
let italic = load_fallback_from_memory(Slot::Italic);
let bold_italic = load_fallback_from_memory(Slot::BoldItalic);
assert!(!regular.is_bold(), "regular slot must not be bold");
assert!(bold.is_bold(), "bold slot must report is_bold");
assert!(!italic.is_bold(), "italic slot must not be bold");
assert!(
bold_italic.is_bold(),
"bold-italic slot must report is_bold"
);
assert!(!regular.is_italic(), "regular slot must not be italic");
assert!(!bold.is_italic(), "bold slot must not be italic");
assert!(italic.is_italic(), "italic slot must report is_italic");
assert!(
bold_italic.is_italic(),
"bold-italic slot must report is_italic"
);
assert_eq!(bold.wght_variation, Some(constants::WGHT_BOLD));
assert_eq!(bold_italic.wght_variation, Some(constants::WGHT_BOLD));
assert_eq!(regular.wght_variation, None);
assert_eq!(italic.wght_variation, None);
}
#[test]
fn alias_shares_metrics_with_target() {
let mut lib = FontLibraryData::default();
lib.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF)
.expect("load regular"),
);
lib.insert_alias(0);
let from_regular = lib.get_font_metrics(&0, 14.0).expect("regular metrics");
let from_alias = lib.get_font_metrics(&1, 14.0).expect("alias metrics");
assert_eq!(from_regular, from_alias);
}
}
#[allow(dead_code)]
fn find_font_path(
db: &crate::font::loader::Database,
font_family: String,
) -> Option<PathBuf> {
info!("Font path search: family '{font_family}'");
let query = crate::font::loader::Query {
families: &[crate::font::loader::Family::Name(&font_family)],
..crate::font::loader::Query::default()
};
if let Some(id) = db.query(&query) {
if let Some((crate::font::loader::Source::File(ref path), _index)) =
db.face_source(id)
{
return Some(path.to_path_buf());
}
}
None
}
#[cfg(not(target_arch = "wasm32"))]
fn load_from_font_source(path: &PathBuf) -> Option<SharedData> {
let cache = get_font_data_cache();
if let Some(cached_data) = cache.get(path) {
return Some(cached_data.clone());
}
let file = std::fs::File::open(path).ok()?;
let mmap = unsafe { memmap2::Mmap::map(&file).ok()? };
let shared_data = SharedData::from_mmap(mmap);
let entry = cache
.entry(path.clone())
.or_insert_with(|| shared_data.clone());
Some(entry.clone())
}
#[cfg(all(test, target_os = "macos"))]
mod postscript_resolver_tests {
use super::*;
#[test]
fn insert_populates_postscript_lookup() {
let handle =
crate::font::macos::FontHandle::from_static_bytes(FONT_CASCADIA_CODE_NF)
.expect("parse CascadiaMono");
let ps_name = handle.postscript_name();
let mut lib = FontLibraryData::default();
let font_data = FontData::from_static_slice(FONT_CASCADIA_CODE_NF)
.expect("load CascadiaMono");
lib.insert(font_data);
assert_eq!(
lib.font_id_for_postscript_name(&ps_name),
Some(0),
"inserted PS name '{ps_name}' should resolve to font_id 0"
);
assert_eq!(
lib.font_id_for_postscript_name("not-a-real-font"),
None,
"unknown PS names must return None, not a stale hit"
);
}
#[test]
fn duplicate_insert_keeps_first_id() {
let handle =
crate::font::macos::FontHandle::from_static_bytes(FONT_CASCADIA_CODE_NF)
.expect("parse CascadiaMono");
let ps_name = handle.postscript_name();
let mut lib = FontLibraryData::default();
lib.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load a"));
lib.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load b"));
assert_eq!(
lib.font_id_for_postscript_name(&ps_name),
Some(0),
"second insert of same face must not clobber the first's font_id"
);
}
#[test]
fn resolve_font_for_char_lazy_discovers_cascade_font() {
use crate::SpanStyle;
use std::sync::Arc;
let mut data = FontLibraryData::default();
data.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load"));
let lib = FontLibrary {
inner: Arc::new(parking_lot::RwLock::new(data)),
};
let starting_len = lib.inner.read().inner.len();
let style = SpanStyle::default();
let (font_id, _is_emoji) = lib.resolve_font_for_char('\u{6C34}', &style, None);
assert_ne!(
font_id, 0,
"lazy discovery should register a new font_id distinct from primary"
);
assert!(
font_id < lib.inner.read().inner.len(),
"returned font_id should index into the library"
);
assert_eq!(
lib.inner.read().inner.len(),
starting_len + 1,
"lazy discovery should have registered exactly one new font"
);
}
#[test]
fn resolve_font_for_char_reuses_discovered_font() {
use crate::SpanStyle;
use std::sync::Arc;
let mut data = FontLibraryData::default();
data.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load"));
let lib = FontLibrary {
inner: Arc::new(parking_lot::RwLock::new(data)),
};
let style = SpanStyle::default();
let (id_a, _) = lib.resolve_font_for_char('\u{6C34}', &style, None);
let len_after_first = lib.inner.read().inner.len();
let (id_b, _) = lib.resolve_font_for_char('\u{6728}', &style, None);
let len_after_second = lib.inner.read().inner.len();
assert_eq!(
id_a, id_b,
"two CJK codepoints from the same cascade font should reuse the same font_id"
);
assert_eq!(
len_after_first, len_after_second,
"the second resolve must not register a duplicate font"
);
}
}
#[cfg(test)]
mod glyph_registry_install_tests {
use super::*;
use crate::font::glyph_registry::GlyphRegistry;
#[test]
fn install_then_lookup_returns_same_arc() {
let library = FontLibrary::default();
let registry = GlyphRegistry::new();
library.install_glyph_registry(42, registry.clone());
let fetched = library
.glyph_registry_for(42)
.expect("entry installed at 42");
assert!(fetched.ptr_eq(®istry));
}
#[test]
fn lookup_returns_none_for_unknown_route() {
let library = FontLibrary::default();
assert!(library.glyph_registry_for(999).is_none());
}
#[test]
fn install_overwrites_same_route() {
let library = FontLibrary::default();
let first = GlyphRegistry::new();
let second = GlyphRegistry::new();
assert!(!first.ptr_eq(&second));
library.install_glyph_registry(7, first.clone());
library.install_glyph_registry(7, second.clone());
let fetched = library.glyph_registry_for(7).expect("entry at 7");
assert!(fetched.ptr_eq(&second));
assert!(!fetched.ptr_eq(&first));
}
#[test]
fn remove_drops_the_entry() {
let library = FontLibrary::default();
let registry = GlyphRegistry::new();
library.install_glyph_registry(3, registry);
assert!(library.glyph_registry_for(3).is_some());
library.remove_glyph_registry(3);
assert!(library.glyph_registry_for(3).is_none());
}
#[test]
fn distinct_routes_hold_distinct_registries() {
let library = FontLibrary::default();
let a = GlyphRegistry::new();
let b = GlyphRegistry::new();
library.install_glyph_registry(1, a.clone());
library.install_glyph_registry(2, b.clone());
assert!(library.glyph_registry_for(1).unwrap().ptr_eq(&a));
assert!(library.glyph_registry_for(2).unwrap().ptr_eq(&b));
}
}