turbo-vision 0.1.0

A Rust implementation of the classic Borland Turbo Vision text-mode UI framework
Documentation
use crate::core::geometry::Rect;
use crate::core::event::Event;
use crate::core::palette::colors;
use crate::core::draw::DrawBuffer;
use crate::terminal::Terminal;
use super::view::{View, write_line_to_terminal};

/// ParamText - Static text with parameter substitution
/// Displays text with placeholders like "File: %s" or "Total: %d items"
pub struct ParamText {
    bounds: Rect,
    template: String,
    text: String,
}

impl ParamText {
    /// Create a new parameterized text control
    /// The template string can contain placeholders:
    /// - %s for string substitution
    /// - %d for numeric substitution
    /// - %% for a literal %
    pub fn new(bounds: Rect, template: &str) -> Self {
        Self {
            bounds,
            template: template.to_string(),
            text: template.to_string(),
        }
    }

    /// Set the template text
    pub fn set_template(&mut self, template: &str) {
        self.template = template.to_string();
        self.text = template.to_string();
    }

    /// Set a string parameter (replaces first %s)
    pub fn set_param_str(&mut self, value: &str) {
        self.text = self.template.replacen("%s", value, 1);
    }

    /// Set multiple string parameters
    pub fn set_params_str(&mut self, values: &[&str]) {
        let mut result = self.template.clone();
        for value in values {
            result = result.replacen("%s", value, 1);
        }
        self.text = result;
    }

    /// Set a numeric parameter (replaces first %d)
    pub fn set_param_num(&mut self, value: i64) {
        let value_str = value.to_string();
        self.text = self.template.replacen("%d", &value_str, 1);
    }

    /// Set text with both string and numeric parameters
    /// Example: template = "File: %s, Size: %d bytes"
    ///          set_params("test.txt", &[1024])
    pub fn set_params(&mut self, str_params: &[&str], num_params: &[i64]) {
        let mut result = self.template.clone();

        // Replace string parameters
        for value in str_params {
            result = result.replacen("%s", value, 1);
        }

        // Replace numeric parameters
        for value in num_params {
            let value_str = value.to_string();
            result = result.replacen("%d", &value_str, 1);
        }

        // Replace %% with %
        result = result.replace("%%", "%");

        self.text = result;
    }

    /// Get the current displayed text
    pub fn get_text(&self) -> &str {
        &self.text
    }

    /// Get the template
    pub fn get_template(&self) -> &str {
        &self.template
    }
}

impl View for ParamText {
    fn bounds(&self) -> Rect {
        self.bounds
    }

    fn set_bounds(&mut self, bounds: Rect) {
        self.bounds = bounds;
    }

    fn draw(&mut self, terminal: &mut Terminal) {
        let width = self.bounds.width() as usize;
        let height = self.bounds.height() as usize;

        // Split text into lines
        let lines: Vec<&str> = self.text.lines().collect();

        for (i, line) in lines.iter().enumerate() {
            if i >= height {
                break;
            }

            let mut buf = DrawBuffer::new(width);
            buf.move_char(0, ' ', colors::DIALOG_NORMAL, width);

            // Truncate line if too long
            let display_text = if line.len() > width {
                &line[..width]
            } else {
                line
            };

            buf.move_str(0, display_text, colors::DIALOG_NORMAL);
            write_line_to_terminal(terminal, self.bounds.a.x, self.bounds.a.y + i as i16, &buf);
        }

        // Fill remaining lines with spaces
        for i in lines.len()..height {
            let mut buf = DrawBuffer::new(width);
            buf.move_char(0, ' ', colors::DIALOG_NORMAL, width);
            write_line_to_terminal(terminal, self.bounds.a.x, self.bounds.a.y + i as i16, &buf);
        }
    }

    fn handle_event(&mut self, _event: &mut Event) {
        // ParamText doesn't handle events
    }
}

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

    #[test]
    fn test_paramtext_creation() {
        let param_text = ParamText::new(Rect::new(0, 0, 20, 1), "Hello %s");
        assert_eq!(param_text.get_template(), "Hello %s");
        assert_eq!(param_text.get_text(), "Hello %s");
    }

    #[test]
    fn test_paramtext_set_param_str() {
        let mut param_text = ParamText::new(Rect::new(0, 0, 20, 1), "Hello %s");
        param_text.set_param_str("World");
        assert_eq!(param_text.get_text(), "Hello World");
    }

    #[test]
    fn test_paramtext_set_param_num() {
        let mut param_text = ParamText::new(Rect::new(0, 0, 20, 1), "Count: %d");
        param_text.set_param_num(42);
        assert_eq!(param_text.get_text(), "Count: 42");
    }

    #[test]
    fn test_paramtext_multiple_params() {
        let mut param_text = ParamText::new(
            Rect::new(0, 0, 40, 1),
            "File: %s, Size: %d bytes"
        );
        param_text.set_params(&["test.txt"], &[1024]);
        assert_eq!(param_text.get_text(), "File: test.txt, Size: 1024 bytes");
    }

    #[test]
    fn test_paramtext_multiple_strings() {
        let mut param_text = ParamText::new(
            Rect::new(0, 0, 40, 1),
            "From %s to %s"
        );
        param_text.set_params_str(&["Alice", "Bob"]);
        assert_eq!(param_text.get_text(), "From Alice to Bob");
    }

    #[test]
    fn test_paramtext_escape_percent() {
        let mut param_text = ParamText::new(
            Rect::new(0, 0, 30, 1),
            "Progress: %d%%"
        );
        param_text.set_params(&[], &[75]);
        assert_eq!(param_text.get_text(), "Progress: 75%");
    }

    #[test]
    fn test_paramtext_set_template() {
        let mut param_text = ParamText::new(Rect::new(0, 0, 20, 1), "Hello %s");
        param_text.set_param_str("World");
        assert_eq!(param_text.get_text(), "Hello World");

        param_text.set_template("Goodbye %s");
        param_text.set_param_str("Moon");
        assert_eq!(param_text.get_text(), "Goodbye Moon");
    }

    #[test]
    fn test_paramtext_complex() {
        let mut param_text = ParamText::new(
            Rect::new(0, 0, 60, 1),
            "User: %s, Files: %d, Size: %d MB (%d%%)"
        );
        param_text.set_params(&["admin"], &[150, 2048, 95]);
        assert_eq!(param_text.get_text(), "User: admin, Files: 150, Size: 2048 MB (95%)");
    }
}