use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::RwLock;
use unicode_normalization::UnicodeNormalization;
static FONT: Lazy<FontRef<'static>> = Lazy::new(|| {
let font_data: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/resx/Verdana.ttf"));
FontRef::try_from_slice(font_data).expect("Error constructing Font")
});
static ASCII_WIDTHS_1_0: Lazy<[f32; 128]> = Lazy::new(|| {
let scaled_font = FONT.as_scaled(PxScale::from(1.0));
let mut arr = [0.0f32; 128];
for b in 0u8..=127u8 {
let c = b as char;
let glyph = scaled_font.scaled_glyph(c);
let gb = scaled_font.glyph_bounds(&glyph);
arr[b as usize] = gb.width();
}
arr
});
static UNICODE_WIDTH_CACHE: Lazy<RwLock<HashMap<(u32, u32), f32>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
pub(crate) trait TextWidth {
fn text_width(&self, height: f32) -> usize;
}
impl<'a> TextWidth for &'a str {
#[inline]
fn text_width(&self, height: f32) -> usize {
let s = self.trim();
if s.is_empty() {
return 0;
}
if s.is_ascii() {
let sum_1_0: f32 = s.as_bytes().iter().map(|&b| ASCII_WIDTHS_1_0[b as usize]).sum();
return (sum_1_0 * height * 1.12).floor() as usize;
}
let scale = PxScale::from(height);
let scaled_font = FONT.as_scaled(scale);
let height_key = (height * 10.0).round() as u32;
let mut total = 0.0f32;
for c in s.nfc() {
let key = (c as u32, height_key);
if let Some(&w) = UNICODE_WIDTH_CACHE.read().unwrap().get(&key) {
total += w * 1.12;
continue;
}
let glyph = scaled_font.scaled_glyph(c);
let gb = scaled_font.glyph_bounds(&glyph);
let measured_w = gb.width();
let w = {
let mut map = UNICODE_WIDTH_CACHE.write().unwrap();
*map.entry(key).or_insert(measured_w)
};
total += w * 1.12;
}
total.floor() as usize
}
}
pub(super) trait SvgPath {
fn svg_path(&self, height: usize, width: usize) -> String;
}
impl<'a> SvgPath for [f32] {
fn svg_path(&self, height: usize, width: usize) -> String {
let len = self.len();
let chart_height = height as f32;
let max = self.iter().copied().fold(0.0_f32, f32::max);
let y_offset = chart_height / max;
let x_offset = width as f32 / (len as f32 - 1.0);
let mut path_str = String::with_capacity(8 + len * 28);
for (i, v) in self.iter().enumerate() {
let x = i as f32 * x_offset;
let y = chart_height - y_offset * v;
if i == 0 {
write!(&mut path_str, "M0 {y}", y = y).unwrap();
}
write!(&mut path_str, "L{x} {y}", x = x, y = y).unwrap()
}
path_str
}
}
#[derive(Default)]
pub(super) struct ContentSize {
pub(super) x: usize,
pub(super) y: usize,
pub(super) rw: usize,
}
pub(super) trait BadgeContentSize {
fn content_size(
&self,
height: usize,
width: usize,
padding: usize,
x_offset: usize,
) -> ContentSize;
}
impl<'a> BadgeContentSize for &'a [f32] {
#[inline]
fn content_size(&self, height: usize, width: usize, padding: usize, _: usize) -> ContentSize {
ContentSize {
x: (width + padding) / 2,
y: height / 2,
rw: width,
}
}
}
impl<'a> BadgeContentSize for &'a str {
#[inline]
fn content_size(
&self,
height: usize,
width: usize,
padding: usize,
x_offset: usize,
) -> ContentSize {
let w = width + x_offset;
let x = (width + padding) / 2 + x_offset;
let y = height / 2;
let rw = w + padding;
ContentSize { x, y, rw }
}
}
#[cfg(test)]
mod tests {
use super::{SvgPath, TextWidth};
#[test]
fn content_str_width() {
let s = "Hello";
let bc = s.text_width(20.);
assert!(bc > 0);
}
#[test]
fn content_text_has_width() {
let text = "".text_width(20.);
assert_eq!(text, 0);
let text = "npm".text_width(20.);
assert_eq!(text, 46);
let text = "long text".text_width(20.);
assert_eq!(text, 90);
}
#[test]
fn path_generate() {
let d: &[f32; 4] = &[2., 4., 3., 2.];
let path = &d.svg_path(20, 100);
assert_eq!(path, "M0 10L0 10L33.333332 0L66.666664 5L100 10")
}
}