Fonts: a Thai fallback, so Thai text no longer draws as boxes (#1913)
Thanks @DuBearte.
This commit is contained in:
1 parent
67d4628753
commit
9c07bdd823
1 file changed
+76
-13
+76
-13
@@ -1,9 +1,10 @@
|
|||||||
//! System font loading and fallback setup for multi-language (CJK: Japanese, Chinese, Korean) support.
|
//! System font loading and fallback setup for multi-language (CJK: Japanese, Chinese, Korean;
|
||||||
|
//! Thai) support.
|
||||||
//!
|
//!
|
||||||
//! The UI's Latin font always comes first; a CJK font (then a Hangul font) is only ever appended as
|
//! The UI's Latin font always comes first; a CJK font (then a Hangul font, then a Thai font) is only
|
||||||
//! a fallback, so English text renders exactly as before and only glyphs the Latin font lacks use
|
//! ever appended as a fallback, so English text renders exactly as before and only glyphs the Latin
|
||||||
//! the CJK face. The Hangul fallback covers Korean where the CJK font found (Meiryo, Hiragino)
|
//! font lacks use the CJK face. The Hangul fallback covers Korean where the CJK font found (Meiryo,
|
||||||
//! lacks Hangul. The order of the CJK and Chinese faces follows the UI language
|
//! Hiragino) lacks Hangul; the Thai fallback covers Thai typed in a name or the agent chat (#1902). The order of the CJK and Chinese faces follows the UI language
|
||||||
//! (see [`fallback_order`]): one face drawing a whole word keeps its glyphs on one baseline.
|
//! (see [`fallback_order`]): one face drawing a whole word keeps its glyphs on one baseline.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -44,9 +45,12 @@ pub fn with_ui_font(mut fonts: egui::FontDefinitions, font: egui::FontData) -> e
|
|||||||
fonts
|
fonts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name of the Hangul fallback entry in [`egui::FontDefinitions`], last.
|
/// Name of the Hangul fallback entry in [`egui::FontDefinitions`], after the CJK ones.
|
||||||
pub const HANGUL_FALLBACK: &str = "hangul_fallback";
|
pub const HANGUL_FALLBACK: &str = "hangul_fallback";
|
||||||
|
|
||||||
|
/// Name of the Thai fallback entry in [`egui::FontDefinitions`], last (#1902).
|
||||||
|
pub const THAI_FALLBACK: &str = "thai_fallback";
|
||||||
|
|
||||||
/// UI languages whose text needs the CJK font. The web app's bundled font covers Japanese only;
|
/// UI languages whose text needs the CJK font. The web app's bundled font covers Japanese only;
|
||||||
/// add Chinese or Korean here together with a font that covers them.
|
/// add Chinese or Korean here together with a font that covers them.
|
||||||
pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "ko"];
|
pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "ko"];
|
||||||
@@ -318,6 +322,32 @@ const HANGUL_FONT_CANDIDATES: &[&str] = &[
|
|||||||
"/usr/share/fonts/nanum/NanumGothic.ttf",
|
"/usr/share/fonts/nanum/NanumGothic.ttf",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Thai fonts, appended last: none of the Latin or CJK faces draw Thai, so without one Thai text
|
||||||
|
/// shows as empty boxes (#1902).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
const THAI_FONT_CANDIDATES: &[&str] = &[
|
||||||
|
// Windows: Leelawadee UI (Windows 8 and later), Leelawadee, Tahoma
|
||||||
|
"C:\\Windows\\Fonts\\LeelawUI.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\leelawad.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\tahoma.ttf",
|
||||||
|
// macOS: Thonburi, Ayuthaya
|
||||||
|
"/System/Library/Fonts/Thonburi.ttc",
|
||||||
|
"/System/Library/Fonts/Supplemental/Thonburi.ttc",
|
||||||
|
"/System/Library/Fonts/Supplemental/Ayuthaya.ttf",
|
||||||
|
"/Library/Fonts/Thonburi.ttf",
|
||||||
|
// Linux: Noto Sans Thai, TLWG Loma and Garuda
|
||||||
|
"/usr/share/fonts/noto/NotoSansThai-Regular.ttf",
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSansThai-Regular.ttf",
|
||||||
|
"/usr/share/fonts/google-noto/NotoSansThai-Regular.ttf",
|
||||||
|
"/usr/share/fonts/google-noto-vf/NotoSansThai[wght].ttf",
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansThai-Regular.ttf",
|
||||||
|
"/usr/share/fonts/truetype/tlwg/Loma.ttf",
|
||||||
|
"/usr/share/fonts/opentype/tlwg/Loma.otf",
|
||||||
|
"/usr/share/fonts/truetype/tlwg/Garuda.ttf",
|
||||||
|
"/usr/share/fonts/opentype/tlwg/Garuda.otf",
|
||||||
|
"/usr/share/fonts/tlwg/Loma.otf",
|
||||||
|
];
|
||||||
|
|
||||||
/// Fonts covering simplified Chinese, appended after the CJK fallback so Chinese renders even
|
/// Fonts covering simplified Chinese, appended after the CJK fallback so Chinese renders even
|
||||||
/// when the CJK font found is a Japanese one without simplified characters (Hiragino Gothic,
|
/// when the CJK font found is a Japanese one without simplified characters (Hiragino Gothic,
|
||||||
/// Meiryo, Yu Gothic).
|
/// Meiryo, Yu Gothic).
|
||||||
@@ -464,11 +494,16 @@ fn find_system_cjk_font() -> Option<Vec<u8>> {
|
|||||||
/// epaint centres each face's own line height in the row, so glyphs from two faces of very
|
/// epaint centres each face's own line height in the row, so glyphs from two faces of very
|
||||||
/// different height (Meiryo ≈ 2.0 em, Microsoft YaHei ≈ 1.4 em) sit at different heights
|
/// different height (Meiryo ≈ 2.0 em, Microsoft YaHei ≈ 1.4 em) sit at different heights
|
||||||
/// inside one word. One face for the whole word keeps them on one baseline.
|
/// inside one word. One face for the whole word keeps them on one baseline.
|
||||||
pub fn fallback_order(language: &str) -> [&'static str; 3] {
|
/// Thai comes last whatever the language: no other face draws it.
|
||||||
if language == "zh" { [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK] } else { [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK] }
|
pub fn fallback_order(language: &str) -> [&'static str; 4] {
|
||||||
|
if language == "zh" {
|
||||||
|
[CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK]
|
||||||
|
} else {
|
||||||
|
[CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure font definitions with system CJK, Chinese and Hangul fallbacks where available,
|
/// Configure font definitions with system CJK, Chinese, Hangul and Thai fallbacks where available,
|
||||||
/// ordered for the UI language (see [`fallback_order`]), after the interface font `ui_font`
|
/// ordered for the UI language (see [`fallback_order`]), after the interface font `ui_font`
|
||||||
/// when one is chosen (see [`ui_font`]).
|
/// when one is chosen (see [`ui_font`]).
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -476,6 +511,7 @@ pub fn setup_fonts_for(ctx: &egui::Context, language: &str, ui_font: &str) {
|
|||||||
let cjk = find_system_cjk_font();
|
let cjk = find_system_cjk_font();
|
||||||
let chinese = find_system_chinese_font().filter(|bytes| cjk.as_ref() != Some(bytes));
|
let chinese = find_system_chinese_font().filter(|bytes| cjk.as_ref() != Some(bytes));
|
||||||
let hangul = HANGUL_FONT_CANDIDATES.iter().find_map(|path| try_read_font(std::path::Path::new(path)));
|
let hangul = HANGUL_FONT_CANDIDATES.iter().find_map(|path| try_read_font(std::path::Path::new(path)));
|
||||||
|
let thai = THAI_FONT_CANDIDATES.iter().find_map(|path| try_read_font(std::path::Path::new(path)));
|
||||||
let mut fonts = egui::FontDefinitions::default();
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
// Set the fonts also when only the interface font changes, so going back to the default
|
// Set the fonts also when only the interface font changes, so going back to the default
|
||||||
// takes the chosen one away.
|
// takes the chosen one away.
|
||||||
@@ -487,7 +523,8 @@ pub fn setup_fonts_for(ctx: &egui::Context, language: &str, ui_font: &str) {
|
|||||||
let bytes = match name {
|
let bytes = match name {
|
||||||
CJK_FALLBACK => cjk.clone(),
|
CJK_FALLBACK => cjk.clone(),
|
||||||
CHINESE_FALLBACK => chinese.clone(),
|
CHINESE_FALLBACK => chinese.clone(),
|
||||||
_ => hangul.clone(),
|
HANGUL_FALLBACK => hangul.clone(),
|
||||||
|
_ => thai.clone(),
|
||||||
};
|
};
|
||||||
if let Some(bytes) = bytes
|
if let Some(bytes) = bytes
|
||||||
&& let Some(with_fallback) = with_fallback(fonts.clone(), name, bytes)
|
&& let Some(with_fallback) = with_fallback(fonts.clone(), name, bytes)
|
||||||
@@ -586,9 +623,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_chinese_ui_puts_the_chinese_face_before_the_japanese_one() {
|
fn a_chinese_ui_puts_the_chinese_face_before_the_japanese_one() {
|
||||||
assert_eq!(fallback_order("zh"), [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK]);
|
assert_eq!(fallback_order("zh"), [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK]);
|
||||||
for lang in ["en", "ja", "ko", "de"] {
|
for lang in ["en", "ja", "ko", "de"] {
|
||||||
assert_eq!(fallback_order(lang), [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK], "{lang} keeps the Japanese font first");
|
assert_eq!(
|
||||||
|
fallback_order(lang),
|
||||||
|
[CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK],
|
||||||
|
"{lang} keeps the Japanese font first"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let defaults = egui::FontDefinitions::default();
|
let defaults = egui::FontDefinitions::default();
|
||||||
let mut fonts = defaults.clone();
|
let mut fonts = defaults.clone();
|
||||||
@@ -598,10 +639,32 @@ mod tests {
|
|||||||
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
|
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
|
||||||
let list = &fonts.families[&family];
|
let list = &fonts.families[&family];
|
||||||
assert_eq!(list.first(), defaults.families[&family].first(), "the Latin UI font stays first");
|
assert_eq!(list.first(), defaults.families[&family].first(), "the Latin UI font stays first");
|
||||||
assert_eq!(&list[list.len() - 3..], [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK]);
|
assert_eq!(&list[list.len() - 4..], [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thai typed into the agent chat drew as empty boxes (#1902): every Thai candidate this
|
||||||
|
/// machine has must parse and cover Thai, and the fonts set up must then draw it.
|
||||||
|
#[test]
|
||||||
|
fn a_thai_fallback_draws_thai() {
|
||||||
|
let found: Vec<Vec<u8>> = THAI_FONT_CANDIDATES.iter().filter_map(|path| try_read_font(std::path::Path::new(path))).collect();
|
||||||
|
for bytes in &found {
|
||||||
|
let face = ttf_parser::Face::parse(bytes, 0).unwrap();
|
||||||
|
for c in "สวัสดี".chars() {
|
||||||
|
assert!(face.glyph_index(c).is_some(), "{c:?} is in the Thai font");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let thai = |fonts: egui::FontDefinitions| {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
ctx.set_fonts(fonts);
|
||||||
|
ctx.run_ui(egui::RawInput::default(), |_| {}).textures_delta.clear();
|
||||||
|
ctx.fonts_mut(|f| f.has_glyphs(&egui::FontId::proportional(14.0), "สวัสดี"))
|
||||||
|
};
|
||||||
|
assert!(!thai(egui::FontDefinitions::default()), "the built-in faces have no Thai");
|
||||||
|
let Some(bytes) = found.into_iter().next() else { return };
|
||||||
|
assert!(thai(with_fallback(egui::FontDefinitions::default(), THAI_FALLBACK, bytes).unwrap()), "Thai draws with the fallback");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hangul_font_falls_back_after_the_cjk_font() {
|
fn hangul_font_falls_back_after_the_cjk_font() {
|
||||||
let defaults = egui::FontDefinitions::default();
|
let defaults = egui::FontDefinitions::default();
|
||||||
|
|||||||
Reference in new issue
Block a user