cosmic_text/font/fallback/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use 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
29/// The `Fallback` trait allows for configurable font fallback lists to be set during construction of the [`FontSystem`].
30///
31/// A custom fallback list can be added via the [`FontSystem::new_with_locale_and_db_and_fallback`] constructor.
32///
33/// A default implementation is provided by the [`PlatformFallback`] struct, which encapsulates the target platform's pre-configured fallback lists.
34///
35/// ```rust
36/// # use unicode_script::Script;
37/// # use cosmic_text::{Fallback, FontSystem};
38/// struct MyFallback;
39/// impl Fallback for MyFallback {
40///     fn common_fallback(&self) -> &[&'static str] {
41///         &[
42///             "Segoe UI",
43///             "Segoe UI Emoji",
44///             "Segoe UI Symbol",
45///             "Segoe UI Historic",
46///         ]
47///     }
48///
49///     fn forbidden_fallback(&self) -> &[&'static str] {
50///         &[]
51///     }
52///
53///     fn script_fallback(&self, script: Script, locale: &str) -> &[&'static str] {
54///         match script {
55///             Script::Adlam => &["Ebrima"],
56///             Script::Bengali => &["Nirmala UI"],
57///             Script::Canadian_Aboriginal => &["Gadugi"],
58///             // ...
59///             _ => &[],
60///        }
61///     }
62/// }
63///
64/// let locale = "en-US".to_string();
65/// let db = fontdb::Database::new();
66/// let font_system = FontSystem::new_with_locale_and_db_and_fallback(locale, db, MyFallback);
67/// ```
68pub trait Fallback: Send + Sync {
69    /// Fallbacks to use after any script specific fallbacks
70    fn common_fallback(&self) -> &[&'static str];
71
72    /// Fallbacks to never use
73    fn forbidden_fallback(&self) -> &[&'static str];
74
75    /// Fallbacks to use per script
76    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    // PERF: Consider using NoHashHasher since Script is just an integer
85    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// Match on lowest font_weight_diff, then script_non_matches, then font_weight
174// Default font gets None for both `weight_offset` and `script_non_matches`, and thus, it is
175// always the first to be popped from the set.
176#[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                    // Default Monospace font
335                    if let Some(mut fallback_info) = mk_mono_fallback_info!(m_key) {
336                        fallback_info.font_weight_diff = None;
337
338                        // Return early if default Monospace font supports all word codepoints.
339                        // Otherewise, add to fallbacks set
340                        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 default family is Monospace fallback to first monospaced font
395            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        //TODO: do we need to do this?
444        //TODO: do not evaluate fonts more than once!
445        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}