bettertui_engine 0.1.0

High-performance terminal UI framework
Documentation
use crate::framebuffer::{Cell, FrameBuffer};
use crate::tree::Color;

pub type AsciiFontName = &'static str;

pub const FONT_NAMES: &[AsciiFontName] = &[
    "tiny",
    "block",
    "shade",
    "slick",
    "huge",
    "grid",
    "pallet",
    "console",
    "simple",
    "simple3d",
    "chrome",
    "simpleblock",
    "3d",
];

fn to_cfonts_font(name: &str) -> Option<cfonts::Fonts> {
    Some(match name {
        "tiny" => cfonts::Fonts::FontTiny,
        "block" => cfonts::Fonts::FontBlock,
        "shade" => cfonts::Fonts::FontShade,
        "slick" => cfonts::Fonts::FontSlick,
        "huge" => cfonts::Fonts::FontHuge,
        "grid" => cfonts::Fonts::FontGrid,
        "pallet" => cfonts::Fonts::FontPallet,
        "console" => cfonts::Fonts::FontConsole,
        "simple" => cfonts::Fonts::FontSimple,
        "simple3d" => cfonts::Fonts::FontSimple3d,
        "chrome" => cfonts::Fonts::FontChrome,
        "simpleblock" => cfonts::Fonts::FontSimpleBlock,
        "3d" => cfonts::Fonts::Font3d,
        _ => return None,
    })
}

fn cfonts_render(text: &str, font: &str) -> Option<cfonts::render::RenderedString> {
    let cfont = to_cfonts_font(font)?;
    let options = cfonts::Options {
        text: text.to_string(),
        font: cfont,
        colors: vec![],
        spaceless: true,
        raw_mode: true,
        ..Default::default()
    };
    Some(cfonts::render(options))
}

pub fn measure_text(text: &str, font_name: &str) -> Option<(usize, usize)> {
    let result = cfonts_render(text, font_name)?;
    let lines = result.vec.len();
    if text.is_empty() {
        return Some((0, lines));
    }
    let width = result.text.lines().map(|l| l.len()).max().unwrap_or(0);
    Some((width, lines))
}

pub fn render_text(text: &str, font_name: &str) -> Option<String> {
    let result = cfonts_render(text, font_name)?;
    Some(result.text)
}

pub fn get_character_positions(text: &str, font_name: &str) -> Option<Vec<usize>> {
    let cfont = to_cfonts_font(font_name)?;
    let mut positions = vec![0usize];
    let mut current = 0usize;

    for ch in text.chars() {
        let options = cfonts::Options {
            text: ch.to_uppercase().to_string(),
            font: cfont.clone(),
            colors: vec![],
            spaceless: true,
            raw_mode: true,
            ..Default::default()
        };
        let result = cfonts::render(options);
        let char_width = result.text.lines().map(|l| l.len()).max().unwrap_or(0);
        current += char_width;
        positions.push(current);
    }

    Some(positions)
}

pub struct AsciiFontSegment {
    pub x: u16,
    pub y: u16,
    pub text: String,
    pub color_index: usize,
}

pub struct AsciiFontLayout {
    pub segments: Vec<AsciiFontSegment>,
    pub width: usize,
    pub height: usize,
}

pub fn layout_text(text: &str, font_name: &str, start_x: u16, start_y: u16) -> Option<AsciiFontLayout> {
    let result = cfonts_render(text, font_name)?;
    let mut segments = Vec::new();
    let mut width = 0usize;

    for (y, line) in result.text.lines().enumerate() {
        let line_len = line.len();
        width = width.max(line_len);
        segments.push(AsciiFontSegment { x: start_x, y: start_y + y as u16, text: line.to_string(), color_index: 0 });
    }

    Some(AsciiFontLayout { segments, width, height: result.vec.len() })
}

pub fn coordinate_to_character_index(x: u16, text: &str, font_name: &str) -> Option<usize> {
    let positions = get_character_positions(text, font_name)?;

    if positions.is_empty() || x < positions[0] as u16 {
        return Some(0);
    }

    for i in 0..positions.len().saturating_sub(1) {
        let current = positions[i] as u16;
        let next = positions[i + 1] as u16;

        if x >= current && x < next {
            let midpoint = current + (next - current) / 2;
            return Some(if x < midpoint { i } else { i + 1 });
        }
    }

    if let Some(&last) = positions.last()
        && x >= last as u16
    {
        return Some(text.len());
    }

    Some(0)
}

