mod app;
mod app_event_context;
mod event_loop;
pub use app::*;
use app_event_context::*;
pub use event_loop::*;
use core::{
render::{Position, Size},
widget::Clipboard,
};
use std::{error::Error, io::stdout, marker::PhantomData};
use crossterm::terminal;
use core::{
input::TerminalEvent,
render::{RenderContext, WriteContext},
widget::{self, Widget},
};
pub type AppRunResult = Result<(), RunFailure>;
#[derive(Debug)]
pub enum RunFailure {
InputError(InputError),
IOError(std::io::Error),
Other(Box<dyn Error>),
}
#[derive(Debug)]
pub enum InputError {
Unsupported,
IO(std::io::Error),
}
pub trait Runtime<Message, A>
where
A: App<Message>,
{
fn run_app(
&mut self,
available_size: Size,
clipboard: &mut impl Clipboard,
render_context: &mut impl RenderContext,
app: A,
) -> AppRunResult;
}
pub struct DefaultRuntime<Message, EL: EventLoop, A: App<Message>> {
event_loop: EL,
phantom_message: PhantomData<Message>,
phantom_app: PhantomData<A>,
}
impl<Message, EL: EventLoop, A: App<Message>> DefaultRuntime<Message, EL, A> {
pub fn new(event_loop: EL) -> Self {
Self {
event_loop,
phantom_message: PhantomData,
phantom_app: PhantomData,
}
}
pub fn run_app_default(&mut self, app: A) -> AppRunResult {
let mut stdout = stdout();
{
crossterm::execute!(
stdout,
crossterm::event::EnableFocusChange,
crossterm::event::EnableMouseCapture,
crossterm::event::EnableBracketedPaste,
)
.map_err(RunFailure::IOError)?;
crossterm::terminal::enable_raw_mode().map_err(RunFailure::IOError)?;
}
let available_size = terminal::size()
.map(|tuple| tuple.into())
.map_err(RunFailure::IOError)?;
let mut clipboard = widget::clipboard().map_err(RunFailure::Other)?;
let mut render_context = WriteContext::new(&mut stdout);
let result = self.run_app(available_size, &mut clipboard, &mut render_context, app);
{
crossterm::terminal::disable_raw_mode().map_err(RunFailure::IOError)?;
}
result
}
}
impl<Message, A: App<Message>> Default for DefaultRuntime<Message, DefaultEventLoop, A> {
fn default() -> Self {
Self {
event_loop: DefaultEventLoop,
phantom_message: Default::default(),
phantom_app: Default::default(),
}
}
}
impl<Message, EL: EventLoop, A: App<Message>> Runtime<Message, A>
for DefaultRuntime<Message, EL, A>
{
fn run_app(
&mut self,
available_size: Size,
clipboard: &mut impl Clipboard,
render_context: &mut impl RenderContext,
app: A,
) -> AppRunResult {
let mut app = app;
let mut available_size = available_size;
let mut widget = app.view();
let mut layout_info = widget.layout(available_size);
let mut render_info = widget
.render(Position::ZERO, &layout_info, render_context)
.map_err(RunFailure::IOError)?;
loop {
let event = self.event_loop.poll();
match event {
Ok(TerminalEvent::App(event)) => {
let mut event_context = AppEventContext::new(
crossterm::cursor::position().unwrap().into(),
clipboard,
);
widget.on_event(&event, &render_info, &mut event_context);
if let Some(message) = event_context.message() {
let mut exit_app = false;
drop(widget);
drop(layout_info);
drop(render_info);
app.update(message, &mut exit_app);
if exit_app {
return AppRunResult::Ok(());
} else {
widget = app.view();
layout_info = widget.layout(available_size);
let result =
widget.render(Position::ZERO, &layout_info, render_context);
match result {
Ok(value) => {
render_info = value;
}
Err(error) => return Err(RunFailure::IOError(error)),
}
}
}
}
Ok(TerminalEvent::Resize(size)) => {
available_size = size;
widget = app.view();
layout_info = widget.layout(available_size);
let result = widget.render(Position::ZERO, &layout_info, render_context);
match result {
Ok(value) => {
render_info = value;
}
Err(error) => return Err(RunFailure::IOError(error)),
}
}
Err(error) => return AppRunResult::Err(RunFailure::InputError(error)),
};
}
}
}