Files
keydr/src/ui/components/menu.rs

172 lines
5.2 KiB
Rust

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::ui::theme::Theme;
pub struct MenuItem {
pub key: String,
pub label: String,
pub description: String,
}
pub struct Menu<'a> {
pub items: Vec<MenuItem>,
pub selected: usize,
pub theme: &'a Theme,
}
impl<'a> Menu<'a> {
pub fn new(theme: &'a Theme) -> Self {
Self {
items: vec![
MenuItem {
key: "1".to_string(),
label: "Adaptive Drill".to_string(),
description: "Phonetic words with adaptive letter unlocking".to_string(),
},
MenuItem {
key: "2".to_string(),
label: "Code Drill".to_string(),
description: "Practice typing code syntax".to_string(),
},
MenuItem {
key: "3".to_string(),
label: "Passage Drill".to_string(),
description: "Type passages from books".to_string(),
},
MenuItem {
key: "t".to_string(),
label: "Skill Tree".to_string(),
description: "View progression branches and launch drills".to_string(),
},
MenuItem {
key: "s".to_string(),
label: "Statistics".to_string(),
description: "View your typing statistics".to_string(),
},
MenuItem {
key: "c".to_string(),
label: "Settings".to_string(),
description: "Configure keydr".to_string(),
},
],
selected: 0,
theme,
}
}
pub fn next(&mut self) {
self.selected = (self.selected + 1) % self.items.len();
}
pub fn prev(&mut self) {
if self.selected > 0 {
self.selected -= 1;
} else {
self.selected = self.items.len() - 1;
}
}
}
impl Widget for &Menu<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = &self.theme.colors;
let block = Block::bordered()
.border_style(Style::default().fg(colors.border()))
.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(5),
Constraint::Length(1),
Constraint::Min(0),
])
.split(inner);
let title_lines = vec![
Line::from(""),
Line::from(Span::styled(
"keydr",
Style::default()
.fg(colors.accent())
.add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
"Terminal Typing Tutor",
Style::default().fg(colors.fg()),
)),
Line::from(""),
];
let title = Paragraph::new(title_lines).alignment(Alignment::Center);
title.render(layout[0], buf);
let menu_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
self.items
.iter()
.map(|_| Constraint::Length(3))
.collect::<Vec<_>>(),
)
.split(layout[2]);
let key_width = self
.items
.iter()
.map(|item| item.key.len())
.max()
.unwrap_or(1);
for (i, item) in self.items.iter().enumerate() {
let is_selected = i == self.selected;
let indicator = if is_selected { ">" } else { " " };
let label_text = format!(
" {indicator} [{key:<key_width$}] {label}",
key = item.key,
key_width = key_width,
label = item.label
);
let desc_text = format!(
" {:indent$}{}",
"",
item.description,
indent = key_width + 4
);
let lines = vec![
Line::from(Span::styled(
&*label_text,
Style::default()
.fg(if is_selected {
colors.accent()
} else {
colors.fg()
})
.add_modifier(if is_selected {
Modifier::BOLD
} else {
Modifier::empty()
}),
)),
Line::from(Span::styled(
&*desc_text,
Style::default().fg(colors.text_pending()),
)),
];
let p = Paragraph::new(lines);
if i < menu_layout.len() {
p.render(menu_layout[i], buf);
}
}
}
}