mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-14 14:26:46 -04:00
Merge pull request #417 from Momentum96/fix/hangul-ui-fallback
Fall back to a system Hangul font so Korean names render in the UI
This commit is contained in:
2 files changed
+81
-11
No files matched your search
@@ -47,3 +47,6 @@ display_name = "Jason Brashear"
|
||||
[people.Silvertree2010]
|
||||
display_name = "Netter"
|
||||
sync_github_name = false
|
||||
|
||||
[people.Momentum96]
|
||||
sync_github_name = true
|
||||
+78
-11
@@ -25,13 +25,30 @@ const BOLD: &[(&str, u32)] = &[
|
||||
("/usr/local/share/fonts/dejavu/DejaVuSans-Bold.ttf", 0),
|
||||
("/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", 0),
|
||||
];
|
||||
/// Hangul fallback, appended after every family so Korean session, track and clip names draw
|
||||
/// whatever the interface language. The Noto Sans CJK collection's face 1 is its Korean face.
|
||||
const HANGUL: &[(&str, u32)] = &[
|
||||
("/System/Library/Fonts/AppleSDGothicNeo.ttc", 0),
|
||||
("/System/Library/Fonts/Supplemental/AppleGothic.ttf", 0),
|
||||
("C:\\Windows\\Fonts\\malgun.ttf", 0),
|
||||
("C:\\Windows\\Fonts\\gulim.ttc", 0),
|
||||
("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", 1),
|
||||
("/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", 1),
|
||||
("/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc", 1),
|
||||
("/usr/share/fonts/truetype/nanum/NanumGothic.ttf", 0),
|
||||
("/usr/share/fonts/naver-nanum/NanumGothic.ttf", 0),
|
||||
];
|
||||
/// Font files larger than this are skipped: egui keeps the whole file in memory.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_FONT_BYTES: u64 = 64 << 20;
|
||||
|
||||
/// Asks fontconfig for the desktop's UI font. Only TrueType/OpenType results are used: egui cannot
|
||||
/// read Type 1 or bitmap fonts, which `fc-match` may return on minimal systems.
|
||||
/// Asks fontconfig for the desktop's font matching `pattern`. Only TrueType/OpenType results are
|
||||
/// used: egui cannot read Type 1 or bitmap fonts, which `fc-match` may return on minimal systems.
|
||||
/// With `lang`, the match must support that language (`fc-match` always returns its best match,
|
||||
/// which is a Latin face when no Korean font is installed).
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn load_from_fontconfig(bold: bool) -> Option<FontData> {
|
||||
let pattern = if bold { "sans-serif:style=Bold" } else { "sans-serif" };
|
||||
let output = std::process::Command::new("fc-match").args(["--format=%{file}\\n%{index}\\n", pattern]).output().ok()?;
|
||||
fn load_from_fontconfig(pattern: &str, lang: Option<&str>) -> Option<FontData> {
|
||||
let output = std::process::Command::new("fc-match").args(["--format=%{file}\\n%{index}\\n%{lang}\\n", pattern]).output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -40,6 +57,14 @@ fn load_from_fontconfig(bold: bool) -> Option<FontData> {
|
||||
let path = lines.next()?.trim();
|
||||
// The upper 16 bits carry a variable font's named instance; the face index is the lower 16.
|
||||
let index = lines.next()?.trim().parse::<u32>().ok()? & 0xFFFF;
|
||||
if let Some(lang) = lang
|
||||
&& !lines.next().unwrap_or_default().split('|').any(|l| l.trim() == lang)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if std::fs::metadata(path).ok()?.len() > MAX_FONT_BYTES {
|
||||
return None;
|
||||
}
|
||||
let bytes = std::fs::read(path).ok()?;
|
||||
let sfnt = matches!(bytes.get(..4), Some(b"\0\x01\0\0" | b"OTTO" | b"true" | b"ttcf"));
|
||||
if !sfnt {
|
||||
@@ -51,17 +76,20 @@ fn load_from_fontconfig(bold: bool) -> Option<FontData> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn load(cands: &[(&str, u32)], bold: bool) -> Option<FontData> {
|
||||
fn load(cands: &[(&str, u32)], pattern: &str, lang: Option<&str>) -> Option<FontData> {
|
||||
if std::env::var_os("SOUNDCRAFT_NO_SYSTEM_FONTS").is_some() {
|
||||
return None;
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
if let Some(font) = load_from_fontconfig(bold) {
|
||||
if let Some(font) = load_from_fontconfig(pattern, lang) {
|
||||
return Some(font);
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
let _ = bold;
|
||||
let _ = (pattern, lang);
|
||||
for (p, idx) in cands {
|
||||
if std::fs::metadata(p).is_ok_and(|m| m.len() > MAX_FONT_BYTES) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(bytes) = std::fs::read(p) {
|
||||
let mut fd = FontData::from_owned(bytes);
|
||||
fd.index = *idx;
|
||||
@@ -72,7 +100,7 @@ fn load(cands: &[(&str, u32)], bold: bool) -> Option<FontData> {
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn load(_: &[(&str, u32)], _: bool) -> Option<FontData> {
|
||||
fn load(_: &[(&str, u32)], _: &str, _: Option<&str>) -> Option<FontData> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -80,22 +108,61 @@ pub fn definitions() -> FontDefinitions {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
let base: Vec<String> = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default();
|
||||
let mut bold_family = base.clone();
|
||||
if let Some(reg) = load(REGULAR, false) {
|
||||
if let Some(reg) = load(REGULAR, "sans-serif", None) {
|
||||
fonts.font_data.insert("system-regular".into(), Arc::new(reg));
|
||||
if let Some(f) = fonts.families.get_mut(&FontFamily::Proportional) {
|
||||
f.insert(0, "system-regular".into());
|
||||
}
|
||||
}
|
||||
if let Some(b) = load(BOLD, true) {
|
||||
if let Some(b) = load(BOLD, "sans-serif:style=Bold", None) {
|
||||
fonts.font_data.insert("system-bold".into(), Arc::new(b));
|
||||
bold_family.insert(0, "system-bold".into());
|
||||
} else if fonts.font_data.contains_key("system-regular") {
|
||||
bold_family.insert(0, "system-regular".into());
|
||||
}
|
||||
fonts.families.insert(FontFamily::Name("bold".into()), bold_family);
|
||||
if let Some(h) = load(HANGUL, "sans-serif:lang=ko", Some("ko")) {
|
||||
fonts.font_data.insert("system-hangul".into(), Arc::new(h));
|
||||
for family in fonts.families.values_mut() {
|
||||
family.push("system-hangul".into());
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
|
||||
pub fn install(ctx: &egui::Context) {
|
||||
ctx.set_fonts(definitions());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hangul_candidates_cover_every_desktop_platform() {
|
||||
for prefix in ["/System/Library/Fonts/", "C:\\Windows\\Fonts\\", "/usr/share/fonts/"] {
|
||||
assert!(HANGUL.iter().any(|(p, _)| p.starts_with(prefix)), "no Hangul candidate under {prefix}");
|
||||
}
|
||||
// Face 0 of the Noto Sans CJK collection is Japanese; the Korean face is 1.
|
||||
assert!(HANGUL.iter().filter(|(p, _)| p.ends_with("NotoSansCJK-Regular.ttc")).all(|(_, i)| *i == 1));
|
||||
}
|
||||
|
||||
/// Korean names draw in every family, not as boxes; skipped when the system has no Korean font.
|
||||
#[test]
|
||||
fn every_family_draws_hangul_with_a_system_korean_font() {
|
||||
let defs = definitions();
|
||||
if !defs.font_data.contains_key("system-hangul") {
|
||||
eprintln!("skipping: no Korean system font");
|
||||
return;
|
||||
}
|
||||
let families: Vec<FontFamily> = defs.families.keys().cloned().collect();
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_fonts(defs);
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||
output.textures_delta.clear();
|
||||
for family in families {
|
||||
let font = egui::FontId::new(13.0, family.clone());
|
||||
assert!(ctx.fonts_mut(|f| f.has_glyphs(&font, "가한국어세션")), "{family:?} has no Hangul");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user