nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
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();
    }
}