ansiq-render 0.1.0

Framebuffer, diff generation, and terminal patch emission for Ansiq.
Documentation
use ansiq_core::{Rect, Style};
use unicode_width::UnicodeWidthChar;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cell {
    pub symbol: char,
    pub style: Style,
}

impl Default for Cell {
    fn default() -> Self {
        Self {
            symbol: ' ',
            style: Style::default(),
        }
    }
}

#[derive(Clone, Debug)]
pub struct FrameBuffer {
    width: u16,
    height: u16,
    cells: Vec<Cell>,
}

impl FrameBuffer {
    pub fn new(width: u16, height: u16) -> Self {
        Self {
            width,
            height,
            cells: vec![Cell::default(); usize::from(width) * usize::from(height)],
        }
    }

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

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

    pub fn is_blank(&self) -> bool {
        self.cells.iter().all(|cell| *cell == Cell::default())
    }

    pub fn get(&self, x: u16, y: u16) -> Cell {
        self.cells[self.index(x, y)]
    }

    pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
        if x >= self.width || y >= self.height {
            return;
        }

        let index = self.index(x, y);
        self.cells[index] = cell;
    }

    pub fn fill_rect(&mut self, rect: Rect, symbol: char, style: Style) {
        for y in rect.y..rect.bottom().min(self.height) {
            for x in rect.x..rect.right().min(self.width) {
                self.set(x, y, Cell { symbol, style });
            }
        }
    }

    pub fn write_str(&mut self, x: u16, y: u16, text: &str, style: Style) {
        self.write_clipped(
            Rect::new(x, y, self.width.saturating_sub(x), 1),
            0,
            0,
            text,
            style,
        );
    }

    pub fn write_clipped(
        &mut self,
        rect: Rect,
        offset_x: u16,
        offset_y: u16,
        text: &str,
        style: Style,
    ) {
        if rect.width == 0 || rect.height == 0 {
            return;
        }

        let mut cursor_x = rect.x.saturating_add(offset_x);
        let cursor_y = rect.y.saturating_add(offset_y);
        if cursor_y >= rect.bottom() || cursor_y >= self.height {
            return;
        }

        for ch in text.chars() {
            let width = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
            if width == 0 {
                continue;
            }
            if cursor_x >= rect.right() || cursor_x >= self.width {
                break;
            }

            self.set(cursor_x, cursor_y, Cell { symbol: ch, style });

            for fill in 1..width {
                let fill_x = cursor_x.saturating_add(fill);
                if fill_x >= rect.right() || fill_x >= self.width {
                    break;
                }
                self.set(fill_x, cursor_y, Cell { symbol: ' ', style });
            }

            cursor_x = cursor_x.saturating_add(width);
        }
    }

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