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 default_software_text_font, FontFamilyKey, SoftwareTextFont, SoftwareTextFontError,
30 SoftwareTextFontSet,
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)>,
84}
85
86impl SoftwareTextFontRegistry {
87 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn register_family(&mut self, family: &FontFamily) -> Result<(), FontLoadError> {
101 let files = font_files_for(family)?;
102 if files.is_empty() {
103 return Err(FontLoadError::EmptyFamily);
104 }
105
106 let mut reads = FontFileReads::default();
107 let mut first_error = None;
108 let mut loaded = 0usize;
109 for file in &files {
110 match self.register_read_face(
111 &mut reads,
112 family,
113 file.weight,
114 file.style,
115 Path::new(&file.path),
116 ) {
117 Ok(()) => loaded += 1,
118 Err(error) => first_error = first_error.or(Some(error)),
119 }
120 }
121
122 match first_error {
123 Some(error) if loaded == 0 => Err(error),
124 _ => Ok(()),
125 }
126 }
127
128 fn register_read_face(
129 &mut self,
130 reads: &mut FontFileReads,
131 family: &FontFamily,
132 weight: FontWeight,
133 style: FontStyle,
134 path: &Path,
135 ) -> Result<(), FontLoadError> {
136 let bytes = reads.read(path)?;
137 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes.to_vec())
138 .map_err(|source| FontLoadError::Parse {
139 path: path.to_path_buf(),
140 source,
141 })?;
142 self.faces.push(face);
143 Ok(())
144 }
145
146 pub fn register_face_reader(
153 &mut self,
154 family: &FontFamily,
155 weight: FontWeight,
156 style: FontStyle,
157 reader: &mut impl Read,
158 ) -> Result<(), FontLoadError> {
159 let mut bytes = Vec::new();
160 reader
161 .read_to_end(&mut bytes)
162 .map_err(|source| FontLoadError::Read {
163 path: PathBuf::new(),
164 source,
165 })?;
166 self.register_face_bytes(family, weight, style, bytes)
167 }
168
169 pub fn register_face_bytes(
171 &mut self,
172 family: &FontFamily,
173 weight: FontWeight,
174 style: FontStyle,
175 bytes: impl Into<Vec<u8>>,
176 ) -> Result<(), FontLoadError> {
177 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes)
178 .map_err(|source| FontLoadError::ParseBytes { source })?;
179 self.faces.push(face);
180 Ok(())
181 }
182
183 pub fn register_fallback_bytes(
188 &mut self,
189 bytes: impl Into<Vec<u8>>,
190 ) -> Result<(), FontLoadError> {
191 let face = SoftwareTextFont::from_bytes(bytes)
192 .map_err(|source| FontLoadError::ParseBytes { source })?;
193 self.faces.push(face);
194 Ok(())
195 }
196
197 pub fn register_system_family(
214 &mut self,
215 directory: impl AsRef<Path>,
216 family: &FontFamily,
217 weights: &[FontWeight],
218 ) -> Result<(), FontLoadError> {
219 let directory = directory.as_ref();
220 let mut reads = FontFileReads::default();
221 let mut first_error = None;
222 let mut loaded = 0usize;
223 for weight in weights {
224 match self.register_read_system_face(
225 &mut reads,
226 directory,
227 family,
228 *weight,
229 FontStyle::Normal,
230 ) {
231 Ok(()) => loaded += 1,
232 Err(error) => first_error = first_error.or(Some(error)),
233 }
234 }
235
236 match first_error {
237 Some(error) if loaded == 0 => Err(error),
238 _ => Ok(()),
239 }
240 }
241
242 pub fn register_system_face(
251 &mut self,
252 directory: impl AsRef<Path>,
253 family: &FontFamily,
254 weight: FontWeight,
255 style: FontStyle,
256 ) -> Result<(), FontLoadError> {
257 self.register_read_system_face(
258 &mut FontFileReads::default(),
259 directory.as_ref(),
260 family,
261 weight,
262 style,
263 )
264 }
265
266 fn register_read_system_face(
267 &mut self,
268 reads: &mut FontFileReads,
269 directory: &Path,
270 family: &FontFamily,
271 weight: FontWeight,
272 style: FontStyle,
273 ) -> Result<(), FontLoadError> {
274 let weight = system_declared_weight(family, weight);
279 if self.has_system_face(family, weight, style) {
280 return Ok(());
285 }
286 let path = system_font_file(directory, family, weight).ok_or_else(|| {
287 FontLoadError::NoSystemFontFile {
288 directory: directory.to_path_buf(),
289 }
290 })?;
291 self.register_read_face(reads, family, weight, style, &path)?;
292 self.system_faces
293 .push((FontFamilyKey::of(family), weight, style));
294 Ok(())
295 }
296
297 fn has_system_face(&self, family: &FontFamily, weight: FontWeight, style: FontStyle) -> bool {
303 self.system_faces
304 .contains(&(FontFamilyKey::of(family), weight, style))
305 }
306
307 pub fn faces(&self) -> &[SoftwareTextFont] {
309 &self.faces
310 }
311
312 pub fn is_empty(&self) -> bool {
313 self.faces.is_empty()
314 }
315
316 pub fn into_font_set_or_default(mut self, fonts: &[&[u8]]) -> SoftwareTextFontSet {
324 for bytes in fonts {
325 let _ = self.register_fallback_bytes((*bytes).to_vec());
327 }
328 if self.faces.is_empty() {
329 if let Some(default_font) = default_software_text_font() {
330 self.faces.push(default_font);
331 }
332 }
333 SoftwareTextFontSet::from_faces(self.faces)
334 }
335}
336
337#[derive(Default)]
344struct FontFileReads {
345 entries: Vec<(PathBuf, Arc<[u8]>)>,
346}
347
348impl FontFileReads {
349 fn read(&mut self, path: &Path) -> Result<Arc<[u8]>, FontLoadError> {
350 if let Some((_, bytes)) = self.entries.iter().find(|(read, _)| read == path) {
351 return Ok(Arc::clone(bytes));
352 }
353 let bytes: Arc<[u8]> = std::fs::read(path)
354 .map_err(|source| FontLoadError::Read {
355 path: path.to_path_buf(),
356 source,
357 })?
358 .into();
359 self.entries.push((path.to_path_buf(), Arc::clone(&bytes)));
360 Ok(bytes)
361 }
362}
363
364pub fn system_declared_weight(family: &FontFamily, weight: FontWeight) -> FontWeight {
408 let Some(files) = system_family_files(family) else {
409 return weight;
410 };
411 closest_declared_weight(files.declared, weight).unwrap_or(weight)
412}
413
414fn closest_declared_weight(declared: &[u16], requested: FontWeight) -> Option<FontWeight> {
416 let mut best: Option<(u16, u16)> = None;
417 for candidate in declared {
418 let score = weight_match_score(*candidate, requested.value());
419 if best.is_none_or(|(_, best_score)| score < best_score) {
421 best = Some((*candidate, score));
422 }
423 }
424 best.map(|(candidate, _)| FontWeight(candidate))
425}
426
427fn weight_match_score(declared: u16, requested: u16) -> u16 {
429 (declared / 100).abs_diff(requested / 100)
430}
431
432pub fn system_font_file(
439 directory: &Path,
440 family: &FontFamily,
441 weight: FontWeight,
442) -> Option<PathBuf> {
443 let files = system_family_files(family)?;
444 files
445 .weighted
446 .iter()
447 .filter(|(candidate_weight, _)| *candidate_weight == weight.value())
448 .map(|(_, name)| directory.join(name))
449 .chain(files.regular.iter().map(|name| directory.join(name)))
450 .find(|path| path.is_file())
451}
452
453struct SystemFamilyFiles {
456 regular: &'static [&'static str],
457 weighted: &'static [(u16, &'static str)],
458 declared: &'static [u16],
463}
464
465const DECLARED_HUNDREDS: &[u16] = &[100, 200, 300, 400, 500, 600, 700, 800, 900];
469
470fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
471 match family {
475 FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
476 regular: &[
477 "Roboto-Regular.ttf",
478 "RobotoStatic-Regular.ttf",
479 "NotoSans-Regular.ttf",
480 "DroidSans.ttf",
481 ],
482 weighted: &[
483 (300, "Roboto-Light.ttf"),
484 (500, "Roboto-Medium.ttf"),
485 (700, "Roboto-Bold.ttf"),
486 (900, "Roboto-Black.ttf"),
487 ],
488 declared: DECLARED_HUNDREDS,
489 }),
490 FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
492 regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
493 weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
494 declared: &[400, 700],
495 }),
496 FontFamily::Monospace => Some(SystemFamilyFiles {
497 regular: &[
498 "DroidSansMono.ttf",
499 "RobotoMono-Regular.ttf",
500 "CutiveMono-Regular.ttf",
501 ],
502 weighted: &[(700, "RobotoMono-Bold.ttf")],
503 declared: &[400, 700],
504 }),
505 FontFamily::Cursive => Some(SystemFamilyFiles {
506 regular: &["DancingScript-Regular.ttf"],
507 weighted: &[(700, "DancingScript-Bold.ttf")],
508 declared: &[400, 700],
509 }),
510 FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
511 }
512}
513
514fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
515 match family {
516 FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
517 FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
518 _ => Err(FontLoadError::NotFileBacked),
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use std::io::Cursor;
525
526 use cranpose_ui::text::{SpanStyle, TextStyle};
527
528 use super::*;
529
530 const REGULAR: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
531 const BOLD: &[u8] = include_bytes!("../assets/NotoSansBold.ttf");
532
533 fn style_for(family: &FontFamily, weight: FontWeight) -> TextStyle {
534 TextStyle {
535 span_style: SpanStyle {
536 font_family: Some(family.clone()),
537 font_weight: Some(weight),
538 ..Default::default()
539 },
540 ..Default::default()
541 }
542 }
543
544 struct ScratchDir(PathBuf);
548
549 impl ScratchDir {
550 fn new(name: &str) -> Self {
551 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
552 .join("../../../target/test-output/cranpose-font-source")
553 .join(name);
554 let _ = std::fs::remove_dir_all(&path);
555 std::fs::create_dir_all(&path).expect("scratch directory");
556 Self(path)
557 }
558
559 fn write(&self, name: &str, bytes: &[u8]) -> PathBuf {
560 let path = self.0.join(name);
561 std::fs::write(&path, bytes).expect("scratch font file");
562 path
563 }
564
565 fn path(&self) -> &Path {
566 &self.0
567 }
568 }
569
570 impl Drop for ScratchDir {
571 fn drop(&mut self) {
572 let _ = std::fs::remove_dir_all(&self.0);
573 }
574 }
575
576 #[test]
577 fn register_family_picks_the_face_matching_the_requested_weight() {
578 let dir = ScratchDir::new("weights");
579 let regular = dir.write("Test-Regular.ttf", REGULAR);
580 let bold = dir.write("Test-Bold.ttf", BOLD);
581 let family = FontFamily::file_backed(vec![
582 FontFile::new(regular.to_string_lossy().into_owned()),
583 FontFile::new(bold.to_string_lossy().into_owned()).with_weight(FontWeight::BOLD),
584 ])
585 .expect("file-backed family");
586
587 let mut registry = SoftwareTextFontRegistry::new();
588 registry.register_family(&family).expect("family loads");
589 let fonts = registry.into_font_set_or_default(&[]);
590
591 let resolved_regular = fonts
592 .resolve(&style_for(&family, FontWeight::NORMAL))
593 .expect("regular face");
594 let resolved_bold = fonts
595 .resolve(&style_for(&family, FontWeight::BOLD))
596 .expect("bold face");
597
598 assert_eq!(resolved_regular.weight(), FontWeight::NORMAL);
599 assert_eq!(resolved_bold.weight(), FontWeight::BOLD);
600 assert_ne!(
601 resolved_regular.content_hash(),
602 resolved_bold.content_hash(),
603 "distinct faces must key the glyph atlas distinctly"
604 );
605 }
606
607 #[test]
608 fn register_family_honours_a_declared_weight_over_the_face_header() {
609 let dir = ScratchDir::new("declared");
610 let path = dir.write("Test-Regular.ttf", REGULAR);
611 let family =
612 FontFamily::file_backed(vec![
613 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight::MEDIUM)
614 ])
615 .expect("file-backed family");
616
617 let mut registry = SoftwareTextFontRegistry::new();
618 registry.register_family(&family).expect("family loads");
619 let fonts = registry.into_font_set_or_default(&[]);
620
621 let resolved = fonts
622 .resolve(&style_for(&family, FontWeight::MEDIUM))
623 .expect("declared face");
624 assert_eq!(resolved.weight(), FontWeight::MEDIUM);
625 }
626
627 #[test]
628 fn register_family_reports_a_missing_file_without_panicking() {
629 let dir = ScratchDir::new("missing");
630 let family = FontFamily::file_backed(vec![FontFile::new(
631 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
632 )])
633 .expect("file-backed family");
634
635 let mut registry = SoftwareTextFontRegistry::new();
636 let error = registry
637 .register_family(&family)
638 .expect_err("a missing file cannot register");
639 assert!(matches!(error, FontLoadError::Read { .. }), "{error}");
640 assert!(registry.is_empty());
641 }
642
643 #[test]
644 fn register_family_reports_a_corrupt_file_without_panicking() {
645 let dir = ScratchDir::new("corrupt");
646 let path = dir.write("Corrupt.ttf", b"this is not a font");
647 let family =
648 FontFamily::file_backed(vec![FontFile::new(path.to_string_lossy().into_owned())])
649 .expect("file-backed family");
650
651 let mut registry = SoftwareTextFontRegistry::new();
652 let error = registry
653 .register_family(&family)
654 .expect_err("a corrupt file cannot register");
655 assert!(matches!(error, FontLoadError::Parse { .. }), "{error}");
656 assert!(registry.is_empty());
657 }
658
659 #[test]
660 #[cfg(feature = "embedded-default-font")]
661 fn a_family_that_failed_to_load_falls_back_to_the_default_face() {
662 let dir = ScratchDir::new("fallback");
663 let family = FontFamily::file_backed(vec![FontFile::new(
664 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
665 )])
666 .expect("file-backed family");
667
668 let mut registry = SoftwareTextFontRegistry::new();
669 let _ = registry.register_family(&family);
670 let fonts = registry.into_font_set_or_default(&[]);
671
672 let resolved = fonts
673 .resolve(&style_for(&family, FontWeight::NORMAL))
674 .expect("fallback face");
675 assert_eq!(
676 resolved.content_hash(),
677 fonts.default_font().expect("default face").content_hash()
678 );
679 }
680
681 #[test]
682 fn a_partly_loadable_family_keeps_the_faces_that_did_load() {
683 let dir = ScratchDir::new("partial");
684 let regular = dir.write("Test-Regular.ttf", REGULAR);
685 let family = FontFamily::file_backed(vec![
686 FontFile::new(regular.to_string_lossy().into_owned()),
687 FontFile::new(dir.path().join("Absent.ttf").to_string_lossy().into_owned())
688 .with_weight(FontWeight::BOLD),
689 ])
690 .expect("file-backed family");
691
692 let mut registry = SoftwareTextFontRegistry::new();
693 registry
694 .register_family(&family)
695 .expect("one readable face is enough");
696 assert_eq!(registry.faces().len(), 1);
697 }
698
699 #[test]
700 fn register_face_reader_accepts_a_font_that_is_not_a_file() {
701 let family = FontFamily::named("Bundled");
702 let mut registry = SoftwareTextFontRegistry::new();
703 registry
704 .register_face_reader(
705 &family,
706 FontWeight::NORMAL,
707 FontStyle::Normal,
708 &mut Cursor::new(REGULAR.to_vec()),
709 )
710 .expect("streamed face loads");
711
712 let fonts = registry.into_font_set_or_default(&[]);
713 let resolved = fonts
714 .resolve(&style_for(&family, FontWeight::NORMAL))
715 .expect("streamed face");
716 assert_eq!(resolved.registered_family(), {
717 let mut expected = SoftwareTextFontRegistry::new();
718 expected
719 .register_face_bytes(
720 &family,
721 FontWeight::NORMAL,
722 FontStyle::Normal,
723 REGULAR.to_vec(),
724 )
725 .expect("face loads");
726 expected.faces()[0].registered_family()
727 });
728 }
729
730 #[test]
731 #[cfg(feature = "embedded-default-font")]
732 fn an_empty_registry_falls_back_to_the_embedded_default_face() {
733 let fonts = SoftwareTextFontRegistry::new().into_font_set_or_default(&[]);
734 assert!(
735 fonts.default_font().is_some(),
736 "the embedded default font must still serve apps that supply nothing"
737 );
738 assert!(fonts
739 .resolve(&TextStyle::default())
740 .is_some_and(|font| font.registered_family().is_none()));
741 }
742
743 #[test]
744 fn system_font_file_prefers_a_weight_specific_static_face() {
745 let dir = ScratchDir::new("system-static");
746 dir.write("Roboto-Regular.ttf", REGULAR);
747 let medium = dir.write("Roboto-Medium.ttf", BOLD);
748
749 assert_eq!(
750 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
751 Some(medium)
752 );
753 }
754
755 #[test]
756 fn system_font_file_falls_back_to_the_regular_face_for_other_weights() {
757 let dir = ScratchDir::new("system-regular");
758 let regular = dir.write("Roboto-Regular.ttf", REGULAR);
759
760 assert_eq!(
761 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
762 Some(regular)
763 );
764 }
765
766 #[test]
767 fn system_font_file_reports_nothing_when_the_directory_is_empty() {
768 let dir = ScratchDir::new("system-empty");
769 assert_eq!(
770 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::NORMAL),
771 None
772 );
773 assert_eq!(
774 system_font_file(dir.path(), &FontFamily::named("Roboto"), FontWeight::NORMAL),
775 None
776 );
777 }
778
779 #[test]
780 fn register_system_family_binds_the_generic_alias_to_the_platform_face() {
781 let dir = ScratchDir::new("system-family");
782 dir.write("Roboto-Regular.ttf", REGULAR);
783 dir.write("Roboto-Bold.ttf", BOLD);
784
785 let mut registry = SoftwareTextFontRegistry::new();
786 registry
787 .register_system_family(
788 dir.path(),
789 &FontFamily::SansSerif,
790 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
791 )
792 .expect("system family loads");
793 let fonts = registry.into_font_set_or_default(&[]);
794
795 assert!(fonts.has_registered_family(&FontFamily::SansSerif));
796 let bold = fonts
797 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::BOLD))
798 .expect("bold system face");
799 assert_eq!(bold.weight(), FontWeight::BOLD);
800 assert_eq!(bold.registered_family(), {
801 let key = fonts
802 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
803 .expect("regular system face")
804 .registered_family();
805 key
806 });
807 }
808
809 #[test]
810 fn register_system_family_reports_an_absent_font_directory() {
811 let mut registry = SoftwareTextFontRegistry::new();
812 let error = registry
813 .register_system_family(
814 Path::new("/definitely/not/a/font/directory"),
815 &FontFamily::SansSerif,
816 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
817 )
818 .expect_err("an absent directory cannot register");
819 assert!(
820 matches!(error, FontLoadError::NoSystemFontFile { .. }),
821 "{error}"
822 );
823 assert!(registry.is_empty());
824 }
825
826 #[test]
827 fn a_system_weight_the_font_config_does_not_declare_resolves_to_the_declared_one() {
828 for (requested, expected) in [(450u16, 400u16), (550, 500), (401, 400), (599, 500)] {
831 assert_eq!(
832 system_declared_weight(&FontFamily::SansSerif, FontWeight(requested)),
833 FontWeight(expected),
834 "sans-serif {requested}"
835 );
836 }
837 for weight in DECLARED_HUNDREDS {
839 assert_eq!(
840 system_declared_weight(&FontFamily::SansSerif, FontWeight(*weight)),
841 FontWeight(*weight)
842 );
843 }
844 assert_eq!(
846 system_declared_weight(&FontFamily::SansSerif, FontWeight(50)),
847 FontWeight(100)
848 );
849 assert_eq!(
850 system_declared_weight(&FontFamily::SansSerif, FontWeight(1000)),
851 FontWeight(900)
852 );
853 }
854
855 #[test]
856 fn a_sparse_system_family_resolves_by_distance_and_keeps_the_lighter_face_on_a_tie() {
857 for (requested, expected) in [(500u16, 400u16), (550, 400), (600, 700), (650, 700)] {
861 assert_eq!(
862 system_declared_weight(&FontFamily::Serif, FontWeight(requested)),
863 FontWeight(expected),
864 "serif {requested}"
865 );
866 }
867 assert_eq!(
870 closest_declared_weight(&[300, 500], FontWeight(400)),
871 Some(FontWeight(300))
872 );
873 assert_eq!(
874 closest_declared_weight(&[500, 300], FontWeight(400)),
875 Some(FontWeight(500)),
876 "declaration order, not magnitude, is what breaks the tie"
877 );
878 }
879
880 #[test]
881 fn a_family_with_no_system_files_keeps_the_weight_it_was_given() {
882 assert_eq!(
885 system_declared_weight(&FontFamily::named("Roboto"), FontWeight(450)),
886 FontWeight(450)
887 );
888 }
889
890 #[test]
891 fn register_system_face_registers_the_declared_weight_not_the_requested_one() {
892 let dir = ScratchDir::new("system-undeclared-weight");
893 dir.write("Roboto-Regular.ttf", REGULAR);
894
895 let mut registry = SoftwareTextFontRegistry::new();
896 registry
897 .register_system_face(
898 dir.path(),
899 &FontFamily::SansSerif,
900 FontWeight(450),
901 FontStyle::Normal,
902 )
903 .expect("system face loads");
904 let fonts = registry.into_font_set_or_default(&[]);
905
906 let resolved = fonts
909 .resolve(&style_for(&FontFamily::SansSerif, FontWeight(450)))
910 .expect("a face for the off-grid request");
911 assert_eq!(
912 resolved.weight(),
913 FontWeight::NORMAL,
914 "450 must land on the 400 entry Android's matcher returns"
915 );
916
917 let mut declared = SoftwareTextFontRegistry::new();
918 declared
919 .register_system_face(
920 dir.path(),
921 &FontFamily::SansSerif,
922 FontWeight::NORMAL,
923 FontStyle::Normal,
924 )
925 .expect("system face loads");
926 let declared = declared.into_font_set_or_default(&[]);
927 assert_eq!(
928 resolved.content_hash(),
929 declared
930 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
931 .expect("the 400 face")
932 .content_hash(),
933 "the off-grid request must produce the very same face, glyph masks included"
934 );
935 }
936
937 #[test]
938 fn system_requests_that_resolve_alike_register_one_face() {
939 let dir = ScratchDir::new("system-dedupe");
940 dir.write("Roboto-Regular.ttf", REGULAR);
941
942 let mut registry = SoftwareTextFontRegistry::new();
943 for weight in [400u16, 450, 499] {
944 registry
945 .register_system_face(
946 dir.path(),
947 &FontFamily::SansSerif,
948 FontWeight(weight),
949 FontStyle::Normal,
950 )
951 .expect("system face loads");
952 }
953
954 assert_eq!(
955 registry.faces().len(),
956 1,
957 "three requests for one declared entry are one face"
958 );
959 }
960
961 #[test]
962 fn an_app_registered_face_keeps_the_weight_it_declares() {
963 let dir = ScratchDir::new("app-off-grid");
967 let path = dir.write("Test-Regular.ttf", REGULAR);
968 let family =
969 FontFamily::file_backed(vec![
970 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight(450))
971 ])
972 .expect("file-backed family");
973
974 let mut registry = SoftwareTextFontRegistry::new();
975 registry.register_family(&family).expect("family loads");
976 let fonts = registry.into_font_set_or_default(&[]);
977
978 assert_eq!(
979 fonts
980 .resolve(&style_for(&family, FontWeight(450)))
981 .expect("app face")
982 .weight(),
983 FontWeight(450)
984 );
985 }
986
987 #[test]
988 fn register_family_rejects_a_family_that_names_no_files() {
989 let mut registry = SoftwareTextFontRegistry::new();
990 let error = registry
991 .register_family(&FontFamily::named("Roboto"))
992 .expect_err("a named family has nothing to read");
993 assert!(matches!(error, FontLoadError::NotFileBacked), "{error}");
994 }
995
996 #[test]
997 fn loaded_typeface_families_register_their_single_file() {
998 let dir = ScratchDir::new("typeface");
999 let path = dir.write("Test-Regular.ttf", REGULAR);
1000 let family = FontFamily::loaded_typeface_path(path.to_string_lossy().into_owned());
1001
1002 let mut registry = SoftwareTextFontRegistry::new();
1003 registry.register_family(&family).expect("typeface loads");
1004 let fonts = registry.into_font_set_or_default(&[]);
1005
1006 assert!(fonts.has_registered_family(&family));
1007 assert!(fonts
1008 .resolve(&style_for(&family, FontWeight::NORMAL))
1009 .is_some_and(|font| font.registered_family().is_some()));
1010 }
1011}