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>,
skip_cells: Vec<bool>,
background: Option<Color>,
}
impl Buffer {
pub fn new(width: usize, height: usize) -> Self {
let cells = vec![Cell::empty(); width * height];
Self {
width,
height,
cells,
skip_cells: vec![false; width * height],
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,
skip_cells: vec![false; width * height],
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.skip_cells[idx] = false;
self.cells[idx] = cell;
if cell_display_width(cell.ch) == 2 && x + 1 < self.width {
let next_idx = self.index(x + 1, y);
self.cells[next_idx] = Cell::new(' ', cell.fg, cell.bg);
self.skip_cells[next_idx] = true;
}
}
}
pub fn is_skip(&self, x: usize, y: usize) -> bool {
if x < self.width && y < self.height {
self.skip_cells[self.index(x, y)]
} else {
false
}
}
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;
self.clear_area(area, bg);
}
pub fn clear_area(&mut self, area: Rect, 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: 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 self.skip_cells[y * self.width + x] {
continue;
}
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];
if self.skip_cells[y * self.width + x] {
out.push(' ');
continue;
}
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];
let mut new_skip_cells = vec![false; 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];
new_skip_cells[y * width + x] = self.skip_cells[idx];
}
}
self.cells = new_cells;
self.skip_cells = new_skip_cells;
self.width = width;
self.height = height;
}
}
fn cell_display_width(ch: char) -> usize {
if ch.is_control() {
return 0;
}
let cp = ch as u32;
if cp >= 0x1100
&& (cp <= 0x115F
|| cp == 0x2329
|| cp == 0x232A
|| (cp >= 0x2E80 && cp <= 0x303E)
|| (cp >= 0x3040 && cp <= 0x33BF)
|| (cp >= 0x3400 && cp <= 0x4DBF)
|| (cp >= 0x4E00 && cp <= 0x9FFF)
|| (cp >= 0xA000 && cp <= 0xA4CF)
|| (cp >= 0xAC00 && cp <= 0xD7AF)
|| (cp >= 0xF900 && cp <= 0xFAFF)
|| (cp >= 0xFE30 && cp <= 0xFE6F)
|| (cp >= 0xFF01 && cp <= 0xFF60)
|| (cp >= 0xFFE0 && cp <= 0xFFE6)
|| (cp >= 0x20000 && cp <= 0x2FFFD)
|| (cp >= 0x30000 && cp <= 0x3FFFD))
{
2
} else {
1
}
}
#[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 ");
}
#[test]
fn test_buffer_marks_wide_glyph_skip_cell() {
let mut buf = Buffer::new(4, 1);
buf.set(0, 0, Cell::new('中', Color::WHITE, None));
assert_eq!(buf.get(0, 0).unwrap().ch, '中');
assert!(buf.is_skip(1, 0));
}
}