UI: let the Chinese and Arabic language gates accept an embedded craft-fonts face (#1294)

can_show only asked the installed-font lookups for those two languages, and
both lookups hard-code None on wasm32, where there are no system fonts. The
Korean gate already falls back to a craft-fonts face (korean_font), but the
web build embeds a Jpan CJK face and an Arabic face precisely as the web's
only fonts, so a Chinese or Arabic interface could never be selected on it,
however complete the catalog; the embedded Arabic face was also never added
to the UI font families (the embedded filter only kept Jpan/Han faces).

Give both gates the embedded-face path the Korean one has:
- can_show("zh"/"ar") answers true when this build embeds a Regular face of
  Hans/Hant or Arab, in addition to an installed font.
- install_fonts picks embedded faces via embedded_ui_faces: every CJK face
  still loads in every language for CJK drawing names (#513), the face of a
  zh interface leads (a Japanese face lacks the simplified-only 图 园 测),
  and an Arabic interface appends its Arabic face.

Nothing changes for a build without CRAFT_FONTS_DIR (CRAFT_FONTS empty):
Chinese still rides on an installed CJK font and Arabic on an installed one.
This commit is contained in:
Galeno authored and GitHub committed 2026-10-19 02:31:34 -04:00
1 parent c400d6d48c
commit ca5a34be12
2 files changed
+95 -17

No files matched your search

+7 -6
View File
@@ -581,19 +581,20 @@ fn italian_locales_plurals_and_accents() {
});
}
/// Chinese shows only with an installed CJK font (none is bundled); without one the interface
/// stays English instead of drawing boxes, and choosing it says why.
/// Chinese shows only with a font for it: an installed CJK font, or one this build embedded from
/// craft-fonts (the web build has no system fonts). Without either the interface stays English
/// instead of drawing boxes, and choosing it says why.
#[test]
fn chinese_needs_an_installed_cjk_font() {
fn chinese_needs_a_cjk_font() {
assert_eq!(locale_language("zh-CN"), Some("zh"));
let mut app = CadApp::new(Session::new(), Services::default());
let r = app.run("ui.language", json!({"lang": "zh"})).unwrap();
assert_eq!(r["interfaceLanguage"], "zh");
let font = crate::theme::cjk_font();
assert_eq!(app.language(), if font.is_some() { "zh" } else { "en" });
let shown = crate::theme::can_show("zh");
assert_eq!(app.language(), if shown { "zh" } else { "en" });
assert_eq!(r["shown"], app.language());
assert!(crate::theme::can_show("uk") && crate::theme::can_show("en"));
if font.is_none() {
if !shown {
assert!(app.status.as_ref().is_some_and(|(s, _)| s.contains("No Chinese font")));
}
}
+88 -11
View File
@@ -360,13 +360,12 @@ pub fn install_fonts(ctx: &egui::Context, language: &str) {
added.push(("system-arabic".into(), std::sync::Arc::new(egui::FontData::from_owned(bytes))));
}
}
let mut embedded: Vec<_> = cadcraft_fonts::CRAFT_FONTS
.iter()
.filter(|f| f.style == "Regular" && f.scripts.iter().any(|s| matches!(*s, "Jpan" | "Hans" | "Hant")))
.collect();
// A sans-serif face reads best in the interface.
embedded.sort_by_key(|f| !f.family.contains("Gothic") && !f.family.contains("Sans"));
added.extend(embedded.first().map(|f| (format!("craft-{}", f.stem()), std::sync::Arc::new(egui::FontData::from_static(f.bytes)))));
let embedded: Vec<_> = cadcraft_fonts::CRAFT_FONTS.iter().collect();
added.extend(
embedded_ui_faces(&embedded, language)
.into_iter()
.map(|f| (format!("craft-{}", f.stem()), std::sync::Arc::new(egui::FontData::from_static(f.bytes)))),
);
// Korean: Noto Sans CJK and most CJK fonts above already cover Hangul; else a Hangul font.
if language == "ko"
&& !cjk_covers_hangul()
@@ -387,6 +386,25 @@ pub fn install_fonts(ctx: &egui::Context, language: &str) {
ctx.set_fonts(fonts);
}
/// The faces `build.rs` embedded from craft-fonts that the interface loads, in order: every CJK
/// face, because file, layout and layer names can be Japanese or Chinese whatever the interface
/// shows (#513), with the interface's own face leading. A sans-serif face reads best, and a
/// Chinese interface takes the face with its own glyphs ahead of a Japanese one — a Japanese face
/// shares most ideographs but lacks the simplified-only ones (图 园 测), which would be tofu.
/// العربية adds its Arabic face after those, egui's bundled faces carrying no Arabic glyphs.
fn embedded_ui_faces<'a>(faces: &[&'a cadcraft_fonts::CraftFont], language: &str) -> Vec<&'a cadcraft_fonts::CraftFont> {
let cjk = |f: &&cadcraft_fonts::CraftFont| f.style == "Regular" && f.scripts.iter().any(|s| matches!(*s, "Jpan" | "Hans" | "Hant"));
let mut out: Vec<_> = faces.iter().copied().filter(cjk).collect();
out.sort_by_key(|f| !f.family.contains("Gothic") && !f.family.contains("Sans"));
if language.starts_with("zh") {
out.sort_by_key(|f| !f.scripts.contains(&"Hans"));
}
if language == "ar" {
out.extend(faces.iter().copied().filter(|f| f.style == "Regular" && f.scripts.contains(&"Arab")));
}
out
}
/// Largest interface font file read or embedded face copied (CJK collections are 15–60 MB).
const MAX_UI_FONT_BYTES: u64 = 96 * 1024 * 1024;
@@ -616,17 +634,32 @@ pub fn shows_hangul(language: &str) -> bool {
cjk_covers_hangul() || (language == "ko" && korean_font().is_some())
}
/// Whether the interface can show `language` without missing-glyph boxes: Chinese needs an
/// installed CJK font, Korean a font with Hangul, العربية an installed Arabic font.
/// Whether the interface can show `language` without missing-glyph boxes: Chinese needs a font
/// with its glyphs (installed, or the CJK face this build embedded from craft-fonts), Korean a
/// font with Hangul, العربية an installed Arabic font or the embedded Arabic face.
pub fn can_show(language: &str) -> bool {
match language {
"ko" => shows_hangul("ko"),
"ar" => arabic_font().is_some(),
l if l.starts_with("zh") => cjk_font().is_some(),
"ar" => arabic_font().is_some() || embedded_script("Arab"),
l if l.starts_with("zh") => cjk_font().is_some() || embedded_script("Hans") || embedded_script("Hant"),
_ => true,
}
}
/// Whether a face this build embedded from craft-fonts carries `script` (ISO 15924). The web
/// build has no system fonts, so there the interface rides on the embedded faces alone; the
/// Korean gate already asks this way ([`korean_font`]) and Arabic and Chinese match it. Only a
/// build made with `CRAFT_FONTS_DIR` embeds any (`build.rs`), so the rest is unchanged.
fn embedded_script(script: &str) -> bool {
any_script(cadcraft_fonts::CRAFT_FONTS, script)
}
/// Whether any face of `faces` carries `script` (ISO 15924), the question [`embedded_script`]
/// asks of the table this build embedded.
fn any_script(faces: &[cadcraft_fonts::CraftFont], script: &str) -> bool {
faces.iter().any(|f| f.style == "Regular" && f.scripts.contains(&script))
}
/// egui's visuals for `theme`, from its tokens.
fn visuals(theme: Theme) -> Visuals {
let t = Tokens::of(theme);
@@ -681,10 +714,54 @@ pub fn apply(ctx: &egui::Context, theme: Theme, follow_system: bool) {
#[cfg(test)]
mod tests {
use cadcraft_fonts::CraftFont;
/// A face shaped like `build.rs` writes them, with no font file (as in `cadcraft-fonts`).
fn face(family: &'static str, scripts: &'static [&'static str]) -> CraftFont {
CraftFont { family, style: "Regular", scripts, bytes: &[] }
}
fn families<'a>(faces: &[&'a CraftFont]) -> Vec<&'a str> {
faces.iter().map(|f| f.family).collect()
}
#[test]
fn korean_font_rejects_invalid_or_glyphless_files() {
for bytes in [vec![], vec![0xff; 32], vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] {
assert!(super::korean_face(std::sync::Arc::new(bytes)).is_none());
}
}
/// The interface's own script leads the embedded faces it loads, and العربية adds its face.
#[test]
fn embedded_faces_lead_with_the_interfaces_own_script() {
let japanese = face("BIZ UDPGothic", &["Jpan", "Latn"]);
let chinese = face("Han Test Face", &["Hans", "Latn"]);
let arabic = face("Noto Sans Arabic", &["Arab"]);
let faces = [&japanese, &chinese, &arabic];
// Every interface reads CJK names, so both CJK faces load, the sans-serif face first.
assert_eq!(families(&super::embedded_ui_faces(&faces, "en")), ["BIZ UDPGothic", "Han Test Face"]);
// A Chinese interface takes the Han face first: a Japanese face lacks 图 园 测.
assert_eq!(families(&super::embedded_ui_faces(&faces, "zh")), ["Han Test Face", "BIZ UDPGothic"]);
// العربية adds the Arabic face, after the CJK ones egui's bundled faces have no glyphs for.
assert_eq!(families(&super::embedded_ui_faces(&faces, "ar")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Arabic"]);
// Another weight is a style of a face, not an interface face of its own.
let bold = CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Jpan"], bytes: &[] };
assert!(super::embedded_ui_faces(&[&bold], "en").is_empty());
}
/// What the gate asks of the table: an embedded face of the script stands in for an installed
/// font, which the web build never has. The default (no `CRAFT_FONTS_DIR`) table answers no,
/// leaving the installed-font answer the gate had before.
#[test]
fn embedded_faces_answer_for_their_scripts() {
let faces = [face("BIZ UDPGothic", &["Jpan", "Latn"]), face("Noto Sans Arabic", &["Arab"])];
assert!(super::any_script(&faces, "Jpan") && super::any_script(&faces, "Arab"));
assert!(!super::any_script(&faces, "Hans") && !super::any_script(&faces, "Hant"));
// A `Bold`-only face is not the interface's, and a script code is matched whole.
let bold = CraftFont { family: "Some Han Bold", style: "Bold", scripts: &["Hans"], bytes: &[] };
assert!(!super::any_script(&[bold], "Hans") && !super::any_script(&faces, "Ara"));
assert!(!super::embedded_script("Hans") || super::can_show("zh"));
assert!(super::can_show("en") && super::can_show("uk"));
}
}