use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use bevy::prelude::*; use bevy_terminal_display::{ crossterm::event::{Event, KeyCode, KeyEventKind}, input::events::TerminalInputEvent, ratatui::{ layout::Rect, style::{Color, Modifier, Style}, widgets::{Block, Borders, Clear, HighlightSpacing, List, ListItem, ListState} }, widgets::TerminalWidget, }; pub struct MenuWidget { pub items: Vec>, pub state: ListState, pub enabled_color: Color, pub disabled_color: Color, pub selected_style: Style, pub area_function: Option Rect + Send + Sync>>, } impl Default for MenuWidget { fn default() -> Self { Self { items: vec![], state: ListState::default().with_selected(Some(0)), enabled_color: Color::White, disabled_color: Color::Gray, selected_style: Style::new() .add_modifier(Modifier::UNDERLINED) .add_modifier(Modifier::BOLD), area_function: None, } } } pub struct MenuOption { pub enabled: Arc, pub label: String, pub event: E, } impl TerminalWidget for MenuWidget { fn render( &mut self, frame: &mut bevy_terminal_display::ratatui::Frame, rect: bevy_terminal_display::ratatui::prelude::Rect, ) { let block = Block::new().borders(Borders::all()); let items: Vec = self .items .iter() .map(|item| { ListItem::from(item).style(Style::new().fg( if item.enabled.load(Ordering::Relaxed) { self.enabled_color } else { self.disabled_color }, )) }) .collect(); let list = List::new(items) .block(block) .highlight_style(self.selected_style) .highlight_symbol(">") .highlight_spacing(HighlightSpacing::Always); let area = if let Some(area) = &self.area_function { area(rect) } else { rect }; frame.render_widget(Clear, area); frame.render_stateful_widget(list, area, &mut self.state); } fn handle_events( &mut self, event: &bevy_terminal_display::input::events::TerminalInputEvent, commands: &mut Commands, ) { if let TerminalInputEvent(Event::Key(key_event)) = event { if key_event.kind == KeyEventKind::Press { match key_event.code { KeyCode::Enter => { if let Some(selected) = self.state.selected() { if let Some(item) = self.items.get(selected) { if item.enabled.load(Ordering::Relaxed) { commands.send_event(item.event.clone()); } } } }, KeyCode::Up => self.state.select_previous(), KeyCode::Down => self.state.select_next(), _ => (), } } } } } impl FromIterator<(&'static str, E)> for MenuWidget { fn from_iter>(iter: I) -> Self { let items = iter .into_iter() .map(|(label, event)| MenuOption { enabled: Arc::new(true.into()), label: label.into(), event, }) .collect(); Self { items, ..Default::default() } } } impl From<&MenuOption> for ListItem<'_> { fn from(value: &MenuOption) -> Self { ListItem::new(value.label.clone()) } }