feat: add web CJK font loading and robust cross-platform font detection

This commit is contained in:
Mithril-3d authored and bherbruck committed 2026-10-12 10:06:50 -04:00
1 parent ab72c5ad56
commit 1fee62dd54
8 files changed
+220 -51

No files matched your search

+2 -7
View File
@@ -217,13 +217,8 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
None
};
let sign = match (to_span, &d.kind) {
(Some(span), _) => {
if span < 0.0 {
-1.0
} else {
1.0
}
}
(Some(span), _) if span < 0.0 => -1.0,
(Some(_), _) => 1.0,
(_, Kind::Extrude { direction: 1, .. }) => -1.0,
_ => 1.0,
};
+160 -26
View File
@@ -1,8 +1,20 @@
//! System font loading and fallback setup for multi-language (CJK/Japanese) support.
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
/// Install font bytes as fallback for Proportional and Monospace font families.
/// Returns true if the font was parsed and installed successfully.
pub fn install_cjk_font(ctx: &egui::Context, bytes: Vec<u8>) -> bool {
if ttf_parser::Face::parse(&bytes, 0).is_err() {
return false;
}
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert("cjk_fallback".to_owned(), Arc::new(egui::FontData::from_owned(bytes)));
fonts.families.entry(egui::FontFamily::Proportional).or_default().push("cjk_fallback".to_owned());
fonts.families.entry(egui::FontFamily::Monospace).or_default().push("cjk_fallback".to_owned());
ctx.set_fonts(fonts);
true
}
/// Candidate font paths to look for on different operating systems.
#[cfg(not(target_arch = "wasm32"))]
const FONT_CANDIDATES: &[&str] = &[
@@ -12,45 +24,167 @@ const FONT_CANDIDATES: &[&str] = &[
"C:\\Windows\\Fonts\\YuGothR.ttc",
"C:\\Windows\\Fonts\\msgothic.ttc",
"C:\\Windows\\Fonts\\BIZ-UDGothicR.ttc",
"C:\\Windows\\Fonts\\BIZ-UDMinchoM.ttc",
"C:\\Windows\\Fonts\\yumin.ttf",
// macOS
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
"/System/Library/Fonts/Hiragino Sans W3.ttc",
"/System/Library/Fonts/Hiragino Sans W4.ttc",
"/System/Library/Fonts/Hiragino Sans W6.ttc",
"/System/Library/Fonts/Supplemental/Hiragino Sans W3.ttc",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Songti.ttc",
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
"/System/Library/Fonts/AppleSDGothicNeo.ttc",
"/Library/Fonts/Arial Unicode.ttf",
"/Library/Fonts/Hiragino Sans W3.ttc",
// Linux
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJKjp-Regular.otf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
"/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf",
"/usr/share/fonts/truetype/fonts-japanese-gothic.ttf",
"/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttf",
"/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf",
"/usr/share/fonts/truetype/vlgothic/VL-PGothic-Regular.ttf",
"/usr/share/fonts/truetype/ipafont/ipag.ttf",
"/usr/share/fonts/truetype/ipaexfont/ipaexg.ttf",
"/usr/share/fonts/opentype/ipafont/ipag.ttf",
"/usr/share/fonts/opentype/ipaexfont/ipaexg.ttf",
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
"/usr/share/fonts/cjkuni-uming/uming.ttc",
];
#[cfg(not(target_arch = "wasm32"))]
fn try_read_font(path: &std::path::Path) -> Option<Vec<u8>> {
if let Ok(bytes) = std::fs::read(path)
&& ttf_parser::Face::parse(&bytes, 0).is_ok()
{
Some(bytes)
} else {
None
}
}
/// Recursively search for matching CJK font files up to a bounded depth.
#[cfg(not(target_arch = "wasm32"))]
fn scan_dir_for_font(dir: &std::path::Path, keywords: &[&str], depth_remaining: usize) -> Option<Vec<u8>> {
if depth_remaining == 0 {
return None;
}
let rd = std::fs::read_dir(dir).ok()?;
let mut subdirs = Vec::new();
for entry in rd.flatten() {
let p = entry.path();
if p.is_file() {
let name = p.file_name().map(|n| n.to_string_lossy().to_ascii_lowercase()).unwrap_or_default();
let is_font = name.ends_with(".ttc") || name.ends_with(".ttf") || name.ends_with(".otf");
if is_font
&& keywords.iter().any(|k| name.contains(k))
&& let Some(bytes) = try_read_font(&p)
{
return Some(bytes);
}
} else if p.is_dir() {
subdirs.push(p);
}
}
for sub in subdirs {
if let Some(bytes) = scan_dir_for_font(&sub, keywords, depth_remaining.saturating_sub(1)) {
return Some(bytes);
}
}
None
}
/// Find a CJK fallback font on the local system or in CRAFT_FONTS_DIR.
#[cfg(not(target_arch = "wasm32"))]
fn find_system_cjk_font() -> Option<Vec<u8>> {
// 1. Check optional craft-fonts pack directory if specified.
if let Ok(craft_dir) = std::env::var("CRAFT_FONTS_DIR") {
let dir = std::path::Path::new(&craft_dir);
let names = ["BIZUDPGothic-Regular.ttf", "BIZ-UDGothicR.ttc", "NotoSansCJK-Regular.ttc", "NotoSansJP-Regular.ttf"];
for name in names {
if let Some(bytes) = try_read_font(&dir.join(name)) {
return Some(bytes);
}
}
}
// 2. Check well-known candidate paths.
for path in FONT_CANDIDATES {
if let Some(bytes) = try_read_font(std::path::Path::new(path)) {
return Some(bytes);
}
}
// 3. Platform-specific dynamic directory search.
#[cfg(target_os = "windows")]
{
if let Ok(windir) = std::env::var("WINDIR").or_else(|_| std::env::var("SystemRoot")) {
let fonts_dir = std::path::PathBuf::from(windir).join("Fonts");
let names = ["meiryo.ttc", "YuGothM.ttc", "YuGothR.ttc", "msgothic.ttc", "BIZ-UDGothicR.ttc"];
for name in names {
if let Some(bytes) = try_read_font(&fonts_dir.join(name)) {
return Some(bytes);
}
}
}
if let Ok(local) = std::env::var("LOCALAPPDATA") {
let user_fonts = std::path::PathBuf::from(local).join("Microsoft").join("Windows").join("Fonts");
if let Some(bytes) = scan_dir_for_font(&user_fonts, &["biz", "gothic", "noto", "yugoth"], 2) {
return Some(bytes);
}
}
}
#[cfg(target_os = "macos")]
{
let dirs = ["/System/Library/Fonts", "/System/Library/Fonts/Supplemental", "/Library/Fonts"];
let keywords = ["hiragino", "pingfang", "arial unicode", "songti", "gothic"];
for d in dirs {
if let Some(bytes) = scan_dir_for_font(std::path::Path::new(d), &keywords, 2) {
return Some(bytes);
}
}
}
#[cfg(target_os = "linux")]
{
let search_dirs = [std::path::PathBuf::from("/usr/share/fonts"), std::path::PathBuf::from("/usr/local/share/fonts")];
let keywords = ["notosanscjk", "noto", "cjk", "takao", "vlgothic", "ipa", "droid"];
for dir in &search_dirs {
if let Some(bytes) = scan_dir_for_font(dir, &keywords, 3) {
return Some(bytes);
}
}
if let Ok(home) = std::env::var("HOME") {
let user_fonts = std::path::PathBuf::from(home).join(".local/share/fonts");
if let Some(bytes) = scan_dir_for_font(&user_fonts, &keywords, 2) {
return Some(bytes);
}
}
}
None
}
/// Configure font definitions with system CJK fallback if available.
#[cfg(not(target_arch = "wasm32"))]
pub fn setup_fonts(ctx: &egui::Context) {
let mut font_bytes: Option<Vec<u8>> = None;
for path in FONT_CANDIDATES {
if let Ok(bytes) = std::fs::read(path)
&& ttf_parser::Face::parse(&bytes, 0).is_ok()
{
font_bytes = Some(bytes);
break;
}
if let Some(bytes) = find_system_cjk_font() {
install_cjk_font(ctx, bytes);
}
let Some(bytes) = font_bytes else {
return;
};
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert("cjk_fallback".to_owned(), Arc::new(egui::FontData::from_owned(bytes)));
fonts.families.entry(egui::FontFamily::Proportional).or_default().push("cjk_fallback".to_owned());
fonts.families.entry(egui::FontFamily::Monospace).or_default().push("cjk_fallback".to_owned());
ctx.set_fonts(fonts);
}
/// Browsers cannot read system font files. A web host can supply licensed fonts through egui.
/// On web, setup_fonts does not perform synchronous disk reads.
/// Use asynchronous loading through [`solvecraft_web`] or host scripts.
#[cfg(target_arch = "wasm32")]
pub fn setup_fonts(_ctx: &egui::Context) {}
+1 -1
View File
@@ -400,7 +400,7 @@ pub(crate) fn tick(app: &mut SolveApp, ctx: &egui::Context) {
}
pub(crate) fn receive(app: &mut SolveApp, token: u64, image: Arc<ColorImage>) {
if !app.issue_report.pending.as_ref().is_some_and(|p| p.token == token) {
if app.issue_report.pending.as_ref().is_none_or(|p| p.token != token) {
return;
}
let stale = app.issue_report.pending.as_ref().is_some_and(|p| p.signature != signature(app, app.dialog.as_ref()));