Fix some theme colors & drill summary delete continue
This commit is contained in:
@@ -385,6 +385,7 @@ fn handle_result_key(app: &mut App, key: KeyEvent) {
|
|||||||
KeyCode::Char('y') => {
|
KeyCode::Char('y') => {
|
||||||
app.delete_session();
|
app.delete_session();
|
||||||
app.history_confirm_delete = false;
|
app.history_confirm_delete = false;
|
||||||
|
app.continue_drill();
|
||||||
}
|
}
|
||||||
KeyCode::Char('n') | KeyCode::Esc => {
|
KeyCode::Char('n') | KeyCode::Esc => {
|
||||||
app.history_confirm_delete = false;
|
app.history_confirm_delete = false;
|
||||||
@@ -1584,6 +1585,8 @@ mod review_tests {
|
|||||||
assert!(!app.history_confirm_delete);
|
assert!(!app.history_confirm_delete);
|
||||||
assert_eq!(app.drill_history.len(), 1);
|
assert_eq!(app.drill_history.len(), 1);
|
||||||
assert_eq!(app.drill_history[0].timestamp, older.timestamp);
|
assert_eq!(app.drill_history[0].timestamp, older.timestamp);
|
||||||
|
assert_eq!(app.screen, AppScreen::Drill);
|
||||||
|
assert!(app.drill.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1598,6 +1601,7 @@ mod review_tests {
|
|||||||
|
|
||||||
assert!(!app.history_confirm_delete);
|
assert!(!app.history_confirm_delete);
|
||||||
assert_eq!(app.drill_history.len(), 2);
|
assert_eq!(app.drill_history.len(), 2);
|
||||||
|
assert_eq!(app.screen, AppScreen::DrillResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -84,6 +84,95 @@ fn brighten_color(color: Color) -> Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn color_to_rgb(color: Color) -> (u8, u8, u8) {
|
||||||
|
match color {
|
||||||
|
Color::Reset => (0, 0, 0),
|
||||||
|
Color::Black => (0, 0, 0),
|
||||||
|
Color::Red => (205, 49, 49),
|
||||||
|
Color::Green => (13, 188, 121),
|
||||||
|
Color::Yellow => (229, 229, 16),
|
||||||
|
Color::Blue => (36, 114, 200),
|
||||||
|
Color::Magenta => (188, 63, 188),
|
||||||
|
Color::Cyan => (17, 168, 205),
|
||||||
|
Color::Gray => (229, 229, 229),
|
||||||
|
Color::DarkGray => (102, 102, 102),
|
||||||
|
Color::LightRed => (241, 76, 76),
|
||||||
|
Color::LightGreen => (35, 209, 139),
|
||||||
|
Color::LightYellow => (245, 245, 67),
|
||||||
|
Color::LightBlue => (59, 142, 234),
|
||||||
|
Color::LightMagenta => (214, 112, 214),
|
||||||
|
Color::LightCyan => (41, 184, 219),
|
||||||
|
Color::White => (255, 255, 255),
|
||||||
|
Color::Rgb(r, g, b) => (r, g, b),
|
||||||
|
Color::Indexed(i) => {
|
||||||
|
if i < 16 {
|
||||||
|
const ANSI16: [(u8, u8, u8); 16] = [
|
||||||
|
(0, 0, 0),
|
||||||
|
(128, 0, 0),
|
||||||
|
(0, 128, 0),
|
||||||
|
(128, 128, 0),
|
||||||
|
(0, 0, 128),
|
||||||
|
(128, 0, 128),
|
||||||
|
(0, 128, 128),
|
||||||
|
(192, 192, 192),
|
||||||
|
(128, 128, 128),
|
||||||
|
(255, 0, 0),
|
||||||
|
(0, 255, 0),
|
||||||
|
(255, 255, 0),
|
||||||
|
(0, 0, 255),
|
||||||
|
(255, 0, 255),
|
||||||
|
(0, 255, 255),
|
||||||
|
(255, 255, 255),
|
||||||
|
];
|
||||||
|
ANSI16[i as usize]
|
||||||
|
} else if i <= 231 {
|
||||||
|
let idx = i - 16;
|
||||||
|
let r = idx / 36;
|
||||||
|
let g = (idx % 36) / 6;
|
||||||
|
let b = idx % 6;
|
||||||
|
let cv = |n: u8| if n == 0 { 0 } else { 55 + n * 40 };
|
||||||
|
(cv(r), cv(g), cv(b))
|
||||||
|
} else {
|
||||||
|
let v = 8 + (i - 232) * 10;
|
||||||
|
(v, v, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_luminance(color: Color) -> f64 {
|
||||||
|
let (r, g, b) = color_to_rgb(color);
|
||||||
|
let to_linear = |c: u8| {
|
||||||
|
let x = c as f64 / 255.0;
|
||||||
|
if x <= 0.03928 {
|
||||||
|
x / 12.92
|
||||||
|
} else {
|
||||||
|
((x + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
0.2126 * to_linear(r) + 0.7152 * to_linear(g) + 0.0722 * to_linear(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contrast_ratio(a: Color, b: Color) -> f64 {
|
||||||
|
let la = relative_luminance(a);
|
||||||
|
let lb = relative_luminance(b);
|
||||||
|
let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
|
||||||
|
(hi + 0.05) / (lo + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readable_fg(bg: Color, preferred: Color) -> Color {
|
||||||
|
let mut best = preferred;
|
||||||
|
let mut best_ratio = contrast_ratio(preferred, bg);
|
||||||
|
for candidate in [Color::White, Color::Black] {
|
||||||
|
let ratio = contrast_ratio(candidate, bg);
|
||||||
|
if ratio > best_ratio {
|
||||||
|
best = candidate;
|
||||||
|
best_ratio = ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
/// Blend a color toward the background at the given ratio (0.0 = full bg, 1.0 = full color).
|
/// Blend a color toward the background at the given ratio (0.0 = full bg, 1.0 = full color).
|
||||||
fn blend_toward_bg(color: Color, bg: Color, ratio: f32) -> Color {
|
fn blend_toward_bg(color: Color, bg: Color, ratio: f32) -> Color {
|
||||||
match (color, bg) {
|
match (color, bg) {
|
||||||
@@ -105,17 +194,17 @@ fn modifier_key_style(
|
|||||||
colors: &crate::ui::theme::ThemeColors,
|
colors: &crate::ui::theme::ThemeColors,
|
||||||
) -> Style {
|
) -> Style {
|
||||||
if is_depressed {
|
if is_depressed {
|
||||||
|
let bg = brighten_color(colors.accent_dim());
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::White)
|
.fg(readable_fg(bg, colors.fg()))
|
||||||
.bg(brighten_color(colors.accent_dim()))
|
.bg(bg)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if is_next {
|
} else if is_next {
|
||||||
let bg = blend_toward_bg(colors.accent(), colors.bg(), 0.35);
|
let bg = blend_toward_bg(colors.accent(), colors.bg(), 0.35);
|
||||||
Style::default().fg(colors.accent()).bg(bg)
|
Style::default().fg(readable_fg(bg, colors.accent())).bg(bg)
|
||||||
} else if is_selected {
|
} else if is_selected {
|
||||||
Style::default()
|
let bg = colors.accent_dim();
|
||||||
.fg(colors.fg())
|
Style::default().fg(readable_fg(bg, colors.fg())).bg(bg)
|
||||||
.bg(colors.accent_dim())
|
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(colors.fg()).bg(colors.bg())
|
Style::default().fg(colors.fg()).bg(colors.bg())
|
||||||
}
|
}
|
||||||
@@ -129,17 +218,17 @@ fn key_style(
|
|||||||
colors: &crate::ui::theme::ThemeColors,
|
colors: &crate::ui::theme::ThemeColors,
|
||||||
) -> Style {
|
) -> Style {
|
||||||
if is_depressed {
|
if is_depressed {
|
||||||
|
let bg = brighten_color(colors.accent_dim());
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::White)
|
.fg(readable_fg(bg, colors.fg()))
|
||||||
.bg(brighten_color(colors.accent_dim()))
|
.bg(bg)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if is_next {
|
} else if is_next {
|
||||||
let bg = blend_toward_bg(colors.accent(), colors.bg(), 0.35);
|
let bg = blend_toward_bg(colors.accent(), colors.bg(), 0.35);
|
||||||
Style::default().fg(colors.accent()).bg(bg)
|
Style::default().fg(readable_fg(bg, colors.accent())).bg(bg)
|
||||||
} else if is_selected {
|
} else if is_selected {
|
||||||
Style::default()
|
let bg = colors.accent_dim();
|
||||||
.fg(colors.fg())
|
Style::default().fg(readable_fg(bg, colors.fg())).bg(bg)
|
||||||
.bg(colors.accent_dim())
|
|
||||||
} else if is_unlocked {
|
} else if is_unlocked {
|
||||||
Style::default().fg(colors.fg()).bg(colors.bg())
|
Style::default().fg(colors.fg()).bg(colors.bg())
|
||||||
} else {
|
} else {
|
||||||
@@ -308,9 +397,10 @@ impl KeyboardDiagram<'_> {
|
|||||||
2 => {
|
2 => {
|
||||||
if offset >= 5 {
|
if offset >= 5 {
|
||||||
if self.caps_lock {
|
if self.caps_lock {
|
||||||
|
let bg = colors.accent_dim();
|
||||||
let style = Style::default()
|
let style = Style::default()
|
||||||
.fg(colors.warning())
|
.fg(readable_fg(bg, colors.warning()))
|
||||||
.bg(colors.accent_dim());
|
.bg(bg);
|
||||||
buf.set_string(inner.x, y, "[Cap]", style);
|
buf.set_string(inner.x, y, "[Cap]", style);
|
||||||
} else {
|
} else {
|
||||||
let style = Style::default().fg(colors.text_pending()).bg(colors.bg());
|
let style = Style::default().fg(colors.text_pending()).bg(colors.bg());
|
||||||
|
|||||||
@@ -448,7 +448,8 @@ impl StatsDashboard<'_> {
|
|||||||
.fg(colors.accent())
|
.fg(colors.accent())
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
)))
|
)))
|
||||||
.border_style(Style::default().fg(colors.accent()));
|
.border_style(Style::default().fg(colors.accent()))
|
||||||
|
.style(Style::default().bg(colors.bg()));
|
||||||
block.render(area, buf);
|
block.render(area, buf);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -458,10 +459,11 @@ impl StatsDashboard<'_> {
|
|||||||
let dataset = Dataset::default()
|
let dataset = Dataset::default()
|
||||||
.marker(symbols::Marker::Braille)
|
.marker(symbols::Marker::Braille)
|
||||||
.graph_type(GraphType::Line)
|
.graph_type(GraphType::Line)
|
||||||
.style(Style::default().fg(colors.success()))
|
.style(Style::default().fg(colors.success()).bg(colors.bg()))
|
||||||
.data(&data);
|
.data(&data);
|
||||||
|
|
||||||
let chart = Chart::new(vec![dataset])
|
let chart = Chart::new(vec![dataset])
|
||||||
|
.style(Style::default().fg(colors.fg()).bg(colors.bg()))
|
||||||
.block(
|
.block(
|
||||||
Block::bordered()
|
Block::bordered()
|
||||||
.title(Line::from(Span::styled(
|
.title(Line::from(Span::styled(
|
||||||
@@ -470,22 +472,32 @@ impl StatsDashboard<'_> {
|
|||||||
.fg(colors.accent())
|
.fg(colors.accent())
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
)))
|
)))
|
||||||
.border_style(Style::default().fg(colors.accent())),
|
.border_style(Style::default().fg(colors.accent()))
|
||||||
|
.style(Style::default().bg(colors.bg())),
|
||||||
)
|
)
|
||||||
.x_axis(
|
.x_axis(
|
||||||
Axis::default()
|
Axis::default()
|
||||||
.title("Drill #")
|
.title("Drill #")
|
||||||
.style(Style::default().fg(colors.text_pending()))
|
.style(Style::default().fg(colors.text_pending()).bg(colors.bg()))
|
||||||
.bounds([0.0, max_x]),
|
.bounds([0.0, max_x]),
|
||||||
)
|
)
|
||||||
.y_axis(
|
.y_axis(
|
||||||
Axis::default()
|
Axis::default()
|
||||||
.title("Accuracy %")
|
.title("Accuracy %")
|
||||||
.style(Style::default().fg(colors.text_pending()))
|
.style(Style::default().fg(colors.text_pending()).bg(colors.bg()))
|
||||||
.labels(vec![
|
.labels(vec![
|
||||||
Span::styled("80", Style::default().fg(colors.text_pending())),
|
Span::styled(
|
||||||
Span::styled("90", Style::default().fg(colors.text_pending())),
|
"80",
|
||||||
Span::styled("100", Style::default().fg(colors.text_pending())),
|
Style::default().fg(colors.text_pending()).bg(colors.bg()),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
"90",
|
||||||
|
Style::default().fg(colors.text_pending()).bg(colors.bg()),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
"100",
|
||||||
|
Style::default().fg(colors.text_pending()).bg(colors.bg()),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.bounds([80.0, 100.0]),
|
.bounds([80.0, 100.0]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,117 @@ struct RenderToken {
|
|||||||
is_line_break: bool,
|
is_line_break: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn color_to_rgb(color: ratatui::style::Color) -> (u8, u8, u8) {
|
||||||
|
use ratatui::style::Color;
|
||||||
|
match color {
|
||||||
|
Color::Reset => (0, 0, 0),
|
||||||
|
Color::Black => (0, 0, 0),
|
||||||
|
Color::Red => (205, 49, 49),
|
||||||
|
Color::Green => (13, 188, 121),
|
||||||
|
Color::Yellow => (229, 229, 16),
|
||||||
|
Color::Blue => (36, 114, 200),
|
||||||
|
Color::Magenta => (188, 63, 188),
|
||||||
|
Color::Cyan => (17, 168, 205),
|
||||||
|
Color::Gray => (229, 229, 229),
|
||||||
|
Color::DarkGray => (102, 102, 102),
|
||||||
|
Color::LightRed => (241, 76, 76),
|
||||||
|
Color::LightGreen => (35, 209, 139),
|
||||||
|
Color::LightYellow => (245, 245, 67),
|
||||||
|
Color::LightBlue => (59, 142, 234),
|
||||||
|
Color::LightMagenta => (214, 112, 214),
|
||||||
|
Color::LightCyan => (41, 184, 219),
|
||||||
|
Color::White => (255, 255, 255),
|
||||||
|
Color::Rgb(r, g, b) => (r, g, b),
|
||||||
|
Color::Indexed(i) => {
|
||||||
|
if i < 16 {
|
||||||
|
const ANSI16: [(u8, u8, u8); 16] = [
|
||||||
|
(0, 0, 0),
|
||||||
|
(128, 0, 0),
|
||||||
|
(0, 128, 0),
|
||||||
|
(128, 128, 0),
|
||||||
|
(0, 0, 128),
|
||||||
|
(128, 0, 128),
|
||||||
|
(0, 128, 128),
|
||||||
|
(192, 192, 192),
|
||||||
|
(128, 128, 128),
|
||||||
|
(255, 0, 0),
|
||||||
|
(0, 255, 0),
|
||||||
|
(255, 255, 0),
|
||||||
|
(0, 0, 255),
|
||||||
|
(255, 0, 255),
|
||||||
|
(0, 255, 255),
|
||||||
|
(255, 255, 255),
|
||||||
|
];
|
||||||
|
ANSI16[i as usize]
|
||||||
|
} else if i <= 231 {
|
||||||
|
let idx = i - 16;
|
||||||
|
let r = idx / 36;
|
||||||
|
let g = (idx % 36) / 6;
|
||||||
|
let b = idx % 6;
|
||||||
|
let cv = |n: u8| if n == 0 { 0 } else { 55 + n * 40 };
|
||||||
|
(cv(r), cv(g), cv(b))
|
||||||
|
} else {
|
||||||
|
let v = 8 + (i - 232) * 10;
|
||||||
|
(v, v, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_luminance(color: ratatui::style::Color) -> f64 {
|
||||||
|
let (r, g, b) = color_to_rgb(color);
|
||||||
|
let to_linear = |c: u8| {
|
||||||
|
let x = c as f64 / 255.0;
|
||||||
|
if x <= 0.03928 {
|
||||||
|
x / 12.92
|
||||||
|
} else {
|
||||||
|
((x + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
0.2126 * to_linear(r) + 0.7152 * to_linear(g) + 0.0722 * to_linear(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contrast_ratio(a: ratatui::style::Color, b: ratatui::style::Color) -> f64 {
|
||||||
|
let la = relative_luminance(a);
|
||||||
|
let lb = relative_luminance(b);
|
||||||
|
let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
|
||||||
|
(hi + 0.05) / (lo + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_cursor_colors(colors: &crate::ui::theme::ThemeColors) -> (ratatui::style::Color, ratatui::style::Color) {
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
let base_bg = colors.bg();
|
||||||
|
let mut cursor_bg = colors.text_cursor_bg();
|
||||||
|
|
||||||
|
// Ensure cursor block stands out from the typing area's background.
|
||||||
|
if contrast_ratio(cursor_bg, base_bg) < 1.8 {
|
||||||
|
let mut best_bg = cursor_bg;
|
||||||
|
let mut best_ratio = contrast_ratio(cursor_bg, base_bg);
|
||||||
|
for candidate in [colors.accent(), colors.focused_key(), colors.warning(), Color::Black, Color::White] {
|
||||||
|
let ratio = contrast_ratio(candidate, base_bg);
|
||||||
|
if ratio > best_ratio {
|
||||||
|
best_bg = candidate;
|
||||||
|
best_ratio = ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor_bg = best_bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick most readable fg on top of chosen cursor background.
|
||||||
|
let mut cursor_fg = colors.text_cursor_fg();
|
||||||
|
let mut best_ratio = contrast_ratio(cursor_fg, cursor_bg);
|
||||||
|
for candidate in [colors.fg(), colors.bg(), Color::Black, Color::White] {
|
||||||
|
let ratio = contrast_ratio(candidate, cursor_bg);
|
||||||
|
if ratio > best_ratio {
|
||||||
|
cursor_fg = candidate;
|
||||||
|
best_ratio = ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(cursor_fg, cursor_bg)
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand target chars into render tokens, handling whitespace display.
|
/// Expand target chars into render tokens, handling whitespace display.
|
||||||
fn build_render_tokens(target: &[char]) -> Vec<RenderToken> {
|
fn build_render_tokens(target: &[char]) -> Vec<RenderToken> {
|
||||||
let mut tokens = Vec::new();
|
let mut tokens = Vec::new();
|
||||||
@@ -71,6 +182,7 @@ fn build_render_tokens(target: &[char]) -> Vec<RenderToken> {
|
|||||||
impl Widget for TypingArea<'_> {
|
impl Widget for TypingArea<'_> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let colors = &self.theme.colors;
|
let colors = &self.theme.colors;
|
||||||
|
let (cursor_fg, cursor_bg) = choose_cursor_colors(colors);
|
||||||
let tokens = build_render_tokens(&self.drill.target);
|
let tokens = build_render_tokens(&self.drill.target);
|
||||||
|
|
||||||
// Group tokens into lines, splitting on line_break tokens
|
// Group tokens into lines, splitting on line_break tokens
|
||||||
@@ -90,9 +202,9 @@ impl Widget for TypingArea<'_> {
|
|||||||
}
|
}
|
||||||
} else if idx == self.drill.cursor {
|
} else if idx == self.drill.cursor {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(colors.text_cursor_fg())
|
.fg(cursor_fg)
|
||||||
.bg(colors.text_cursor_bg())
|
.bg(cursor_bg)
|
||||||
.add_modifier(Modifier::REVERSED | Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(colors.text_pending())
|
Style::default().fg(colors.text_pending())
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user