use super::messages::{InputEvent, RenderCommand};
use super::{InputActor, RendererActor};
use crate::buffer::{Buffer, Cell, Rgb};
use crate::layout::Rect;
use crossbeam_channel::{bounded, Receiver, Sender, TryRecvError};
use crossterm::{
cursor,
event::EnableMouseCapture,
execute,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::io::{self};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub target_fps: u32,
pub input_poll_timeout: Duration,
pub enable_mouse: bool,
pub alternate_screen: bool,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
target_fps: 60,
input_poll_timeout: Duration::from_millis(10),
enable_mouse: false,
alternate_screen: true,
}
}
}
pub struct Engine {
config: EngineConfig,
input_rx: Receiver<InputEvent>,
render_tx: Sender<RenderCommand>,
input_actor: Option<InputActor>,
#[allow(dead_code)]
renderer_actor: Option<RendererActor>,
buffer: Buffer,
width: u16,
height: u16,
frame_start: Instant,
frame_duration: Duration,
frame_count: u64,
running: bool,
}
impl Engine {
pub fn new() -> io::Result<Self> {
Self::with_config(EngineConfig::default())
}
pub fn with_config(config: EngineConfig) -> io::Result<Self> {
let (width, height) = terminal::size()?;
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
if config.alternate_screen {
execute!(stdout, EnterAlternateScreen)?;
}
if config.enable_mouse {
execute!(stdout, EnableMouseCapture)?;
}
execute!(stdout, cursor::Hide)?;
let (input_tx, input_rx) = bounded::<InputEvent>(64);
let (render_tx, render_rx) = bounded::<RenderCommand>(16);
let input_actor = InputActor::spawn(input_tx, config.input_poll_timeout);
let renderer_actor = RendererActor::spawn(render_rx, width, height);
let frame_duration = Duration::from_secs(1) / config.target_fps;
Ok(Self {
config,
input_rx,
render_tx,
input_actor: Some(input_actor),
renderer_actor: Some(renderer_actor),
buffer: Buffer::new(width, height),
width,
height,
frame_start: Instant::now(),
frame_duration,
frame_count: 0,
running: true,
})
}
pub const fn width(&self) -> u16 {
self.width
}
pub const fn height(&self) -> u16 {
self.height
}
pub const fn buffer(&self) -> &Buffer {
&self.buffer
}
pub const fn buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffer
}
pub const fn input_receiver(&self) -> &Receiver<InputEvent> {
&self.input_rx
}
pub const fn is_running(&self) -> bool {
self.running
}
pub const fn stop(&mut self) {
self.running = false;
}
pub fn poll_input(&self) -> Option<InputEvent> {
match self.input_rx.try_recv() {
Ok(event) => Some(event),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
Some(InputEvent::Error("Input channel disconnected".to_string()))
}
}
}
pub fn wait_input(&self, timeout: Duration) -> Option<InputEvent> {
self.input_rx.recv_timeout(timeout).ok()
}
pub fn drain_input(&self) -> Vec<InputEvent> {
let mut events = Vec::new();
while let Ok(event) = self.input_rx.try_recv() {
events.push(event);
}
events
}
pub fn request_redraw(&self) {
let _ = self.render_tx.send(RenderCommand::FullRedraw(Box::new(self.buffer.clone())));
}
pub fn request_update(&self) {
let _ = self.render_tx.send(RenderCommand::Update(Box::new(self.buffer.clone())));
}
pub fn set_cursor(&self, x: Option<u16>, y: u16) {
let _ = self.render_tx.send(RenderCommand::SetCursor { x, y });
}
pub fn write_raw(&self, bytes: Vec<u8>) {
let _ = self.render_tx.send(RenderCommand::RawOutput { bytes });
}
pub fn handle_resize(&mut self, width: u16, height: u16) {
self.width = width;
self.height = height;
self.buffer.resize(width, height);
let _ = self.render_tx.send(RenderCommand::Resize { width, height });
}
pub fn begin_frame(&mut self) {
self.frame_start = Instant::now();
}
pub fn end_frame(&mut self) {
self.frame_count += 1;
self.request_update();
let elapsed = self.frame_start.elapsed();
if elapsed < self.frame_duration {
std::thread::sleep(self.frame_duration - elapsed);
}
}
pub const fn frame_count(&self) -> u64 {
self.frame_count
}
pub fn set_cell(&mut self, x: u16, y: u16, cell: Cell) {
self.buffer.set(x, y, cell);
}
pub fn set_grapheme(&mut self, x: u16, y: u16, grapheme: &str, fg: Rgb, bg: Rgb) -> u8 {
self.buffer.set_grapheme(x, y, grapheme, fg, bg)
}
pub fn clear(&mut self) {
self.buffer.clear();
}
pub fn fill_rect(&mut self, rect: Rect, cell: Cell) {
self.buffer.fill_rect(rect.x, rect.y, rect.width, rect.height, cell);
}
pub fn draw_text(&mut self, x: u16, y: u16, text: &str, fg: Rgb, bg: Rgb) -> u16 {
let mut col = x;
for grapheme in unicode_segmentation::UnicodeSegmentation::graphemes(text, true) {
if col >= self.width {
break;
}
let width = self.buffer.set_grapheme(col, y, grapheme, fg, bg);
col += u16::from(width);
}
col - x
}
}
impl Drop for Engine {
fn drop(&mut self) {
if let Some(actor) = self.input_actor.take() {
actor.join();
}
let _ = self.render_tx.send(RenderCommand::Shutdown);
let mut stdout = io::stdout();
let _ = execute!(stdout, cursor::Show);
if self.config.enable_mouse {
let _ = execute!(stdout, crossterm::event::DisableMouseCapture);
}
if self.config.alternate_screen {
let _ = execute!(stdout, LeaveAlternateScreen);
}
let _ = terminal::disable_raw_mode();
}
}