|
1 | 1 | use crate::config::Theme; |
2 | 2 |
|
3 | | -use super::test::{results, Test}; |
| 3 | +use super::test::{results, Test, TestWord}; |
4 | 4 |
|
5 | 5 | use crossterm::event::KeyCode; |
6 | 6 | use crossterm::event::KeyEvent; |
@@ -101,62 +101,7 @@ impl ThemedWidget for &Test { |
101 | 101 | input.render(buf); |
102 | 102 |
|
103 | 103 | let target_lines: Vec<Line> = { |
104 | | - let words = iter::empty::<Vec<Span>>() |
105 | | - // already typed words |
106 | | - .chain(self.words[..self.current_word].iter().map(|w| { |
107 | | - vec![Span::styled( |
108 | | - w.text.clone() + " ", |
109 | | - if w.progress == w.text { |
110 | | - theme.prompt_correct |
111 | | - } else { |
112 | | - theme.prompt_incorrect |
113 | | - }, |
114 | | - )] |
115 | | - })) |
116 | | - // current word |
117 | | - .chain({ |
118 | | - let progress_ind = self.words[self.current_word] |
119 | | - .progress |
120 | | - .len() |
121 | | - .min(self.words[self.current_word].text.len()); |
122 | | - |
123 | | - let correct = self.words[self.current_word] |
124 | | - .text |
125 | | - .starts_with(&self.words[self.current_word].progress[..]); |
126 | | - |
127 | | - let (typed, untyped) = |
128 | | - self.words[self.current_word] |
129 | | - .text |
130 | | - .split_at(ceil_char_boundary( |
131 | | - &self.words[self.current_word].text, |
132 | | - progress_ind, |
133 | | - )); |
134 | | - |
135 | | - let mut remaining = untyped.chars().chain(iter::once(' ')); |
136 | | - let cursor = remaining.next().unwrap(); |
137 | | - |
138 | | - iter::once(vec![ |
139 | | - Span::styled( |
140 | | - typed, |
141 | | - if correct { |
142 | | - theme.prompt_current_correct |
143 | | - } else { |
144 | | - theme.prompt_current_incorrect |
145 | | - }, |
146 | | - ), |
147 | | - Span::styled( |
148 | | - cursor.to_string(), |
149 | | - theme.prompt_current_untyped.patch(theme.prompt_cursor), |
150 | | - ), |
151 | | - Span::styled(remaining.collect::<String>(), theme.prompt_current_untyped), |
152 | | - ]) |
153 | | - }) |
154 | | - // remaining words |
155 | | - .chain( |
156 | | - self.words[self.current_word + 1..] |
157 | | - .iter() |
158 | | - .map(|w| vec![Span::styled(w.text.clone() + " ", theme.prompt_untyped)]), |
159 | | - ); |
| 104 | + let words = words_to_spans(&self.words, self.current_word, theme); |
160 | 105 |
|
161 | 106 | let mut lines: Vec<Line> = Vec::new(); |
162 | 107 | let mut current_line: Vec<Span> = Vec::new(); |
@@ -189,6 +134,144 @@ impl ThemedWidget for &Test { |
189 | 134 | } |
190 | 135 | } |
191 | 136 |
|
| 137 | + |
| 138 | + |
| 139 | +fn words_to_spans<'a>( |
| 140 | + words: &'a [TestWord], |
| 141 | + current_word: usize, |
| 142 | + theme: &'a Theme, |
| 143 | +) -> Vec<Vec<Span<'a>>> { |
| 144 | + let mut spans = Vec::new(); |
| 145 | + |
| 146 | + for word in &words[..current_word] { |
| 147 | + let parts = split_typed_word(word); |
| 148 | + spans.push(word_parts_to_spans(parts, theme)); |
| 149 | + } |
| 150 | + |
| 151 | + let parts_current = split_current_word(&words[current_word]); |
| 152 | + spans.push(word_parts_to_spans(parts_current, theme)); |
| 153 | + |
| 154 | + for word in &words[current_word + 1..] { |
| 155 | + let parts = vec![(word.text.clone(), Status::Untyped)]; |
| 156 | + spans.push(word_parts_to_spans(parts, theme)); |
| 157 | + } |
| 158 | + spans |
| 159 | +} |
| 160 | + |
| 161 | +#[derive(PartialEq, Clone, Copy, Debug)] |
| 162 | +enum Status { |
| 163 | + Correct, |
| 164 | + Incorrect, |
| 165 | + CurrentUntyped, |
| 166 | + CurrentCorrect, |
| 167 | + CurrentIncorrect, |
| 168 | + Cursor, |
| 169 | + Untyped, |
| 170 | + Overtyped, |
| 171 | +} |
| 172 | +fn split_current_word(word: &TestWord) -> Vec<(String, Status)> { |
| 173 | + let mut parts = Vec::new(); |
| 174 | + let mut cur_string = String::new(); |
| 175 | + let mut cur_status = Status::Untyped; |
| 176 | + |
| 177 | + let mut progress = word.progress.chars(); |
| 178 | + for tc in word.text.chars() { |
| 179 | + let p = progress.next(); |
| 180 | + let status = match p { |
| 181 | + None => Status::CurrentUntyped, |
| 182 | + Some(c) => match c { |
| 183 | + c if c == tc => Status::CurrentCorrect, |
| 184 | + _ => Status::CurrentIncorrect, |
| 185 | + }, |
| 186 | + }; |
| 187 | + |
| 188 | + if status == cur_status { |
| 189 | + cur_string.push(tc); |
| 190 | + } else { |
| 191 | + if !cur_string.is_empty() { |
| 192 | + parts.push((cur_string, cur_status)); |
| 193 | + cur_string = String::new(); |
| 194 | + } |
| 195 | + cur_string.push(tc); |
| 196 | + cur_status = status; |
| 197 | + |
| 198 | + // first currentuntyped is cursor |
| 199 | + if status == Status::CurrentUntyped { |
| 200 | + parts.push((cur_string, Status::Cursor)); |
| 201 | + cur_string = String::new(); |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + if !cur_string.is_empty() { |
| 206 | + parts.push((cur_string, cur_status)); |
| 207 | + } |
| 208 | + let overtyped = progress.collect::<String>(); |
| 209 | + if !overtyped.is_empty() { |
| 210 | + parts.push((overtyped, Status::Overtyped)); |
| 211 | + } |
| 212 | + parts |
| 213 | +} |
| 214 | + |
| 215 | +fn split_typed_word(word: &TestWord) -> Vec<(String, Status)> { |
| 216 | + let mut parts = Vec::new(); |
| 217 | + let mut cur_string = String::new(); |
| 218 | + let mut cur_status = Status::Untyped; |
| 219 | + |
| 220 | + let mut progress = word.progress.chars(); |
| 221 | + for tc in word.text.chars() { |
| 222 | + let p = progress.next(); |
| 223 | + let status = match p { |
| 224 | + None => Status::Untyped, |
| 225 | + Some(c) => match c { |
| 226 | + c if c == tc => Status::Correct, |
| 227 | + _ => Status::Incorrect, |
| 228 | + }, |
| 229 | + }; |
| 230 | + |
| 231 | + if status == cur_status { |
| 232 | + cur_string.push(tc); |
| 233 | + } else { |
| 234 | + if !cur_string.is_empty() { |
| 235 | + parts.push((cur_string, cur_status)); |
| 236 | + cur_string = String::new(); |
| 237 | + } |
| 238 | + cur_string.push(tc); |
| 239 | + cur_status = status; |
| 240 | + } |
| 241 | + } |
| 242 | + if !cur_string.is_empty() { |
| 243 | + parts.push((cur_string, cur_status)); |
| 244 | + } |
| 245 | + |
| 246 | + let overtyped = progress.collect::<String>(); |
| 247 | + if !overtyped.is_empty() { |
| 248 | + parts.push((overtyped, Status::Overtyped)); |
| 249 | + } |
| 250 | + parts |
| 251 | +} |
| 252 | + |
| 253 | + |
| 254 | + |
| 255 | +fn word_parts_to_spans<'a>(parts: Vec<(String, Status)>, theme: &'a Theme) -> Vec<Span<'a>> { |
| 256 | + let mut spans = Vec::new(); |
| 257 | + for (text, status) in parts { |
| 258 | + let style = match status { |
| 259 | + Status::Correct => theme.prompt_correct, |
| 260 | + Status::Incorrect => theme.prompt_incorrect, |
| 261 | + Status::Untyped => theme.prompt_untyped, |
| 262 | + Status::CurrentUntyped => theme.prompt_current_untyped, |
| 263 | + Status::CurrentCorrect => theme.prompt_current_correct, |
| 264 | + Status::CurrentIncorrect => theme.prompt_current_incorrect, |
| 265 | + Status::Cursor => theme.prompt_current_untyped.patch(theme.prompt_cursor), |
| 266 | + Status::Overtyped => theme.prompt_incorrect, |
| 267 | + }; |
| 268 | + |
| 269 | + spans.push(Span::styled(text, style)); |
| 270 | + } |
| 271 | + spans.push(Span::styled(" ", theme.prompt_untyped)); |
| 272 | + spans |
| 273 | +} |
| 274 | + |
192 | 275 | impl ThemedWidget for &results::Results { |
193 | 276 | fn render(self, area: Rect, buf: &mut Buffer, theme: &Theme) { |
194 | 277 | buf.set_style(area, theme.default); |
@@ -337,3 +420,96 @@ fn ceil_char_boundary(string: &str, index: usize) -> usize { |
337 | 420 | ceil_char_boundary(string, index + 1) |
338 | 421 | } |
339 | 422 | } |
| 423 | + |
| 424 | +#[cfg(test)] |
| 425 | +mod test_ui { |
| 426 | + use super::*; |
| 427 | + |
| 428 | + mod split_words { |
| 429 | + use super::Status::*; |
| 430 | + use super::*; |
| 431 | + |
| 432 | + struct TestCase { |
| 433 | + word: &'static str, |
| 434 | + progress: &'static str, |
| 435 | + expected: Vec<(&'static str, Status)>, |
| 436 | + } |
| 437 | + |
| 438 | + fn setup(test_case: TestCase) -> (TestWord, Vec<(String, Status)>) { |
| 439 | + let mut word = TestWord::from(test_case.word); |
| 440 | + word.progress = test_case.progress.to_string(); |
| 441 | + |
| 442 | + let expected = test_case |
| 443 | + .expected |
| 444 | + .iter() |
| 445 | + .map(|(s, v)| (s.to_string(), v.clone())) |
| 446 | + .collect::<Vec<_>>(); |
| 447 | + |
| 448 | + (word, expected) |
| 449 | + } |
| 450 | + |
| 451 | + #[test] |
| 452 | + fn typed_words_split() { |
| 453 | + let cases = vec![ |
| 454 | + TestCase { |
| 455 | + word: "monkeytype", |
| 456 | + progress: "monkeytype", |
| 457 | + expected: vec![("monkeytype", Correct)], |
| 458 | + }, |
| 459 | + TestCase { |
| 460 | + word: "monkeytype", |
| 461 | + progress: "monkeXtype", |
| 462 | + expected: vec![("monke", Correct), ("y", Incorrect), ("type", Correct)], |
| 463 | + }, |
| 464 | + TestCase { |
| 465 | + word: "monkeytype", |
| 466 | + progress: "monkeas", |
| 467 | + expected: vec![("monke", Correct), ("yt", Incorrect), ("ype", Untyped)], |
| 468 | + }, |
| 469 | + ]; |
| 470 | + |
| 471 | + for case in cases { |
| 472 | + let (word, expected) = setup(case); |
| 473 | + let got = split_typed_word(&word); |
| 474 | + assert_eq!(got, expected); |
| 475 | + } |
| 476 | + } |
| 477 | + |
| 478 | + #[test] |
| 479 | + fn current_word_split() { |
| 480 | + let cases = vec![ |
| 481 | + TestCase { |
| 482 | + word: "monkeytype", |
| 483 | + progress: "monkeytype", |
| 484 | + expected: vec![("monkeytype", CurrentCorrect)], |
| 485 | + }, |
| 486 | + TestCase { |
| 487 | + word: "monkeytype", |
| 488 | + progress: "monke", |
| 489 | + expected: vec![ |
| 490 | + ("monke", CurrentCorrect), |
| 491 | + ("y", Cursor), |
| 492 | + ("type", CurrentUntyped), |
| 493 | + ], |
| 494 | + }, |
| 495 | + TestCase { |
| 496 | + word: "monkeytype", |
| 497 | + progress: "monkeXt", |
| 498 | + expected: vec![ |
| 499 | + ("monke", CurrentCorrect), |
| 500 | + ("y", CurrentIncorrect), |
| 501 | + ("t", CurrentCorrect), |
| 502 | + ("y", Cursor), |
| 503 | + ("pe", CurrentUntyped), |
| 504 | + ], |
| 505 | + }, |
| 506 | + ]; |
| 507 | + |
| 508 | + for case in cases { |
| 509 | + let (word, expected) = setup(case); |
| 510 | + let got = split_current_word(&word); |
| 511 | + assert_eq!(got, expected); |
| 512 | + } |
| 513 | + } |
| 514 | + } |
| 515 | +} |
0 commit comments