trace_game/windows/
practice_window.rs1use 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), Constraint::Percentage(10), Constraint::Percentage(40), ]
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), 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 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}