const CJK_CANDIDATES: &[&str] = &[
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/simkai.ttf",
"C:/Windows/Fonts/simfang.ttf",
"C:/Windows/Fonts/simsunb.ttf",
"C:/Windows/Fonts/NotoSansSC-VF.ttf",
"/usr/share/fonts/truetype/unifont/unifont.ttf",
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
];
pub const CJK_FONT_NAME: &str = "ek-cjk";
pub fn apply_cjk_fonts(ctx: &egui::Context) -> Option<String> {
let mut fonts = egui::FontDefinitions::default();
for path in CJK_CANDIDATES {
let lower = path.to_ascii_lowercase();
if !(lower.ends_with(".ttf") || lower.ends_with(".otf")) {
continue;
}
let Ok(bytes) = std::fs::read(path) else {
continue;
};
fonts.font_data.insert(
CJK_FONT_NAME.to_owned(),
std::sync::Arc::new(egui::FontData::from_owned(bytes)),
);
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
fonts
.families
.entry(family)
.or_default()
.push(CJK_FONT_NAME.to_owned());
}
ctx.set_fonts(fonts);
eprintln!("[ek-ui] CJK 回退字体已装载: {path}");
return Some(path.to_string());
}
eprintln!("[ek-ui] 未找到系统 CJK 字体候选,中文可能显示为豆腐块(环境侧)");
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn f0_apply_cjk_fonts_registers_fallback() {
let hit_cell = std::cell::RefCell::new(None);
let recorder = &hit_cell;
let mut harness = egui_kittest::Harness::new(move |ctx: &egui::Context| {
let mut slot = recorder.borrow_mut();
if slot.is_none() {
*slot = apply_cjk_fonts(ctx);
}
drop(slot);
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("观测台 插件 行情 委托");
});
});
harness.run();
let hit = hit_cell.borrow();
if let Some(path) = &*hit {
assert!(!path.is_empty());
let probe = "观测台插件状态版本依赖行情委托成交持仓资金冻结订阅授权密码回灌撤单";
let font_id = egui::FontId::proportional(16.0);
let mut missing: Vec<char> = Vec::new();
harness.ctx.fonts_mut(|f| {
for ch in probe.chars() {
if !f.has_glyph(&font_id, ch) {
missing.push(ch);
}
}
});
assert!(
missing.is_empty(),
"回退字体缺字形: {missing:?} (font: {path})"
);
}
}
}