1#![forbid(unsafe_code)]
2
3use crate::script_segmentation::{RunDirection, Script};
66use lru::LruCache;
67use rustc_hash::FxHasher;
68use smallvec::SmallVec;
69use std::hash::{Hash, Hasher};
70use std::num::NonZeroUsize;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
81pub struct FontId(pub u32);
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88pub struct FontFeature {
89 pub tag: [u8; 4],
91 pub value: u32,
93}
94
95impl FontFeature {
96 #[inline]
98 pub const fn new(tag: [u8; 4], value: u32) -> Self {
99 Self { tag, value }
100 }
101
102 #[inline]
104 pub const fn enabled(tag: [u8; 4]) -> Self {
105 Self { tag, value: 1 }
106 }
107
108 #[inline]
110 pub const fn disabled(tag: [u8; 4]) -> Self {
111 Self { tag, value: 0 }
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
119pub struct FontFeatures {
120 features: SmallVec<[FontFeature; 4]>,
121}
122
123impl FontFeatures {
124 #[inline]
126 pub fn new() -> Self {
127 Self {
128 features: SmallVec::new(),
129 }
130 }
131
132 #[inline]
134 pub fn push(&mut self, feature: FontFeature) {
135 self.features.push(feature);
136 }
137
138 pub fn from_slice(features: &[FontFeature]) -> Self {
140 Self {
141 features: SmallVec::from_slice(features),
142 }
143 }
144
145 #[inline]
147 pub fn len(&self) -> usize {
148 self.features.len()
149 }
150
151 #[inline]
153 pub fn is_empty(&self) -> bool {
154 self.features.is_empty()
155 }
156
157 #[inline]
159 pub fn iter(&self) -> impl Iterator<Item = &FontFeature> {
160 self.features.iter()
161 }
162
163 pub fn canonicalize(&mut self) {
165 self.features.sort_by_key(|f| f.tag);
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct ShapedGlyph {
179 pub glyph_id: u32,
181 pub cluster: u32,
187 pub x_advance: i32,
189 pub y_advance: i32,
191 pub x_offset: i32,
193 pub y_offset: i32,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct ShapedRun {
202 pub glyphs: Vec<ShapedGlyph>,
204 pub total_advance: i32,
206}
207
208impl ShapedRun {
209 #[inline]
211 pub fn len(&self) -> usize {
212 self.glyphs.len()
213 }
214
215 #[inline]
217 pub fn is_empty(&self) -> bool {
218 self.glyphs.is_empty()
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Hash)]
246pub struct ShapingKey {
247 pub text_hash: u64,
249 pub text_len: u32,
251 pub script: Script,
253 pub direction: RunDirection,
255 pub style_id: u64,
257 pub font_id: FontId,
259 pub size_256ths: u32,
261 pub features: FontFeatures,
263 pub generation: u64,
265}
266
267impl ShapingKey {
268 #[allow(clippy::too_many_arguments)]
270 pub fn new(
271 text: &str,
272 script: Script,
273 direction: RunDirection,
274 style_id: u64,
275 font_id: FontId,
276 size_256ths: u32,
277 features: &FontFeatures,
278 generation: u64,
279 ) -> Self {
280 let mut hasher = FxHasher::default();
281 text.hash(&mut hasher);
282 let text_hash = hasher.finish();
283
284 Self {
285 text_hash,
286 text_len: text.len() as u32,
287 script,
288 direction,
289 style_id,
290 font_id,
291 size_256ths,
292 features: features.clone(),
293 generation,
294 }
295 }
296}
297
298pub trait TextShaper {
311 fn shape(
324 &self,
325 text: &str,
326 script: Script,
327 direction: RunDirection,
328 features: &FontFeatures,
329 ) -> ShapedRun;
330}
331
332pub struct NoopShaper;
345
346impl TextShaper for NoopShaper {
347 fn shape(
348 &self,
349 text: &str,
350 _script: Script,
351 _direction: RunDirection,
352 _features: &FontFeatures,
353 ) -> ShapedRun {
354 use unicode_segmentation::UnicodeSegmentation;
355
356 let mut glyphs = Vec::new();
357 let mut total_advance = 0i32;
358
359 for (byte_offset, grapheme) in text.grapheme_indices(true) {
360 let first_char = grapheme.chars().next().unwrap_or('\0');
361 let width = crate::grapheme_width(grapheme) as i32;
362
363 glyphs.push(ShapedGlyph {
364 glyph_id: first_char as u32,
365 cluster: byte_offset as u32,
366 x_advance: width,
367 y_advance: 0,
368 x_offset: 0,
369 y_offset: 0,
370 });
371
372 total_advance += width;
373 }
374
375 ShapedRun {
376 glyphs,
377 total_advance,
378 }
379 }
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
388pub struct ShapingCacheStats {
389 pub hits: u64,
391 pub misses: u64,
393 pub stale_evictions: u64,
395 pub size: usize,
397 pub capacity: usize,
399 pub generation: u64,
401}
402
403impl ShapingCacheStats {
404 #[must_use]
406 pub fn hit_rate(&self) -> f64 {
407 let total = self.hits + self.misses;
408 if total == 0 {
409 0.0
410 } else {
411 self.hits as f64 / total as f64
412 }
413 }
414}
415
416#[derive(Debug, Clone)]
418struct CachedEntry {
419 run: ShapedRun,
420 generation: u64,
421}
422
423pub struct ShapingCache<S: TextShaper> {
441 shaper: S,
442 cache: LruCache<ShapingKey, CachedEntry>,
443 generation: u64,
444 stats: ShapingCacheStats,
445}
446
447impl<S: TextShaper> ShapingCache<S> {
448 pub fn new(shaper: S, capacity: usize) -> Self {
450 let cap = NonZeroUsize::new(capacity.max(1)).expect("capacity must be > 0");
451 Self {
452 shaper,
453 cache: LruCache::new(cap),
454 generation: 0,
455 stats: ShapingCacheStats {
456 capacity,
457 ..Default::default()
458 },
459 }
460 }
461
462 pub fn shape(
468 &mut self,
469 text: &str,
470 script: Script,
471 direction: RunDirection,
472 font_id: FontId,
473 size_256ths: u32,
474 features: &FontFeatures,
475 ) -> ShapedRun {
476 self.shape_with_style(text, script, direction, 0, font_id, size_256ths, features)
477 }
478
479 #[allow(clippy::too_many_arguments)]
481 pub fn shape_with_style(
482 &mut self,
483 text: &str,
484 script: Script,
485 direction: RunDirection,
486 style_id: u64,
487 font_id: FontId,
488 size_256ths: u32,
489 features: &FontFeatures,
490 ) -> ShapedRun {
491 let key = ShapingKey::new(
492 text,
493 script,
494 direction,
495 style_id,
496 font_id,
497 size_256ths,
498 features,
499 self.generation,
500 );
501
502 if let Some(entry) = self.cache.get(&key) {
504 if entry.generation == self.generation {
505 self.stats.hits += 1;
506 return entry.run.clone();
507 }
508 self.stats.stale_evictions += 1;
510 }
511
512 self.stats.misses += 1;
514 let run = self.shaper.shape(text, script, direction, features);
515
516 self.cache.put(
517 key,
518 CachedEntry {
519 run: run.clone(),
520 generation: self.generation,
521 },
522 );
523
524 self.stats.size = self.cache.len();
525 run
526 }
527
528 pub fn invalidate(&mut self) {
538 self.generation += 1;
539 self.stats.generation = self.generation;
540 }
541
542 pub fn clear(&mut self) {
544 self.cache.clear();
545 self.generation += 1;
546 self.stats = ShapingCacheStats {
547 capacity: self.stats.capacity,
548 generation: self.generation,
549 ..Default::default()
550 };
551 }
552
553 #[inline]
555 pub fn stats(&self) -> ShapingCacheStats {
556 ShapingCacheStats {
557 size: self.cache.len(),
558 ..self.stats
559 }
560 }
561
562 #[inline]
564 pub fn generation(&self) -> u64 {
565 self.generation
566 }
567
568 #[inline]
570 pub fn shaper(&self) -> &S {
571 &self.shaper
572 }
573
574 pub fn resize(&mut self, new_capacity: usize) {
579 let cap = NonZeroUsize::new(new_capacity.max(1)).expect("capacity must be > 0");
580 self.cache.resize(cap);
581 self.stats.capacity = new_capacity;
582 self.stats.size = self.cache.len();
583 }
584}
585
586#[cfg(feature = "shaping")]
591mod rustybuzz_backend {
592 use super::*;
593
594 pub struct RustybuzzShaper {
607 face: rustybuzz::Face<'static>,
608 }
609
610 impl RustybuzzShaper {
611 pub fn new(face: rustybuzz::Face<'static>) -> Self {
617 Self { face }
618 }
619
620 fn to_rb_script(script: Script) -> rustybuzz::Script {
622 use rustybuzz::script;
623 match script {
624 Script::Latin => script::LATIN,
625 Script::Greek => script::GREEK,
626 Script::Cyrillic => script::CYRILLIC,
627 Script::Armenian => script::ARMENIAN,
628 Script::Hebrew => script::HEBREW,
629 Script::Arabic => script::ARABIC,
630 Script::Syriac => script::SYRIAC,
631 Script::Thaana => script::THAANA,
632 Script::Devanagari => script::DEVANAGARI,
633 Script::Bengali => script::BENGALI,
634 Script::Gurmukhi => script::GURMUKHI,
635 Script::Gujarati => script::GUJARATI,
636 Script::Oriya => script::ORIYA,
637 Script::Tamil => script::TAMIL,
638 Script::Telugu => script::TELUGU,
639 Script::Kannada => script::KANNADA,
640 Script::Malayalam => script::MALAYALAM,
641 Script::Sinhala => script::SINHALA,
642 Script::Thai => script::THAI,
643 Script::Lao => script::LAO,
644 Script::Tibetan => script::TIBETAN,
645 Script::Myanmar => script::MYANMAR,
646 Script::Georgian => script::GEORGIAN,
647 Script::Hangul => script::HANGUL,
648 Script::Ethiopic => script::ETHIOPIC,
649 Script::Han => script::HAN,
650 Script::Hiragana => script::HIRAGANA,
651 Script::Katakana => script::KATAKANA,
652 Script::Bopomofo => script::BOPOMOFO,
653 Script::Common | Script::Inherited | Script::Unknown => script::COMMON,
654 }
655 }
656
657 fn to_rb_direction(direction: RunDirection) -> rustybuzz::Direction {
659 match direction {
660 RunDirection::Ltr => rustybuzz::Direction::LeftToRight,
661 RunDirection::Rtl => rustybuzz::Direction::RightToLeft,
662 }
663 }
664
665 fn to_rb_feature(feature: &FontFeature) -> rustybuzz::Feature {
667 let tag = rustybuzz::ttf_parser::Tag::from_bytes(&feature.tag);
668 rustybuzz::Feature::new(tag, feature.value, ..)
669 }
670 }
671
672 impl TextShaper for RustybuzzShaper {
673 fn shape(
674 &self,
675 text: &str,
676 script: Script,
677 direction: RunDirection,
678 features: &FontFeatures,
679 ) -> ShapedRun {
680 let mut buffer = rustybuzz::UnicodeBuffer::new();
681 buffer.push_str(text);
682 buffer.set_script(Self::to_rb_script(script));
683 buffer.set_direction(Self::to_rb_direction(direction));
684
685 let rb_features: Vec<rustybuzz::Feature> =
686 features.iter().map(Self::to_rb_feature).collect();
687
688 let output = rustybuzz::shape(&self.face, &rb_features, buffer);
689
690 let infos = output.glyph_infos();
691 let positions = output.glyph_positions();
692
693 let mut glyphs = Vec::with_capacity(infos.len());
694 let mut total_advance = 0i32;
695
696 for (info, pos) in infos.iter().zip(positions.iter()) {
697 glyphs.push(ShapedGlyph {
698 glyph_id: info.glyph_id,
699 cluster: info.cluster,
700 x_advance: pos.x_advance,
701 y_advance: pos.y_advance,
702 x_offset: pos.x_offset,
703 y_offset: pos.y_offset,
704 });
705 total_advance += pos.x_advance;
706 }
707
708 ShapedRun {
709 glyphs,
710 total_advance,
711 }
712 }
713 }
714}
715
716#[cfg(feature = "shaping")]
717pub use rustybuzz_backend::RustybuzzShaper;
718
719#[cfg(test)]
724mod tests {
725 use super::*;
726 use crate::script_segmentation::{RunDirection, Script};
727
728 #[test]
733 fn font_feature_new() {
734 let f = FontFeature::new(*b"liga", 1);
735 assert_eq!(f.tag, *b"liga");
736 assert_eq!(f.value, 1);
737 }
738
739 #[test]
740 fn font_feature_enabled_disabled() {
741 let on = FontFeature::enabled(*b"kern");
742 assert_eq!(on.value, 1);
743
744 let off = FontFeature::disabled(*b"kern");
745 assert_eq!(off.value, 0);
746 }
747
748 #[test]
749 fn font_features_push_and_iter() {
750 let mut ff = FontFeatures::new();
751 assert!(ff.is_empty());
752
753 ff.push(FontFeature::enabled(*b"liga"));
754 ff.push(FontFeature::enabled(*b"kern"));
755 assert_eq!(ff.len(), 2);
756
757 let tags: Vec<[u8; 4]> = ff.iter().map(|f| f.tag).collect();
758 assert_eq!(tags, vec![*b"liga", *b"kern"]);
759 }
760
761 #[test]
762 fn font_features_canonicalize() {
763 let mut ff = FontFeatures::from_slice(&[
764 FontFeature::enabled(*b"kern"),
765 FontFeature::enabled(*b"aalt"),
766 FontFeature::enabled(*b"liga"),
767 ]);
768 ff.canonicalize();
769 let tags: Vec<[u8; 4]> = ff.iter().map(|f| f.tag).collect();
770 assert_eq!(tags, vec![*b"aalt", *b"kern", *b"liga"]);
771 }
772
773 #[test]
774 fn font_features_default_is_empty() {
775 let ff = FontFeatures::default();
776 assert!(ff.is_empty());
777 }
778
779 #[test]
784 fn shaped_run_len_and_empty() {
785 let empty = ShapedRun {
786 glyphs: vec![],
787 total_advance: 0,
788 };
789 assert!(empty.is_empty());
790 assert_eq!(empty.len(), 0);
791
792 let non_empty = ShapedRun {
793 glyphs: vec![ShapedGlyph {
794 glyph_id: 65,
795 cluster: 0,
796 x_advance: 600,
797 y_advance: 0,
798 x_offset: 0,
799 y_offset: 0,
800 }],
801 total_advance: 600,
802 };
803 assert!(!non_empty.is_empty());
804 assert_eq!(non_empty.len(), 1);
805 }
806
807 #[test]
812 fn shaping_key_same_input_same_key() {
813 let ff = FontFeatures::default();
814 let k1 = ShapingKey::new(
815 "Hello",
816 Script::Latin,
817 RunDirection::Ltr,
818 0,
819 FontId(0),
820 3072,
821 &ff,
822 0,
823 );
824 let k2 = ShapingKey::new(
825 "Hello",
826 Script::Latin,
827 RunDirection::Ltr,
828 0,
829 FontId(0),
830 3072,
831 &ff,
832 0,
833 );
834 assert_eq!(k1, k2);
835 }
836
837 #[test]
838 fn shaping_key_differs_by_text() {
839 let ff = FontFeatures::default();
840 let k1 = ShapingKey::new(
841 "Hello",
842 Script::Latin,
843 RunDirection::Ltr,
844 0,
845 FontId(0),
846 3072,
847 &ff,
848 0,
849 );
850 let k2 = ShapingKey::new(
851 "World",
852 Script::Latin,
853 RunDirection::Ltr,
854 0,
855 FontId(0),
856 3072,
857 &ff,
858 0,
859 );
860 assert_ne!(k1, k2);
861 }
862
863 #[test]
864 fn shaping_key_differs_by_font() {
865 let ff = FontFeatures::default();
866 let k1 = ShapingKey::new(
867 "Hello",
868 Script::Latin,
869 RunDirection::Ltr,
870 0,
871 FontId(0),
872 3072,
873 &ff,
874 0,
875 );
876 let k2 = ShapingKey::new(
877 "Hello",
878 Script::Latin,
879 RunDirection::Ltr,
880 0,
881 FontId(1),
882 3072,
883 &ff,
884 0,
885 );
886 assert_ne!(k1, k2);
887 }
888
889 #[test]
890 fn shaping_key_differs_by_size() {
891 let ff = FontFeatures::default();
892 let k1 = ShapingKey::new(
893 "Hello",
894 Script::Latin,
895 RunDirection::Ltr,
896 0,
897 FontId(0),
898 3072,
899 &ff,
900 0,
901 );
902 let k2 = ShapingKey::new(
903 "Hello",
904 Script::Latin,
905 RunDirection::Ltr,
906 0,
907 FontId(0),
908 4096,
909 &ff,
910 0,
911 );
912 assert_ne!(k1, k2);
913 }
914
915 #[test]
916 fn shaping_key_differs_by_generation() {
917 let ff = FontFeatures::default();
918 let k1 = ShapingKey::new(
919 "Hello",
920 Script::Latin,
921 RunDirection::Ltr,
922 0,
923 FontId(0),
924 3072,
925 &ff,
926 0,
927 );
928 let k2 = ShapingKey::new(
929 "Hello",
930 Script::Latin,
931 RunDirection::Ltr,
932 0,
933 FontId(0),
934 3072,
935 &ff,
936 1,
937 );
938 assert_ne!(k1, k2);
939 }
940
941 #[test]
942 fn shaping_key_differs_by_features() {
943 let mut ff1 = FontFeatures::default();
944 ff1.push(FontFeature::enabled(*b"liga"));
945
946 let ff2 = FontFeatures::default();
947
948 let k1 = ShapingKey::new(
949 "Hello",
950 Script::Latin,
951 RunDirection::Ltr,
952 0,
953 FontId(0),
954 3072,
955 &ff1,
956 0,
957 );
958 let k2 = ShapingKey::new(
959 "Hello",
960 Script::Latin,
961 RunDirection::Ltr,
962 0,
963 FontId(0),
964 3072,
965 &ff2,
966 0,
967 );
968 assert_ne!(k1, k2);
969 }
970
971 #[test]
972 fn shaping_key_hashable() {
973 use std::collections::HashSet;
974 let ff = FontFeatures::default();
975 let key = ShapingKey::new(
976 "test",
977 Script::Latin,
978 RunDirection::Ltr,
979 0,
980 FontId(0),
981 3072,
982 &ff,
983 0,
984 );
985 let mut set = HashSet::new();
986 set.insert(key.clone());
987 assert!(set.contains(&key));
988 }
989
990 #[test]
995 fn noop_shaper_ascii() {
996 let shaper = NoopShaper;
997 let ff = FontFeatures::default();
998 let run = shaper.shape("Hello", Script::Latin, RunDirection::Ltr, &ff);
999
1000 assert_eq!(run.len(), 5);
1001 assert_eq!(run.total_advance, 5); assert_eq!(run.glyphs[0].glyph_id, b'H' as u32);
1005 assert_eq!(run.glyphs[1].glyph_id, b'e' as u32);
1006 assert_eq!(run.glyphs[4].glyph_id, b'o' as u32);
1007
1008 assert_eq!(run.glyphs[0].cluster, 0);
1010 assert_eq!(run.glyphs[1].cluster, 1);
1011 assert_eq!(run.glyphs[4].cluster, 4);
1012 }
1013
1014 #[test]
1015 fn noop_shaper_empty() {
1016 let shaper = NoopShaper;
1017 let ff = FontFeatures::default();
1018 let run = shaper.shape("", Script::Latin, RunDirection::Ltr, &ff);
1019 assert!(run.is_empty());
1020 assert_eq!(run.total_advance, 0);
1021 }
1022
1023 #[test]
1024 fn noop_shaper_wide_chars() {
1025 let shaper = NoopShaper;
1026 let ff = FontFeatures::default();
1027 let run = shaper.shape("\u{4E16}\u{754C}", Script::Han, RunDirection::Ltr, &ff);
1029
1030 assert_eq!(run.len(), 2);
1031 assert_eq!(run.total_advance, 4); assert_eq!(run.glyphs[0].x_advance, 2);
1033 assert_eq!(run.glyphs[1].x_advance, 2);
1034 }
1035
1036 #[test]
1037 fn noop_shaper_combining_marks() {
1038 let shaper = NoopShaper;
1039 let ff = FontFeatures::default();
1040 let run = shaper.shape("e\u{0301}", Script::Latin, RunDirection::Ltr, &ff);
1042
1043 assert_eq!(run.len(), 1);
1045 assert_eq!(run.total_advance, 1);
1046 assert_eq!(run.glyphs[0].glyph_id, b'e' as u32);
1047 assert_eq!(run.glyphs[0].cluster, 0);
1048 }
1049
1050 #[test]
1051 fn noop_shaper_ignores_direction_and_features() {
1052 let shaper = NoopShaper;
1053 let mut ff = FontFeatures::new();
1054 ff.push(FontFeature::enabled(*b"liga"));
1055
1056 let ltr = shaper.shape("ABC", Script::Latin, RunDirection::Ltr, &ff);
1057 let rtl = shaper.shape("ABC", Script::Latin, RunDirection::Rtl, &ff);
1058
1059 assert_eq!(ltr, rtl);
1061 }
1062
1063 #[test]
1068 fn cache_hit_on_second_call() {
1069 let mut cache = ShapingCache::new(NoopShaper, 64);
1070 let ff = FontFeatures::default();
1071
1072 let r1 = cache.shape(
1073 "Hello",
1074 Script::Latin,
1075 RunDirection::Ltr,
1076 FontId(0),
1077 3072,
1078 &ff,
1079 );
1080 let r2 = cache.shape(
1081 "Hello",
1082 Script::Latin,
1083 RunDirection::Ltr,
1084 FontId(0),
1085 3072,
1086 &ff,
1087 );
1088
1089 assert_eq!(r1, r2);
1090 assert_eq!(cache.stats().hits, 1);
1091 assert_eq!(cache.stats().misses, 1);
1092 }
1093
1094 #[test]
1095 fn cache_miss_on_different_text() {
1096 let mut cache = ShapingCache::new(NoopShaper, 64);
1097 let ff = FontFeatures::default();
1098
1099 cache.shape(
1100 "Hello",
1101 Script::Latin,
1102 RunDirection::Ltr,
1103 FontId(0),
1104 3072,
1105 &ff,
1106 );
1107 cache.shape(
1108 "World",
1109 Script::Latin,
1110 RunDirection::Ltr,
1111 FontId(0),
1112 3072,
1113 &ff,
1114 );
1115
1116 assert_eq!(cache.stats().hits, 0);
1117 assert_eq!(cache.stats().misses, 2);
1118 }
1119
1120 #[test]
1121 fn cache_miss_on_different_font() {
1122 let mut cache = ShapingCache::new(NoopShaper, 64);
1123 let ff = FontFeatures::default();
1124
1125 cache.shape(
1126 "Hello",
1127 Script::Latin,
1128 RunDirection::Ltr,
1129 FontId(0),
1130 3072,
1131 &ff,
1132 );
1133 cache.shape(
1134 "Hello",
1135 Script::Latin,
1136 RunDirection::Ltr,
1137 FontId(1),
1138 3072,
1139 &ff,
1140 );
1141
1142 assert_eq!(cache.stats().misses, 2);
1143 }
1144
1145 #[test]
1146 fn cache_miss_on_different_size() {
1147 let mut cache = ShapingCache::new(NoopShaper, 64);
1148 let ff = FontFeatures::default();
1149
1150 cache.shape(
1151 "Hello",
1152 Script::Latin,
1153 RunDirection::Ltr,
1154 FontId(0),
1155 3072,
1156 &ff,
1157 );
1158 cache.shape(
1159 "Hello",
1160 Script::Latin,
1161 RunDirection::Ltr,
1162 FontId(0),
1163 4096,
1164 &ff,
1165 );
1166
1167 assert_eq!(cache.stats().misses, 2);
1168 }
1169
1170 #[test]
1171 fn cache_invalidation_bumps_generation() {
1172 let mut cache = ShapingCache::new(NoopShaper, 64);
1173 assert_eq!(cache.generation(), 0);
1174
1175 cache.invalidate();
1176 assert_eq!(cache.generation(), 1);
1177
1178 cache.invalidate();
1179 assert_eq!(cache.generation(), 2);
1180 }
1181
1182 #[test]
1183 fn cache_stale_entries_are_reshared() {
1184 let mut cache = ShapingCache::new(NoopShaper, 64);
1185 let ff = FontFeatures::default();
1186
1187 cache.shape(
1189 "Hello",
1190 Script::Latin,
1191 RunDirection::Ltr,
1192 FontId(0),
1193 3072,
1194 &ff,
1195 );
1196 assert_eq!(cache.stats().misses, 1);
1197 assert_eq!(cache.stats().hits, 0);
1198
1199 cache.invalidate();
1201
1202 cache.shape(
1204 "Hello",
1205 Script::Latin,
1206 RunDirection::Ltr,
1207 FontId(0),
1208 3072,
1209 &ff,
1210 );
1211 assert_eq!(cache.stats().misses, 2);
1212 assert_eq!(cache.stats().stale_evictions, 0); }
1214
1215 #[test]
1216 fn cache_clear_resets_everything() {
1217 let mut cache = ShapingCache::new(NoopShaper, 64);
1218 let ff = FontFeatures::default();
1219
1220 cache.shape(
1221 "Hello",
1222 Script::Latin,
1223 RunDirection::Ltr,
1224 FontId(0),
1225 3072,
1226 &ff,
1227 );
1228 cache.shape(
1229 "World",
1230 Script::Latin,
1231 RunDirection::Ltr,
1232 FontId(0),
1233 3072,
1234 &ff,
1235 );
1236
1237 cache.clear();
1238
1239 let stats = cache.stats();
1240 assert_eq!(stats.hits, 0);
1241 assert_eq!(stats.misses, 0);
1242 assert_eq!(stats.size, 0);
1243 assert!(cache.generation() > 0);
1244 }
1245
1246 #[test]
1247 fn cache_resize_evicts_lru() {
1248 let mut cache = ShapingCache::new(NoopShaper, 4);
1249 let ff = FontFeatures::default();
1250
1251 for i in 0..4u8 {
1253 let text = format!("text{i}");
1254 cache.shape(
1255 &text,
1256 Script::Latin,
1257 RunDirection::Ltr,
1258 FontId(0),
1259 3072,
1260 &ff,
1261 );
1262 }
1263 assert_eq!(cache.stats().size, 4);
1264
1265 cache.resize(2);
1267 assert!(cache.stats().size <= 2);
1268 }
1269
1270 #[test]
1271 fn cache_with_style_id() {
1272 let mut cache = ShapingCache::new(NoopShaper, 64);
1273 let ff = FontFeatures::default();
1274
1275 let r1 = cache.shape_with_style(
1276 "Hello",
1277 Script::Latin,
1278 RunDirection::Ltr,
1279 1,
1280 FontId(0),
1281 3072,
1282 &ff,
1283 );
1284 let r2 = cache.shape_with_style(
1285 "Hello",
1286 Script::Latin,
1287 RunDirection::Ltr,
1288 2,
1289 FontId(0),
1290 3072,
1291 &ff,
1292 );
1293
1294 assert_eq!(cache.stats().misses, 2);
1296 assert_eq!(r1, r2);
1298 }
1299
1300 #[test]
1301 fn cache_stats_hit_rate() {
1302 let stats = ShapingCacheStats {
1303 hits: 75,
1304 misses: 25,
1305 ..Default::default()
1306 };
1307 let rate = stats.hit_rate();
1308 assert!((rate - 0.75).abs() < f64::EPSILON);
1309
1310 let empty = ShapingCacheStats::default();
1311 assert_eq!(empty.hit_rate(), 0.0);
1312 }
1313
1314 #[test]
1315 fn cache_shaper_accessible() {
1316 let cache = ShapingCache::new(NoopShaper, 64);
1317 let _shaper: &NoopShaper = cache.shaper();
1318 }
1319
1320 #[test]
1325 fn shape_partitioned_runs() {
1326 use crate::script_segmentation::partition_text_runs;
1327
1328 let text = "Hello\u{4E16}\u{754C}World";
1329 let runs = partition_text_runs(text, None, None);
1330
1331 let mut cache = ShapingCache::new(NoopShaper, 64);
1332 let ff = FontFeatures::default();
1333
1334 let mut total_advance = 0;
1335 for run in &runs {
1336 let shaped = cache.shape(
1337 run.text(text),
1338 run.script,
1339 run.direction,
1340 FontId(0),
1341 3072,
1342 &ff,
1343 );
1344 total_advance += shaped.total_advance;
1345 }
1346
1347 assert_eq!(total_advance, 14);
1349 }
1350
1351 #[test]
1352 fn shape_empty_run() {
1353 let mut cache = ShapingCache::new(NoopShaper, 64);
1354 let ff = FontFeatures::default();
1355 let run = cache.shape("", Script::Latin, RunDirection::Ltr, FontId(0), 3072, &ff);
1356 assert!(run.is_empty());
1357 }
1358}