pub fn render_font_to_frame_buffer(
    buffer: &mut FrameBuffer,
    text: &str,
    x: u16,
    y: u16,
    colors: &[Color],
    background_color: Color,
    font_name: &str,
) -> Option<(usize, usize)> {
    let result = cfonts_render(text, font_name)?;
    let buf_width = buffer.width();
    let buf_height = buffer.height();
    let fg = colors.first().copied().unwrap_or(Color::Default);

    let mut max_line_width = 0usize;

    for (line_idx, line) in result.text.lines().enumerate() {
        let render_y = y + line_idx as u16;
        if render_y >= buf_height {
            break;
        }
        max_line_width = max_line_width.max(line.len());

        for (char_idx, ch) in line.chars().enumerate() {
            let render_x = x + char_idx as u16;
            if render_x >= buf_width {
                break;
            }
            if ch != ' ' {
                buffer.set(render_x, render_y, Cell::new(ch).with_fg(fg).with_bg(background_color));
            }
        }
    }

    Some((max_line_width, result.vec.len()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn load_all_fonts() {
        for name in FONT_NAMES {
            let result = measure_text("ABC", name);
            assert!(result.is_some(), "Font '{}' failed to load", name);
            let (w, h) = result.unwrap();
            assert!(w > 0);
            assert!(h > 0);
        }
    }

    #[test]
    fn measure_tiny() {
        let (w, h) = measure_text("HI", "tiny").unwrap();
        assert_eq!(h, 2);
        assert!(w > 0);
    }

    #[test]
    fn measure_block() {
        let (w, h) = measure_text("A", "block").unwrap();
        assert_eq!(h, 6);
        assert!(w > 0);
    }

    #[test]
    fn measure_empty_string() {
        let (w, h) = measure_text("", "tiny").unwrap();
        assert_eq!(w, 0);
        assert_eq!(h, 2);
    }

    #[test]
    fn measure_unknown_font() {
        let result = measure_text("test", "nonexistent");
        assert!(result.is_none());
    }

    #[test]
    fn get_positions() {
        let positions = get_character_positions("AB", "tiny").unwrap();
        assert_eq!(positions.len(), 3);
        assert_eq!(positions[0], 0);
        assert!(positions[1] > 0);
    }

    #[test]
    fn get_positions_unknown_font() {
        let result = get_character_positions("test", "nonexistent");
        assert!(result.is_none());
    }

    #[test]
    fn layout_unknown_font() {
        let result = layout_text("test", "nonexistent", 0, 0);
        assert!(result.is_none());
    }

    #[test]
    fn layout_tiny_hi() {
        let layout = layout_text("HI", "tiny", 0, 0).unwrap();
        assert!(layout.width > 0);
        assert_eq!(layout.height, 2);
        assert!(!layout.segments.is_empty());
    }

    #[test]
    fn layout_with_offset() {
        let layout = layout_text("A", "tiny", 10, 5).unwrap();
        for seg in &layout.segments {
            assert!(seg.x >= 10);
            assert!(seg.y >= 5);
        }
    }

    #[test]
    fn layout_huge() {
        let layout = layout_text("A", "huge", 0, 0).unwrap();
        assert_eq!(layout.height, 11);
        assert!(layout.segments.len() > 5);
    }

    #[test]
    fn measure_all_font_names() {
        for name in &["tiny", "block", "shade", "slick", "huge", "grid", "pallet"] {
            let result = measure_text("Hello", name);
            assert!(result.is_some(), "Failed to measure font: {}", name);
        }
    }

    #[test]
    fn render_text_returns_string() {
        let output = render_text("HELLO", "tiny").unwrap();
        assert!(!output.is_empty());
        assert!(output.lines().count() >= 2);
    }

    #[test]
    fn render_text_unknown_font() {
        let result = render_text("test", "nonexistent");
        assert!(result.is_none());
    }
}