Consistently refer to drills as drills now, rename from lesson/practice
Also fix some issues in the stats screen.
This commit is contained in:
@@ -6,16 +6,16 @@ use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::{Block, Widget};
|
||||
|
||||
use crate::session::result::LessonResult;
|
||||
use crate::session::result::DrillResult;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
pub struct ActivityHeatmap<'a> {
|
||||
history: &'a [LessonResult],
|
||||
history: &'a [DrillResult],
|
||||
theme: &'a Theme,
|
||||
}
|
||||
|
||||
impl<'a> ActivityHeatmap<'a> {
|
||||
pub fn new(history: &'a [LessonResult], theme: &'a Theme) -> Self {
|
||||
pub fn new(history: &'a [DrillResult], theme: &'a Theme) -> Self {
|
||||
Self { history, theme }
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ impl Widget for ActivityHeatmap<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Activity ")
|
||||
.title(" Daily Activity (Sessions per Day) ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Widget for WpmChart<'_> {
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Lesson")
|
||||
.title("Drill #")
|
||||
.style(Style::default().fg(colors.text_pending()))
|
||||
.bounds([0.0, max_x]),
|
||||
)
|
||||
|
||||
@@ -4,16 +4,16 @@ use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget};
|
||||
|
||||
use crate::session::result::LessonResult;
|
||||
use crate::session::result::DrillResult;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
pub struct Dashboard<'a> {
|
||||
pub result: &'a LessonResult,
|
||||
pub result: &'a DrillResult,
|
||||
pub theme: &'a Theme,
|
||||
}
|
||||
|
||||
impl<'a> Dashboard<'a> {
|
||||
pub fn new(result: &'a LessonResult, theme: &'a Theme) -> Self {
|
||||
pub fn new(result: &'a DrillResult, theme: &'a Theme) -> Self {
|
||||
Self { result, theme }
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ impl Widget for Dashboard<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Lesson Complete ")
|
||||
.title(" Drill Complete ")
|
||||
.border_style(Style::default().fg(colors.accent()))
|
||||
.style(Style::default().bg(colors.bg()));
|
||||
let inner = block.inner(area);
|
||||
|
||||
@@ -24,17 +24,17 @@ impl<'a> Menu<'a> {
|
||||
items: vec![
|
||||
MenuItem {
|
||||
key: "1".to_string(),
|
||||
label: "Adaptive Practice".to_string(),
|
||||
label: "Adaptive Drill".to_string(),
|
||||
description: "Phonetic words with adaptive letter unlocking".to_string(),
|
||||
},
|
||||
MenuItem {
|
||||
key: "2".to_string(),
|
||||
label: "Code Practice".to_string(),
|
||||
label: "Code Drill".to_string(),
|
||||
description: "Practice typing code syntax".to_string(),
|
||||
},
|
||||
MenuItem {
|
||||
key: "3".to_string(),
|
||||
label: "Passage Mode".to_string(),
|
||||
label: "Passage Drill".to_string(),
|
||||
description: "Type passages from books".to_string(),
|
||||
},
|
||||
MenuItem {
|
||||
|
||||
@@ -5,12 +5,12 @@ use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget};
|
||||
|
||||
use crate::engine::key_stats::KeyStatsStore;
|
||||
use crate::session::result::LessonResult;
|
||||
use crate::session::result::DrillResult;
|
||||
use crate::ui::components::activity_heatmap::ActivityHeatmap;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
pub struct StatsDashboard<'a> {
|
||||
pub history: &'a [LessonResult],
|
||||
pub history: &'a [DrillResult],
|
||||
pub key_stats: &'a KeyStatsStore,
|
||||
pub active_tab: usize,
|
||||
pub target_wpm: u32,
|
||||
@@ -21,7 +21,7 @@ pub struct StatsDashboard<'a> {
|
||||
|
||||
impl<'a> StatsDashboard<'a> {
|
||||
pub fn new(
|
||||
history: &'a [LessonResult],
|
||||
history: &'a [DrillResult],
|
||||
key_stats: &'a KeyStatsStore,
|
||||
active_tab: usize,
|
||||
target_wpm: u32,
|
||||
@@ -54,7 +54,7 @@ impl Widget for StatsDashboard<'_> {
|
||||
|
||||
if self.history.is_empty() {
|
||||
let msg = Paragraph::new(Line::from(Span::styled(
|
||||
"No lessons completed yet. Start typing!",
|
||||
"No drills completed yet. Start typing!",
|
||||
Style::default().fg(colors.text_pending()),
|
||||
)));
|
||||
msg.render(inner, buf);
|
||||
@@ -71,7 +71,7 @@ impl Widget for StatsDashboard<'_> {
|
||||
.split(inner);
|
||||
|
||||
// Tab header
|
||||
let tabs = ["[D] Dashboard", "[H] History", "[K] Keystrokes"];
|
||||
let tabs = ["[1] Dashboard", "[2] History", "[3] Keystrokes"];
|
||||
let tab_spans: Vec<Span> = tabs
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -113,7 +113,7 @@ impl Widget for StatsDashboard<'_> {
|
||||
let footer_text = if self.active_tab == 1 {
|
||||
" [ESC] Back [Tab] Next tab [j/k] Navigate [x] Delete"
|
||||
} else {
|
||||
" [ESC] Back [Tab] Next tab [D/H/K] Switch tab"
|
||||
" [ESC] Back [Tab] Next tab [1/2/3] Switch tab"
|
||||
};
|
||||
let footer = Paragraph::new(Line::from(Span::styled(
|
||||
footer_text,
|
||||
@@ -198,7 +198,7 @@ impl StatsDashboard<'_> {
|
||||
|
||||
let summary = vec![
|
||||
Line::from(vec![
|
||||
Span::styled(" Lessons: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(" Drills: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(
|
||||
&*total_str,
|
||||
Style::default()
|
||||
@@ -249,8 +249,9 @@ impl StatsDashboard<'_> {
|
||||
fn render_wpm_bar_graph(&self, area: Rect, buf: &mut Buffer) {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let target_label = format!(" WPM per Drill (Last 20, Target: {}) ", self.target_wpm);
|
||||
let block = Block::bordered()
|
||||
.title(" WPM (Last 20) ")
|
||||
.title(target_label)
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
@@ -276,20 +277,40 @@ impl StatsDashboard<'_> {
|
||||
|
||||
let max_wpm = recent.iter().fold(0.0f64, |a, &b| a.max(b)).max(10.0);
|
||||
let target = self.target_wpm as f64;
|
||||
let bar_count = (inner.width as usize).min(recent.len());
|
||||
|
||||
// Reserve left margin for Y-axis labels
|
||||
let y_label_width: u16 = 4;
|
||||
let chart_x = inner.x + y_label_width;
|
||||
let chart_width = inner.width.saturating_sub(y_label_width);
|
||||
|
||||
if chart_width < 5 {
|
||||
return;
|
||||
}
|
||||
|
||||
let bar_count = (chart_width as usize).min(recent.len());
|
||||
let bar_spacing = if bar_count > 0 {
|
||||
inner.width / bar_count as u16
|
||||
chart_width / bar_count as u16
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Y-axis labels (max, mid, 0)
|
||||
let max_label = format!("{:.0}", max_wpm);
|
||||
let mid_label = format!("{:.0}", max_wpm / 2.0);
|
||||
buf.set_string(inner.x, inner.y, &max_label, Style::default().fg(colors.text_pending()));
|
||||
if inner.height > 3 {
|
||||
let mid_y = inner.y + inner.height / 2;
|
||||
buf.set_string(inner.x, mid_y, &mid_label, Style::default().fg(colors.text_pending()));
|
||||
}
|
||||
buf.set_string(inner.x, inner.y + inner.height - 1, "0", Style::default().fg(colors.text_pending()));
|
||||
|
||||
let bar_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
|
||||
// Render each bar as a column
|
||||
let start_idx = recent.len().saturating_sub(bar_count);
|
||||
for (i, &wpm) in recent[start_idx..].iter().enumerate() {
|
||||
let x = inner.x + i as u16 * bar_spacing;
|
||||
if x >= inner.x + inner.width {
|
||||
let x = chart_x + i as u16 * bar_spacing;
|
||||
if x >= chart_x + chart_width {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -343,7 +364,7 @@ impl StatsDashboard<'_> {
|
||||
|
||||
if data.is_empty() {
|
||||
let block = Block::bordered()
|
||||
.title(" Accuracy Trend ")
|
||||
.title(" Accuracy % (Last 50 Drills) ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
block.render(area, buf);
|
||||
return;
|
||||
@@ -360,18 +381,18 @@ impl StatsDashboard<'_> {
|
||||
let chart = Chart::new(vec![dataset])
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title(" Accuracy Trend ")
|
||||
.title(" Accuracy % (Last 50 Drills) ")
|
||||
.border_style(Style::default().fg(colors.border())),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Lesson")
|
||||
.title("Drill #")
|
||||
.style(Style::default().fg(colors.text_pending()))
|
||||
.bounds([0.0, max_x]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("%")
|
||||
.title("Accuracy %")
|
||||
.style(Style::default().fg(colors.text_pending()))
|
||||
.bounds([80.0, 100.0]),
|
||||
);
|
||||
@@ -481,7 +502,7 @@ impl StatsDashboard<'_> {
|
||||
)),
|
||||
];
|
||||
|
||||
let recent: Vec<&LessonResult> = self.history.iter().rev().take(20).collect();
|
||||
let recent: Vec<&DrillResult> = self.history.iter().rev().take(20).collect();
|
||||
let total = self.history.len();
|
||||
|
||||
for (i, result) in recent.iter().enumerate() {
|
||||
@@ -534,16 +555,21 @@ impl StatsDashboard<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Character Speed Distribution ")
|
||||
.title(" Avg Key Time by Character ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
if inner.width < 52 || inner.height < 2 {
|
||||
let columns_per_row: usize = 13;
|
||||
let col_width: u16 = 4;
|
||||
let row_height: u16 = 3;
|
||||
|
||||
if inner.width < columns_per_row as u16 * col_width || inner.height < row_height {
|
||||
return;
|
||||
}
|
||||
|
||||
let letters: Vec<char> = ('a'..='z').collect();
|
||||
let row_count = if inner.height >= row_height * 2 { 2 } else { 1 };
|
||||
let max_time = letters
|
||||
.iter()
|
||||
.filter_map(|&ch| self.key_stats.stats.get(&ch))
|
||||
@@ -553,9 +579,13 @@ impl StatsDashboard<'_> {
|
||||
|
||||
let bar_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
|
||||
for (i, &ch) in letters.iter().enumerate() {
|
||||
let x = inner.x + (i as u16 * 2).min(inner.width.saturating_sub(1));
|
||||
if x >= inner.x + inner.width {
|
||||
for (i, &ch) in letters.iter().take(columns_per_row * row_count).enumerate() {
|
||||
let row = i / columns_per_row;
|
||||
let col = i % columns_per_row;
|
||||
let x = inner.x + (col as u16 * col_width);
|
||||
let y = inner.y + row as u16 * row_height;
|
||||
|
||||
if x + col_width > inner.x + inner.width || y + 2 >= inner.y + inner.height {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -576,35 +606,37 @@ impl StatsDashboard<'_> {
|
||||
};
|
||||
|
||||
// Letter label
|
||||
buf.set_string(x, inner.y, &ch.to_string(), Style::default().fg(color));
|
||||
buf.set_string(x, y, &ch.to_string(), Style::default().fg(color));
|
||||
|
||||
// Bar indicator
|
||||
if inner.height >= 2 {
|
||||
let bar_char = if time > 0.0 {
|
||||
let idx = ((ratio * 7.0).round() as usize).min(7);
|
||||
bar_chars[idx]
|
||||
} else {
|
||||
' '
|
||||
};
|
||||
buf.set_string(
|
||||
x,
|
||||
inner.y + 1,
|
||||
&bar_char.to_string(),
|
||||
Style::default().fg(color),
|
||||
);
|
||||
}
|
||||
let bar_char = if time > 0.0 {
|
||||
let idx = ((ratio * 7.0).round() as usize).min(7);
|
||||
bar_chars[idx]
|
||||
} else {
|
||||
' '
|
||||
};
|
||||
buf.set_string(x, y + 1, &bar_char.to_string(), Style::default().fg(color));
|
||||
|
||||
// Time label on row 3
|
||||
if inner.height >= 3 && time > 0.0 {
|
||||
let time_label = format!("{time:.0}");
|
||||
if x + time_label.len() as u16 <= inner.x + inner.width {
|
||||
buf.set_string(
|
||||
x,
|
||||
inner.y + 2,
|
||||
&time_label,
|
||||
Style::default().fg(colors.text_pending()),
|
||||
);
|
||||
}
|
||||
// Time label on row 3, render seconds when value exceeds 999ms.
|
||||
if time > 0.0 {
|
||||
let time_label = if time > 999.0 {
|
||||
format!("({:.0}s)", time / 1000.0)
|
||||
} else {
|
||||
format!("{time:.0}")
|
||||
};
|
||||
let label = if time_label.len() > col_width as usize {
|
||||
let start = time_label.len() - col_width as usize;
|
||||
&time_label[start..]
|
||||
} else {
|
||||
&time_label
|
||||
};
|
||||
let label_x = x + col_width.saturating_sub(label.len() as u16);
|
||||
buf.set_string(
|
||||
label_x,
|
||||
y + 2,
|
||||
label,
|
||||
Style::default().fg(colors.text_pending()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,7 +681,7 @@ impl StatsDashboard<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Keyboard Accuracy ")
|
||||
.title(" Keyboard Accuracy % ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
@@ -732,7 +764,7 @@ impl StatsDashboard<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Slowest ")
|
||||
.title(" Slowest Keys (ms) ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
@@ -765,7 +797,7 @@ impl StatsDashboard<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Fastest ")
|
||||
.title(" Fastest Keys (ms) ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
@@ -798,7 +830,7 @@ impl StatsDashboard<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Worst Accuracy ")
|
||||
.title(" Worst Accuracy Keys (%) ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
@@ -858,7 +890,7 @@ impl StatsDashboard<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Overall ")
|
||||
.title(" Overall Totals ")
|
||||
.border_style(Style::default().fg(colors.border()));
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
@@ -4,19 +4,36 @@ use ratatui::style::Style;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget};
|
||||
|
||||
use crate::session::lesson::LessonState;
|
||||
use crate::session::result::LessonResult;
|
||||
use crate::session::drill::DrillState;
|
||||
use crate::session::result::DrillResult;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
pub struct StatsSidebar<'a> {
|
||||
lesson: &'a LessonState,
|
||||
last_result: Option<&'a LessonResult>,
|
||||
drill: &'a DrillState,
|
||||
last_result: Option<&'a DrillResult>,
|
||||
history: &'a [DrillResult],
|
||||
theme: &'a Theme,
|
||||
}
|
||||
|
||||
impl<'a> StatsSidebar<'a> {
|
||||
pub fn new(lesson: &'a LessonState, last_result: Option<&'a LessonResult>, theme: &'a Theme) -> Self {
|
||||
Self { lesson, last_result, theme }
|
||||
pub fn new(
|
||||
drill: &'a DrillState,
|
||||
last_result: Option<&'a DrillResult>,
|
||||
history: &'a [DrillResult],
|
||||
theme: &'a Theme,
|
||||
) -> Self {
|
||||
Self { drill, last_result, history, theme }
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a delta value with arrow indicator
|
||||
fn format_delta(delta: f64, suffix: &str) -> String {
|
||||
if delta > 0.0 {
|
||||
format!("\u{2191}+{:.1}{suffix}", delta)
|
||||
} else if delta < 0.0 {
|
||||
format!("\u{2193}{:.1}{suffix}", delta)
|
||||
} else {
|
||||
format!("={suffix}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +43,11 @@ impl Widget for StatsSidebar<'_> {
|
||||
|
||||
let has_last = self.last_result.is_some();
|
||||
|
||||
// Split sidebar into current stats and last lesson sections
|
||||
// Split sidebar into current stats and last drill sections
|
||||
let sections = if has_last {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(10), Constraint::Min(10)])
|
||||
.constraints([Constraint::Min(10), Constraint::Min(12)])
|
||||
.split(area)
|
||||
} else {
|
||||
Layout::default()
|
||||
@@ -39,14 +56,14 @@ impl Widget for StatsSidebar<'_> {
|
||||
.split(area)
|
||||
};
|
||||
|
||||
// Current lesson stats
|
||||
// Current drill stats
|
||||
{
|
||||
let wpm = self.lesson.wpm();
|
||||
let accuracy = self.lesson.accuracy();
|
||||
let progress = self.lesson.progress() * 100.0;
|
||||
let correct = self.lesson.correct_count();
|
||||
let incorrect = self.lesson.typo_count();
|
||||
let elapsed = self.lesson.elapsed_secs();
|
||||
let wpm = self.drill.wpm();
|
||||
let accuracy = self.drill.accuracy();
|
||||
let progress = self.drill.progress() * 100.0;
|
||||
let correct = self.drill.correct_count();
|
||||
let incorrect = self.drill.typo_count();
|
||||
let elapsed = self.drill.elapsed_secs();
|
||||
|
||||
let wpm_str = format!("{wpm:.0}");
|
||||
let acc_str = format!("{accuracy:.1}%");
|
||||
@@ -104,51 +121,94 @@ impl Widget for StatsSidebar<'_> {
|
||||
paragraph.render(sections[0], buf);
|
||||
}
|
||||
|
||||
// Last lesson stats
|
||||
// Last drill stats with session impact deltas
|
||||
if let Some(last) = self.last_result {
|
||||
let wpm_str = format!("{:.0}", last.wpm);
|
||||
let acc_str = format!("{:.1}%", last.accuracy);
|
||||
let chars_str = format!("{}", last.total_chars);
|
||||
let time_str = format!("{:.1}s", last.elapsed_secs);
|
||||
let errors_str = format!("{}", last.incorrect);
|
||||
|
||||
let lines = vec![
|
||||
// Compute deltas: compare last drill to the average of all prior drills
|
||||
// (excluding the last one which is the current result)
|
||||
let prior_count = self.history.len().saturating_sub(1);
|
||||
let (wpm_delta, acc_delta) = if prior_count > 0 {
|
||||
let prior = &self.history[..prior_count];
|
||||
let avg_wpm = prior.iter().map(|r| r.wpm).sum::<f64>() / prior.len() as f64;
|
||||
let avg_acc = prior.iter().map(|r| r.accuracy).sum::<f64>() / prior.len() as f64;
|
||||
(last.wpm - avg_wpm, last.accuracy - avg_acc)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
let wpm_delta_str = format_delta(wpm_delta, "");
|
||||
let acc_delta_str = format_delta(acc_delta, "%");
|
||||
|
||||
let wpm_delta_color = if wpm_delta > 0.0 {
|
||||
colors.success()
|
||||
} else if wpm_delta < 0.0 {
|
||||
colors.error()
|
||||
} else {
|
||||
colors.text_pending()
|
||||
};
|
||||
|
||||
let acc_delta_color = if acc_delta > 0.0 {
|
||||
colors.success()
|
||||
} else if acc_delta < 0.0 {
|
||||
colors.error()
|
||||
} else {
|
||||
colors.text_pending()
|
||||
};
|
||||
|
||||
let mut lines = vec![
|
||||
Line::from(vec![
|
||||
Span::styled("WPM: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(wpm_str, Style::default().fg(colors.accent())),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("Accuracy: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(
|
||||
acc_str,
|
||||
Style::default().fg(if last.accuracy >= 95.0 {
|
||||
colors.success()
|
||||
} else if last.accuracy >= 85.0 {
|
||||
colors.warning()
|
||||
} else {
|
||||
colors.error()
|
||||
}),
|
||||
),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("Chars: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(chars_str, Style::default().fg(colors.fg())),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Errors: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(errors_str, Style::default().fg(colors.error())),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("Time: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(time_str, Style::default().fg(colors.fg())),
|
||||
]),
|
||||
];
|
||||
|
||||
if prior_count > 0 {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" vs avg: ", Style::default().fg(colors.text_pending())),
|
||||
Span::styled(wpm_delta_str, Style::default().fg(wpm_delta_color)),
|
||||
]));
|
||||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Accuracy: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(
|
||||
acc_str,
|
||||
Style::default().fg(if last.accuracy >= 95.0 {
|
||||
colors.success()
|
||||
} else if last.accuracy >= 85.0 {
|
||||
colors.warning()
|
||||
} else {
|
||||
colors.error()
|
||||
}),
|
||||
),
|
||||
]));
|
||||
|
||||
if prior_count > 0 {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" vs avg: ", Style::default().fg(colors.text_pending())),
|
||||
Span::styled(acc_delta_str, Style::default().fg(acc_delta_color)),
|
||||
]));
|
||||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Errors: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(errors_str, Style::default().fg(colors.error())),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Time: ", Style::default().fg(colors.fg())),
|
||||
Span::styled(time_str, Style::default().fg(colors.fg())),
|
||||
]));
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Last Lesson ")
|
||||
.title(" Last Drill ")
|
||||
.border_style(Style::default().fg(colors.border()))
|
||||
.style(Style::default().bg(colors.bg()));
|
||||
|
||||
|
||||
@@ -5,17 +5,17 @@ use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget, Wrap};
|
||||
|
||||
use crate::session::input::CharStatus;
|
||||
use crate::session::lesson::LessonState;
|
||||
use crate::session::drill::DrillState;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
pub struct TypingArea<'a> {
|
||||
lesson: &'a LessonState,
|
||||
drill: &'a DrillState,
|
||||
theme: &'a Theme,
|
||||
}
|
||||
|
||||
impl<'a> TypingArea<'a> {
|
||||
pub fn new(lesson: &'a LessonState, theme: &'a Theme) -> Self {
|
||||
Self { lesson, theme }
|
||||
pub fn new(drill: &'a DrillState, theme: &'a Theme) -> Self {
|
||||
Self { drill, theme }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,21 +24,21 @@ impl Widget for TypingArea<'_> {
|
||||
let colors = &self.theme.colors;
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
|
||||
for (i, &target_ch) in self.lesson.target.iter().enumerate() {
|
||||
if i < self.lesson.cursor {
|
||||
let style = match &self.lesson.input[i] {
|
||||
for (i, &target_ch) in self.drill.target.iter().enumerate() {
|
||||
if i < self.drill.cursor {
|
||||
let style = match &self.drill.input[i] {
|
||||
CharStatus::Correct => Style::default().fg(colors.text_correct()),
|
||||
CharStatus::Incorrect(_) => Style::default()
|
||||
.fg(colors.text_incorrect())
|
||||
.bg(colors.text_incorrect_bg())
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
};
|
||||
let display = match &self.lesson.input[i] {
|
||||
let display = match &self.drill.input[i] {
|
||||
CharStatus::Incorrect(actual) => *actual,
|
||||
_ => target_ch,
|
||||
};
|
||||
spans.push(Span::styled(display.to_string(), style));
|
||||
} else if i == self.lesson.cursor {
|
||||
} else if i == self.drill.cursor {
|
||||
let style = Style::default()
|
||||
.fg(colors.text_cursor_fg())
|
||||
.bg(colors.text_cursor_bg());
|
||||
|
||||
Reference in New Issue
Block a user