#![allow(clippy::collapsible_match)]
use crate::additional_tui::*;
use crate::math_item::*;
use std::char;
use std::default;
use std::io;
use std::time::Instant;
use std::vec;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use ratatui::layout::Alignment;
use ratatui::text;
use ratatui::text::Span;
use ratatui::{
DefaultTerminal, Frame,
buffer::Buffer,
layout::{Constraint, Layout, Rect},
style::{self, Color, Stylize},
symbols::border::{self},
text::Line,
widgets::{Block, Paragraph, Widget},
};
use anyhow::{Context, Result};
pub fn run_ratatui() -> Result<()> {
color_eyre::install().unwrap();
ratatui::run(|terminal| App::default().run(terminal))?;
Ok(())
}
#[derive(Debug, Default)]
struct Statistic {
total: usize,
correct: usize,
wrong: usize,
time: f32,
}
#[derive(Debug, Default)]
struct PlayState {
game_params: GameParams,
statistic: Statistic,
problem_last: u32,
problem: Example,
user_input: String,
has_minus: bool,
has_point: bool,
answers_history: Vec<bool>,
}
#[derive(Debug)]
struct App {
current_screen: Screen,
current_menu: Option<MenuItems>,
language: Language,
main_choies: i16,
horisontal_choise: isize,
horisontsl_choised: Vec<isize>,
time: Instant,
exit: bool,
play_state: PlayState,
}
impl Default for App {
fn default() -> Self {
Self {
current_screen: Screen::Main,
current_menu: Some(MenuItems::load_main_menu()),
language: Language::default(),
main_choies: 0,
horisontal_choise: 0,
horisontsl_choised: Vec::new(),
time: Instant::now(),
exit: false,
play_state: PlayState::default(),
}
}
}
impl App {
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
fn draw(&self, frame: &mut Frame) {
frame.render_widget(self, frame.area());
}
fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Enter => self.handle_enter(),
KeyCode::Char('q') => self.exit(),
_ => {}
}
match self.current_screen {
Screen::Main => match key_event.code {
KeyCode::Char('j') => self.increase_ch(),
KeyCode::Char('k') => self.substract_ch(),
_ => {}
},
Screen::GameSettings => match key_event.code {
KeyCode::Char('j') => self.increase_ch(),
KeyCode::Char('k') => self.substract_ch(),
KeyCode::Char(c) => self.handle_game_params(c),
_ => {}
},
Screen::Play => match key_event.code {
KeyCode::Char(char) => self.push_user_input(char),
KeyCode::Backspace => self.pop_user_input(),
_ => {}
},
Screen::Result => match key_event.code {
KeyCode::Char(char) => {
if char == 'k' {
self.main_choies -= 1;
if self.main_choies < 4 {
self.main_choies = 5;
}
} else if char == 'j' {
self.main_choies += 1;
if self.main_choies > 5 {
self.main_choies = 4;
}
}
}
_ => {}
},
_ => {}
}
}
fn handle_game_params(&mut self, char: char) {
match self.main_choies {
0 => match char {
'l' => {
self.play_state.game_params.amount =
self.play_state.game_params.amount.saturating_add(1)
}
'h' => {
if self.play_state.game_params.amount > 1 {
self.play_state.game_params.amount -= 1;
}
}
_ => {}
},
1 => match char {
'l' => self.play_state.game_params.from += 1.0,
'h' => self.play_state.game_params.from -= 1.0,
_ => {}
},
2 => match char {
'l' => self.play_state.game_params.to += 1.0,
'h' => self.play_state.game_params.to -= 1.0,
_ => {}
},
3 => match char {
'l' => self.play_state.game_params.digits_after_num += 1,
'h' => self.play_state.game_params.digits_after_num -= 1,
_ => {}
},
4 => match char {
'l' => {
self.horisontal_choise += 1;
if self.horisontal_choise > 3 {
self.horisontal_choise = 0;
}
}
'h' => {
self.horisontal_choise -= 1;
if self.horisontal_choise < 0 {
self.horisontal_choise = 3;
}
}
_ => {}
},
_ => {}
}
}
fn handle_enter(&mut self) {
match self.current_screen {
Screen::Play => {
self.check_answer();
return;
}
Screen::GameSettings => {
if self.main_choies == 4 {
if let Some(index) = self
.horisontsl_choised
.iter()
.position(|x| x == &self.horisontal_choise)
{
self.horisontsl_choised.remove(index);
} else {
self.horisontsl_choised.push(self.horisontal_choise);
}
}
}
_ => {}
}
if let Some(menu) = &self.current_menu {
let item = &menu.items[self.main_choies as usize];
if let Some(next) = item.next_screen {
self.change_screen(next);
self.main_choies = 0;
} else {
if self.current_screen == Screen::Main && item.id == 3 {
self.exit = true;
}
}
}
}
fn change_screen(&mut self, screen: Screen) {
self.current_screen = screen;
self.main_choies = 0;
match screen {
Screen::Main => {
self.current_menu = Some(MenuItems::load_main_menu());
}
Screen::Settings => self.current_menu = Some(MenuItems::load_settings_menu()),
Screen::GameSettings => self.current_menu = Some(MenuItems::load_play_menu()),
Screen::Play => {
self.play_state.problem_last = self.play_state.game_params.amount;
self.play_state.statistic.total = self.play_state.game_params.amount as usize;
self.set_types();
self.new_problem();
self.time = Instant::now();
self.current_menu = None;
}
Screen::Result => {
self.current_menu = Some(MenuItems::load_result_menu());
}
Screen::Ways => {}
_ => {}
}
}
fn check_answer(&mut self) {
if self.play_state.user_input.is_empty() {
return;
}
let user_answer: f32 = self.play_state.user_input.parse().unwrap();
if user_answer == self.play_state.problem.answer {
self.play_state.answers_history.push(true);
self.play_state.statistic.correct += 1;
} else {
self.play_state.answers_history.push(false);
self.play_state.statistic.wrong += 1;
}
self.play_state.problem_last -= 1;
self.clear_enter_line();
self.new_problem();
}
fn clear_enter_line(&mut self) {
self.play_state.user_input = String::new();
}
fn pop_user_input(&mut self) {
if let Some(deleted_char) = self.play_state.user_input.pop() {
if deleted_char == '.' || deleted_char == ',' {
self.play_state.has_point = false;
}
if deleted_char == '-' {
self.play_state.has_minus = false;
}
}
}
fn push_user_input(&mut self, char: char) {
if char.is_numeric() {
self.play_state.user_input.push(char);
return;
}
if (char == '.' || char == ',')
&& !self.play_state.has_point
&& !self.play_state.user_input.is_empty()
{
self.play_state.has_point = true;
self.play_state.user_input.push(char);
return;
}
if char == '-' && self.play_state.user_input.is_empty() && !self.play_state.has_minus {
self.play_state.has_minus = true;
self.play_state.user_input.push(char);
}
}
fn increase_ch(&mut self) {
self.main_choies += 1;
if let Some(menu) = &self.current_menu {
if self.main_choies > menu.amount {
self.main_choies = 0
}
}
}
fn substract_ch(&mut self) {
self.main_choies -= 1;
if let Some(menu) = &self.current_menu {
if self.main_choies < 0 {
self.main_choies = menu.amount
}
}
}
fn exit(&mut self) {
if self.current_screen == Screen::Main {
self.exit = true;
} else if self.current_screen == Screen::Play {
self.change_screen(Screen::GameSettings);
self.play_state = PlayState::default()
} else {
self.change_screen(Screen::Main)
}
}
fn handle_events(&mut self) -> io::Result<()> {
match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event)
}
_ => {}
};
Ok(())
}
fn new_problem(&mut self) {
if self.play_state.problem_last == 0 {
self.change_screen(Screen::Result);
self.play_state.statistic.time = self.time.elapsed().as_secs_f32();
self.play_state.answers_history = Vec::default();
}
let problem = generate_problem(
&self.play_state.game_params.type_problems,
&self.play_state.game_params,
);
self.play_state.problem = problem;
}
fn set_types(&mut self) {
let mut types = Vec::new();
for type_ in &self.horisontsl_choised {
match type_ {
0 => types.push(TypeProblem::Addition),
1 => types.push(TypeProblem::Subtraction),
2 => types.push(TypeProblem::Multiplication),
3 => types.push(TypeProblem::Division),
_ => {}
}
}
self.play_state.game_params.type_problems = types;
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let title = Line::from("Little Math Trainer".bold());
let instruction = Line::from(vec![
"Quit".into(),
"<Q>".blue().bold(),
" Up".into(),
"<K> ".blue().bold(),
" Down".into(),
"<J> ".blue().bold(),
" Increase".into(),
"<L> ".blue().bold(),
" Decrease".into(),
"<H> ".blue().bold(),
]);
let screen = Block::bordered()
.title(title.centered())
.title_bottom(instruction.centered())
.border_set(border::THICK);
let inner_area = screen.inner(area);
let vertical_splits = Layout::vertical([
Constraint::Percentage(5),
Constraint::Percentage(90),
Constraint::Percentage(5),
])
.split(inner_area);
screen.render(area, buf);
match self.current_screen {
Screen::Main | Screen::Settings => {
let menu_area = Layout::horizontal([
Constraint::Fill(1),
Constraint::Percentage(40),
Constraint::Fill(1),
])
.split(vertical_splits[1]);
if let Some(menu) = &self.current_menu {
let menu_len = menu.items.len();
let constraints = vec![Constraint::Length(3); menu_len];
let box_chunks = Layout::vertical(constraints).split(menu_area[1]);
for (i, item) in menu.items.iter().enumerate() {
let is_selected = i as i16 == self.main_choies;
let item_box = Block::bordered()
.border_set(if is_selected {
border::THICK
} else {
border::PLAIN
})
.border_style(if is_selected {
style::Color::Green
} else {
style::Color::default()
});
let display_text = item.text.first().map(|s| s.as_str()).unwrap_or("");
Paragraph::new(display_text)
.centered()
.block(item_box)
.render(box_chunks[i], buf);
}
}
}
Screen::Play => {
let menu_area = Layout::horizontal([
Constraint::Fill(1),
Constraint::Percentage(55),
Constraint::Fill(1),
])
.split(vertical_splits[1]);
let box_chunks = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
])
.split(menu_area[1]);
let problem_box = Block::new();
Paragraph::new(self.play_state.problem.problem.as_str())
.centered()
.block(problem_box)
.render(box_chunks[0], buf);
let answer_box = Block::bordered().border_set(border::PLAIN);
let input_text = if self.play_state.user_input.is_empty() {
"Enter answer".fg(Color::Rgb(115, 112, 110))
} else {
self.play_state.user_input.as_str().white()
};
let alignment = if self.play_state.user_input.is_empty() {
Alignment::Center
} else {
Alignment::Right
};
Paragraph::new(input_text)
.alignment(alignment)
.block(answer_box)
.render(box_chunks[1], buf);
let history_answer_box = Block::bordered().border_set(border::PLAIN);
let spans: Vec<_> = self
.play_state
.answers_history
.iter()
.map(|&is_correct| {
if is_correct {
"● ".green()
} else {
"● ".red()
}
})
.collect();
Paragraph::new(Line::from(spans))
.left_aligned()
.block(history_answer_box)
.render(box_chunks[2], buf);
}
Screen::GameSettings => {
let menu_area = Layout::horizontal([
Constraint::Fill(1),
Constraint::Percentage(50),
Constraint::Fill(1),
])
.split(vertical_splits[1]);
if let Some(menu) = &self.current_menu {
let menu_len = menu.items.len();
let constraints = vec![Constraint::Length(3); menu_len];
let box_chunks = Layout::vertical(constraints).split(menu_area[1]);
for (i, item) in menu.items.iter().enumerate() {
let is_selected = i as i16 == self.main_choies;
let item_box = Block::bordered()
.border_set(if is_selected {
border::THICK
} else {
border::PLAIN
})
.border_style(if is_selected {
style::Color::Green
} else {
style::Color::default()
});
let label = item.text.first().map(|s| s.as_str()).unwrap_or("");
if item.id == 4 {
let inner_area = &item_box.inner(box_chunks[4]);
item_box.clone().render(box_chunks[i], buf);
let sub_chunks =
Layout::horizontal(vec![Constraint::Percentage(25); 4])
.split(*inner_area);
let types = vec!["Add", "Sub", "Mult", "Div"];
for (sub_id, text) in types.iter().enumerate() {
if self.horisontal_choise == sub_id as isize {
Paragraph::new(Span::from(*text).green().bold())
.centered()
.render(sub_chunks[sub_id], buf);
} else if self.horisontsl_choised.contains(&(sub_id as isize)) {
Paragraph::new(Span::from(*text).yellow().bold())
.centered()
.render(sub_chunks[sub_id], buf);
} else {
Paragraph::new(*text)
.centered()
.render(sub_chunks[sub_id], buf);
}
}
}
let display_content = match item.id {
0 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.game_params.amount.yellow().bold(),
]),
1 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.game_params.from.yellow().bold(),
]),
2 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.game_params.to.yellow().bold(),
]),
3 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.game_params.digits_after_num.yellow().bold(),
]),
5 => Line::from(vec![label.green().bold()]),
_ => Line::from(label),
};
Paragraph::new(display_content)
.centered()
.block(item_box.clone())
.render(box_chunks[i], buf);
}
}
}
Screen::Result => {
let menu_area = Layout::horizontal([
Constraint::Fill(1),
Constraint::Percentage(50),
Constraint::Fill(1),
])
.split(vertical_splits[1]);
if let Some(menu) = &self.current_menu {
let menu_len = menu.items.len();
let constraints = vec![Constraint::Length(3); menu_len];
let box_chunks = Layout::vertical(constraints).split(menu_area[1]);
for (i, item) in menu.items.iter().enumerate() {
let is_selected =
(item.id == 4 || item.id == 5) && i as i16 == self.main_choies;
let item_box = Block::bordered()
.border_set(if is_selected {
border::THICK
} else {
border::PLAIN
})
.border_style(if is_selected {
style::Color::Green
} else {
style::Color::default()
});
let label = item.text.first().map(|s| s.as_str()).unwrap_or("");
let display_content = match item.id {
0 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.statistic.total.yellow().bold(),
]),
1 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.statistic.correct.yellow().bold(),
]),
2 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.statistic.wrong.yellow().bold(),
]),
3 => Line::from(vec![
label.into(),
": ".into(),
self.play_state.statistic.time.yellow().bold(),
]),
4 => Line::from(vec![label.green().bold()]),
5 => Line::from(vec![label.yellow().bold()]),
_ => Line::from(label),
};
Paragraph::new(display_content)
.centered()
.block(item_box.clone())
.render(box_chunks[i], buf);
}
}
}
_ => {}
}
}
}