1use std::{
21 io::Read,
22 path::{Path, PathBuf},
23 sync::Arc,
24};
25
26use cranpose_ui::text::{FontFamily, FontFile, FontStyle, FontWeight};
27
28use crate::software_text_raster::{
29 FontFamilyKey, SoftwareTextFont, SoftwareTextFontError, SoftwareTextFontSet,
30 default_software_text_font,
31};
32
33pub const ANDROID_SYSTEM_FONT_DIR: &str = "/system/fonts";
35
36pub const DEFAULT_SYSTEM_FAMILY_WEIGHTS: &[FontWeight] =
39 &[FontWeight::NORMAL, FontWeight::MEDIUM, FontWeight::BOLD];
40
41#[derive(Debug, thiserror::Error)]
47pub enum FontLoadError {
48 #[error("font family declares no faces")]
49 EmptyFamily,
50 #[error("font family is not backed by files, so it has nothing to load")]
51 NotFileBacked,
52 #[error("no system font file for this family under {directory}")]
53 NoSystemFontFile { directory: PathBuf },
54 #[error("failed to read font file {path}: {source}")]
55 Read {
56 path: PathBuf,
57 #[source]
58 source: std::io::Error,
59 },
60 #[error("failed to parse font file {path}: {source}")]
61 Parse {
62 path: PathBuf,
63 #[source]
64 source: SoftwareTextFontError,
65 },
66 #[error("failed to parse font bytes: {source}")]
67 ParseBytes {
68 #[source]
69 source: SoftwareTextFontError,
70 },
71}
72
73#[derive(Clone, Default)]
79pub struct SoftwareTextFontRegistry {
80 faces: Vec<SoftwareTextFont>,
81 system_faces: Vec<(FontFamilyKey, FontWeight, FontStyle)>,
82}
83
84impl SoftwareTextFontRegistry {
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub fn register_family(&mut self, family: &FontFamily) -> Result<(), FontLoadError> {
99 let files = font_files_for(family)?;
100 if files.is_empty() {
101 return Err(FontLoadError::EmptyFamily);
102 }
103
104 let mut reads = FontFileReads::default();
105 let mut first_error = None;
106 let mut loaded = 0usize;
107 for file in &files {
108 match self.register_read_face(
109 &mut reads,
110 family,
111 file.weight,
112 file.style,
113 Path::new(&file.path),
114 ) {
115 Ok(()) => loaded += 1,
116 Err(error) => first_error = first_error.or(Some(error)),
117 }
118 }
119
120 match first_error {
121 Some(error) if loaded == 0 => Err(error),
122 _ => Ok(()),
123 }
124 }
125
126 fn register_read_face(
127 &mut self,
128 reads: &mut FontFileReads,
129 family: &FontFamily,
130 weight: FontWeight,
131 style: FontStyle,
132 path: &Path,
133 ) -> Result<(), FontLoadError> {
134 let bytes = reads.read(path)?;
135 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes.to_vec())
136 .map_err(|source| FontLoadError::Parse {
137 path: path.to_path_buf(),
138 source,
139 })?;
140 self.faces.push(face);
141 Ok(())
142 }
143
144 pub fn register_face_reader(
151 &mut self,
152 family: &FontFamily,
153 weight: FontWeight,
154 style: FontStyle,
155 reader: &mut impl Read,
156 ) -> Result<(), FontLoadError> {
157 let mut bytes = Vec::new();
158 reader
159 .read_to_end(&mut bytes)
160 .map_err(|source| FontLoadError::Read {
161 path: PathBuf::new(),
162 source,
163 })?;
164 self.register_face_bytes(family, weight, style, bytes)
165 }
166
167 pub fn register_face_bytes(
169 &mut self,
170 family: &FontFamily,
171 weight: FontWeight,
172 style: FontStyle,
173 bytes: impl Into<Vec<u8>>,
174 ) -> Result<(), FontLoadError> {
175 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes)
176 .map_err(|source| FontLoadError::ParseBytes { source })?;
177 self.faces.push(face);
178 Ok(())
179 }
180
181 pub fn register_fallback_bytes(
186 &mut self,
187 bytes: impl Into<Vec<u8>>,
188 ) -> Result<(), FontLoadError> {
189 let face = SoftwareTextFont::from_bytes(bytes)
190 .map_err(|source| FontLoadError::ParseBytes { source })?;
191 self.faces.push(face);
192 Ok(())
193 }
194
195 pub fn register_system_family(
212 &mut self,
213 directory: impl AsRef<Path>,
214 family: &FontFamily,
215 weights: &[FontWeight],
216 ) -> Result<(), FontLoadError> {
217 let directory = directory.as_ref();
218 let mut reads = FontFileReads::default();
219 let mut first_error = None;
220 let mut loaded = 0usize;
221 for weight in weights {
222 match self.register_read_system_face(
223 &mut reads,
224 directory,
225 family,
226 *weight,
227 FontStyle::Normal,
228 ) {
229 Ok(()) => loaded += 1,
230 Err(error) => first_error = first_error.or(Some(error)),
231 }
232 }
233
234 match first_error {
235 Some(error) if loaded == 0 => Err(error),
236 _ => Ok(()),
237 }
238 }
239
240 pub fn register_system_face(
249 &mut self,
250 directory: impl AsRef<Path>,
251 family: &FontFamily,
252 weight: FontWeight,
253 style: FontStyle,
254 ) -> Result<(), FontLoadError> {
255 self.register_read_system_face(
256 &mut FontFileReads::default(),
257 directory.as_ref(),
258 family,
259 weight,
260 style,
261 )
262 }
263
264 fn register_read_system_face(
265 &mut self,
266 reads: &mut FontFileReads,
267 directory: &Path,
268 family: &FontFamily,
269 weight: FontWeight,
270 style: FontStyle,
271 ) -> Result<(), FontLoadError> {
272 let weight = system_declared_weight(family, weight);
273 if self.has_system_face(family, weight, style) {
274 return Ok(());
275 }
276 let path = system_font_file(directory, family, weight).ok_or_else(|| {
277 FontLoadError::NoSystemFontFile {
278 directory: directory.to_path_buf(),
279 }
280 })?;
281 self.register_read_face(reads, family, weight, style, &path)?;
282 self.system_faces
283 .push((FontFamilyKey::of(family), weight, style));
284 Ok(())
285 }
286
287 fn has_system_face(&self, family: &FontFamily, weight: FontWeight, style: FontStyle) -> bool {
288 self.system_faces
289 .contains(&(FontFamilyKey::of(family), weight, style))
290 }
291
292 pub fn faces(&self) -> &[SoftwareTextFont] {
294 &self.faces
295 }
296
297 pub fn is_empty(&self) -> bool {
298 self.faces.is_empty()
299 }
300
301 pub fn into_font_set_or_default(mut self, fonts: &[&[u8]]) -> SoftwareTextFontSet {
309 for bytes in fonts {
310 let _ = self.register_fallback_bytes((*bytes).to_vec());
311 }
312 if self.faces.is_empty()
313 && let Some(default_font) = default_software_text_font()
314 {
315 self.faces.push(default_font);
316 }
317 SoftwareTextFontSet::from_faces(self.faces)
318 }
319}
320
321#[derive(Default)]
322struct FontFileReads {
323 entries: Vec<(PathBuf, Arc<[u8]>)>,
324}
325
326impl FontFileReads {
327 fn read(&mut self, path: &Path) -> Result<Arc<[u8]>, FontLoadError> {
328 if let Some((_, bytes)) = self.entries.iter().find(|(read, _)| read == path) {
329 return Ok(Arc::clone(bytes));
330 }
331 let bytes: Arc<[u8]> = std::fs::read(path)
332 .map_err(|source| FontLoadError::Read {
333 path: path.to_path_buf(),
334 source,
335 })?
336 .into();
337 self.entries.push((path.to_path_buf(), Arc::clone(&bytes)));
338 Ok(bytes)
339 }
340}
341
342pub fn system_declared_weight(family: &FontFamily, weight: FontWeight) -> FontWeight {
386 let Some(files) = system_family_files(family) else {
387 return weight;
388 };
389 closest_declared_weight(files.declared, weight).unwrap_or(weight)
390}
391
392fn closest_declared_weight(declared: &[u16], requested: FontWeight) -> Option<FontWeight> {
393 let mut best: Option<(u16, u16)> = None;
394 for candidate in declared {
395 let score = weight_match_score(*candidate, requested.value());
396 if best.is_none_or(|(_, best_score)| score < best_score) {
397 best = Some((*candidate, score));
398 }
399 }
400 best.map(|(candidate, _)| FontWeight(candidate))
401}
402
403fn weight_match_score(declared: u16, requested: u16) -> u16 {
404 (declared / 100).abs_diff(requested / 100)
405}
406
407pub fn system_font_file(
414 directory: &Path,
415 family: &FontFamily,
416 weight: FontWeight,
417) -> Option<PathBuf> {
418 let files = system_family_files(family)?;
419 files
420 .weighted
421 .iter()
422 .filter(|(candidate_weight, _)| *candidate_weight == weight.value())
423 .map(|(_, name)| directory.join(name))
424 .chain(files.regular.iter().map(|name| directory.join(name)))
425 .find(|path| path.is_file())
426}
427
428struct SystemFamilyFiles {
429 regular: &'static [&'static str],
430 weighted: &'static [(u16, &'static str)],
431 declared: &'static [u16],
432}
433
434const DECLARED_HUNDREDS: &[u16] = &[100, 200, 300, 400, 500, 600, 700, 800, 900];
435
436fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
437 match family {
438 FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
439 regular: &[
440 "Roboto-Regular.ttf",
441 "RobotoStatic-Regular.ttf",
442 "NotoSans-Regular.ttf",
443 "DroidSans.ttf",
444 ],
445 weighted: &[
446 (300, "Roboto-Light.ttf"),
447 (500, "Roboto-Medium.ttf"),
448 (700, "Roboto-Bold.ttf"),
449 (900, "Roboto-Black.ttf"),
450 ],
451 declared: DECLARED_HUNDREDS,
452 }),
453 FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
454 regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
455 weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
456 declared: &[400, 700],
457 }),
458 FontFamily::Monospace => Some(SystemFamilyFiles {
459 regular: &[
460 "DroidSansMono.ttf",
461 "RobotoMono-Regular.ttf",
462 "CutiveMono-Regular.ttf",
463 ],
464 weighted: &[(700, "RobotoMono-Bold.ttf")],
465 declared: &[400, 700],
466 }),
467 FontFamily::Cursive => Some(SystemFamilyFiles {
468 regular: &["DancingScript-Regular.ttf"],
469 weighted: &[(700, "DancingScript-Bold.ttf")],
470 declared: &[400, 700],
471 }),
472 FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
473 }
474}
475
476fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
477 match family {
478 FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
479 FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
480 _ => Err(FontLoadError::NotFileBacked),
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use std::io::Cursor;
487
488 use cranpose_ui::text::{SpanStyle, TextStyle};
489
490 use super::*;
491
492 const REGULAR: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
493 const BOLD: &[u8] = include_bytes!("../assets/NotoSansBold.ttf");
494
495 fn style_for(family: &FontFamily, weight: FontWeight) -> TextStyle {
496 TextStyle {
497 span_style: SpanStyle {
498 font_family: Some(family.clone()),
499 font_weight: Some(weight),
500 ..Default::default()
501 },
502 ..Default::default()
503 }
504 }
505
506 struct ScratchDir(PathBuf);
507
508 impl ScratchDir {
509 fn new(name: &str) -> Self {
510 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
511 .join("../../../target/test-output/cranpose-font-source")
512 .join(name);
513 let _ = std::fs::remove_dir_all(&path);
514 std::fs::create_dir_all(&path).expect("scratch directory");
515 Self(path)
516 }
517
518 fn write(&self, name: &str, bytes: &[u8]) -> PathBuf {
519 let path = self.0.join(name);
520 std::fs::write(&path, bytes).expect("scratch font file");
521 path
522 }
523
524 fn path(&self) -> &Path {
525 &self.0
526 }
527 }
528
529 impl Drop for ScratchDir {
530 fn drop(&mut self) {
531 let _ = std::fs::remove_dir_all(&self.0);
532 }
533 }
534
535 #[test]
536 fn register_family_picks_the_face_matching_the_requested_weight() {
537 let dir = ScratchDir::new("weights");
538 let regular = dir.write("Test-Regular.ttf", REGULAR);
539 let bold = dir.write("Test-Bold.ttf", BOLD);
540 let family = FontFamily::file_backed(vec![
541 FontFile::new(regular.to_string_lossy().into_owned()),
542 FontFile::new(bold.to_string_lossy().into_owned()).with_weight(FontWeight::BOLD),
543 ])
544 .expect("file-backed family");
545
546 let mut registry = SoftwareTextFontRegistry::new();
547 registry.register_family(&family).expect("family loads");
548 let fonts = registry.into_font_set_or_default(&[]);
549
550 let resolved_regular = fonts
551 .resolve(&style_for(&family, FontWeight::NORMAL))
552 .expect("regular face");
553 let resolved_bold = fonts
554 .resolve(&style_for(&family, FontWeight::BOLD))
555 .expect("bold face");
556
557 assert_eq!(resolved_regular.weight(), FontWeight::NORMAL);
558 assert_eq!(resolved_bold.weight(), FontWeight::BOLD);
559 assert_ne!(
560 resolved_regular.content_hash(),
561 resolved_bold.content_hash(),
562 "distinct faces must key the glyph atlas distinctly"
563 );
564 }
565
566 #[test]
567 fn register_family_honours_a_declared_weight_over_the_face_header() {
568 let dir = ScratchDir::new("declared");
569 let path = dir.write("Test-Regular.ttf", REGULAR);
570 let family = FontFamily::file_backed(vec![
571 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight::MEDIUM),
572 ])
573 .expect("file-backed family");
574
575 let mut registry = SoftwareTextFontRegistry::new();
576 registry.register_family(&family).expect("family loads");
577 let fonts = registry.into_font_set_or_default(&[]);
578
579 let resolved = fonts
580 .resolve(&style_for(&family, FontWeight::MEDIUM))
581 .expect("declared face");
582 assert_eq!(resolved.weight(), FontWeight::MEDIUM);
583 }
584
585 #[test]
586 fn register_family_reports_a_missing_file_without_panicking() {
587 let dir = ScratchDir::new("missing");
588 let family = FontFamily::file_backed(vec![FontFile::new(
589 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
590 )])
591 .expect("file-backed family");
592
593 let mut registry = SoftwareTextFontRegistry::new();
594 let error = registry
595 .register_family(&family)
596 .expect_err("a missing file cannot register");
597 assert!(matches!(error, FontLoadError::Read { .. }), "{error}");
598 assert!(registry.is_empty());
599 }
600
601 #[test]
602 fn register_family_reports_a_corrupt_file_without_panicking() {
603 let dir = ScratchDir::new("corrupt");
604 let path = dir.write("Corrupt.ttf", b"this is not a font");
605 let family =
606 FontFamily::file_backed(vec![FontFile::new(path.to_string_lossy().into_owned())])
607 .expect("file-backed family");
608
609 let mut registry = SoftwareTextFontRegistry::new();
610 let error = registry
611 .register_family(&family)
612 .expect_err("a corrupt file cannot register");
613 assert!(matches!(error, FontLoadError::Parse { .. }), "{error}");
614 assert!(registry.is_empty());
615 }
616
617 #[test]
618 #[cfg(feature = "embedded-default-font")]
619 fn a_family_that_failed_to_load_falls_back_to_the_default_face() {
620 let dir = ScratchDir::new("fallback");
621 let family = FontFamily::file_backed(vec![FontFile::new(
622 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
623 )])
624 .expect("file-backed family");
625
626 let mut registry = SoftwareTextFontRegistry::new();
627 let _ = registry.register_family(&family);
628 let fonts = registry.into_font_set_or_default(&[]);
629
630 let resolved = fonts
631 .resolve(&style_for(&family, FontWeight::NORMAL))
632 .expect("fallback face");
633 assert_eq!(
634 resolved.content_hash(),
635 fonts.default_font().expect("default face").content_hash()
636 );
637 }
638
639 #[test]
640 fn a_partly_loadable_family_keeps_the_faces_that_did_load() {
641 let dir = ScratchDir::new("partial");
642 let regular = dir.write("Test-Regular.ttf", REGULAR);
643 let family = FontFamily::file_backed(vec![
644 FontFile::new(regular.to_string_lossy().into_owned()),
645 FontFile::new(dir.path().join("Absent.ttf").to_string_lossy().into_owned())
646 .with_weight(FontWeight::BOLD),
647 ])
648 .expect("file-backed family");
649
650 let mut registry = SoftwareTextFontRegistry::new();
651 registry
652 .register_family(&family)
653 .expect("one readable face is enough");
654 assert_eq!(registry.faces().len(), 1);
655 }
656
657 #[test]
658 fn register_face_reader_accepts_a_font_that_is_not_a_file() {
659 let family = FontFamily::named("Bundled");
660 let mut registry = SoftwareTextFontRegistry::new();
661 registry
662 .register_face_reader(
663 &family,
664 FontWeight::NORMAL,
665 FontStyle::Normal,
666 &mut Cursor::new(REGULAR.to_vec()),
667 )
668 .expect("streamed face loads");
669
670 let fonts = registry.into_font_set_or_default(&[]);
671 let resolved = fonts
672 .resolve(&style_for(&family, FontWeight::NORMAL))
673 .expect("streamed face");
674 assert_eq!(resolved.registered_family(), {
675 let mut expected = SoftwareTextFontRegistry::new();
676 expected
677 .register_face_bytes(
678 &family,
679 FontWeight::NORMAL,
680 FontStyle::Normal,
681 REGULAR.to_vec(),
682 )
683 .expect("face loads");
684 expected.faces()[0].registered_family()
685 });
686 }
687
688 #[test]
689 #[cfg(feature = "embedded-default-font")]
690 fn an_empty_registry_falls_back_to_the_embedded_default_face() {
691 let fonts = SoftwareTextFontRegistry::new().into_font_set_or_default(&[]);
692 assert!(
693 fonts.default_font().is_some(),
694 "the embedded default font must still serve apps that supply nothing"
695 );
696 assert!(
697 fonts
698 .resolve(&TextStyle::default())
699 .is_some_and(|font| font.registered_family().is_none())
700 );
701 }
702
703 #[test]
704 fn system_font_file_prefers_a_weight_specific_static_face() {
705 let dir = ScratchDir::new("system-static");
706 dir.write("Roboto-Regular.ttf", REGULAR);
707 let medium = dir.write("Roboto-Medium.ttf", BOLD);
708
709 assert_eq!(
710 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
711 Some(medium)
712 );
713 }
714
715 #[test]
716 fn system_font_file_falls_back_to_the_regular_face_for_other_weights() {
717 let dir = ScratchDir::new("system-regular");
718 let regular = dir.write("Roboto-Regular.ttf", REGULAR);
719
720 assert_eq!(
721 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
722 Some(regular)
723 );
724 }
725
726 #[test]
727 fn system_font_file_reports_nothing_when_the_directory_is_empty() {
728 let dir = ScratchDir::new("system-empty");
729 assert_eq!(
730 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::NORMAL),
731 None
732 );
733 assert_eq!(
734 system_font_file(dir.path(), &FontFamily::named("Roboto"), FontWeight::NORMAL),
735 None
736 );
737 }
738
739 #[test]
740 fn register_system_family_binds_the_generic_alias_to_the_platform_face() {
741 let dir = ScratchDir::new("system-family");
742 dir.write("Roboto-Regular.ttf", REGULAR);
743 dir.write("Roboto-Bold.ttf", BOLD);
744
745 let mut registry = SoftwareTextFontRegistry::new();
746 registry
747 .register_system_family(
748 dir.path(),
749 &FontFamily::SansSerif,
750 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
751 )
752 .expect("system family loads");
753 let fonts = registry.into_font_set_or_default(&[]);
754
755 assert!(fonts.has_registered_family(&FontFamily::SansSerif));
756 let bold = fonts
757 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::BOLD))
758 .expect("bold system face");
759 assert_eq!(bold.weight(), FontWeight::BOLD);
760 assert_eq!(bold.registered_family(), {
761 fonts
762 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
763 .expect("regular system face")
764 .registered_family()
765 });
766 }
767
768 #[test]
769 fn register_system_family_reports_an_absent_font_directory() {
770 let mut registry = SoftwareTextFontRegistry::new();
771 let error = registry
772 .register_system_family(
773 Path::new("/definitely/not/a/font/directory"),
774 &FontFamily::SansSerif,
775 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
776 )
777 .expect_err("an absent directory cannot register");
778 assert!(
779 matches!(error, FontLoadError::NoSystemFontFile { .. }),
780 "{error}"
781 );
782 assert!(registry.is_empty());
783 }
784
785 #[test]
786 fn a_system_weight_the_font_config_does_not_declare_resolves_to_the_declared_one() {
787 for (requested, expected) in [(450u16, 400u16), (550, 500), (401, 400), (599, 500)] {
788 assert_eq!(
789 system_declared_weight(&FontFamily::SansSerif, FontWeight(requested)),
790 FontWeight(expected),
791 "sans-serif {requested}"
792 );
793 }
794 for weight in DECLARED_HUNDREDS {
795 assert_eq!(
796 system_declared_weight(&FontFamily::SansSerif, FontWeight(*weight)),
797 FontWeight(*weight)
798 );
799 }
800 assert_eq!(
801 system_declared_weight(&FontFamily::SansSerif, FontWeight(50)),
802 FontWeight(100)
803 );
804 assert_eq!(
805 system_declared_weight(&FontFamily::SansSerif, FontWeight(1000)),
806 FontWeight(900)
807 );
808 }
809
810 #[test]
811 fn a_sparse_system_family_resolves_by_distance_and_keeps_the_lighter_face_on_a_tie() {
812 for (requested, expected) in [(500u16, 400u16), (550, 400), (600, 700), (650, 700)] {
813 assert_eq!(
814 system_declared_weight(&FontFamily::Serif, FontWeight(requested)),
815 FontWeight(expected),
816 "serif {requested}"
817 );
818 }
819 assert_eq!(
820 closest_declared_weight(&[300, 500], FontWeight(400)),
821 Some(FontWeight(300))
822 );
823 assert_eq!(
824 closest_declared_weight(&[500, 300], FontWeight(400)),
825 Some(FontWeight(500)),
826 "declaration order, not magnitude, is what breaks the tie"
827 );
828 }
829
830 #[test]
831 fn a_family_with_no_system_files_keeps_the_weight_it_was_given() {
832 assert_eq!(
833 system_declared_weight(&FontFamily::named("Roboto"), FontWeight(450)),
834 FontWeight(450)
835 );
836 }
837
838 #[test]
839 fn register_system_face_registers_the_declared_weight_not_the_requested_one() {
840 let dir = ScratchDir::new("system-undeclared-weight");
841 dir.write("Roboto-Regular.ttf", REGULAR);
842
843 let mut registry = SoftwareTextFontRegistry::new();
844 registry
845 .register_system_face(
846 dir.path(),
847 &FontFamily::SansSerif,
848 FontWeight(450),
849 FontStyle::Normal,
850 )
851 .expect("system face loads");
852 let fonts = registry.into_font_set_or_default(&[]);
853
854 let resolved = fonts
855 .resolve(&style_for(&FontFamily::SansSerif, FontWeight(450)))
856 .expect("a face for the off-grid request");
857 assert_eq!(
858 resolved.weight(),
859 FontWeight::NORMAL,
860 "450 must land on the 400 entry Android's matcher returns"
861 );
862
863 let mut declared = SoftwareTextFontRegistry::new();
864 declared
865 .register_system_face(
866 dir.path(),
867 &FontFamily::SansSerif,
868 FontWeight::NORMAL,
869 FontStyle::Normal,
870 )
871 .expect("system face loads");
872 let declared = declared.into_font_set_or_default(&[]);
873 assert_eq!(
874 resolved.content_hash(),
875 declared
876 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
877 .expect("the 400 face")
878 .content_hash(),
879 "the off-grid request must produce the very same face, glyph masks included"
880 );
881 }
882
883 #[test]
884 fn system_requests_that_resolve_alike_register_one_face() {
885 let dir = ScratchDir::new("system-dedupe");
886 dir.write("Roboto-Regular.ttf", REGULAR);
887
888 let mut registry = SoftwareTextFontRegistry::new();
889 for weight in [400u16, 450, 499] {
890 registry
891 .register_system_face(
892 dir.path(),
893 &FontFamily::SansSerif,
894 FontWeight(weight),
895 FontStyle::Normal,
896 )
897 .expect("system face loads");
898 }
899
900 assert_eq!(
901 registry.faces().len(),
902 1,
903 "three requests for one declared entry are one face"
904 );
905 }
906
907 #[test]
908 fn an_app_registered_face_keeps_the_weight_it_declares() {
909 let dir = ScratchDir::new("app-off-grid");
910 let path = dir.write("Test-Regular.ttf", REGULAR);
911 let family = FontFamily::file_backed(vec![
912 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight(450)),
913 ])
914 .expect("file-backed family");
915
916 let mut registry = SoftwareTextFontRegistry::new();
917 registry.register_family(&family).expect("family loads");
918 let fonts = registry.into_font_set_or_default(&[]);
919
920 assert_eq!(
921 fonts
922 .resolve(&style_for(&family, FontWeight(450)))
923 .expect("app face")
924 .weight(),
925 FontWeight(450)
926 );
927 }
928
929 #[test]
930 fn register_family_rejects_a_family_that_names_no_files() {
931 let mut registry = SoftwareTextFontRegistry::new();
932 let error = registry
933 .register_family(&FontFamily::named("Roboto"))
934 .expect_err("a named family has nothing to read");
935 assert!(matches!(error, FontLoadError::NotFileBacked), "{error}");
936 }
937
938 #[test]
939 fn loaded_typeface_families_register_their_single_file() {
940 let dir = ScratchDir::new("typeface");
941 let path = dir.write("Test-Regular.ttf", REGULAR);
942 let family = FontFamily::loaded_typeface_path(path.to_string_lossy().into_owned());
943
944 let mut registry = SoftwareTextFontRegistry::new();
945 registry.register_family(&family).expect("typeface loads");
946 let fonts = registry.into_font_set_or_default(&[]);
947
948 assert!(fonts.has_registered_family(&family));
949 assert!(
950 fonts
951 .resolve(&style_for(&family, FontWeight::NORMAL))
952 .is_some_and(|font| font.registered_family().is_some())
953 );
954 }
955}