use std::time::{Duration, Instant}; use color_eyre::{owo_colors::OwoColorize, Result}; use gilrs::{Gamepad, Gilrs}; use ratatui::{ buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Alignment, Constraint, Flex, Layout, Rect}, style::{palette::tailwind, Color, Style, Stylize}, symbols::Marker, widgets::{ canvas::{Canvas, Circle, Shape}, Block, BorderType, Gauge, Padding, Paragraph, Widget, }, DefaultTerminal, }; use Constraint::{Fill, Length, Min, Percentage}; use crate::gamepad_manager::GamepadManager; pub struct App { pub manager: GamepadManager, state: AppState, tick: u64, } #[derive(Default, PartialEq, Eq)] pub enum AppState { #[default] Running, Quitting, } impl App { pub fn new(manager: GamepadManager) -> Self { App { manager, state: AppState::Running, tick: 0, } } pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { let tick_rate = Duration::from_millis(20); let mut last_tick = Instant::now(); while self.state == AppState::Running { terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; let timeout = tick_rate.saturating_sub(last_tick.elapsed()); if event::poll(timeout)? { self.handle_events()?; } self.handle_gamepad_inputs()?; if last_tick.elapsed() >= tick_rate { self.update(); last_tick = Instant::now(); } } Ok(()) } fn handle_gamepad_inputs(&mut self) -> Result<()> { while let Some(gilrs::Event { id, .. }) = self.manager.gilrs.next_event() { self.manager.set_active_gamepad(id); } Ok(()) } fn handle_events(&mut self) -> Result<()> { if let ratatui::crossterm::event::Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('r') => { self.manager.scan_gamepads(); return Ok(()); } KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => self.quit(), _ => {} } } } Ok(()) } fn update(&mut self) { self.tick = self.tick.wrapping_add(1); } fn quit(&mut self) { self.state = AppState::Quitting; } fn render_title(area: Rect, buf: &mut Buffer) { Paragraph::new("App Example Title") .block( Block::bordered() .border_type(BorderType::Rounded) .padding(Padding::top(1)), ) .alignment(Alignment::Center) .render(area, buf); } fn render_connected_gamepad(&self, area: Rect, buf: &mut Buffer) { Paragraph::new(format!( "{} connected gamepads", self.manager.connected_gamepads() )) .block( Block::bordered() .border_type(BorderType::Rounded) .padding(Padding::top(1)), ) .alignment(Alignment::Center) .bold() .render(area, buf); } fn render_right_buttons(&self, area: Rect, buf: &mut Buffer) { let north_button = self.create_action_button_ui(gilrs::Button::North); let east_button = self.create_action_button_ui(gilrs::Button::East); let south_button = self.create_action_button_ui(gilrs::Button::South); let west_button = self.create_action_button_ui(gilrs::Button::West); let layers = Layout::vertical([Fill(1), Fill(1), Fill(1)]); let [top, mid, bot] = layers.areas(area); let top_layer = Layout::horizontal([Fill(1), Fill(1), Fill(1)]); let [_, north, _] = top_layer.areas(top); let mid_layer = Layout::horizontal([Fill(1), Fill(1), Fill(1)]); let [west, _, east] = mid_layer.areas(mid); let bot_layer = Layout::horizontal([Fill(1), Fill(1), Fill(1)]); let [_, south, _] = bot_layer.areas(bot); north_button.render(north, buf); east_button.render(east, buf); south_button.render(south, buf); west_button.render(west, buf); } fn create_action_button_ui(&self, dir: gilrs::Button) -> Block { let color = match self.manager.active_gamepad() { Ok(gamepad) => match gamepad.is_pressed(dir) { true => Color::Green, false => Color::Gray, }, Err(_) => Color::Gray, }; Block::new().style(Style::default().bg(color)) } } impl Widget for &App { fn render(self, area: Rect, buf: &mut Buffer) { let layout = Layout::vertical([Length(5), Min(0), Length(5)]); let [header_area, inner_area, footer_area] = layout.areas(area); let header_layout = Layout::horizontal([Fill(1), Fill(4)]); let [tabs_area, title_area] = header_layout.areas(header_area); let gamepad_layout = Layout::vertical([ Percentage(10), Percentage(50), Percentage(30), Percentage(10), ]); let [_, buttons, joysticks, _] = gamepad_layout.areas(inner_area); let buttons_layout = Layout::horizontal([Fill(1), Fill(2), Fill(2), Fill(2), Fill(1)]); let [_, left_buttons, mid_buttons, right_buttons, _] = buttons_layout.areas(buttons); // let details_layout = Layout::horizontal([Fill(2), Fill(2)]); // let [right_details_area, left_details_area] = details_layout.areas(details_area); App::render_title(title_area, buf); self.render_connected_gamepad(tabs_area, buf); Block::bordered().render(inner_area, buf); // Block::bordered() // .style(Style::default().bg(tailwind::GREEN.c900)) // .render(right_details_area, buf); // Block::new() // .style(Style::default().bg(tailwind::YELLOW.c300)) // .render(details_area, buf); self.render_right_buttons(right_buttons, buf); // Block::bordered() // .style(Style::default().bg(tailwind::TEAL.c300)) // .render(left_details_area, buf); // let gauge_progress: u16 = self // .tick // .wrapping_div(100) // .clamp(0, 100) // .try_into() // .unwrap(); // Gauge::default() // .gauge_style(tailwind::BLUE.c900) // .percent(gauge_progress) // .render(right_details_area, buf); // self.render_title(title_area, buf); // self.render_tabs(tabs_area, buf); // self.selected_tab.render(inner_area, buf); // render_footer(footer_area, buf); } }