Skip to main content

cosmic_text/font/fallback/
unix.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use unicode_script::Script;
4
5use super::Fallback;
6
7/// A platform-specific font fallback list, for Unix.
8#[derive(Debug)]
9pub struct PlatformFallback;
10
11impl Fallback for PlatformFallback {
12    fn common_fallback(&self) -> &'static [&'static str] {
13        common_fallback()
14    }
15
16    fn forbidden_fallback(&self) -> &'static [&'static str] {
17        forbidden_fallback()
18    }
19
20    fn script_fallback(
21        &self,
22        script: unicode_script::Script,
23        locale: &str,
24    ) -> &'static [&'static str] {
25        script_fallback(script, locale)
26    }
27}
28
29// Fallbacks to use after any script specific fallbacks
30const fn common_fallback() -> &'static [&'static str] {
31    //TODO: abstract style (sans/serif/monospaced)
32    &[
33        /* Sans-serif fallbacks */
34        "Noto Sans",
35        /* More sans-serif fallbacks */
36        "DejaVu Sans",
37        "FreeSans",
38        /* Mono fallbacks */
39        "Noto Sans Mono",
40        "DejaVu Sans Mono",
41        "FreeMono",
42        /* Symbols fallbacks */
43        "Noto Sans Symbols",
44        "Noto Sans Symbols2",
45        /* Emoji fallbacks*/
46        "Noto Color Emoji",
47        //TODO: Add CJK script here for doublewides?
48    ]
49}
50
51// Fallbacks to never use
52const fn forbidden_fallback() -> &'static [&'static str] {
53    &[]
54}
55
56fn han_unification(locale: &str) -> &'static [&'static str] {
57    // The locale comes from `sys_locale`, which reports BCP-47 tags that
58    // usually carry a region ("ja-JP", "ko-KR", "zh-TW"); other callers may
59    // hand in POSIX-style tags ("ja_JP", "ja_JP.UTF-8"). Matching the whole
60    // tag against bare language codes meant e.g. "ja-JP" fell through to the
61    // Simplified Chinese default, so Japanese systems rendered Han with SC
62    // glyph forms. Compare subtags instead, keeping the zh distinctions.
63    let mut subtags = locale.split(['-', '_', '.', '@']);
64    let lang = subtags.next().unwrap_or(locale);
65    let subtag = subtags.next();
66    match (lang, subtag) {
67        // Japan
68        ("ja", _) => &["Noto Sans CJK JP"],
69        // Korea
70        ("ko", _) => &["Noto Sans CJK KR"],
71        // Hong Kong
72        ("zh", Some("HK")) => &["Noto Sans CJK HK"],
73        // Taiwan, and Traditional-script tags like "zh-Hant(-…)"
74        ("zh", Some("TW")) | ("zh", Some("Hant")) => &["Noto Sans CJK TC"],
75        // Simplified Chinese is the default (also catches "zh-CN" for China)
76        _ => &["Noto Sans CJK SC"],
77    }
78}
79
80// Fallbacks to use per script
81fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
82    //TODO: abstract style (sans/serif/monospaced)
83    match script {
84        Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"],
85        Script::Arabic => &["Noto Sans Arabic"],
86        Script::Armenian => &["Noto Sans Armenian"],
87        Script::Bengali => &["Noto Sans Bengali"],
88        Script::Bopomofo => han_unification(locale),
89        //TODO: DejaVu Sans would typically be selected for braille characters,
90        // but this breaks alignment when used alongside monospaced text.
91        // By requesting the use of FreeMono first, this issue can be avoided.
92        Script::Braille => &["FreeMono"],
93        Script::Buhid => &["Noto Sans Buhid"],
94        Script::Chakma => &["Noto Sans Chakma"],
95        Script::Cherokee => &["Noto Sans Cherokee"],
96        Script::Deseret => &["Noto Sans Deseret"],
97        Script::Devanagari => &["Noto Sans Devanagari"],
98        Script::Ethiopic => &["Noto Sans Ethiopic"],
99        Script::Georgian => &["Noto Sans Georgian"],
100        Script::Gothic => &["Noto Sans Gothic"],
101        Script::Grantha => &["Noto Sans Grantha"],
102        Script::Gujarati => &["Noto Sans Gujarati"],
103        Script::Gurmukhi => &["Noto Sans Gurmukhi"],
104        Script::Han => han_unification(locale),
105        Script::Hangul => han_unification("ko"),
106        Script::Hanunoo => &["Noto Sans Hanunoo"],
107        Script::Hebrew => &["Noto Sans Hebrew"],
108        Script::Hiragana => han_unification("ja"),
109        Script::Javanese => &["Noto Sans Javanese"],
110        Script::Kannada => &["Noto Sans Kannada"],
111        Script::Katakana => han_unification("ja"),
112        Script::Khmer => &["Noto Sans Khmer"],
113        Script::Lao => &["Noto Sans Lao"],
114        Script::Malayalam => &["Noto Sans Malayalam"],
115        Script::Mongolian => &["Noto Sans Mongolian"],
116        Script::Myanmar => &["Noto Sans Myanmar"],
117        Script::Oriya => &["Noto Sans Oriya"],
118        Script::Runic => &["Noto Sans Runic"],
119        Script::Sinhala => &["Noto Sans Sinhala"],
120        Script::Syriac => &["Noto Sans Syriac"],
121        Script::Tagalog => &["Noto Sans Tagalog"],
122        Script::Tagbanwa => &["Noto Sans Tagbanwa"],
123        Script::Tai_Le => &["Noto Sans Tai Le"],
124        Script::Tai_Tham => &["Noto Sans Tai Tham"],
125        Script::Tai_Viet => &["Noto Sans Tai Viet"],
126        Script::Tamil => &["Noto Sans Tamil"],
127        Script::Telugu => &["Noto Sans Telugu"],
128        Script::Thaana => &["Noto Sans Thaana"],
129        Script::Thai => &["Noto Sans Thai"],
130        //TODO: no sans script?
131        Script::Tibetan => &["Noto Serif Tibetan"],
132        Script::Tifinagh => &["Noto Sans Tifinagh"],
133        Script::Vai => &["Noto Sans Vai"],
134        //TODO: Use han_unification?
135        Script::Yi => &["Noto Sans Yi", "Noto Sans CJK SC"],
136        _ => &[],
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::han_unification;
143
144    #[test]
145    fn han_unification_handles_locales_with_region_subtags() {
146        // Bare language codes (previous behavior, still supported)
147        assert_eq!(han_unification("ja"), &["Noto Sans CJK JP"]);
148        assert_eq!(han_unification("ko"), &["Noto Sans CJK KR"]);
149        // BCP-47 tags as reported by sys_locale
150        assert_eq!(han_unification("ja-JP"), &["Noto Sans CJK JP"]);
151        assert_eq!(han_unification("ko-KR"), &["Noto Sans CJK KR"]);
152        assert_eq!(han_unification("zh-HK"), &["Noto Sans CJK HK"]);
153        assert_eq!(han_unification("zh-TW"), &["Noto Sans CJK TC"]);
154        assert_eq!(han_unification("zh-Hant"), &["Noto Sans CJK TC"]);
155        assert_eq!(han_unification("zh-Hant-TW"), &["Noto Sans CJK TC"]);
156        // POSIX-style tags
157        assert_eq!(han_unification("ja_JP"), &["Noto Sans CJK JP"]);
158        assert_eq!(han_unification("ja_JP.UTF-8"), &["Noto Sans CJK JP"]);
159        assert_eq!(han_unification("zh_HK.UTF-8"), &["Noto Sans CJK HK"]);
160        // Simplified Chinese default
161        assert_eq!(han_unification("zh"), &["Noto Sans CJK SC"]);
162        assert_eq!(han_unification("zh-CN"), &["Noto Sans CJK SC"]);
163        assert_eq!(han_unification("zh-Hans"), &["Noto Sans CJK SC"]);
164        assert_eq!(han_unification("en-US"), &["Noto Sans CJK SC"]);
165    }
166}