use ratatui::buffer::Buffer; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Widget}; use crate::session::result::DrillResult; use crate::ui::theme::Theme; pub struct Dashboard<'a> { pub result: &'a DrillResult, pub theme: &'a Theme, } impl<'a> Dashboard<'a> { pub fn new(result: &'a DrillResult, theme: &'a Theme) -> Self { Self { result, theme } } } impl Widget for Dashboard<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let colors = &self.theme.colors; let block = Block::bordered() .title(" Drill Complete ") .border_style(Style::default().fg(colors.accent())) .style(Style::default().bg(colors.bg())); let inner = block.inner(area); block.render(area, buf); let layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(2), Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), Constraint::Min(0), Constraint::Length(2), ]) .split(inner); let title = Paragraph::new(Line::from(Span::styled( "Results", Style::default() .fg(colors.accent()) .add_modifier(Modifier::BOLD), ))) .alignment(Alignment::Center); title.render(layout[0], buf); let wpm_text = format!("{:.0} WPM", self.result.wpm); let cpm_text = format!(" ({:.0} CPM)", self.result.cpm); let wpm_line = Line::from(vec![ Span::styled(" Speed: ", Style::default().fg(colors.fg())), Span::styled( &*wpm_text, Style::default() .fg(colors.accent()) .add_modifier(Modifier::BOLD), ), Span::styled(&*cpm_text, Style::default().fg(colors.text_pending())), ]); Paragraph::new(wpm_line).render(layout[1], buf); let acc_color = if self.result.accuracy >= 95.0 { colors.success() } else if self.result.accuracy >= 85.0 { colors.warning() } else { colors.error() }; let acc_text = format!("{:.1}%", self.result.accuracy); let acc_detail = format!( " ({}/{} correct)", self.result.correct, self.result.total_chars ); let acc_line = Line::from(vec![ Span::styled(" Accuracy: ", Style::default().fg(colors.fg())), Span::styled( &*acc_text, Style::default().fg(acc_color).add_modifier(Modifier::BOLD), ), Span::styled(&*acc_detail, Style::default().fg(colors.text_pending())), ]); Paragraph::new(acc_line).render(layout[2], buf); let time_text = format!("{:.1}s", self.result.elapsed_secs); let time_line = Line::from(vec![ Span::styled(" Time: ", Style::default().fg(colors.fg())), Span::styled(&*time_text, Style::default().fg(colors.fg())), ]); Paragraph::new(time_line).render(layout[3], buf); let error_text = format!("{}", self.result.incorrect); let chars_line = Line::from(vec![ Span::styled(" Errors: ", Style::default().fg(colors.fg())), Span::styled( &*error_text, Style::default().fg(if self.result.incorrect == 0 { colors.success() } else { colors.error() }), ), ]); Paragraph::new(chars_line).render(layout[4], buf); let help = Paragraph::new(Line::from(vec![ Span::styled(" [r] Retry ", Style::default().fg(colors.accent())), Span::styled("[q] Menu ", Style::default().fg(colors.accent())), Span::styled("[s] Stats", Style::default().fg(colors.accent())), ])); help.render(layout[6], buf); } }