rulet 2.0.0

A rusty figlet implementation
Documentation
use std::{fmt::Display, rc::Rc};

use figfont::{FIGfont, Layout, SubCharacter};

use crate::{figchar::FIGchar, smush_subchars::smush_subchars};

#[derive(Debug, Clone)]
pub struct FIGline {
    font: Rc<FIGfont>,
    chars: Vec<FIGchar>,
}
impl FIGline {
    pub fn new(text: &str, font: Rc<FIGfont>) -> Self {
        let mut n = Self::empty(font);

        for char in text.chars() {
            n.push_char(char);
        }
        n
    }
    pub fn empty(font: Rc<FIGfont>) -> Self {
        Self {
            font,
            chars: vec![],
        }
    }
    pub fn is_empty(&self) -> bool {
        self.chars.is_empty() || self.chars.iter().all(|c| c.is_whitespace())
    }
    pub fn push_str(&mut self, s: &str) {
        for c in s.chars() {
            self.push_char(c);
        }
    }
    fn push_char(&mut self, c: char) {
        let figchar = self.font.get(c as i32);
        self.chars.push(figchar.into())
    }
    pub fn width(&self) -> usize {
        RenderedFIGline::from(self).width()
    }
    //pub fn matrix(&self) -> Vec<Vec<SubCharacter>> {
    //    RenderedFIGline::from(self).matrix
    //}
}
impl Display for FIGline {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let rendered = RenderedFIGline::from(self);
        write!(f, "{rendered}")
    }
}

/// A FIGure that consists of a single line of FIGlet text, rendered to a matrix of subchars.
#[derive(Debug, Clone, PartialEq, Eq)]
struct RenderedFIGline {
    layout: Layout,
    matrix: Vec<Vec<SubCharacter>>,
}
impl RenderedFIGline {
    fn empty(font: &FIGfont) -> Self {
        Self {
            layout: font.header().layout(),
            matrix: vec![vec![]; font.header().height()],
        }
    }
    /// Pushes character into self, following the font's kerning/smushing rules
    fn push_char(&mut self, c: &FIGchar) {
        if self.layout.contains(Layout::HORIZONTAL_KERNING) {
            self.kerned_char(c);
        } else if self.layout.contains(Layout::HORIZONTAL_SMUSH) {
            self.smush_char(c);
        } else {
            self.append_char(c);
        }
    }
    fn width(&self) -> usize {
        self.matrix[0].len()
    }
    /// Appends a FIGchar, without smushing or kerning
    fn append_char(&mut self, c: &FIGchar) {
        let new_matrix = c.lines().to_vec();
        for (y, mut line) in new_matrix.into_iter().enumerate() {
            self.matrix[y].append(&mut line);
        }
    }
    /// Appends this FIGchar, with proper kerning
    fn kerned_char(&mut self, c: &FIGchar) {
        if self.matrix[0].is_empty() {
            self.append_char(c);
            return;
        }

        let left_offsets = self.trailing_spaces();
        let right_offsets = c.leading_spaces();

        // get non-overlapping valid offset
        let mut offset = c.width();
        for y in 0..left_offsets.len() {
            let left = left_offsets[y].0;
            let right = right_offsets[y].0;
            let off = left + right;
            offset = offset.min(off);
        }
        // add new character with that offset
        self.add_with_offset(c, offset);
    }
    /// Appends this FIGchar with proper horizontal smushing
    fn smush_char(&mut self, c: &FIGchar) {
        if self.matrix[0].is_empty() {
            self.append_char(c);
            return;
        }

        let left_offsets = self.trailing_spaces();
        let right_offsets = c.leading_spaces();

        // get non-overlapping valid offset
        let mut offset = c.width();
        for y in 0..left_offsets.len() {
            let left = &left_offsets[y];
            let right = &right_offsets[y];
            let blank = if left.1.is_blank() || right.1.is_blank() {
                if self.layout.contains(Layout::HORIZONTAL_HARDBLANK)
                    && left.1.is_blank()
                    && right.1.is_blank()
                {
                    1
                } else {
                    0
                }
            } else {
                1
            };
            let off = left.0 + right.0 + blank;
            offset = offset.min(off);
        }
        // add new character with that offset
        self.add_with_offset(c, offset);
    }
    /// inserts a FIGchar with is left edge `offset` subcharacters to the left of the end of the current FIGure,
    /// smushing and overlaying characters as requested by the FIGure's FIGfont  
    fn add_with_offset(&mut self, c: &FIGchar, offset: usize) {
        for (y, line) in c.lines().iter().enumerate() {
            let current = &mut self.matrix[y];

            // replace existing chars
            for i in 0..offset {
                let pos_current = current.len() - offset + i;
                if let Some(r) = &line.get(i) {
                    current[pos_current] = smush_subchars(self.layout, &current[pos_current], r);
                }
            }

            let app_from = offset.min(line.len());
            current.append(&mut line[app_from..line.len()].to_vec());
        }
    }
    /// Returns the amount of trailing space per line, and the last non-space subchar
    fn trailing_spaces(&self) -> Vec<(usize, SubCharacter)> {
        self.matrix
            .iter()
            .map(|line| {
                line.iter()
                    .rev()
                    .enumerate()
                    .find_map(|(i, c)| {
                        if *c != SubCharacter::Symbol(" ".to_string()) {
                            Some((i, c.clone()))
                        } else {
                            None
                        }
                    })
                    .unwrap_or((line.len(), SubCharacter::Blank))
            })
            .collect()
    }
}
impl From<&FIGline> for RenderedFIGline {
    fn from(value: &FIGline) -> Self {
        let mut n = RenderedFIGline::empty(&value.font);
        for c in &value.chars {
            n.push_char(c);
        }
        n
    }
}
impl Display for RenderedFIGline {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for (i, line) in self.matrix.iter().enumerate() {
            let line_str: String = line.iter().map(|c| c.to_string()).collect();
            write!(f, "{}", &line_str)?;
            if i < self.matrix.len() - 1 {
                writeln!(f)?;
            }
        }
        Ok(())
    }
}