use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::Modifier;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::stats::TestResult;
use crate::ui::theme::Theme;
#[derive(Debug, Clone)]
pub struct ResultsState;
impl Default for ResultsState {
fn default() -> Self {
Self
}
}
pub fn render(
f: &mut Frame,
result: &TestResult,
_state: &ResultsState,
theme: &Theme,
area: Rect,
) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(3),
])
.split(area);
render_title(f, result, theme, chunks[0]);
render_main_results(f, result, theme, chunks[1]);
render_help(f, theme, chunks[2]);
}
fn render_title(f: &mut Frame, result: &TestResult, theme: &Theme, area: Rect) {
let title = Paragraph::new(vec![
Line::from(Span::styled(
" test complete! ",
theme.style_accent().add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
format!("{} mode • {} • {}s", result.mode.to_lowercase(), result.language, result.duration),
theme.style_muted(),
)),
])
.alignment(Alignment::Center);
f.render_widget(title, area);
}
fn render_main_results(f: &mut Frame, result: &TestResult, theme: &Theme, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(area);
let wpm_row = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
render_big_stat(
f,
"wpm",
&format!("{:.0}", result.wpm),
theme.style(theme.success),
theme,
wpm_row[0],
);
render_big_stat(
f,
"raw wpm",
&format!("{:.0}", result.raw_wpm),
theme.style(theme.accent),
theme,
wpm_row[1],
);
let acc_row = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
let accuracy_style = if result.accuracy >= 95.0 {
theme.style(theme.success)
} else if result.accuracy >= 85.0 {
theme.style(theme.warning)
} else {
theme.style(theme.error)
};
render_big_stat(
f,
"accuracy",
&format!("{:.1}%", result.accuracy),
accuracy_style,
theme,
acc_row[0],
);
render_big_stat(
f,
"errors",
&format!("{}", result.errors),
theme.style(theme.error),
theme,
acc_row[1],
);
let char_row = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[2]);
render_big_stat(
f,
"characters",
&format!("{}", result.total_chars),
theme.style(theme.muted),
theme,
char_row[0],
);
render_big_stat(
f,
"duration",
&format!("{}s", result.duration),
theme.style(theme.muted),
theme,
char_row[1],
);
}
fn render_big_stat(
f: &mut Frame,
label: &str,
value: &str,
value_style: ratatui::style::Style,
theme: &Theme,
area: Rect,
) {
let text = vec![
Line::from(vec![Span::styled(
value,
value_style.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(label, theme.style_muted())]),
];
let para = Paragraph::new(text).alignment(Alignment::Center);
f.render_widget(para, area);
}
fn render_help(f: &mut Frame, theme: &Theme, area: Rect) {
let help_text = vec![
Span::styled("tab/r", theme.style_accent()),
Span::styled(" restart • ", theme.style_muted()),
Span::styled("ctrl+p", theme.style_accent()),
Span::styled(" profile • ", theme.style_muted()),
Span::styled("esc", theme.style_accent()),
Span::styled(" menu", theme.style_muted()),
];
let help = Paragraph::new(Line::from(help_text)).alignment(Alignment::Center);
f.render_widget(help, area);
}