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)>,
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 && let Some(default_font) = default_software_text_font()
330 {
331 self.faces.push(default_font);
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 = FontFamily::file_backed(vec![
612 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight::MEDIUM),
613 ])
614 .expect("file-backed family");
615
616 let mut registry = SoftwareTextFontRegistry::new();
617 registry.register_family(&family).expect("family loads");
618 let fonts = registry.into_font_set_or_default(&[]);
619
620 let resolved = fonts
621 .resolve(&style_for(&family, FontWeight::MEDIUM))
622 .expect("declared face");
623 assert_eq!(resolved.weight(), FontWeight::MEDIUM);
624 }
625
626 #[test]
627 fn register_family_reports_a_missing_file_without_panicking() {
628 let dir = ScratchDir::new("missing");
629 let family = FontFamily::file_backed(vec![FontFile::new(
630 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
631 )])
632 .expect("file-backed family");
633
634 let mut registry = SoftwareTextFontRegistry::new();
635 let error = registry
636 .register_family(&family)
637 .expect_err("a missing file cannot register");
638 assert!(matches!(error, FontLoadError::Read { .. }), "{error}");
639 assert!(registry.is_empty());
640 }
641
642 #[test]
643 fn register_family_reports_a_corrupt_file_without_panicking() {
644 let dir = ScratchDir::new("corrupt");
645 let path = dir.write("Corrupt.ttf", b"this is not a font");
646 let family =
647 FontFamily::file_backed(vec![FontFile::new(path.to_string_lossy().into_owned())])
648 .expect("file-backed family");
649
650 let mut registry = SoftwareTextFontRegistry::new();
651 let error = registry
652 .register_family(&family)
653 .expect_err("a corrupt file cannot register");
654 assert!(matches!(error, FontLoadError::Parse { .. }), "{error}");
655 assert!(registry.is_empty());
656 }
657
658 #[test]
659 #[cfg(feature = "embedded-default-font")]
660 fn a_family_that_failed_to_load_falls_back_to_the_default_face() {
661 let dir = ScratchDir::new("fallback");
662 let family = FontFamily::file_backed(vec![FontFile::new(
663 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
664 )])
665 .expect("file-backed family");
666
667 let mut registry = SoftwareTextFontRegistry::new();
668 let _ = registry.register_family(&family);
669 let fonts = registry.into_font_set_or_default(&[]);
670
671 let resolved = fonts
672 .resolve(&style_for(&family, FontWeight::NORMAL))
673 .expect("fallback face");
674 assert_eq!(
675 resolved.content_hash(),
676 fonts.default_font().expect("default face").content_hash()
677 );
678 }
679
680 #[test]
681 fn a_partly_loadable_family_keeps_the_faces_that_did_load() {
682 let dir = ScratchDir::new("partial");
683 let regular = dir.write("Test-Regular.ttf", REGULAR);
684 let family = FontFamily::file_backed(vec![
685 FontFile::new(regular.to_string_lossy().into_owned()),
686 FontFile::new(dir.path().join("Absent.ttf").to_string_lossy().into_owned())
687 .with_weight(FontWeight::BOLD),
688 ])
689 .expect("file-backed family");
690
691 let mut registry = SoftwareTextFontRegistry::new();
692 registry
693 .register_family(&family)
694 .expect("one readable face is enough");
695 assert_eq!(registry.faces().len(), 1);
696 }
697
698 #[test]
699 fn register_face_reader_accepts_a_font_that_is_not_a_file() {
700 let family = FontFamily::named("Bundled");
701 let mut registry = SoftwareTextFontRegistry::new();
702 registry
703 .register_face_reader(
704 &family,
705 FontWeight::NORMAL,
706 FontStyle::Normal,
707 &mut Cursor::new(REGULAR.to_vec()),
708 )
709 .expect("streamed face loads");
710
711 let fonts = registry.into_font_set_or_default(&[]);
712 let resolved = fonts
713 .resolve(&style_for(&family, FontWeight::NORMAL))
714 .expect("streamed face");
715 assert_eq!(resolved.registered_family(), {
716 let mut expected = SoftwareTextFontRegistry::new();
717 expected
718 .register_face_bytes(
719 &family,
720 FontWeight::NORMAL,
721 FontStyle::Normal,
722 REGULAR.to_vec(),
723 )
724 .expect("face loads");
725 expected.faces()[0].registered_family()
726 });
727 }
728
729 #[test]
730 #[cfg(feature = "embedded-default-font")]
731 fn an_empty_registry_falls_back_to_the_embedded_default_face() {
732 let fonts = SoftwareTextFontRegistry::new().into_font_set_or_default(&[]);
733 assert!(
734 fonts.default_font().is_some(),
735 "the embedded default font must still serve apps that supply nothing"
736 );
737 assert!(
738 fonts
739 .resolve(&TextStyle::default())
740 .is_some_and(|font| font.registered_family().is_none())
741 );
742 }
743
744 #[test]
745 fn system_font_file_prefers_a_weight_specific_static_face() {
746 let dir = ScratchDir::new("system-static");
747 dir.write("Roboto-Regular.ttf", REGULAR);
748 let medium = dir.write("Roboto-Medium.ttf", BOLD);
749
750 assert_eq!(
751 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
752 Some(medium)
753 );
754 }
755
756 #[test]
757 fn system_font_file_falls_back_to_the_regular_face_for_other_weights() {
758 let dir = ScratchDir::new("system-regular");
759 let regular = dir.write("Roboto-Regular.ttf", REGULAR);
760
761 assert_eq!(
762 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
763 Some(regular)
764 );
765 }
766
767 #[test]
768 fn system_font_file_reports_nothing_when_the_directory_is_empty() {
769 let dir = ScratchDir::new("system-empty");
770 assert_eq!(
771 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::NORMAL),
772 None
773 );
774 assert_eq!(
775 system_font_file(dir.path(), &FontFamily::named("Roboto"), FontWeight::NORMAL),
776 None
777 );
778 }
779
780 #[test]
781 fn register_system_family_binds_the_generic_alias_to_the_platform_face() {
782 let dir = ScratchDir::new("system-family");
783 dir.write("Roboto-Regular.ttf", REGULAR);
784 dir.write("Roboto-Bold.ttf", BOLD);
785
786 let mut registry = SoftwareTextFontRegistry::new();
787 registry
788 .register_system_family(
789 dir.path(),
790 &FontFamily::SansSerif,
791 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
792 )
793 .expect("system family loads");
794 let fonts = registry.into_font_set_or_default(&[]);
795
796 assert!(fonts.has_registered_family(&FontFamily::SansSerif));
797 let bold = fonts
798 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::BOLD))
799 .expect("bold system face");
800 assert_eq!(bold.weight(), FontWeight::BOLD);
801 assert_eq!(bold.registered_family(), {
802 fonts
803 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
804 .expect("regular system face")
805 .registered_family()
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 = FontFamily::file_backed(vec![
969 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight(450)),
970 ])
971 .expect("file-backed family");
972
973 let mut registry = SoftwareTextFontRegistry::new();
974 registry.register_family(&family).expect("family loads");
975 let fonts = registry.into_font_set_or_default(&[]);
976
977 assert_eq!(
978 fonts
979 .resolve(&style_for(&family, FontWeight(450)))
980 .expect("app face")
981 .weight(),
982 FontWeight(450)
983 );
984 }
985
986 #[test]
987 fn register_family_rejects_a_family_that_names_no_files() {
988 let mut registry = SoftwareTextFontRegistry::new();
989 let error = registry
990 .register_family(&FontFamily::named("Roboto"))
991 .expect_err("a named family has nothing to read");
992 assert!(matches!(error, FontLoadError::NotFileBacked), "{error}");
993 }
994
995 #[test]
996 fn loaded_typeface_families_register_their_single_file() {
997 let dir = ScratchDir::new("typeface");
998 let path = dir.write("Test-Regular.ttf", REGULAR);
999 let family = FontFamily::loaded_typeface_path(path.to_string_lossy().into_owned());
1000
1001 let mut registry = SoftwareTextFontRegistry::new();
1002 registry.register_family(&family).expect("typeface loads");
1003 let fonts = registry.into_font_set_or_default(&[]);
1004
1005 assert!(fonts.has_registered_family(&family));
1006 assert!(
1007 fonts
1008 .resolve(&style_for(&family, FontWeight::NORMAL))
1009 .is_some_and(|font| font.registered_family().is_some())
1010 );
1011 }
1012}