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