use crate::prelude::KeyCode as WinitKeyCode;
use crate::prelude::{
Atmosphere, ElementState, State as NightshadeState, Vec2, Vec4, spawn_camera,
};
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 MIN_FONT_SIZE: f32 = 12.0;
const MAX_FONT_SIZE: f32 = 32.0;
const TARGET_COLUMNS: f32 = 80.0;
const MONOSPACE_WIDTH_FACTOR: f32 = 0.6;
pub struct NightshadeAdapter {
tui_state: Box<dyn TuiState>,
tui_world: TuiWorld,
row_slots: Vec<usize>,
ui_root: Option<freecs::Entity>,
grid_columns: u16,
grid_rows: u16,
font_size: f32,
dpi_scale: 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_slots: Vec::new(),
ui_root: None,
grid_columns: 0,
grid_rows: 0,
font_size: MIN_FONT_SIZE,
dpi_scale: 1.0,
cell_grid: CellGrid::new(0, 0),
initialized_grid: false,
prev_mouse_column: 0,
prev_mouse_row: 0,
}
}
fn compute_font_size(&self, logical_width: f32) -> f32 {
let ideal = logical_width / (TARGET_COLUMNS * MONOSPACE_WIDTH_FACTOR);
ideal.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE)
}
fn calculate_grid_dimensions(&self, window_width: u32, window_height: u32) -> (u16, u16) {
let logical_width = window_width as f32 / self.dpi_scale;
let logical_height = window_height as f32 / self.dpi_scale;
let cell_width = self.font_size * MONOSPACE_WIDTH_FACTOR;
let cell_height = self.font_size * 1.2;
let columns = (logical_width / cell_width).floor() as u16;
let rows = (logical_height / 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 logical_x = pixel_x / self.dpi_scale;
let logical_y = pixel_y / self.dpi_scale;
let cell_width = self.font_size * MONOSPACE_WIDTH_FACTOR;
let cell_height = self.font_size * 1.2;
let column = (logical_x / cell_width).floor() as i32;
let row = (logical_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 build_row_ui(&mut self, nightshade_world: &mut crate::ecs::world::World) {
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::layout_types::FlowDirection;
use crate::ecs::ui::types::Anchor;
use crate::ecs::ui::units::Ab;
if let Some(root) = self.ui_root.take() {
for slot in &self.row_slots {
nightshade_world
.resources
.retained_ui
.text_slot_character_colors
.remove(slot);
nightshade_world
.resources
.retained_ui
.text_slot_character_background_colors
.remove(slot);
}
nightshade_world.ui_despawn_node(root);
}
self.row_slots.clear();
let monospace_width = self.font_size * MONOSPACE_WIDTH_FACTOR;
let line_height = self.font_size * 1.2;
let mut tree = UiTreeBuilder::new(nightshade_world);
let container = tree
.add_node()
.window(
Ab(Vec2::zeros()),
Ab(Vec2::new(10000.0, 10000.0)),
Anchor::TopLeft,
)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.without_pointer_events()
.entity();
tree.push_parent(container);
for _row_index in 0..self.grid_rows {
let row_string: String = " ".repeat(self.grid_columns as usize);
let text_slot = tree.world_mut().resources.text_cache.add_text(&row_string);
tree.add_node()
.flow_child(Ab(Vec2::new(
monospace_width * self.grid_columns as f32,
line_height,
)))
.with_text_slot(text_slot, self.font_size)
.with_font_index(crate::render::wgpu::font_atlas::BITMAP_MONO_RANGE.start)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Top)
.with_monospace_width(monospace_width)
.with_color::<crate::ecs::ui::state::UiBase>(Vec4::new(1.0, 1.0, 1.0, 1.0))
.without_pointer_events()
.done();
self.row_slots.push(text_slot);
}
tree.pop_parent();
self.ui_root = Some(tree.finish());
}
fn update_hud_rows(&mut self, nightshade_world: &mut crate::ecs::world::World) {
for row_index in 0..self.grid_rows {
let slot = self.row_slots[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)));
}
nightshade_world
.resources
.text_cache
.set_text(slot, row_string);
nightshade_world
.resources
.retained_ui
.text_slot_character_colors
.insert(slot, row_colors);
nightshade_world
.resources
.retained_ui
.text_slot_character_background_colors
.insert(slot, row_bg_colors);
}
}
}
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.retained_ui.enabled = true;
world.resources.retained_ui.background_color = Some(Vec4::new(0.0, 0.0, 0.0, 1.0));
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,
nalgebra_glm::Vec3::new(0.0, 0.0, 10.0),
"TUI Camera".to_string(),
);
world.resources.active_camera = Some(camera);
self.dpi_scale = world.resources.window.cached_scale_factor.max(1.0);
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)
};
self.font_size = self.compute_font_size(window_width as f32 / self.dpi_scale);
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.build_row_ui(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;
}
self.dpi_scale = world.resources.window.cached_scale_factor.max(1.0);
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))
};
self.font_size = self.compute_font_size(window_width as f32 / self.dpi_scale);
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.build_row_ui(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);
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();
}
}