use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Arc; use eframe::egui::{FontData, FontDefinitions, FontFamily}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CjkFontCandidate { pub path: PathBuf, pub index: u32, } pub fn install_cjk_font(ctx: &eframe::egui::Context) -> Option { let candidate = find_cjk_font_candidate()?; let bytes = std::fs::read(&candidate.path).ok()?; let mut font_data = FontData::from_owned(bytes); font_data.index = candidate.index; let mut definitions = FontDefinitions::default(); definitions .font_data .insert("genarrative-cjk".to_owned(), Arc::new(font_data)); // 中文注释:作为 fallback 注入,保留 egui 默认拉丁/图标字体,同时补齐中文 glyph。 for family in [FontFamily::Proportional, FontFamily::Monospace] { definitions .families .entry(family) .or_default() .push("genarrative-cjk".to_owned()); } ctx.set_fonts(definitions); Some(candidate) } pub fn find_cjk_font_candidate() -> Option { if let Ok(path) = std::env::var("GENARRATIVE_SERVER_PANEL_CJK_FONT") { if let Some(candidate) = parse_font_spec(&path) { return Some(candidate); } } const KNOWN_PATHS: &[(&str, u32)] = &[ ("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", 2), ("/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc", 2), ("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", 0), ("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", 0), ( "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", 0, ), ( "/home/dsk/.local/share/fonts/genarrative-cjk/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", 0, ), ]; for (path, index) in KNOWN_PATHS { if Path::new(path).is_file() { return Some(CjkFontCandidate { path: PathBuf::from(path), index: *index, }); } } for family in [ "Noto Sans CJK SC", "WenQuanYi Zen Hei", "Droid Sans Fallback", ] { if let Some(candidate) = find_with_fc_match(family) { return Some(candidate); } } None } fn parse_font_spec(raw: &str) -> Option { let trimmed = raw.trim(); if trimmed.is_empty() { return None; } let (path, index) = trimmed .rsplit_once('|') .and_then(|(path, index)| Some((path, index.parse().ok()?))) .unwrap_or((trimmed, 0)); let path = PathBuf::from(path); path.is_file().then_some(CjkFontCandidate { path, index }) } fn find_with_fc_match(family: &str) -> Option { let output = Command::new("fc-match") .arg("-f") .arg("%{file}|%{index}\n") .arg(family) .output() .ok()?; if !output.status.success() { return None; } let stdout = String::from_utf8_lossy(&output.stdout); stdout.lines().find_map(parse_font_spec) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_font_path_with_index() { let candidate = parse_font_spec("/tmp/missing-font.ttc|2"); assert_eq!(candidate, None); } #[test] fn finds_existing_system_cjk_font() { let candidate = find_cjk_font_candidate(); assert!( candidate .as_ref() .is_some_and(|candidate| candidate.path.is_file()), "expected at least one CJK font on this development host" ); } }