cosmic_text/font/
system.rs

1use crate::{Attrs, Font, FontMatchAttrs, HashMap, ShapeBuffer};
2use alloc::collections::BTreeSet;
3use alloc::string::String;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6use core::fmt;
7use core::ops::{Deref, DerefMut};
8
9// re-export fontdb and rustybuzz
10pub use fontdb;
11pub use rustybuzz;
12
13use super::fallback::MonospaceFallbackInfo;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub struct FontMatchKey {
17    pub(crate) font_weight_diff: u16,
18    pub(crate) font_weight: u16,
19    pub(crate) id: fontdb::ID,
20}
21
22struct FontCachedCodepointSupportInfo {
23    supported: Vec<u32>,
24    not_supported: Vec<u32>,
25}
26
27impl FontCachedCodepointSupportInfo {
28    const SUPPORTED_MAX_SZ: usize = 512;
29    const NOT_SUPPORTED_MAX_SZ: usize = 1024;
30
31    fn new() -> Self {
32        Self {
33            supported: Vec::with_capacity(Self::SUPPORTED_MAX_SZ),
34            not_supported: Vec::with_capacity(Self::NOT_SUPPORTED_MAX_SZ),
35        }
36    }
37
38    #[inline(always)]
39    fn unknown_has_codepoint(
40        &mut self,
41        font_codepoints: &[u32],
42        codepoint: u32,
43        supported_insert_pos: usize,
44        not_supported_insert_pos: usize,
45    ) -> bool {
46        let ret = font_codepoints.contains(&codepoint);
47        if ret {
48            // don't bother inserting if we are going to truncate the entry away
49            if supported_insert_pos != Self::SUPPORTED_MAX_SZ {
50                self.supported.insert(supported_insert_pos, codepoint);
51                self.supported.truncate(Self::SUPPORTED_MAX_SZ);
52            }
53        } else {
54            // don't bother inserting if we are going to truncate the entry away
55            if not_supported_insert_pos != Self::NOT_SUPPORTED_MAX_SZ {
56                self.not_supported
57                    .insert(not_supported_insert_pos, codepoint);
58                self.not_supported.truncate(Self::NOT_SUPPORTED_MAX_SZ);
59            }
60        }
61        ret
62    }
63
64    #[inline(always)]
65    fn has_codepoint(&mut self, font_codepoints: &[u32], codepoint: u32) -> bool {
66        match self.supported.binary_search(&codepoint) {
67            Ok(_) => true,
68            Err(supported_insert_pos) => match self.not_supported.binary_search(&codepoint) {
69                Ok(_) => false,
70                Err(not_supported_insert_pos) => self.unknown_has_codepoint(
71                    font_codepoints,
72                    codepoint,
73                    supported_insert_pos,
74                    not_supported_insert_pos,
75                ),
76            },
77        }
78    }
79}
80
81/// Access to the system fonts.
82pub struct FontSystem {
83    /// The locale of the system.
84    locale: String,
85
86    /// The underlying font database.
87    db: fontdb::Database,
88
89    /// Cache for loaded fonts from the database.
90    font_cache: HashMap<fontdb::ID, Option<Arc<Font>>>,
91
92    /// Sorted unique ID's of all Monospace fonts in DB
93    monospace_font_ids: Vec<fontdb::ID>,
94
95    /// Sorted unique ID's of all Monospace fonts in DB per script.
96    /// A font may support multiple scripts of course, so the same ID
97    /// may appear in multiple map value vecs.
98    per_script_monospace_font_ids: HashMap<[u8; 4], Vec<fontdb::ID>>,
99
100    /// Cache for font codepoint support info
101    font_codepoint_support_info_cache: HashMap<fontdb::ID, FontCachedCodepointSupportInfo>,
102
103    /// Cache for font matches.
104    font_matches_cache: HashMap<FontMatchAttrs, Arc<Vec<FontMatchKey>>>,
105
106    /// Scratch buffer for shaping and laying out.
107    pub(crate) shape_buffer: ShapeBuffer,
108
109    /// Buffer for use in `FontFallbackIter`.
110    pub(crate) monospace_fallbacks_buffer: BTreeSet<MonospaceFallbackInfo>,
111
112    /// Cache for shaped runs
113    #[cfg(feature = "shape-run-cache")]
114    pub shape_run_cache: crate::ShapeRunCache,
115}
116
117impl fmt::Debug for FontSystem {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.debug_struct("FontSystem")
120            .field("locale", &self.locale)
121            .field("db", &self.db)
122            .finish()
123    }
124}
125
126impl FontSystem {
127    const FONT_MATCHES_CACHE_SIZE_LIMIT: usize = 256;
128    /// Create a new [`FontSystem`], that allows access to any installed system fonts
129    ///
130    /// # Timing
131    ///
132    /// This function takes some time to run. On the release build, it can take up to a second,
133    /// while debug builds can take up to ten times longer. For this reason, it should only be
134    /// called once, and the resulting [`FontSystem`] should be shared.
135    pub fn new() -> Self {
136        Self::new_with_fonts(core::iter::empty())
137    }
138
139    /// Create a new [`FontSystem`] with a pre-specified set of fonts.
140    pub fn new_with_fonts(fonts: impl IntoIterator<Item = fontdb::Source>) -> Self {
141        let locale = Self::get_locale();
142        log::debug!("Locale: {}", locale);
143
144        let mut db = fontdb::Database::new();
145
146        Self::load_fonts(&mut db, fonts.into_iter());
147
148        //TODO: configurable default fonts
149        db.set_monospace_family("Noto Sans Mono");
150        db.set_sans_serif_family("Open Sans");
151        db.set_serif_family("DejaVu Serif");
152
153        Self::new_with_locale_and_db(locale, db)
154    }
155
156    /// Create a new [`FontSystem`] with a pre-specified locale and font database.
157    pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self {
158        let mut monospace_font_ids = db
159            .faces()
160            .filter(|face_info| {
161                face_info.monospaced && !face_info.post_script_name.contains("Emoji")
162            })
163            .map(|face_info| face_info.id)
164            .collect::<Vec<_>>();
165        monospace_font_ids.sort();
166
167        let mut per_script_monospace_font_ids: HashMap<[u8; 4], BTreeSet<fontdb::ID>> =
168            HashMap::default();
169
170        if cfg!(feature = "monospace_fallback") {
171            monospace_font_ids.iter().for_each(|&id| {
172                db.with_face_data(id, |font_data, face_index| {
173                    let _ = ttf_parser::Face::parse(font_data, face_index).map(|face| {
174                        face.tables()
175                            .gpos
176                            .into_iter()
177                            .chain(face.tables().gsub)
178                            .flat_map(|table| table.scripts)
179                            .inspect(|script| {
180                                per_script_monospace_font_ids
181                                    .entry(script.tag.to_bytes())
182                                    .or_default()
183                                    .insert(id);
184                            })
185                    });
186                });
187            });
188        }
189
190        let per_script_monospace_font_ids = per_script_monospace_font_ids
191            .into_iter()
192            .map(|(k, v)| (k, Vec::from_iter(v)))
193            .collect();
194
195        Self {
196            locale,
197            db,
198            monospace_font_ids,
199            per_script_monospace_font_ids,
200            font_cache: Default::default(),
201            font_matches_cache: Default::default(),
202            font_codepoint_support_info_cache: Default::default(),
203            monospace_fallbacks_buffer: BTreeSet::default(),
204            #[cfg(feature = "shape-run-cache")]
205            shape_run_cache: crate::ShapeRunCache::default(),
206            shape_buffer: ShapeBuffer::default(),
207        }
208    }
209
210    /// Get the locale.
211    pub fn locale(&self) -> &str {
212        &self.locale
213    }
214
215    /// Get the database.
216    pub fn db(&self) -> &fontdb::Database {
217        &self.db
218    }
219
220    /// Get a mutable reference to the database.
221    pub fn db_mut(&mut self) -> &mut fontdb::Database {
222        self.font_matches_cache.clear();
223        &mut self.db
224    }
225
226    /// Consume this [`FontSystem`] and return the locale and database.
227    pub fn into_locale_and_db(self) -> (String, fontdb::Database) {
228        (self.locale, self.db)
229    }
230
231    /// Get a font by its ID.
232    pub fn get_font(&mut self, id: fontdb::ID) -> Option<Arc<Font>> {
233        self.font_cache
234            .entry(id)
235            .or_insert_with(|| {
236                #[cfg(feature = "std")]
237                unsafe {
238                    self.db.make_shared_face_data(id);
239                }
240                match Font::new(&self.db, id) {
241                    Some(font) => Some(Arc::new(font)),
242                    None => {
243                        log::warn!(
244                            "failed to load font '{}'",
245                            self.db.face(id)?.post_script_name
246                        );
247                        None
248                    }
249                }
250            })
251            .clone()
252    }
253
254    pub fn is_monospace(&self, id: fontdb::ID) -> bool {
255        self.monospace_font_ids.binary_search(&id).is_ok()
256    }
257
258    pub fn get_monospace_ids_for_scripts(
259        &self,
260        scripts: impl Iterator<Item = [u8; 4]>,
261    ) -> Vec<fontdb::ID> {
262        let mut ret = scripts
263            .filter_map(|script| self.per_script_monospace_font_ids.get(&script))
264            .flat_map(|ids| ids.iter().copied())
265            .collect::<Vec<_>>();
266        ret.sort();
267        ret.dedup();
268        ret
269    }
270
271    #[inline(always)]
272    pub fn get_font_supported_codepoints_in_word(
273        &mut self,
274        id: fontdb::ID,
275        word: &str,
276    ) -> Option<usize> {
277        self.get_font(id).map(|font| {
278            let code_points = font.unicode_codepoints();
279            let cache = self
280                .font_codepoint_support_info_cache
281                .entry(id)
282                .or_insert_with(FontCachedCodepointSupportInfo::new);
283            word.chars()
284                .filter(|ch| cache.has_codepoint(code_points, u32::from(*ch)))
285                .count()
286        })
287    }
288
289    pub fn get_font_matches(&mut self, attrs: Attrs<'_>) -> Arc<Vec<FontMatchKey>> {
290        // Clear the cache first if it reached the size limit
291        if self.font_matches_cache.len() >= Self::FONT_MATCHES_CACHE_SIZE_LIMIT {
292            log::trace!("clear font mache cache");
293            self.font_matches_cache.clear();
294        }
295
296        self.font_matches_cache
297            //TODO: do not create AttrsOwned unless entry does not already exist
298            .entry(attrs.into())
299            .or_insert_with(|| {
300                #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
301                let now = std::time::Instant::now();
302
303                let mut font_match_keys = self
304                    .db
305                    .faces()
306                    .filter(|face| attrs.matches(face))
307                    .map(|face| FontMatchKey {
308                        font_weight_diff: attrs.weight.0.abs_diff(face.weight.0),
309                        font_weight: face.weight.0,
310                        id: face.id,
311                    })
312                    .collect::<Vec<_>>();
313
314                // Sort so we get the keys with weight_offset=0 first
315                font_match_keys.sort();
316
317                #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
318                {
319                    let elapsed = now.elapsed();
320                    log::debug!("font matches for {:?} in {:?}", attrs, elapsed);
321                }
322
323                Arc::new(font_match_keys)
324            })
325            .clone()
326    }
327
328    #[cfg(feature = "std")]
329    fn get_locale() -> String {
330        sys_locale::get_locale().unwrap_or_else(|| {
331            log::warn!("failed to get system locale, falling back to en-US");
332            String::from("en-US")
333        })
334    }
335
336    #[cfg(not(feature = "std"))]
337    fn get_locale() -> String {
338        String::from("en-US")
339    }
340
341    #[cfg(feature = "std")]
342    fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
343        #[cfg(not(target_arch = "wasm32"))]
344        let now = std::time::Instant::now();
345
346        db.load_system_fonts();
347
348        for source in fonts {
349            db.load_font_source(source);
350        }
351
352        #[cfg(not(target_arch = "wasm32"))]
353        log::debug!(
354            "Parsed {} font faces in {}ms.",
355            db.len(),
356            now.elapsed().as_millis()
357        );
358    }
359
360    #[cfg(not(feature = "std"))]
361    fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
362        for source in fonts {
363            db.load_font_source(source);
364        }
365    }
366}
367
368/// A value borrowed together with an [`FontSystem`]
369#[derive(Debug)]
370pub struct BorrowedWithFontSystem<'a, T> {
371    pub(crate) inner: &'a mut T,
372    pub(crate) font_system: &'a mut FontSystem,
373}
374
375impl<T> Deref for BorrowedWithFontSystem<'_, T> {
376    type Target = T;
377
378    fn deref(&self) -> &Self::Target {
379        self.inner
380    }
381}
382
383impl<T> DerefMut for BorrowedWithFontSystem<'_, T> {
384    fn deref_mut(&mut self) -> &mut Self::Target {
385        self.inner
386    }
387}