diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..62d8a86 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[profile.dev.package."*"] +opt-level = 2 diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..06dad8b --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,25 @@ +name: Build +on: [push] + +jobs: + Build: + env: + RUNNER_TOOL_CACHE: /toolcache + container: rust:alpine + steps: + - name: Install node + run: apk add nodejs gcc libc-dev pkgconf libx11-dev alsa-lib-dev eudev-dev tar + - name: Check out repository code + uses: actions/checkout@v4 + - name: Restore cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Build + run: cargo build --release diff --git a/.gitignore b/.gitignore index ea8c4bf..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index a082732..c0d16e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,4 @@ version = "0.15" default-features = false [dependencies.bevy_terminal_display] -git = "https://git.soaos.dev/bevy_terminal_display" +git = "https://git.soaos.dev/soaos/bevy_terminal_display" diff --git a/src/lib.rs b/src/lib.rs index e7a11a9..6547fe6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,129 @@ -fn main() { - println!("Hello, world!"); +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()) + } }