nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::tui::ecs::components::{Label, Position, Sprite, TermColor, ZIndex};
use crate::tui::ecs::world::*;
use crate::tui::group::EntityGroup;

pub fn word_wrap(text: &str, max_width: usize) -> Vec<String> {
    if max_width == 0 {
        return Vec::new();
    }

    let mut lines = Vec::new();
    for paragraph in text.split('\n') {
        if paragraph.is_empty() {
            lines.push(String::new());
            continue;
        }

        let words: Vec<&str> = paragraph.split_whitespace().collect();
        if words.is_empty() {
            lines.push(String::new());
            continue;
        }

        let mut current_line = String::new();
        for word in words {
            if current_line.is_empty() {
                if word.len() > max_width {
                    let mut remaining = word;
                    while remaining.len() > max_width {
                        lines.push(remaining[..max_width].to_string());
                        remaining = &remaining[max_width..];
                    }
                    current_line = remaining.to_string();
                } else {
                    current_line = word.to_string();
                }
            } else if current_line.len() + 1 + word.len() <= max_width {
                current_line.push(' ');
                current_line.push_str(word);
            } else {
                lines.push(current_line);
                if word.len() > max_width {
                    let mut remaining = word;
                    while remaining.len() > max_width {
                        lines.push(remaining[..max_width].to_string());
                        remaining = &remaining[max_width..];
                    }
                    current_line = remaining.to_string();
                } else {
                    current_line = word.to_string();
                }
            }
        }
        if !current_line.is_empty() {
            lines.push(current_line);
        }
    }

    lines
}

pub struct MenuColors {
    pub normal_foreground: TermColor,
    pub normal_background: TermColor,
    pub selected_foreground: TermColor,
    pub selected_background: TermColor,
}

impl Default for MenuColors {
    fn default() -> Self {
        Self {
            normal_foreground: TermColor::White,
            normal_background: TermColor::Black,
            selected_foreground: TermColor::Black,
            selected_background: TermColor::White,
        }
    }
}

pub struct Menu {
    items: Vec<String>,
    selected_index: usize,
    position_column: f64,
    position_row: f64,
    colors: MenuColors,
    z_index: i32,
    entities: EntityGroup,
}

impl Menu {
    pub fn new(
        items: Vec<String>,
        position_column: f64,
        position_row: f64,
        colors: MenuColors,
        z_index: i32,
    ) -> Self {
        Self {
            items,
            selected_index: 0,
            position_column,
            position_row,
            colors,
            z_index,
            entities: EntityGroup::new(),
        }
    }

    pub fn up(&mut self) {
        if self.selected_index > 0 {
            self.selected_index -= 1;
        }
    }

    pub fn down(&mut self) {
        if self.selected_index + 1 < self.items.len() {
            self.selected_index += 1;
        }
    }

    pub fn select_at(&mut self, index: usize) {
        if index < self.items.len() {
            self.selected_index = index;
        }
    }

    pub fn selected_index(&self) -> usize {
        self.selected_index
    }

    pub fn selected(&self) -> &str {
        &self.items[self.selected_index]
    }

    pub fn render(&mut self, world: &mut World) {
        self.entities.despawn_all(world);

        for (item_index, item) in self.items.iter().enumerate() {
            let is_selected = item_index == self.selected_index;
            let foreground = if is_selected {
                self.colors.selected_foreground
            } else {
                self.colors.normal_foreground
            };
            let background = if is_selected {
                self.colors.selected_background
            } else {
                self.colors.normal_background
            };

            let display = if is_selected {
                format!("> {}", item)
            } else {
                format!("  {}", item)
            };

            let entity = self.entities.spawn_one(world, POSITION | LABEL | Z_INDEX);
            world.set_position(
                entity,
                Position {
                    column: self.position_column,
                    row: self.position_row + item_index as f64,
                },
            );
            world.set_label(
                entity,
                Label {
                    text: display,
                    foreground,
                    background,
                },
            );
            world.set_z_index(entity, ZIndex(self.z_index));
        }
    }

    pub fn despawn(&mut self, world: &mut World) {
        self.entities.despawn_all(world);
    }
}

pub struct ProgressBarColors {
    pub filled_foreground: TermColor,
    pub filled_background: TermColor,
    pub empty_foreground: TermColor,
    pub empty_background: TermColor,
}

pub struct ProgressBar {
    width: usize,
    position_column: f64,
    position_row: f64,
    colors: ProgressBarColors,
    z_index: i32,
    entities: EntityGroup,
}

impl ProgressBar {
    pub fn new(
        width: usize,
        position_column: f64,
        position_row: f64,
        colors: ProgressBarColors,
        z_index: i32,
    ) -> Self {
        Self {
            width,
            position_column,
            position_row,
            colors,
            z_index,
            entities: EntityGroup::new(),
        }
    }

    pub fn render(&mut self, world: &mut World, fraction: f64) {
        self.entities.despawn_all(world);

        let clamped = fraction.clamp(0.0, 1.0);
        let filled_count = (clamped * self.width as f64).round() as usize;

        for cell_index in 0..self.width {
            let is_filled = cell_index < filled_count;
            let character = if is_filled { '' } else { '' };
            let foreground = if is_filled {
                self.colors.filled_foreground
            } else {
                self.colors.empty_foreground
            };
            let background = if is_filled {
                self.colors.filled_background
            } else {
                self.colors.empty_background
            };

            let entity = self.entities.spawn_one(world, POSITION | SPRITE | Z_INDEX);
            world.set_position(
                entity,
                Position {
                    column: self.position_column + cell_index as f64,
                    row: self.position_row,
                },
            );
            world.set_sprite(
                entity,
                Sprite {
                    character,
                    foreground,
                    background,
                },
            );
            world.set_z_index(entity, ZIndex(self.z_index));
        }
    }

    pub fn despawn(&mut self, world: &mut World) {
        self.entities.despawn_all(world);
    }
}