use crate::backend::{Backend, CursorStyle};
use crate::event::Event;
use crate::grid::{Grid, Pos, Rect, Size};
use crate::surface::Surface;
use alloc::collections::VecDeque;
use alloc::vec::Vec;
use ixy::HasSize;
mod input;
mod present;
mod retain;
pub struct Terminal<B: Backend> {
current: Grid,
previous: Grid,
flattened_current: Option<Grid>,
flattened_previous: Option<Grid>,
backend: B,
queued_events: VecDeque<Event>,
flattened_stale: bool,
present_count: u64,
retained_layers: Vec<bool>,
dropped_layers: Vec<bool>,
}
impl<B: Backend> Terminal<B> {
#[must_use]
pub fn new(backend: B) -> Self {
let size = backend.size();
let current = Grid::new(size.width(), size.height());
let previous = Grid::new(size.width(), size.height());
Self {
current,
previous,
flattened_current: None,
flattened_previous: None,
backend,
queued_events: VecDeque::new(),
flattened_stale: false,
present_count: 0,
retained_layers: Vec::new(),
dropped_layers: Vec::new(),
}
}
pub const fn surface(&mut self) -> Surface<'_> {
let area = self.area();
Surface::new(&mut self.current, area, 0)
}
#[must_use]
pub const fn size(&self) -> Size {
self.current.size()
}
#[must_use]
pub const fn area(&self) -> Rect {
self.current.size().to_rect()
}
pub fn resize(&mut self, width: u16, height: u16) {
self.current.resize(width, height);
self.previous.resize(width, height);
if let Some(flattened_current) = &mut self.flattened_current {
flattened_current.resize(width, height);
}
if let Some(flattened_previous) = &mut self.flattened_previous {
flattened_previous.resize(width, height);
}
self.previous.clear_all();
if let Some(flattened_previous) = &mut self.flattened_previous {
flattened_previous.clear_all();
}
self.retained_layers.clear();
self.dropped_layers.clear();
self.backend.resize(Size::new(width, height));
}
pub fn set_cursor_visible(&mut self, visible: bool) {
self.backend.set_cursor_visible(visible);
}
pub fn set_cursor_position(&mut self, position: Pos) {
self.backend.set_cursor_position(position);
}
pub fn set_cursor_style(&mut self, style: CursorStyle) {
self.backend.set_cursor_style(style);
}
#[must_use]
pub const fn grid(&self) -> &Grid {
&self.current
}
pub const fn grid_mut(&mut self) -> &mut Grid {
&mut self.current
}
#[must_use]
pub const fn backend(&self) -> &B {
&self.backend
}
pub const fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
}
impl<B: Backend> core::fmt::Debug for Terminal<B> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Terminal")
.field("size", &self.size())
.field("present_count", &self.present_count)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::Headless;
use crate::color::Style;
use crate::tile::Tile;
#[test]
fn test_terminal_grid_mut() {
let backend = Headless::new(10, 10);
let mut terminal = Terminal::new(backend);
assert_eq!(terminal.grid()[Pos::new(0, 0)].glyph(), ' ');
terminal
.grid_mut()
.put_tile(0, (0, 0), Tile::new('X', Style::default()));
assert_eq!(terminal.grid()[Pos::new(0, 0)].glyph(), 'X');
}
#[test]
fn test_terminal_size() {
let term = Terminal::new(Headless::new(40, 20));
assert_eq!(term.size(), Size::new(40, 20));
}
#[test]
fn test_terminal_area() {
let term = Terminal::new(Headless::new(40, 20));
assert_eq!(term.area(), Rect::new(0, 0, 40, 20));
}
#[test]
#[should_panic(expected = "Grid width must be at least 1")]
fn test_terminal_new_zero_width_backend_panics() {
let _ = Terminal::new(Headless::new(0, 5));
}
#[test]
fn test_terminal_new_zero_height_backend_does_not_panic() {
let term = Terminal::new(Headless::new(5, 0));
assert_eq!(term.size(), Size::new(5, 0));
}
#[test]
#[should_panic(expected = "Grid width must be at least 1")]
fn test_terminal_new_zero_by_zero_backend_panics() {
let _ = Terminal::new(Headless::new(0, 0));
}
#[test]
fn test_terminal_resize_to_zero_by_zero_is_allowed() {
let mut term = Terminal::new(Headless::new(10, 10));
term.resize(0, 0);
assert_eq!(term.size(), Size::new(0, 0));
term.resize(10, 10);
assert_eq!(term.size(), Size::new(10, 10));
}
#[test]
#[should_panic(expected = "Grid width must be at least 1")]
fn test_terminal_present_multi_layer_at_zero_width_panics() {
let mut term = Terminal::new(Headless::new(10, 10));
term.surface()
.on_layer(1)
.put((0, 0), 'B', Style::default());
term.resize(0, 10);
let _ = term.present();
}
#[test]
fn test_terminal_cursor_passthroughs_forward_to_backend() {
let mut term = Terminal::new(Headless::new(10, 10));
term.set_cursor_visible(true);
assert!(term.backend().cursor_visible());
term.set_cursor_position(Pos::new(3, 4));
assert_eq!(term.backend().cursor_position(), Pos::new(3, 4));
term.set_cursor_style(CursorStyle::SteadyBar);
assert_eq!(term.backend().cursor_style(), CursorStyle::SteadyBar);
}
#[test]
fn test_terminal_resize_changes_dimensions() {
let mut term = Terminal::new(Headless::new(10, 10));
term.resize(30, 15);
assert_eq!(term.size(), Size::new(30, 15));
assert_eq!(term.grid().width(), 30);
assert_eq!(term.grid().height(), 15);
}
#[test]
fn test_terminal_resize_preserves_current_content() {
let mut term = Terminal::new(Headless::new(10, 10));
term.surface().put((2, 2), 'X', Style::default());
term.resize(20, 20);
assert_eq!(term.grid()[Pos::new(2, 2)].glyph(), 'X');
assert_eq!(term.grid()[Pos::new(15, 15)].glyph(), ' ');
}
#[test]
fn test_terminal_resize_event_auto_applies() {
let mut term = Terminal::new(Headless::new(10, 10));
term.backend_mut().push_event(Event::Resize(80, 25));
let event = term.poll(core::time::Duration::ZERO);
assert_eq!(event, Some(Event::Resize(80, 25)));
assert_eq!(term.size(), Size::new(80, 25));
}
#[test]
fn test_terminal_resize_after_flatten_buffers_allocated() {
let mut term = Terminal::new(Headless::new(3, 3));
term.draw(|s| {
s.put((0, 0), 'A', Style::default());
s.on_layer(1).put((1, 1), 'B', Style::default());
})
.expect("draw failed");
term.resize(5, 5);
assert_eq!(term.size(), Size::new(5, 5));
term.draw(|s| {
s.put((0, 0), 'A', Style::default());
s.on_layer(1).put((1, 1), 'B', Style::default());
})
.expect("draw failed");
assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'A');
assert_eq!(term.backend().grid()[Pos::new(1, 1)].glyph(), 'B');
}
#[test]
fn test_terminal_resize_new_cells_accessible() {
let mut term = Terminal::new(Headless::new(3, 3));
term.draw(|s| s.put((0, 0), 'A', Style::default()))
.expect("draw failed");
term.resize(5, 5);
term.draw(|s| s.put((4, 4), 'B', Style::default()))
.expect("draw failed");
assert_eq!(term.backend().grid()[Pos::new(4, 4)].glyph(), 'B');
assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'A');
}
#[test]
fn test_terminal_resize_clears_pending_retention() {
use crate::surface::Layer;
let mut term = Terminal::new(Headless::new(3, 1));
term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'W', Style::default()))
.expect("draw failed");
term.retain_layer(Layer::World);
term.resize(3, 1);
term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'X', Style::default()))
.expect("draw failed");
assert_eq!(
term.backend().grid()[Pos::new(0, 0)].glyph(),
'X',
"pending retention must not survive resize: a still-active blit from the \
(resize-cleared) previous frame would have overwritten this fresh draw"
);
}
}