use crate::spec_ai_tui::buffer::Buffer;
use crate::spec_ai_tui::event::{Event, EventLoop};
use crate::spec_ai_tui::geometry::Rect;
use crate::spec_ai_tui::terminal::Terminal;
use std::io;
use std::time::Duration;
pub trait App {
type State;
fn init(&self) -> Self::State;
fn handle_event(&mut self, event: Event, state: &mut Self::State) -> bool;
fn render(&self, state: &Self::State, area: Rect, buf: &mut Buffer);
fn on_tick(&mut self, _state: &mut Self::State) {}
}
pub struct AppRunner<A: App> {
app: A,
terminal: Terminal,
event_loop: EventLoop,
tick_rate: Duration,
}
impl<A: App> AppRunner<A> {
pub fn new(app: A) -> io::Result<Self> {
let terminal = Terminal::new()?;
let tick_rate = Duration::from_millis(100);
let event_loop = EventLoop::new(tick_rate);
Ok(Self {
app,
terminal,
event_loop,
tick_rate,
})
}
pub fn tick_rate(mut self, rate: Duration) -> Self {
self.tick_rate = rate;
self.event_loop = EventLoop::new(rate);
self
}
pub fn event_sender(&self) -> tokio::sync::mpsc::UnboundedSender<Event> {
self.event_loop.sender()
}
pub async fn run(&mut self) -> io::Result<()> {
let _raw_guard = self.terminal.enter_raw_mode()?;
let mut state = self.app.init();
self.render(&state)?;
#[allow(clippy::while_let_loop)]
loop {
if let Some(event) = self.event_loop.next().await {
if let Event::Resize { .. } = &event {
self.terminal.refresh_size()?;
self.terminal.invalidate();
}
if matches!(event, Event::Tick) {
self.app.on_tick(&mut state);
}
if !self.app.handle_event(event, &mut state) {
break;
}
self.render(&state)?;
} else {
break;
}
}
Ok(())
}
fn render(&mut self, state: &A::State) -> io::Result<()> {
let area = self.terminal.full_rect();
let mut buf = Buffer::new(area);
self.app.render(state, area, &mut buf);
self.terminal.draw(&buf)
}
}
#[allow(dead_code)]
pub struct SimpleApp<S, F, R>
where
F: FnMut(Event, &mut S) -> bool,
R: Fn(&S, Rect, &mut Buffer),
{
init_fn: Box<dyn Fn() -> S>,
handle_fn: F,
render_fn: R,
}
impl<S, F, R> SimpleApp<S, F, R>
where
F: FnMut(Event, &mut S) -> bool,
R: Fn(&S, Rect, &mut Buffer),
{
#[allow(dead_code)]
pub fn new<I>(init: I, handle: F, render: R) -> Self
where
I: Fn() -> S + 'static,
{
Self {
init_fn: Box::new(init),
handle_fn: handle,
render_fn: render,
}
}
}
impl<S, F, R> App for SimpleApp<S, F, R>
where
F: FnMut(Event, &mut S) -> bool,
R: Fn(&S, Rect, &mut Buffer),
{
type State = S;
fn init(&self) -> Self::State {
(self.init_fn)()
}
fn handle_event(&mut self, event: Event, state: &mut Self::State) -> bool {
(self.handle_fn)(event, state)
}
fn render(&self, state: &Self::State, area: Rect, buf: &mut Buffer) {
(self.render_fn)(state, area, buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec_ai_tui::style::Style;
struct TestApp;
impl App for TestApp {
type State = u32;
fn init(&self) -> Self::State {
0
}
fn handle_event(&mut self, event: Event, state: &mut Self::State) -> bool {
if event.is_quit() {
return false;
}
*state += 1;
true
}
fn render(&self, state: &Self::State, area: Rect, buf: &mut Buffer) {
let text = format!("Count: {}", state);
buf.set_string(area.x, area.y, &text, Style::default());
}
}
#[test]
fn test_app_init() {
let app = TestApp;
let state = app.init();
assert_eq!(state, 0);
}
#[test]
fn test_app_render() {
let app = TestApp;
let state = 42u32;
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::new(area);
app.render(&state, area, &mut buf);
assert_eq!(buf.get(0, 0).unwrap().symbol, "C");
assert_eq!(buf.get(7, 0).unwrap().symbol, "4");
}
}