Fix #130: installed fonts are found by name whatever ran before
Opening, placing, pasting or importing a file, the font menus and MCP now find the installed system fonts (Arial, Times New Roman...) in a fresh session, so a file renders the same whatever happened earlier, and PDF import keeps the real family names (Microsoft YaHei, not "Microsoft Ya Hei"). Root cause: FontDb only consulted the system font catalog, which stayed empty until a CJK fallback or Character panel > Show System Fonts happened to scan the font folders. Every lookup by family name (face, families, styles, has_family) now waits for one cataloging scan per session: the app starts it in the background, other processes run it on first use. The scan reads only each file's table directory and name table (about 70 ms for 780 faces here, instead of reading every font file whole), the catalog is indexed by family, the family list is cached, and an installed style of a loaded family (a bundled family also installed with more weights) loads when asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
285d94a3e6
commit
49e8413ebe
|
|
@ -24,6 +24,8 @@ mod text;
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_fontnames;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
//! Type names the available family as the font spells it, not as the PostScript name splits into
|
||||
//! words (#130).
|
||||
|
||||
use vectorcraft_doc::NodeKind;
|
||||
use vectorcraft_text::FontDb;
|
||||
|
||||
use crate::import::import;
|
||||
|
||||
/// The bundled Source Sans 3 renamed `family` (as long as the original name).
|
||||
fn renamed(family: &str) -> Vec<u8> {
|
||||
let mut data = include_bytes!("../../../../assets/fonts/SourceSans3-Regular.ttf").to_vec();
|
||||
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())] {
|
||||
assert_eq!(from.len(), to.len());
|
||||
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
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postscript_names_resolve_to_the_available_family() {
|
||||
// Split into words, its PostScript name would read "My Face Ya Hei3".
|
||||
assert!(FontDb::global().add_font(renamed("MyFaceYaHei 3")) > 0);
|
||||
let body = "/MyFaceYaHei3-Bold findfont 12 scalefont setfont 10 20 moveto (Hi) show";
|
||||
let eps = format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n{body}\nshowpage\n%%EOF\n");
|
||||
let r = import(eps.as_bytes()).unwrap();
|
||||
let mut styles = vec![];
|
||||
r.document.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
styles.extend(t.runs.iter().map(|r| (r.style.font_family.clone(), r.style.font_style.clone())));
|
||||
}
|
||||
});
|
||||
assert_eq!(styles, [("MyFaceYaHei 3".to_string(), "Bold".to_string())]);
|
||||
}
|
||||
|
|
@ -223,7 +223,9 @@ impl Interp<'_> {
|
|||
if !(size.is_finite() && size > 1e-3 && size < 1e5) {
|
||||
return ps_err("undefinedresult", "a font size");
|
||||
}
|
||||
let (font_family, font_style) = family_style(&name);
|
||||
let (family, font_style) = family_style(&name);
|
||||
// The available family's own spelling: `MicrosoftYaHei` is Microsoft YaHei, not "Microsoft Ya Hei".
|
||||
let font_family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family);
|
||||
let text: String = s.iter().map(|b| char::from(*b)).filter(|c| !c.is_control()).collect();
|
||||
let fill = if self.g.paint.is_none() { Paint::solid(vectorcraft_color::Color::BLACK) } else { self.g.paint.clone() };
|
||||
let style = CharStyle { font_family, font_style, size, fill, stroke: Paint::None, ..CharStyle::default() };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
//! PDF import names type by the installed fonts in a new session (#130). One test in its own
|
||||
//! process, so the process-wide font database starts fresh, as it does when the app starts.
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use vectorcraft_doc::NodeKind;
|
||||
use vectorcraft_pdf::{ImportOptions, import_with_report};
|
||||
use vectorcraft_testkit::pdf::{PdfPage, pdf_with};
|
||||
use vectorcraft_text::{FontDb, system_font_dirs};
|
||||
|
||||
#[test]
|
||||
fn a_fresh_session_resolves_pdf_fonts_to_installed_families() {
|
||||
let bundled = FontDb::with_font_dirs(vec![]).families();
|
||||
let probe = FontDb::with_font_dirs(system_font_dirs());
|
||||
probe.load_system_fonts();
|
||||
let installed = probe.families();
|
||||
// A family whose PDF name is its name without the spaces (as "TimesNewRoman").
|
||||
let Some(family) = installed.iter().find(|f| !bundled.contains(f) && f.contains(' ') && f.chars().all(|c| c.is_ascii_alphanumeric() || c == ' '))
|
||||
else {
|
||||
eprintln!("no system fonts installed: nothing to check");
|
||||
return;
|
||||
};
|
||||
let resources = format!("/Font << /F1 << /Type /Font /Subtype /TrueType /BaseFont /{} >> >>", family.replace(' ', ""));
|
||||
let page = PdfPage { resources, ..PdfPage::new(100.0, 100.0, "BT /F1 12 Tf 10 30 Td (Hi) Tj ET") };
|
||||
let r = import_with_report(&pdf_with(&[page], &[], None), &ImportOptions::default()).unwrap();
|
||||
let mut families = vec![];
|
||||
r.document.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
families.extend(t.runs.iter().map(|r| r.style.font_family.clone()));
|
||||
}
|
||||
});
|
||||
assert_eq!(families, std::slice::from_ref(family));
|
||||
assert!(!r.warnings.iter().any(|w| w.contains(family.as_str())), "{:?}", r.warnings);
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
//! 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 once, loaded
|
||||
//! on demand), outline cache.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use kurbo::BezPath;
|
||||
|
|
@ -139,21 +141,34 @@ impl FontFace {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone, Debug)]
|
||||
struct CatalogEntry {
|
||||
family: String,
|
||||
style: String,
|
||||
path: std::path::PathBuf,
|
||||
/// One installed family found by the system font scan: its name and the file of each style.
|
||||
#[derive(Debug, Default)]
|
||||
struct CatalogFamily {
|
||||
name: String,
|
||||
/// (style, file) of each face.
|
||||
faces: Vec<(String, PathBuf)>,
|
||||
}
|
||||
|
||||
/// The installed fonts by ASCII-lowercased family name (lookups ignore ASCII case, as
|
||||
/// [`FontDb::face`] does). Always empty on wasm, which has no system fonts.
|
||||
type Catalog = HashMap<String, CatalogFamily>;
|
||||
|
||||
/// Process-wide font database.
|
||||
pub struct FontDb {
|
||||
faces: RwLock<Vec<Arc<FontFace>>>,
|
||||
outlines: Mutex<HashMap<(u32, u32), Arc<BezPath>>>,
|
||||
catalog: RwLock<Catalog>,
|
||||
/// 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<PathBuf>,
|
||||
/// Set once the font folders have been scanned. Lookups by family name wait for the first
|
||||
/// scan, so what they find never depends on what ran before them.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
catalog: RwLock<Vec<CatalogEntry>>,
|
||||
/// System fallback state: scanned yet, and characters no system font covers.
|
||||
cataloged: std::sync::OnceLock<()>,
|
||||
/// [`FontDb::family_list`], built on demand and dropped when fonts are added or rescanned.
|
||||
family_cache: Mutex<Option<Arc<[String]>>>,
|
||||
generation: AtomicU64,
|
||||
/// System fallback state: characters no system font covers.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
sys: Mutex<SysFallback>,
|
||||
}
|
||||
|
|
@ -162,7 +177,6 @@ pub struct FontDb {
|
|||
#[derive(Default)]
|
||||
struct SysFallback {
|
||||
enabled: bool,
|
||||
scanned: bool,
|
||||
misses: std::collections::HashSet<char>,
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +217,13 @@ 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, style) names.
|
||||
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))
|
||||
}
|
||||
|
||||
/// Parse every face in `data` (a font file or collection). Returns `(index, family, style)`.
|
||||
fn enumerate_faces(data: &[u8]) -> Vec<(u32, String, String)> {
|
||||
let count = match FileRef::new(data) {
|
||||
|
|
@ -212,14 +233,90 @@ fn enumerate_faces(data: &[u8]) -> Vec<(u32, String, String)> {
|
|||
};
|
||||
(0..count)
|
||||
.filter_map(|i| {
|
||||
let f = skrifa::FontRef::from_index(data, i).ok()?;
|
||||
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());
|
||||
let (family, style) = face_names(&skrifa::FontRef::from_index(data, i).ok()?)?;
|
||||
Some((i, family, style))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// (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.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn file_face_names(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.chunks_exact(4).filter_map(|c| be32(c, 0)).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.chunks_exact(16).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`].
|
||||
pub fn system_font_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs: Vec<PathBuf> = Vec::new();
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
return dirs;
|
||||
}
|
||||
let home = std::env::var_os("HOME").map(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(PathBuf::from).unwrap_or_else(|| "C:\\Windows".into());
|
||||
dirs.push(root.join("Fonts"));
|
||||
if let Some(l) = std::env::var_os("LOCALAPPDATA") {
|
||||
dirs.push(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
|
||||
}
|
||||
|
||||
fn make_face(bytes: FontBytes, index: u32, family: String, style: String, path: Option<std::path::PathBuf>) -> Option<FontFace> {
|
||||
let data: &[u8] = match &bytes {
|
||||
FontBytes::Static(b) => b,
|
||||
|
|
@ -247,8 +344,13 @@ fn make_face(bytes: FontBytes, index: u32, family: String, style: String, path:
|
|||
})
|
||||
}
|
||||
|
||||
/// `s` lowercased, without anything but letters and digits.
|
||||
fn norm_chars(s: &str) -> impl Iterator<Item = char> + '_ {
|
||||
s.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase)
|
||||
}
|
||||
|
||||
fn norm(s: &str) -> String {
|
||||
s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect()
|
||||
norm_chars(s).collect()
|
||||
}
|
||||
|
||||
/// Weight implied by a style name (400 = regular).
|
||||
|
|
@ -278,7 +380,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<PathBuf>) -> Self {
|
||||
let mut faces = Vec::new();
|
||||
for data in BUNDLED {
|
||||
for (i, family, style) in enumerate_faces(data) {
|
||||
|
|
@ -290,8 +393,12 @@ impl FontDb {
|
|||
Self {
|
||||
faces: RwLock::new(faces),
|
||||
outlines: Mutex::new(HashMap::new()),
|
||||
catalog: RwLock::new(Catalog::new()),
|
||||
font_dirs,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
catalog: RwLock::new(Vec::new()),
|
||||
cataloged: std::sync::OnceLock::new(),
|
||||
family_cache: Mutex::new(None),
|
||||
generation: AtomicU64::new(0),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
sys: Mutex::new(SysFallback { enabled: true, ..Default::default() }),
|
||||
}
|
||||
|
|
@ -308,34 +415,65 @@ 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())
|
||||
}
|
||||
|
||||
/// 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.sort_by_key(|a| a.to_lowercase());
|
||||
v.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
|
||||
v
|
||||
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Catalog> {
|
||||
self.ensure_catalog();
|
||||
self.catalog.read().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Style names available for `family` (Regular first, then by weight).
|
||||
/// Fonts were added or the catalog changed: cached family lists are stale.
|
||||
fn changed(&self) {
|
||||
*self.family_cache.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
self.generation.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// A number that changes whenever the available fonts do (fonts added, system fonts
|
||||
/// rescanned): lists built from [`FontDb::family_list`] are current while it doesn't.
|
||||
pub fn generation(&self) -> u64 {
|
||||
self.generation.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Family names available (loaded plus installed system fonts), sorted and deduplicated.
|
||||
pub fn families(&self) -> Vec<String> {
|
||||
self.family_list().to_vec()
|
||||
}
|
||||
|
||||
/// [`FontDb::families`], shared: cheap to call every frame.
|
||||
pub fn family_list(&self) -> Arc<[String]> {
|
||||
let catalog = self.read_catalog();
|
||||
let mut cache = self.family_cache.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(list) = cache.as_ref() {
|
||||
return list.clone();
|
||||
}
|
||||
let mut v: Vec<String> = self.read_faces().iter().map(|f| f.family.clone()).collect();
|
||||
v.extend(catalog.values().map(|c| c.name.clone()));
|
||||
v.sort_by_key(|a| a.to_lowercase());
|
||||
v.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
|
||||
let list: Arc<[String]> = v.into();
|
||||
*cache = Some(list.clone());
|
||||
list
|
||||
}
|
||||
|
||||
/// Style names available for `family`: upright styles by weight, then italics.
|
||||
pub fn styles(&self, family: &str) -> Vec<String> {
|
||||
let mut v: Vec<(bool, f32, String)> =
|
||||
self.read_faces().iter().filter(|f| f.family.eq_ignore_ascii_case(family)).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() {
|
||||
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()));
|
||||
if let Some(c) = self.read_catalog().get(&family.to_ascii_lowercase()) {
|
||||
for (style, _) in &c.faces {
|
||||
if !v.iter().any(|(_, _, s)| s.eq_ignore_ascii_case(style)) {
|
||||
v.push((style_italic(style), style_weight(style), style.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
v.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)).then(a.2.cmp(&b.2)));
|
||||
|
|
@ -350,49 +488,66 @@ impl FontDb {
|
|||
}
|
||||
|
||||
/// [`FontDb::add_font`] for the bytes of the file at `path` (when known).
|
||||
fn add_font_from(&self, bytes: Vec<u8>, path: Option<&std::path::Path>) -> usize {
|
||||
fn add_font_from(&self, bytes: Vec<u8>, path: Option<&Path>) -> usize {
|
||||
let data = Arc::new(bytes);
|
||||
let mut added = 0;
|
||||
for (i, family, style) in enumerate_faces(&data) {
|
||||
if self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(&family) && f.style.eq_ignore_ascii_case(&style)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(f) = make_face(FontBytes::Owned(data.clone()), i, family, style, path.map(std::path::Path::to_path_buf)) {
|
||||
if let Some(f) = make_face(FontBytes::Owned(data.clone()), i, family, style, path.map(Path::to_path_buf)) {
|
||||
self.faces.write().unwrap_or_else(|e| e.into_inner()).push(Arc::new(f));
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
if added > 0 {
|
||||
self.changed();
|
||||
}
|
||||
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. A no-op on wasm.
|
||||
fn ensure_catalog(&self) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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"));
|
||||
}
|
||||
}
|
||||
let mut found = Vec::new();
|
||||
let mut stack = dirs;
|
||||
// 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 catalog = Catalog::new();
|
||||
let mut n = 0;
|
||||
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();
|
||||
|
|
@ -404,26 +559,29 @@ 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) {
|
||||
found.push(CatalogEntry { family, style, path: p.clone() });
|
||||
for (family, style) in file_face_names(&p) {
|
||||
let entry = catalog.entry(family.to_ascii_lowercase()).or_default();
|
||||
if entry.name.is_empty() {
|
||||
entry.name = family;
|
||||
}
|
||||
entry.faces.push((style, p.clone()));
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let n = found.len();
|
||||
log::debug!("cataloged {n} system font faces");
|
||||
*self.catalog.write().unwrap_or_else(|e| e.into_inner()) = found;
|
||||
*self.catalog.write().unwrap_or_else(|e| e.into_inner()) = catalog;
|
||||
self.changed();
|
||||
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 mut p: Vec<_> = cat.iter().filter(|c| c.family.eq_ignore_ascii_case(family)).map(|c| c.path.clone()).collect();
|
||||
p.dedup();
|
||||
p
|
||||
};
|
||||
let mut paths: Vec<PathBuf> =
|
||||
self.read_catalog().get(&family.to_ascii_lowercase()).map(|c| c.faces.iter().map(|(_, p)| p.clone()).collect()).unwrap_or_default();
|
||||
paths.sort();
|
||||
paths.dedup();
|
||||
let mut any = false;
|
||||
for p in paths {
|
||||
if let Ok(data) = std::fs::read(&p) {
|
||||
|
|
@ -434,19 +592,33 @@ impl FontDb {
|
|||
}
|
||||
|
||||
/// Resolve a family + style to a face, falling back to the closest style of the family, then to
|
||||
/// Source Sans 3 Regular. `None` only if no font at all is loaded (the bundled fonts failed to
|
||||
/// parse), in which case text has no glyphs.
|
||||
/// Source Sans 3 Regular. Installed system fonts are found by name whatever ran before. `None`
|
||||
/// only if no font at all is loaded (the bundled fonts failed to parse), in which case text has
|
||||
/// no glyphs.
|
||||
pub fn face(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
if let Some(f) = self.find(family, style) {
|
||||
return Some(f);
|
||||
let found = self.find(family, style);
|
||||
if found.as_ref().is_some_and(|f| norm(&f.style) == norm(style)) {
|
||||
return found;
|
||||
}
|
||||
// The family, or this style of it, is installed but not loaded yet.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.load_cataloged(family)
|
||||
if (found.is_none() || self.is_cataloged(family, style))
|
||||
&& self.load_cataloged(family)
|
||||
&& let Some(f) = self.find(family, style)
|
||||
{
|
||||
return Some(f);
|
||||
}
|
||||
self.find(FALLBACK_FAMILY, style).or_else(|| self.find(FALLBACK_FAMILY, "Regular")).or_else(|| self.read_faces().first().cloned())
|
||||
found
|
||||
.or_else(|| self.find(FALLBACK_FAMILY, style))
|
||||
.or_else(|| self.find(FALLBACK_FAMILY, "Regular"))
|
||||
.or_else(|| self.read_faces().first().cloned())
|
||||
}
|
||||
|
||||
/// Is `style` of `family` among the installed fonts?
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn is_cataloged(&self, family: &str, style: &str) -> bool {
|
||||
let ns = norm(style);
|
||||
self.read_catalog().get(&family.to_ascii_lowercase()).is_some_and(|c| c.faces.iter().any(|(s, _)| norm(s) == ns))
|
||||
}
|
||||
|
||||
/// The loaded face with [`FontFace::id`] `id` (the face a laid-out glyph came from).
|
||||
|
|
@ -454,12 +626,22 @@ impl FontDb {
|
|||
self.read_faces().iter().find(|f| f.id == id).cloned()
|
||||
}
|
||||
|
||||
/// Is `family` available (loaded)?
|
||||
/// The available family `name` names, ignoring case and anything but letters and digits, as
|
||||
/// PostScript names write families ("MicrosoftYaHei" is Microsoft YaHei).
|
||||
pub fn find_family(&self, name: &str) -> Option<String> {
|
||||
self.family_list().iter().find(|f| norm_chars(f).eq(norm_chars(name))).cloned()
|
||||
}
|
||||
|
||||
/// Is `family` available (loaded, or installed on the system)?
|
||||
pub fn has_family(&self, family: &str) -> bool {
|
||||
self.is_loaded(family) || self.read_catalog().contains_key(&family.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
pub(crate) fn is_loaded(&self, family: &str) -> bool {
|
||||
self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(family))
|
||||
}
|
||||
|
||||
fn find(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
pub(crate) fn find(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
let faces = self.read_faces();
|
||||
let cands: Vec<&Arc<FontFace>> = faces.iter().filter(|f| f.family.eq_ignore_ascii_case(family)).collect();
|
||||
if cands.is_empty() {
|
||||
|
|
@ -481,7 +663,7 @@ impl FontDb {
|
|||
}
|
||||
|
||||
/// First face (fallback family first, then load order) that covers `c`; on native, system
|
||||
/// fonts are cataloged and loaded lazily the first time no loaded face covers a character.
|
||||
/// fonts are loaded lazily the first time no loaded face covers a character.
|
||||
pub(crate) fn fallback_for(&self, c: char, exclude: u32) -> Option<Arc<FontFace>> {
|
||||
if let Some(f) = self.loaded_fallback(c, exclude) {
|
||||
return Some(f);
|
||||
|
|
@ -508,29 +690,18 @@ 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();
|
||||
for fam in SYSTEM_FALLBACKS {
|
||||
if !self.has_family(fam) && cataloged.iter().any(|f| f.eq_ignore_ascii_case(fam)) {
|
||||
self.load_cataloged(fam);
|
||||
if covered(self) {
|
||||
return true;
|
||||
}
|
||||
if !self.is_loaded(fam) && self.load_cataloged(fam) && covered(self) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let mut paths: Vec<std::path::PathBuf> = self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|e| e.path.clone()).collect();
|
||||
let mut paths: Vec<PathBuf> = self.read_catalog().values().flat_map(|c| c.faces.iter().map(|(_, p)| p.clone())).collect();
|
||||
paths.sort();
|
||||
paths.dedup();
|
||||
for p in paths {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ mod shape;
|
|||
pub mod thread;
|
||||
|
||||
pub use features::OtFeatures;
|
||||
pub use fontdb::{FALLBACK_FAMILY, FontDb, FontFace, style_weight};
|
||||
pub use fontdb::{FALLBACK_FAMILY, FontDb, FontFace, style_weight, system_font_dirs};
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
pub use layout::{layout, layout_with};
|
||||
pub use vectorcraft_doc::TextObject;
|
||||
|
|
@ -366,4 +366,6 @@ mod tests_scripts;
|
|||
#[cfg(test)]
|
||||
mod tests_snap;
|
||||
#[cfg(test)]
|
||||
mod tests_sysfonts;
|
||||
#[cfg(test)]
|
||||
mod tests_typo;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ use kurbo::{Affine, Shape};
|
|||
use vectorcraft_doc::{CharStyle, Justify, TextKind, TextRun};
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
/// The bundled fonts only: what these tests measure doesn't change with the fonts installed.
|
||||
fn db() -> &'static FontDb {
|
||||
FontDb::global()
|
||||
static DB: std::sync::OnceLock<FontDb> = std::sync::OnceLock::new();
|
||||
DB.get_or_init(|| FontDb::with_font_dirs(vec![]))
|
||||
}
|
||||
|
||||
fn style(size: f64) -> CharStyle {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
//! Installed system fonts: found by family name by every lookup, whatever ran before it (#130).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
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!("vc-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").unwrap();
|
||||
assert_eq!((f.family.as_str(), f.style.as_str()), (FAMILY, "Bold"));
|
||||
assert_eq!(f.path().map(std::fs::canonicalize).unwrap().unwrap(), std::fs::canonicalize(dir.join("Sub/Sysfont-Bold.TTF")).unwrap());
|
||||
assert!(db().has_family(FAMILY));
|
||||
assert!(has(&db().families(), FAMILY));
|
||||
assert_eq!(db().styles(FAMILY), ["Regular", "Bold"]);
|
||||
assert_eq!(db().load_system_fonts(), 2);
|
||||
assert_eq!(db().find_family("SysfontSans3").as_deref(), Some(FAMILY), "a PostScript-style name");
|
||||
// Missing fonts stay missing.
|
||||
let db = db();
|
||||
assert!(!db.has_family("No Such Font"));
|
||||
assert_eq!(db.face("No Such Font", "Regular").unwrap().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").unwrap();
|
||||
assert_eq!((bold.style.as_str(), bold.face_index()), ("Bold", 1));
|
||||
assert_eq!(db.find(FAMILY, "Regular").unwrap().face_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));
|
||||
let (generation, families) = (db.generation(), db.family_list());
|
||||
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.family_list(), FAMILY) && !has(&families, FAMILY));
|
||||
assert_ne!(db.generation(), generation);
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_family_list_is_shared_until_fonts_change() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let a = db.family_list();
|
||||
assert!(Arc::ptr_eq(&a, &db.family_list()));
|
||||
assert!(db.add_font(renamed("SourceSans3-Regular.ttf")) > 0);
|
||||
let b = db.family_list();
|
||||
assert!(!Arc::ptr_eq(&a, &b) && has(&b, FAMILY) && !has(&a, FAMILY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_styles_of_a_loaded_family_load_when_asked_for() {
|
||||
let dir = font_dir("styles");
|
||||
let db = FontDb::with_font_dirs(vec![dir]);
|
||||
// The family is loaded with only its Regular face (as when a bundled family is also installed).
|
||||
assert_eq!(db.add_font(renamed("SourceSans3-Regular.ttf")), 1);
|
||||
assert_eq!(db.face(FAMILY, "Bold").unwrap().style, "Bold");
|
||||
// A style nobody has still gets the closest one.
|
||||
assert_eq!(db.face(FAMILY, "Black").unwrap().style, "Bold");
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//! A new session finds the installed system fonts by family name (#130). 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 (AGENTS.md › Robustness).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use vectorcraft_text::{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();
|
||||
let installed = probe.families();
|
||||
let Some(family) = installed.iter().find(|f| !bundled.contains(f)) else {
|
||||
eprintln!("no system fonts installed: nothing to check");
|
||||
return;
|
||||
};
|
||||
// The first lookup in the session: nothing has scanned the font folders yet.
|
||||
let face = FontDb::global().face(family, "Regular").unwrap();
|
||||
assert_eq!(&face.family, family);
|
||||
assert!(FontDb::global().families().contains(family));
|
||||
}
|
||||
|
|
@ -296,6 +296,8 @@ const SYSTEM_CLIPBOARD_POLL: f64 = 0.25;
|
|||
|
||||
impl VectorcraftApp {
|
||||
pub fn new(mut session: Session, services: Services) -> Self {
|
||||
// The font menus and the first file opened need the installed fonts: catalog them now.
|
||||
vectorcraft_text::FontDb::global().scan_in_background();
|
||||
if let Some(store) = &services.recovery_store {
|
||||
session.recovery.set_store(store.clone());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let fnames: Vec<&str> = fams.iter().map(String::as_str).collect();
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(i) = widgets::dropdown(ui, "gl-family", &family, &fnames, (w * 0.6).max(80.0)) {
|
||||
let st = db.styles(fnames[i]).into_iter().next().unwrap_or_else(|| "Regular".into());
|
||||
let st = db.face(fnames[i], "Regular").map_or_else(|| "Regular".into(), |f| f.style.clone());
|
||||
set_pstate(ui.ctx(), "gl-font", Some((fnames[i].to_string(), st)));
|
||||
}
|
||||
let styles = db.styles(&family);
|
||||
|
|
|
|||
Loading…
Reference in New Issue