use crate::geom::{Pos, Rect, Size};
use crate::style::{Border, Color, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CellStyle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
pub symbol: String,
pub style: CellStyle,
pub continuation: bool,
}
impl Default for Cell {
fn default() -> Self {
Self {
symbol: " ".to_string(),
style: CellStyle::default(),
continuation: false,
}
}
}
#[derive(Debug, Clone)]
pub struct DiffOp {
pub pos: Pos,
pub cell: Cell,
}
pub struct Buffer {
pub size: Size,
pub cells: Vec<Cell>,
}
impl Buffer {
pub fn new(size: Size) -> Self {
let len = size.width as usize * size.height as usize;
Self {
size,
cells: vec![Cell::default(); len],
}
}
pub fn resize(&mut self, size: Size) {
self.size = size;
let len = size.width as usize * size.height as usize;
self.cells = vec![Cell::default(); len];
}
pub fn clear(&mut self) {
for cell in &mut self.cells {
*cell = Cell::default();
}
}
pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
if x >= self.size.width || y >= self.size.height {
return None;
}
let index = y as usize * self.size.width as usize + x as usize;
self.cells.get_mut(index)
}
pub fn write_text(&mut self, pos: Pos, clip: Rect, text: &str, style: &Style) {
let y = pos.y;
if y < clip.y || y >= clip.y.saturating_add(clip.height) {
return;
}
let cell_style = style.into_cell_style();
let clip_end = clip.x.saturating_add(clip.width);
let mut x = pos.x;
for ch in text.chars() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
if w == 0 {
continue; }
if x.saturating_add(w) > clip_end {
break;
}
if x < clip.x {
x = x.saturating_add(w);
continue;
}
if let Some(cell) = self.get_mut(x, y) {
cell.symbol = ch.to_string();
cell.style = cell_style;
cell.continuation = false;
}
if w > 1 {
for i in 1..w {
if let Some(cont) = self.get_mut(x + i, y) {
cont.symbol = " ".to_string();
cont.style = cell_style;
cont.continuation = true;
}
}
}
x = x.saturating_add(w);
}
}
pub fn draw_border(&mut self, rect: Rect, border: Border, style: &Style) {
let (tl, t, tr, l, r, bl, b, br) = match border {
Border::None => return,
Border::Plain => ("┌", "─", "┐", "│", "│", "└", "─", "┘"),
Border::Rounded => ("╭", "─", "╮", "│", "│", "╰", "─", "╯"),
Border::Double => ("╔", "═", "╗", "║", "║", "╚", "═", "╝"),
};
let cell_style = style.into_cell_style();
let right = rect.x + rect.width.saturating_sub(1);
let bottom = rect.y + rect.height.saturating_sub(1);
self.set_cell(rect.x, rect.y, tl, cell_style);
self.set_cell(right, rect.y, tr, cell_style);
self.set_cell(rect.x, bottom, bl, cell_style);
self.set_cell(right, bottom, br, cell_style);
for x in (rect.x + 1)..right {
self.set_cell(x, rect.y, t, cell_style);
self.set_cell(x, bottom, b, cell_style);
}
for y in (rect.y + 1)..bottom {
self.set_cell(rect.x, y, l, cell_style);
self.set_cell(right, y, r, cell_style);
}
}
fn set_cell(&mut self, x: u16, y: u16, symbol: &str, style: CellStyle) {
if let Some(cell) = self.get_mut(x, y) {
cell.symbol = symbol.to_string();
cell.style = style;
}
}
pub fn all_ops(&self) -> Vec<DiffOp> {
let mut ops = Vec::with_capacity(self.cells.len());
for y in 0..self.size.height {
for x in 0..self.size.width {
let idx = y as usize * self.size.width as usize + x as usize;
if self.cells[idx].continuation {
continue;
}
ops.push(DiffOp {
pos: Pos { x, y },
cell: self.cells[idx].clone(),
});
}
}
ops
}
pub fn diff(&self, next: &Buffer) -> Vec<DiffOp> {
let mut ops = Vec::new();
for y in 0..next.size.height {
for x in 0..next.size.width {
let next_idx = y as usize * next.size.width as usize + x as usize;
let next_cell = &next.cells[next_idx];
if next_cell.continuation {
continue;
}
let changed = if x < self.size.width && y < self.size.height {
let self_idx = y as usize * self.size.width as usize + x as usize;
&self.cells[self_idx] != next_cell
} else {
true
};
if changed {
ops.push(DiffOp {
pos: Pos { x, y },
cell: next_cell.clone(),
});
}
}
}
ops
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geom::Size;
use crate::style::Style;
#[test]
fn test_new_buffer() {
let buf = Buffer::new(Size { width: 3, height: 2 });
assert_eq!(buf.size.width, 3);
assert_eq!(buf.size.height, 2);
assert_eq!(buf.cells.len(), 6);
}
#[test]
fn test_clear() {
let mut buf = Buffer::new(Size { width: 2, height: 1 });
buf.get_mut(0, 0).unwrap().symbol = "X".into();
buf.clear();
assert_eq!(buf.cells[0].symbol, " ");
}
#[test]
fn test_write_text_clipped() {
let mut buf = Buffer::new(Size { width: 5, height: 1 });
buf.write_text(Pos::default(), Rect { x: 0, y: 0, width: 3, height: 1 }, "hello", &Style::default());
assert_eq!(buf.cells[0].symbol, "h");
assert_eq!(buf.cells[2].symbol, "l");
assert_eq!(buf.cells[3].symbol, " "); }
#[test]
fn test_diff() {
let front = Buffer::new(Size { width: 2, height: 1 });
let mut back = Buffer::new(Size { width: 2, height: 1 });
back.get_mut(0, 0).unwrap().symbol = "X".into();
let ops = front.diff(&back);
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].pos.x, 0);
}
}