cosmic_text/font/fallback/
mod.rs
1use alloc::borrow::ToOwned;
4use alloc::string::String;
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::{mem, ops::Range};
8use fontdb::Family;
9use unicode_script::Script;
10
11use crate::{BuildHasher, Font, FontMatchKey, FontSystem, HashMap, ShapeBuffer};
12
13#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "windows")))]
14#[path = "other.rs"]
15mod platform;
16
17#[cfg(target_os = "macos")]
18#[path = "macos.rs"]
19mod platform;
20
21#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
22#[path = "unix.rs"]
23mod platform;
24
25#[cfg(target_os = "windows")]
26#[path = "windows.rs"]
27mod platform;
28
29pub trait Fallback: Send + Sync {
69 fn common_fallback(&self) -> &[&'static str];
71
72 fn forbidden_fallback(&self) -> &[&'static str];
74
75 fn script_fallback(&self, script: Script, locale: &str) -> &[&'static str];
77}
78
79#[derive(Debug, Default)]
80pub(crate) struct Fallbacks {
81 lists: Vec<&'static str>,
82 common_fallback_range: Range<usize>,
83 forbidden_fallback_range: Range<usize>,
84 script_fallback_ranges: HashMap<Script, Range<usize>>,
86 locale: String,
87}
88
89impl Fallbacks {
90 pub(crate) fn new(fallbacks: &dyn Fallback, scripts: &[Script], locale: &str) -> Self {
91 let common_fallback = fallbacks.common_fallback();
92
93 let forbidden_fallback = fallbacks.forbidden_fallback();
94
95 let mut lists =
96 Vec::with_capacity(common_fallback.len() + forbidden_fallback.len() + scripts.len());
97
98 let mut index = lists.len();
99 let mut new_range = |lists: &Vec<&str>| {
100 let old_index = index;
101 index = lists.len();
102 old_index..index
103 };
104
105 lists.extend_from_slice(common_fallback);
106 let common_fallback_range = new_range(&lists);
107
108 lists.extend_from_slice(forbidden_fallback);
109 let forbidden_fallback_range = new_range(&lists);
110
111 let mut script_fallback_ranges =
112 HashMap::with_capacity_and_hasher(scripts.len(), BuildHasher::default());
113 for &script in scripts {
114 let script_fallback = fallbacks.script_fallback(script, locale);
115 lists.extend_from_slice(script_fallback);
116 let script_fallback_range = new_range(&lists);
117 script_fallback_ranges.insert(script, script_fallback_range);
118 }
119
120 let locale = locale.to_owned();
121 Self {
122 lists,
123 common_fallback_range,
124 forbidden_fallback_range,
125 script_fallback_ranges,
126 locale,
127 }
128 }
129
130 pub(crate) fn extend(&mut self, fallbacks: &dyn Fallback, scripts: &[Script]) {
131 self.lists.reserve(scripts.len());
132
133 let mut index = self.lists.len();
134 let mut new_range = |lists: &Vec<&str>| {
135 let old_index = index;
136 index = lists.len();
137 old_index..index
138 };
139
140 for &script in scripts {
141 self.script_fallback_ranges
142 .entry(script)
143 .or_insert_with_key(|&script| {
144 let script_fallback = fallbacks.script_fallback(script, &self.locale);
145 self.lists.extend_from_slice(script_fallback);
146 new_range(&self.lists)
147 });
148 }
149 }
150
151 pub(crate) fn common_fallback(&self) -> &[&'static str] {
152 &self.lists[self.common_fallback_range.clone()]
153 }
154
155 pub(crate) fn forbidden_fallback(&self) -> &[&'static str] {
156 &self.lists[self.forbidden_fallback_range.clone()]
157 }
158
159 pub(crate) fn script_fallback(&self, script: Script) -> &[&'static str] {
160 self.script_fallback_ranges
161 .get(&script)
162 .map_or(&[], |range| &self.lists[range.clone()])
163 }
164}
165
166pub use platform::PlatformFallback;
167
168#[cfg(not(feature = "warn_on_missing_glyphs"))]
169use log::debug as missing_warn;
170#[cfg(feature = "warn_on_missing_glyphs")]
171use log::warn as missing_warn;
172
173#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
177pub(crate) struct MonospaceFallbackInfo {
178 font_weight_diff: Option<u16>,
179 codepoint_non_matches: Option<usize>,
180 font_weight: u16,
181 id: fontdb::ID,
182}
183
184pub struct FontFallbackIter<'a> {
185 font_system: &'a mut FontSystem,
186 font_match_keys: &'a [FontMatchKey],
187 default_families: &'a [&'a Family<'a>],
188 default_i: usize,
189 scripts: &'a [Script],
190 word: &'a str,
191 script_i: (usize, usize),
192 common_i: usize,
193 other_i: usize,
194 end: bool,
195}
196
197impl<'a> FontFallbackIter<'a> {
198 pub fn new(
199 font_system: &'a mut FontSystem,
200 font_match_keys: &'a [FontMatchKey],
201 default_families: &'a [&'a Family<'a>],
202 scripts: &'a [Script],
203 word: &'a str,
204 ) -> Self {
205 font_system
206 .fallbacks
207 .extend(font_system.dyn_fallback.as_ref(), scripts);
208 font_system.monospace_fallbacks_buffer.clear();
209 Self {
210 font_system,
211 font_match_keys,
212 default_families,
213 default_i: 0,
214 scripts,
215 word,
216 script_i: (0, 0),
217 common_i: 0,
218 other_i: 0,
219 end: false,
220 }
221 }
222
223 pub fn check_missing(&mut self, word: &str) {
224 if self.end {
225 missing_warn!(
226 "Failed to find any fallback for {:?} locale '{}': '{}'",
227 self.scripts,
228 self.font_system.locale(),
229 word
230 );
231 } else if self.other_i > 0 {
232 missing_warn!(
233 "Failed to find preset fallback for {:?} locale '{}', used '{}': '{}'",
234 self.scripts,
235 self.font_system.locale(),
236 self.face_name(self.font_match_keys[self.other_i - 1].id),
237 word
238 );
239 } else if !self.scripts.is_empty() && self.common_i > 0 {
240 let family = self.font_system.fallbacks.common_fallback()[self.common_i - 1];
241 missing_warn!(
242 "Failed to find script fallback for {:?} locale '{}', used '{}': '{}'",
243 self.scripts,
244 self.font_system.locale(),
245 family,
246 word
247 );
248 }
249 }
250
251 pub fn face_name(&self, id: fontdb::ID) -> &str {
252 if let Some(face) = self.font_system.db().face(id) {
253 if let Some((name, _)) = face.families.first() {
254 name
255 } else {
256 &face.post_script_name
257 }
258 } else {
259 "invalid font id"
260 }
261 }
262
263 pub fn shape_caches(&mut self) -> &mut ShapeBuffer {
264 &mut self.font_system.shape_buffer
265 }
266
267 fn face_contains_family(&self, id: fontdb::ID, family_name: &str) -> bool {
268 if let Some(face) = self.font_system.db().face(id) {
269 face.families.iter().any(|(name, _)| name == family_name)
270 } else {
271 false
272 }
273 }
274
275 fn default_font_match_key(&self) -> Option<&FontMatchKey> {
276 let default_family = self.default_families[self.default_i - 1];
277 let default_family_name = self.font_system.db().family_name(default_family);
278
279 self.font_match_keys
280 .iter()
281 .filter(|m_key| m_key.font_weight_diff == 0)
282 .find(|m_key| self.face_contains_family(m_key.id, default_family_name))
283 }
284
285 fn next_item(&mut self, fallbacks: &Fallbacks) -> Option<<Self as Iterator>::Item> {
286 if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
287 if let Some(font) = self.font_system.get_font(fallback_info.id) {
288 return Some(font);
289 }
290 }
291
292 let font_match_keys_iter = |is_mono| {
293 self.font_match_keys
294 .iter()
295 .filter(move |m_key| m_key.font_weight_diff == 0 || is_mono)
296 };
297
298 'DEF_FAM: while self.default_i < self.default_families.len() {
299 self.default_i += 1;
300 let is_mono = self.default_families[self.default_i - 1] == &Family::Monospace;
301 let default_font_match_key = self.default_font_match_key().cloned();
302 let word_chars_count = self.word.chars().count();
303
304 macro_rules! mk_mono_fallback_info {
305 ($m_key:expr) => {{
306 let supported_cp_count_opt = self
307 .font_system
308 .get_font_supported_codepoints_in_word($m_key.id, self.word);
309
310 supported_cp_count_opt.map(|supported_cp_count| {
311 let codepoint_non_matches = word_chars_count - supported_cp_count;
312
313 MonospaceFallbackInfo {
314 font_weight_diff: Some($m_key.font_weight_diff),
315 codepoint_non_matches: Some(codepoint_non_matches),
316 font_weight: $m_key.font_weight,
317 id: $m_key.id,
318 }
319 })
320 }};
321 }
322
323 match (is_mono, default_font_match_key.as_ref()) {
324 (false, None) => break 'DEF_FAM,
325 (false, Some(m_key)) => {
326 if let Some(font) = self.font_system.get_font(m_key.id) {
327 return Some(font);
328 } else {
329 break 'DEF_FAM;
330 }
331 }
332 (true, None) => (),
333 (true, Some(m_key)) => {
334 if let Some(mut fallback_info) = mk_mono_fallback_info!(m_key) {
336 fallback_info.font_weight_diff = None;
337
338 if fallback_info.codepoint_non_matches == Some(0) {
341 if let Some(font) = self.font_system.get_font(m_key.id) {
342 return Some(font);
343 }
344 } else {
345 assert!(self
346 .font_system
347 .monospace_fallbacks_buffer
348 .insert(fallback_info));
349 }
350 }
351 }
352 };
353
354 let mono_ids_for_scripts = if is_mono && !self.scripts.is_empty() {
355 let scripts = self.scripts.iter().filter_map(|script| {
356 let script_as_lower = script.short_name().to_lowercase();
357 <[u8; 4]>::try_from(script_as_lower.as_bytes()).ok()
358 });
359 self.font_system.get_monospace_ids_for_scripts(scripts)
360 } else {
361 Vec::new()
362 };
363
364 for m_key in font_match_keys_iter(is_mono) {
365 if Some(m_key.id) != default_font_match_key.as_ref().map(|m_key| m_key.id) {
366 let is_mono_id = if mono_ids_for_scripts.is_empty() {
367 self.font_system.is_monospace(m_key.id)
368 } else {
369 mono_ids_for_scripts.binary_search(&m_key.id).is_ok()
370 };
371
372 if is_mono_id {
373 let supported_cp_count_opt = self
374 .font_system
375 .get_font_supported_codepoints_in_word(m_key.id, self.word);
376 if let Some(supported_cp_count) = supported_cp_count_opt {
377 let codepoint_non_matches =
378 self.word.chars().count() - supported_cp_count;
379
380 let fallback_info = MonospaceFallbackInfo {
381 font_weight_diff: Some(m_key.font_weight_diff),
382 codepoint_non_matches: Some(codepoint_non_matches),
383 font_weight: m_key.font_weight,
384 id: m_key.id,
385 };
386 assert!(self
387 .font_system
388 .monospace_fallbacks_buffer
389 .insert(fallback_info));
390 }
391 }
392 }
393 }
394 if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
396 if let Some(font) = self.font_system.get_font(fallback_info.id) {
397 return Some(font);
398 }
399 }
400 }
401
402 while self.script_i.0 < self.scripts.len() {
403 let script = self.scripts[self.script_i.0];
404
405 let script_families = fallbacks.script_fallback(script);
406
407 while self.script_i.1 < script_families.len() {
408 let script_family = script_families[self.script_i.1];
409 self.script_i.1 += 1;
410 for m_key in font_match_keys_iter(false) {
411 if self.face_contains_family(m_key.id, script_family) {
412 if let Some(font) = self.font_system.get_font(m_key.id) {
413 return Some(font);
414 }
415 }
416 }
417 log::debug!(
418 "failed to find family '{}' for script {:?} and locale '{}'",
419 script_family,
420 script,
421 self.font_system.locale(),
422 );
423 }
424
425 self.script_i.0 += 1;
426 self.script_i.1 = 0;
427 }
428
429 let common_families = fallbacks.common_fallback();
430 while self.common_i < common_families.len() {
431 let common_family = common_families[self.common_i];
432 self.common_i += 1;
433 for m_key in font_match_keys_iter(false) {
434 if self.face_contains_family(m_key.id, common_family) {
435 if let Some(font) = self.font_system.get_font(m_key.id) {
436 return Some(font);
437 }
438 }
439 }
440 log::debug!("failed to find family '{}'", common_family);
441 }
442
443 let forbidden_families = fallbacks.forbidden_fallback();
446 while self.other_i < self.font_match_keys.len() {
447 let id = self.font_match_keys[self.other_i].id;
448 self.other_i += 1;
449 if forbidden_families
450 .iter()
451 .all(|family_name| !self.face_contains_family(id, family_name))
452 {
453 if let Some(font) = self.font_system.get_font(id) {
454 return Some(font);
455 }
456 }
457 }
458
459 self.end = true;
460 None
461 }
462}
463
464impl Iterator for FontFallbackIter<'_> {
465 type Item = Arc<Font>;
466 fn next(&mut self) -> Option<Self::Item> {
467 let mut fallbacks = mem::take(&mut self.font_system.fallbacks);
468 let item = self.next_item(&fallbacks);
469 mem::swap(&mut fallbacks, &mut self.font_system.fallbacks);
470 item
471 }
472}