diff --git a/crates/ui-egui/src/cjk.rs b/crates/ui-egui/src/cjk.rs index 1ebdf0102..82b6bab66 100644 --- a/crates/ui-egui/src/cjk.rs +++ b/crates/ui-egui/src/cjk.rs @@ -23,23 +23,34 @@ pub(crate) fn craft_fonts() -> impl Iterator Option> { static FONT: OnceLock>> = OnceLock::new(); - FONT.get_or_init(|| { - if cfg!(target_arch = "wasm32") { - return None; - } - filmcraft_text::fonts::scan_system(); - let faces: Vec<_> = filmcraft_text::fonts::all_faces().into_iter().filter(|f| f.info.origin == "system" && !f.info.italic).collect(); - let covers = |f: &filmcraft_text::fonts::Face| f.covers_text(SAMPLE); - let by_weight = |f: &&Arc| f.info.weight.abs_diff(400); - let preferred = PREFERRED.iter().find_map(|name| faces.iter().filter(|f| f.info.family.eq_ignore_ascii_case(name) && covers(f)).min_by_key(by_weight)); - let face = preferred.or_else(|| faces.iter().filter(|f| covers(f)).min_by_key(by_weight))?; - Some(Arc::new(FontData { font: std::borrow::Cow::Owned(face.data()?), index: face.info.index, tweak: Default::default() })) - }) - .clone() + FONT.get_or_init(|| find_system_font(SAMPLE, PREFERRED)).clone() +} + +fn korean_font() -> Option> { + static FONT: OnceLock>> = OnceLock::new(); + FONT.get_or_init(|| find_system_font(KOREAN_SAMPLE, KOREAN_PREFERRED)).clone() +} + +/// A font collection's face index matters: Windows CJK UI fonts commonly live in TTC files. +/// Keep the owned bytes behind an Arc so theme reinstalls do not reread or leak a font file. +fn find_system_font(sample: &str, preferred: &[&str]) -> Option> { + if cfg!(target_arch = "wasm32") { + return None; + } + filmcraft_text::fonts::scan_system(); + let faces: Vec<_> = filmcraft_text::fonts::all_faces().into_iter().filter(|f| f.info.origin == "system" && !f.info.italic).collect(); + let covers = |f: &filmcraft_text::fonts::Face| f.covers_text(sample); + let by_weight = |f: &&Arc| f.info.weight.abs_diff(400); + let found = preferred.iter().find_map(|name| faces.iter().filter(|f| f.info.family.eq_ignore_ascii_case(name) && covers(f)).min_by_key(by_weight)); + let face = found.or_else(|| faces.iter().filter(|f| covers(f)).min_by_key(by_weight))?; + Some(Arc::new(FontData { font: std::borrow::Cow::Owned(face.data()?), index: face.info.index, tweak: Default::default() })) } /// Whether [`install`] has a Chinese face to add: craft-fonts Hans/Hant faces, or an installed @@ -52,6 +63,16 @@ pub(crate) fn available() -> bool { /// interface's Latin typography and Japanese glyph forms, and covers the simplified and /// traditional hanzi in media and track names that Japanese fonts lack. pub(crate) fn install(fonts: &mut FontDefinitions) { + install_chinese(fonts); + if let Some(font) = korean_font() { + fonts.font_data.insert("system-korean".into(), font); + for stack in fonts.families.values_mut() { + stack.push("system-korean".into()); + } + } +} + +fn install_chinese(fonts: &mut FontDefinitions) { let chinese: Vec<_> = craft_fonts().collect(); if chinese.is_empty() { if let Some(font) = system_font() { diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 3cf300f59..21a50012a 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -633,6 +633,33 @@ mod tests { ctx.run_ui(egui::RawInput::default(), |_| {}).textures_delta.clear(); } + /// Typed, pasted or IME-composed Hangul draws real glyphs, not the missing-glyph box (artcraft#2093). + #[test] + fn korean_text_renders_with_an_installed_korean_font() { + const KOREAN: &str = "한국어 입력 테스트"; + filmcraft_text::fonts::scan_system(); + let available = filmcraft_text::fonts::all_faces().iter().any(|f| f.info.origin == "system" && KOREAN.chars().all(|c| f.has_char(c))); + if !available { + eprintln!("SKIPPED: no installed Korean font"); + return; + } + let ctx = egui::Context::default(); + install(&ctx, &Tokens::for_kind(ThemeKind::Dark)); + ctx.run_ui(egui::RawInput::default(), |_| {}).textures_delta.clear(); + ctx.fonts_mut(|fonts| { + for family in font_families() { + let font = FontId::new(13.0, family); + let missing = fonts.layout_no_wrap("\u{fffd}".into(), font.clone(), Color32::WHITE); + let missing_uv = missing.rows[0].glyphs[0].uv_rect; + let text = fonts.layout_no_wrap(KOREAN.into(), font.clone(), Color32::WHITE); + for glyph in text.rows.iter().flat_map(|r| &r.glyphs).filter(|g| g.chr != ' ') { + assert_ne!(glyph.uv_rect, missing_uv, "missing {} in {font:?}", glyph.chr); + } + } + }); + ctx.run_ui(egui::RawInput::default(), |_| {}).textures_delta.clear(); + } + /// Built with craft-fonts: every font family ends with the Japanese faces (BIZ UDPGothic /// first), and Japanese text gets real glyphs, not the replacement box. #[test]