2025-03-19 07:37:02 -04:00

130 lines
4.0 KiB
Rust

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<E: bevy::prelude::Event> {
pub items: Vec<MenuOption<E>>,
pub state: ListState,
pub enabled_color: Color,
pub disabled_color: Color,
pub selected_style: Style,
pub area_function: Option<Box<dyn Fn(Rect) -> Rect + Send + Sync>>,
}
impl<E: bevy::prelude::Event> Default for MenuWidget<E> {
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<E: bevy::prelude::Event> {
pub enabled: Arc<AtomicBool>,
pub label: String,
pub event: E,
}
impl<E: bevy::prelude::Event + Clone> TerminalWidget for MenuWidget<E> {
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<ListItem> = 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<E: bevy::prelude::Event> FromIterator<(&'static str, E)> for MenuWidget<E> {
fn from_iter<I: IntoIterator<Item = (&'static str, E)>>(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<E: bevy::prelude::Event> From<&MenuOption<E>> for ListItem<'_> {
fn from(value: &MenuOption<E>) -> Self {
ListItem::new(value.label.clone())
}
}