use crate::App; use ratatui::{ buffer::Buffer, crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind}, layout::{Constraint, Flex, Layout, Rect}, style::{Modifier, Style, Stylize, palette::tailwind::*}, text::{Line, Span}, widgets::{ Block, Borders, HighlightSpacing, List, ListState, Padding, Paragraph, StatefulWidget, Widget, }, }; const GAME_HEADER_STYLE: Style = Style::new() .fg(NEUTRAL.c100) .bg(BLUE.c600) .add_modifier(Modifier::BOLD); const PROFILE_HEADER_STYLE: Style = Style::new() .fg(NEUTRAL.c100) .bg(VIOLET.c600) .add_modifier(Modifier::BOLD); const HEADER_STYLE: Style = Style::new().fg(ROSE.c500).add_modifier(Modifier::BOLD); const SELECTED_STYLE: Style = Style::new().bg(NEUTRAL.c700).add_modifier(Modifier::BOLD); const FOOTER_LEFT_STYLE: Style = Style::new().fg(PURPLE.c400).add_modifier(Modifier::BOLD); const FOOTER_RIGHT_STYLE: Style = Style::new().fg(LIME.c400); pub struct MenuList { items: Vec<&'static str>, state: ListState, } impl FromIterator<&'static str> for MenuList { fn from_iter>(iter: I) -> Self { let items = iter.into_iter().collect(); let state = ListState::default(); Self { items, state } } } impl Default for App { fn default() -> Self { Self { should_exit: false, menu: MenuList::from_iter(["40L", "Blitz", "txLadder", "Config"]), } } } impl App { fn render_header(&mut self, area: Rect, buf: &mut Buffer) { Block::new() .title(Line::raw(" txtris ").centered().style(HEADER_STYLE)) .borders(Borders::TOP) .render(area, buf); } fn render_footer(&mut self, area: Rect, buf: &mut Buffer) { Block::new() .title( Line::raw(" Atiran © 2025 ") .left_aligned() .style(FOOTER_LEFT_STYLE), ) .title( Line::raw(format!("v{} ", env!("CARGO_PKG_VERSION"))) .right_aligned() .style(FOOTER_RIGHT_STYLE), ) .borders(Borders::BOTTOM) .render(area, buf); } fn render_list(&mut self, area: Rect, buf: &mut Buffer) { let block = Block::new() .title(Line::raw(" Game ").centered().style(GAME_HEADER_STYLE)) .padding(Padding::symmetric(2, 1)) .borders(Borders::ALL); let items: Vec<&'static str> = self.menu.items.clone(); let list = List::new(items) .block(block) .highlight_style(SELECTED_STYLE) .highlight_symbol(" ") .highlight_spacing(HighlightSpacing::Always); StatefulWidget::render(list, area, buf, &mut self.menu.state); } fn render_profile(&mut self, area: Rect, buf: &mut Buffer) { let block = Block::new() .title( Line::raw(" Profile ") .centered() .style(PROFILE_HEADER_STYLE), ) .padding(Padding::symmetric(3, 1)) .borders(Borders::ALL); let text = vec![ Line::from(Span::styled("", Style::new().bold().underlined())), Line::from(vec![ Span::styled("40L: ", Style::new().blue()), Span::raw("N/A"), ]), Line::from(vec![ Span::styled("Blitz: ", Style::new().green()), Span::raw("N/A"), ]), Line::from(vec![ Span::styled("txLadder: ", Style::new().magenta()), Span::raw("N/A"), ]), ]; Paragraph::new(text).block(block).render(area, buf); } pub fn handle_key(&mut self, key: KeyEvent) { if key.kind != KeyEventKind::Press { return; } match key.code { KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true, KeyCode::Char('j') | KeyCode::Down => self.select_next(), KeyCode::Char('k') | KeyCode::Up => self.select_previous(), KeyCode::Char('c') => { if key.modifiers.contains(event::KeyModifiers::CONTROL) { self.should_exit = true; } } _ => {} } } fn select_next(&mut self) { self.menu.state.select_next(); } fn select_previous(&mut self) { self.menu.state.select_previous(); } } fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect { let [area] = Layout::horizontal([horizontal]) .flex(Flex::Center) .areas(area); let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); area } impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { let [header_area, main_area, footer_area] = Layout::vertical([ Constraint::Length(1), Constraint::Fill(1), Constraint::Length(2), ]) .areas(area); let center_area = center( main_area, Constraint::Length(50), Constraint::Percentage(60), ); let [profile_area, list_outer_area] = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(center_area); self.render_header(header_area, buf); self.render_profile(profile_area, buf); self.render_list(list_outer_area, buf); self.render_footer(footer_area, buf); } }