#![forbid(unsafe_code)]
use crate::script_segmentation::{RunDirection, Script};
use lru::LruCache;
use rustc_hash::FxHasher;
use smallvec::SmallVec;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FontId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FontFeature {
pub tag: [u8; 4],
pub value: u32,
}
impl FontFeature {
#[inline]
pub const fn new(tag: [u8; 4], value: u32) -> Self {
Self { tag, value }
}
#[inline]
pub const fn enabled(tag: [u8; 4]) -> Self {
Self { tag, value: 1 }
}
#[inline]
pub const fn disabled(tag: [u8; 4]) -> Self {
Self { tag, value: 0 }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct FontFeatures {
features: SmallVec<[FontFeature; 4]>,
}
impl FontFeatures {
#[inline]
pub fn new() -> Self {
Self {
features: SmallVec::new(),
}
}
#[inline]
pub fn push(&mut self, feature: FontFeature) {
self.features.push(feature);
}
pub fn from_slice(features: &[FontFeature]) -> Self {
Self {
features: SmallVec::from_slice(features),
}
}
#[inline]
pub fn len(&self) -> usize {
self.features.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.features.is_empty()
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &FontFeature> {
self.features.iter()
}
#[inline]
pub fn feature_value(&self, tag: [u8; 4]) -> Option<u32> {
self.features.iter().find(|f| f.tag == tag).map(|f| f.value)
}
pub fn set_feature_value(&mut self, tag: [u8; 4], value: u32) {
if let Some(existing) = self.features.iter_mut().find(|f| f.tag == tag) {
existing.value = value;
} else {
self.features.push(FontFeature::new(tag, value));
}
self.canonicalize();
}
pub fn set_standard_ligatures(&mut self, enabled: bool) {
let value = u32::from(enabled);
self.set_feature_value(*b"liga", value);
self.set_feature_value(*b"clig", value);
}
#[must_use]
pub fn standard_ligatures_enabled(&self) -> Option<bool> {
let mut saw_explicit = false;
let mut enabled = true;
for tag in [*b"liga", *b"clig"] {
if let Some(value) = self.feature_value(tag) {
saw_explicit = true;
enabled &= value != 0;
}
}
saw_explicit.then_some(enabled)
}
pub fn canonicalize(&mut self) {
self.features.sort_by_key(|f| f.tag);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShapedGlyph {
pub glyph_id: u32,
pub cluster: u32,
pub x_advance: i32,
pub y_advance: i32,
pub x_offset: i32,
pub y_offset: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShapedRun {
pub glyphs: Vec<ShapedGlyph>,
pub total_advance: i32,
}
impl ShapedRun {
#[inline]
pub fn len(&self) -> usize {
self.glyphs.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.glyphs.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ShapingKey {
pub text_hash: u64,
pub text_len: u32,
pub script: Script,
pub direction: RunDirection,
pub style_id: u64,
pub font_id: FontId,
pub size_256ths: u32,
pub features: FontFeatures,
}
impl ShapingKey {
#[allow(clippy::too_many_arguments)]
pub fn new(
text: &str,
script: Script,
direction: RunDirection,
style_id: u64,
font_id: FontId,
size_256ths: u32,
features: &FontFeatures,
) -> Self {
let mut hasher = FxHasher::default();
text.hash(&mut hasher);
let text_hash = hasher.finish();
Self {
text_hash,
text_len: text.len() as u32,
script,
direction,
style_id,
font_id,
size_256ths,
features: features.clone(),
}
}
}
pub trait TextShaper {
fn shape(
&self,
text: &str,
script: Script,
direction: RunDirection,
features: &FontFeatures,
) -> ShapedRun;
}
pub struct NoopShaper;
impl TextShaper for NoopShaper {
fn shape(
&self,
text: &str,
_script: Script,
_direction: RunDirection,
_features: &FontFeatures,
) -> ShapedRun {
use unicode_segmentation::UnicodeSegmentation;
let mut glyphs = Vec::new();
let mut total_advance = 0i32;
for (byte_offset, grapheme) in text.grapheme_indices(true) {
let first_char = grapheme.chars().next().unwrap_or('\0');
let width = crate::grapheme_width(grapheme) as i32;
glyphs.push(ShapedGlyph {
glyph_id: first_char as u32,
cluster: byte_offset as u32,
x_advance: width,
y_advance: 0,
x_offset: 0,
y_offset: 0,
});
total_advance += width;
}
ShapedRun {
glyphs,
total_advance,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ShapingCacheStats {
pub hits: u64,
pub misses: u64,
pub stale_evictions: u64,
pub size: usize,
pub capacity: usize,
pub generation: u64,
}
impl ShapingCacheStats {
#[must_use]
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
#[derive(Debug, Clone)]
struct CachedEntry {
run: ShapedRun,
generation: u64,
}
pub struct ShapingCache<S: TextShaper> {
shaper: S,
cache: LruCache<ShapingKey, CachedEntry>,
generation: u64,
stats: ShapingCacheStats,
}
impl<S: TextShaper> ShapingCache<S> {
pub fn new(shaper: S, capacity: usize) -> Self {
let cap = NonZeroUsize::new(capacity.max(1)).expect("capacity must be > 0");
Self {
shaper,
cache: LruCache::new(cap),
generation: 0,
stats: ShapingCacheStats {
capacity,
..Default::default()
},
}
}
pub fn shape(
&mut self,
text: &str,
script: Script,
direction: RunDirection,
font_id: FontId,
size_256ths: u32,
features: &FontFeatures,
) -> ShapedRun {
self.shape_with_style(text, script, direction, 0, font_id, size_256ths, features)
}
#[allow(clippy::too_many_arguments)]
pub fn shape_with_style(
&mut self,
text: &str,
script: Script,
direction: RunDirection,
style_id: u64,
font_id: FontId,
size_256ths: u32,
features: &FontFeatures,
) -> ShapedRun {
let key = ShapingKey::new(
text,
script,
direction,
style_id,
font_id,
size_256ths,
features,
);
if let Some(entry) = self.cache.get(&key) {
if entry.generation == self.generation {
self.stats.hits += 1;
return entry.run.clone();
}
self.stats.stale_evictions += 1;
}
self.stats.misses += 1;
let run = self.shaper.shape(text, script, direction, features);
self.cache.put(
key,
CachedEntry {
run: run.clone(),
generation: self.generation,
},
);
self.stats.size = self.cache.len();
run
}
pub fn invalidate(&mut self) {
self.generation += 1;
self.stats.generation = self.generation;
}
pub fn clear(&mut self) {
self.cache.clear();
self.generation += 1;
self.stats = ShapingCacheStats {
capacity: self.stats.capacity,
generation: self.generation,
..Default::default()
};
}
#[inline]
pub fn stats(&self) -> ShapingCacheStats {
ShapingCacheStats {
size: self.cache.len(),
..self.stats
}
}
#[inline]
pub fn generation(&self) -> u64 {
self.generation
}
#[inline]
pub fn shaper(&self) -> &S {
&self.shaper
}
pub fn resize(&mut self, new_capacity: usize) {
let cap = NonZeroUsize::new(new_capacity.max(1)).expect("capacity must be > 0");
self.cache.resize(cap);
self.stats.capacity = new_capacity;
self.stats.size = self.cache.len();
}
}
#[cfg(feature = "shaping")]
mod rustybuzz_backend {
use super::*;
pub struct RustybuzzShaper {
face: rustybuzz::Face<'static>,
}
impl RustybuzzShaper {
pub fn new(face: rustybuzz::Face<'static>) -> Self {
Self { face }
}
fn to_rb_script(script: Script) -> rustybuzz::Script {
use rustybuzz::script;
match script {
Script::Latin => script::LATIN,
Script::Greek => script::GREEK,
Script::Cyrillic => script::CYRILLIC,
Script::Armenian => script::ARMENIAN,
Script::Hebrew => script::HEBREW,
Script::Arabic => script::ARABIC,
Script::Syriac => script::SYRIAC,
Script::Thaana => script::THAANA,
Script::Devanagari => script::DEVANAGARI,
Script::Bengali => script::BENGALI,
Script::Gurmukhi => script::GURMUKHI,
Script::Gujarati => script::GUJARATI,
Script::Oriya => script::ORIYA,
Script::Tamil => script::TAMIL,
Script::Telugu => script::TELUGU,
Script::Kannada => script::KANNADA,
Script::Malayalam => script::MALAYALAM,
Script::Sinhala => script::SINHALA,
Script::Thai => script::THAI,
Script::Lao => script::LAO,
Script::Tibetan => script::TIBETAN,
Script::Myanmar => script::MYANMAR,
Script::Georgian => script::GEORGIAN,
Script::Hangul => script::HANGUL,
Script::Ethiopic => script::ETHIOPIC,
Script::Han => script::HAN,
Script::Hiragana => script::HIRAGANA,
Script::Katakana => script::KATAKANA,
Script::Bopomofo => script::BOPOMOFO,
Script::Common | Script::Inherited | Script::Unknown => script::COMMON,
}
}
fn to_rb_direction(direction: RunDirection) -> rustybuzz::Direction {
match direction {
RunDirection::Ltr => rustybuzz::Direction::LeftToRight,
RunDirection::Rtl => rustybuzz::Direction::RightToLeft,
}
}
fn to_rb_feature(feature: &FontFeature) -> rustybuzz::Feature {
let tag = rustybuzz::ttf_parser::Tag::from_bytes(&feature.tag);
rustybuzz::Feature::new(tag, feature.value, ..)
}
}
impl TextShaper for RustybuzzShaper {
fn shape(
&self,
text: &str,
script: Script,
direction: RunDirection,
features: &FontFeatures,
) -> ShapedRun {
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_script(Self::to_rb_script(script));
buffer.set_direction(Self::to_rb_direction(direction));
let rb_features: Vec<rustybuzz::Feature> =
features.iter().map(Self::to_rb_feature).collect();
let output = rustybuzz::shape(&self.face, &rb_features, buffer);
let infos = output.glyph_infos();
let positions = output.glyph_positions();
let mut glyphs = Vec::with_capacity(infos.len());
let mut total_advance = 0i32;
for (info, pos) in infos.iter().zip(positions.iter()) {
glyphs.push(ShapedGlyph {
glyph_id: info.glyph_id,
cluster: info.cluster,
x_advance: pos.x_advance,
y_advance: pos.y_advance,
x_offset: pos.x_offset,
y_offset: pos.y_offset,
});
total_advance += pos.x_advance;
}
ShapedRun {
glyphs,
total_advance,
}
}
}
}
#[cfg(feature = "shaping")]
pub use rustybuzz_backend::RustybuzzShaper;
#[cfg(test)]
mod tests {
use super::*;
use crate::script_segmentation::{RunDirection, Script};
#[test]
fn font_feature_new() {
let f = FontFeature::new(*b"liga", 1);
assert_eq!(f.tag, *b"liga");
assert_eq!(f.value, 1);
}
#[test]
fn font_feature_enabled_disabled() {
let on = FontFeature::enabled(*b"kern");
assert_eq!(on.value, 1);
let off = FontFeature::disabled(*b"kern");
assert_eq!(off.value, 0);
}
#[test]
fn font_features_push_and_iter() {
let mut ff = FontFeatures::new();
assert!(ff.is_empty());
ff.push(FontFeature::enabled(*b"liga"));
ff.push(FontFeature::enabled(*b"kern"));
assert_eq!(ff.len(), 2);
let tags: Vec<[u8; 4]> = ff.iter().map(|f| f.tag).collect();
assert_eq!(tags, vec![*b"liga", *b"kern"]);
}
#[test]
fn font_features_canonicalize() {
let mut ff = FontFeatures::from_slice(&[
FontFeature::enabled(*b"kern"),
FontFeature::enabled(*b"aalt"),
FontFeature::enabled(*b"liga"),
]);
ff.canonicalize();
let tags: Vec<[u8; 4]> = ff.iter().map(|f| f.tag).collect();
assert_eq!(tags, vec![*b"aalt", *b"kern", *b"liga"]);
}
#[test]
fn font_features_default_is_empty() {
let ff = FontFeatures::default();
assert!(ff.is_empty());
}
#[test]
fn font_features_set_feature_value_upserts() {
let mut ff = FontFeatures::new();
ff.set_feature_value(*b"liga", 1);
ff.set_feature_value(*b"liga", 0);
ff.set_feature_value(*b"kern", 1);
assert_eq!(ff.feature_value(*b"liga"), Some(0));
assert_eq!(ff.feature_value(*b"kern"), Some(1));
assert_eq!(ff.len(), 2, "upsert should not duplicate existing tags");
}
#[test]
fn font_features_standard_ligatures_toggle() {
let mut ff = FontFeatures::new();
assert_eq!(ff.standard_ligatures_enabled(), None);
ff.set_standard_ligatures(true);
assert_eq!(ff.feature_value(*b"liga"), Some(1));
assert_eq!(ff.feature_value(*b"clig"), Some(1));
assert_eq!(ff.standard_ligatures_enabled(), Some(true));
ff.set_standard_ligatures(false);
assert_eq!(ff.feature_value(*b"liga"), Some(0));
assert_eq!(ff.feature_value(*b"clig"), Some(0));
assert_eq!(ff.standard_ligatures_enabled(), Some(false));
}
#[test]
fn shaped_run_len_and_empty() {
let empty = ShapedRun {
glyphs: vec![],
total_advance: 0,
};
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
let non_empty = ShapedRun {
glyphs: vec![ShapedGlyph {
glyph_id: 65,
cluster: 0,
x_advance: 600,
y_advance: 0,
x_offset: 0,
y_offset: 0,
}],
total_advance: 600,
};
assert!(!non_empty.is_empty());
assert_eq!(non_empty.len(), 1);
}
#[test]
fn shaping_key_same_input_same_key() {
let ff = FontFeatures::default();
let k1 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
let k2 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
assert_eq!(k1, k2);
}
#[test]
fn shaping_key_differs_by_text() {
let ff = FontFeatures::default();
let k1 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
let k2 = ShapingKey::new(
"World",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
assert_ne!(k1, k2);
}
#[test]
fn shaping_key_differs_by_font() {
let ff = FontFeatures::default();
let k1 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
let k2 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(1),
3072,
&ff,
);
assert_ne!(k1, k2);
}
#[test]
fn shaping_key_differs_by_size() {
let ff = FontFeatures::default();
let k1 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
let k2 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
4096,
&ff,
);
assert_ne!(k1, k2);
}
#[test]
fn shaping_key_generation_is_not_part_of_key() {
let ff = FontFeatures::default();
let k1 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
let k2 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
assert_eq!(k1, k2);
}
#[test]
fn shaping_key_differs_by_features() {
let mut ff1 = FontFeatures::default();
ff1.push(FontFeature::enabled(*b"liga"));
let ff2 = FontFeatures::default();
let k1 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff1,
);
let k2 = ShapingKey::new(
"Hello",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff2,
);
assert_ne!(k1, k2);
}
#[test]
fn shaping_key_hashable() {
use std::collections::HashSet;
let ff = FontFeatures::default();
let key = ShapingKey::new(
"test",
Script::Latin,
RunDirection::Ltr,
0,
FontId(0),
3072,
&ff,
);
let mut set = HashSet::new();
set.insert(key.clone());
assert!(set.contains(&key));
}
#[test]
fn noop_shaper_ascii() {
let shaper = NoopShaper;
let ff = FontFeatures::default();
let run = shaper.shape("Hello", Script::Latin, RunDirection::Ltr, &ff);
assert_eq!(run.len(), 5);
assert_eq!(run.total_advance, 5);
assert_eq!(run.glyphs[0].glyph_id, b'H' as u32);
assert_eq!(run.glyphs[1].glyph_id, b'e' as u32);
assert_eq!(run.glyphs[4].glyph_id, b'o' as u32);
assert_eq!(run.glyphs[0].cluster, 0);
assert_eq!(run.glyphs[1].cluster, 1);
assert_eq!(run.glyphs[4].cluster, 4);
}
#[test]
fn noop_shaper_empty() {
let shaper = NoopShaper;
let ff = FontFeatures::default();
let run = shaper.shape("", Script::Latin, RunDirection::Ltr, &ff);
assert!(run.is_empty());
assert_eq!(run.total_advance, 0);
}
#[test]
fn noop_shaper_wide_chars() {
let shaper = NoopShaper;
let ff = FontFeatures::default();
let run = shaper.shape("\u{4E16}\u{754C}", Script::Han, RunDirection::Ltr, &ff);
assert_eq!(run.len(), 2);
assert_eq!(run.total_advance, 4); assert_eq!(run.glyphs[0].x_advance, 2);
assert_eq!(run.glyphs[1].x_advance, 2);
}
#[test]
fn noop_shaper_combining_marks() {
let shaper = NoopShaper;
let ff = FontFeatures::default();
let run = shaper.shape("e\u{0301}", Script::Latin, RunDirection::Ltr, &ff);
assert_eq!(run.len(), 1);
assert_eq!(run.total_advance, 1);
assert_eq!(run.glyphs[0].glyph_id, b'e' as u32);
assert_eq!(run.glyphs[0].cluster, 0);
}
#[test]
fn noop_shaper_ignores_direction_and_features() {
let shaper = NoopShaper;
let mut ff = FontFeatures::new();
ff.push(FontFeature::enabled(*b"liga"));
let ltr = shaper.shape("ABC", Script::Latin, RunDirection::Ltr, &ff);
let rtl = shaper.shape("ABC", Script::Latin, RunDirection::Rtl, &ff);
assert_eq!(ltr, rtl);
}
#[test]
fn cache_hit_on_second_call() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
let r1 = cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
let r2 = cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
assert_eq!(r1, r2);
assert_eq!(cache.stats().hits, 1);
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn cache_miss_on_different_text() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
cache.shape(
"World",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
assert_eq!(cache.stats().hits, 0);
assert_eq!(cache.stats().misses, 2);
}
#[test]
fn cache_miss_on_different_font() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(1),
3072,
&ff,
);
assert_eq!(cache.stats().misses, 2);
}
#[test]
fn cache_miss_on_different_size() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
4096,
&ff,
);
assert_eq!(cache.stats().misses, 2);
}
#[test]
fn cache_miss_on_ligature_feature_toggle() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let mut ligatures_on = FontFeatures::default();
ligatures_on.set_standard_ligatures(true);
let mut ligatures_off = FontFeatures::default();
ligatures_off.set_standard_ligatures(false);
cache.shape(
"office affine",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ligatures_on,
);
cache.shape(
"office affine",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ligatures_off,
);
assert_eq!(
cache.stats().misses,
2,
"ligature mode changes must produce distinct cache keys"
);
}
#[test]
fn cache_hit_with_canonicalized_ligature_feature_order() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let mut ff_a = FontFeatures::new();
ff_a.push(FontFeature::new(*b"clig", 1));
ff_a.push(FontFeature::new(*b"liga", 1));
ff_a.canonicalize();
let mut ff_b = FontFeatures::new();
ff_b.push(FontFeature::new(*b"liga", 1));
ff_b.push(FontFeature::new(*b"clig", 1));
ff_b.canonicalize();
cache.shape(
"offline profile",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff_a,
);
cache.shape(
"offline profile",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff_b,
);
assert_eq!(
cache.stats().hits,
1,
"equivalent ligature features must hit the same key after canonicalization"
);
}
#[test]
fn cache_invalidation_bumps_generation() {
let mut cache = ShapingCache::new(NoopShaper, 64);
assert_eq!(cache.generation(), 0);
cache.invalidate();
assert_eq!(cache.generation(), 1);
cache.invalidate();
assert_eq!(cache.generation(), 2);
}
#[test]
fn cache_stale_entries_are_reshared() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
assert_eq!(cache.stats().misses, 1);
assert_eq!(cache.stats().hits, 0);
cache.invalidate();
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
assert_eq!(cache.stats().misses, 2);
assert_eq!(cache.stats().stale_evictions, 1);
}
#[test]
fn cache_invalidation_recomputes_ligature_entries_after_font_change() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let mut ligatures_on = FontFeatures::default();
ligatures_on.set_standard_ligatures(true);
cache.shape(
"office affine",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ligatures_on,
);
assert_eq!(cache.stats().misses, 1);
assert_eq!(cache.stats().hits, 0);
cache.shape(
"office affine",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ligatures_on,
);
assert_eq!(cache.stats().hits, 1);
cache.invalidate();
cache.shape(
"office affine",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ligatures_on,
);
let stats = cache.stats();
assert_eq!(stats.misses, 2);
assert_eq!(stats.stale_evictions, 1);
assert_eq!(stats.generation, 1);
}
#[test]
fn cache_clear_resets_everything() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
cache.shape(
"Hello",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
cache.shape(
"World",
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
cache.clear();
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.size, 0);
assert!(cache.generation() > 0);
}
#[test]
fn cache_resize_evicts_lru() {
let mut cache = ShapingCache::new(NoopShaper, 4);
let ff = FontFeatures::default();
for i in 0..4u8 {
let text = format!("text{i}");
cache.shape(
&text,
Script::Latin,
RunDirection::Ltr,
FontId(0),
3072,
&ff,
);
}
assert_eq!(cache.stats().size, 4);
cache.resize(2);
assert!(cache.stats().size <= 2);
}
#[test]
fn cache_with_style_id() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
let r1 = cache.shape_with_style(
"Hello",
Script::Latin,
RunDirection::Ltr,
1,
FontId(0),
3072,
&ff,
);
let r2 = cache.shape_with_style(
"Hello",
Script::Latin,
RunDirection::Ltr,
2,
FontId(0),
3072,
&ff,
);
assert_eq!(cache.stats().misses, 2);
assert_eq!(r1, r2);
}
#[test]
fn cache_stats_hit_rate() {
let stats = ShapingCacheStats {
hits: 75,
misses: 25,
..Default::default()
};
let rate = stats.hit_rate();
assert!((rate - 0.75).abs() < f64::EPSILON);
let empty = ShapingCacheStats::default();
assert_eq!(empty.hit_rate(), 0.0);
}
#[test]
fn cache_shaper_accessible() {
let cache = ShapingCache::new(NoopShaper, 64);
let _shaper: &NoopShaper = cache.shaper();
}
#[test]
fn shape_partitioned_runs() {
use crate::script_segmentation::partition_text_runs;
let text = "Hello\u{4E16}\u{754C}World";
let runs = partition_text_runs(text, None, None);
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
let mut total_advance = 0;
for run in &runs {
let shaped = cache.shape(
run.text(text),
run.script,
run.direction,
FontId(0),
3072,
&ff,
);
total_advance += shaped.total_advance;
}
assert_eq!(total_advance, 14);
}
#[test]
fn shape_empty_run() {
let mut cache = ShapingCache::new(NoopShaper, 64);
let ff = FontFeatures::default();
let run = cache.shape("", Script::Latin, RunDirection::Ltr, FontId(0), 3072, &ff);
assert!(run.is_empty());
}
}