use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
layout::{Alignment, Constraint, Layout},
style::Style,
text::{Line, Span},
widgets::{Clear, Paragraph, Wrap},
Frame,
};
use super::{
button_danger_style, button_focused_style, button_style, centered_rect, dialog_block,
hint_style, DialogAction,
};
use crate::ui::theme;
#[derive(Debug)]
pub struct ConfirmDialog {
pub title: String,
pub message: String,
pub confirm_text: String,
pub cancel_text: String,
pub selected_yes: bool,
pub destructive: bool,
}
impl ConfirmDialog {
pub fn new(title: impl Into<String>, message: impl Into<String>) -> Self {
Self {
title: title.into(),
message: message.into(),
confirm_text: "Yes".to_string(),
cancel_text: "No".to_string(),
selected_yes: false, destructive: false,
}
}
pub fn delete_task(task_title: &str) -> Self {
Self {
title: "Delete Task?".to_string(),
message: format!(
"\"{}\"\n\nThis action cannot be undone.",
task_title
),
confirm_text: "Delete".to_string(),
cancel_text: "Cancel".to_string(),
selected_yes: false,
destructive: true,
}
}
pub fn with_confirm_text(mut self, text: impl Into<String>) -> Self {
self.confirm_text = text.into();
self
}
pub fn with_cancel_text(mut self, text: impl Into<String>) -> Self {
self.cancel_text = text.into();
self
}
pub fn destructive(mut self) -> Self {
self.destructive = true;
self
}
pub fn handle_key(&mut self, key: KeyEvent) -> DialogAction {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => DialogAction::Submit,
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => DialogAction::Cancel,
KeyCode::Left | KeyCode::Right | KeyCode::Tab | KeyCode::Char('h') | KeyCode::Char('l') => {
self.selected_yes = !self.selected_yes;
DialogAction::None
}
KeyCode::Enter => {
if self.selected_yes {
DialogAction::Submit
} else {
DialogAction::Cancel
}
}
_ => DialogAction::None,
}
}
pub fn render(&self, frame: &mut Frame) {
let area = frame.area();
let dialog_width = 50.min(area.width.saturating_sub(4));
let dialog_height = 10.min(area.height.saturating_sub(4));
let dialog_area = centered_rect(dialog_width, dialog_height, area);
frame.render_widget(Clear, area);
frame.render_widget(
Paragraph::new("").style(Style::default().bg(theme::BG_DARK)),
area,
);
let block = dialog_block(&self.title, self.destructive);
let inner = block.inner(dialog_area);
frame.render_widget(block, dialog_area);
let chunks = Layout::vertical([
Constraint::Min(3), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
.split(inner);
let message = Paragraph::new(self.message.as_str())
.style(Style::default().fg(theme::TEXT_PRIMARY))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(message, chunks[0]);
let yes_style = if self.selected_yes {
if self.destructive {
button_danger_style()
} else {
button_focused_style()
}
} else {
button_style()
};
let no_style = if !self.selected_yes {
button_focused_style()
} else {
button_style()
};
let buttons = Line::from(vec![
Span::styled(format!(" {} ", self.confirm_text), yes_style),
Span::raw(" "),
Span::styled(format!(" {} ", self.cancel_text), no_style),
]);
let button_paragraph = Paragraph::new(buttons).alignment(Alignment::Center);
frame.render_widget(button_paragraph, chunks[2]);
let hint = Paragraph::new("y/n or ←/→ to select, Enter to confirm")
.style(hint_style())
.alignment(Alignment::Center);
frame.render_widget(hint, chunks[3]);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
#[test]
fn test_new_dialog() {
let dialog = ConfirmDialog::new("Test", "Are you sure?");
assert_eq!(dialog.title, "Test");
assert_eq!(dialog.message, "Are you sure?");
assert!(!dialog.selected_yes);
}
#[test]
fn test_delete_task_dialog() {
let dialog = ConfirmDialog::delete_task("My Task");
assert!(dialog.title.contains("Delete"));
assert!(dialog.message.contains("My Task"));
assert!(dialog.destructive);
}
#[test]
fn test_handle_key_yes() {
let mut dialog = ConfirmDialog::new("Test", "Message");
let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
assert_eq!(dialog.handle_key(key), DialogAction::Submit);
}
#[test]
fn test_handle_key_no() {
let mut dialog = ConfirmDialog::new("Test", "Message");
let key = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE);
assert_eq!(dialog.handle_key(key), DialogAction::Cancel);
}
#[test]
fn test_handle_key_escape() {
let mut dialog = ConfirmDialog::new("Test", "Message");
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(dialog.handle_key(key), DialogAction::Cancel);
}
#[test]
fn test_handle_key_toggle() {
let mut dialog = ConfirmDialog::new("Test", "Message");
assert!(!dialog.selected_yes);
let key = KeyEvent::new(KeyCode::Left, KeyModifiers::NONE);
dialog.handle_key(key);
assert!(dialog.selected_yes);
dialog.handle_key(key);
assert!(!dialog.selected_yes);
}
#[test]
fn test_handle_key_enter_yes() {
let mut dialog = ConfirmDialog::new("Test", "Message");
dialog.selected_yes = true;
let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(dialog.handle_key(key), DialogAction::Submit);
}
#[test]
fn test_handle_key_enter_no() {
let mut dialog = ConfirmDialog::new("Test", "Message");
dialog.selected_yes = false;
let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(dialog.handle_key(key), DialogAction::Cancel);
}
}