use std::collections::VecDeque;
use super::cursor::Cursor;
use super::sgr::{Sgr, render_row};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Cell {
pub ch: char,
pub sgr: Sgr,
}
impl Default for Cell {
fn default() -> Self {
Self {
ch: ' ',
sgr: Sgr::default(),
}
}
}
impl Cell {
pub(super) const fn blank(sgr: Sgr) -> Self {
Self { ch: ' ', sgr }
}
pub(super) fn is_blank(&self) -> bool {
self.ch == ' ' && self.sgr == Sgr::default()
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum EraseMode {
ToEnd,
ToStart,
All,
}
impl EraseMode {
pub(super) const fn from_param(param: usize) -> Option<Self> {
match param {
0 => Some(Self::ToEnd),
1 => Some(Self::ToStart),
2 => Some(Self::All),
_ => None,
}
}
}
#[derive(Debug)]
pub(super) struct Grid {
rows: usize,
cols: usize,
lines: VecDeque<Vec<Cell>>,
}
impl Grid {
pub(super) fn new(rows: usize, cols: usize) -> Self {
let rows = rows.max(1);
let cols = cols.max(1);
Self {
rows,
cols,
lines: std::iter::repeat_with(|| vec![Cell::default(); cols])
.take(rows)
.collect(),
}
}
pub(super) const fn rows(&self) -> usize {
self.rows
}
pub(super) const fn cols(&self) -> usize {
self.cols
}
pub(super) fn set(&mut self, row: usize, col: usize, cell: Cell) {
if let Some(target) = self.lines.get_mut(row).and_then(|line| line.get_mut(col)) {
*target = cell;
}
}
pub(super) fn line_feed(&mut self, cursor: &mut Cursor, blank: &Cell) -> Option<String> {
if cursor.row + 1 < self.rows {
cursor.row += 1;
None
} else {
self.scroll_up(1, blank).into_iter().next()
}
}
pub(super) fn scroll_up(&mut self, n: usize, blank: &Cell) -> Vec<String> {
let n = n.max(1).min(self.rows);
let mut evicted = Vec::with_capacity(n);
for _ in 0..n {
if let Some(line) = self.lines.pop_front() {
evicted.push(render_row(&line));
}
self.lines.push_back(vec![blank.clone(); self.cols]);
}
evicted
}
pub(super) fn scroll_down(&mut self, n: usize, blank: &Cell) {
let n = n.max(1).min(self.rows);
for _ in 0..n {
self.lines.pop_back();
self.lines.push_front(vec![blank.clone(); self.cols]);
}
}
pub(super) fn erase_line(&mut self, cursor: &Cursor, mode: EraseMode, blank: &Cell) {
let cols = self.cols;
let Some(line) = self.lines.get_mut(cursor.row) else {
return;
};
let range = match mode {
EraseMode::ToEnd => cursor.col.min(cols)..cols,
EraseMode::ToStart => 0..(cursor.col + 1).min(cols),
EraseMode::All => 0..cols,
};
for cell in &mut line[range] {
*cell = blank.clone();
}
}
pub(super) fn erase_display(&mut self, cursor: &Cursor, mode: EraseMode, blank: &Cell) {
match mode {
EraseMode::ToEnd => {
self.erase_line(cursor, EraseMode::ToEnd, blank);
self.clear_rows(cursor.row + 1..self.rows, blank);
}
EraseMode::ToStart => {
self.clear_rows(0..cursor.row, blank);
self.erase_line(cursor, EraseMode::ToStart, blank);
}
EraseMode::All => self.clear_rows(0..self.rows, blank),
}
}
fn clear_rows(&mut self, range: std::ops::Range<usize>, blank: &Cell) {
for row in range {
if let Some(line) = self.lines.get_mut(row) {
for cell in line {
*cell = blank.clone();
}
}
}
}
pub(super) fn render(&self) -> Vec<String> {
self.lines.iter().map(|line| render_row(line)).collect()
}
}
#[cfg(test)]
mod tests {
use super::super::cursor::Cursor;
use super::{Cell, EraseMode, Grid};
fn write(grid: &mut Grid, row: usize, text: &str) {
for (col, ch) in text.chars().enumerate() {
grid.set(
row,
col,
Cell {
ch,
..Cell::default()
},
);
}
}
#[test]
fn scroll_up_evicts_top_row_and_backfills_blank() {
let mut grid = Grid::new(2, 4);
write(&mut grid, 0, "aa");
write(&mut grid, 1, "bb");
let evicted = grid.scroll_up(1, &Cell::default());
assert_eq!(evicted, vec!["aa".to_string()]);
assert_eq!(grid.render(), vec!["bb".to_string(), String::new()]);
}
#[test]
fn line_feed_scrolls_only_at_the_bottom() {
let mut grid = Grid::new(2, 4);
let mut cursor = Cursor::default();
assert!(grid.line_feed(&mut cursor, &Cell::default()).is_none());
assert_eq!(cursor.row, 1);
write(&mut grid, 1, "xx");
assert_eq!(
grid.line_feed(&mut cursor, &Cell::default()).as_deref(),
Some("")
);
assert_eq!(cursor.row, 1);
assert_eq!(grid.render(), vec!["xx".to_string(), String::new()]);
}
#[test]
fn erase_line_modes_clear_the_right_span() {
let mut grid = Grid::new(1, 5);
write(&mut grid, 0, "abcde");
let cursor = Cursor { row: 0, col: 2 };
grid.erase_line(&cursor, EraseMode::ToEnd, &Cell::default());
assert_eq!(grid.render(), vec!["ab".to_string()]);
write(&mut grid, 0, "abcde");
grid.erase_line(&cursor, EraseMode::ToStart, &Cell::default());
assert_eq!(grid.render(), vec![" de".to_string()]);
}
#[test]
fn erase_display_to_end_clears_below() {
let mut grid = Grid::new(3, 3);
write(&mut grid, 0, "aaa");
write(&mut grid, 1, "bbb");
write(&mut grid, 2, "ccc");
let cursor = Cursor { row: 1, col: 1 };
grid.erase_display(&cursor, EraseMode::ToEnd, &Cell::default());
assert_eq!(
grid.render(),
vec!["aaa".to_string(), "b".to_string(), String::new()]
);
}
#[test]
fn render_trims_trailing_blanks_per_row() {
let mut grid = Grid::new(1, 6);
write(&mut grid, 0, "hi");
assert_eq!(grid.render(), vec!["hi".to_string()]);
}
}