mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
Merge pull request #964 from mjohnson7cd/fix/2093-korean-ui-font
fix(ui): add Korean font fallback for UI text
This commit is contained in:
+36
-15
@@ -23,23 +23,34 @@ pub(crate) fn craft_fonts() -> impl Iterator<Item = &'static filmcraft_text::fon
|
||||
filmcraft_text::fonts::CRAFT_FONTS.iter().filter(|f| f.scripts.iter().any(|s| matches!(*s, "Hans" | "Hant")))
|
||||
}
|
||||
|
||||
/// A font collection's face index matters: Windows Chinese 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.
|
||||
// Hangul: neither the UI fonts nor the Chinese/Japanese fallbacks have it, so typed, pasted and
|
||||
// IME-composed Korean drew as missing-glyph boxes (artcraft#2093).
|
||||
const KOREAN_SAMPLE: &str = "한국어입력테스트";
|
||||
const KOREAN_PREFERRED: &[&str] = &["Malgun Gothic", "Apple SD Gothic Neo", "Noto Sans CJK KR", "Noto Sans KR", "Source Han Sans K", "NanumGothic"];
|
||||
|
||||
fn system_font() -> Option<Arc<FontData>> {
|
||||
static FONT: OnceLock<Option<Arc<FontData>>> = 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<filmcraft_text::fonts::Face>| 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<Arc<FontData>> {
|
||||
static FONT: OnceLock<Option<Arc<FontData>>> = 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<Arc<FontData>> {
|
||||
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<filmcraft_text::fonts::Face>| 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() {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user