Merge pull request #1143 from marcoarnulfo/fix/ui-simplified-chinese-font

This commit is contained in:
Brandon Thomas
2026-10-13 04:28:32 -04:00
+46 -3
View File
@@ -397,7 +397,6 @@ fn install_script_fallbacks(family_fonts: &mut FontDefinitions, language: &str)
// Kana is unambiguously Japanese; Han ideographs are shared, so the order decides which
// language's face draws them.
const KANA: (char, &str) = ('あ', "japanese-system");
const HAN: (char, &str) = ('文', "chinese-system");
// Hangul is probed last: with no Korean font installed the lookup can land on a catch-all
// face (Unifont) that also covers Arabic or Indic text but can't shape it, so the
// script-specific faces must be registered ahead of it. A face already seen is skipped.
@@ -405,7 +404,8 @@ fn install_script_fallbacks(family_fonts: &mut FontDefinitions, language: &str)
// Han text follows the interface language, not the operating system's locale.
fonts::set_cjk_locale(cjk_locale(language));
let base = fonts::resolve("Inter", "Regular").face;
let cjk = if is_chinese(language) { [HAN, KANA] } else { [KANA, HAN] };
let han = (han_probe(language), "chinese-system");
let cjk = if is_chinese(language) { [han, KANA] } else { [KANA, han] };
let mut names = Vec::new();
let mut seen = Vec::new();
for &(probe, name) in cjk.iter().chain(SCRIPT_PROBES).chain(std::iter::once(&HANGUL)) {
@@ -442,6 +442,15 @@ fn cjk_locale(language: &str) -> &str {
}
}
/// The Han character that picks the Chinese fallback face. A Simplified Chinese interface asks for
/// a simplified-only hanzi: a Japanese face covers the shared ideographs (文) but not 页, 选 or 删,
/// and the fallback takes a face already loaded first, so the Japanese face loaded for kana would
/// draw the Chinese interface with boxes (#1141).
#[cfg(not(target_arch = "wasm32"))]
fn han_probe(language: &str) -> char {
if language == "zh-hans" { '页' } else { '文' }
}
/// Whether `language` is one of the Chinese catalogs.
#[cfg(not(target_arch = "wasm32"))]
fn is_chinese(language: &str) -> bool {
@@ -586,6 +595,34 @@ mod script_font_tests {
}
}
/// #1141: switching the interface to Simplified Chinese drew most labels as boxes. The English
/// interface had already loaded a Japanese face for kana, and that face covers the Han probe,
/// so it became the Chinese fallback too, without the simplified-only hanzi.
#[test]
fn simplified_chinese_interface_draws_simplified_hanzi_after_another_language() {
let ctx = egui::Context::default();
let tokens = super::Tokens::for_kind(super::ThemeKind::Dark);
super::install(&ctx, &tokens, "en");
super::install(&ctx, &tokens, "zh-hans");
let mut out = ctx.run_ui(egui::RawInput::default(), |_| {});
out.textures_delta.clear();
let font = egui::FontId::new(13.0, egui::FontFamily::Proportional);
let missing: String = ctx.fonts_mut(|fonts| {
let replacement = fonts.layout_no_wrap("\u{10ffff}".into(), font.clone(), egui::Color32::WHITE).rows[0].glyphs[0].uv_rect;
// Simplified-only hanzi of the zh-hans catalog (选择, 删除, 图层, 设置, 导入, 页面).
"选删层设导页"
.chars()
.filter(|ch| {
let uv = fonts.layout_no_wrap(ch.to_string(), font.clone(), egui::Color32::WHITE).rows[0].glyphs[0].uv_rect;
(uv.min, uv.max) == (replacement.min, replacement.max)
})
.collect()
});
// Asked after the layout: the coverage lookup itself loads a face with these hanzi.
let drawable: String = missing.chars().filter(|&ch| installed(ch)).collect();
assert!(drawable.is_empty(), "an installed face covers {drawable}, but the Simplified Chinese interface draws boxes");
}
/// egui shapes each run of one face: Arabic letters take their joined forms, and an Arabic
/// word reads right to left (its first letter drawn rightmost).
#[test]
@@ -666,8 +703,14 @@ mod tests {
ctx.run_ui(Default::default(), |_| {}).textures_delta.clear();
for family in [FontFamily::Proportional, FontFamily::Monospace, FontFamily::Name("medium".into()), FontFamily::Name("semibold".into())] {
let id = FontId::new(12.0, family);
// Not has_glyph: it compares face keys, so Inter's `•` reads as missing when no face in
// the stack has egui's `◻` and Inter's `?` becomes the replacement, as happens once
// another test has loaded a system face without `◻` (the fallback tries loaded faces first).
let uv = |s: String| ctx.fonts_mut(|f| f.layout_no_wrap(s, id.clone(), egui::Color32::WHITE).rows[0].glyphs[0].uv_rect);
let missing = uv("\u{10ffff}".into());
for c in "▸•›·▶◀●○⚠…×".chars() {
assert!(ctx.fonts_mut(|f| f.has_glyph(&id, c)), "{c} has no glyph in {id:?}");
let drawn = uv(c.to_string());
assert_ne!((drawn.min, drawn.max), (missing.min, missing.max), "{c} has no glyph in {id:?}");
}
}
}