use crate::text::Font;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)]
pub struct FontMetrics {
widths: HashMap<char, u16>,
default_width: u16,
}
impl FontMetrics {
pub fn new(default_width: u16) -> Self {
Self {
widths: HashMap::new(),
default_width,
}
}
pub fn with_widths(mut self, widths: &[(char, u16)]) -> Self {
for &(ch, width) in widths {
self.widths.insert(ch, width);
}
self
}
pub fn from_char_map(widths: HashMap<char, u16>, default_width: u16) -> Self {
Self {
widths,
default_width,
}
}
pub fn char_width(&self, ch: char) -> u16 {
self.widths.get(&ch).copied().unwrap_or(self.default_width)
}
}
#[derive(Clone, Debug)]
pub struct FontMetricsStore {
inner: Arc<RwLock<HashMap<String, Arc<FontMetrics>>>>,
}
impl FontMetricsStore {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn register(&self, font_name: impl Into<String>, metrics: FontMetrics) {
let name = font_name.into();
match self.inner.write() {
Ok(mut map) => {
map.insert(name, Arc::new(metrics));
}
Err(e) => {
tracing::warn!(
"FontMetricsStore lock is poisoned; could not register '{}': {}",
name,
e
);
}
}
}
pub fn get(&self, font_name: &str) -> Option<Arc<FontMetrics>> {
let map = self.inner.read().ok()?;
map.get(font_name).cloned()
}
pub fn len(&self) -> usize {
self.inner.read().map(|m| m.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.inner.read().map(|m| m.is_empty()).unwrap_or(true)
}
}
impl Default for FontMetricsStore {
fn default() -> Self {
Self::new()
}
}
lazy_static::lazy_static! {
static ref CUSTOM_FONT_METRICS: RwLock<HashMap<String, FontMetrics>> =
RwLock::new(HashMap::new());
}
lazy_static::lazy_static! {
static ref FONT_METRICS: HashMap<Font, FontMetrics> = {
use crate::text::encoding::winansi_decode_char;
use crate::text::fonts::get_standard_font_metrics;
let fonts = [
Font::Helvetica,
Font::HelveticaBold,
Font::HelveticaOblique,
Font::HelveticaBoldOblique,
Font::TimesRoman,
Font::TimesBold,
Font::TimesItalic,
Font::TimesBoldItalic,
Font::Courier,
Font::CourierBold,
Font::CourierOblique,
Font::CourierBoldOblique,
];
let mut metrics = HashMap::new();
for font in fonts {
if let Some(sm) = get_standard_font_metrics(&font) {
let mut widths = HashMap::new();
for code in 0u16..=255 {
let ch = winansi_decode_char(code as u8);
widths.insert(ch, sm.widths[code as usize] as u16);
}
metrics.insert(
font,
FontMetrics::from_char_map(widths, sm.default_width as u16),
);
}
}
metrics
};
}
pub fn measure_text_with(
text: &str,
font: &Font,
font_size: f64,
store: Option<&FontMetricsStore>,
) -> f64 {
if font.is_symbolic() {
return text.len() as f64 * font_size * 0.6;
}
let metrics = lookup(font, store);
let width_units: u32 = text.chars().map(|ch| metrics.char_width(ch) as u32).sum();
(width_units as f64 / 1000.0) * font_size
}
#[inline]
pub fn measure_text(text: &str, font: &Font, font_size: f64) -> f64 {
measure_text_with(text, font, font_size, None)
}
pub fn measure_char_with(
ch: char,
font: Font,
font_size: f64,
store: Option<&FontMetricsStore>,
) -> f64 {
if font.is_symbolic() {
return font_size * 0.6;
}
let metrics = lookup(&font, store);
(metrics.char_width(ch) as f64 / 1000.0) * font_size
}
#[inline]
pub fn measure_char(ch: char, font: Font, font_size: f64) -> f64 {
measure_char_with(ch, font, font_size, None)
}
pub fn split_into_words(text: &str) -> Vec<&str> {
let mut words = Vec::new();
let mut start = 0;
let mut in_space = false;
for (i, ch) in text.char_indices() {
if ch.is_whitespace() {
if !in_space {
if i > start {
words.push(&text[start..i]);
}
start = i;
in_space = true;
}
} else if in_space {
if i > start {
words.push(&text[start..i]);
}
start = i;
in_space = false;
}
}
if start < text.len() {
words.push(&text[start..]);
}
words
}
#[deprecated(
since = "2.8.0",
note = "use Document::add_font_from_bytes; the global registry is process-wide and not bounded — see issue #230"
)]
pub fn register_custom_font_metrics(font_name: String, metrics: FontMetrics) {
match CUSTOM_FONT_METRICS.write() {
Ok(mut custom_metrics) => {
custom_metrics.insert(font_name, metrics);
}
Err(e) => {
tracing::warn!(
"Font metrics registry lock is poisoned; \
could not register metrics for font '{}': {}",
font_name,
e
);
}
}
}
#[deprecated(
since = "2.8.0",
note = "use FontMetricsStore::get via a Document — the global registry is process-wide and not bounded — see issue #230"
)]
pub fn get_custom_font_metrics(font_name: &str) -> Option<FontMetrics> {
if let Ok(custom_metrics) = CUSTOM_FONT_METRICS.read() {
custom_metrics.get(font_name).cloned()
} else {
None
}
}
fn lookup(font: &Font, store: Option<&FontMetricsStore>) -> FontMetrics {
match font {
Font::Custom(font_name) => {
if let Some(s) = store {
if let Some(arc_m) = s.get(font_name) {
return (*arc_m).clone();
}
}
if let Some(custom_metrics) = get_custom_font_metrics_internal(font_name) {
return custom_metrics;
}
warn_unknown_custom_font_once(font_name);
(*default_custom_metrics_arc()).clone()
}
_ => FONT_METRICS.get(font).cloned().unwrap_or_else(|| {
tracing::debug!(
"Warning: Standard font metrics not found for {:?}, using default",
font
);
(*default_custom_metrics_arc()).clone()
}),
}
}
fn get_custom_font_metrics_internal(font_name: &str) -> Option<FontMetrics> {
if let Ok(custom_metrics) = CUSTOM_FONT_METRICS.read() {
custom_metrics.get(font_name).cloned()
} else {
None
}
}
lazy_static::lazy_static! {
static ref DEFAULT_CUSTOM_METRICS_ARC: Arc<FontMetrics> =
Arc::new(create_default_custom_metrics());
}
fn default_custom_metrics_arc() -> Arc<FontMetrics> {
DEFAULT_CUSTOM_METRICS_ARC.clone()
}
lazy_static::lazy_static! {
static ref WARNED_UNKNOWN_FONTS: RwLock<std::collections::HashSet<String>> =
RwLock::new(std::collections::HashSet::new());
}
fn warn_unknown_custom_font_once(font_name: &str) {
{
if let Ok(set) = WARNED_UNKNOWN_FONTS.read() {
if set.contains(font_name) {
return;
}
}
}
if let Ok(mut set) = WARNED_UNKNOWN_FONTS.write() {
if set.insert(font_name.to_string()) {
tracing::warn!(
"custom font '{}' measured but not registered; widths will use \
defaults — register via Document::add_font_from_bytes",
font_name
);
}
}
}
pub(crate) fn create_default_custom_metrics() -> FontMetrics {
lazy_static::lazy_static! {
static ref DEFAULT_CUSTOM_METRICS: FontMetrics = build_default_custom_metrics();
}
DEFAULT_CUSTOM_METRICS.clone()
}
#[cfg(test)]
pub(crate) static DEFAULT_CUSTOM_METRICS_BUILD_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
fn build_default_custom_metrics() -> FontMetrics {
#[cfg(test)]
DEFAULT_CUSTOM_METRICS_BUILD_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut metrics = FontMetrics::new(556).with_widths(&[
(' ', 278),
('!', 278),
('"', 355),
('#', 556),
('$', 556),
('%', 889),
('&', 667),
('\'', 191),
('(', 333),
(')', 333),
('*', 389),
('+', 584),
(',', 278),
('-', 333),
('.', 278),
('/', 278),
('0', 556),
('1', 556),
('2', 556),
('3', 556),
('4', 556),
('5', 556),
('6', 556),
('7', 556),
('8', 556),
('9', 556),
(':', 278),
(';', 278),
('<', 584),
('=', 584),
('>', 584),
('?', 556),
('@', 1015),
('A', 667),
('B', 667),
('C', 722),
('D', 722),
('E', 667),
('F', 611),
('G', 778),
('H', 722),
('I', 278),
('J', 500),
('K', 667),
('L', 556),
('M', 833),
('N', 722),
('O', 778),
('P', 667),
('Q', 778),
('R', 722),
('S', 667),
('T', 611),
('U', 722),
('V', 667),
('W', 944),
('X', 667),
('Y', 667),
('Z', 611),
('[', 278),
('\\', 278),
(']', 278),
('^', 469),
('_', 556),
('`', 333),
('a', 556),
('b', 556),
('c', 500),
('d', 556),
('e', 556),
('f', 278),
('g', 556),
('h', 556),
('i', 222),
('j', 222),
('k', 500),
('l', 222),
('m', 833),
('n', 556),
('o', 556),
('p', 556),
('q', 556),
('r', 333),
('s', 500),
('t', 278),
('u', 556),
('v', 500),
('w', 722),
('x', 500),
('y', 500),
('z', 500),
('{', 334),
('|', 260),
('}', 334),
('~', 584),
]);
let cjk_ranges: &[(u32, u32)] = &[
(0x3000, 0x303F), (0x3040, 0x309F), (0x30A0, 0x30FF), (0x4E00, 0x9FFF), (0xF900, 0xFAFF), (0xFF00, 0xFFEF), ];
for &(start, end) in cjk_ranges {
for code_point in start..=end {
if let Some(ch) = char::from_u32(code_point) {
metrics.widths.insert(ch, 1000);
}
}
}
metrics
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_metrics_creation() {
let metrics = FontMetrics::new(500);
assert_eq!(metrics.default_width, 500);
assert!(metrics.widths.is_empty());
}
#[test]
fn test_font_metrics_with_widths() {
let widths = [('A', 600), ('B', 700), ('C', 650)];
let metrics = FontMetrics::new(500).with_widths(&widths);
assert_eq!(metrics.char_width('A'), 600);
assert_eq!(metrics.char_width('B'), 700);
assert_eq!(metrics.char_width('C'), 650);
assert_eq!(metrics.char_width('Z'), 500); }
#[test]
fn test_font_metrics_clone() {
let widths = [('A', 600), ('B', 700)];
let metrics1 = FontMetrics::new(500).with_widths(&widths);
let metrics2 = metrics1.clone();
assert_eq!(metrics1.char_width('A'), metrics2.char_width('A'));
assert_eq!(metrics1.default_width, metrics2.default_width);
}
fn afm_width(ch: char, font: Font) -> u16 {
measure_char(ch, font, 1000.0).round() as u16
}
#[test]
fn test_non_ascii_winansi_width_matches_afm_helvetica() {
assert_eq!(afm_width('í', Font::Helvetica), 278);
assert_ne!(
afm_width('í', Font::Helvetica),
556,
"must not fall back to default_width"
);
assert_eq!(afm_width('á', Font::Helvetica), 556); assert_eq!(afm_width('ñ', Font::Helvetica), 556); assert_eq!(afm_width('é', Font::Helvetica), 556); assert_eq!(afm_width('—', Font::Helvetica), 1000); assert_eq!(afm_width('–', Font::Helvetica), 556); assert_eq!(afm_width('•', Font::Helvetica), 350); assert_eq!(afm_width('©', Font::Helvetica), 737); assert_eq!(afm_width('€', Font::Helvetica), 556); assert_eq!(afm_width('’', Font::Helvetica), 222); }
#[test]
fn test_non_ascii_winansi_width_matches_afm_helvetica_bold() {
assert_eq!(afm_width('í', Font::HelveticaBold), 278);
assert_eq!(afm_width('é', Font::HelveticaBold), 556);
assert_eq!(afm_width('—', Font::HelveticaBold), 1000);
assert_eq!(afm_width('’', Font::HelveticaBold), 278); }
#[test]
fn test_non_ascii_winansi_width_matches_afm_times() {
assert_eq!(afm_width('í', Font::TimesRoman), 278); assert_eq!(afm_width('é', Font::TimesRoman), 444); assert_eq!(afm_width('ñ', Font::TimesRoman), 500); assert_eq!(afm_width('—', Font::TimesRoman), 1000);
assert_eq!(afm_width('©', Font::TimesRoman), 760);
assert_eq!(afm_width('™', Font::TimesRoman), 980); }
#[test]
fn test_non_ascii_winansi_width_courier_is_monospace() {
for ch in ['í', 'é', 'ñ', '—', '•', '©', '€', '’'] {
assert_eq!(afm_width(ch, Font::Courier), 600, "char {ch:?}");
}
}
#[test]
fn test_non_ascii_measure_text_string() {
let w = measure_text("café", &Font::Helvetica, 1000.0);
assert!((w - 1890.0).abs() < 0.5, "got {w}");
}
#[test]
fn test_measure_text_helvetica() {
let text = "Hello";
let width = measure_text(text, &Font::Helvetica, 12.0);
assert!((width - 27.336).abs() < 0.01);
}
#[test]
fn test_measure_text_courier() {
let text = "ABC";
let width = measure_text(text, &Font::Courier, 10.0);
assert_eq!(width, 18.0);
}
#[test]
fn test_measure_text_symbolic_fonts() {
let text = "ABC";
let symbol_width = measure_text(text, &Font::Symbol, 12.0);
let zapf_width = measure_text(text, &Font::ZapfDingbats, 12.0);
let expected = 3.0 * 12.0 * 0.6; assert_eq!(symbol_width, expected);
assert_eq!(zapf_width, expected);
}
#[test]
fn test_measure_char_helvetica() {
let width = measure_char('A', Font::Helvetica, 12.0);
assert!((width - 8.004).abs() < 0.01);
}
#[test]
fn test_measure_char_courier() {
let width = measure_char('X', Font::Courier, 10.0);
assert_eq!(width, 6.0);
}
#[test]
fn test_measure_char_symbolic() {
let symbol_width = measure_char('A', Font::Symbol, 15.0);
let zapf_width = measure_char('B', Font::ZapfDingbats, 15.0);
let expected = 15.0 * 0.6; assert_eq!(symbol_width, expected);
assert_eq!(zapf_width, expected);
}
#[test]
fn test_split_into_words_simple() {
let text = "Hello World";
let words = split_into_words(text);
assert_eq!(words, vec!["Hello", " ", "World"]);
}
#[test]
fn test_split_into_words_multiple_spaces() {
let text = "Hello World";
let words = split_into_words(text);
assert_eq!(words, vec!["Hello", " ", "World"]);
}
#[test]
fn test_split_into_words_leading_trailing_spaces() {
let text = " Hello World ";
let words = split_into_words(text);
assert_eq!(words, vec![" ", "Hello", " ", "World", " "]);
}
#[test]
fn test_split_into_words_tabs_newlines() {
let text = "Hello\tWorld\nTest";
let words = split_into_words(text);
assert_eq!(words, vec!["Hello", "\t", "World", "\n", "Test"]);
}
#[test]
fn test_split_into_words_empty() {
let text = "";
let words = split_into_words(text);
assert!(words.is_empty());
}
#[test]
fn test_split_into_words_only_spaces() {
let text = " ";
let words = split_into_words(text);
assert_eq!(words, vec![" "]);
}
#[test]
fn test_split_into_words_single_word() {
let text = "Hello";
let words = split_into_words(text);
assert_eq!(words, vec!["Hello"]);
}
#[test]
fn test_all_font_metrics_exist() {
let fonts = [
Font::Helvetica,
Font::HelveticaBold,
Font::HelveticaOblique,
Font::HelveticaBoldOblique,
Font::TimesRoman,
Font::TimesBold,
Font::TimesItalic,
Font::TimesBoldItalic,
Font::Courier,
Font::CourierBold,
Font::CourierOblique,
Font::CourierBoldOblique,
];
for font in &fonts {
let _width = measure_text("A", font, 12.0);
}
}
#[test]
fn test_helvetica_specific_characters() {
let chars = [
(' ', 278),
('A', 667),
('B', 667),
('C', 722),
('a', 556),
('b', 556),
('0', 556),
('1', 556),
('@', 1015),
('M', 833),
('W', 944),
('i', 222),
];
for (ch, expected_width) in &chars {
let width = measure_char(*ch, Font::Helvetica, 1000.0);
let expected = *expected_width as f64;
assert!(
(width - expected).abs() < 0.1,
"Character '{ch}' width mismatch: {width} vs {expected}"
);
}
}
#[test]
fn test_times_specific_characters() {
let chars = [
(' ', 250),
('A', 722),
('B', 667),
('C', 667),
('a', 444),
('b', 500),
('0', 500),
('1', 500),
('@', 921),
('M', 889),
('W', 944),
('i', 278),
];
for (ch, expected_width) in &chars {
let width = measure_char(*ch, Font::TimesRoman, 1000.0);
let expected = *expected_width as f64;
assert_eq!(width, expected, "Character '{ch}' width mismatch");
}
}
#[test]
fn test_courier_monospace_property() {
let chars = [
' ', 'A', 'B', 'C', 'a', 'b', '0', '1', '@', 'M', 'W', 'i', '~',
];
for ch in &chars {
let width = measure_char(*ch, Font::Courier, 1000.0);
assert_eq!(width, 600.0, "Courier character '{ch}' should be 600 units");
}
}
#[test]
fn test_font_size_scaling() {
let sizes = [6.0, 12.0, 18.0, 24.0, 36.0];
for size in &sizes {
let width = measure_char('A', Font::Helvetica, *size);
let expected = 667.0 * size / 1000.0; assert!(
(width - expected).abs() < 0.01,
"Size {size} scaling incorrect"
);
}
}
#[test]
fn test_measure_text_empty_string() {
let width = measure_text("", &Font::Helvetica, 12.0);
assert_eq!(width, 0.0);
}
#[test]
fn test_measure_text_consistency() {
let text = "Hello";
let total_width = measure_text(text, &Font::Helvetica, 12.0);
let individual_sum: f64 = text
.chars()
.map(|ch| measure_char(ch, Font::Helvetica, 12.0))
.sum();
assert!((total_width - individual_sum).abs() < 0.01);
}
#[test]
fn test_font_variants_use_base_metrics() {
let base_width = measure_char('A', Font::Helvetica, 12.0);
let oblique_width = measure_char('A', Font::HelveticaOblique, 12.0);
let bold_oblique_width = measure_char('A', Font::HelveticaBoldOblique, 12.0);
assert_eq!(base_width, oblique_width);
let bold_width = measure_char('A', Font::HelveticaBold, 12.0);
assert_eq!(bold_width, bold_oblique_width);
}
#[test]
fn test_unicode_characters_default_width() {
let unicode_chars = ['β', 'π', 'δ', '中', '雪'];
for ch in &unicode_chars {
let helvetica_width = measure_char(*ch, Font::Helvetica, 12.0);
let times_width = measure_char(*ch, Font::TimesRoman, 12.0);
let courier_width = measure_char(*ch, Font::Courier, 12.0);
let helvetica_expected = 278.0 * 12.0 / 1000.0;
let times_expected = 250.0 * 12.0 / 1000.0;
let courier_expected = 600.0 * 12.0 / 1000.0;
assert!(
(helvetica_width - helvetica_expected).abs() < 0.01,
"Helvetica width mismatch"
);
assert!(
(times_width - times_expected).abs() < 0.01,
"Times width mismatch"
);
assert!(
(courier_width - courier_expected).abs() < 0.01,
"Courier width mismatch"
);
}
}
#[test]
#[allow(deprecated)]
fn test_register_custom_font_metrics() {
let metrics = FontMetrics::new(750).with_widths(&[('A', 800), ('B', 850)]);
register_custom_font_metrics("TestFont".to_string(), metrics);
let retrieved = get_custom_font_metrics("TestFont");
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.char_width('A'), 800);
assert_eq!(retrieved.char_width('B'), 850);
assert_eq!(retrieved.char_width('Z'), 750); }
#[test]
#[allow(deprecated)]
fn test_get_custom_font_metrics_not_found() {
let result = get_custom_font_metrics("NonExistentFont12345");
let _ = result;
}
#[test]
#[allow(deprecated)]
fn test_measure_text_custom_font() {
let metrics = FontMetrics::new(500).with_widths(&[('A', 600), ('B', 600), ('C', 600)]);
register_custom_font_metrics("MyCustomFont".to_string(), metrics);
let width = measure_text("ABC", &Font::Custom("MyCustomFont".to_string()), 10.0);
assert!((width - 18.0).abs() < 0.01);
}
#[test]
#[allow(deprecated)]
fn test_measure_char_custom_font() {
let metrics = FontMetrics::new(500).with_widths(&[('X', 700)]);
register_custom_font_metrics("CustomCharTest".to_string(), metrics);
let width = measure_char('X', Font::Custom("CustomCharTest".to_string()), 10.0);
assert!((width - 7.0).abs() < 0.01);
}
#[test]
fn test_custom_font_no_auto_register_default() {
let unique = format!("NoAutoRegister_{}", std::process::id());
let width = measure_char('A', Font::Custom(unique.clone()), 10.0);
let expected = 667.0 * 10.0 / 1000.0;
assert!((width - expected).abs() < 0.01);
#[allow(deprecated)]
let metrics = get_custom_font_metrics(&unique);
assert!(
metrics.is_none(),
"read path must not auto-register unknown custom fonts"
);
}
#[test]
fn test_create_default_custom_metrics() {
let metrics = create_default_custom_metrics();
assert_eq!(metrics.char_width('A'), 667);
assert_eq!(metrics.char_width(' '), 278);
assert_eq!(metrics.char_width('0'), 556);
assert_eq!(metrics.char_width('你'), 1000); assert_eq!(metrics.default_width, 556);
}
#[test]
fn test_create_default_custom_metrics_is_cached() {
use std::sync::atomic::Ordering;
let before = DEFAULT_CUSTOM_METRICS_BUILD_COUNT.load(Ordering::Relaxed);
for _ in 0..1000 {
let _ = create_default_custom_metrics();
}
let after = DEFAULT_CUSTOM_METRICS_BUILD_COUNT.load(Ordering::Relaxed);
let delta = after - before;
assert!(
delta <= 1,
"build_default_custom_metrics ran {} times during 1000 calls; cache broken",
delta
);
}
#[test]
fn test_times_roman_metrics() {
let width = measure_char('A', Font::TimesRoman, 10.0);
let expected = 722.0 * 10.0 / 1000.0;
assert!((width - expected).abs() < 0.01);
}
#[test]
fn test_helvetica_bold_metrics() {
let width = measure_char('A', Font::HelveticaBold, 10.0);
let expected = 722.0 * 10.0 / 1000.0;
assert!((width - expected).abs() < 0.01);
}
#[test]
fn test_times_variants_use_their_own_metrics() {
let w = |f| (measure_char('A', f, 1000.0)).round() as u16;
assert_eq!(w(Font::TimesRoman), 722);
assert_eq!(w(Font::TimesBold), 722);
assert_eq!(w(Font::TimesItalic), 611);
assert_eq!(w(Font::TimesBoldItalic), 667);
assert_ne!(w(Font::TimesItalic), w(Font::TimesRoman));
}
#[test]
fn test_courier_variants_use_base_metrics() {
let base_width = measure_char('X', Font::Courier, 12.0);
let bold_width = measure_char('X', Font::CourierBold, 12.0);
let oblique_width = measure_char('X', Font::CourierOblique, 12.0);
let bold_oblique_width = measure_char('X', Font::CourierBoldOblique, 12.0);
assert_eq!(base_width, bold_width);
assert_eq!(base_width, oblique_width);
assert_eq!(base_width, bold_oblique_width);
}
fn reset_warned_unknown_fonts() {
if let Ok(mut set) = WARNED_UNKNOWN_FONTS.write() {
set.clear();
}
}
#[test]
fn test_warn_unknown_font_rate_limited_once_per_name() {
let unique = format!("RateLimitTask2_{}", std::process::id());
reset_warned_unknown_fonts();
warn_unknown_custom_font_once(&unique);
warn_unknown_custom_font_once(&unique);
warn_unknown_custom_font_once(&unique);
let set = WARNED_UNKNOWN_FONTS.read().expect("lock");
assert!(
set.contains(&unique),
"name should be in the warned set after first call"
);
let count = set.iter().filter(|n| *n == &unique).count();
assert_eq!(
count, 1,
"warn_unknown_custom_font_once must rate-limit to one entry per name"
);
}
#[test]
fn test_unknown_custom_font_does_not_register_on_read() {
let unique = format!("UnknownNameTask2_{}", std::process::id());
let _ = measure_text("hello", &Font::Custom(unique.clone()), 12.0);
#[allow(deprecated)]
let leaked = get_custom_font_metrics(&unique);
assert!(
leaked.is_none(),
"read path must not auto-register '{}'",
unique
);
}
#[test]
fn test_unknown_custom_font_returns_default_widths() {
let unique = format!("UnknownReturnTask2_{}", std::process::id());
let width = measure_text("AAAA", &Font::Custom(unique), 12.0);
assert!(
(width - 32.016).abs() < 0.01,
"unknown custom fonts must use the default metrics (A=667), got {}",
width
);
}
#[test]
fn test_split_into_words_mixed_whitespace() {
let words = split_into_words("A B C D");
assert_eq!(words, vec!["A", " ", "B", " ", "C", " ", "D"]);
}
#[test]
fn test_font_metrics_store_register_and_get() {
let store = FontMetricsStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
let metrics = FontMetrics::new(500).with_widths(&[('A', 700), ('B', 720)]);
store.register("MyFont", metrics);
assert_eq!(store.len(), 1);
assert!(!store.is_empty());
let got = store.get("MyFont").expect("font should be present");
assert_eq!(got.char_width('A'), 700);
assert_eq!(got.char_width('B'), 720);
assert_eq!(got.char_width('Z'), 500); }
#[test]
fn test_font_metrics_store_overwrite_same_name() {
let store = FontMetricsStore::new();
store.register("X", FontMetrics::new(500).with_widths(&[('A', 600)]));
store.register("X", FontMetrics::new(500).with_widths(&[('A', 800)]));
let got = store.get("X").unwrap();
assert_eq!(got.char_width('A'), 800); assert_eq!(store.len(), 1);
}
#[test]
fn test_font_metrics_store_clone_shares_state() {
let store_a = FontMetricsStore::new();
let store_b = store_a.clone();
store_a.register("Shared", FontMetrics::new(400));
assert_eq!(store_b.len(), 1, "clone must share the underlying registry");
assert!(store_b.get("Shared").is_some());
store_b.register("AlsoShared", FontMetrics::new(400));
assert_eq!(store_a.len(), 2);
}
#[test]
fn test_font_metrics_store_get_miss_returns_none_no_side_effects() {
let store = FontMetricsStore::new();
assert!(store.get("Unknown").is_none());
assert_eq!(store.len(), 0); assert!(store.is_empty());
}
#[test]
fn test_lookup_document_scope_takes_precedence_over_global() {
let unique = format!("PrecedenceTask3_{}", std::process::id());
#[allow(deprecated)]
register_custom_font_metrics(
unique.clone(),
FontMetrics::new(500).with_widths(&[('A', 100)]),
);
let store = FontMetricsStore::new();
store.register(
unique.clone(),
FontMetrics::new(500).with_widths(&[('A', 900)]),
);
let resolved = lookup(&Font::Custom(unique), Some(&store));
assert_eq!(
resolved.char_width('A'),
900,
"Document scope must win over global"
);
}
#[test]
fn test_lookup_falls_through_to_global_when_store_misses() {
let unique = format!("FallthroughTask3_{}", std::process::id());
#[allow(deprecated)]
register_custom_font_metrics(
unique.clone(),
FontMetrics::new(500).with_widths(&[('A', 333)]),
);
let empty_store = FontMetricsStore::new();
let resolved = lookup(&Font::Custom(unique), Some(&empty_store));
assert_eq!(
resolved.char_width('A'),
333,
"must fall through to legacy global when Document store misses"
);
}
#[test]
fn test_lookup_with_none_store_uses_global_then_default() {
let unique = format!("NoneStoreTask3_{}", std::process::id());
let resolved = lookup(&Font::Custom(unique), None);
assert_eq!(resolved.char_width('A'), 667); }
#[test]
fn test_measure_text_with_uses_document_scope() {
let unique = format!("MeasureWithTask4_{}", std::process::id());
let store = FontMetricsStore::new();
store.register(
unique.clone(),
FontMetrics::new(500).with_widths(&[('A', 1000)]),
);
let width = measure_text_with("AAAA", &Font::Custom(unique), 12.0, Some(&store));
assert!((width - 48.0).abs() < 0.01, "got {}", width);
}
#[test]
fn test_measure_text_back_compat_shim_passes_none() {
let unique = format!("BackCompatTask4_{}", std::process::id());
let width = measure_text("AAAA", &Font::Custom(unique), 12.0);
assert!((width - 32.016).abs() < 0.01, "got {}", width);
}
#[test]
fn test_measure_char_with_uses_document_scope() {
let unique = format!("MeasureCharWithTask4_{}", std::process::id());
let store = FontMetricsStore::new();
store.register(
unique.clone(),
FontMetrics::new(500).with_widths(&[('Z', 800)]),
);
let width = measure_char_with('Z', Font::Custom(unique), 10.0, Some(&store));
assert!((width - 8.0).abs() < 0.01, "got {}", width);
}
}