130 lines
4.0 KiB
Rust
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())
|
|
}
|
|
}
|