Skip to main content

text/
fallback.rs

1// fallback -- characters a font lacks, drawn with another font (font fallback)
2//
3//   set_fallback_fonts(ids)        (shaping.rs) the app's own chain, tried first; shaping uses
4//                                  it as well
5//   set_system_fallback(on)        then system fonts: the families of FAMILIES (this OS's
6//                                  list, after the ';' separated families of
7//                                  jfgui_FALLBACK_FONTS), found with a FontDb of the font
8//                                  folders (scanned on the first missing character; its cache
9//                                  is cache://fontdb.fdb, not with jfgui_FONTDIRS) and loaded
10//                                  when first needed
11//   set_fallback_lang(tag)         the UI language: its fonts (FontConf::lang_families, after
12//                                  jfgui_FALLBACK_FONTS) come before FAMILIES, so Han
13//                                  characters take the language's forms (ja, zh-Hans, zh-Hant,
14//                                  ko) and scripts their usual font
15//   system_fallback() -> bool
16//   fallback_for(cp) -> Option<font id>   the first font of the chain that has cp (remembered
17//                                  per code point, also when none has it)
18//   fallback_active() -> bool      a chain is set (run / shaping only look for fallbacks then)
19//
20// Characters that fonts lack on purpose (controls, zero-width spaces and joiners, bidi marks,
21// variation selectors, tags) never fall back.  Fallback glyphs share the baseline and the
22// pixel size of the run; ascent, descent and line height stay the first font's.
23
24use alloc::collections::BTreeMap;
25use alloc::string::String;
26use alloc::vec::Vec;
27
28use crate::fontconf::FontConf;
29use crate::fontdb::FontDb;
30use crate::Text;
31
32#[cfg(windows)]
33const FAMILIES: &[&str] = &[
34    "Segoe UI", "Segoe UI Symbol", "Segoe UI Emoji", "Segoe UI Historic", "Microsoft YaHei", "Microsoft JhengHei",
35    "Yu Gothic", "Malgun Gothic", "Nirmala UI", "Leelawadee UI", "Ebrima", "Gadugi", "Microsoft Himalaya",
36    "Mongolian Baiti", "Myanmar Text", "Javanese Text", "Sylfaen", "Cambria Math", "Arial", "Times New Roman",
37    "MS Gothic", "SimSun", "Arial Unicode MS",
38];
39
40#[cfg(not(windows))]
41const FAMILIES: &[&str] = &[
42    "DejaVu Sans", "Noto Sans", "Liberation Sans", "Noto Sans CJK SC", "Noto Sans CJK JP", "Noto Sans CJK KR",
43    "Noto Sans CJK TC", "Noto Color Emoji", "Noto Emoji", "Noto Sans Symbols", "Noto Sans Symbols2", "Noto Sans Math",
44    "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devanagari", "Noto Sans Bengali", "Noto Sans Tamil",
45    "Noto Sans Thai", "Noto Sans Ethiopic", "Noto Sans Armenian", "Noto Sans Georgian", "WenQuanYi Micro Hei",
46    "Droid Sans Fallback", "FreeSans", "FreeSerif", "Unifont", "DejaVu Serif", "DejaVu Sans Mono",
47];
48
49const CACHE_MAX: usize = 8192; // remembered code points
50const NONE: u32 = u32::MAX;
51
52#[derive(Default)]
53pub(crate) struct Fallback {
54    /// the app's chain (set_fallback_fonts)
55    pub(crate) app: Vec<u32>,
56    system: bool,
57    db: Option<FontDb>,
58    /// the system families in order with their font id (-1: not there / failed; 0 + ...:
59    /// loaded); None: not tried yet
60    families: Vec<(String, Option<i32>)>,
61    cache: BTreeMap<u32, u32>,
62    /// set_fallback_lang; the families list is built again when it changes
63    lang: String,
64    built: bool,
65}
66
67/// characters that fonts lack on purpose
68fn never(cp: u32) -> bool {
69    cp < 0x20
70        || (0x7F..=0x9F).contains(&cp)
71        || (0x200B..=0x200F).contains(&cp)
72        || (0x2028..=0x202E).contains(&cp)
73        || (0x2060..=0x206F).contains(&cp)
74        || (0xFE00..=0xFE0F).contains(&cp)
75        || cp == 0xFEFF
76        || (0xE0000..=0xE01EF).contains(&cp)
77}
78
79impl Text {
80    /// look for missing characters in system fonts too (after set_fallback_fonts' chain)
81    pub fn set_system_fallback(&mut self, on: bool) {
82        if self.fb.system != on {
83            self.fb.system = on;
84            self.fb.cache.clear();
85        }
86    }
87
88    /// the language whose fonts the system fallback tries first ("" none)
89    pub fn set_fallback_lang(&mut self, tag: &str) {
90        if self.fb.lang != tag {
91            self.fb.lang = String::from(tag);
92            self.fb.built = false;
93            self.fb.cache.clear();
94        }
95    }
96
97    /// whether system fonts are searched for missing characters
98    pub fn system_fallback(&self) -> bool {
99        self.fb.system
100    }
101
102    /// whether run / shaping look for fallback fonts at all
103    pub fn fallback_active(&self) -> bool {
104        self.fb.system || !self.fb.app.is_empty()
105    }
106
107    // set_fallback_fonts: the app's chain changed
108    pub(crate) fn fallback_app(&mut self, ids: &[u32]) {
109        self.fb.app = ids.to_vec();
110        self.fb.cache.clear();
111    }
112
113    /// the first font of the fallback chain that has cp (None: none has it)
114    pub fn fallback_for(&mut self, cp: u32) -> Option<u32> {
115        if never(cp) || !self.fallback_active() {
116            return None;
117        }
118        if let Some(&id) = self.fb.cache.get(&cp) {
119            return (id != NONE).then_some(id);
120        }
121        let found = self.fallback_search(cp);
122        if self.fb.cache.len() >= CACHE_MAX {
123            self.fb.cache.clear();
124        }
125        self.fb.cache.insert(cp, found.unwrap_or(NONE));
126        found
127    }
128
129    fn fallback_search(&mut self, cp: u32) -> Option<u32> {
130        let app = self.fb.app.clone();
131        if let Some(id) = app.into_iter().find(|&id| self.valid(id) && self.has(id, cp)) {
132            return Some(id);
133        }
134        if !self.fb.system {
135            return None;
136        }
137        self.fallback_scan();
138        for k in 0..self.fb.families.len() {
139            let id = match self.fb.families[k].1 {
140                Some(id) => id,
141                None => {
142                    let fam = self.fb.families[k].0.clone();
143                    let mut db = self.fb.db.take().unwrap_or_default();
144                    let id = db.load(self, &fam, 400, false);
145                    self.fb.db = Some(db);
146                    self.fb.families[k].1 = Some(id);
147                    id
148                }
149            };
150            if id >= 0 && self.has(id as u32, cp) {
151                return Some(id as u32);
152            }
153        }
154        None
155    }
156
157    // the font folders' faces (once) and the family list (again after a language change)
158    fn fallback_scan(&mut self) {
159        if self.fb.db.is_some() {
160            if !self.fb.built {
161                let db = self.fb.db.take().unwrap_or_default();
162                self.fallback_families(&db);
163                self.fb.db = Some(db);
164            }
165            return;
166        }
167        let mut db = FontDb::new();
168        // the cache belongs to the system folders; jfgui_FONTDIRS (tests) scans without it
169        let cache = plat::env("jfgui_FONTDIRS").is_none().then_some("cache://fontdb.fdb");
170        if let Some(c) = cache {
171            db.load_cache(c);
172        }
173        db.scan(0);
174        if let Some(c) = cache {
175            db.save_cache(c);
176        }
177        self.fallback_families(&db);
178        self.fb.db = Some(db);
179    }
180
181    // the installed families of jfgui_FALLBACK_FONTS, the language's and FAMILIES in order
182    fn fallback_families(&mut self, db: &FontDb) {
183        let mut fams: Vec<String> = plat::env("jfgui_FALLBACK_FONTS")
184            .map(|v| v.split(';').map(str::trim).filter(|s| !s.is_empty()).map(String::from).collect())
185            .unwrap_or_default();
186        if !self.fb.lang.is_empty() {
187            fams.extend(FontConf::system().lang_families(&self.fb.lang));
188        }
189        fams.extend(FAMILIES.iter().map(|s| String::from(*s)));
190        let mut seen: Vec<String> = Vec::new();
191        self.fb.families = fams
192            .into_iter()
193            .filter(|f| {
194                let new = !seen.iter().any(|s| s.eq_ignore_ascii_case(f));
195                seen.push(f.clone());
196                new && db.find(f, 400, false).is_some()
197            })
198            .map(|f| (f, None))
199            .collect();
200        self.fb.built = true;
201    }
202}