use std::io::{Result, Write};
use anathema_geometry::{Pos, Size};
use anathema_value_resolver::Attributes;
use anathema_widgets::paint::Glyph;
use anathema_widgets::{GlyphMap, Style, WidgetRenderer};
use crossterm::event::EnableMouseCapture;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
use crossterm::{ExecutableCommand, QueueableCommand, cursor};
use super::LocalPos;
use super::buffer::{Buffer, Change, diff, draw_changes};
pub struct Screen {
pub(crate) new_buffer: Buffer,
old_buffer: Buffer,
changes: Vec<(LocalPos, Option<Style>, Change)>,
}
impl Screen {
pub(super) fn hide_cursor(mut output: impl Write) -> Result<()> {
output.queue(cursor::Hide)?;
Ok(())
}
pub(super) fn show_cursor(mut output: impl Write) -> Result<()> {
output.queue(cursor::Show)?;
Ok(())
}
pub(super) fn enable_mouse(mut output: impl Write) -> Result<()> {
output.queue(EnableMouseCapture)?;
Ok(())
}
pub fn new(size: impl Into<Size>) -> Self {
let size: Size = size.into();
Self {
old_buffer: Buffer::new(size),
new_buffer: Buffer::new(size),
changes: vec![],
}
}
pub(super) fn resize(&mut self, new_size: Size) {
self.old_buffer = Buffer::new(new_size);
self.new_buffer = Buffer::reset(new_size);
}
pub(crate) fn erase(&mut self) {
self.erase_region(LocalPos::ZERO, self.size());
}
pub(crate) fn erase_region(&mut self, pos: LocalPos, size: Size) {
let to_x = (size.width + pos.x).min(self.size().width);
let to_y = (size.height + pos.y).min(self.size().height);
for x in pos.x.min(to_x)..to_x {
for y in pos.y.min(to_y)..to_y {
self.new_buffer.reset_cell(LocalPos::new(x, y));
}
}
}
pub(crate) fn paint_glyph(&mut self, glyph: Glyph, pos: LocalPos) {
self.new_buffer.put_glyph(glyph, pos);
}
pub(crate) fn update_cell(&mut self, style: Style, pos: LocalPos) {
self.new_buffer.update_cell(style, pos);
}
pub(crate) fn render(&mut self, mut output: impl Write, glyph_map: &GlyphMap) -> Result<()> {
diff(&mut self.old_buffer, &mut self.new_buffer, &mut self.changes)?;
if self.changes.is_empty() {
return Ok(());
}
draw_changes(&mut output, glyph_map, &self.changes)?;
self.changes.clear();
output.flush()?;
Ok(())
}
pub fn enter_alt_screen(mut output: impl Write) -> Result<()> {
output.execute(EnterAlternateScreen)?;
Ok(())
}
pub fn enable_raw_mode() -> Result<()> {
enable_raw_mode()?;
Ok(())
}
pub fn disable_raw_mode() -> Result<()> {
disable_raw_mode()?;
Ok(())
}
pub fn restore(&mut self, mut output: impl Write, leave_alt_screen: bool) -> Result<()> {
disable_raw_mode()?;
if leave_alt_screen {
output.execute(LeaveAlternateScreen)?;
}
#[cfg(not(target_os = "windows"))]
output.execute(crossterm::event::DisableMouseCapture)?;
output.execute(cursor::Show)?;
Ok(())
}
}
impl WidgetRenderer for Screen {
fn draw_glyph(&mut self, c: Glyph, pos: Pos) {
let Ok(screen_pos) = pos.try_into() else { return };
self.paint_glyph(c, screen_pos);
}
fn set_attributes(&mut self, attribs: &Attributes<'_>, pos: Pos) {
let style = Style::from_cell_attribs(attribs);
self.set_style(style, pos)
}
fn set_style(&mut self, style: Style, local_pos: Pos) {
let Ok(screen_pos) = local_pos.try_into() else { return };
self.update_cell(style, screen_pos);
}
fn size(&self) -> Size {
self.new_buffer.size()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::tui::buffer::{Cell, CellState};
fn state_at(buffer: &Buffer, x: usize, y: usize) -> CellState {
let cell = buffer[(x, y)];
cell.state
}
fn char_at(buffer: &Buffer, x: usize, y: usize) -> char {
match state_at(buffer, x, y) {
CellState::Occupied(Glyph::Single(c, _)) => c,
_ => panic!(),
}
}
#[test]
fn render() {
let mut render_output = vec![];
let glyph_map = GlyphMap::empty();
let mut screen = Screen::new(Size::new(1, 1));
screen.paint_glyph(Glyph::from_char('x', 1), LocalPos::ZERO);
screen.render(&mut render_output, &glyph_map).unwrap();
let expected = Cell::new(Glyph::from_char('x', 1), Style::reset());
let actual = screen.new_buffer.inner[0];
assert_eq!(expected, actual);
}
#[test]
fn erase_region() {
let mut render_output = vec![];
let glyph_map = GlyphMap::empty();
let mut screen = Screen::new(Size::new(2, 2));
screen.render(&mut render_output, &glyph_map).unwrap();
screen.erase_region(LocalPos::new(1, 1), Size::new(2, 2));
let top_left = screen.new_buffer.inner[0];
assert_eq!(Cell::empty(), top_left);
let bottom_right = screen.new_buffer.inner[3];
assert_eq!(Cell::empty(), bottom_right);
}
#[test]
#[should_panic(expected = "position out of bounds")]
fn put_outside_of_screen() {
let glyph_map = GlyphMap::empty();
let mut screen = Screen::new(Size::new(1, 1));
screen.paint_glyph(Glyph::from_char('x', 1), LocalPos::new(3, 0));
screen.render(&mut vec![], &glyph_map).unwrap();
}
#[test]
fn erasing_unicode_with_continuation_cell() {
let glyph_map = GlyphMap::empty();
let mut screen = Screen::new(Size::new(4, 1));
let bunny = '🐇';
screen.paint_glyph(Glyph::from_char('1', 1), LocalPos::new(2, 0));
screen.paint_glyph(Glyph::from_char('0', 1), LocalPos::new(3, 0));
screen.paint_glyph(Glyph::from_char(bunny, 2), LocalPos::new(0, 0));
screen.render(&mut vec![], &glyph_map).unwrap();
assert_eq!(char_at(&screen.new_buffer, 0, 0), bunny);
assert_eq!(state_at(&screen.new_buffer, 1, 0), CellState::Continuation);
assert_eq!(char_at(&screen.new_buffer, 2, 0), '1');
assert_eq!(char_at(&screen.new_buffer, 3, 0), '0');
screen.erase();
screen.paint_glyph(Glyph::from_char('1', 1), LocalPos::new(3, 0));
screen.paint_glyph(Glyph::from_char(bunny, 2), LocalPos::new(1, 0));
screen.render(&mut vec![], &glyph_map).unwrap();
assert_eq!(state_at(&screen.new_buffer, 0, 0), CellState::Empty);
assert_eq!(char_at(&screen.new_buffer, 1, 0), bunny);
assert_eq!(state_at(&screen.new_buffer, 2, 0), CellState::Continuation);
assert_eq!(char_at(&screen.new_buffer, 3, 0), '1');
}
}