use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use std::sync::Arc;
use super::run::{RichTextRun, RichTextWidth};
use super::style::RichTextStyleSheet;
use crate::color::Color;
use crate::style_vocab::{HAlign, Palette};
use crate::text::{FontFamilyEntry, TextStyle};
const CAPACITY: usize = 256;
#[derive(Debug, Clone, PartialEq)]
pub struct RichKey {
source: String,
style: TextStyle,
brush: Color,
sheet: usize,
palette: Palette,
dpi: u64,
width: Option<i32>,
alignment: HAlign,
}
impl RichKey {
#[allow(clippy::too_many_arguments)]
pub fn new(
source: &str,
style: &TextStyle,
brush: Color,
sheet: &Arc<RichTextStyleSheet>,
palette: &Palette,
dpi: f64,
width: RichTextWidth,
alignment: HAlign,
) -> Self {
Self {
source: source.to_string(),
style: style.clone(),
brush,
sheet: Arc::as_ptr(sheet) as usize,
palette: *palette,
dpi: dpi.to_bits(),
width: match width {
RichTextWidth::Natural => None,
RichTextWidth::Fixed(px) => Some(px.round() as i32),
},
alignment,
}
}
fn hash_value(&self) -> u64 {
let mut h = DefaultHasher::new();
self.source.hash(&mut h);
hash_style(&self.style, &mut h);
for c in self.brush.components {
c.to_bits().hash(&mut h);
}
self.sheet.hash(&mut h);
for anchor in [self.palette.paper, self.palette.ink, self.palette.accent] {
for c in anchor.components {
c.to_bits().hash(&mut h);
}
}
self.dpi.hash(&mut h);
self.width.hash(&mut h);
self.alignment.hash(&mut h);
h.finish()
}
}
fn hash_style(style: &TextStyle, h: &mut DefaultHasher) {
style.size_pt.to_bits().hash(h);
style.weight.hash(h);
style.width.to_bits().hash(h);
format!("{:?}", style.style).hash(h);
format!("{:?}", style.line_height).hash(h);
style.letter_spacing_pt.to_bits().hash(h);
style.underline.hash(h);
style.strikethrough.hash(h);
for f in &style.families {
match f {
FontFamilyEntry::Named(n) => n.hash(h),
FontFamilyEntry::Generic(k) => format!("{k:?}").hash(h),
}
}
for f in &style.features {
f.tag.hash(h);
f.value.hash(h);
}
for v in &style.variations {
v.tag.hash(h);
v.value.to_bits().hash(h);
}
}
struct Entry {
key: RichKey,
run: Rc<RichTextRun>,
last_used: u64,
}
#[derive(Default)]
pub struct RichShapeCache {
entries: RefCell<HashMap<u64, Vec<Entry>>>,
clock: RefCell<u64>,
len: RefCell<usize>,
}
impl RichShapeCache {
pub fn new() -> Self {
Self::default()
}
pub fn clear(&self) {
self.entries.borrow_mut().clear();
*self.len.borrow_mut() = 0;
}
pub fn len(&self) -> usize {
*self.len.borrow()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get_or_shape(
&self,
key: RichKey,
build: impl FnOnce() -> RichTextRun,
) -> Rc<RichTextRun> {
let hash = key.hash_value();
let stamp = {
let mut clock = self.clock.borrow_mut();
*clock += 1;
*clock
};
{
let mut entries = self.entries.borrow_mut();
if let Some(bucket) = entries.get_mut(&hash) {
if let Some(e) = bucket.iter_mut().find(|e| e.key == key) {
e.last_used = stamp;
return Rc::clone(&e.run);
}
}
}
let run = Rc::new(build());
{
let mut entries = self.entries.borrow_mut();
entries.entry(hash).or_default().push(Entry {
key,
run: Rc::clone(&run),
last_used: stamp,
});
*self.len.borrow_mut() += 1;
}
self.evict_if_full();
run
}
fn evict_if_full(&self) {
if *self.len.borrow() <= CAPACITY {
return;
}
let target = CAPACITY * 3 / 4;
let mut entries = self.entries.borrow_mut();
let mut ages: Vec<u64> = entries
.values()
.flat_map(|b| b.iter().map(|e| e.last_used))
.collect();
ages.sort_unstable();
let cutoff = ages[ages.len().saturating_sub(target)];
let mut kept = 0usize;
entries.retain(|_, bucket| {
bucket.retain(|e| e.last_used >= cutoff);
kept += bucket.len();
!bucket.is_empty()
});
*self.len.borrow_mut() = kept;
}
}
impl std::fmt::Debug for RichShapeCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RichShapeCache")
.field("len", &self.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(source: &str, sheet: &Arc<RichTextStyleSheet>, palette: &Palette) -> RichKey {
RichKey::new(
source,
&TextStyle::new(12.0),
Color::from_rgba8(0, 0, 0, 255),
sheet,
palette,
96.0,
RichTextWidth::Natural,
HAlign::Start,
)
}
fn shape(source: &str, sheet: &Arc<RichTextStyleSheet>, palette: &Palette) -> RichTextRun {
RichTextRun::new(
source,
&TextStyle::new(12.0),
Color::from_rgba8(0, 0, 0, 255),
sheet,
palette,
96.0,
)
}
#[test]
fn the_same_key_hands_back_the_same_run() {
let cache = RichShapeCache::new();
let sheet = Arc::new(RichTextStyleSheet::new());
let palette = Palette::default();
let a = cache.get_or_shape(key("**hi**", &sheet, &palette), || {
shape("**hi**", &sheet, &palette)
});
let b = cache.get_or_shape(key("**hi**", &sheet, &palette), || {
panic!("second lookup must hit the cache")
});
assert!(Rc::ptr_eq(&a, &b));
assert_eq!(cache.len(), 1);
}
#[test]
fn a_different_palette_is_a_different_entry() {
let cache = RichShapeCache::new();
let sheet = Arc::new(RichTextStyleSheet::new());
let light = Palette::default();
let dark = Palette::new(
Color::from_rgba8(0, 0, 0, 255),
Color::from_rgba8(255, 255, 255, 255),
Color::from_rgba8(51, 105, 232, 255),
);
cache.get_or_shape(key("hi", &sheet, &light), || shape("hi", &sheet, &light));
cache.get_or_shape(key("hi", &sheet, &dark), || shape("hi", &sheet, &dark));
assert_eq!(cache.len(), 2);
}
#[test]
fn a_different_sheet_is_a_different_entry() {
let cache = RichShapeCache::new();
let palette = Palette::default();
let a = Arc::new(RichTextStyleSheet::new());
let b = Arc::new(RichTextStyleSheet::new());
cache.get_or_shape(key("hi", &a, &palette), || shape("hi", &a, &palette));
cache.get_or_shape(key("hi", &b, &palette), || shape("hi", &b, &palette));
assert_eq!(cache.len(), 2, "sheets are keyed by identity");
}
#[test]
fn stale_entries_are_evicted_once_over_capacity() {
let cache = RichShapeCache::new();
let sheet = Arc::new(RichTextStyleSheet::new());
let palette = Palette::default();
for i in 0..(CAPACITY + 20) {
let src = format!("label {i}");
cache.get_or_shape(key(&src, &sheet, &palette), || {
shape(&src, &sheet, &palette)
});
}
assert!(cache.len() <= CAPACITY, "got {}", cache.len());
assert!(!cache.is_empty());
}
#[test]
fn clear_empties_the_cache() {
let cache = RichShapeCache::new();
let sheet = Arc::new(RichTextStyleSheet::new());
let palette = Palette::default();
cache.get_or_shape(key("hi", &sheet, &palette), || {
shape("hi", &sheet, &palette)
});
assert!(!cache.is_empty());
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn recently_used_entries_survive_eviction() {
let cache = RichShapeCache::new();
let sheet = Arc::new(RichTextStyleSheet::new());
let palette = Palette::default();
let hot = "the label that keeps being drawn";
let first = cache.get_or_shape(key(hot, &sheet, &palette), || shape(hot, &sheet, &palette));
for i in 0..(CAPACITY + 20) {
let src = format!("label {i}");
cache.get_or_shape(key(&src, &sheet, &palette), || {
shape(&src, &sheet, &palette)
});
cache.get_or_shape(key(hot, &sheet, &palette), || shape(hot, &sheet, &palette));
}
let again = cache.get_or_shape(key(hot, &sheet, &palette), || {
panic!("the hot entry must not have been evicted")
});
assert!(Rc::ptr_eq(&first, &again));
}
}