scrin 0.1.37

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::color::Color;
use crate::core::rect::Rect;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Cell {
    pub ch: char,
    pub fg: Color,
    pub bg: Option<Color>,
    pub bold: bool,
    pub italic: bool,
    pub underlined: bool,
}

impl Cell {
    pub fn new(ch: char, fg: Color, bg: Option<Color>) -> Self {
        Self {
            ch,
            fg,
            bg,
            bold: false,
            italic: false,
            underlined: false,
        }
    }

    pub fn with_bold(mut self, bold: bool) -> Self {
        self.bold = bold;
        self
    }

    pub fn with_italic(mut self, italic: bool) -> Self {
        self.italic = italic;
        self
    }

    pub fn with_underlined(mut self, underlined: bool) -> Self {
        self.underlined = underlined;
        self
    }

    pub fn empty() -> Self {
        Self {
            ch: ' ',
            fg: Color::BLACK,
            bg: None,
            bold: false,
            italic: false,
            underlined: false,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Buffer {
    pub width: usize,
    pub height: usize,
    cells: Vec<Cell>,
    background: Option<Color>,
}

impl Buffer {
    pub fn new(width: usize, height: usize) -> Self {
        let cells = vec![Cell::empty(); width * height];
        Self {
            width,
            height,
            cells,
            background: None,
        }
    }

    pub fn with_background(width: usize, height: usize, bg: Option<Color>) -> Self {
        let cell = match bg {
            Some(c) => Cell {
                ch: ' ',
                fg: c,
                bg: Some(c),
                bold: false,
                italic: false,
                underlined: false,
            },
            None => Cell::empty(),
        };
        let cells = vec![cell; width * height];
        Self {
            width,
            height,
            cells,
            background: bg,
        }
    }

    pub fn index(&self, x: usize, y: usize) -> usize {
        y * self.width + x
    }

    pub fn get(&self, x: usize, y: usize) -> Option<&Cell> {
        if x < self.width && y < self.height {
            Some(&self.cells[self.index(x, y)])
        } else {
            None
        }
    }

    pub fn set(&mut self, x: usize, y: usize, cell: Cell) {
        if x < self.width && y < self.height {
            let idx = self.index(x, y);
            self.cells[idx] = cell;
        }
    }

    pub fn set_str(&mut self, x: usize, y: usize, s: &str, fg: Color, bg: Option<Color>) {
        for (i, ch) in s.chars().enumerate() {
            let cell_x = x + i;
            if cell_x < self.width {
                self.set(
                    cell_x,
                    y,
                    Cell {
                        ch,
                        fg,
                        bg,
                        bold: false,
                        italic: false,
                        underlined: false,
                    },
                );
            }
        }
    }

    pub fn set_str_bold(&mut self, x: usize, y: usize, s: &str, fg: Color, bg: Option<Color>) {
        for (i, ch) in s.chars().enumerate() {
            let cell_x = x + i;
            if cell_x < self.width {
                self.set(
                    cell_x,
                    y,
                    Cell {
                        ch,
                        fg,
                        bg,
                        bold: true,
                        italic: false,
                        underlined: false,
                    },
                );
            }
        }
    }

    pub fn fill(&mut self, area: Rect, ch: char, fg: Color, bg: Option<Color>) {
        for y in area.y as usize..area.bottom() as usize {
            for x in area.x as usize..area.right() as usize {
                self.set(
                    x,
                    y,
                    Cell {
                        ch,
                        fg,
                        bg,
                        bold: false,
                        italic: false,
                        underlined: false,
                    },
                );
            }
        }
    }

    pub fn clear(&mut self, area: Rect) {
        let bg = self.background;
        for y in area.y as usize..area.bottom() as usize {
            for x in area.x as usize..area.right() as usize {
                self.set(
                    x,
                    y,
                    Cell {
                        ch: ' ',
                        fg: Color::BLACK,
                        bg,
                        bold: false,
                        italic: false,
                        underlined: false,
                    },
                );
            }
        }
    }

    pub fn to_ansi_string(&self) -> String {
        let mut out = String::with_capacity(self.width * self.height * 8);
        let mut last_fg = Color::BLACK;
        let mut last_bg: Option<Color> = None;
        let mut last_bold = false;
        let mut last_italic = false;
        let mut last_underlined = false;

        for y in 0..self.height {
            out.push_str(&format!("\x1b[{};1H", y + 1));

            for x in 0..self.width {
                let cell = &self.cells[y * self.width + x];

                if cell.fg != last_fg {
                    out.push_str(&cell.fg.to_ansi_fg());
                    last_fg = cell.fg;
                }
                if cell.bg != last_bg {
                    match cell.bg {
                        Some(c) => out.push_str(&c.to_ansi_bg()),
                        None => out.push_str("\x1b[49m"),
                    }
                    last_bg = cell.bg;
                }
                if cell.bold != last_bold {
                    if cell.bold {
                        out.push_str("\x1b[1m");
                    } else {
                        out.push_str("\x1b[22m");
                    }
                    last_bold = cell.bold;
                }
                if cell.italic != last_italic {
                    if cell.italic {
                        out.push_str("\x1b[3m");
                    } else {
                        out.push_str("\x1b[23m");
                    }
                    last_italic = cell.italic;
                }
                if cell.underlined != last_underlined {
                    if cell.underlined {
                        out.push_str("\x1b[4m");
                    } else {
                        out.push_str("\x1b[24m");
                    }
                    last_underlined = cell.underlined;
                }
                out.push(cell.ch);
            }
        }
        out.push_str("\x1b[0m");
        out
    }

    pub fn to_plain_string(&self) -> String {
        let mut out = String::new();
        for y in 0..self.height {
            for x in 0..self.width {
                let cell = &self.cells[y * self.width + x];
                out.push(cell.ch);
            }
            if y < self.height - 1 {
                out.push('\n');
            }
        }
        out
    }

    pub fn width(&self) -> usize {
        self.width
    }

    pub fn height(&self) -> usize {
        self.height
    }

    pub fn cells(&self) -> &[Cell] {
        &self.cells
    }

    pub fn cells_mut(&mut self) -> &mut [Cell] {
        &mut self.cells
    }

    pub fn merge_from(&mut self, other: &Buffer, x: usize, y: usize) {
        for oy in 0..other.height {
            for ox in 0..other.width {
                let tx = x + ox;
                let ty = y + oy;
                if tx < self.width && ty < self.height {
                    if let Some(cell) = other.get(ox, oy) {
                        if cell.ch != ' ' || cell.bg.is_some() {
                            self.set(tx, ty, *cell);
                        }
                    }
                }
            }
        }
    }

    pub fn blend_cell(&mut self, x: usize, y: usize, cell: Cell) {
        if x < self.width && y < self.height {
            if let Some(existing) = self.get(x, y) {
                if cell.ch != ' ' {
                    self.set(x, y, cell);
                } else if cell.bg.is_some() {
                    let mut blended = *existing;
                    blended.bg = cell.bg;
                    self.set(x, y, blended);
                }
            }
        }
    }

    pub fn resize(&mut self, width: usize, height: usize) {
        let mut new_cells = vec![Cell::empty(); width * height];
        for y in 0..self.height.min(height) {
            for x in 0..self.width.min(width) {
                let idx = y * self.width + x;
                new_cells[y * width + x] = self.cells[idx];
            }
        }
        self.cells = new_cells;
        self.width = width;
        self.height = height;
    }
}

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

    #[test]
    fn test_buffer_creation() {
        let buf = Buffer::new(10, 5);
        assert_eq!(buf.width(), 10);
        assert_eq!(buf.height(), 5);
    }

    #[test]
    fn test_buffer_set_get() {
        let mut buf = Buffer::new(10, 10);
        let cell = Cell::new('A', Color::WHITE, Some(Color::BLACK));
        buf.set(5, 3, cell);
        assert_eq!(buf.get(5, 3).unwrap().ch, 'A');
    }

    #[test]
    fn test_buffer_fill() {
        let mut buf = Buffer::new(10, 10);
        let area = Rect::new(2, 2, 4, 3);
        buf.fill(area, 'X', Color::GREEN, Some(Color::BLACK));
        assert_eq!(buf.get(2, 2).unwrap().ch, 'X');
        assert_eq!(buf.get(5, 4).unwrap().ch, 'X');
        assert_eq!(buf.get(1, 1).unwrap().ch, ' ');
    }

    #[test]
    fn test_buffer_to_plain_string() {
        let mut buf = Buffer::new(3, 2);
        buf.set_str(0, 0, "Hi", Color::WHITE, None);
        buf.set(2, 0, Cell::new('!', Color::WHITE, None));
        assert_eq!(buf.to_plain_string(), "Hi!\n   ");
    }
}