cosmic_text/font/fallback/
mod.rs1use alloc::borrow::ToOwned;
4use alloc::string::String;
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::{mem, ops::Range};
8use fontdb::Family;
9use unicode_script::Script;
10
11use crate::{BuildHasher, Font, FontMatchKey, FontSystem, HashMap, ShapeBuffer};
12
13#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "windows")))]
14#[path = "other.rs"]
15mod platform;
16
17#[cfg(target_os = "macos")]
18#[path = "macos.rs"]
19mod platform;
20
21#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
22#[path = "unix.rs"]
23mod platform;
24
25#[cfg(target_os = "windows")]
26#[path = "windows.rs"]
27mod platform;
28
29pub trait Fallback: Send + Sync {
69 fn common_fallback(&self) -> &[&'static str];
71
72 fn forbidden_fallback(&self) -> &[&'static str];
74
75 fn script_fallback(&self, script: Script, locale: &str) -> &[&'static str];
77}
78
79#[derive(Debug, Default)]
80pub struct Fallbacks {
81 lists: Vec<&'static str>,
82 common_fallback_range: Range<usize>,
83 forbidden_fallback_range: Range<usize>,
84 script_fallback_ranges: HashMap<Script, Range<usize>>,
86 locale: String,
87}
88
89impl Fallbacks {
90 pub(crate) fn new(fallbacks: &dyn Fallback, scripts: &[Script], locale: &str) -> Self {
91 let common_fallback = fallbacks.common_fallback();
92
93 let forbidden_fallback = fallbacks.forbidden_fallback();
94
95 let mut lists =
96 Vec::with_capacity(common_fallback.len() + forbidden_fallback.len() + scripts.len());
97
98 let mut index = lists.len();
99 let mut new_range = |lists: &Vec<&str>| {
100 let old_index = index;
101 index = lists.len();
102 old_index..index
103 };
104
105 lists.extend_from_slice(common_fallback);
106 let common_fallback_range = new_range(&lists);
107
108 lists.extend_from_slice(forbidden_fallback);
109 let forbidden_fallback_range = new_range(&lists);
110
111 let mut script_fallback_ranges =
112 HashMap::with_capacity_and_hasher(scripts.len(), BuildHasher::default());
113 for &script in scripts {
114 let script_fallback = fallbacks.script_fallback(script, locale);
115 lists.extend_from_slice(script_fallback);
116 let script_fallback_range = new_range(&lists);
117 script_fallback_ranges.insert(script, script_fallback_range);
118 }
119
120 let locale = locale.to_owned();
121 Self {
122 lists,
123 common_fallback_range,
124 forbidden_fallback_range,
125 script_fallback_ranges,
126 locale,
127 }
128 }
129
130 pub(crate) fn extend(&mut self, fallbacks: &dyn Fallback, scripts: &[Script]) {
131 self.lists.reserve(scripts.len());
132
133 let mut index = self.lists.len();
134 let mut new_range = |lists: &Vec<&str>| {
135 let old_index = index;
136 index = lists.len();
137 old_index..index
138 };
139
140 for &script in scripts {
141 self.script_fallback_ranges
142 .entry(script)
143 .or_insert_with_key(|&script| {
144 let script_fallback = fallbacks.script_fallback(script, &self.locale);
145 self.lists.extend_from_slice(script_fallback);
146 new_range(&self.lists)
147 });
148 }
149 }
150
151 pub(crate) fn common_fallback(&self) -> &[&'static str] {
152 &self.lists[self.common_fallback_range.clone()]
153 }
154
155 pub(crate) fn forbidden_fallback(&self) -> &[&'static str] {
156 &self.lists[self.forbidden_fallback_range.clone()]
157 }
158
159 pub(crate) fn script_fallback(&self, script: Script) -> &[&'static str] {
160 self.script_fallback_ranges
161 .get(&script)
162 .map_or(&[], |range| &self.lists[range.clone()])
163 }
164}
165
166pub use platform::PlatformFallback;
167
168#[cfg(not(feature = "warn_on_missing_glyphs"))]
169use log::debug as missing_warn;
170#[cfg(feature = "warn_on_missing_glyphs")]
171use log::warn as missing_warn;
172
173#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
177pub struct MonospaceFallbackInfo {
178 font_weight_diff: Option<u16>,
179 codepoint_non_matches: Option<usize>,
180 font_weight: u16,
181 id: fontdb::ID,
182}
183
184#[derive(Debug)]
185pub struct FontFallbackIter<'a> {
186 font_system: &'a mut FontSystem,
187 font_match_keys: &'a [FontMatchKey],
188 default_families: &'a [&'a Family<'a>],
189 default_i: usize,
190 scripts: &'a [Script],
191 word: &'a str,
192 script_i: (usize, usize),
193 common_i: usize,
194 other_i: usize,
195 end: bool,
196 ideal_weight: fontdb::Weight,
197}
198
199impl<'a> FontFallbackIter<'a> {
200 pub fn new(
201 font_system: &'a mut FontSystem,
202 font_match_keys: &'a [FontMatchKey],
203 default_families: &'a [&'a Family<'a>],
204 scripts: &'a [Script],
205 word: &'a str,
206 ideal_weight: fontdb::Weight,
207 ) -> Self {
208 font_system
209 .fallbacks
210 .extend(font_system.dyn_fallback.as_ref(), scripts);
211 font_system.monospace_fallbacks_buffer.clear();
212 Self {
213 font_system,
214 font_match_keys,
215 default_families,
216 default_i: 0,
217 scripts,
218 word,
219 script_i: (0, 0),
220 common_i: 0,
221 other_i: 0,
222 end: false,
223 ideal_weight,
224 }
225 }
226
227 pub fn check_missing(&self, word: &str) {
228 if self.end {
229 missing_warn!(
230 "Failed to find any fallback for {:?} locale '{}': '{}'",
231 self.scripts,
232 self.font_system.locale(),
233 word
234 );
235 } else if self.other_i > 0 {
236 missing_warn!(
237 "Failed to find preset fallback for {:?} locale '{}', used '{}': '{}'",
238 self.scripts,
239 self.font_system.locale(),
240 self.face_name(self.font_match_keys[self.other_i - 1].id),
241 word
242 );
243 } else if !self.scripts.is_empty() && self.common_i > 0 {
244 let family = self.font_system.fallbacks.common_fallback()[self.common_i - 1];
245 missing_warn!(
246 "Failed to find script fallback for {:?} locale '{}', used '{}': '{}'",
247 self.scripts,
248 self.font_system.locale(),
249 family,
250 word
251 );
252 }
253 }
254
255 pub fn face_name(&self, id: fontdb::ID) -> &str {
256 self.font_system
257 .db()
258 .face(id)
259 .map_or("invalid font id", |face| {
260 if let Some((name, _)) = face.families.first() {
261 name
262 } else {
263 &face.post_script_name
264 }
265 })
266 }
267
268 pub fn shape_caches(&mut self) -> &mut ShapeBuffer {
269 &mut self.font_system.shape_buffer
270 }
271
272 fn face_contains_family(&self, id: fontdb::ID, family_name: &str) -> bool {
273 self.font_system
274 .db()
275 .face(id)
276 .is_some_and(|face| face.families.iter().any(|(name, _)| name == family_name))
277 }
278
279 fn default_font_match_key(&self) -> Option<&FontMatchKey> {
280 let default_family = self.default_families[self.default_i - 1];
281 let default_family_name = self.font_system.db().family_name(default_family);
282
283 self.font_match_keys
284 .iter()
285 .filter(|m_key| m_key.font_weight_diff == 0)
286 .find(|m_key| self.face_contains_family(m_key.id, default_family_name))
287 }
288
289 fn next_item(&mut self, fallbacks: &Fallbacks) -> Option<<Self as Iterator>::Item> {
290 if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
291 if let Some(font) = self
292 .font_system
293 .get_font(fallback_info.id, self.ideal_weight)
294 {
295 return Some(font);
296 }
297 }
298
299 let font_match_keys_iter = |is_mono| {
300 self.font_match_keys
301 .iter()
302 .filter(move |m_key| m_key.font_weight_diff == 0 || is_mono)
303 };
304
305 'DEF_FAM: while self.default_i < self.default_families.len() {
306 self.default_i += 1;
307 let is_mono = self.default_families[self.default_i - 1] == &Family::Monospace;
308 let default_font_match_key = self.default_font_match_key().copied();
309 let word_chars_count = self.word.chars().count();
310
311 macro_rules! mk_mono_fallback_info {
312 ($m_key:expr) => {{
313 let supported_cp_count_opt =
314 self.font_system.get_font_supported_codepoints_in_word(
315 $m_key.id,
316 self.ideal_weight,
317 self.word,
318 );
319
320 supported_cp_count_opt.map(|supported_cp_count| {
321 let codepoint_non_matches = word_chars_count - supported_cp_count;
322
323 MonospaceFallbackInfo {
324 font_weight_diff: Some($m_key.font_weight_diff),
325 codepoint_non_matches: Some(codepoint_non_matches),
326 font_weight: $m_key.font_weight,
327 id: $m_key.id,
328 }
329 })
330 }};
331 }
332
333 match (is_mono, default_font_match_key.as_ref()) {
334 (false, None) => break 'DEF_FAM,
335 (false, Some(m_key)) => {
336 if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
337 return Some(font);
338 }
339 break 'DEF_FAM;
340 }
341 (true, None) => (),
342 (true, Some(m_key)) => {
343 if let Some(mut fallback_info) = mk_mono_fallback_info!(m_key) {
345 fallback_info.font_weight_diff = None;
346
347 if fallback_info.codepoint_non_matches == Some(0) {
350 if let Some(font) =
351 self.font_system.get_font(m_key.id, self.ideal_weight)
352 {
353 return Some(font);
354 }
355 } else {
356 assert!(self
357 .font_system
358 .monospace_fallbacks_buffer
359 .insert(fallback_info));
360 }
361 }
362 }
363 }
364
365 let mono_ids_for_scripts = if is_mono && !self.scripts.is_empty() {
366 let scripts = self.scripts.iter().filter_map(|script| {
367 let script_as_lower = script.short_name().to_lowercase();
368 <[u8; 4]>::try_from(script_as_lower.as_bytes()).ok()
369 });
370 self.font_system.get_monospace_ids_for_scripts(scripts)
371 } else {
372 Vec::new()
373 };
374
375 for m_key in font_match_keys_iter(is_mono) {
376 if Some(m_key.id) != default_font_match_key.as_ref().map(|m_key| m_key.id) {
377 let is_mono_id = if mono_ids_for_scripts.is_empty() {
378 self.font_system.is_monospace(m_key.id)
379 } else {
380 mono_ids_for_scripts.binary_search(&m_key.id).is_ok()
381 };
382
383 if is_mono_id {
384 let supported_cp_count_opt =
385 self.font_system.get_font_supported_codepoints_in_word(
386 m_key.id,
387 self.ideal_weight,
388 self.word,
389 );
390 if let Some(supported_cp_count) = supported_cp_count_opt {
391 let codepoint_non_matches =
392 self.word.chars().count() - supported_cp_count;
393
394 let fallback_info = MonospaceFallbackInfo {
395 font_weight_diff: Some(m_key.font_weight_diff),
396 codepoint_non_matches: Some(codepoint_non_matches),
397 font_weight: m_key.font_weight,
398 id: m_key.id,
399 };
400 assert!(self
401 .font_system
402 .monospace_fallbacks_buffer
403 .insert(fallback_info));
404 }
405 }
406 }
407 }
408 if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
410 if let Some(font) = self
411 .font_system
412 .get_font(fallback_info.id, self.ideal_weight)
413 {
414 return Some(font);
415 }
416 }
417 }
418
419 while self.script_i.0 < self.scripts.len() {
420 let script = self.scripts[self.script_i.0];
421
422 let script_families = fallbacks.script_fallback(script);
423
424 while self.script_i.1 < script_families.len() {
425 let script_family = script_families[self.script_i.1];
426 self.script_i.1 += 1;
427 for m_key in font_match_keys_iter(false) {
428 if self.face_contains_family(m_key.id, script_family) {
429 if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
430 return Some(font);
431 }
432 }
433 }
434 log::debug!(
435 "failed to find family '{}' for script {:?} and locale '{}'",
436 script_family,
437 script,
438 self.font_system.locale(),
439 );
440 }
441
442 self.script_i.0 += 1;
443 self.script_i.1 = 0;
444 }
445
446 let common_families = fallbacks.common_fallback();
447 while self.common_i < common_families.len() {
448 let common_family = common_families[self.common_i];
449 self.common_i += 1;
450 for m_key in font_match_keys_iter(false) {
451 if self.face_contains_family(m_key.id, common_family) {
452 if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
453 return Some(font);
454 }
455 }
456 }
457 log::debug!("failed to find family '{common_family}'");
458 }
459
460 let forbidden_families = fallbacks.forbidden_fallback();
463 while self.other_i < self.font_match_keys.len() {
464 let id = self.font_match_keys[self.other_i].id;
465 self.other_i += 1;
466 if forbidden_families
467 .iter()
468 .all(|family_name| !self.face_contains_family(id, family_name))
469 {
470 if let Some(font) = self.font_system.get_font(id, self.ideal_weight) {
471 return Some(font);
472 }
473 }
474 }
475
476 self.end = true;
477 None
478 }
479}
480
481impl Iterator for FontFallbackIter<'_> {
482 type Item = Arc<Font>;
483 fn next(&mut self) -> Option<Self::Item> {
484 let mut fallbacks = mem::take(&mut self.font_system.fallbacks);
485 let item = self.next_item(&fallbacks);
486 mem::swap(&mut fallbacks, &mut self.font_system.fallbacks);
487 item
488 }
489}