use std::collections::HashMap;
use peniko::Brush;
use crate::style::{
FamilyName, FontFamily, FontStyle, GenericSlot, LineHeight, TextAlign, TextStyle,
to_parley_align,
};
pub(crate) const DEFAULT_CAPACITY: usize = 128;
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub struct ShapeCacheStats {
pub shapes: u64,
pub line_breaks: u64,
pub hits: u64,
pub evictions: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct ShapeKey {
text: String,
family: FamilyBits,
weight: u32,
slant: SlantBits,
size: u32,
color: [u32; 4],
letter_spacing: u32,
line_height: LineHeightBits,
align: TextAlign,
}
impl ShapeKey {
pub(crate) fn new(text: &str, style: &TextStyle) -> Self {
Self {
text: text.to_string(),
family: FamilyBits::from(&style.family),
weight: style.weight.value().to_bits(),
slant: SlantBits::from(style.style),
size: style.size.to_bits(),
color: style.color.components.map(f32::to_bits),
letter_spacing: style.letter_spacing.to_bits(),
line_height: LineHeightBits::from(style.line_height),
align: style.align,
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum FamilyBits {
SystemUi,
Named(Vec<String>),
NamedWithGeneric(Vec<(Option<String>, Option<u8>)>),
}
impl From<&FontFamily> for FamilyBits {
fn from(family: &FontFamily) -> Self {
match family {
FontFamily::SystemUi => FamilyBits::SystemUi,
FontFamily::Named(names) => FamilyBits::Named(names.clone()),
FontFamily::NamedWithGeneric(families) => {
let bits = families
.iter()
.map(|f| match f {
FamilyName::Named(name) => (Some(name.clone()), None),
FamilyName::Generic(slot) => {
let discriminant = match slot {
GenericSlot::Monospace => 0u8,
GenericSlot::SansSerif => 1u8,
GenericSlot::Serif => 2u8,
GenericSlot::SystemUi => 3u8,
GenericSlot::Emoji => 4u8,
};
(None, Some(discriminant))
}
})
.collect();
FamilyBits::NamedWithGeneric(bits)
}
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum SlantBits {
Normal,
Italic,
Oblique(Option<u32>),
}
impl From<FontStyle> for SlantBits {
fn from(style: FontStyle) -> Self {
match style {
FontStyle::Normal => SlantBits::Normal,
FontStyle::Italic => SlantBits::Italic,
FontStyle::Oblique(angle) => SlantBits::Oblique(angle.map(f32::to_bits)),
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum LineHeightBits {
MetricsRelative(u32),
FontSizeRelative(u32),
Absolute(u32),
}
impl From<LineHeight> for LineHeightBits {
fn from(line_height: LineHeight) -> Self {
match line_height {
LineHeight::MetricsRelative(v) => LineHeightBits::MetricsRelative(v.to_bits()),
LineHeight::FontSizeRelative(v) => LineHeightBits::FontSizeRelative(v.to_bits()),
LineHeight::Absolute(v) => LineHeightBits::Absolute(v.to_bits()),
}
}
}
struct Entry {
layout: parley::Layout<Brush>,
broken_width: Option<f32>,
last_used: u64,
}
pub(crate) struct ShapeCache {
entries: HashMap<ShapeKey, Entry>,
capacity: usize,
tick: u64,
stats: ShapeCacheStats,
}
impl ShapeCache {
pub(crate) fn new(capacity: usize) -> Self {
Self {
entries: HashMap::new(),
capacity: capacity.max(1),
tick: 0,
stats: ShapeCacheStats::default(),
}
}
pub(crate) fn get(
&mut self,
key: &ShapeKey,
max_width: Option<f32>,
) -> Option<parley::Layout<Brush>> {
self.tick += 1;
let tick = self.tick;
let entry = self.entries.get_mut(key)?;
entry.last_used = tick;
if !same_width(entry.broken_width, max_width) {
entry.layout.break_all_lines(max_width);
entry.layout.align(
to_parley_align(key.align),
parley::layout::AlignmentOptions::default(),
);
entry.broken_width = max_width;
self.stats.line_breaks += 1;
} else {
self.stats.hits += 1;
}
Some(entry.layout.clone())
}
pub(crate) fn insert(
&mut self,
key: ShapeKey,
layout: parley::Layout<Brush>,
broken_width: Option<f32>,
) {
self.stats.shapes += 1;
self.tick += 1;
if self.entries.len() >= self.capacity
&& !self.entries.contains_key(&key)
&& let Some(evict) = self
.entries
.iter()
.min_by_key(|(_, e)| e.last_used)
.map(|(k, _)| k.clone())
{
self.entries.remove(&evict);
self.stats.evictions += 1;
}
self.entries.insert(
key,
Entry {
layout,
broken_width,
last_used: self.tick,
},
);
}
pub(crate) fn stats(&self) -> ShapeCacheStats {
self.stats
}
}
fn same_width(a: Option<f32>, b: Option<f32>) -> bool {
match (a, b) {
(Some(a), Some(b)) => a.to_bits() == b.to_bits(),
(None, None) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use peniko::Color;
fn key(text: &str, size: f32) -> ShapeKey {
ShapeKey::new(text, &TextStyle::new(size, Color::BLACK))
}
#[test]
fn key_ignores_width_but_distinguishes_text_and_style() {
assert_eq!(key("hello", 16.0), key("hello", 16.0));
assert_ne!(key("hello", 16.0), key("world", 16.0));
assert_ne!(key("hello", 16.0), key("hello", 24.0));
}
#[test]
fn key_distinguishes_alignment() {
let start = ShapeKey::new(
"hello",
&TextStyle {
align: TextAlign::Start,
..TextStyle::new(16.0, Color::BLACK)
},
);
let center = ShapeKey::new(
"hello",
&TextStyle {
align: TextAlign::Center,
..TextStyle::new(16.0, Color::BLACK)
},
);
assert_ne!(start, center);
}
#[test]
fn same_width_compares_optionals_bitwise() {
assert!(same_width(None, None));
assert!(same_width(Some(80.0), Some(80.0)));
assert!(!same_width(Some(80.0), None));
assert!(!same_width(Some(80.0), Some(81.0)));
}
}