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 pub fn register_face_path(
128 &mut self,
129 family: &FontFamily,
130 weight: FontWeight,
131 style: FontStyle,
132 path: impl AsRef<Path>,
133 ) -> Result<(), FontLoadError> {
134 self.register_read_face(
135 &mut FontFileReads::default(),
136 family,
137 weight,
138 style,
139 path.as_ref(),
140 )
141 }
142
143 fn register_read_face(
144 &mut self,
145 reads: &mut FontFileReads,
146 family: &FontFamily,
147 weight: FontWeight,
148 style: FontStyle,
149 path: &Path,
150 ) -> Result<(), FontLoadError> {
151 let bytes = reads.read(path)?;
152 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes.to_vec())
153 .map_err(|source| FontLoadError::Parse {
154 path: path.to_path_buf(),
155 source,
156 })?;
157 self.faces.push(face);
158 Ok(())
159 }
160
161 pub fn register_face_reader(
168 &mut self,
169 family: &FontFamily,
170 weight: FontWeight,
171 style: FontStyle,
172 reader: &mut impl Read,
173 ) -> Result<(), FontLoadError> {
174 let mut bytes = Vec::new();
175 reader
176 .read_to_end(&mut bytes)
177 .map_err(|source| FontLoadError::Read {
178 path: PathBuf::new(),
179 source,
180 })?;
181 self.register_face_bytes(family, weight, style, bytes)
182 }
183
184 pub fn register_face_bytes(
186 &mut self,
187 family: &FontFamily,
188 weight: FontWeight,
189 style: FontStyle,
190 bytes: impl Into<Vec<u8>>,
191 ) -> Result<(), FontLoadError> {
192 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes)
193 .map_err(|source| FontLoadError::ParseBytes { source })?;
194 self.faces.push(face);
195 Ok(())
196 }
197
198 pub fn register_fallback_bytes(
203 &mut self,
204 bytes: impl Into<Vec<u8>>,
205 ) -> Result<(), FontLoadError> {
206 let face = SoftwareTextFont::from_bytes(bytes)
207 .map_err(|source| FontLoadError::ParseBytes { source })?;
208 self.faces.push(face);
209 Ok(())
210 }
211
212 pub fn register_system_family(
229 &mut self,
230 directory: impl AsRef<Path>,
231 family: &FontFamily,
232 weights: &[FontWeight],
233 ) -> Result<(), FontLoadError> {
234 let directory = directory.as_ref();
235 let mut reads = FontFileReads::default();
236 let mut first_error = None;
237 let mut loaded = 0usize;
238 for weight in weights {
239 match self.register_read_system_face(
240 &mut reads,
241 directory,
242 family,
243 *weight,
244 FontStyle::Normal,
245 ) {
246 Ok(()) => loaded += 1,
247 Err(error) => first_error = first_error.or(Some(error)),
248 }
249 }
250
251 match first_error {
252 Some(error) if loaded == 0 => Err(error),
253 _ => Ok(()),
254 }
255 }
256
257 pub fn register_system_face(
266 &mut self,
267 directory: impl AsRef<Path>,
268 family: &FontFamily,
269 weight: FontWeight,
270 style: FontStyle,
271 ) -> Result<(), FontLoadError> {
272 self.register_read_system_face(
273 &mut FontFileReads::default(),
274 directory.as_ref(),
275 family,
276 weight,
277 style,
278 )
279 }
280
281 fn register_read_system_face(
282 &mut self,
283 reads: &mut FontFileReads,
284 directory: &Path,
285 family: &FontFamily,
286 weight: FontWeight,
287 style: FontStyle,
288 ) -> Result<(), FontLoadError> {
289 let weight = system_declared_weight(family, weight);
294 if self.has_system_face(family, weight, style) {
295 return Ok(());
300 }
301 let path = system_font_file(directory, family, weight).ok_or_else(|| {
302 FontLoadError::NoSystemFontFile {
303 directory: directory.to_path_buf(),
304 }
305 })?;
306 self.register_read_face(reads, family, weight, style, &path)?;
307 self.system_faces
308 .push((FontFamilyKey::of(family), weight, style));
309 Ok(())
310 }
311
312 fn has_system_face(&self, family: &FontFamily, weight: FontWeight, style: FontStyle) -> bool {
318 self.system_faces
319 .contains(&(FontFamilyKey::of(family), weight, style))
320 }
321
322 pub fn faces(&self) -> &[SoftwareTextFont] {
324 &self.faces
325 }
326
327 pub fn is_empty(&self) -> bool {
328 self.faces.is_empty()
329 }
330
331 pub fn into_font_set_or_default(mut self, fonts: &[&[u8]]) -> SoftwareTextFontSet {
339 for bytes in fonts {
340 let _ = self.register_fallback_bytes((*bytes).to_vec());
342 }
343 if self.faces.is_empty() {
344 if let Some(default_font) = default_software_text_font() {
345 self.faces.push(default_font);
346 }
347 }
348 SoftwareTextFontSet::from_faces(self.faces)
349 }
350}
351
352#[derive(Default)]
359struct FontFileReads {
360 entries: Vec<(PathBuf, Arc<[u8]>)>,
361}
362
363impl FontFileReads {
364 fn read(&mut self, path: &Path) -> Result<Arc<[u8]>, FontLoadError> {
365 if let Some((_, bytes)) = self.entries.iter().find(|(read, _)| read == path) {
366 return Ok(Arc::clone(bytes));
367 }
368 let bytes: Arc<[u8]> = std::fs::read(path)
369 .map_err(|source| FontLoadError::Read {
370 path: path.to_path_buf(),
371 source,
372 })?
373 .into();
374 self.entries.push((path.to_path_buf(), Arc::clone(&bytes)));
375 Ok(bytes)
376 }
377}
378
379pub fn system_declared_weight(family: &FontFamily, weight: FontWeight) -> FontWeight {
423 let Some(files) = system_family_files(family) else {
424 return weight;
425 };
426 closest_declared_weight(files.declared, weight).unwrap_or(weight)
427}
428
429fn closest_declared_weight(declared: &[u16], requested: FontWeight) -> Option<FontWeight> {
431 let mut best: Option<(u16, u16)> = None;
432 for candidate in declared {
433 let score = weight_match_score(*candidate, requested.value());
434 if best.is_none_or(|(_, best_score)| score < best_score) {
436 best = Some((*candidate, score));
437 }
438 }
439 best.map(|(candidate, _)| FontWeight(candidate))
440}
441
442fn weight_match_score(declared: u16, requested: u16) -> u16 {
444 (declared / 100).abs_diff(requested / 100)
445}
446
447pub fn system_font_file(
454 directory: &Path,
455 family: &FontFamily,
456 weight: FontWeight,
457) -> Option<PathBuf> {
458 let files = system_family_files(family)?;
459 files
460 .weighted
461 .iter()
462 .filter(|(candidate_weight, _)| *candidate_weight == weight.value())
463 .map(|(_, name)| directory.join(name))
464 .chain(files.regular.iter().map(|name| directory.join(name)))
465 .find(|path| path.is_file())
466}
467
468struct SystemFamilyFiles {
471 regular: &'static [&'static str],
472 weighted: &'static [(u16, &'static str)],
473 declared: &'static [u16],
478}
479
480const DECLARED_HUNDREDS: &[u16] = &[100, 200, 300, 400, 500, 600, 700, 800, 900];
484
485fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
486 match family {
490 FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
491 regular: &[
492 "Roboto-Regular.ttf",
493 "RobotoStatic-Regular.ttf",
494 "NotoSans-Regular.ttf",
495 "DroidSans.ttf",
496 ],
497 weighted: &[
498 (300, "Roboto-Light.ttf"),
499 (500, "Roboto-Medium.ttf"),
500 (700, "Roboto-Bold.ttf"),
501 (900, "Roboto-Black.ttf"),
502 ],
503 declared: DECLARED_HUNDREDS,
504 }),
505 FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
507 regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
508 weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
509 declared: &[400, 700],
510 }),
511 FontFamily::Monospace => Some(SystemFamilyFiles {
512 regular: &[
513 "DroidSansMono.ttf",
514 "RobotoMono-Regular.ttf",
515 "CutiveMono-Regular.ttf",
516 ],
517 weighted: &[(700, "RobotoMono-Bold.ttf")],
518 declared: &[400, 700],
519 }),
520 FontFamily::Cursive => Some(SystemFamilyFiles {
521 regular: &["DancingScript-Regular.ttf"],
522 weighted: &[(700, "DancingScript-Bold.ttf")],
523 declared: &[400, 700],
524 }),
525 FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
526 }
527}
528
529fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
530 match family {
531 FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
532 FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
533 _ => Err(FontLoadError::NotFileBacked),
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use cranpose_ui::text::{SpanStyle, TextStyle};
541 use std::io::Cursor;
542
543 const REGULAR: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
544 const BOLD: &[u8] = include_bytes!("../assets/NotoSansBold.ttf");
545
546 fn style_for(family: &FontFamily, weight: FontWeight) -> TextStyle {
547 TextStyle {
548 span_style: SpanStyle {
549 font_family: Some(family.clone()),
550 font_weight: Some(weight),
551 ..Default::default()
552 },
553 ..Default::default()
554 }
555 }
556
557 struct ScratchDir(PathBuf);
561
562 impl ScratchDir {
563 fn new(name: &str) -> Self {
564 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
565 .join("../../../target/test-output/cranpose-font-source")
566 .join(name);
567 let _ = std::fs::remove_dir_all(&path);
568 std::fs::create_dir_all(&path).expect("scratch directory");
569 Self(path)
570 }
571
572 fn write(&self, name: &str, bytes: &[u8]) -> PathBuf {
573 let path = self.0.join(name);
574 std::fs::write(&path, bytes).expect("scratch font file");
575 path
576 }
577
578 fn path(&self) -> &Path {
579 &self.0
580 }
581 }
582
583 impl Drop for ScratchDir {
584 fn drop(&mut self) {
585 let _ = std::fs::remove_dir_all(&self.0);
586 }
587 }
588
589 #[test]
590 fn register_family_picks_the_face_matching_the_requested_weight() {
591 let dir = ScratchDir::new("weights");
592 let regular = dir.write("Test-Regular.ttf", REGULAR);
593 let bold = dir.write("Test-Bold.ttf", BOLD);
594 let family = FontFamily::file_backed(vec![
595 FontFile::new(regular.to_string_lossy().into_owned()),
596 FontFile::new(bold.to_string_lossy().into_owned()).with_weight(FontWeight::BOLD),
597 ])
598 .expect("file-backed family");
599
600 let mut registry = SoftwareTextFontRegistry::new();
601 registry.register_family(&family).expect("family loads");
602 let fonts = registry.into_font_set_or_default(&[]);
603
604 let resolved_regular = fonts
605 .resolve(&style_for(&family, FontWeight::NORMAL))
606 .expect("regular face");
607 let resolved_bold = fonts
608 .resolve(&style_for(&family, FontWeight::BOLD))
609 .expect("bold face");
610
611 assert_eq!(resolved_regular.weight(), FontWeight::NORMAL);
612 assert_eq!(resolved_bold.weight(), FontWeight::BOLD);
613 assert_ne!(
614 resolved_regular.content_hash(),
615 resolved_bold.content_hash(),
616 "distinct faces must key the glyph atlas distinctly"
617 );
618 }
619
620 #[test]
621 fn register_family_honours_a_declared_weight_over_the_face_header() {
622 let dir = ScratchDir::new("declared");
623 let path = dir.write("Test-Regular.ttf", REGULAR);
624 let family =
625 FontFamily::file_backed(vec![
626 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight::MEDIUM)
627 ])
628 .expect("file-backed family");
629
630 let mut registry = SoftwareTextFontRegistry::new();
631 registry.register_family(&family).expect("family loads");
632 let fonts = registry.into_font_set_or_default(&[]);
633
634 let resolved = fonts
635 .resolve(&style_for(&family, FontWeight::MEDIUM))
636 .expect("declared face");
637 assert_eq!(resolved.weight(), FontWeight::MEDIUM);
638 }
639
640 #[test]
641 fn register_family_reports_a_missing_file_without_panicking() {
642 let dir = ScratchDir::new("missing");
643 let family = FontFamily::file_backed(vec![FontFile::new(
644 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
645 )])
646 .expect("file-backed family");
647
648 let mut registry = SoftwareTextFontRegistry::new();
649 let error = registry
650 .register_family(&family)
651 .expect_err("a missing file cannot register");
652 assert!(matches!(error, FontLoadError::Read { .. }), "{error}");
653 assert!(registry.is_empty());
654 }
655
656 #[test]
657 fn register_family_reports_a_corrupt_file_without_panicking() {
658 let dir = ScratchDir::new("corrupt");
659 let path = dir.write("Corrupt.ttf", b"this is not a font");
660 let family =
661 FontFamily::file_backed(vec![FontFile::new(path.to_string_lossy().into_owned())])
662 .expect("file-backed family");
663
664 let mut registry = SoftwareTextFontRegistry::new();
665 let error = registry
666 .register_family(&family)
667 .expect_err("a corrupt file cannot register");
668 assert!(matches!(error, FontLoadError::Parse { .. }), "{error}");
669 assert!(registry.is_empty());
670 }
671
672 #[test]
673 #[cfg(feature = "embedded-default-font")]
674 fn a_family_that_failed_to_load_falls_back_to_the_default_face() {
675 let dir = ScratchDir::new("fallback");
676 let family = FontFamily::file_backed(vec![FontFile::new(
677 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
678 )])
679 .expect("file-backed family");
680
681 let mut registry = SoftwareTextFontRegistry::new();
682 let _ = registry.register_family(&family);
683 let fonts = registry.into_font_set_or_default(&[]);
684
685 let resolved = fonts
686 .resolve(&style_for(&family, FontWeight::NORMAL))
687 .expect("fallback face");
688 assert_eq!(
689 resolved.content_hash(),
690 fonts.default_font().expect("default face").content_hash()
691 );
692 }
693
694 #[test]
695 fn a_partly_loadable_family_keeps_the_faces_that_did_load() {
696 let dir = ScratchDir::new("partial");
697 let regular = dir.write("Test-Regular.ttf", REGULAR);
698 let family = FontFamily::file_backed(vec![
699 FontFile::new(regular.to_string_lossy().into_owned()),
700 FontFile::new(dir.path().join("Absent.ttf").to_string_lossy().into_owned())
701 .with_weight(FontWeight::BOLD),
702 ])
703 .expect("file-backed family");
704
705 let mut registry = SoftwareTextFontRegistry::new();
706 registry
707 .register_family(&family)
708 .expect("one readable face is enough");
709 assert_eq!(registry.faces().len(), 1);
710 }
711
712 #[test]
713 fn register_face_reader_accepts_a_font_that_is_not_a_file() {
714 let family = FontFamily::named("Bundled");
715 let mut registry = SoftwareTextFontRegistry::new();
716 registry
717 .register_face_reader(
718 &family,
719 FontWeight::NORMAL,
720 FontStyle::Normal,
721 &mut Cursor::new(REGULAR.to_vec()),
722 )
723 .expect("streamed face loads");
724
725 let fonts = registry.into_font_set_or_default(&[]);
726 let resolved = fonts
727 .resolve(&style_for(&family, FontWeight::NORMAL))
728 .expect("streamed face");
729 assert_eq!(resolved.registered_family(), {
730 let mut expected = SoftwareTextFontRegistry::new();
731 expected
732 .register_face_bytes(
733 &family,
734 FontWeight::NORMAL,
735 FontStyle::Normal,
736 REGULAR.to_vec(),
737 )
738 .expect("face loads");
739 expected.faces()[0].registered_family()
740 });
741 }
742
743 #[test]
744 #[cfg(feature = "embedded-default-font")]
745 fn an_empty_registry_falls_back_to_the_embedded_default_face() {
746 let fonts = SoftwareTextFontRegistry::new().into_font_set_or_default(&[]);
747 assert!(
748 fonts.default_font().is_some(),
749 "the embedded default font must still serve apps that supply nothing"
750 );
751 assert!(fonts
752 .resolve(&TextStyle::default())
753 .is_some_and(|font| font.registered_family().is_none()));
754 }
755
756 #[test]
757 fn system_font_file_prefers_a_weight_specific_static_face() {
758 let dir = ScratchDir::new("system-static");
759 dir.write("Roboto-Regular.ttf", REGULAR);
760 let medium = dir.write("Roboto-Medium.ttf", BOLD);
761
762 assert_eq!(
763 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
764 Some(medium)
765 );
766 }
767
768 #[test]
769 fn system_font_file_falls_back_to_the_regular_face_for_other_weights() {
770 let dir = ScratchDir::new("system-regular");
771 let regular = dir.write("Roboto-Regular.ttf", REGULAR);
772
773 assert_eq!(
774 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
775 Some(regular)
776 );
777 }
778
779 #[test]
780 fn system_font_file_reports_nothing_when_the_directory_is_empty() {
781 let dir = ScratchDir::new("system-empty");
782 assert_eq!(
783 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::NORMAL),
784 None
785 );
786 assert_eq!(
787 system_font_file(dir.path(), &FontFamily::named("Roboto"), FontWeight::NORMAL),
788 None
789 );
790 }
791
792 #[test]
793 fn register_system_family_binds_the_generic_alias_to_the_platform_face() {
794 let dir = ScratchDir::new("system-family");
795 dir.write("Roboto-Regular.ttf", REGULAR);
796 dir.write("Roboto-Bold.ttf", BOLD);
797
798 let mut registry = SoftwareTextFontRegistry::new();
799 registry
800 .register_system_family(
801 dir.path(),
802 &FontFamily::SansSerif,
803 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
804 )
805 .expect("system family loads");
806 let fonts = registry.into_font_set_or_default(&[]);
807
808 assert!(fonts.has_registered_family(&FontFamily::SansSerif));
809 let bold = fonts
810 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::BOLD))
811 .expect("bold system face");
812 assert_eq!(bold.weight(), FontWeight::BOLD);
813 assert_eq!(bold.registered_family(), {
814 let key = fonts
815 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
816 .expect("regular system face")
817 .registered_family();
818 key
819 });
820 }
821
822 #[test]
823 fn register_system_family_reports_an_absent_font_directory() {
824 let mut registry = SoftwareTextFontRegistry::new();
825 let error = registry
826 .register_system_family(
827 Path::new("/definitely/not/a/font/directory"),
828 &FontFamily::SansSerif,
829 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
830 )
831 .expect_err("an absent directory cannot register");
832 assert!(
833 matches!(error, FontLoadError::NoSystemFontFile { .. }),
834 "{error}"
835 );
836 assert!(registry.is_empty());
837 }
838
839 #[test]
840 fn a_system_weight_the_font_config_does_not_declare_resolves_to_the_declared_one() {
841 for (requested, expected) in [(450u16, 400u16), (550, 500), (401, 400), (599, 500)] {
844 assert_eq!(
845 system_declared_weight(&FontFamily::SansSerif, FontWeight(requested)),
846 FontWeight(expected),
847 "sans-serif {requested}"
848 );
849 }
850 for weight in DECLARED_HUNDREDS {
852 assert_eq!(
853 system_declared_weight(&FontFamily::SansSerif, FontWeight(*weight)),
854 FontWeight(*weight)
855 );
856 }
857 assert_eq!(
859 system_declared_weight(&FontFamily::SansSerif, FontWeight(50)),
860 FontWeight(100)
861 );
862 assert_eq!(
863 system_declared_weight(&FontFamily::SansSerif, FontWeight(1000)),
864 FontWeight(900)
865 );
866 }
867
868 #[test]
869 fn a_sparse_system_family_resolves_by_distance_and_keeps_the_lighter_face_on_a_tie() {
870 for (requested, expected) in [(500u16, 400u16), (550, 400), (600, 700), (650, 700)] {
874 assert_eq!(
875 system_declared_weight(&FontFamily::Serif, FontWeight(requested)),
876 FontWeight(expected),
877 "serif {requested}"
878 );
879 }
880 assert_eq!(
883 closest_declared_weight(&[300, 500], FontWeight(400)),
884 Some(FontWeight(300))
885 );
886 assert_eq!(
887 closest_declared_weight(&[500, 300], FontWeight(400)),
888 Some(FontWeight(500)),
889 "declaration order, not magnitude, is what breaks the tie"
890 );
891 }
892
893 #[test]
894 fn a_family_with_no_system_files_keeps_the_weight_it_was_given() {
895 assert_eq!(
898 system_declared_weight(&FontFamily::named("Roboto"), FontWeight(450)),
899 FontWeight(450)
900 );
901 }
902
903 #[test]
904 fn register_system_face_registers_the_declared_weight_not_the_requested_one() {
905 let dir = ScratchDir::new("system-undeclared-weight");
906 dir.write("Roboto-Regular.ttf", REGULAR);
907
908 let mut registry = SoftwareTextFontRegistry::new();
909 registry
910 .register_system_face(
911 dir.path(),
912 &FontFamily::SansSerif,
913 FontWeight(450),
914 FontStyle::Normal,
915 )
916 .expect("system face loads");
917 let fonts = registry.into_font_set_or_default(&[]);
918
919 let resolved = fonts
922 .resolve(&style_for(&FontFamily::SansSerif, FontWeight(450)))
923 .expect("a face for the off-grid request");
924 assert_eq!(
925 resolved.weight(),
926 FontWeight::NORMAL,
927 "450 must land on the 400 entry Android's matcher returns"
928 );
929
930 let mut declared = SoftwareTextFontRegistry::new();
931 declared
932 .register_system_face(
933 dir.path(),
934 &FontFamily::SansSerif,
935 FontWeight::NORMAL,
936 FontStyle::Normal,
937 )
938 .expect("system face loads");
939 let declared = declared.into_font_set_or_default(&[]);
940 assert_eq!(
941 resolved.content_hash(),
942 declared
943 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
944 .expect("the 400 face")
945 .content_hash(),
946 "the off-grid request must produce the very same face, glyph masks included"
947 );
948 }
949
950 #[test]
951 fn system_requests_that_resolve_alike_register_one_face() {
952 let dir = ScratchDir::new("system-dedupe");
953 dir.write("Roboto-Regular.ttf", REGULAR);
954
955 let mut registry = SoftwareTextFontRegistry::new();
956 for weight in [400u16, 450, 499] {
957 registry
958 .register_system_face(
959 dir.path(),
960 &FontFamily::SansSerif,
961 FontWeight(weight),
962 FontStyle::Normal,
963 )
964 .expect("system face loads");
965 }
966
967 assert_eq!(
968 registry.faces().len(),
969 1,
970 "three requests for one declared entry are one face"
971 );
972 }
973
974 #[test]
975 fn an_app_registered_face_keeps_the_weight_it_declares() {
976 let dir = ScratchDir::new("app-off-grid");
980 let path = dir.write("Test-Regular.ttf", REGULAR);
981 let family =
982 FontFamily::file_backed(vec![
983 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight(450))
984 ])
985 .expect("file-backed family");
986
987 let mut registry = SoftwareTextFontRegistry::new();
988 registry.register_family(&family).expect("family loads");
989 let fonts = registry.into_font_set_or_default(&[]);
990
991 assert_eq!(
992 fonts
993 .resolve(&style_for(&family, FontWeight(450)))
994 .expect("app face")
995 .weight(),
996 FontWeight(450)
997 );
998 }
999
1000 #[test]
1001 fn register_family_rejects_a_family_that_names_no_files() {
1002 let mut registry = SoftwareTextFontRegistry::new();
1003 let error = registry
1004 .register_family(&FontFamily::named("Roboto"))
1005 .expect_err("a named family has nothing to read");
1006 assert!(matches!(error, FontLoadError::NotFileBacked), "{error}");
1007 }
1008
1009 #[test]
1010 fn loaded_typeface_families_register_their_single_file() {
1011 let dir = ScratchDir::new("typeface");
1012 let path = dir.write("Test-Regular.ttf", REGULAR);
1013 let family = FontFamily::loaded_typeface_path(path.to_string_lossy().into_owned());
1014
1015 let mut registry = SoftwareTextFontRegistry::new();
1016 registry.register_family(&family).expect("typeface loads");
1017 let fonts = registry.into_font_set_or_default(&[]);
1018
1019 assert!(fonts.has_registered_family(&family));
1020 assert!(fonts
1021 .resolve(&style_for(&family, FontWeight::NORMAL))
1022 .is_some_and(|font| font.registered_family().is_some()));
1023 }
1024}