Fonts: installed fonts are found by name whatever ran before

FontDb cataloged the system fonts only when a character needed a fallback
(nothing else calls load_system_fonts), so a document set in an installed
family was composed in Source Sans 3, and font.list and preflight reported
the family missing (#22).

Every lookup by family name now waits for one scan of the font folders per
session, as VectorCraft does since #130: the app starts the scan in the
background at startup, other processes run it on first use. The scan reads
only each file's table directory and name table, and skips folders it has
already read, so a symlink loop can't hang it. has_family counts installed
families. FontDb::with_font_dirs and system_font_dirs let tests use their
own font folder.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
japbcoelhoandClaude Opus 5.5 authored and Brandon Thomas committed 2026-10-06 09:05:40 -04:00
1 parent b8e3b2c029
commit 48e409e214
5 files changed
+340 -50

No files matched your search

+178 -49
View File
@@ -1,4 +1,5 @@
//! Font database: bundled OFL fonts, user fonts, optional system font catalog, outline cache.
//! Font database: bundled OFL fonts, user fonts, the installed system fonts (cataloged the first
//! time a lookup needs them, native only), outline cache.
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
@@ -223,7 +224,14 @@ pub struct FontDb {
outlines: Mutex<HashMap<(u32, u32), Arc<BezPath>>>,
#[cfg(not(target_arch = "wasm32"))]
catalog: RwLock<Vec<CatalogEntry>>,
/// System fallback state: scanned yet, and characters no system font covers.
/// The folders the system font scan reads (the platform's font folders for [`FontDb::global`]).
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
font_dirs: Vec<std::path::PathBuf>,
/// Set once the font folders have been scanned. Lookups by family name wait for the first
/// scan, so what they find doesn't depend on what ran before them.
#[cfg(not(target_arch = "wasm32"))]
cataloged: std::sync::OnceLock<()>,
/// System fallback state: enabled, and characters no system font covers.
#[cfg(not(target_arch = "wasm32"))]
sys: Mutex<SysFallback>,
}
@@ -232,7 +240,6 @@ pub struct FontDb {
#[derive(Default)]
struct SysFallback {
enabled: bool,
scanned: bool,
misses: std::collections::HashSet<char>,
}
@@ -273,6 +280,93 @@ fn name(font: &skrifa::FontRef<'_>, ids: &[StringId]) -> Option<String> {
ids.iter().find_map(|id| font.localized_strings(*id).english_or_first().map(|s| s.to_string()).filter(|s| !s.is_empty()))
}
/// A face's family and style names (the default instance's style for a variable font).
fn face_names(f: &skrifa::FontRef<'_>) -> Option<(String, String)> {
let family = name(f, &[StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME])?;
let style = name(f, &[StringId::TYPOGRAPHIC_SUBFAMILY_NAME, StringId::SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into());
Some((family, style))
}
/// (family, style) of every face in the font file at `path`, reading only its table directories
/// and `name` tables: a scan opens hundreds of font files, many of them megabytes long. A
/// variable font is cataloged under its default style; its named instances appear once it loads.
#[cfg(not(target_arch = "wasm32"))]
fn file_face_names(path: &std::path::Path) -> Vec<(String, String)> {
use std::io::{Read, Seek, SeekFrom};
/// Caps on what a (possibly damaged) file can make the scan read.
const MAX_FACES: u32 = 256;
const MAX_NAME_TABLE: u32 = 1 << 20;
let Ok(mut file) = std::fs::File::open(path) else { return vec![] };
let mut read_at = |offset: u64, len: usize| -> Option<Vec<u8>> {
let mut buf = vec![0; len];
file.seek(SeekFrom::Start(offset)).ok()?;
file.read_exact(&mut buf).ok()?;
Some(buf)
};
let be32 = |b: &[u8], at: usize| b.get(at..at + 4).and_then(|s| s.try_into().ok()).map(u32::from_be_bytes);
let Some(head) = read_at(0, 12) else { return vec![] };
// A collection lists where each face's table directory starts.
let starts: Vec<u32> = if head.starts_with(b"ttcf") {
let n = be32(&head, 8).unwrap_or(0).min(MAX_FACES) as usize;
read_at(12, n * 4).map(|b| b.as_chunks::<4>().0.iter().map(|c| u32::from_be_bytes(*c)).collect()).unwrap_or_default()
} else {
vec![0]
};
starts
.into_iter()
.filter_map(|start| {
let dir = read_at(start.into(), 12)?;
let tables = u16::from_be_bytes(dir.get(4..6)?.try_into().ok()?) as usize;
let records = read_at(u64::from(start) + 12, tables * 16)?;
let rec = records.as_chunks::<16>().0.iter().find(|r| r.starts_with(b"name"))?;
let (offset, len) = (be32(rec, 8)?, be32(rec, 12)?);
if len > MAX_NAME_TABLE {
return None;
}
let table = read_at(offset.into(), len as usize)?;
// A one-table font holding just the `name` table, to read it as the font itself would.
let mut font = Vec::with_capacity(28 + table.len());
font.extend_from_slice(&0x0001_0000_u32.to_be_bytes());
font.extend_from_slice(&[0, 1, 0, 16, 0, 0, 0, 0]);
font.extend_from_slice(b"name");
for v in [0, 28, len] {
font.extend_from_slice(&u32::to_be_bytes(v));
}
font.extend_from_slice(&table);
face_names(&skrifa::FontRef::new(&font).ok()?)
})
.collect()
}
/// The platform's font folders (the system's and the user's), scanned by [`FontDb::global`].
/// Empty on wasm.
pub fn system_font_dirs() -> Vec<std::path::PathBuf> {
let mut dirs: Vec<std::path::PathBuf> = Vec::new();
if cfg!(target_arch = "wasm32") {
return dirs;
}
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
if cfg!(target_os = "macos") {
dirs.extend(["/System/Library/Fonts", "/Library/Fonts"].map(Into::into));
if let Some(h) = &home {
dirs.push(h.join("Library/Fonts"));
}
} else if cfg!(windows) {
let root = std::env::var_os("WINDIR").map(std::path::PathBuf::from).unwrap_or_else(|| "C:\\Windows".into());
dirs.push(root.join("Fonts"));
if let Some(l) = std::env::var_os("LOCALAPPDATA") {
dirs.push(std::path::PathBuf::from(l).join("Microsoft\\Windows\\Fonts"));
}
} else {
dirs.extend(["/usr/share/fonts", "/usr/local/share/fonts"].map(Into::into));
if let Some(h) = &home {
dirs.push(h.join(".fonts"));
dirs.push(h.join(".local/share/fonts"));
}
}
dirs
}
/// One face found in a font file: index, family, style and (variable fonts) the named instance's
/// axis settings.
type Found = (u32, String, String, Vec<([u8; 4], f32)>);
@@ -288,7 +382,7 @@ fn enumerate_faces(data: &[u8]) -> Vec<Found> {
let mut out = Vec::new();
for i in 0..count {
let Ok(f) = skrifa::FontRef::from_index(data, i) else { continue };
let Some(family) = name(&f, &[StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME]) else { continue };
let Some((family, style)) = face_names(&f) else { continue };
let axes = f.axes();
let mut named = Vec::new();
for ni in f.named_instances().iter() {
@@ -300,7 +394,6 @@ fn enumerate_faces(data: &[u8]) -> Vec<Found> {
named.push((i, style, coords));
}
if named.is_empty() {
let style = name(&f, &[StringId::TYPOGRAPHIC_SUBFAMILY_NAME, StringId::SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into());
out.push((i, family, style, Vec::new()));
} else {
out.extend(named.into_iter().map(|(i, s, c)| (i, family.clone(), s, c)));
@@ -400,7 +493,8 @@ fn style_italic(style: &str) -> bool {
}
impl FontDb {
fn new_bundled() -> Self {
/// A database holding the bundled fonts, whose system font scan reads `font_dirs`.
pub fn with_font_dirs(font_dirs: Vec<std::path::PathBuf>) -> Self {
let mut faces = Vec::new();
for data in BUNDLED {
for (i, family, style, coords) in enumerate_faces(data) {
@@ -414,6 +508,9 @@ impl FontDb {
outlines: Mutex::new(HashMap::new()),
#[cfg(not(target_arch = "wasm32"))]
catalog: RwLock::new(Vec::new()),
font_dirs,
#[cfg(not(target_arch = "wasm32"))]
cataloged: std::sync::OnceLock::new(),
#[cfg(not(target_arch = "wasm32"))]
sys: Mutex::new(SysFallback { enabled: true, ..Default::default() }),
}
@@ -430,21 +527,30 @@ impl FontDb {
let _ = on;
}
/// Process-wide database preloaded with the bundled fonts.
/// Process-wide database: the bundled fonts, then the installed system fonts, cataloged the
/// first time a lookup by family name needs them (or ahead of time by
/// [`FontDb::scan_in_background`]).
pub fn global() -> &'static FontDb {
static DB: std::sync::OnceLock<FontDb> = std::sync::OnceLock::new();
DB.get_or_init(FontDb::new_bundled)
DB.get_or_init(|| FontDb::with_font_dirs(system_font_dirs()))
}
fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> {
self.faces.read().unwrap_or_else(|e| e.into_inner())
}
/// The system font catalog, scanned first if it hasn't been yet.
#[cfg(not(target_arch = "wasm32"))]
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> {
self.ensure_catalog();
self.catalog.read().unwrap_or_else(|e| e.into_inner())
}
/// Family names available (loaded plus cataloged system fonts), sorted and deduplicated.
pub fn families(&self) -> Vec<String> {
let mut v: Vec<String> = self.read_faces().iter().map(|f| f.family.clone()).collect();
#[cfg(not(target_arch = "wasm32"))]
v.extend(self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|c| c.family.clone()));
v.extend(self.read_catalog().iter().map(|c| c.family.clone()));
v.sort_by_key(|a| a.to_lowercase());
v.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
v
@@ -459,7 +565,7 @@ impl FontDb {
.map(|f| (f.italic, f.weight, f.style.clone()))
.collect();
#[cfg(not(target_arch = "wasm32"))]
for c in self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter() {
for c in self.read_catalog().iter() {
if c.family.eq_ignore_ascii_case(family) && !v.iter().any(|(_, _, s)| s.eq_ignore_ascii_case(&c.style)) {
v.push((style_italic(&c.style), style_weight(&c.style), c.style.clone()));
}
@@ -486,34 +592,47 @@ impl FontDb {
added
}
/// Scan the platform's font directories and catalog the faces found (native only; not called by
/// default). Font data is loaded lazily when a cataloged family is first resolved. Returns the
/// number of faces cataloged.
/// Catalog the installed fonts unless that has been done: the first caller scans the font
/// folders, any other waits for that scan to finish.
#[cfg(not(target_arch = "wasm32"))]
fn ensure_catalog(&self) {
self.cataloged.get_or_init(|| {
self.scan_font_dirs();
});
}
/// Catalog the installed fonts on a background thread, so the first lookup by family name
/// (opening a file, the font menus) doesn't wait for the scan. A no-op once they are
/// cataloged, and on wasm.
pub fn scan_in_background(&'static self) {
#[cfg(not(target_arch = "wasm32"))]
if self.cataloged.get().is_none() {
// A failed spawn leaves the scan to the first lookup that needs it.
let _ = std::thread::Builder::new().name("font-scan".into()).spawn(move || self.ensure_catalog());
}
}
/// Scan the font folders again (fonts installed or removed since), cataloging the faces
/// found (native only). Font data is loaded when a cataloged family is first resolved.
/// Returns the number of faces cataloged.
#[cfg(not(target_arch = "wasm32"))]
pub fn load_system_fonts(&self) -> usize {
let mut dirs: Vec<std::path::PathBuf> = Vec::new();
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
if cfg!(target_os = "macos") {
dirs.extend(["/System/Library/Fonts", "/Library/Fonts"].map(Into::into));
if let Some(h) = &home {
dirs.push(h.join("Library/Fonts"));
}
} else if cfg!(windows) {
let root = std::env::var_os("WINDIR").map(std::path::PathBuf::from).unwrap_or_else(|| "C:\\Windows".into());
dirs.push(root.join("Fonts"));
if let Some(l) = std::env::var_os("LOCALAPPDATA") {
dirs.push(std::path::PathBuf::from(l).join("Microsoft\\Windows\\Fonts"));
}
} else {
dirs.extend(["/usr/share/fonts", "/usr/local/share/fonts"].map(Into::into));
if let Some(h) = &home {
dirs.push(h.join(".fonts"));
dirs.push(h.join(".local/share/fonts"));
}
}
// The first scan, or a rescan once it is done (never both at once).
let mut first = None;
self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs()));
first.unwrap_or_else(|| self.scan_font_dirs())
}
#[cfg(not(target_arch = "wasm32"))]
fn scan_font_dirs(&self) -> usize {
let mut found = Vec::new();
let mut stack = dirs;
let mut stack = self.font_dirs.clone();
// Each folder once, however links lead back to it.
let mut visited = std::collections::HashSet::new();
while let Some(d) = stack.pop() {
if !visited.insert(std::fs::canonicalize(&d).unwrap_or_else(|_| d.clone())) {
continue;
}
let Ok(rd) = std::fs::read_dir(&d) else { continue };
for e in rd.flatten() {
let p = e.path();
@@ -525,8 +644,7 @@ impl FontDb {
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
continue;
}
let Ok(data) = std::fs::read(&p) else { continue };
for (_, family, style, _) in enumerate_faces(&data) {
for (family, style) in file_face_names(&p) {
found.push(CatalogEntry { family, style, path: p.clone() });
}
}
@@ -537,11 +655,13 @@ impl FontDb {
n
}
/// Load the files of the installed `family`. Returns whether any face was added.
#[cfg(not(target_arch = "wasm32"))]
fn load_cataloged(&self, family: &str) -> bool {
let paths: Vec<std::path::PathBuf> = {
let cat = self.catalog.read().unwrap_or_else(|e| e.into_inner());
let cat = self.read_catalog();
let mut p: Vec<_> = cat.iter().filter(|c| c.family.eq_ignore_ascii_case(family)).map(|c| c.path.clone()).collect();
p.sort();
p.dedup();
p
};
@@ -555,7 +675,7 @@ impl FontDb {
}
/// Resolve a family + style to a face, falling back to the closest style of the family, then to
/// Source Sans 3 Regular.
/// Source Sans 3 Regular. Installed system fonts are found by name whatever ran before.
pub fn face(&self, family: &str, style: &str) -> Arc<FontFace> {
if let Some(f) = self.find(family, style) {
return f;
@@ -572,8 +692,19 @@ impl FontDb {
.unwrap_or_else(last_resort_face)
}
/// Is `family` available (loaded)?
/// Is `family` available (loaded, or installed on the system)?
pub fn has_family(&self, family: &str) -> bool {
if self.is_loaded(family) {
return true;
}
#[cfg(not(target_arch = "wasm32"))]
if self.read_catalog().iter().any(|c| c.family.eq_ignore_ascii_case(family)) {
return true;
}
false
}
fn is_loaded(&self, family: &str) -> bool {
self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(family))
}
@@ -676,22 +807,15 @@ impl FontDb {
return false;
}
{
let mut sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
if !sys.enabled || sys.misses.contains(&c) {
return false;
}
if !sys.scanned {
sys.scanned = true;
if self.catalog.read().unwrap_or_else(|e| e.into_inner()).is_empty() {
drop(sys);
self.load_system_fonts();
}
}
}
let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c));
let cataloged: Vec<String> = self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|e| e.family.clone()).collect();
let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect();
for fam in SYSTEM_FALLBACKS {
if !self.has_family(fam) && cataloged.iter().any(|f| f.eq_ignore_ascii_case(fam)) {
if !self.is_loaded(fam) && cataloged.iter().any(|f| f.eq_ignore_ascii_case(fam)) {
self.load_cataloged(fam);
if covered(self) {
return true;
@@ -757,3 +881,8 @@ impl OutlinePen for FlipPen {
self.0.close_path();
}
}
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
#[path = "tests_sysfonts.rs"]
mod tests_sysfonts;
+1 -1
View File
@@ -9,7 +9,7 @@
mod fontdb;
pub use fontdb::{FALLBACK_FAMILY, FaceRef, FontDb, FontFace, JAPANESE_FALLBACK, base_style, bundled};
pub use fontdb::{FALLBACK_FAMILY, FaceRef, FontDb, FontFace, JAPANESE_FALLBACK, base_style, bundled, system_font_dirs};
pub use harfrust::Feature;
use harfrust::{Direction, ShapeOptions, Tag, UnicodeBuffer};
pub use kurbo::BezPath;
+136
View File
@@ -0,0 +1,136 @@
//! Installed system fonts: found by family name by every lookup, whatever ran before it (#22).
use std::path::{Path, PathBuf};
use super::*;
const FAMILY: &str = "Sysfont Sans3";
/// A bundled Source Sans 3 file renamed [`FAMILY`] (as long as the original name).
fn renamed(file: &str) -> Vec<u8> {
let mut data = std::fs::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts").join(file)).unwrap();
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
for (from, to) in [(utf16("Source Sans 3"), utf16(FAMILY)), (b"Source Sans 3".to_vec(), FAMILY.as_bytes().to_vec())] {
let mut i = 0;
while let Some(at) = data[i..].windows(from.len()).position(|w| w == from) {
data[i + at..i + at + to.len()].copy_from_slice(&to);
i += at + to.len();
}
}
data
}
/// The fonts in `faces` as one collection (TTC) file.
fn collection(faces: &[Vec<u8>]) -> Vec<u8> {
let mut out = b"ttcf".to_vec();
out.extend_from_slice(&0x0001_0000_u32.to_be_bytes());
out.extend_from_slice(&(faces.len() as u32).to_be_bytes());
let mut base = 12 + 4 * faces.len();
for f in faces {
out.extend_from_slice(&(base as u32).to_be_bytes());
base += f.len();
}
for f in faces {
// Table offsets count from the start of the collection.
let start = out.len() as u32;
let mut f = f.clone();
let tables = u16::from_be_bytes([f[4], f[5]]) as usize;
for r in 0..tables {
let at = 12 + r * 16 + 8;
let off = u32::from_be_bytes(f[at..at + 4].try_into().unwrap()) + start;
f[at..at + 4].copy_from_slice(&off.to_be_bytes());
}
out.extend_from_slice(&f);
}
out
}
/// A font folder (with a subfolder, a damaged font and a file that isn't a font) holding
/// [`FAMILY`] Regular and Bold.
fn font_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("dc-sysfonts-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("Sub")).unwrap();
std::fs::write(dir.join("Sysfont-Regular.ttf"), renamed("SourceSans3-Regular.ttf")).unwrap();
std::fs::write(dir.join("Sub/Sysfont-Bold.TTF"), renamed("SourceSans3-Bold.ttf")).unwrap();
std::fs::write(dir.join("damaged.ttf"), b"ttcf\0\x01\0\0\xff\xff\xff\xff").unwrap();
std::fs::write(dir.join("readme.txt"), FAMILY).unwrap();
dir
}
fn has(list: &[String], family: &str) -> bool {
list.iter().any(|f| f == family)
}
#[test]
fn every_lookup_by_name_finds_installed_fonts_in_a_fresh_database() {
let dir = font_dir("fresh");
// Each lookup is the first thing a new database is asked. A folder listed twice is read once.
let db = || FontDb::with_font_dirs(vec![dir.clone(), dir.join("Sub/..")]);
let f = db().face("sysfont sans3", "Bold");
assert_eq!((f.family.as_str(), f.style.as_str()), (FAMILY, "Bold"));
assert!(db().has_family(FAMILY));
assert!(has(&db().families(), FAMILY));
assert_eq!(db().styles(FAMILY), ["Regular", "Bold"]);
assert_eq!(db().load_system_fonts(), 2);
// Missing fonts stay missing.
let db = db();
assert!(!db.has_family("No Such Font"));
assert_eq!(db.face("No Such Font", "Regular").family, FALLBACK_FAMILY);
}
#[test]
fn the_scan_reads_collections_and_loads_fonts_only_when_used() {
let dir = font_dir("collection");
std::fs::remove_file(dir.join("Sysfont-Regular.ttf")).unwrap();
std::fs::remove_file(dir.join("Sub/Sysfont-Bold.TTF")).unwrap();
std::fs::write(dir.join("Sysfont.ttc"), collection(&[renamed("SourceSans3-Regular.ttf"), renamed("SourceSans3-Bold.ttf")])).unwrap();
let db = FontDb::with_font_dirs(vec![dir.clone()]);
assert_eq!(db.styles(FAMILY), ["Regular", "Bold"]);
// Cataloged, not loaded.
assert!(!db.is_loaded(FAMILY));
let bold = db.face(FAMILY, "Bold");
assert_eq!((bold.style.as_str(), bold.index()), ("Bold", 1));
assert_eq!(db.face(FAMILY, "Regular").index(), 0, "the collection's faces load together");
}
#[test]
fn rescanning_finds_fonts_installed_since() {
let dir = font_dir("rescan");
let later = dir.join("Later");
let db = FontDb::with_font_dirs(vec![later.clone()]);
assert!(!db.has_family(FAMILY));
std::fs::create_dir_all(&later).unwrap();
std::fs::copy(dir.join("Sysfont-Regular.ttf"), later.join("Sysfont-Regular.ttf")).unwrap();
assert!(!db.has_family(FAMILY), "the folders are scanned once, not on every miss");
assert_eq!(db.load_system_fonts(), 1);
assert!(db.has_family(FAMILY) && has(&db.families(), FAMILY));
}
#[test]
fn a_background_scan_serves_the_first_lookup() {
let dir = font_dir("background");
let db: &'static FontDb = Box::leak(Box::new(FontDb::with_font_dirs(vec![dir])));
db.scan_in_background();
// Waits for the scan when it is still running.
assert!(has(&db.families(), FAMILY));
assert_eq!(db.load_system_fonts(), 2, "a rescan catalogs the two faces again");
}
#[cfg(unix)]
#[test]
fn a_link_back_to_a_parent_folder_ends_the_scan() {
let dir = font_dir("loop");
std::os::unix::fs::symlink(&dir, dir.join("Sub/loop")).unwrap();
let db = FontDb::with_font_dirs(vec![dir]);
assert_eq!(db.load_system_fonts(), 2);
}
#[test]
fn the_scan_reads_names_without_loading_the_font() {
let dir = font_dir("names");
assert_eq!(file_face_names(&dir.join("Sysfont-Regular.ttf")), [(FAMILY.to_string(), "Regular".to_string())]);
assert!(file_face_names(&dir.join("damaged.ttf")).is_empty());
assert!(file_face_names(&dir.join("readme.txt")).is_empty());
assert!(file_face_names(&dir.join("missing.ttf")).is_empty());
}
+23
View File
@@ -0,0 +1,23 @@
//! A new session finds the installed system fonts by family name (#22). One test in its own
//! process, so the process-wide database starts fresh, as it does when the app starts.
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use designcraft_fonts::{FontDb, system_font_dirs};
#[test]
fn a_fresh_session_finds_installed_fonts_by_name() {
let bundled = FontDb::with_font_dirs(vec![]).families();
let probe = FontDb::with_font_dirs(system_font_dirs());
probe.load_system_fonts();
// An installed family that isn't bundled and that loads.
let installed = probe.families();
let Some(family) = installed.iter().filter(|f| !bundled.contains(f)).find(|f| probe.face(f, "Regular").family == **f) else {
eprintln!("no system fonts installed: nothing to check");
return;
};
// The first lookup in the session: nothing has scanned the font folders yet.
assert!(FontDb::global().has_family(family));
assert_eq!(&FontDb::global().face(family, "Regular").family, family);
assert!(FontDb::global().families().contains(family));
}
+2
View File
@@ -367,6 +367,8 @@ pub struct DesignApp {
impl DesignApp {
pub fn new(session: Session, services: Services) -> Self {
// The font menus and the first file opened need the installed fonts: catalog them now.
designcraft_fonts::FontDb::global().scan_in_background();
DesignApp {
session,
ui: UiState::default(),