1use std::collections::BTreeMap;
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
12pub enum FontStyle {
13 Normal,
14 Italic,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FontSource {
26 Bundled,
28 Project,
30 Local,
33}
34
35#[derive(Clone)]
37pub struct FontData {
38 pub id: String,
40 pub bytes: Arc<[u8]>,
42 pub index: u32,
44 pub source: FontSource,
46}
47
48impl std::fmt::Debug for FontData {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("FontData")
51 .field("id", &self.id)
52 .field("bytes_len", &self.bytes.len())
53 .field("index", &self.index)
54 .field("source", &self.source)
55 .finish()
56 }
57}
58
59pub trait FontProvider {
63 #[must_use]
72 fn resolve(&self, families: &[String], weight: u16, style: FontStyle) -> Option<FontData>;
73
74 #[must_use]
76 fn by_id(&self, id: &str) -> Option<FontData>;
77
78 #[must_use]
80 fn all_faces(&self) -> Vec<FontData>;
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
85struct FaceKey {
86 family_lower: String,
87 weight: u16,
88 style: FontStyle,
89}
90
91pub struct BytesFontProvider {
98 by_key: BTreeMap<FaceKey, FontData>,
99 by_id: BTreeMap<String, FontData>,
100}
101
102impl std::fmt::Debug for BytesFontProvider {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 let ids: Vec<&str> = self.by_id.keys().map(String::as_str).collect();
105 f.debug_struct("BytesFontProvider")
106 .field("registered_faces", &ids)
107 .finish()
108 }
109}
110
111impl BytesFontProvider {
112 #[must_use]
114 pub fn new() -> Self {
115 Self {
116 by_key: BTreeMap::new(),
117 by_id: BTreeMap::new(),
118 }
119 }
120
121 pub fn register(
136 &mut self,
137 family: &str,
138 weight: u16,
139 style: FontStyle,
140 bytes: Arc<[u8]>,
141 index: u32,
142 source: FontSource,
143 ) -> String {
144 let family_lower = family.to_lowercase();
145 let family_kebab = family_lower.replace(' ', "-");
146 let style_str = match style {
147 FontStyle::Normal => "normal",
148 FontStyle::Italic => "italic",
149 };
150 let base_id = format!("{family_kebab}-{weight}-{style_str}");
151
152 let key = FaceKey {
153 family_lower,
154 weight,
155 style,
156 };
157
158 let id = match self.by_key.get(&key) {
161 Some(existing) => existing.id.clone(),
162 None => {
163 let mut candidate = base_id.clone();
164 let mut n = 2u32;
165 while self.by_id.contains_key(&candidate) {
166 candidate = format!("{base_id}-{n}");
167 n += 1;
168 }
169 candidate
170 }
171 };
172
173 let data = FontData {
174 id: id.clone(),
175 bytes,
176 index,
177 source,
178 };
179
180 self.by_key.insert(key, data.clone());
181 self.by_id.insert(id.clone(), data);
182
183 id
184 }
185
186 #[must_use]
188 pub fn available_families(&self) -> Vec<String> {
189 let mut families: Vec<String> =
190 self.by_key.keys().map(|k| k.family_lower.clone()).collect();
191 families.dedup();
192 families
193 }
194}
195
196impl Default for BytesFontProvider {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl FontProvider for BytesFontProvider {
203 fn resolve(&self, families: &[String], weight: u16, style: FontStyle) -> Option<FontData> {
204 for family in families {
205 let family_lower = family.to_lowercase();
206
207 let exact_key = FaceKey {
209 family_lower: family_lower.clone(),
210 weight,
211 style,
212 };
213 if let Some(data) = self.by_key.get(&exact_key) {
214 return Some(data.clone());
215 }
216
217 let fallback = self
219 .by_key
220 .range(
221 FaceKey {
222 family_lower: family_lower.clone(),
223 weight: 0,
224 style: FontStyle::Normal,
225 }..,
226 )
227 .find(|(k, _)| k.family_lower == family_lower)
228 .map(|(_, v)| v.clone());
229
230 if fallback.is_some() {
231 return fallback;
232 }
233 }
234 None
235 }
236
237 fn by_id(&self, id: &str) -> Option<FontData> {
238 self.by_id.get(id).cloned()
239 }
240
241 fn all_faces(&self) -> Vec<FontData> {
242 self.by_id.values().cloned().collect()
243 }
244}
245
246#[must_use]
268pub fn default_provider() -> BytesFontProvider {
269 let sans: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_REGULAR);
270 let sans_bold: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_BOLD);
271 let sans_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_ITALIC);
272 let sans_bold_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_BOLD_ITALIC);
273 let serif: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_REGULAR);
274 let serif_bold: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_BOLD);
275 let serif_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_ITALIC);
276 let serif_bold_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_BOLD_ITALIC);
277 let mono: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_MONO_REGULAR);
278 let mono_bold: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_MONO_BOLD);
279 let mut provider = BytesFontProvider::new();
280 let b = FontSource::Bundled;
281 provider.register("Noto Sans", 400, FontStyle::Normal, sans, 0, b);
282 provider.register("Noto Sans", 700, FontStyle::Normal, sans_bold, 0, b);
283 provider.register("Noto Sans", 400, FontStyle::Italic, sans_italic, 0, b);
284 provider.register("Noto Sans", 700, FontStyle::Italic, sans_bold_italic, 0, b);
285 provider.register("Noto Serif", 400, FontStyle::Normal, serif, 0, b);
286 provider.register("Noto Serif", 700, FontStyle::Normal, serif_bold, 0, b);
287 provider.register("Noto Serif", 400, FontStyle::Italic, serif_italic, 0, b);
288 provider.register(
289 "Noto Serif",
290 700,
291 FontStyle::Italic,
292 serif_bold_italic,
293 0,
294 b,
295 );
296 provider.register("Noto Sans Mono", 400, FontStyle::Normal, mono, 0, b);
297 provider.register("Noto Sans Mono", 700, FontStyle::Normal, mono_bold, 0, b);
298 provider
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 fn is_valid_tt_header(bytes: &[u8]) -> bool {
307 bytes.len() > 1000 && bytes.starts_with(&[0x00, 0x01, 0x00, 0x00])
308 }
309
310 #[test]
311 fn default_provider_resolves_noto_sans() {
312 let p = default_provider();
313 let result = p.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal);
314 assert!(result.is_some(), "expected Some for Noto Sans 400 Normal");
315 let data = result.unwrap();
316 assert!(
317 is_valid_tt_header(&data.bytes),
318 "expected TrueType header and len > 1000, got len={}",
319 data.bytes.len()
320 );
321 }
322
323 #[test]
324 fn default_provider_resolves_noto_serif_matrix() {
325 let p = default_provider();
326 for (weight, style) in [
327 (400, FontStyle::Normal),
328 (700, FontStyle::Normal),
329 (400, FontStyle::Italic),
330 (700, FontStyle::Italic),
331 ] {
332 let result = p.resolve(&["Noto Serif".to_string()], weight, style);
333 assert!(
334 result.is_some(),
335 "expected Some for Noto Serif {weight} {style:?}"
336 );
337 let data = result.unwrap();
338 assert_eq!(data.source, FontSource::Bundled, "serif must be bundled");
339 assert!(
340 is_valid_tt_header(&data.bytes),
341 "expected TrueType header for Noto Serif {weight} {style:?}"
342 );
343 }
344 }
345
346 #[test]
347 fn default_provider_resolves_noto_sans_mono() {
348 let p = default_provider();
349 let result = p.resolve(&["Noto Sans Mono".to_string()], 400, FontStyle::Normal);
350 assert!(
351 result.is_some(),
352 "expected Some for Noto Sans Mono 400 Normal"
353 );
354 let data = result.unwrap();
355 assert!(
356 is_valid_tt_header(&data.bytes),
357 "expected TrueType header and len > 1000, got len={}",
358 data.bytes.len()
359 );
360 assert!(
361 data.id.contains("noto-sans-mono"),
362 "id should contain noto-sans-mono, got {}",
363 data.id
364 );
365 }
366
367 #[test]
368 fn default_provider_distinguishes_sans_and_mono() {
369 let p = default_provider();
372 let sans = p
373 .resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
374 .expect("sans resolves");
375 let mono = p
376 .resolve(&["Noto Sans Mono".to_string()], 400, FontStyle::Normal)
377 .expect("mono resolves");
378 assert_ne!(sans.id, mono.id, "sans and mono must have distinct ids");
379 assert_ne!(
380 sans.bytes.len(),
381 mono.bytes.len(),
382 "sans and mono must be different font files"
383 );
384 }
385
386 #[test]
387 fn case_insensitive_family_lookup() {
388 let p = default_provider();
389 let lower = p.resolve(&["noto sans".to_string()], 400, FontStyle::Normal);
390 let mixed = p.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal);
391 assert!(lower.is_some(), "lowercase family should resolve");
392 assert!(mixed.is_some(), "mixed-case family should resolve");
393 assert_eq!(lower.unwrap().id, mixed.unwrap().id);
394 }
395
396 #[test]
397 fn weight_fallback_resolves_unregistered_weight() {
398 let p = default_provider();
399 let result = p.resolve(&["Noto Sans".to_string()], 900, FontStyle::Normal);
401 assert!(
402 result.is_some(),
403 "weight 900 should fall back to a registered face"
404 );
405 let data = result.unwrap();
406 assert!(data.id.contains("noto-sans"), "id should contain noto-sans");
407 }
408
409 #[test]
410 fn bold_italic_resolves_distinct_combined_face() {
411 let p = default_provider();
414 let bold = p
415 .resolve(&["Noto Sans".to_string()], 700, FontStyle::Normal)
416 .expect("bold resolves");
417 let italic = p
418 .resolve(&["Noto Sans".to_string()], 400, FontStyle::Italic)
419 .expect("italic resolves");
420 let bold_italic = p
421 .resolve(&["Noto Sans".to_string()], 700, FontStyle::Italic)
422 .expect("bold-italic resolves");
423 assert!(
424 bold_italic.id.contains("700") && bold_italic.id.contains("italic"),
425 "bold-italic id should encode both 700 and italic, got {}",
426 bold_italic.id
427 );
428 assert_ne!(bold_italic.id, bold.id, "must differ from bold-upright");
429 assert_ne!(bold_italic.id, italic.id, "must differ from regular-italic");
430 }
431
432 #[test]
433 fn italic_style_resolves_distinct_italic_face() {
434 let p = default_provider();
437 let normal = p
438 .resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
439 .expect("normal resolves");
440 let italic = p
441 .resolve(&["Noto Sans".to_string()], 400, FontStyle::Italic)
442 .expect("italic resolves");
443 assert!(
444 italic.id.contains("italic"),
445 "italic id should encode the italic style, got {}",
446 italic.id
447 );
448 assert_ne!(
449 normal.id, italic.id,
450 "normal and italic must have distinct ids"
451 );
452 assert_ne!(
453 normal.bytes.len(),
454 italic.bytes.len(),
455 "normal and italic must be different font files"
456 );
457 }
458
459 #[test]
460 fn bold_weight_resolves_distinct_bold_face() {
461 let p = default_provider();
464 let regular = p
465 .resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
466 .expect("regular resolves");
467 let bold = p
468 .resolve(&["Noto Sans".to_string()], 700, FontStyle::Normal)
469 .expect("bold resolves");
470 assert!(
471 bold.id.contains("noto-sans-700"),
472 "bold id should encode weight 700, got {}",
473 bold.id
474 );
475 assert_ne!(
476 regular.id, bold.id,
477 "regular and bold must have distinct ids"
478 );
479 assert_ne!(
480 regular.bytes.len(),
481 bold.bytes.len(),
482 "regular and bold must be different font files"
483 );
484 }
485
486 #[test]
487 fn mono_bold_weight_resolves_distinct_bold_face() {
488 let p = default_provider();
491 let mono_regular = p
492 .resolve(&["Noto Sans Mono".to_string()], 400, FontStyle::Normal)
493 .expect("mono regular resolves");
494 let mono_bold = p
495 .resolve(&["Noto Sans Mono".to_string()], 700, FontStyle::Normal)
496 .expect("mono bold resolves");
497 assert!(
498 mono_bold.id.contains("noto-sans-mono-700"),
499 "mono bold id should encode weight 700, got {}",
500 mono_bold.id
501 );
502 assert_ne!(
503 mono_regular.id, mono_bold.id,
504 "mono regular and mono bold must have distinct ids"
505 );
506 assert_ne!(
507 mono_regular.bytes.len(),
508 mono_bold.bytes.len(),
509 "mono regular and mono bold must be different font files"
510 );
511 }
512
513 #[test]
514 fn unknown_family_returns_none() {
515 let p = default_provider();
516 let result = p.resolve(&["Nonexistent".to_string()], 400, FontStyle::Normal);
517 assert!(result.is_none(), "unknown family must return None");
518 }
519
520 #[test]
521 fn by_id_roundtrip() {
522 let p = default_provider();
523 let resolved = p
524 .resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
525 .expect("should resolve");
526 let by_id = p
527 .by_id(&resolved.id)
528 .expect("by_id should return same face");
529 assert_eq!(resolved.id, by_id.id);
530 assert_eq!(resolved.bytes.len(), by_id.bytes.len());
531 }
532
533 #[test]
534 fn by_id_unknown_returns_none() {
535 let p = default_provider();
536 assert!(p.by_id("no-such-font-0-normal").is_none());
537 }
538
539 #[test]
540 fn manual_register_and_resolve() {
541 let mut p = BytesFontProvider::new();
542 let dummy_bytes: Arc<[u8]> = Arc::from(vec![0u8; 64].as_slice());
543 let id = p.register(
544 "Test Family",
545 400,
546 FontStyle::Normal,
547 dummy_bytes.clone(),
548 0,
549 FontSource::Project,
550 );
551 assert_eq!(id, "test-family-400-normal");
552
553 let resolved = p.resolve(&["Test Family".to_string()], 400, FontStyle::Normal);
554 assert!(resolved.is_some());
555 assert_eq!(resolved.unwrap().id, "test-family-400-normal");
556 }
557
558 #[test]
559 fn stable_id_format() {
560 let mut p = BytesFontProvider::new();
561 let bytes: Arc<[u8]> = Arc::from(vec![0u8; 4].as_slice());
562 let id = p.register(
563 "My Font",
564 700,
565 FontStyle::Italic,
566 bytes,
567 0,
568 FontSource::Local,
569 );
570 assert_eq!(id, "my-font-700-italic");
571 }
572
573 #[test]
574 fn re_registering_same_face_reuses_id() {
575 let mut p = BytesFontProvider::new();
576 let bytes: Arc<[u8]> = Arc::from(vec![1u8; 8].as_slice());
577 let id1 = p.register(
578 "Inter",
579 400,
580 FontStyle::Normal,
581 bytes.clone(),
582 0,
583 FontSource::Project,
584 );
585 let id2 = p.register(
586 "Inter",
587 400,
588 FontStyle::Normal,
589 bytes,
590 0,
591 FontSource::Project,
592 );
593 assert_eq!(id1, id2, "same face re-registration keeps a stable id");
594 }
595
596 #[test]
597 fn kebab_colliding_families_get_distinct_ids() {
598 let mut p = BytesFontProvider::new();
601 let a: Arc<[u8]> = Arc::from(vec![0xAAu8; 4].as_slice());
602 let b: Arc<[u8]> = Arc::from(vec![0xBBu8; 4].as_slice());
603 let id_a = p.register("My Font", 400, FontStyle::Normal, a, 0, FontSource::Local);
604 let id_b = p.register("my-font", 400, FontStyle::Normal, b, 0, FontSource::Local);
605 assert_eq!(id_a, "my-font-400-normal");
606 assert_ne!(id_a, id_b, "colliding families must not share an id");
607 assert_eq!(p.by_id(&id_a).unwrap().bytes[0], 0xAA);
609 assert_eq!(p.by_id(&id_b).unwrap().bytes[0], 0xBB);
610 }
611
612 #[test]
613 fn resolve_carries_registered_source() {
614 let mut p = BytesFontProvider::new();
618 let bytes: Arc<[u8]> = Arc::from(vec![0u8; 8].as_slice());
619 p.register(
620 "Local Face",
621 400,
622 FontStyle::Normal,
623 bytes,
624 0,
625 FontSource::Local,
626 );
627 let local = p
628 .resolve(&["Local Face".to_string()], 400, FontStyle::Normal)
629 .expect("local face resolves");
630 assert_eq!(local.source, FontSource::Local);
631
632 let bundled = default_provider()
633 .resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
634 .expect("bundled face resolves");
635 assert_eq!(bundled.source, FontSource::Bundled);
636 }
637
638 #[test]
639 fn default_provider_faces_are_all_bundled() {
640 let p = default_provider();
643 for face in p.all_faces() {
644 assert_eq!(
645 face.source,
646 FontSource::Bundled,
647 "default provider face {} must be Bundled",
648 face.id
649 );
650 }
651 }
652}