1use crate::{Attrs, Font, FontMatchAttrs, HashMap, ShapeBuffer};
2use alloc::boxed::Box;
3use alloc::collections::BTreeSet;
4use alloc::string::String;
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::fmt;
8use core::ops::{Deref, DerefMut};
9use fontdb::Query;
10use skrifa::raw::{ReadError, TableProvider as _};
11
12pub use fontdb;
14pub use harfrust;
15
16use super::fallback::{Fallback, Fallbacks, MonospaceFallbackInfo, PlatformFallback};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub struct FontMatchKey {
20 pub(crate) font_weight_diff: u16,
21 pub(crate) font_weight: u16,
22 pub(crate) id: fontdb::ID,
23}
24
25struct FontCachedCodepointSupportInfo {
26 supported: Vec<u32>,
27 not_supported: Vec<u32>,
28}
29
30impl FontCachedCodepointSupportInfo {
31 const SUPPORTED_MAX_SZ: usize = 512;
32 const NOT_SUPPORTED_MAX_SZ: usize = 1024;
33
34 fn new() -> Self {
35 Self {
36 supported: Vec::with_capacity(Self::SUPPORTED_MAX_SZ),
37 not_supported: Vec::with_capacity(Self::NOT_SUPPORTED_MAX_SZ),
38 }
39 }
40
41 #[inline(always)]
42 fn unknown_has_codepoint(
43 &mut self,
44 font_codepoints: &[u32],
45 codepoint: u32,
46 supported_insert_pos: usize,
47 not_supported_insert_pos: usize,
48 ) -> bool {
49 let ret = font_codepoints.contains(&codepoint);
50 if ret {
51 if supported_insert_pos != Self::SUPPORTED_MAX_SZ {
53 self.supported.insert(supported_insert_pos, codepoint);
54 self.supported.truncate(Self::SUPPORTED_MAX_SZ);
55 }
56 } else {
57 if not_supported_insert_pos != Self::NOT_SUPPORTED_MAX_SZ {
59 self.not_supported
60 .insert(not_supported_insert_pos, codepoint);
61 self.not_supported.truncate(Self::NOT_SUPPORTED_MAX_SZ);
62 }
63 }
64 ret
65 }
66
67 #[inline(always)]
68 fn has_codepoint(&mut self, font_codepoints: &[u32], codepoint: u32) -> bool {
69 match self.supported.binary_search(&codepoint) {
70 Ok(_) => true,
71 Err(supported_insert_pos) => match self.not_supported.binary_search(&codepoint) {
72 Ok(_) => false,
73 Err(not_supported_insert_pos) => self.unknown_has_codepoint(
74 font_codepoints,
75 codepoint,
76 supported_insert_pos,
77 not_supported_insert_pos,
78 ),
79 },
80 }
81 }
82}
83
84pub struct FontSystem {
86 locale: String,
88
89 db: fontdb::Database,
91
92 font_cache: HashMap<(fontdb::ID, fontdb::Weight), Option<Arc<Font>>>,
94
95 monospace_font_ids: Vec<fontdb::ID>,
97
98 per_script_monospace_font_ids: HashMap<[u8; 4], Vec<fontdb::ID>>,
102
103 font_codepoint_support_info_cache: HashMap<fontdb::ID, FontCachedCodepointSupportInfo>,
105
106 font_matches_cache: HashMap<FontMatchAttrs, Arc<Vec<FontMatchKey>>>,
108
109 pub(crate) shape_buffer: ShapeBuffer,
111
112 pub(crate) monospace_fallbacks_buffer: BTreeSet<MonospaceFallbackInfo>,
114
115 #[cfg(feature = "shape-run-cache")]
117 pub shape_run_cache: crate::ShapeRunCache,
118
119 pub(crate) dyn_fallback: Box<dyn Fallback>,
121
122 pub(crate) fallbacks: Fallbacks,
124}
125
126impl fmt::Debug for FontSystem {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.debug_struct("FontSystem")
129 .field("locale", &self.locale)
130 .field("db", &self.db)
131 .finish_non_exhaustive()
132 }
133}
134
135impl FontSystem {
136 const FONT_MATCHES_CACHE_SIZE_LIMIT: usize = 256;
137 pub fn new() -> Self {
145 Self::new_with_fonts(core::iter::empty())
146 }
147
148 pub fn new_with_fonts(fonts: impl IntoIterator<Item = fontdb::Source>) -> Self {
150 let locale = Self::get_locale();
151 log::debug!("Locale: {locale}");
152
153 let mut db = fontdb::Database::new();
154
155 Self::load_fonts(&mut db, fonts.into_iter());
156
157 db.set_monospace_family("Noto Sans Mono");
159 db.set_sans_serif_family("Open Sans");
160 db.set_serif_family("DejaVu Serif");
161
162 Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
163 }
164
165 pub fn new_with_locale_and_db_and_fallback(
167 locale: String,
168 db: fontdb::Database,
169 impl_fallback: impl Fallback + 'static,
170 ) -> Self {
171 let mut monospace_font_ids = db
172 .faces()
173 .filter(|face_info| {
174 face_info.monospaced && !face_info.post_script_name.contains("Emoji")
175 })
176 .map(|face_info| face_info.id)
177 .collect::<Vec<_>>();
178 monospace_font_ids.sort();
179
180 let mut per_script_monospace_font_ids: HashMap<[u8; 4], BTreeSet<fontdb::ID>> =
181 HashMap::default();
182
183 if cfg!(feature = "monospace_fallback") {
184 for &id in &monospace_font_ids {
185 db.with_face_data(id, |font_data, face_index| {
186 let face = skrifa::FontRef::from_index(font_data, face_index)?;
187 for script in face
188 .gpos()?
189 .script_list()?
190 .script_records()
191 .iter()
192 .chain(face.gsub()?.script_list()?.script_records().iter())
193 {
194 per_script_monospace_font_ids
195 .entry(script.script_tag().into_bytes())
196 .or_default()
197 .insert(id);
198 }
199 Ok::<_, ReadError>(())
200 });
201 }
202 }
203
204 let per_script_monospace_font_ids = per_script_monospace_font_ids
205 .into_iter()
206 .map(|(k, v)| (k, Vec::from_iter(v)))
207 .collect();
208
209 let fallbacks = Fallbacks::new(&impl_fallback, &[], &locale);
210
211 Self {
212 locale,
213 db,
214 monospace_font_ids,
215 per_script_monospace_font_ids,
216 font_cache: HashMap::default(),
217 font_matches_cache: HashMap::default(),
218 font_codepoint_support_info_cache: HashMap::default(),
219 monospace_fallbacks_buffer: BTreeSet::default(),
220 #[cfg(feature = "shape-run-cache")]
221 shape_run_cache: crate::ShapeRunCache::default(),
222 shape_buffer: ShapeBuffer::default(),
223 dyn_fallback: Box::new(impl_fallback),
224 fallbacks,
225 }
226 }
227
228 pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self {
230 Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
231 }
232
233 pub fn locale(&self) -> &str {
235 &self.locale
236 }
237
238 pub const fn db(&self) -> &fontdb::Database {
240 &self.db
241 }
242
243 pub fn db_mut(&mut self) -> &mut fontdb::Database {
245 self.font_matches_cache.clear();
246 &mut self.db
247 }
248
249 pub fn into_locale_and_db(self) -> (String, fontdb::Database) {
251 (self.locale, self.db)
252 }
253
254 pub fn get_font(&mut self, id: fontdb::ID, weight: fontdb::Weight) -> Option<Arc<Font>> {
256 self.font_cache
257 .entry((id, weight))
258 .or_insert_with(|| {
259 #[cfg(feature = "std")]
260 unsafe {
261 self.db.make_shared_face_data(id);
262 }
263 if let Some(font) = Font::new(&self.db, id, weight) {
264 Some(Arc::new(font))
265 } else {
266 log::warn!(
267 "failed to load font '{}'",
268 self.db.face(id)?.post_script_name
269 );
270 None
271 }
272 })
273 .clone()
274 }
275
276 pub fn is_monospace(&self, id: fontdb::ID) -> bool {
277 self.monospace_font_ids.binary_search(&id).is_ok()
278 }
279
280 pub fn get_monospace_ids_for_scripts(
281 &self,
282 scripts: impl Iterator<Item = [u8; 4]>,
283 ) -> Vec<fontdb::ID> {
284 let mut ret = scripts
285 .filter_map(|script| self.per_script_monospace_font_ids.get(&script))
286 .flat_map(|ids| ids.iter().copied())
287 .collect::<Vec<_>>();
288 ret.sort();
289 ret.dedup();
290 ret
291 }
292
293 #[inline(always)]
294 pub fn get_font_supported_codepoints_in_word(
295 &mut self,
296 id: fontdb::ID,
297 weight: fontdb::Weight,
298 word: &str,
299 ) -> Option<usize> {
300 self.get_font(id, weight).map(|font| {
301 let code_points = font.unicode_codepoints();
302 let cache = self
303 .font_codepoint_support_info_cache
304 .entry(id)
305 .or_insert_with(FontCachedCodepointSupportInfo::new);
306 word.chars()
307 .filter(|ch| cache.has_codepoint(code_points, u32::from(*ch)))
308 .count()
309 })
310 }
311
312 pub fn get_font_matches(&mut self, attrs: &Attrs<'_>) -> Arc<Vec<FontMatchKey>> {
313 if self.font_matches_cache.len() >= Self::FONT_MATCHES_CACHE_SIZE_LIMIT {
315 log::trace!("clear font mache cache");
316 self.font_matches_cache.clear();
317 }
318
319 self.font_matches_cache
320 .entry(attrs.into())
322 .or_insert_with(|| {
323 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
324 let now = std::time::Instant::now();
325
326 let mut font_match_keys = self
327 .db
328 .faces()
329 .filter(|face| attrs.matches(face))
330 .map(|face| FontMatchKey {
331 font_weight_diff: attrs.weight.0.abs_diff(face.weight.0),
332 font_weight: face.weight.0,
333 id: face.id,
334 })
335 .collect::<Vec<_>>();
336
337 font_match_keys.sort();
339
340 let query = Query {
342 families: &[attrs.family],
343 weight: attrs.weight,
344 stretch: attrs.stretch,
345 style: attrs.style,
346 };
347
348 if let Some(id) = self.db.query(&query) {
349 if let Some(i) = font_match_keys
350 .iter()
351 .enumerate()
352 .find(|(_i, key)| key.id == id)
353 .map(|(i, _)| i)
354 {
355 let match_key = font_match_keys.remove(i);
357 font_match_keys.insert(0, match_key);
358 } else if let Some(face) = self.db.face(id) {
359 let match_key = FontMatchKey {
361 font_weight_diff: attrs.weight.0.abs_diff(face.weight.0),
362 font_weight: face.weight.0,
363 id,
364 };
365 font_match_keys.insert(0, match_key);
366 } else {
367 log::error!("Could not get face from db, that should've been there.");
368 }
369 }
370
371 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
372 {
373 let elapsed = now.elapsed();
374 log::debug!("font matches for {attrs:?} in {elapsed:?}");
375 }
376
377 Arc::new(font_match_keys)
378 })
379 .clone()
380 }
381
382 #[cfg(feature = "std")]
383 fn get_locale() -> String {
384 sys_locale::get_locale().unwrap_or_else(|| {
385 log::warn!("failed to get system locale, falling back to en-US");
386 String::from("en-US")
387 })
388 }
389
390 #[cfg(not(feature = "std"))]
391 fn get_locale() -> String {
392 String::from("en-US")
393 }
394
395 #[cfg(feature = "std")]
396 fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
397 #[cfg(not(target_arch = "wasm32"))]
398 let now = std::time::Instant::now();
399
400 db.load_system_fonts();
401
402 for source in fonts {
403 db.load_font_source(source);
404 }
405
406 #[cfg(not(target_arch = "wasm32"))]
407 log::debug!(
408 "Parsed {} font faces in {}ms.",
409 db.len(),
410 now.elapsed().as_millis()
411 );
412 }
413
414 #[cfg(not(feature = "std"))]
415 fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
416 for source in fonts {
417 db.load_font_source(source);
418 }
419 }
420}
421
422#[derive(Debug)]
424pub struct BorrowedWithFontSystem<'a, T> {
425 pub(crate) inner: &'a mut T,
426 pub(crate) font_system: &'a mut FontSystem,
427}
428
429impl<T> Deref for BorrowedWithFontSystem<'_, T> {
430 type Target = T;
431
432 fn deref(&self) -> &Self::Target {
433 self.inner
434 }
435}
436
437impl<T> DerefMut for BorrowedWithFontSystem<'_, T> {
438 fn deref_mut(&mut self) -> &mut Self::Target {
439 self.inner
440 }
441}