1use std::io::Read;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use cranpose_ui::text::{FontFamily, FontFile, FontStyle, FontWeight};
25
26use crate::software_text_raster::{
27 default_software_text_font, FontFamilyKey, SoftwareTextFont, SoftwareTextFontError,
28 SoftwareTextFontSet,
29};
30
31pub const ANDROID_SYSTEM_FONT_DIR: &str = "/system/fonts";
33
34pub const DEFAULT_SYSTEM_FAMILY_WEIGHTS: &[FontWeight] =
37 &[FontWeight::NORMAL, FontWeight::MEDIUM, FontWeight::BOLD];
38
39#[derive(Debug, thiserror::Error)]
45pub enum FontLoadError {
46 #[error("font family declares no faces")]
47 EmptyFamily,
48 #[error("font family is not backed by files, so it has nothing to load")]
49 NotFileBacked,
50 #[error("no system font file for this family under {directory}")]
51 NoSystemFontFile { directory: PathBuf },
52 #[error("failed to read font file {path}: {source}")]
53 Read {
54 path: PathBuf,
55 #[source]
56 source: std::io::Error,
57 },
58 #[error("failed to parse font file {path}: {source}")]
59 Parse {
60 path: PathBuf,
61 #[source]
62 source: SoftwareTextFontError,
63 },
64 #[error("failed to parse font bytes: {source}")]
65 ParseBytes {
66 #[source]
67 source: SoftwareTextFontError,
68 },
69}
70
71#[derive(Clone, Default)]
77pub struct SoftwareTextFontRegistry {
78 faces: Vec<SoftwareTextFont>,
79 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);
277 if self.has_system_face(family, weight, style) {
278 return Ok(());
283 }
284 let path = system_font_file(directory, family, weight).ok_or_else(|| {
285 FontLoadError::NoSystemFontFile {
286 directory: directory.to_path_buf(),
287 }
288 })?;
289 self.register_read_face(reads, family, weight, style, &path)?;
290 self.system_faces
291 .push((FontFamilyKey::of(family), weight, style));
292 Ok(())
293 }
294
295 fn has_system_face(&self, family: &FontFamily, weight: FontWeight, style: FontStyle) -> bool {
301 self.system_faces
302 .contains(&(FontFamilyKey::of(family), weight, style))
303 }
304
305 pub fn faces(&self) -> &[SoftwareTextFont] {
307 &self.faces
308 }
309
310 pub fn is_empty(&self) -> bool {
311 self.faces.is_empty()
312 }
313
314 pub fn into_font_set_or_default(mut self, fonts: &[&[u8]]) -> SoftwareTextFontSet {
322 for bytes in fonts {
323 let _ = self.register_fallback_bytes((*bytes).to_vec());
325 }
326 if self.faces.is_empty() {
327 if let Some(default_font) = default_software_text_font() {
328 self.faces.push(default_font);
329 }
330 }
331 SoftwareTextFontSet::from_faces(self.faces)
332 }
333}
334
335#[derive(Default)]
342struct FontFileReads {
343 entries: Vec<(PathBuf, Arc<[u8]>)>,
344}
345
346impl FontFileReads {
347 fn read(&mut self, path: &Path) -> Result<Arc<[u8]>, FontLoadError> {
348 if let Some((_, bytes)) = self.entries.iter().find(|(read, _)| read == path) {
349 return Ok(Arc::clone(bytes));
350 }
351 let bytes: Arc<[u8]> = std::fs::read(path)
352 .map_err(|source| FontLoadError::Read {
353 path: path.to_path_buf(),
354 source,
355 })?
356 .into();
357 self.entries.push((path.to_path_buf(), Arc::clone(&bytes)));
358 Ok(bytes)
359 }
360}
361
362pub fn system_declared_weight(family: &FontFamily, weight: FontWeight) -> FontWeight {
406 let Some(files) = system_family_files(family) else {
407 return weight;
408 };
409 closest_declared_weight(files.declared, weight).unwrap_or(weight)
410}
411
412fn closest_declared_weight(declared: &[u16], requested: FontWeight) -> Option<FontWeight> {
414 let mut best: Option<(u16, u16)> = None;
415 for candidate in declared {
416 let score = weight_match_score(*candidate, requested.value());
417 if best.is_none_or(|(_, best_score)| score < best_score) {
419 best = Some((*candidate, score));
420 }
421 }
422 best.map(|(candidate, _)| FontWeight(candidate))
423}
424
425fn weight_match_score(declared: u16, requested: u16) -> u16 {
427 (declared / 100).abs_diff(requested / 100)
428}
429
430pub fn system_font_file(
437 directory: &Path,
438 family: &FontFamily,
439 weight: FontWeight,
440) -> Option<PathBuf> {
441 let files = system_family_files(family)?;
442 files
443 .weighted
444 .iter()
445 .filter(|(candidate_weight, _)| *candidate_weight == weight.value())
446 .map(|(_, name)| directory.join(name))
447 .chain(files.regular.iter().map(|name| directory.join(name)))
448 .find(|path| path.is_file())
449}
450
451struct SystemFamilyFiles {
454 regular: &'static [&'static str],
455 weighted: &'static [(u16, &'static str)],
456 declared: &'static [u16],
461}
462
463const DECLARED_HUNDREDS: &[u16] = &[100, 200, 300, 400, 500, 600, 700, 800, 900];
467
468fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
469 match family {
473 FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
474 regular: &[
475 "Roboto-Regular.ttf",
476 "RobotoStatic-Regular.ttf",
477 "NotoSans-Regular.ttf",
478 "DroidSans.ttf",
479 ],
480 weighted: &[
481 (300, "Roboto-Light.ttf"),
482 (500, "Roboto-Medium.ttf"),
483 (700, "Roboto-Bold.ttf"),
484 (900, "Roboto-Black.ttf"),
485 ],
486 declared: DECLARED_HUNDREDS,
487 }),
488 FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
490 regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
491 weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
492 declared: &[400, 700],
493 }),
494 FontFamily::Monospace => Some(SystemFamilyFiles {
495 regular: &[
496 "DroidSansMono.ttf",
497 "RobotoMono-Regular.ttf",
498 "CutiveMono-Regular.ttf",
499 ],
500 weighted: &[(700, "RobotoMono-Bold.ttf")],
501 declared: &[400, 700],
502 }),
503 FontFamily::Cursive => Some(SystemFamilyFiles {
504 regular: &["DancingScript-Regular.ttf"],
505 weighted: &[(700, "DancingScript-Bold.ttf")],
506 declared: &[400, 700],
507 }),
508 FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
509 }
510}
511
512fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
513 match family {
514 FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
515 FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
516 _ => Err(FontLoadError::NotFileBacked),
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use cranpose_ui::text::{SpanStyle, TextStyle};
524 use std::io::Cursor;
525
526 const REGULAR: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
527 const BOLD: &[u8] = include_bytes!("../assets/NotoSansBold.ttf");
528
529 fn style_for(family: &FontFamily, weight: FontWeight) -> TextStyle {
530 TextStyle {
531 span_style: SpanStyle {
532 font_family: Some(family.clone()),
533 font_weight: Some(weight),
534 ..Default::default()
535 },
536 ..Default::default()
537 }
538 }
539
540 struct ScratchDir(PathBuf);
544
545 impl ScratchDir {
546 fn new(name: &str) -> Self {
547 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
548 .join("../../../target/test-output/cranpose-font-source")
549 .join(name);
550 let _ = std::fs::remove_dir_all(&path);
551 std::fs::create_dir_all(&path).expect("scratch directory");
552 Self(path)
553 }
554
555 fn write(&self, name: &str, bytes: &[u8]) -> PathBuf {
556 let path = self.0.join(name);
557 std::fs::write(&path, bytes).expect("scratch font file");
558 path
559 }
560
561 fn path(&self) -> &Path {
562 &self.0
563 }
564 }
565
566 impl Drop for ScratchDir {
567 fn drop(&mut self) {
568 let _ = std::fs::remove_dir_all(&self.0);
569 }
570 }
571
572 #[test]
573 fn register_family_picks_the_face_matching_the_requested_weight() {
574 let dir = ScratchDir::new("weights");
575 let regular = dir.write("Test-Regular.ttf", REGULAR);
576 let bold = dir.write("Test-Bold.ttf", BOLD);
577 let family = FontFamily::file_backed(vec![
578 FontFile::new(regular.to_string_lossy().into_owned()),
579 FontFile::new(bold.to_string_lossy().into_owned()).with_weight(FontWeight::BOLD),
580 ])
581 .expect("file-backed family");
582
583 let mut registry = SoftwareTextFontRegistry::new();
584 registry.register_family(&family).expect("family loads");
585 let fonts = registry.into_font_set_or_default(&[]);
586
587 let resolved_regular = fonts
588 .resolve(&style_for(&family, FontWeight::NORMAL))
589 .expect("regular face");
590 let resolved_bold = fonts
591 .resolve(&style_for(&family, FontWeight::BOLD))
592 .expect("bold face");
593
594 assert_eq!(resolved_regular.weight(), FontWeight::NORMAL);
595 assert_eq!(resolved_bold.weight(), FontWeight::BOLD);
596 assert_ne!(
597 resolved_regular.content_hash(),
598 resolved_bold.content_hash(),
599 "distinct faces must key the glyph atlas distinctly"
600 );
601 }
602
603 #[test]
604 fn register_family_honours_a_declared_weight_over_the_face_header() {
605 let dir = ScratchDir::new("declared");
606 let path = dir.write("Test-Regular.ttf", REGULAR);
607 let family =
608 FontFamily::file_backed(vec![
609 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight::MEDIUM)
610 ])
611 .expect("file-backed family");
612
613 let mut registry = SoftwareTextFontRegistry::new();
614 registry.register_family(&family).expect("family loads");
615 let fonts = registry.into_font_set_or_default(&[]);
616
617 let resolved = fonts
618 .resolve(&style_for(&family, FontWeight::MEDIUM))
619 .expect("declared face");
620 assert_eq!(resolved.weight(), FontWeight::MEDIUM);
621 }
622
623 #[test]
624 fn register_family_reports_a_missing_file_without_panicking() {
625 let dir = ScratchDir::new("missing");
626 let family = FontFamily::file_backed(vec![FontFile::new(
627 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
628 )])
629 .expect("file-backed family");
630
631 let mut registry = SoftwareTextFontRegistry::new();
632 let error = registry
633 .register_family(&family)
634 .expect_err("a missing file cannot register");
635 assert!(matches!(error, FontLoadError::Read { .. }), "{error}");
636 assert!(registry.is_empty());
637 }
638
639 #[test]
640 fn register_family_reports_a_corrupt_file_without_panicking() {
641 let dir = ScratchDir::new("corrupt");
642 let path = dir.write("Corrupt.ttf", b"this is not a font");
643 let family =
644 FontFamily::file_backed(vec![FontFile::new(path.to_string_lossy().into_owned())])
645 .expect("file-backed family");
646
647 let mut registry = SoftwareTextFontRegistry::new();
648 let error = registry
649 .register_family(&family)
650 .expect_err("a corrupt file cannot register");
651 assert!(matches!(error, FontLoadError::Parse { .. }), "{error}");
652 assert!(registry.is_empty());
653 }
654
655 #[test]
656 #[cfg(feature = "embedded-default-font")]
657 fn a_family_that_failed_to_load_falls_back_to_the_default_face() {
658 let dir = ScratchDir::new("fallback");
659 let family = FontFamily::file_backed(vec![FontFile::new(
660 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
661 )])
662 .expect("file-backed family");
663
664 let mut registry = SoftwareTextFontRegistry::new();
665 let _ = registry.register_family(&family);
666 let fonts = registry.into_font_set_or_default(&[]);
667
668 let resolved = fonts
669 .resolve(&style_for(&family, FontWeight::NORMAL))
670 .expect("fallback face");
671 assert_eq!(
672 resolved.content_hash(),
673 fonts.default_font().expect("default face").content_hash()
674 );
675 }
676
677 #[test]
678 fn a_partly_loadable_family_keeps_the_faces_that_did_load() {
679 let dir = ScratchDir::new("partial");
680 let regular = dir.write("Test-Regular.ttf", REGULAR);
681 let family = FontFamily::file_backed(vec![
682 FontFile::new(regular.to_string_lossy().into_owned()),
683 FontFile::new(dir.path().join("Absent.ttf").to_string_lossy().into_owned())
684 .with_weight(FontWeight::BOLD),
685 ])
686 .expect("file-backed family");
687
688 let mut registry = SoftwareTextFontRegistry::new();
689 registry
690 .register_family(&family)
691 .expect("one readable face is enough");
692 assert_eq!(registry.faces().len(), 1);
693 }
694
695 #[test]
696 fn register_face_reader_accepts_a_font_that_is_not_a_file() {
697 let family = FontFamily::named("Bundled");
698 let mut registry = SoftwareTextFontRegistry::new();
699 registry
700 .register_face_reader(
701 &family,
702 FontWeight::NORMAL,
703 FontStyle::Normal,
704 &mut Cursor::new(REGULAR.to_vec()),
705 )
706 .expect("streamed face loads");
707
708 let fonts = registry.into_font_set_or_default(&[]);
709 let resolved = fonts
710 .resolve(&style_for(&family, FontWeight::NORMAL))
711 .expect("streamed face");
712 assert_eq!(resolved.registered_family(), {
713 let mut expected = SoftwareTextFontRegistry::new();
714 expected
715 .register_face_bytes(
716 &family,
717 FontWeight::NORMAL,
718 FontStyle::Normal,
719 REGULAR.to_vec(),
720 )
721 .expect("face loads");
722 expected.faces()[0].registered_family()
723 });
724 }
725
726 #[test]
727 #[cfg(feature = "embedded-default-font")]
728 fn an_empty_registry_falls_back_to_the_embedded_default_face() {
729 let fonts = SoftwareTextFontRegistry::new().into_font_set_or_default(&[]);
730 assert!(
731 fonts.default_font().is_some(),
732 "the embedded default font must still serve apps that supply nothing"
733 );
734 assert!(fonts
735 .resolve(&TextStyle::default())
736 .is_some_and(|font| font.registered_family().is_none()));
737 }
738
739 #[test]
740 fn system_font_file_prefers_a_weight_specific_static_face() {
741 let dir = ScratchDir::new("system-static");
742 dir.write("Roboto-Regular.ttf", REGULAR);
743 let medium = dir.write("Roboto-Medium.ttf", BOLD);
744
745 assert_eq!(
746 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
747 Some(medium)
748 );
749 }
750
751 #[test]
752 fn system_font_file_falls_back_to_the_regular_face_for_other_weights() {
753 let dir = ScratchDir::new("system-regular");
754 let regular = dir.write("Roboto-Regular.ttf", REGULAR);
755
756 assert_eq!(
757 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
758 Some(regular)
759 );
760 }
761
762 #[test]
763 fn system_font_file_reports_nothing_when_the_directory_is_empty() {
764 let dir = ScratchDir::new("system-empty");
765 assert_eq!(
766 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::NORMAL),
767 None
768 );
769 assert_eq!(
770 system_font_file(dir.path(), &FontFamily::named("Roboto"), FontWeight::NORMAL),
771 None
772 );
773 }
774
775 #[test]
776 fn register_system_family_binds_the_generic_alias_to_the_platform_face() {
777 let dir = ScratchDir::new("system-family");
778 dir.write("Roboto-Regular.ttf", REGULAR);
779 dir.write("Roboto-Bold.ttf", BOLD);
780
781 let mut registry = SoftwareTextFontRegistry::new();
782 registry
783 .register_system_family(
784 dir.path(),
785 &FontFamily::SansSerif,
786 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
787 )
788 .expect("system family loads");
789 let fonts = registry.into_font_set_or_default(&[]);
790
791 assert!(fonts.has_registered_family(&FontFamily::SansSerif));
792 let bold = fonts
793 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::BOLD))
794 .expect("bold system face");
795 assert_eq!(bold.weight(), FontWeight::BOLD);
796 assert_eq!(bold.registered_family(), {
797 let key = fonts
798 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
799 .expect("regular system face")
800 .registered_family();
801 key
802 });
803 }
804
805 #[test]
806 fn register_system_family_reports_an_absent_font_directory() {
807 let mut registry = SoftwareTextFontRegistry::new();
808 let error = registry
809 .register_system_family(
810 Path::new("/definitely/not/a/font/directory"),
811 &FontFamily::SansSerif,
812 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
813 )
814 .expect_err("an absent directory cannot register");
815 assert!(
816 matches!(error, FontLoadError::NoSystemFontFile { .. }),
817 "{error}"
818 );
819 assert!(registry.is_empty());
820 }
821
822 #[test]
823 fn a_system_weight_the_font_config_does_not_declare_resolves_to_the_declared_one() {
824 for (requested, expected) in [(450u16, 400u16), (550, 500), (401, 400), (599, 500)] {
827 assert_eq!(
828 system_declared_weight(&FontFamily::SansSerif, FontWeight(requested)),
829 FontWeight(expected),
830 "sans-serif {requested}"
831 );
832 }
833 for weight in DECLARED_HUNDREDS {
835 assert_eq!(
836 system_declared_weight(&FontFamily::SansSerif, FontWeight(*weight)),
837 FontWeight(*weight)
838 );
839 }
840 assert_eq!(
842 system_declared_weight(&FontFamily::SansSerif, FontWeight(50)),
843 FontWeight(100)
844 );
845 assert_eq!(
846 system_declared_weight(&FontFamily::SansSerif, FontWeight(1000)),
847 FontWeight(900)
848 );
849 }
850
851 #[test]
852 fn a_sparse_system_family_resolves_by_distance_and_keeps_the_lighter_face_on_a_tie() {
853 for (requested, expected) in [(500u16, 400u16), (550, 400), (600, 700), (650, 700)] {
857 assert_eq!(
858 system_declared_weight(&FontFamily::Serif, FontWeight(requested)),
859 FontWeight(expected),
860 "serif {requested}"
861 );
862 }
863 assert_eq!(
866 closest_declared_weight(&[300, 500], FontWeight(400)),
867 Some(FontWeight(300))
868 );
869 assert_eq!(
870 closest_declared_weight(&[500, 300], FontWeight(400)),
871 Some(FontWeight(500)),
872 "declaration order, not magnitude, is what breaks the tie"
873 );
874 }
875
876 #[test]
877 fn a_family_with_no_system_files_keeps_the_weight_it_was_given() {
878 assert_eq!(
881 system_declared_weight(&FontFamily::named("Roboto"), FontWeight(450)),
882 FontWeight(450)
883 );
884 }
885
886 #[test]
887 fn register_system_face_registers_the_declared_weight_not_the_requested_one() {
888 let dir = ScratchDir::new("system-undeclared-weight");
889 dir.write("Roboto-Regular.ttf", REGULAR);
890
891 let mut registry = SoftwareTextFontRegistry::new();
892 registry
893 .register_system_face(
894 dir.path(),
895 &FontFamily::SansSerif,
896 FontWeight(450),
897 FontStyle::Normal,
898 )
899 .expect("system face loads");
900 let fonts = registry.into_font_set_or_default(&[]);
901
902 let resolved = fonts
905 .resolve(&style_for(&FontFamily::SansSerif, FontWeight(450)))
906 .expect("a face for the off-grid request");
907 assert_eq!(
908 resolved.weight(),
909 FontWeight::NORMAL,
910 "450 must land on the 400 entry Android's matcher returns"
911 );
912
913 let mut declared = SoftwareTextFontRegistry::new();
914 declared
915 .register_system_face(
916 dir.path(),
917 &FontFamily::SansSerif,
918 FontWeight::NORMAL,
919 FontStyle::Normal,
920 )
921 .expect("system face loads");
922 let declared = declared.into_font_set_or_default(&[]);
923 assert_eq!(
924 resolved.content_hash(),
925 declared
926 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
927 .expect("the 400 face")
928 .content_hash(),
929 "the off-grid request must produce the very same face, glyph masks included"
930 );
931 }
932
933 #[test]
934 fn system_requests_that_resolve_alike_register_one_face() {
935 let dir = ScratchDir::new("system-dedupe");
936 dir.write("Roboto-Regular.ttf", REGULAR);
937
938 let mut registry = SoftwareTextFontRegistry::new();
939 for weight in [400u16, 450, 499] {
940 registry
941 .register_system_face(
942 dir.path(),
943 &FontFamily::SansSerif,
944 FontWeight(weight),
945 FontStyle::Normal,
946 )
947 .expect("system face loads");
948 }
949
950 assert_eq!(
951 registry.faces().len(),
952 1,
953 "three requests for one declared entry are one face"
954 );
955 }
956
957 #[test]
958 fn an_app_registered_face_keeps_the_weight_it_declares() {
959 let dir = ScratchDir::new("app-off-grid");
963 let path = dir.write("Test-Regular.ttf", REGULAR);
964 let family =
965 FontFamily::file_backed(vec![
966 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight(450))
967 ])
968 .expect("file-backed family");
969
970 let mut registry = SoftwareTextFontRegistry::new();
971 registry.register_family(&family).expect("family loads");
972 let fonts = registry.into_font_set_or_default(&[]);
973
974 assert_eq!(
975 fonts
976 .resolve(&style_for(&family, FontWeight(450)))
977 .expect("app face")
978 .weight(),
979 FontWeight(450)
980 );
981 }
982
983 #[test]
984 fn register_family_rejects_a_family_that_names_no_files() {
985 let mut registry = SoftwareTextFontRegistry::new();
986 let error = registry
987 .register_family(&FontFamily::named("Roboto"))
988 .expect_err("a named family has nothing to read");
989 assert!(matches!(error, FontLoadError::NotFileBacked), "{error}");
990 }
991
992 #[test]
993 fn loaded_typeface_families_register_their_single_file() {
994 let dir = ScratchDir::new("typeface");
995 let path = dir.write("Test-Regular.ttf", REGULAR);
996 let family = FontFamily::loaded_typeface_path(path.to_string_lossy().into_owned());
997
998 let mut registry = SoftwareTextFontRegistry::new();
999 registry.register_family(&family).expect("typeface loads");
1000 let fonts = registry.into_font_set_or_default(&[]);
1001
1002 assert!(fonts.has_registered_family(&family));
1003 assert!(fonts
1004 .resolve(&style_for(&family, FontWeight::NORMAL))
1005 .is_some_and(|font| font.registered_family().is_some()));
1006 }
1007}