diff --git a/client/src/main.rs b/client/src/main.rs index 36e229c..b6c7bd9 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,31 +1,11 @@ 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, - }, + crossterm::event::{self, Event}, }; -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); +mod menu; fn main() -> Result<()> { color_eyre::install()?; @@ -35,103 +15,12 @@ fn main() -> Result<()> { app_result } -struct App { +pub struct App { should_exit: bool, - menu: MenuList, -} - -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"]), - } - } + menu: menu::MenuList, } 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(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<()> { while !self.should_exit { terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; @@ -141,62 +30,4 @@ impl App { } Ok(()) } - - 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); - } } diff --git a/client/src/menu.rs b/client/src/menu.rs new file mode 100644 index 0000000..488ac44 --- /dev/null +++ b/client/src/menu.rs @@ -0,0 +1,179 @@ +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); + } +}