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 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 struct MonospaceFallbackInfo {
178    font_weight_diff: Option<u16>,
179    codepoint_non_matches: Option<usize>,
180    font_weight: u16,
181    id: fontdb::ID,
182}
183
184#[derive(Debug)]
185pub struct FontFallbackIter<'a> {
186    font_system: &'a mut FontSystem,
187    font_match_keys: &'a [FontMatchKey],
188    default_families: &'a [&'a Family<'a>],
189    default_i: usize,
190    scripts: &'a [Script],
191    word: &'a str,
192    script_i: (usize, usize),
193    common_i: usize,
194    other_i: usize,
195    end: bool,
196    ideal_weight: fontdb::Weight,
197}
198
199impl<'a> FontFallbackIter<'a> {
200    pub fn new(
201        font_system: &'a mut FontSystem,
202        font_match_keys: &'a [FontMatchKey],
203        default_families: &'a [&'a Family<'a>],
204        scripts: &'a [Script],
205        word: &'a str,
206        ideal_weight: fontdb::Weight,
207    ) -> Self {
208        font_system
209            .fallbacks
210            .extend(font_system.dyn_fallback.as_ref(), scripts);
211        font_system.monospace_fallbacks_buffer.clear();
212        Self {
213            font_system,
214            font_match_keys,
215            default_families,
216            default_i: 0,
217            scripts,
218            word,
219            script_i: (0, 0),
220            common_i: 0,
221            other_i: 0,
222            end: false,
223            ideal_weight,
224        }
225    }
226
227    pub fn check_missing(&self, word: &str) {
228        if self.end {
229            missing_warn!(
230                "Failed to find any fallback for {:?} locale '{}': '{}'",
231                self.scripts,
232                self.font_system.locale(),
233                word
234            );
235        } else if self.other_i > 0 {
236            missing_warn!(
237                "Failed to find preset fallback for {:?} locale '{}', used '{}': '{}'",
238                self.scripts,
239                self.font_system.locale(),
240                self.face_name(self.font_match_keys[self.other_i - 1].id),
241                word
242            );
243        } else if !self.scripts.is_empty() && self.common_i > 0 {
244            let family = self.font_system.fallbacks.common_fallback()[self.common_i - 1];
245            missing_warn!(
246                "Failed to find script fallback for {:?} locale '{}', used '{}': '{}'",
247                self.scripts,
248                self.font_system.locale(),
249                family,
250                word
251            );
252        }
253    }
254
255    pub fn face_name(&self, id: fontdb::ID) -> &str {
256        self.font_system
257            .db()
258            .face(id)
259            .map_or("invalid font id", |face| {
260                if let Some((name, _)) = face.families.first() {
261                    name
262                } else {
263                    &face.post_script_name
264                }
265            })
266    }
267
268    pub fn shape_caches(&mut self) -> &mut ShapeBuffer {
269        &mut self.font_system.shape_buffer
270    }
271
272    fn face_contains_family(&self, id: fontdb::ID, family_name: &str) -> bool {
273        self.font_system
274            .db()
275            .face(id)
276            .is_some_and(|face| face.families.iter().any(|(name, _)| name == family_name))
277    }
278
279    fn default_font_match_key(&self) -> Option<&FontMatchKey> {
280        let default_family = self.default_families[self.default_i - 1];
281        let default_family_name = self.font_system.db().family_name(default_family);
282
283        self.font_match_keys
284            .iter()
285            .filter(|m_key| m_key.font_weight_diff == 0)
286            .find(|m_key| self.face_contains_family(m_key.id, default_family_name))
287    }
288
289    fn next_item(&mut self, fallbacks: &Fallbacks) -> Option<<Self as Iterator>::Item> {
290        if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
291            if let Some(font) = self
292                .font_system
293                .get_font(fallback_info.id, self.ideal_weight)
294            {
295                return Some(font);
296            }
297        }
298
299        let font_match_keys_iter = |is_mono| {
300            self.font_match_keys
301                .iter()
302                .filter(move |m_key| m_key.font_weight_diff == 0 || is_mono)
303        };
304
305        'DEF_FAM: while self.default_i < self.default_families.len() {
306            self.default_i += 1;
307            let is_mono = self.default_families[self.default_i - 1] == &Family::Monospace;
308            let default_font_match_key = self.default_font_match_key().copied();
309            let word_chars_count = self.word.chars().count();
310
311            macro_rules! mk_mono_fallback_info {
312                ($m_key:expr) => {{
313                    let supported_cp_count_opt =
314                        self.font_system.get_font_supported_codepoints_in_word(
315                            $m_key.id,
316                            self.ideal_weight,
317                            self.word,
318                        );
319
320                    supported_cp_count_opt.map(|supported_cp_count| {
321                        let codepoint_non_matches = word_chars_count - supported_cp_count;
322
323                        MonospaceFallbackInfo {
324                            font_weight_diff: Some($m_key.font_weight_diff),
325                            codepoint_non_matches: Some(codepoint_non_matches),
326                            font_weight: $m_key.font_weight,
327                            id: $m_key.id,
328                        }
329                    })
330                }};
331            }
332
333            match (is_mono, default_font_match_key.as_ref()) {
334                (false, None) => break 'DEF_FAM,
335                (false, Some(m_key)) => {
336                    if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
337                        return Some(font);
338                    }
339                    break 'DEF_FAM;
340                }
341                (true, None) => (),
342                (true, Some(m_key)) => {
343                    // Default Monospace font
344                    if let Some(mut fallback_info) = mk_mono_fallback_info!(m_key) {
345                        fallback_info.font_weight_diff = None;
346
347                        // Return early if default Monospace font supports all word codepoints.
348                        // Otherewise, add to fallbacks set
349                        if fallback_info.codepoint_non_matches == Some(0) {
350                            if let Some(font) =
351                                self.font_system.get_font(m_key.id, self.ideal_weight)
352                            {
353                                return Some(font);
354                            }
355                        } else {
356                            assert!(self
357                                .font_system
358                                .monospace_fallbacks_buffer
359                                .insert(fallback_info));
360                        }
361                    }
362                }
363            }
364
365            let mono_ids_for_scripts = if is_mono && !self.scripts.is_empty() {
366                let scripts = self.scripts.iter().filter_map(|script| {
367                    let script_as_lower = script.short_name().to_lowercase();
368                    <[u8; 4]>::try_from(script_as_lower.as_bytes()).ok()
369                });
370                self.font_system.get_monospace_ids_for_scripts(scripts)
371            } else {
372                Vec::new()
373            };
374
375            for m_key in font_match_keys_iter(is_mono) {
376                if Some(m_key.id) != default_font_match_key.as_ref().map(|m_key| m_key.id) {
377                    let is_mono_id = if mono_ids_for_scripts.is_empty() {
378                        self.font_system.is_monospace(m_key.id)
379                    } else {
380                        mono_ids_for_scripts.binary_search(&m_key.id).is_ok()
381                    };
382
383                    if is_mono_id {
384                        let supported_cp_count_opt =
385                            self.font_system.get_font_supported_codepoints_in_word(
386                                m_key.id,
387                                self.ideal_weight,
388                                self.word,
389                            );
390                        if let Some(supported_cp_count) = supported_cp_count_opt {
391                            let codepoint_non_matches =
392                                self.word.chars().count() - supported_cp_count;
393
394                            let fallback_info = MonospaceFallbackInfo {
395                                font_weight_diff: Some(m_key.font_weight_diff),
396                                codepoint_non_matches: Some(codepoint_non_matches),
397                                font_weight: m_key.font_weight,
398                                id: m_key.id,
399                            };
400                            assert!(self
401                                .font_system
402                                .monospace_fallbacks_buffer
403                                .insert(fallback_info));
404                        }
405                    }
406                }
407            }
408            // If default family is Monospace fallback to first monospaced font
409            if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
410                if let Some(font) = self
411                    .font_system
412                    .get_font(fallback_info.id, self.ideal_weight)
413                {
414                    return Some(font);
415                }
416            }
417        }
418
419        while self.script_i.0 < self.scripts.len() {
420            let script = self.scripts[self.script_i.0];
421
422            let script_families = fallbacks.script_fallback(script);
423
424            while self.script_i.1 < script_families.len() {
425                let script_family = script_families[self.script_i.1];
426                self.script_i.1 += 1;
427                for m_key in font_match_keys_iter(false) {
428                    if self.face_contains_family(m_key.id, script_family) {
429                        if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
430                            return Some(font);
431                        }
432                    }
433                }
434                log::debug!(
435                    "failed to find family '{}' for script {:?} and locale '{}'",
436                    script_family,
437                    script,
438                    self.font_system.locale(),
439                );
440            }
441
442            self.script_i.0 += 1;
443            self.script_i.1 = 0;
444        }
445
446        let common_families = fallbacks.common_fallback();
447        while self.common_i < common_families.len() {
448            let common_family = common_families[self.common_i];
449            self.common_i += 1;
450            for m_key in font_match_keys_iter(false) {
451                if self.face_contains_family(m_key.id, common_family) {
452                    if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
453                        return Some(font);
454                    }
455                }
456            }
457            log::debug!("failed to find family '{common_family}'");
458        }
459
460        //TODO: do we need to do this?
461        //TODO: do not evaluate fonts more than once!
462        let forbidden_families = fallbacks.forbidden_fallback();
463        while self.other_i < self.font_match_keys.len() {
464            let id = self.font_match_keys[self.other_i].id;
465            self.other_i += 1;
466            if forbidden_families
467                .iter()
468                .all(|family_name| !self.face_contains_family(id, family_name))
469            {
470                if let Some(font) = self.font_system.get_font(id, self.ideal_weight) {
471                    return Some(font);
472                }
473            }
474        }
475
476        self.end = true;
477        None
478    }
479}
480
481impl Iterator for FontFallbackIter<'_> {
482    type Item = Arc<Font>;
483    fn next(&mut self) -> Option<Self::Item> {
484        let mut fallbacks = mem::take(&mut self.font_system.fallbacks);
485        let item = self.next_item(&fallbacks);
486        mem::swap(&mut fallbacks, &mut self.font_system.fallbacks);
487        item
488    }
489}