rtcom_tui/menu/
confirm.rs1use crossterm::event::{KeyCode, KeyEvent};
11use ratatui::{
12 buffer::Buffer,
13 layout::Rect,
14 style::{Modifier, Style},
15 text::{Line, Span},
16 widgets::{Block, Paragraph, Widget},
17};
18
19use crate::modal::{centred_rect, Dialog, DialogAction, DialogOutcome};
20
21const PREFERRED_WIDTH: u16 = 50;
23const PREFERRED_HEIGHT: u16 = 8;
25
26pub struct ConfirmDialog {
34 title: String,
35 prompt: String,
36 on_confirm: DialogAction,
37 preferred_width: u16,
38 preferred_height: u16,
39}
40
41impl ConfirmDialog {
42 #[must_use]
48 pub fn new(
49 title: impl Into<String>,
50 prompt: impl Into<String>,
51 on_confirm: DialogAction,
52 ) -> Self {
53 Self {
54 title: title.into(),
55 prompt: prompt.into(),
56 on_confirm,
57 preferred_width: PREFERRED_WIDTH,
58 preferred_height: PREFERRED_HEIGHT,
59 }
60 }
61}
62
63impl Dialog for ConfirmDialog {
64 fn title(&self) -> &str {
65 self.title.as_str()
66 }
67
68 fn render(&self, area: Rect, buf: &mut Buffer) {
69 let block = Block::bordered().title(self.title.as_str());
70 let inner = block.inner(area);
71 block.render(area, buf);
72 let lines = vec![
73 Line::from(self.prompt.as_str()),
74 Line::from(""),
75 Line::from(Span::styled(
76 " [Y]es [N]o / Esc to cancel ",
77 Style::default().add_modifier(Modifier::DIM),
78 )),
79 ];
80 Paragraph::new(lines).render(inner, buf);
81 }
82
83 fn handle_key(&mut self, key: KeyEvent) -> DialogOutcome {
84 match key.code {
85 KeyCode::Char('y' | 'Y') | KeyCode::Enter => {
86 DialogOutcome::Action(self.on_confirm.clone())
87 }
88 KeyCode::Char('n' | 'N') | KeyCode::Esc => DialogOutcome::Close,
89 _ => DialogOutcome::Consumed,
90 }
91 }
92
93 fn preferred_size(&self, outer: Rect) -> Rect {
94 centred_rect(outer, self.preferred_width, self.preferred_height)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
102
103 const fn key(code: KeyCode) -> KeyEvent {
104 KeyEvent::new(code, KeyModifiers::NONE)
105 }
106
107 fn dialog() -> ConfirmDialog {
108 ConfirmDialog::new("Title", "Are you sure?", DialogAction::WriteProfile)
109 }
110
111 #[test]
112 fn lowercase_y_confirms() {
113 let mut d = dialog();
114 let out = d.handle_key(key(KeyCode::Char('y')));
115 assert!(matches!(
116 out,
117 DialogOutcome::Action(DialogAction::WriteProfile)
118 ));
119 }
120
121 #[test]
122 fn uppercase_y_confirms() {
123 let mut d = dialog();
124 let out = d.handle_key(key(KeyCode::Char('Y')));
125 assert!(matches!(
126 out,
127 DialogOutcome::Action(DialogAction::WriteProfile)
128 ));
129 }
130
131 #[test]
132 fn enter_confirms() {
133 let mut d = dialog();
134 let out = d.handle_key(key(KeyCode::Enter));
135 assert!(matches!(
136 out,
137 DialogOutcome::Action(DialogAction::WriteProfile)
138 ));
139 }
140
141 #[test]
142 fn lowercase_n_cancels() {
143 let mut d = dialog();
144 let out = d.handle_key(key(KeyCode::Char('n')));
145 assert!(matches!(out, DialogOutcome::Close));
146 }
147
148 #[test]
149 fn uppercase_n_cancels() {
150 let mut d = dialog();
151 let out = d.handle_key(key(KeyCode::Char('N')));
152 assert!(matches!(out, DialogOutcome::Close));
153 }
154
155 #[test]
156 fn esc_cancels() {
157 let mut d = dialog();
158 let out = d.handle_key(key(KeyCode::Esc));
159 assert!(matches!(out, DialogOutcome::Close));
160 }
161
162 #[test]
163 fn other_key_consumed() {
164 let mut d = dialog();
165 let out = d.handle_key(key(KeyCode::Char('x')));
166 assert!(matches!(out, DialogOutcome::Consumed));
167 }
168
169 #[test]
170 fn title_round_trips() {
171 let d = ConfirmDialog::new("Write profile", "prompt", DialogAction::WriteProfile);
172 assert_eq!(d.title(), "Write profile");
173 }
174
175 #[test]
176 fn preferred_size_50x8_centred() {
177 let d = dialog();
178 let outer = Rect {
179 x: 0,
180 y: 0,
181 width: 80,
182 height: 24,
183 };
184 let pref = d.preferred_size(outer);
185 assert_eq!(pref.width, 50);
186 assert_eq!(pref.height, 8);
187 }
188}