trace_game/windows/
practice_window.rs

1use crate::get_app_path;
2use crate::add_to_commands;
3use crate::generate_all_chars;
4use crate::{
5    convert_string_to_chars, windows::*, AppParagraph, CharStatus, ParagraphChar, State, Utc,
6    Window, WindowCommand,
7};
8use crossterm::event::KeyCode;
9use rand::prelude::SliceRandom;
10use std::{collections::HashMap, path::Path, rc::Rc};
11use tui::{
12    backend::Backend, layout::Alignment, layout::Constraint, layout::Direction, layout::Layout,
13    style::Color, style::Modifier, style::Style, text::Span, text::Spans, widgets::Block,
14    widgets::Borders, widgets::Gauge, widgets::Paragraph, widgets::Wrap, Frame,
15};
16
17pub fn practice_window<B: Backend>(state: Rc<State>) -> Box<dyn Fn(&mut Frame<B>)> {
18    Box::new(move |f| {
19        let spans: Vec<Span> = state.chars.iter().map(|c| c.to_span()).collect();
20        let layout = Layout::default()
21            .vertical_margin(f.size().height / 5)
22            .horizontal_margin(f.size().width / 3)
23            .constraints(
24                [
25                    Constraint::Percentage(50), //Paragraph space
26                    Constraint::Percentage(10), //Live statistics space
27                    Constraint::Percentage(40), //Paragraph information
28                ]
29                .as_ref(),
30            )
31            .split(f.size());
32        let statistics = Layout::default()
33            .direction(Direction::Horizontal)
34            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
35            .split(layout[1]);
36        let progress_info = Layout::default()
37            .direction(Direction::Vertical)
38            .constraints(
39                [
40                    Constraint::Percentage(20), //First player progress bar
41                    Constraint::Percentage(1),
42                ]
43                .as_ref(),
44            )
45            .split(layout[2]);
46
47        let paragraph = Paragraph::new(vec![Spans::from(spans)])
48            .alignment(Alignment::Center)
49            .wrap(Wrap { trim: false });
50        f.render_widget(paragraph, layout[0]);
51
52        let time_elapsed = Utc::now() - state.initial_time;
53        let wpm =
54            state.word_count as f64 / (time_elapsed.num_milliseconds() as f64 / 1000.0 / 60.0);
55        let formatted_wpm = format!("{:.2}", wpm);
56        let wpm_widget = create_label_widget("WPM: ", &formatted_wpm, Color::Yellow);
57        f.render_widget(wpm_widget, statistics[0]);
58
59        let accuracy =
60            (state.chars.len() - state.total_error_count) as f64 / state.chars.len() as f64 * 100.0;
61        let formatted_accuracy = format!("{:.2} %", accuracy);
62        let accuracy_widget = create_label_widget("Accuracy: ", &formatted_accuracy, Color::Yellow);
63        f.render_widget(accuracy_widget, statistics[1]);
64
65        let progress = state.index as f64 / state.chars.len() as f64 * 100.0;
66        let progress_widget = Gauge::default()
67            .block(
68                Block::default()
69                    .borders(Borders::TOP)
70                    .title(state.user_name.to_string())
71                    .border_style(Style::default().fg(Color::DarkGray)),
72            )
73            .gauge_style(
74                Style::default()
75                    .fg(Color::LightCyan)
76                    .bg(Color::Black)
77                    .add_modifier(Modifier::ITALIC),
78            )
79            .percent(progress as u16);
80        f.render_widget(progress_widget, progress_info[0]);
81    })
82}
83pub fn create_empty_practice_window<B: 'static + Backend>(state: &mut State) -> Option<Window<B>> {
84    state.reset();
85    state.paragraph = get_random_app_paragraph();
86    state.word_count = state.paragraph.content.split(' ').count();
87    state.chars = convert_string_to_chars(state.paragraph.content.to_string());
88    state.initial_time = Utc::now();
89    create_practice_window(state)
90}
91fn get_random_app_paragraph() -> AppParagraph {
92    let path = get_app_path("database.csv");
93    let random_par = csv::Reader::from_path(&path)
94        .and_then(|mut reader| {
95            let mut records: Vec<AppParagraph> = vec![];
96            for result in reader.deserialize() {
97                match result {
98                    Ok(r) => records.push(r),
99                    Err(r) => return Err(r),
100                }
101            }
102            Ok(records)
103        })
104        .and_then(|paragraphs: Vec<AppParagraph>| {
105            let random_par = paragraphs.choose(&mut rand::thread_rng());
106            Ok(random_par
107                .expect("Couldn't get a random paragraph!")
108                .clone())
109        });
110    match random_par {
111        Ok(p) => p,
112        Err(why) => panic!("{}", why),
113    }
114}
115fn create_practice_window<B: 'static + Backend>(_: &mut State) -> Option<Window<B>> {
116    fn handle_backspace_press<B: 'static + Backend>(state: &mut State) -> Option<Window<B>> {
117        if state.index != state.chars.len() {
118            state.chars[state.index] =
119                ParagraphChar::new(state.chars[state.index].character, CharStatus::Default);
120        }
121        if state.index > 0 {
122            //Going back to the previous inputted char, because the current is not inputted.
123            state.index -= 1;
124        }
125        let current_char = &state.chars[state.index];
126        let defaulted_char = match current_char.status {
127            CharStatus::Current => ParagraphChar::new(current_char.character, CharStatus::Current),
128            CharStatus::Correct => ParagraphChar::new(current_char.character, CharStatus::Current),
129            CharStatus::Wrong => {
130                state.current_error_count -= 1;
131                ParagraphChar::new(current_char.character, CharStatus::Current)
132            }
133            CharStatus::Default => ParagraphChar::new(current_char.character, CharStatus::Current),
134        };
135        state.chars[state.index] = defaulted_char;
136        create_practice_window(state)
137    }
138
139    let mut commands = HashMap::from([
140        (
141            KeyCode::Esc,
142            WindowCommand {
143                activator_key: KeyCode::Esc,
144                action: Box::new(create_main_menu_window),
145            },
146        ),
147        (
148            KeyCode::Backspace,
149            WindowCommand {
150                activator_key: KeyCode::Backspace,
151                action: Box::new(handle_backspace_press),
152            },
153        ),
154    ]);
155
156    let chars = generate_all_chars();
157    add_to_commands(&mut commands, &chars, Box::new(handle_char_press));
158    Some(Window {
159        ui: practice_window,
160        commands,
161    })
162}
163
164fn handle_char_press<B: 'static + Backend>(
165    pressed_character: char,
166) -> Box<dyn Fn(&mut State) -> Option<Window<B>>> {
167    Box::new(move |state: &mut State| {
168        let current_char = &state.chars[state.index];
169        let is_correct = current_char.character == pressed_character;
170        let status = if is_correct {
171            CharStatus::Correct
172        } else {
173            CharStatus::Wrong
174        };
175
176        let transformed_char = ParagraphChar::new(current_char.character, status);
177        state.chars[state.index] = transformed_char;
178
179        state.index += 1;
180
181        if !is_correct {
182            state.current_error_count += 1;
183            state.total_error_count += 1;
184        }
185
186        let end_of_paragraph = state.index == state.chars.len();
187
188        if end_of_paragraph && state.current_error_count == 0 {
189            state.end_time = Utc::now();
190            create_end_window(state)
191        } else {
192            if !end_of_paragraph {
193                let current_char = &state.chars[state.index];
194                let transformed_char =
195                    ParagraphChar::new(current_char.character, CharStatus::Current);
196                state.chars[state.index] = transformed_char;
197            }
198            create_practice_window(state)
199        }
200    })
201}