1use 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; const NONE: u32 = u32::MAX;
51
52#[derive(Default)]
53pub(crate) struct Fallback {
54 pub(crate) app: Vec<u32>,
56 system: bool,
57 db: Option<FontDb>,
58 families: Vec<(String, Option<i32>)>,
61 cache: BTreeMap<u32, u32>,
62 lang: String,
64 built: bool,
65}
66
67fn 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 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 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 pub fn system_fallback(&self) -> bool {
99 self.fb.system
100 }
101
102 pub fn fallback_active(&self) -> bool {
104 self.fb.system || !self.fb.app.is_empty()
105 }
106
107 pub(crate) fn fallback_app(&mut self, ids: &[u32]) {
109 self.fb.app = ids.to_vec();
110 self.fb.cache.clear();
111 }
112
113 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 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 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 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}