use std::convert::Infallible;
use crate::buffer::{Buffer, Cell};
use crate::geometry::{Position, Rect, Size};
use crate::term::traits::{Backend, ClearType, WindowSize};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestBackend {
buffer: Buffer,
cursor: Position,
cursor_visible: bool,
scrollback: Vec<Vec<Cell>>,
}
impl TestBackend {
pub fn new(width: u16, height: u16) -> Self {
Self {
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
cursor: Position::ORIGIN,
cursor_visible: true,
scrollback: Vec::new(),
}
}
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
pub fn buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffer
}
pub fn cursor_position(&self) -> Position {
self.cursor
}
pub fn cursor_visible(&self) -> bool {
self.cursor_visible
}
pub fn scrollback(&self) -> &[Vec<Cell>] {
&self.scrollback
}
pub fn resize(&mut self, width: u16, height: u16) {
self.buffer.resize(Rect::new(0, 0, width, height));
self.cursor.x = self.cursor.x.min(width.saturating_sub(1));
self.cursor.y = self.cursor.y.min(height.saturating_sub(1));
}
pub fn rows(&self) -> Vec<String> {
(0..self.buffer.area.height)
.map(|y| {
(0..self.buffer.area.width)
.map(|x| self.buffer[(x, y)].symbol().to_string())
.collect()
})
.collect()
}
}
impl Backend for TestBackend {
type Error = Infallible;
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
if let Some(target) = self.buffer.cell_mut((x, y)) {
*target = cell.clone();
}
}
Ok(())
}
fn append_lines(&mut self, n: u16) -> Result<(), Self::Error> {
let height = self.buffer.area.height;
let width = self.buffer.area.width;
if height == 0 || n == 0 {
return Ok(());
}
let shift = n.min(height);
for y in 0..shift {
let row: Vec<Cell> = (0..width).map(|x| self.buffer[(x, y)].clone()).collect();
self.scrollback.push(row);
}
for y in 0..height.saturating_sub(shift) {
for x in 0..width {
let source = self.buffer[(x, y + shift)].clone();
self.buffer[(x, y)] = source;
}
}
for y in height.saturating_sub(shift)..height {
for x in 0..width {
self.buffer[(x, y)].reset();
}
}
self.cursor.y = height.saturating_sub(1);
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), Self::Error> {
self.cursor_visible = false;
Ok(())
}
fn show_cursor(&mut self) -> Result<(), Self::Error> {
self.cursor_visible = true;
Ok(())
}
fn get_cursor_position(&mut self) -> Result<Position, Self::Error> {
Ok(self.cursor)
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), Self::Error> {
self.cursor = position.into();
Ok(())
}
fn clear(&mut self) -> Result<(), Self::Error> {
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
let area = self.buffer.area;
let Position { x: cx, y: cy } = self.cursor;
let mut reset = |x: u16, y: u16| {
if let Some(cell) = self.buffer.cell_mut((x, y)) {
cell.reset();
}
};
match clear_type {
ClearType::All => {
for y in 0..area.height {
for x in 0..area.width {
reset(x, y);
}
}
}
ClearType::AfterCursor => {
for x in cx..area.width {
reset(x, cy);
}
for y in cy.saturating_add(1)..area.height {
for x in 0..area.width {
reset(x, y);
}
}
}
ClearType::BeforeCursor => {
for y in 0..cy {
for x in 0..area.width {
reset(x, y);
}
}
for x in 0..=cx.min(area.width.saturating_sub(1)) {
reset(x, cy);
}
}
ClearType::CurrentLine => {
for x in 0..area.width {
reset(x, cy);
}
}
ClearType::UntilNewLine => {
for x in cx..area.width {
reset(x, cy);
}
}
}
Ok(())
}
fn size(&self) -> Result<Size, Self::Error> {
Ok(self.buffer.area.as_size())
}
fn window_size(&mut self) -> Result<WindowSize, Self::Error> {
Ok(WindowSize {
columns_rows: self.buffer.area.as_size(),
pixels: Size::new(
self.buffer.area.width.saturating_mul(8),
self.buffer.area.height.saturating_mul(16),
),
})
}
fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(
&mut self,
region: core::ops::Range<u16>,
line_count: u16,
) -> Result<(), Self::Error> {
let width = self.buffer.area.width;
let end = region.end.min(self.buffer.area.height);
let start = region.start.min(end);
let rows = end - start;
let shift = line_count.min(rows);
if start == 0 {
for y in start..start + shift {
let row: Vec<Cell> = (0..width).map(|x| self.buffer[(x, y)].clone()).collect();
self.scrollback.push(row);
}
}
for y in start..end.saturating_sub(shift) {
for x in 0..width {
let source = self.buffer[(x, y + shift)].clone();
self.buffer[(x, y)] = source;
}
}
for y in end.saturating_sub(shift)..end {
for x in 0..width {
self.buffer[(x, y)].reset();
}
}
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(
&mut self,
region: core::ops::Range<u16>,
line_count: u16,
) -> Result<(), Self::Error> {
let width = self.buffer.area.width;
let end = region.end.min(self.buffer.area.height);
let start = region.start.min(end);
let rows = end - start;
let shift = line_count.min(rows);
for y in (start + shift..end).rev() {
for x in 0..width {
let source = self.buffer[(x, y - shift)].clone();
self.buffer[(x, y)] = source;
}
}
for y in start..(start + shift).min(end) {
for x in 0..width {
self.buffer[(x, y)].reset();
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Style;
fn seeded(width: u16, height: u16) -> TestBackend {
let mut backend = TestBackend::new(width, height);
for y in 0..height {
let row = format!("{y}").repeat(width as usize);
backend
.buffer_mut()
.set_string(0, y, &row, Style::default());
}
backend
}
#[test]
fn draw_applies_cells_at_absolute_positions() {
let mut backend = TestBackend::new(3, 2);
let cell = Cell::new("x");
backend.draw([(1u16, 1u16, &cell)].into_iter()).unwrap();
assert_eq!(backend.rows(), vec![" ".to_string(), " x ".to_string()]);
}
#[test]
fn draw_ignores_cells_outside_the_screen() {
let mut backend = TestBackend::new(2, 1);
let cell = Cell::new("x");
backend.draw([(9u16, 9u16, &cell)].into_iter()).unwrap();
assert_eq!(backend.rows(), vec![" ".to_string()]);
}
#[test]
fn append_lines_scrolls_the_top_into_scrollback() {
let mut backend = seeded(2, 3);
backend.append_lines(1).unwrap();
assert_eq!(
backend.rows(),
vec!["11".to_string(), "22".to_string(), " ".to_string()]
);
assert_eq!(backend.scrollback().len(), 1);
assert_eq!(backend.scrollback()[0][0].symbol(), "0");
}
#[test]
fn append_lines_past_the_screen_height_clears_it() {
let mut backend = seeded(2, 2);
backend.append_lines(9).unwrap();
assert_eq!(backend.rows(), vec![" ".to_string(), " ".to_string()]);
assert_eq!(backend.scrollback().len(), 2);
}
#[test]
fn append_lines_of_zero_is_a_no_op() {
let mut backend = seeded(2, 2);
backend.append_lines(0).unwrap();
assert_eq!(backend.rows(), vec!["00".to_string(), "11".to_string()]);
assert!(backend.scrollback().is_empty());
}
#[test]
fn clear_region_after_cursor_leaves_earlier_rows() {
let mut backend = seeded(2, 3);
backend.set_cursor_position((1, 1)).unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
assert_eq!(
backend.rows(),
vec!["00".to_string(), "1 ".to_string(), " ".to_string()]
);
}
#[test]
fn clear_region_before_cursor_leaves_later_rows() {
let mut backend = seeded(2, 3);
backend.set_cursor_position((0, 1)).unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
assert_eq!(
backend.rows(),
vec![" ".to_string(), " 1".to_string(), "22".to_string()]
);
}
#[test]
fn clear_region_current_line_clears_only_that_row() {
let mut backend = seeded(2, 2);
backend.set_cursor_position((1, 0)).unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
assert_eq!(backend.rows(), vec![" ".to_string(), "11".to_string()]);
}
#[test]
fn cursor_visibility_is_tracked() {
let mut backend = TestBackend::new(1, 1);
assert!(backend.cursor_visible());
backend.hide_cursor().unwrap();
assert!(!backend.cursor_visible());
backend.show_cursor().unwrap();
assert!(backend.cursor_visible());
}
#[test]
fn resize_reports_the_new_size() {
let mut backend = TestBackend::new(2, 2);
backend.resize(5, 3);
assert_eq!(backend.size().unwrap(), Size::new(5, 3));
}
#[test]
fn resize_clamps_the_cursor_into_the_new_bounds() {
let mut backend = TestBackend::new(10, 10);
backend.set_cursor_position((9, 9)).unwrap();
backend.resize(4, 3);
assert_eq!(backend.cursor_position(), Position::new(3, 2));
}
#[test]
fn resize_to_zero_does_not_underflow_the_cursor() {
let mut backend = TestBackend::new(4, 4);
backend.set_cursor_position((3, 3)).unwrap();
backend.resize(0, 0);
assert_eq!(backend.cursor_position(), Position::ORIGIN);
}
}