use crate::text::font::{FontSource, horizontal_kerning};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextExtent {
pub width: f64,
pub height: f64,
pub ascent: f64,
pub descent: f64,
}
impl FontSource {
#[must_use]
pub fn measure(&self, text: &str, font_size_px: f64) -> TextExtent {
let Some(face) = self.primary_face() else {
return TextExtent {
width: 0.0,
height: 0.0,
ascent: 0.0,
descent: 0.0,
};
};
let units_per_em = f64::from(face.units_per_em());
if units_per_em <= 0.0 {
return TextExtent {
width: 0.0,
height: 0.0,
ascent: 0.0,
descent: 0.0,
};
}
let scale = font_size_px / units_per_em;
let mut advance_units: f64 = 0.0;
let mut prev_glyph = None;
for ch in text.chars() {
let Some(glyph_id) = face.glyph_index(ch) else {
continue;
};
if let Some(prev) = prev_glyph {
advance_units += horizontal_kerning(&face, prev, glyph_id);
}
if let Some(advance) = face.glyph_hor_advance(glyph_id) {
advance_units += f64::from(advance);
}
prev_glyph = Some(glyph_id);
}
let ascent = f64::from(face.ascender()) * scale;
let descent = (f64::from(face.descender()) * scale).abs();
TextExtent {
width: advance_units * scale,
height: ascent + descent,
ascent,
descent,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn source() -> FontSource {
FontSource::dejavu_sans()
}
#[test]
fn empty_string_has_zero_width() {
let extent = source().measure("", 12.0);
assert_eq!(extent.width, 0.0);
}
#[test]
fn non_empty_has_positive_width() {
let extent = source().measure("hello", 12.0);
assert!(extent.width > 0.0, "width was {}", extent.width);
}
#[test]
fn width_scales_monotonically_with_size() {
let small = source().measure("rizzma", 10.0).width;
let medium = source().measure("rizzma", 20.0).width;
let large = source().measure("rizzma", 40.0).width;
assert!(small < medium, "{small} !< {medium}");
assert!(medium < large, "{medium} !< {large}");
}
#[test]
fn width_scales_linearly_with_size() {
let base = source().measure("rizzma", 10.0).width;
let doubled = source().measure("rizzma", 20.0).width;
assert!((doubled - 2.0 * base).abs() < 1e-9);
}
#[test]
fn wide_glyphs_exceed_narrow_glyphs() {
let wide = source().measure("WW", 12.0).width;
let narrow = source().measure("ii", 12.0).width;
assert!(wide > narrow, "WW ({wide}) should exceed ii ({narrow})");
}
#[test]
fn vertical_metrics_are_sane() {
let extent = source().measure("Ag", 12.0);
assert!(extent.ascent > 0.0, "ascent was {}", extent.ascent);
assert!(extent.descent >= 0.0, "descent was {}", extent.descent);
assert!((extent.height - (extent.ascent + extent.descent)).abs() < 1e-9);
}
#[test]
fn kerning_tucks_in_known_pairs() {
let src = source();
let v = src.measure("V", 100.0).width;
let a = src.measure("A", 100.0).width;
let va = src.measure("VA", 100.0).width;
assert!(
va < v + a,
"expected kerning to narrow 'VA' ({va}) below V+A ({})",
v + a
);
let t = src.measure("T", 100.0).width;
let ta = src.measure("Ta", 100.0).width;
assert!(ta < t + a, "expected 'Ta' ({ta}) to kern below T+a");
}
#[test]
fn non_kerning_pair_is_plain_sum() {
let src = source();
let x = src.measure("x", 100.0).width;
let xx = src.measure("xx", 100.0).width;
assert!((xx - 2.0 * x).abs() < 1e-9, "xx {xx} != 2*x {}", 2.0 * x);
}
#[test]
fn empty_source_returns_zeroes() {
let extent = FontSource::new().measure("hello", 12.0);
assert_eq!(
extent,
TextExtent {
width: 0.0,
height: 0.0,
ascent: 0.0,
descent: 0.0,
}
);
}
}