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
9pub 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 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 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
81pub struct FontSystem {
83 locale: String,
85
86 db: fontdb::Database,
88
89 font_cache: HashMap<fontdb::ID, Option<Arc<Font>>>,
91
92 monospace_font_ids: Vec<fontdb::ID>,
94
95 per_script_monospace_font_ids: HashMap<[u8; 4], Vec<fontdb::ID>>,
99
100 font_codepoint_support_info_cache: HashMap<fontdb::ID, FontCachedCodepointSupportInfo>,
102
103 font_matches_cache: HashMap<FontMatchAttrs, Arc<Vec<FontMatchKey>>>,
105
106 pub(crate) shape_buffer: ShapeBuffer,
108
109 pub(crate) monospace_fallbacks_buffer: BTreeSet<MonospaceFallbackInfo>,
111
112 #[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 pub fn new() -> Self {
136 Self::new_with_fonts(core::iter::empty())
137 }
138
139 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 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 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 pub fn locale(&self) -> &str {
212 &self.locale
213 }
214
215 pub fn db(&self) -> &fontdb::Database {
217 &self.db
218 }
219
220 pub fn db_mut(&mut self) -> &mut fontdb::Database {
222 self.font_matches_cache.clear();
223 &mut self.db
224 }
225
226 pub fn into_locale_and_db(self) -> (String, fontdb::Database) {
228 (self.locale, self.db)
229 }
230
231 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 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 .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 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#[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}