use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Stylize,
symbols::border,
text::{Line, Text},
widgets::{
block::{Position, Title},
Block, Paragraph, Widget,
},
DefaultTerminal, Frame,
};
use crate::canvas::Canvas;
#[derive(Default)]
struct App {
cursor_x: u16,
cursor_y: u16,
canvas: Canvas,
exit: bool,
rect: Option<crate::Rect>,
}
impl App {
fn run(&mut self, mut terminal: DefaultTerminal) -> Result<()> {
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
fn draw(&self, frame: &mut Frame) {
frame.render_widget(self, frame.area());
frame.set_cursor_position((self.cursor_x + 1, self.cursor_y + 1));
}
fn handle_events(&mut self) -> Result<()> {
match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event)
}
_ => {}
};
Ok(())
}
fn warp_cursor(&mut self, x: u16, y: u16) {
self.cursor_x = x;
self.cursor_y = y;
log::debug!("Moved cursor to ({}, {})", self.cursor_x, self.cursor_y);
}
fn move_cursor(&mut self, x: i16, y: i16) {
self.cursor_x = self.cursor_x.saturating_add_signed(x);
self.cursor_y = self.cursor_y.saturating_add_signed(y);
log::debug!("Moved cursor to ({}, {})", self.cursor_x, self.cursor_y);
if let Some(rect) = &mut self.rect {
rect.x2 = self.cursor_x;
rect.y2 = self.cursor_y;
log::debug!("Updated rect to {rect:?}");
}
}
fn handle_key_event(&mut self, key: KeyEvent) {
log::trace!("Handling key {:?}", key);
match key.code {
KeyCode::Char('q') => self.exit = true,
KeyCode::Char('w') => self.move_cursor(0, -1),
KeyCode::Char('s') => self.move_cursor(0, 1),
KeyCode::Char('a') => self.move_cursor(-1, 0),
KeyCode::Char('d') => self.move_cursor(1, 0),
KeyCode::Char('r') => {
self.rect = Some(crate::Rect {
x1: self.cursor_x,
y1: self.cursor_y,
x2: 0,
y2: 0,
});
log::debug!("Added rect {:?}", self.rect);
self.move_cursor(1, 1);
}
KeyCode::Enter => {
if let Some(rect) = &self.rect {
log::debug!("Confirming rect {:?}", self.rect);
rect.draw(&mut self.canvas);
self.rect = None;
}
}
KeyCode::Esc => {
if let Some(rect) = self.rect.take() {
log::debug!("Cancelling rect {:?}", self.rect);
self.warp_cursor(rect.x1, rect.y1);
}
}
_ => {}
}
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let title = Title::from("Boxt".bold());
let instructions = Title::from(Line::from(vec![
" Move ".into(),
"<WASD>".blue().bold(),
" Rect ".into(),
"<R>".blue().bold(),
" Quit ".into(),
"<Q> ".blue().bold(),
]));
let block = Block::bordered()
.title(title.alignment(Alignment::Center))
.title(
instructions
.alignment(Alignment::Center)
.position(Position::Bottom),
)
.border_set(border::THICK);
let mut canvas = self.canvas.clone();
if let Some(rect) = &self.rect {
log::debug!("Drawing rect: {rect:?}");
rect.draw(&mut canvas);
}
let text = Text::raw(canvas.to_string());
Paragraph::new(text).block(block).render(area, buf);
}
}
pub fn start() -> Result<()> {
let mut terminal = ratatui::init();
terminal.clear()?;
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
fn buf_string(buf: &Buffer) -> String {
buf.content
.chunks(buf.area.width as usize)
.map(|line| {
line.iter()
.map(|cell| cell.symbol().to_string())
.collect::<Vec<_>>()
.join("")
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn test_render_empty() {
let app = App::default();
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 8));
app.render(buf.area, &mut buf);
assert_snapshot!(buf_string(&buf));
}
#[test]
fn test_draw_rect() {
let mut app = App::default();
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 8));
app.handle_key_event(KeyCode::Char('r').into());
app.handle_key_event(KeyCode::Char('s').into());
app.handle_key_event(KeyCode::Char('d').into());
app.handle_key_event(KeyCode::Enter.into());
app.handle_key_event(KeyCode::Char('d').into());
app.handle_key_event(KeyCode::Char('d').into());
app.handle_key_event(KeyCode::Char('r').into());
app.handle_key_event(KeyCode::Char('s').into());
app.handle_key_event(KeyCode::Char('d').into());
app.render(buf.area, &mut buf);
assert_snapshot!(buf_string(&buf));
}
#[test]
fn test_cancel_rect() {
let mut app = App::default();
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 8));
app.handle_key_event(KeyCode::Char('r').into());
app.handle_key_event(KeyCode::Char('s').into());
app.handle_key_event(KeyCode::Char('d').into());
app.handle_key_event(KeyCode::Esc.into());
app.handle_key_event(KeyCode::Char('d').into());
app.handle_key_event(KeyCode::Char('d').into());
app.handle_key_event(KeyCode::Char('r').into());
app.handle_key_event(KeyCode::Char('s').into());
app.handle_key_event(KeyCode::Char('d').into());
app.render(buf.area, &mut buf);
assert_snapshot!(buf_string(&buf));
}
}