use crate::prelude::KeyCode as WinitKeyCode;
use crate::prelude::{
Atmosphere, ElementState, Entity, HudAnchor, State as NightshadeState, TextProperties, Vec2,
Vec3, Vec4, spawn_camera, spawn_hud_text_with_properties,
};
use crate::tui::backend::grid_renderer::{CellGrid, build_grid_from_tui_world};
use crate::tui::backend::key_map;
use crate::tui::ecs::events::Message as TuiMessage;
use crate::tui::ecs::resources::MouseButton as TuiMouseButton;
use crate::tui::ecs::world::{self as tui_world, World as TuiWorld};
use crate::tui::run::State as TuiState;
const DEFAULT_FONT_SIZE: f32 = 16.0;
const MONOSPACE_WIDTH_FACTOR: f32 = 0.6;
pub struct NightshadeAdapter {
tui_state: Box<dyn TuiState>,
tui_world: TuiWorld,
row_entities: Vec<Entity>,
grid_columns: u16,
grid_rows: u16,
font_size: f32,
cell_grid: CellGrid,
initialized_grid: bool,
prev_mouse_column: u16,
prev_mouse_row: u16,
}
impl NightshadeAdapter {
pub fn new(state: Box<dyn TuiState>) -> Self {
Self {
tui_state: state,
tui_world: TuiWorld::default(),
row_entities: Vec::new(),
grid_columns: 0,
grid_rows: 0,
font_size: DEFAULT_FONT_SIZE,
cell_grid: CellGrid::new(0, 0),
initialized_grid: false,
prev_mouse_column: 0,
prev_mouse_row: 0,
}
}
fn calculate_grid_dimensions(&self, window_width: u32, window_height: u32) -> (u16, u16) {
let cell_width = self.font_size * MONOSPACE_WIDTH_FACTOR;
let cell_height = self.font_size * 1.2;
let columns = (window_width as f32 / cell_width).floor() as u16;
let rows = (window_height as f32 / cell_height).floor() as u16;
(columns.max(1), rows.max(1))
}
fn pixel_to_grid(&self, pixel_x: f32, pixel_y: f32) -> (u16, u16) {
let cell_width = self.font_size * MONOSPACE_WIDTH_FACTOR;
let cell_height = self.font_size * 1.2;
let column = (pixel_x / cell_width).floor() as i32;
let row = (pixel_y / cell_height).floor() as i32;
let column = column.clamp(0, self.grid_columns.saturating_sub(1) as i32) as u16;
let row = row.clamp(0, self.grid_rows.saturating_sub(1) as i32) as u16;
(column, row)
}
fn spawn_row_entities(&mut self, nightshade_world: &mut crate::ecs::world::World) {
if !self.row_entities.is_empty() {
nightshade_world.despawn_entities(&self.row_entities);
self.row_entities.clear();
}
let monospace_width = self.font_size * MONOSPACE_WIDTH_FACTOR;
let line_height = 1.2;
for row_index in 0..self.grid_rows {
let row_string: String = " ".repeat(self.grid_columns as usize);
let y_position = row_index as f32 * self.font_size * line_height;
let entity = spawn_hud_text_with_properties(
nightshade_world,
row_string,
HudAnchor::TopLeft,
Vec2::new(0.0, y_position),
TextProperties {
font_size: self.font_size,
color: Vec4::new(1.0, 1.0, 1.0, 1.0),
monospace_width: Some(monospace_width),
line_height,
..Default::default()
},
);
nightshade_world.queue_add_components(
entity,
crate::ecs::world::TEXT_CHARACTER_COLORS
| crate::ecs::world::TEXT_CHARACTER_BACKGROUND_COLORS,
);
nightshade_world.apply_commands();
if let Some(char_colors) = nightshade_world.get_text_character_colors_mut(entity) {
char_colors.colors = vec![None; self.grid_columns as usize];
char_colors.dirty = true;
}
if let Some(bg_colors) =
nightshade_world.get_text_character_background_colors_mut(entity)
{
bg_colors.colors = vec![None; self.grid_columns as usize];
bg_colors.dirty = true;
}
self.row_entities.push(entity);
}
}
fn update_hud_rows(&mut self, nightshade_world: &mut crate::ecs::world::World) {
for row_index in 0..self.grid_rows {
let entity = self.row_entities[row_index as usize];
let mut row_string = String::with_capacity(self.grid_columns as usize);
let mut row_colors: Vec<Option<Vec4>> = Vec::with_capacity(self.grid_columns as usize);
let mut row_bg_colors: Vec<Option<Vec4>> =
Vec::with_capacity(self.grid_columns as usize);
for column_index in 0..self.grid_columns {
let cell = self.cell_grid.get(column_index, row_index);
row_string.push(cell.character);
let [r, g, b, a] = cell.foreground.to_vec4();
row_colors.push(Some(Vec4::new(r, g, b, a)));
let [br, bg, bb, ba] = cell.background.to_vec4();
row_bg_colors.push(Some(Vec4::new(br, bg, bb, ba)));
}
if let Some(hud_text) = nightshade_world.get_hud_text(entity) {
let text_index = hud_text.text_index;
nightshade_world
.resources
.text_cache
.set_text(text_index, row_string);
}
if let Some(hud_text) = nightshade_world.get_hud_text_mut(entity) {
hud_text.dirty = true;
}
if let Some(char_colors) = nightshade_world.get_text_character_colors_mut(entity) {
char_colors.colors = row_colors;
char_colors.dirty = true;
}
if let Some(bg_colors) =
nightshade_world.get_text_character_background_colors_mut(entity)
{
bg_colors.colors = row_bg_colors;
bg_colors.dirty = true;
}
}
}
}
impl NightshadeState for NightshadeAdapter {
fn title(&self) -> &str {
self.tui_state.title()
}
fn initialize(&mut self, world: &mut crate::ecs::world::World) {
world.resources.user_interface.enabled = false;
world.resources.graphics.atmosphere = Atmosphere::None;
world.resources.graphics.clear_color = [0.0, 0.0, 0.0, 1.0];
world.resources.graphics.show_grid = false;
let camera = spawn_camera(world, Vec3::new(0.0, 0.0, 10.0), "TUI Camera".to_string());
world.resources.active_camera = Some(camera);
let (window_width, window_height) = if let Some(handle) = &world.resources.window.handle {
let size = handle.inner_size();
(size.width, size.height)
} else {
(800, 600)
};
let (columns, rows) = self.calculate_grid_dimensions(window_width, window_height);
self.grid_columns = columns;
self.grid_rows = rows;
self.cell_grid = CellGrid::new(columns, rows);
self.spawn_row_entities(world);
self.tui_world.resources.terminal_size.columns = columns;
self.tui_world.resources.terminal_size.rows = rows;
self.tui_state.initialize(&mut self.tui_world);
self.initialized_grid = true;
}
fn on_keyboard_input(
&mut self,
world: &mut crate::ecs::world::World,
winit_key: WinitKeyCode,
key_state: ElementState,
) {
let shift = world
.resources
.input
.keyboard
.is_key_pressed(WinitKeyCode::ShiftLeft)
|| world
.resources
.input
.keyboard
.is_key_pressed(WinitKeyCode::ShiftRight);
let Some(tui_key) = key_map::from_winit(winit_key, shift) else {
return;
};
let pressed = key_state == ElementState::Pressed;
if pressed {
self.tui_world.resources.keyboard.pressed.insert(tui_key);
self.tui_world
.resources
.keyboard
.just_pressed
.insert(tui_key);
} else {
self.tui_world.resources.keyboard.pressed.remove(&tui_key);
self.tui_world
.resources
.keyboard
.just_released
.insert(tui_key);
}
self.tui_state
.on_keyboard_input(&mut self.tui_world, tui_key, pressed);
}
fn on_mouse_input(
&mut self,
world: &mut crate::ecs::world::World,
state: winit::event::ElementState,
button: winit::event::MouseButton,
) {
let tui_button = match button {
winit::event::MouseButton::Left => TuiMouseButton::Left,
winit::event::MouseButton::Right => TuiMouseButton::Right,
winit::event::MouseButton::Middle => TuiMouseButton::Middle,
_ => return,
};
let pressed = state == ElementState::Pressed;
let pixel_position = world.resources.input.mouse.position;
let (column, row) = self.pixel_to_grid(pixel_position.x, pixel_position.y);
self.tui_world.resources.mouse.column = column;
self.tui_world.resources.mouse.row = row;
match tui_button {
TuiMouseButton::Left => {
self.tui_world.resources.mouse.left_pressed = pressed;
if pressed {
self.tui_world.resources.mouse.left_just_pressed = true;
} else {
self.tui_world.resources.mouse.left_just_released = true;
}
}
TuiMouseButton::Right => {
self.tui_world.resources.mouse.right_pressed = pressed;
if pressed {
self.tui_world.resources.mouse.right_just_pressed = true;
} else {
self.tui_world.resources.mouse.right_just_released = true;
}
}
TuiMouseButton::Middle => {
self.tui_world.resources.mouse.middle_pressed = pressed;
if pressed {
self.tui_world.resources.mouse.middle_just_pressed = true;
} else {
self.tui_world.resources.mouse.middle_just_released = true;
}
}
}
self.tui_state
.on_mouse_input(&mut self.tui_world, tui_button, column, row, pressed);
}
fn run_systems(&mut self, world: &mut crate::ecs::world::World) {
if !self.initialized_grid {
return;
}
let (window_width, window_height) = if let Some(handle) = &world.resources.window.handle {
let size = handle.inner_size();
(size.width, size.height)
} else {
world
.resources
.window
.cached_viewport_size
.unwrap_or((800, 600))
};
let (new_columns, new_rows) = self.calculate_grid_dimensions(window_width, window_height);
if new_columns != self.grid_columns || new_rows != self.grid_rows {
self.grid_columns = new_columns;
self.grid_rows = new_rows;
self.cell_grid = CellGrid::new(new_columns, new_rows);
self.spawn_row_entities(world);
self.tui_world.resources.terminal_size.columns = new_columns;
self.tui_world.resources.terminal_size.rows = new_rows;
}
self.tui_world.resources.timing.delta_seconds =
world.resources.window.timing.delta_time as f64;
self.tui_world.resources.timing.elapsed += self.tui_world.resources.timing.delta_seconds;
self.tui_world.resources.timing.frame_count += 1;
let pixel_position = world.resources.input.mouse.position;
let (current_column, current_row) = self.pixel_to_grid(pixel_position.x, pixel_position.y);
self.tui_world.resources.mouse.column = current_column;
self.tui_world.resources.mouse.row = current_row;
if current_column != self.prev_mouse_column || current_row != self.prev_mouse_row {
self.prev_mouse_column = current_column;
self.prev_mouse_row = current_row;
self.tui_state
.on_mouse_move(&mut self.tui_world, current_column, current_row);
}
self.tui_state.run_systems(&mut self.tui_world);
let messages: Vec<TuiMessage> = self
.tui_world
.resources
.event_bus
.messages
.drain(..)
.collect();
for message in &messages {
self.tui_state.handle_event(&mut self.tui_world, message);
}
tui_world::apply_commands(&mut self.tui_world);
if let Some(next) = self.tui_state.next_state(&mut self.tui_world) {
self.tui_state = next;
self.tui_state.initialize(&mut self.tui_world);
}
build_grid_from_tui_world(&self.tui_world, &mut self.cell_grid);
self.update_hud_rows(world);
crate::ecs::text::systems::sync_hud_text_system(world);
if self.tui_world.resources.should_exit {
world.resources.window.should_exit = true;
}
self.tui_world.resources.keyboard.clear_frame();
self.tui_world.resources.mouse.clear_frame();
}
}