1use alloc::collections::BTreeSet;
14use alloc::string::{String, ToString};
15use alloc::vec::Vec;
16
17use crate::config::{FcFallbackConfig, GenericFamily};
18use crate::{
19 FcFontCache, FcFontCacheInner, FcPattern, FcWeight, FontId, FontMatch, FontMatchNoFallback,
20 PatternMatch, ResolvedFontRun, TraceMsg, UnicodeRange, DEFAULT_UNICODE_FALLBACK_SCRIPTS,
21};
22
23pub const MAX_FACES_PER_FAMILY: usize = 5;
26
27pub const MAX_AUTO_FALLBACKS_PER_SCRIPT: usize = 4;
30
31pub const UNICODE_FALLBACK_SOURCE: &str = "(unicode-fallback)";
34
35pub const LAST_RESORT_SOURCE: &str = "(last-resort)";
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ScriptFallbackGroup {
42 pub range: UnicodeRange,
44 pub fonts: Vec<FontMatch>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct CssFallbackGroup {
52 pub css_name: String,
54 pub fonts: Vec<FontMatch>,
56 pub script_fonts: Vec<ScriptFallbackGroup>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct FontFallbackChain {
63 pub css_fallbacks: Vec<CssFallbackGroup>,
65 pub unicode_fallbacks: Vec<ScriptFallbackGroup>,
68 pub last_resort: Vec<FontMatch>,
71 pub original_stack: Vec<String>,
73}
74
75impl FontFallbackChain {
76 pub fn empty(stack: &[String]) -> Self {
78 Self {
79 css_fallbacks: Vec::new(),
80 unicode_fallbacks: Vec::new(),
81 last_resort: Vec::new(),
82 original_stack: stack.to_vec(),
83 }
84 }
85
86 pub fn resolve_codepoint(&self, cp: u32) -> Option<(FontId, &str)> {
88 for group in &self.css_fallbacks {
89 for script in &group.script_fonts {
90 if script.range.start <= cp && cp <= script.range.end {
91 if let Some(m) = script.fonts.iter().find(|m| covers(&m.unicode_ranges, cp)) {
92 return Some((m.id, group.css_name.as_str()));
93 }
94 }
95 }
96 if let Some(m) = group.fonts.iter().find(|m| covers(&m.unicode_ranges, cp)) {
97 return Some((m.id, group.css_name.as_str()));
98 }
99 }
100 for script in &self.unicode_fallbacks {
101 if script.range.start <= cp && cp <= script.range.end {
102 if let Some(m) = script.fonts.iter().find(|m| covers(&m.unicode_ranges, cp)) {
103 return Some((m.id, UNICODE_FALLBACK_SOURCE));
104 }
105 }
106 }
107 self.last_resort.first().map(|m| (m.id, LAST_RESORT_SOURCE))
108 }
109
110 pub fn resolve_char(&self, _cache: &FcFontCache, ch: char) -> Option<(FontId, String)> {
113 self.resolve_codepoint(ch as u32)
114 .map(|(id, source)| (id, source.to_string()))
115 }
116
117 pub fn resolve_text(
119 &self,
120 cache: &FcFontCache,
121 text: &str,
122 ) -> Vec<(char, Option<(FontId, String)>)> {
123 text.chars()
124 .map(|ch| (ch, self.resolve_char(cache, ch)))
125 .collect()
126 }
127
128 pub fn query_for_text(&self, _cache: &FcFontCache, text: &str) -> Vec<ResolvedFontRun> {
132 let mut runs: Vec<ResolvedFontRun> = Vec::new();
133 let mut current: Option<(Option<FontId>, &str)> = None;
134 let mut run_start = 0usize;
135
136 for (byte_idx, ch) in text.char_indices() {
137 let resolved = match self.resolve_codepoint(ch as u32) {
138 Some((id, source)) => (Some(id), source),
139 None => (None, ""),
140 };
141 match current {
142 Some((font, _)) if font == resolved.0 => {}
143 Some((font, source)) => {
144 runs.push(ResolvedFontRun {
145 text: text[run_start..byte_idx].to_string(),
146 start_byte: run_start,
147 end_byte: byte_idx,
148 font_id: font,
149 css_source: source.to_string(),
150 });
151 run_start = byte_idx;
152 current = Some(resolved);
153 }
154 None => current = Some(resolved),
155 }
156 }
157
158 if let Some((font, source)) = current {
159 if run_start < text.len() {
160 runs.push(ResolvedFontRun {
161 text: text[run_start..].to_string(),
162 start_byte: run_start,
163 end_byte: text.len(),
164 font_id: font,
165 css_source: source.to_string(),
166 });
167 }
168 }
169
170 runs
171 }
172
173 pub fn fonts(&self) -> impl Iterator<Item = &FontMatch> {
175 let mut seen = BTreeSet::new();
176 self.css_fallbacks
177 .iter()
178 .flat_map(|g| {
179 g.script_fonts
180 .iter()
181 .flat_map(|s| s.fonts.iter())
182 .chain(g.fonts.iter())
183 })
184 .chain(self.unicode_fallbacks.iter().flat_map(|s| s.fonts.iter()))
185 .chain(self.last_resort.iter())
186 .filter(move |m| seen.insert(m.id))
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Hash)]
194pub(crate) struct FontChainCacheKey {
195 pub(crate) font_families: Vec<String>,
196 pub(crate) weight: FcWeight,
197 pub(crate) italic: PatternMatch,
198 pub(crate) oblique: PatternMatch,
199 pub(crate) scripts_hash: u64,
200}
201
202fn hash_scripts(ranges: &[UnicodeRange]) -> u64 {
203 let mut sorted: Vec<UnicodeRange> = ranges.to_vec();
204 sorted.sort();
205 sorted.dedup();
206 let mut buf = Vec::with_capacity(sorted.len() * 8);
207 for r in &sorted {
208 buf.extend_from_slice(&r.start.to_le_bytes());
209 buf.extend_from_slice(&r.end.to_le_bytes());
210 }
211 crate::utils::content_hash_u64(&buf)
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
216pub struct RankKey {
217 pub deficit: u32,
219 pub style: i32,
221 pub italic: u8,
223 pub dedication_inv: u64,
225 pub breadth: u64,
227 pub name: String,
229}
230
231impl RankKey {
232 pub fn for_block(
235 style: &FcPattern,
236 candidate: &FcPattern,
237 block: &UnicodeRange,
238 ) -> Option<Self> {
239 let overlap = overlap_size(&candidate.unicode_ranges, block);
240 if overlap == 0 {
241 return None;
242 }
243 let breadth = breadth(&candidate.unicode_ranges);
244 Some(Self {
245 deficit: range_width(block).saturating_sub(overlap),
246 style: FcFontCache::calculate_style_score(style, candidate),
247 italic: (candidate.italic == PatternMatch::True) as u8,
248 dedication_inv: breadth.saturating_mul(1024) / overlap as u64,
249 breadth,
250 name: candidate.name.clone().unwrap_or_default(),
251 })
252 }
253
254 pub fn for_style(style: &FcPattern, candidate: &FcPattern) -> Self {
256 Self {
257 deficit: 0,
258 style: FcFontCache::calculate_style_score(style, candidate),
259 italic: (candidate.italic == PatternMatch::True) as u8,
260 dedication_inv: 0,
261 breadth: breadth(&candidate.unicode_ranges),
262 name: candidate.name.clone().unwrap_or_default(),
263 }
264 }
265
266 pub fn for_request(
268 style: &FcPattern,
269 candidate: &FcPattern,
270 requested: &[UnicodeRange],
271 ) -> Self {
272 let (requested_width, overlap) = requested.iter().fold((0u32, 0u32), |(w, o), r| {
273 (
274 w.saturating_add(range_width(r)),
275 o.saturating_add(overlap_size(&candidate.unicode_ranges, r)),
276 )
277 });
278 Self {
279 deficit: 0,
280 style: FcFontCache::calculate_style_score(style, candidate),
281 italic: (candidate.italic == PatternMatch::True) as u8,
282 dedication_inv: requested_width.saturating_sub(overlap) as u64,
283 breadth: breadth(&candidate.unicode_ranges),
284 name: candidate.name.clone().unwrap_or_default(),
285 }
286 }
287}
288
289pub fn covers(ranges: &[UnicodeRange], cp: u32) -> bool {
291 let i = ranges.partition_point(|r| r.end < cp);
292 ranges.get(i).is_some_and(|r| r.start <= cp)
293}
294
295pub fn range_width(r: &UnicodeRange) -> u32 {
297 r.end.saturating_sub(r.start).saturating_add(1)
298}
299
300pub fn overlap_size(ranges: &[UnicodeRange], block: &UnicodeRange) -> u32 {
302 let mut total = 0u32;
303 let first = ranges.partition_point(|r| r.end < block.start);
304 for r in &ranges[first..] {
305 if r.start > block.end {
306 break;
307 }
308 let start = r.start.max(block.start);
309 let end = r.end.min(block.end);
310 if start <= end {
311 total = total.saturating_add(end - start + 1);
312 }
313 }
314 total.min(range_width(block))
315}
316
317pub fn breadth(ranges: &[UnicodeRange]) -> u64 {
319 ranges.iter().map(|r| range_width(r) as u64).sum()
320}
321
322fn style_request(weight: FcWeight, italic: PatternMatch, oblique: PatternMatch) -> FcPattern {
323 FcPattern {
324 weight,
325 italic,
326 oblique,
327 ..Default::default()
328 }
329}
330
331fn font_match(id: FontId, meta: &FcPattern) -> FontMatch {
332 FontMatch {
333 id,
334 unicode_ranges: meta.unicode_ranges.clone(),
335 fallbacks: Vec::new(),
336 }
337}
338
339fn ids(fonts: &[FontMatch]) -> impl Iterator<Item = FontId> + '_ {
340 fonts.iter().map(|m| m.id)
341}
342
343fn faces_for_family(
346 state: &FcFontCacheInner,
347 family: &str,
348 style: &FcPattern,
349 exclude: &BTreeSet<FontId>,
350) -> Vec<FontMatch> {
351 let key = crate::utils::normalize_family_name(family);
352 if key.is_empty() {
353 return Vec::new();
354 }
355 let Some(ids) = state.family_index.get(&key) else {
356 return Vec::new();
357 };
358 let mut ranked: Vec<(RankKey, FontId, &FcPattern)> = ids
359 .iter()
360 .filter(|id| !exclude.contains(id))
361 .filter_map(|id| {
362 let meta = state.metadata.get(id)?;
363 Some((RankKey::for_style(style, meta), *id, meta))
364 })
365 .collect();
366 ranked.sort();
367 ranked.truncate(MAX_FACES_PER_FAMILY);
368 ranked
369 .into_iter()
370 .map(|(_, id, meta)| font_match(id, meta))
371 .collect()
372}
373
374fn faces_for_families(
377 state: &FcFontCacheInner,
378 families: &[String],
379 style: &FcPattern,
380 exclude: &BTreeSet<FontId>,
381) -> Vec<FontMatch> {
382 let mut out: Vec<FontMatch> = Vec::new();
383 let mut seen = exclude.clone();
384 for family in families {
385 for m in faces_for_family(state, family, style, &seen) {
386 seen.insert(m.id);
387 out.push(m);
388 }
389 }
390 out
391}
392
393fn faces_unconfigured(state: &FcFontCacheInner, style: &FcPattern) -> Vec<FontMatch> {
395 let mut ranked: Vec<(RankKey, FontId, &FcPattern)> = state
396 .metadata
397 .iter()
398 .map(|(id, meta)| (RankKey::for_style(style, meta), *id, meta))
399 .collect();
400 ranked.sort();
401 ranked.truncate(MAX_FACES_PER_FAMILY);
402 ranked
403 .into_iter()
404 .map(|(_, id, meta)| font_match(id, meta))
405 .collect()
406}
407
408fn ranked_coverage_candidates(
411 state: &FcFontCacheInner,
412 block: &UnicodeRange,
413 style: &FcPattern,
414 exclude: &BTreeSet<FontId>,
415 limit: usize,
416) -> Vec<FontMatch> {
417 let mut ranked: Vec<(RankKey, FontId, &FcPattern)> = state
418 .metadata
419 .iter()
420 .filter(|(id, _)| !exclude.contains(id))
421 .filter_map(|(id, meta)| Some((RankKey::for_block(style, meta, block)?, *id, meta)))
422 .collect();
423 ranked.sort();
424 ranked.truncate(limit);
425 ranked
426 .into_iter()
427 .map(|(_, id, meta)| font_match(id, meta))
428 .collect()
429}
430
431pub(crate) fn build_chain(
434 state: &FcFontCacheInner,
435 config: &FcFallbackConfig,
436 stack: &[String],
437 style: &FcPattern,
438 scripts: &[UnicodeRange],
439) -> FontFallbackChain {
440 let mut css_base: BTreeSet<FontId> = BTreeSet::new();
441 let mut css_all: BTreeSet<FontId> = BTreeSet::new();
442 let mut generics_in_stack: Vec<GenericFamily> = Vec::new();
443 let mut css_fallbacks: Vec<CssFallbackGroup> = Vec::with_capacity(stack.len());
444
445 for family in stack {
446 match GenericFamily::from_css(family) {
447 Some(generic) => {
448 if !generics_in_stack.contains(&generic) {
449 generics_in_stack.push(generic);
450 }
451 let fonts =
452 faces_for_families(state, config.generic_candidates(generic), style, &css_base);
453 css_base.extend(ids(&fonts));
454 css_all.extend(ids(&fonts));
455
456 let mut script_fonts = Vec::new();
457 for block in scripts {
458 let names = config.script_candidates(Some(generic), block);
459 let fonts = faces_for_families(state, &names, style, &css_base);
460 if !fonts.is_empty() {
461 css_all.extend(ids(&fonts));
462 script_fonts.push(ScriptFallbackGroup {
463 range: *block,
464 fonts,
465 });
466 }
467 }
468 css_fallbacks.push(CssFallbackGroup {
469 css_name: family.clone(),
470 fonts,
471 script_fonts,
472 });
473 }
474 None => {
475 let mut fonts = faces_for_family(state, family, style, &css_base);
476 if fonts.is_empty() {
477 fonts = faces_for_families(
478 state,
479 config.substitutions_for(family),
480 style,
481 &css_base,
482 );
483 }
484 css_base.extend(ids(&fonts));
485 css_all.extend(ids(&fonts));
486 css_fallbacks.push(CssFallbackGroup {
487 css_name: family.clone(),
488 fonts,
489 script_fonts: Vec::new(),
490 });
491 }
492 }
493 }
494
495 if css_all.is_empty() {
497 for group in &mut css_fallbacks {
498 if GenericFamily::from_css(&group.css_name).is_some() {
499 group.fonts = faces_unconfigured(state, style);
500 css_all.extend(ids(&group.fonts));
501 }
502 }
503 }
504
505 let preference_generics: Vec<GenericFamily> = if generics_in_stack.is_empty() {
507 alloc::vec![config.default_generic]
508 } else {
509 generics_in_stack
510 };
511 let mut unicode_fallbacks: Vec<ScriptFallbackGroup> = Vec::new();
512 for block in scripts {
513 let mut fonts: Vec<FontMatch> = Vec::new();
514 let mut seen = css_all.clone();
515 let mut take = |found: Vec<FontMatch>, seen: &mut BTreeSet<FontId>| {
516 for m in found {
517 seen.insert(m.id);
518 fonts.push(m);
519 }
520 };
521 for generic in &preference_generics {
522 let names = config.script_candidates(Some(*generic), block);
523 take(faces_for_families(state, &names, style, &seen), &mut seen);
524 }
525 let names = config.script_candidates(None, block);
526 take(faces_for_families(state, &names, style, &seen), &mut seen);
527 take(
528 ranked_coverage_candidates(state, block, style, &seen, MAX_AUTO_FALLBACKS_PER_SCRIPT),
529 &mut seen,
530 );
531 if !fonts.is_empty() {
532 unicode_fallbacks.push(ScriptFallbackGroup {
533 range: *block,
534 fonts,
535 });
536 }
537 }
538
539 let last_resort = faces_for_families(state, &config.last_resort, style, &BTreeSet::new());
540
541 FontFallbackChain {
542 css_fallbacks,
543 unicode_fallbacks,
544 last_resort,
545 original_stack: stack.to_vec(),
546 }
547}
548
549impl FcFontCache {
550 pub fn resolve_font_chain(
552 &self,
553 font_families: &[String],
554 weight: FcWeight,
555 italic: PatternMatch,
556 oblique: PatternMatch,
557 trace: &mut Vec<TraceMsg>,
558 ) -> FontFallbackChain {
559 self.resolve_font_chain_with_scripts(font_families, weight, italic, oblique, None, trace)
560 }
561
562 pub fn resolve_font_chain_with_scripts(
564 &self,
565 font_families: &[String],
566 weight: FcWeight,
567 italic: PatternMatch,
568 oblique: PatternMatch,
569 scripts_hint: Option<&[UnicodeRange]>,
570 _trace: &mut Vec<TraceMsg>,
571 ) -> FontFallbackChain {
572 let scripts = scripts_hint.unwrap_or(DEFAULT_UNICODE_FALLBACK_SCRIPTS);
573 let key = FontChainCacheKey {
574 font_families: font_families.to_vec(),
575 weight,
576 italic,
577 oblique,
578 scripts_hash: hash_scripts(scripts),
579 };
580
581 {
582 let memo = match self.shared.chain_cache.lock() {
583 Ok(m) => m,
584 Err(e) => match e {},
585 };
586 if let Some(chain) = memo.get(&key) {
587 return chain.clone();
588 }
589 }
590
591 let chain = {
592 let state = self.state_read();
593 build_chain(
594 &state,
595 &state.fallback_config,
596 font_families,
597 &style_request(weight, italic, oblique),
598 scripts,
599 )
600 };
601
602 let mut memo = match self.shared.chain_cache.lock() {
603 Ok(m) => m,
604 Err(e) => match e {},
605 };
606 memo.insert(key, chain.clone());
607 chain
608 }
609
610 pub fn compute_fallbacks(
612 &self,
613 font_id: &FontId,
614 _trace: &mut Vec<TraceMsg>,
615 ) -> Vec<FontMatchNoFallback> {
616 let state = self.state_read();
617 let Some(pattern) = state.metadata.get(font_id) else {
618 return Vec::new();
619 };
620 let requested: &[UnicodeRange] = if pattern.unicode_ranges.is_empty() {
621 DEFAULT_UNICODE_FALLBACK_SCRIPTS
622 } else {
623 &pattern.unicode_ranges
624 };
625 let mut ranked: Vec<(RankKey, FontId, &FcPattern)> = state
626 .metadata
627 .iter()
628 .filter(|(id, meta)| {
629 *id != font_id
630 && requested
631 .iter()
632 .any(|r| overlap_size(&meta.unicode_ranges, r) > 0)
633 })
634 .map(|(id, meta)| (RankKey::for_request(pattern, meta, requested), *id, meta))
635 .collect();
636 ranked.sort();
637 ranked
638 .into_iter()
639 .map(|(_, id, meta)| FontMatchNoFallback {
640 id,
641 unicode_ranges: meta.unicode_ranges.clone(),
642 })
643 .collect()
644 }
645}