From 82a45eab0801f8d71f0616d98b2857226d935811 Mon Sep 17 00:00:00 2001 From: Abdulmujeeb Raji Date: Thu, 20 Feb 2025 10:41:29 +0000 Subject: [PATCH] split into modules --- client/src/game.rs | 96 +++++++++++++++++++ client/src/main.rs | 231 +-------------------------------------------- client/src/menu.rs | 143 ++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 227 deletions(-) create mode 100644 client/src/game.rs create mode 100644 client/src/menu.rs diff --git a/client/src/game.rs b/client/src/game.rs new file mode 100644 index 0000000..708e411 --- /dev/null +++ b/client/src/game.rs @@ -0,0 +1,96 @@ +use color_eyre::Result; +use ratatui::{ + DefaultTerminal, + buffer::Buffer, + crossterm::event::{self, Event, KeyEvent, KeyEventKind}, + layout::{Constraint, Flex, Layout, Rect}, + widgets::Widget, +}; + +use crate::menu; + +#[derive(Clone)] +pub enum Mode { + Exit, + MainMenu(menu::OptionList), + FortyL, + Blitz, + TxLadder, + Config, +} + +impl Default for Mode { + fn default() -> Self { + Mode::MainMenu(menu::OptionList::from_iter(["40L", "Blitz", "txLadder", "Config"])) + } +} + +impl Mode { + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + loop { + match self { + Mode::Exit => { + break + } + _ => { + terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; + if let Event::Key(key) = event::read()? { + self = self.handle_key(key); + }; + } + } + } + Ok(()) + } + + fn handle_key(self, key: KeyEvent) -> Mode { + if key.kind != KeyEventKind::Press { + return self + } + + match self { + Mode::MainMenu(ref menu_list) => menu_list.handle_key(self.clone(), key), + _ => return self, + } + } +} + +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 Mode { + 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); + + menu::render_header(header_area, buf); + + match self { + Mode::MainMenu(menu_list) => { + menu_list.render(list_outer_area, buf); + menu::render_profile(profile_area, buf); + } + _ => {}, + } + menu::render_footer(footer_area, buf); + } +} diff --git a/client/src/main.rs b/client/src/main.rs index af96326..9b06c40 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,235 +1,12 @@ use color_eyre::Result; -use ratatui::{ - DefaultTerminal, - buffer::Buffer, - crossterm::event::{self, Event, 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(ZINC.c100) - .bg(BLUE.c600) - .add_modifier(Modifier::BOLD); -const PROFILE_HEADER_STYLE: Style = Style::new() - .fg(ZINC.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(ZINC.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); +mod game; +mod menu; fn main() -> Result<()> { color_eyre::install()?; let terminal = ratatui::init(); - let app_result = GameMode::default().run(terminal); + let app_result = game::Mode::default().run(terminal); ratatui::restore(); app_result -} - -#[derive(Clone)] -enum GameMode { - Exit, - MainMenu(MenuList), - FortyL, - Blitz, - TxLadder, - Config, -} - -#[derive(Clone)] -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 MenuList { - fn handle_key(&self, current_mode: GameMode, key: KeyEvent) -> GameMode { - match key.code { - KeyCode::Char('q') | KeyCode::Esc => GameMode::Exit, - 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) { - GameMode::Exit - } else { - current_mode - } - } - _ => current_mode - } - } - - fn select_next(&self) -> GameMode { - let mut menu_list = self.clone(); - menu_list.state.select_next(); - GameMode::MainMenu(menu_list) - } - - fn select_previous(&self) -> GameMode { - let mut menu_list = self.clone(); - menu_list.state.select_previous(); - GameMode::MainMenu(menu_list) - } - - fn render(&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.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.state); - } -} - -impl Default for GameMode { - fn default() -> Self { - GameMode::MainMenu(MenuList::from_iter(["40L", "Blitz", "txLadder", "Config"])) - } -} - -impl GameMode { - 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_profile(&mut self, area: Rect, buf: &mut Buffer) { - let block = Block::new() - .title( - Line::raw(" Profile ") - .centered() - .style(PROFILE_HEADER_STYLE), - ) - .padding(Padding::symmetric(2, 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); - } - - fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { - loop { - match self { - GameMode::Exit => { - break - } - _ => { - terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; - if let Event::Key(key) = event::read()? { - self = self.handle_key(key); - }; - } - } - } - Ok(()) - } - - fn handle_key(self, key: KeyEvent) -> GameMode { - if key.kind != KeyEventKind::Press { - return self - } - - match self { - GameMode::MainMenu(ref menu_list) => menu_list.handle_key(self.clone(), key), - _ => return self, - } - } -} - -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 GameMode { - 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); - - match self { - GameMode::MainMenu(menu_list) => { - menu_list.render(list_outer_area, buf); - self.render_profile(profile_area, buf); - } - _ => {}, - } - self.render_footer(footer_area, buf); - } -} +} \ No newline at end of file diff --git a/client/src/menu.rs b/client/src/menu.rs new file mode 100644 index 0000000..e885e44 --- /dev/null +++ b/client/src/menu.rs @@ -0,0 +1,143 @@ +// Main menu sections +use ratatui::{ + buffer::Buffer, + crossterm::event::{self, KeyCode, KeyEvent}, + layout::Rect, + text::{Line, Span}, + style::{Modifier, Style, Stylize, palette::tailwind::*}, + widgets::{ + Block, Borders, HighlightSpacing, List, + ListState, Padding, StatefulWidget, Paragraph + }, +}; +use ratatui::prelude::*; + +use crate::game; + +pub fn render_header(area: Rect, buf: &mut Buffer) { + Block::new() + .title(Line::raw(" txtris ").centered().style(HEADER_STYLE)) + .borders(Borders::TOP) + .render(area, buf); +} + +pub fn render_footer(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); +} + +pub fn render_profile(area: Rect, buf: &mut Buffer) { + let block = Block::new() + .title( + Line::raw(" Profile ") + .centered() + .style(PROFILE_HEADER_STYLE), + ) + .padding(Padding::symmetric(2, 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); +} + +#[derive(Clone)] +pub struct OptionList { + items: Vec<&'static str>, + state: ListState, +} + +const GAME_HEADER_STYLE: Style = Style::new() + .fg(ZINC.c100) + .bg(BLUE.c600) + .add_modifier(Modifier::BOLD); +const PROFILE_HEADER_STYLE: Style = Style::new() + .fg(ZINC.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(ZINC.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); + +impl FromIterator<&'static str> for OptionList { + fn from_iter>(iter: I) -> Self { + let items = iter.into_iter().collect(); + let state = ListState::default(); + Self { items, state } + } +} + +impl OptionList { + pub fn handle_key(&self, current_mode: game::Mode, key: KeyEvent) -> game::Mode { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => game::Mode::Exit, + 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) { + game::Mode::Exit + } else { + current_mode + } + } + _ => current_mode + } + } + + fn select_next(&self) -> game::Mode { + let mut menu_list = self.clone(); + menu_list.state.select_next(); + game::Mode::MainMenu(menu_list) + } + + fn select_previous(&self) -> game::Mode { + let mut menu_list = self.clone(); + menu_list.state.select_previous(); + game::Mode::MainMenu(menu_list) + } + + pub fn render(&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.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.state); + } +} \ No newline at end of file