salvation_cosmic_text/font/
system.rs

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