diff --git a/client/src/game.rs b/client/src/game.rs new file mode 100644 index 0000000..0ec3fb6 --- /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(menu_list) => menu_list.handle_key(key), + _ => Mode::Exit, + } + } +} + +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::render_profile(profile_area, buf); + menu_list.render(list_outer_area, buf); + } + _ => {}, + } + menu::render_footer(footer_area, buf); + } +} diff --git a/client/src/main.rs b/client/src/main.rs index b6c7bd9..a079f11 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,33 +1,12 @@ use color_eyre::Result; -use ratatui::{ - DefaultTerminal, - crossterm::event::{self, Event}, -}; - +mod game; mod menu; fn main() -> Result<()> { color_eyre::install()?; let terminal = ratatui::init(); - let app_result = App::default().run(terminal); + let app_result = game::Mode::default().run(terminal); ratatui::restore(); app_result } - -pub struct App { - should_exit: bool, - menu: menu::MenuList, -} - -impl App { - fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { - while !self.should_exit { - terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; - if let Event::Key(key) = event::read()? { - self.handle_key(key); - }; - } - Ok(()) - } -} diff --git a/client/src/menu.rs b/client/src/menu.rs index 488ac44..6e2cf04 100644 --- a/client/src/menu.rs +++ b/client/src/menu.rs @@ -1,38 +1,93 @@ -use crate::App; - +// Main menu sections use ratatui::{ buffer::Buffer, - crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind}, - layout::{Constraint, Flex, Layout, Rect}, - style::{Modifier, Style, Stylize, palette::tailwind::*}, + crossterm::event::{self, KeyCode, KeyEvent}, + layout::Rect, text::{Line, Span}, + style::{Modifier, Style, Stylize, palette::tailwind::*}, widgets::{ - Block, Borders, HighlightSpacing, List, ListState, Padding, Paragraph, StatefulWidget, - Widget, + Block, Borders, HighlightSpacing, List, + ListState, Padding, StatefulWidget, Paragraph }, }; +use ratatui::prelude::*; + +use crate::game; const GAME_HEADER_STYLE: Style = Style::new() - .fg(NEUTRAL.c100) + .fg(ZINC.c100) .bg(BLUE.c600) .add_modifier(Modifier::BOLD); const PROFILE_HEADER_STYLE: Style = Style::new() - .fg(NEUTRAL.c100) + .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(NEUTRAL.c700).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); -pub struct MenuList { +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, } -impl FromIterator<&'static str> for MenuList { +impl FromIterator<&'static str> for OptionList { fn from_iter>(iter: I) -> Self { let items = iter.into_iter().collect(); let state = ListState::default(); @@ -40,46 +95,44 @@ impl FromIterator<&'static str> for MenuList { } } -impl Default for App { - fn default() -> Self { - Self { - should_exit: false, - menu: MenuList::from_iter(["40L", "Blitz", "txLadder", "Config"]), +impl OptionList { + pub fn handle_key(mut self, key: KeyEvent) -> game::Mode { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => game::Mode::Exit, + KeyCode::Char('j') | KeyCode::Down => { + self.select_next(); + game::Mode::MainMenu(self) + } + KeyCode::Char('k') | KeyCode::Up => { + self.select_previous(); + game::Mode::MainMenu(self) + } + KeyCode::Char('c') => { + if key.modifiers.contains(event::KeyModifiers::CONTROL) { + game::Mode::Exit + } else { + game::Mode::MainMenu(self) + } + } + _ => game::Mode::MainMenu(self), } } -} -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 select_next(&mut self) { + self.state.select_next(); } - 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 select_previous(&mut self) { + self.state.select_previous(); } - fn render_list(&mut self, area: Rect, buf: &mut Buffer) { + 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.menu.items.clone(); + let items: Vec<&'static str> = self.items.clone(); let list = List::new(items) .block(block) @@ -87,93 +140,6 @@ impl App { .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); + StatefulWidget::render(list, area, buf, &mut self.state); } } diff --git a/server/Cargo.lock b/server/Cargo.lock new file mode 100644 index 0000000..f1b1e97 --- /dev/null +++ b/server/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "server" +version = "0.1.0" diff --git a/txcore/Cargo.lock b/txcore/Cargo.lock new file mode 100644 index 0000000..f1d6024 --- /dev/null +++ b/txcore/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "txcore" +version = "0.1.0"