use crate::tui::ecs::events::Message;
use crate::tui::ecs::resources::MouseButton;
use crate::tui::ecs::world::{self, LABEL, POSITION, SPRITE, TILEMAP, World};
use crate::tui::key_code::KeyCode;
use crate::tui::run::State;
use crate::tui::terminal::input;
use crate::tui::terminal::render::{Cell, Renderer};
use crossterm::{cursor, execute, terminal};
use std::io::{self, Stdout};
use std::time::{Duration, Instant};
struct TerminalGuard {
stdout: Stdout,
}
impl TerminalGuard {
fn new() -> io::Result<Self> {
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
execute!(
stdout,
terminal::EnterAlternateScreen,
cursor::Hide,
terminal::Clear(terminal::ClearType::All),
crossterm::event::EnableMouseCapture
)?;
Ok(Self { stdout })
}
fn stdout_mut(&mut self) -> &mut Stdout {
&mut self.stdout
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = execute!(
self.stdout,
crossterm::event::DisableMouseCapture,
cursor::Show,
terminal::LeaveAlternateScreen
);
let _ = terminal::disable_raw_mode();
}
}
pub fn launch(mut state: Box<dyn State>) -> Result<(), Box<dyn std::error::Error>> {
let mut world = World::default();
let (columns, rows) = terminal::size()?;
world.resources.terminal_size.columns = columns;
world.resources.terminal_size.rows = rows;
let mut renderer = Renderer::new(columns, rows);
let mut guard = TerminalGuard::new()?;
let title = state.title().to_string();
if !title.is_empty() {
execute!(guard.stdout_mut(), terminal::SetTitle(&title))?;
}
state.initialize(&mut world);
let mut last_frame_time = Instant::now();
let mut prev_mouse_column: u16 = 0;
let mut prev_mouse_row: u16 = 0;
loop {
let now = Instant::now();
let delta = now.duration_since(last_frame_time);
last_frame_time = now;
world.resources.timing.delta_seconds = delta.as_secs_f64();
world.resources.timing.elapsed += delta.as_secs_f64();
world.resources.timing.frame_count += 1;
input::poll_input(&mut world);
let just_pressed: Vec<KeyCode> = world
.resources
.keyboard
.just_pressed
.iter()
.copied()
.collect();
let just_released: Vec<KeyCode> = world
.resources
.keyboard
.just_released
.iter()
.copied()
.collect();
for key in &just_pressed {
state.on_keyboard_input(&mut world, *key, true);
}
for key in &just_released {
state.on_keyboard_input(&mut world, *key, false);
}
if world.resources.mouse.left_just_pressed {
let column = world.resources.mouse.column;
let row = world.resources.mouse.row;
state.on_mouse_input(&mut world, MouseButton::Left, column, row, true);
}
if world.resources.mouse.right_just_pressed {
let column = world.resources.mouse.column;
let row = world.resources.mouse.row;
state.on_mouse_input(&mut world, MouseButton::Right, column, row, true);
}
if world.resources.mouse.middle_just_pressed {
let column = world.resources.mouse.column;
let row = world.resources.mouse.row;
state.on_mouse_input(&mut world, MouseButton::Middle, column, row, true);
}
if world.resources.mouse.left_just_released {
let column = world.resources.mouse.column;
let row = world.resources.mouse.row;
state.on_mouse_input(&mut world, MouseButton::Left, column, row, false);
}
if world.resources.mouse.right_just_released {
let column = world.resources.mouse.column;
let row = world.resources.mouse.row;
state.on_mouse_input(&mut world, MouseButton::Right, column, row, false);
}
if world.resources.mouse.middle_just_released {
let column = world.resources.mouse.column;
let row = world.resources.mouse.row;
state.on_mouse_input(&mut world, MouseButton::Middle, column, row, false);
}
let current_mouse_column = world.resources.mouse.column;
let current_mouse_row = world.resources.mouse.row;
if current_mouse_column != prev_mouse_column || current_mouse_row != prev_mouse_row {
prev_mouse_column = current_mouse_column;
prev_mouse_row = current_mouse_row;
state.on_mouse_move(&mut world, current_mouse_column, current_mouse_row);
}
state.run_systems(&mut world);
let messages: Vec<Message> = world.resources.event_bus.messages.drain(..).collect();
for message in &messages {
state.handle_event(&mut world, message);
}
world::apply_commands(&mut world);
if let Some(next) = state.next_state(&mut world) {
state = next;
state.initialize(&mut world);
continue;
}
let current_columns = world.resources.terminal_size.columns;
let current_rows = world.resources.terminal_size.rows;
if renderer.width != current_columns || renderer.height != current_rows {
renderer.resize(current_columns, current_rows);
}
render_frame(&world, &mut renderer);
renderer.present(guard.stdout_mut())?;
if world.resources.should_exit {
break;
}
let target_fps = world.resources.timing.target_fps;
if target_fps > 0 {
let frame_duration = Duration::from_secs_f64(1.0 / target_fps as f64);
let elapsed_this_frame = Instant::now().duration_since(now);
if elapsed_this_frame < frame_duration {
std::thread::sleep(frame_duration - elapsed_this_frame);
}
}
}
Ok(())
}
enum DrawableKind {
Sprite,
Label,
Tilemap,
}
fn render_frame(world: &World, renderer: &mut Renderer) {
renderer.clear();
let camera = world.resources.camera;
let mut drawables: Vec<(i32, freecs::Entity, DrawableKind)> = Vec::new();
for entity in world.query_entities(POSITION | SPRITE) {
let has_label = world.get_label(entity).is_some();
let has_tilemap = world.get_tilemap(entity).is_some();
if has_label || has_tilemap {
continue;
}
let visible = world
.get_visibility(entity)
.is_none_or(|visibility| visibility.visible);
if !visible {
continue;
}
let z = world.get_z_index(entity).map_or(0, |z_index| z_index.0);
drawables.push((z, entity, DrawableKind::Sprite));
}
for entity in world.query_entities(POSITION | LABEL) {
let visible = world
.get_visibility(entity)
.is_none_or(|visibility| visibility.visible);
if !visible {
continue;
}
let z = world.get_z_index(entity).map_or(0, |z_index| z_index.0);
drawables.push((z, entity, DrawableKind::Label));
}
for entity in world.query_entities(POSITION | TILEMAP) {
let visible = world
.get_visibility(entity)
.is_none_or(|visibility| visibility.visible);
if !visible {
continue;
}
let z = world.get_z_index(entity).map_or(0, |z_index| z_index.0);
drawables.push((z, entity, DrawableKind::Tilemap));
}
drawables.sort_by_key(|(z, _, _)| *z);
for (_, entity, kind) in &drawables {
let Some(position) = world.get_position(*entity) else {
continue;
};
let screen_column = (position.column - camera.offset_column).round() as i32;
let screen_row = (position.row - camera.offset_row).round() as i32;
match kind {
DrawableKind::Sprite => {
let Some(sprite) = world.get_sprite(*entity) else {
continue;
};
if screen_column >= 0
&& screen_column < renderer.width as i32
&& screen_row >= 0
&& screen_row < renderer.height as i32
{
renderer.set_cell(
screen_column as u16,
screen_row as u16,
Cell {
character: sprite.character,
foreground: sprite.foreground.to_crossterm_color(),
background: sprite.background.to_crossterm_color(),
},
);
}
}
DrawableKind::Label => {
let Some(label) = world.get_label(*entity) else {
continue;
};
let foreground = label.foreground.to_crossterm_color();
let background = label.background.to_crossterm_color();
for (char_index, character) in label.text.chars().enumerate() {
let col = screen_column + char_index as i32;
if col >= 0
&& col < renderer.width as i32
&& screen_row >= 0
&& screen_row < renderer.height as i32
{
renderer.set_cell(
col as u16,
screen_row as u16,
Cell {
character,
foreground,
background,
},
);
}
}
}
DrawableKind::Tilemap => {
let Some(tilemap) = world.get_tilemap(*entity) else {
continue;
};
for tile_row in 0..tilemap.height {
for tile_column in 0..tilemap.width {
let cell = &tilemap.cells[tile_row * tilemap.width + tile_column];
if cell.character == '\0' {
continue;
}
let col = screen_column + tile_column as i32;
let row = screen_row + tile_row as i32;
if col >= 0
&& col < renderer.width as i32
&& row >= 0
&& row < renderer.height as i32
{
renderer.set_cell(
col as u16,
row as u16,
Cell {
character: cell.character,
foreground: cell.foreground.to_crossterm_color(),
background: cell.background.to_crossterm_color(),
},
);
}
}
}
}
}
}
}