use crate::compat::{HashMap, RefCell, String, ToString};
use crate::core::Font;
use crate::render::{RenderContext, TextMetrics};
const CACHE_LIMIT: usize = 768;
struct MeasureEntry {
font: Font,
text: String,
dpi_bits: u64,
metrics: TextMetrics,
stamp: u64,
}
struct EstimateEntry {
font: Font,
text: String,
width: u32,
stamp: u64,
}
thread_local! {
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static MEASUREMENTS: RefCell<(HashMap<u64, MeasureEntry>, u64)> =
RefCell::new((HashMap::new(), 0));
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static ESTIMATES: RefCell<(HashMap<u64, EstimateEntry>, u64)> =
RefCell::new((HashMap::new(), 0));
}
fn fingerprint(text: &str, font: &Font, tag: u64, dpi_bits: u64) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325u64 ^ tag;
let mut mix = |value: u64| {
hash = (hash ^ value).wrapping_mul(0x0000_0100_0000_01b3);
};
mix(dpi_bits);
for byte in font.family().bytes() {
mix(u64::from(byte));
}
mix(u64::from(font.size().to_bits()));
mix(u64::from(font.weight()));
mix(u64::from(font.is_italic()));
mix(u64::from(font.letter_spacing().to_bits()));
mix(u64::from(font.word_spacing().to_bits()));
mix(u64::from(font.line_height().to_bits()));
mix(0xff);
for byte in text.bytes() {
mix(u64::from(byte));
}
hash
}
pub(crate) fn measure_text(context: &RenderContext, text: &str, font: &Font) -> TextMetrics {
let key = fingerprint(text, font, 1, u64::from(context.dpi_scale().to_bits()));
let dpi_bits = u64::from(context.dpi_scale().to_bits());
let hit = MEASUREMENTS.with(|cache| {
let mut cache = cache.borrow_mut();
let (entries, stamp) = &mut *cache;
let entry = entries.get_mut(&key)?;
if entry.font != *font || entry.text != text || entry.dpi_bits != dpi_bits {
return None;
}
*stamp = stamp.wrapping_add(1);
entry.stamp = *stamp;
Some(entry.metrics)
});
if let Some(metrics) = hit {
return metrics;
}
let metrics = context.measure_text(text, font);
MEASUREMENTS.with(|cache| {
let mut cache = cache.borrow_mut();
let (entries, stamp) = &mut *cache;
if entries.len() >= CACHE_LIMIT {
evict_measure(entries);
}
*stamp = stamp.wrapping_add(1);
entries.insert(
key,
MeasureEntry {
font: font.clone(),
text: text.to_string(),
dpi_bits,
metrics,
stamp: *stamp,
},
);
});
metrics
}
pub(crate) fn estimate_text_width(text: &str, font: &Font) -> u32 {
let key = fingerprint(text, font, 2, 0);
let hit = ESTIMATES.with(|cache| {
let mut cache = cache.borrow_mut();
let (entries, stamp) = &mut *cache;
let entry = entries.get_mut(&key)?;
if entry.font != *font || entry.text != text {
return None;
}
*stamp = stamp.wrapping_add(1);
entry.stamp = *stamp;
Some(entry.width)
});
if let Some(width) = hit {
return width;
}
let width = crate::widget::metrics::estimate_text_width(text, font, 1.0);
ESTIMATES.with(|cache| {
let mut cache = cache.borrow_mut();
let (entries, stamp) = &mut *cache;
if entries.len() >= CACHE_LIMIT {
evict_estimate(entries);
}
*stamp = stamp.wrapping_add(1);
entries.insert(
key,
EstimateEntry { font: font.clone(), text: text.to_string(), width, stamp: *stamp },
);
});
width
}
fn evict_measure(entries: &mut HashMap<u64, MeasureEntry>) {
let victim = entries.iter().min_by_key(|(_, entry)| entry.stamp).map(|(key, _)| *key);
if let Some(victim) = victim {
entries.remove(&victim);
}
}
fn evict_estimate(entries: &mut HashMap<u64, EstimateEntry>) {
let victim = entries.iter().min_by_key(|(_, entry)| entry.stamp).map(|(key, _)| *key);
if let Some(victim) = victim {
entries.remove(&victim);
}
}