badgeland 0.13.0

Fast badge generator for any purpose
Documentation
use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
use std::{cmp::Ordering, ops::Range};
use unicode_normalization::UnicodeNormalization;

fn get_font() -> FontRef<'static> {
    let font_data: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/resx/Verdana.ttf"));
    FontRef::try_from_slice(font_data).expect("Error constructing Font")
}

pub(crate) trait TextWidth {
    fn get_text_width(&self, height: f32) -> usize;
}

impl<'a> TextWidth for &'a str {
    #[inline]
    fn get_text_width(&self, height: f32) -> usize {
        let font = get_font();

        let scale = PxScale::from(height as f32);
        let scaled_font = font.as_scaled(scale);

        let normalized: String = self.trim().nfc().collect();
        normalized
            .chars()
            .map(|c| {
                let glyph = scaled_font.scaled_glyph(c);
                let gb = scaled_font.glyph_bounds(&glyph);
                gb.width() * 1.12
            })
            .fold(0.0, |acc, w| acc + w)
            .floor() as usize
    }
}

#[derive(Debug, Default, Clone)]
pub struct Path<'a> {
    values: &'a [f32],
    chart_height: f32,
    x_offset: f32,
    y_offset: f32,
    index: Range<usize>,
}

impl<'a> Path<'a> {
    pub fn new(values: &'a [f32], height: usize, width: usize) -> Self {
        let len = values.len();
        let chart_height = height as f32;
        let max = values
            .iter()
            .max_by(|&a, &b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
            .unwrap_or(&0.);

        let y_offset = chart_height / max;
        let x_offset = width as f32 / (len as f32 - 1.0);

        Path {
            values,
            chart_height,
            x_offset,
            y_offset,
            index: 0..len,
        }
    }
}

impl<'a> Iterator for Path<'a> {
    type Item = (f32, f32);
    fn next(&mut self) -> Option<Self::Item> {
        self.index.next().map(|i| {
            let x = i as f32 * self.x_offset;
            let y = self.chart_height - self.y_offset * self.values[i];
            (x, y)
        })
    }
}

#[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::{Path, TextWidth};

    #[test]
    fn content_str_width() {
        let s = "Hello";
        let bc = s.get_text_width(20.);
        assert!(bc > 0);
    }
    #[test]
    fn content_text_has_width() {
        let text = "".get_text_width(20.);
        assert_eq!(text, 0);
        let text = "npm".get_text_width(20.);
        assert_eq!(text, 46);
        let text = "long text".get_text_width(20.);
        assert_eq!(text, 90);
    }

    #[test]
    fn path_generate() {
        let d = [2., 4., 3., 2.];
        let path = Path::new(&d, 20, 100).into_iter().collect::<Vec<_>>();

        assert_eq!(path.len(), 4);
        assert_eq!(
            path,
            vec![(0.0, 10.0), (33.333332, 0.0), (66.666664, 5.0), (100.0, 10.0)]
        )
    }
}