pub extern crate crossterm;
pub mod pixel;
pub mod rect_style;
pub mod screen;
mod utils;
#[cfg(feature = "event")]
pub mod events;
#[cfg(feature = "form")]
pub mod forms;
pub use crossterm::event::{KeyCode, KeyModifiers, MouseButton};
pub use crossterm::style::Color;
use crossterm::terminal::{self, ClearType};
use crossterm::{
event::{self, Event, KeyEvent, MouseEvent, MouseEventKind},
ErrorKind,
};
use crossterm::{execute, queue, style};
use pixel::Pixel;
use rect_style::BorderStyle;
use screen::Screen;
use std::io::Write;
use std::io::{stdout, Stdout};
#[allow(clippy::needless_doctest_main)]
pub struct ConsoleEngine {
stdout: Stdout,
time_limit: std::time::Duration,
pub frame_count: usize,
width: u32,
height: u32,
screen: Screen,
screen_last_frame: Screen,
instant: std::time::Instant,
keys_pressed: Vec<KeyEvent>,
keys_held: Vec<KeyEvent>,
keys_released: Vec<KeyEvent>,
mouse_events: Vec<MouseEvent>,
resize_events: Vec<(u16, u16)>,
}
impl ConsoleEngine {
pub fn init(width: u32, height: u32, target_fps: u32) -> Result<ConsoleEngine, ErrorKind> {
assert!(target_fps > 0, "Target FPS needs to be greater than zero.");
let mut engine = ConsoleEngine {
stdout: stdout(),
time_limit: std::time::Duration::from_millis(1000 / target_fps as u64),
frame_count: 0,
width,
height,
screen: Screen::new(width, height),
screen_last_frame: Screen::new_empty(width, height),
instant: std::time::Instant::now(),
keys_pressed: vec![],
keys_held: vec![],
keys_released: vec![],
mouse_events: vec![],
resize_events: vec![],
};
let previous_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
Self::handle_panic(panic_info);
previous_panic_hook(panic_info);
std::process::exit(1);
}));
engine.begin()?;
engine.try_resize(width, height)?;
Ok(engine)
}
pub fn init_fill(target_fps: u32) -> Result<ConsoleEngine, ErrorKind> {
let size = crossterm::terminal::size().unwrap();
ConsoleEngine::init(size.0 as u32, size.1 as u32, target_fps)
}
pub fn init_fill_require(
width: u32,
height: u32,
target_fps: u32,
) -> Result<ConsoleEngine, ErrorKind> {
let mut engine = ConsoleEngine::init_fill(target_fps)?;
engine.try_resize(width, height)?;
Ok(engine)
}
fn try_resize(&mut self, width: u32, height: u32) -> Result<(), ErrorKind> {
let size = crossterm::terminal::size()?;
if (size.0 as u32) < width || (size.1 as u32) < height {
execute!(
self.stdout,
crossterm::terminal::SetSize(width as u16, height as u16),
crossterm::terminal::SetSize(width as u16, height as u16)
)?;
self.resize(width, height);
}
if crossterm::terminal::size()? < (width as u16, height as u16) {
Err(ErrorKind::new(std::io::ErrorKind::Other, format!("Your terminal must have at least a width and height of {}x{} characters. Currently has {}x{}", width, height, size.0, size.1)))
} else {
Ok(())
}
}
fn begin(&mut self) -> Result<(), ErrorKind> {
terminal::enable_raw_mode().unwrap();
execute!(
self.stdout,
terminal::EnterAlternateScreen,
terminal::Clear(ClearType::All),
crossterm::cursor::Hide,
crossterm::cursor::MoveTo(0, 0),
crossterm::event::EnableMouseCapture
)
}
fn end(&mut self) {
execute!(
self.stdout,
crossterm::cursor::Show,
style::SetBackgroundColor(Color::Reset),
style::SetForegroundColor(Color::Reset),
crossterm::event::DisableMouseCapture,
terminal::LeaveAlternateScreen
)
.unwrap();
terminal::disable_raw_mode().unwrap();
}
fn handle_panic(_panic_info: &std::panic::PanicInfo) {
execute!(
stdout(),
crossterm::cursor::Show,
style::SetBackgroundColor(Color::Reset),
style::SetForegroundColor(Color::Reset),
crossterm::event::DisableMouseCapture,
terminal::LeaveAlternateScreen
)
.unwrap();
terminal::disable_raw_mode().unwrap();
}
pub fn set_title(&mut self, title: &str) {
execute!(self.stdout, crossterm::terminal::SetTitle(title)).ok();
}
pub fn get_width(&self) -> u32 {
self.screen.get_width()
}
pub fn get_height(&self) -> u32 {
self.screen.get_height()
}
pub fn clear_screen(&mut self) {
self.screen.clear()
}
pub fn fill(&mut self, pixel: Pixel) {
self.screen.fill(pixel);
}
pub fn print(&mut self, x: i32, y: i32, string: &str) {
self.screen.print(x, y, string)
}
pub fn print_fbg(&mut self, x: i32, y: i32, string: &str, fg: Color, bg: Color) {
self.screen.print_fbg(x, y, string, fg, bg)
}
pub fn print_screen(&mut self, x: i32, y: i32, source: &Screen) {
self.screen.print_screen(x, y, source)
}
pub fn print_screen_alpha(&mut self, x: i32, y: i32, source: &Screen, alpha_character: char) {
self.screen
.print_screen_alpha(x, y, source, alpha_character)
}
pub fn line(&mut self, start_x: i32, start_y: i32, end_x: i32, end_y: i32, character: Pixel) {
self.screen.line(start_x, start_y, end_x, end_y, character)
}
pub fn rect(&mut self, start_x: i32, start_y: i32, end_x: i32, end_y: i32, character: Pixel) {
self.screen.rect(start_x, start_y, end_x, end_y, character)
}
pub fn rect_border(
&mut self,
start_x: i32,
start_y: i32,
end_x: i32,
end_y: i32,
rect_style: BorderStyle,
) {
self.screen
.rect_border(start_x, start_y, end_x, end_y, rect_style)
}
pub fn fill_rect(
&mut self,
start_x: i32,
start_y: i32,
end_x: i32,
end_y: i32,
character: Pixel,
) {
self.screen
.fill_rect(start_x, start_y, end_x, end_y, character)
}
pub fn circle(&mut self, x: i32, y: i32, radius: u32, character: Pixel) {
self.screen.circle(x, y, radius, character)
}
pub fn fill_circle(&mut self, x: i32, y: i32, radius: u32, character: Pixel) {
self.screen.fill_circle(x, y, radius, character)
}
#[allow(clippy::too_many_arguments)]
pub fn triangle(
&mut self,
x1: i32,
y1: i32,
x2: i32,
y2: i32,
x3: i32,
y3: i32,
character: Pixel,
) {
self.screen.triangle(x1, y1, x2, y2, x3, y3, character)
}
#[allow(clippy::too_many_arguments)]
pub fn fill_triangle(
&mut self,
x1: i32,
y1: i32,
x2: i32,
y2: i32,
x3: i32,
y3: i32,
character: Pixel,
) {
self.screen.fill_triangle(x1, y1, x2, y2, x3, y3, character)
}
pub fn scroll(&mut self, h_scroll: i32, v_scroll: i32, background: Pixel) {
self.screen.scroll(h_scroll, v_scroll, background);
}
pub fn set_pxl(&mut self, x: i32, y: i32, character: Pixel) {
self.screen.set_pxl(x, y, character)
}
pub fn get_pxl(&self, x: i32, y: i32) -> Result<Pixel, String> {
self.screen.get_pxl(x, y)
}
pub fn resize(&mut self, new_width: u32, new_height: u32) {
self.screen.resize(new_width, new_height);
self.width = new_width;
self.height = new_height;
self.screen_last_frame = Screen::new_empty(self.width, self.height);
}
pub fn extract(
&self,
start_x: i32,
start_y: i32,
end_x: i32,
end_y: i32,
default: Pixel,
) -> Screen {
self.screen.extract(start_x, start_y, end_x, end_y, default)
}
pub fn set_screen(&mut self, screen: &Screen) {
self.width = screen.get_width();
self.height = screen.get_height();
self.screen = screen.clone();
self.request_full_draw();
}
pub fn get_screen(&self) -> Screen {
self.screen.clone()
}
pub fn draw(&mut self) {
queue!(self.stdout, crossterm::cursor::MoveTo(0, 0)).unwrap();
let mut first = true;
let mut current_colors: (Color, Color) = (Color::Reset, Color::Reset);
let mut moving = false;
self.screen_last_frame.check_empty(); let mut skip_next = false;
for y in 0..self.height as i32 {
for x in 0..self.width as i32 {
let pixel = self.screen.get_pxl(x, y).unwrap();
if skip_next {
skip_next = false;
continue;
}
if let Some(char_width) = unicode_width::UnicodeWidthChar::width(pixel.chr) {
if char_width > 1 {
skip_next = true;
}
}
if self.screen_last_frame.is_empty()
|| pixel != self.screen_last_frame.get_pxl(x, y).unwrap()
{
if moving {
queue!(self.stdout, crossterm::cursor::MoveTo(x as u16, y as u16)).unwrap();
moving = false;
}
if current_colors != pixel.get_colors() || first {
current_colors = pixel.get_colors();
queue!(
self.stdout,
style::SetForegroundColor(pixel.fg),
style::SetBackgroundColor(pixel.bg),
style::Print(pixel.chr)
)
.unwrap();
first = false;
} else {
queue!(self.stdout, style::Print(pixel.chr)).unwrap();
}
} else {
moving = true
}
}
if y < self.height as i32 - 1 {
queue!(self.stdout, style::Print("\r\n")).unwrap();
}
}
self.stdout.flush().unwrap();
self.screen_last_frame = self.screen.clone();
}
pub fn request_full_draw(&mut self) {
self.screen_last_frame = Screen::new_empty(self.width, self.height);
}
pub fn wait_frame(&mut self) {
let mut captured_keyboard: Vec<KeyEvent> = vec![];
let mut captured_mouse: Vec<MouseEvent> = vec![];
let mut captured_resize: Vec<(u16, u16)> = vec![];
let mut elapsed_time = self.instant.elapsed();
while self.time_limit > elapsed_time {
let remaining_time = self.time_limit - elapsed_time;
if let Ok(has_event) = event::poll(std::time::Duration::from_millis(
(remaining_time.as_millis() % self.time_limit.as_millis()) as u64,
)) {
if has_event {
if let Ok(current_event) = event::read() {
match current_event {
Event::Key(evt) => {
captured_keyboard.push(evt);
}
Event::Mouse(evt) => {
captured_mouse.push(evt);
}
Event::Resize(w, h) => {
captured_resize.push((w, h));
}
};
}
}
}
elapsed_time = self.instant.elapsed();
}
self.instant = std::time::Instant::now();
self.frame_count = self.frame_count.wrapping_add(1);
let held = utils::intersect(
&utils::union(&self.keys_pressed, &self.keys_held),
&captured_keyboard,
);
self.keys_released = utils::outersect_left(&self.keys_held, &held);
self.keys_pressed = utils::outersect_left(&captured_keyboard, &held);
self.keys_held = utils::union(&held, &self.keys_pressed);
self.mouse_events = captured_mouse;
self.resize_events = captured_resize;
}
#[cfg(feature = "event")]
pub fn poll(&mut self) -> events::Event {
use std::time::Duration;
let mut elapsed_time = self.instant.elapsed();
loop {
let remaining_time = if self.time_limit > elapsed_time {
self.time_limit - elapsed_time
} else {
Duration::from_millis(0)
};
if let Ok(has_event) = event::poll(std::time::Duration::from_millis(
(remaining_time.as_millis() % self.time_limit.as_millis()) as u64,
)) {
if has_event {
if let Ok(current_event) = event::read() {
match current_event {
Event::Key(evt) => return events::Event::Key(evt),
Event::Mouse(evt) => return events::Event::Mouse(evt),
Event::Resize(w, h) => return events::Event::Resize(w, h),
};
}
}
}
elapsed_time = self.instant.elapsed();
if self.time_limit <= elapsed_time {
break;
}
}
self.instant = std::time::Instant::now();
self.frame_count = self.frame_count.wrapping_add(1);
events::Event::Frame
}
pub fn check_resize(&mut self) {
if crossterm::terminal::size().unwrap() != (self.width as u16, self.height as u16) {
let size = crossterm::terminal::size().unwrap();
let new_width = size.0 as u32;
let new_height = size.1 as u32;
self.resize(new_width, new_height);
}
}
pub fn is_key_pressed(&self, key: KeyCode) -> bool {
self.is_key_pressed_with_modifier(key, KeyModifiers::NONE)
}
pub fn is_key_pressed_with_modifier(&self, key: KeyCode, modifier: KeyModifiers) -> bool {
self.keys_pressed.contains(&KeyEvent::new(key, modifier))
}
pub fn is_key_held(&self, key: KeyCode) -> bool {
self.is_key_held_with_modifier(key, KeyModifiers::NONE)
}
pub fn is_key_held_with_modifier(&self, key: KeyCode, modifier: KeyModifiers) -> bool {
self.keys_held.contains(&KeyEvent::new(key, modifier))
}
pub fn is_key_released(&self, key: KeyCode) -> bool {
self.is_key_released_with_modifier(key, KeyModifiers::NONE)
}
pub fn is_key_released_with_modifier(&self, key: KeyCode, modifier: KeyModifiers) -> bool {
self.keys_released.contains(&KeyEvent::new(key, modifier))
}
pub fn get_mouse_press(&self, button: MouseButton) -> Option<(u32, u32)> {
self.get_mouse_press_with_modifier(button, KeyModifiers::NONE)
}
pub fn get_mouse_press_with_modifier(
&self,
button: MouseButton,
modifier: KeyModifiers,
) -> Option<(u32, u32)> {
for evt in self.mouse_events.iter() {
if let MouseEventKind::Down(mouse) = evt.kind {
if mouse == button && evt.modifiers == modifier {
return Some((evt.column as u32, evt.row as u32));
}
};
}
None
}
pub fn get_resize(&self) -> Option<(u16, u16)> {
for evt in self.resize_events.iter() {
if let Event::Resize(w, h) = Event::Resize(evt.0, evt.1) {
return Some((w, h));
};
}
None
}
pub fn get_mouse_held(&self, button: MouseButton) -> Option<(u32, u32)> {
self.get_mouse_held_with_modifier(button, KeyModifiers::NONE)
}
pub fn get_mouse_held_with_modifier(
&self,
button: MouseButton,
modifier: KeyModifiers,
) -> Option<(u32, u32)> {
for evt in self.mouse_events.iter() {
if let MouseEventKind::Drag(mouse) = evt.kind {
if mouse == button && evt.modifiers == modifier {
return Some((evt.column as u32, evt.row as u32));
}
};
}
None
}
pub fn get_mouse_released(&self, button: MouseButton) -> Option<(u32, u32)> {
self.get_mouse_released_with_modifier(button, KeyModifiers::NONE)
}
pub fn get_mouse_released_with_modifier(
&self,
button: MouseButton,
modifier: KeyModifiers,
) -> Option<(u32, u32)> {
for evt in self.mouse_events.iter() {
if let MouseEventKind::Up(mouse) = evt.kind {
if mouse == button && evt.modifiers == modifier {
return Some((evt.column as u32, evt.row as u32));
}
};
}
None
}
pub fn is_mouse_scrolled_down(&self) -> bool {
self.is_mouse_scrolled_down_with_modifier(KeyModifiers::NONE)
}
pub fn is_mouse_scrolled_down_with_modifier(&self, modifier: KeyModifiers) -> bool {
for evt in self.mouse_events.iter() {
if let MouseEventKind::ScrollDown = evt.kind {
if evt.modifiers == modifier {
return true;
}
};
}
false
}
pub fn is_mouse_scrolled_up(&self) -> bool {
self.is_mouse_scrolled_up_with_modifier(KeyModifiers::NONE)
}
pub fn is_mouse_scrolled_up_with_modifier(&self, modifier: KeyModifiers) -> bool {
for evt in self.mouse_events.iter() {
if let MouseEventKind::ScrollUp = evt.kind {
if evt.modifiers == modifier {
return true;
}
};
}
false
}
}
impl Drop for ConsoleEngine {
fn drop(&mut self) {
self.end();
}
}