1use ratatui::{
2 buffer::Buffer,
3 layout::{Alignment, Constraint, Direction, Layout, Rect},
4 style::{Color, Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget, Wrap},
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DialogType {
12 Info,
13 Success,
14 Warning,
15 Error,
16 Confirm,
17}
18
19pub struct Dialog<'a> {
21 title: &'a str,
23 message: &'a str,
25 dialog_type: DialogType,
27 buttons: Vec<&'a str>,
29 selected_button: usize,
31 width_percent: f32,
33 height_percent: f32,
35 style: Style,
37 button_selected_style: Style,
39 button_style: Style,
41 button_areas: Vec<Rect>,
43}
44
45impl<'a> Dialog<'a> {
46 pub fn new(title: &'a str, message: &'a str) -> Self {
48 Self {
49 title,
50 message,
51 dialog_type: DialogType::Info,
52 buttons: vec!["OK"],
53 selected_button: 0,
54 width_percent: 0.6,
55 height_percent: 0.4,
56 style: Style::default().fg(Color::White).bg(Color::Black),
57 button_selected_style: Style::default()
58 .fg(Color::Black)
59 .bg(Color::Cyan)
60 .add_modifier(Modifier::BOLD),
61 button_style: Style::default().fg(Color::White).bg(Color::DarkGray),
62 button_areas: Vec::new(),
63 }
64 }
65
66 pub fn dialog_type(mut self, dialog_type: DialogType) -> Self {
68 self.dialog_type = dialog_type;
69 self
70 }
71
72 pub fn buttons(mut self, buttons: Vec<&'a str>) -> Self {
74 self.buttons = buttons;
75 self
76 }
77
78 pub fn width_percent(mut self, percent: f32) -> Self {
80 self.width_percent = percent.clamp(0.1, 1.0);
81 self
82 }
83
84 pub fn height_percent(mut self, percent: f32) -> Self {
86 self.height_percent = percent.clamp(0.1, 1.0);
87 self
88 }
89
90 pub fn select_next_button(&mut self) {
92 if !self.buttons.is_empty() && self.selected_button < self.buttons.len() - 1 {
93 self.selected_button += 1;
94 }
95 }
96
97 pub fn select_previous_button(&mut self) {
99 if self.selected_button > 0 {
100 self.selected_button -= 1;
101 }
102 }
103
104 pub fn get_selected_button(&self) -> usize {
106 self.selected_button
107 }
108
109 pub fn get_selected_button_text(&self) -> Option<&str> {
111 self.buttons.get(self.selected_button).copied()
112 }
113
114 pub fn handle_click(&self, column: u16, row: u16) -> Option<usize> {
116 for (idx, area) in self.button_areas.iter().enumerate() {
117 if column >= area.x
118 && column < area.x + area.width
119 && row >= area.y
120 && row < area.y + area.height
121 {
122 return Some(idx);
123 }
124 }
125 None
126 }
127
128 pub fn confirm(title: &'a str, message: &'a str) -> Self {
130 Self::new(title, message)
131 .dialog_type(DialogType::Confirm)
132 .buttons(vec!["Yes", "No"])
133 }
134
135 pub fn info(title: &'a str, message: &'a str) -> Self {
137 Self::new(title, message).dialog_type(DialogType::Info)
138 }
139
140 pub fn success(title: &'a str, message: &'a str) -> Self {
142 Self::new(title, message).dialog_type(DialogType::Success)
143 }
144
145 pub fn warning(title: &'a str, message: &'a str) -> Self {
147 Self::new(title, message).dialog_type(DialogType::Warning)
148 }
149
150 pub fn error(title: &'a str, message: &'a str) -> Self {
152 Self::new(title, message).dialog_type(DialogType::Error)
153 }
154
155 fn get_border_color(&self) -> Color {
157 match self.dialog_type {
158 DialogType::Info => Color::Cyan,
159 DialogType::Success => Color::Green,
160 DialogType::Warning => Color::Yellow,
161 DialogType::Error => Color::Red,
162 DialogType::Confirm => Color::Blue,
163 }
164 }
165}
166
167impl Widget for Dialog<'_> {
168 fn render(mut self, area: Rect, buf: &mut Buffer) {
169 let dialog_width = (area.width as f32 * self.width_percent) as u16;
171 let dialog_height = (area.height as f32 * self.height_percent) as u16;
172 let dialog_x = (area.width.saturating_sub(dialog_width)) / 2;
173 let dialog_y = (area.height.saturating_sub(dialog_height)) / 2;
174
175 let dialog_area = Rect {
176 x: area.x + dialog_x,
177 y: area.y + dialog_y,
178 width: dialog_width,
179 height: dialog_height,
180 };
181
182 Clear.render(dialog_area, buf);
184
185 let block = Block::default()
187 .title(self.title)
188 .borders(Borders::ALL)
189 .border_type(BorderType::Rounded)
190 .border_style(Style::default().fg(self.get_border_color()))
191 .style(self.style);
192
193 let inner = block.inner(dialog_area);
194 block.render(dialog_area, buf);
195
196 let chunks = Layout::default()
198 .direction(Direction::Vertical)
199 .constraints([Constraint::Min(3), Constraint::Length(3)])
200 .split(inner);
201
202 let message = Paragraph::new(self.message)
204 .style(self.style)
205 .alignment(Alignment::Center)
206 .wrap(Wrap { trim: true });
207 message.render(chunks[0], buf);
208
209 self.button_areas.clear();
211
212 if !self.buttons.is_empty() {
213 let total_button_width: usize = self.buttons.iter().map(|b| b.len() + 4).sum(); let button_area_width = chunks[1].width as usize;
215 let start_x = if total_button_width < button_area_width {
216 chunks[1].x + ((button_area_width - total_button_width) / 2) as u16
217 } else {
218 chunks[1].x
219 };
220
221 let mut x = start_x;
222 let y = chunks[1].y + 1;
223
224 for (idx, button_text) in self.buttons.iter().enumerate() {
225 let button_width = button_text.len() as u16 + 2; let style = if idx == self.selected_button {
227 self.button_selected_style
228 } else {
229 self.button_style
230 };
231
232 let button_area = Rect {
233 x,
234 y,
235 width: button_width,
236 height: 1,
237 };
238
239 self.button_areas.push(button_area);
240
241 for bx in x..x + button_width {
243 if let Some(cell) = buf.cell_mut((bx, y)) {
244 cell.set_style(style);
245 }
246 }
247
248 let button_line =
249 Line::from(vec![Span::styled(format!(" {} ", button_text), style)]);
250
251 buf.set_line(x, y, &button_line, button_width);
252 x += button_width + 2; }
254 }
255 }
256}
257
258pub struct DialogWidget<'a> {
260 dialog: &'a mut Dialog<'a>,
261}
262
263impl<'a> DialogWidget<'a> {
264 pub fn new(dialog: &'a mut Dialog<'a>) -> Self {
265 Self { dialog }
266 }
267}
268
269impl Widget for DialogWidget<'_> {
270 fn render(self, area: Rect, buf: &mut Buffer) {
271 let dialog_width = (area.width as f32 * self.dialog.width_percent) as u16;
273 let dialog_height = (area.height as f32 * self.dialog.height_percent) as u16;
274 let dialog_x = (area.width.saturating_sub(dialog_width)) / 2;
275 let dialog_y = (area.height.saturating_sub(dialog_height)) / 2;
276
277 let dialog_area = Rect {
278 x: area.x + dialog_x,
279 y: area.y + dialog_y,
280 width: dialog_width,
281 height: dialog_height,
282 };
283
284 Clear.render(dialog_area, buf);
286
287 let block = Block::default()
289 .title(self.dialog.title)
290 .borders(Borders::ALL)
291 .border_type(BorderType::Rounded)
292 .border_style(Style::default().fg(self.dialog.get_border_color()))
293 .style(self.dialog.style);
294
295 let inner = block.inner(dialog_area);
296 block.render(dialog_area, buf);
297
298 let chunks = Layout::default()
300 .direction(Direction::Vertical)
301 .constraints([Constraint::Min(3), Constraint::Length(3)])
302 .split(inner);
303
304 let message = Paragraph::new(self.dialog.message)
306 .style(self.dialog.style)
307 .alignment(Alignment::Center)
308 .wrap(Wrap { trim: true });
309 message.render(chunks[0], buf);
310
311 self.dialog.button_areas.clear();
313
314 if !self.dialog.buttons.is_empty() {
315 let total_button_width: usize = self.dialog.buttons.iter().map(|b| b.len() + 4).sum();
316 let button_area_width = chunks[1].width as usize;
317 let start_x = if total_button_width < button_area_width {
318 chunks[1].x + ((button_area_width - total_button_width) / 2) as u16
319 } else {
320 chunks[1].x
321 };
322
323 let mut x = start_x;
324 let y = chunks[1].y + 1;
325
326 for (idx, button_text) in self.dialog.buttons.iter().enumerate() {
327 let button_width = button_text.len() as u16 + 2;
328 let style = if idx == self.dialog.selected_button {
329 self.dialog.button_selected_style
330 } else {
331 self.dialog.button_style
332 };
333
334 let button_area = Rect {
335 x,
336 y,
337 width: button_width,
338 height: 1,
339 };
340
341 self.dialog.button_areas.push(button_area);
342
343 for bx in x..x + button_width {
345 if let Some(cell) = buf.cell_mut((bx, y)) {
346 cell.set_style(style);
347 }
348 }
349
350 let button_line =
351 Line::from(vec![Span::styled(format!(" {} ", button_text), style)]);
352
353 buf.set_line(x, y, &button_line, button_width);
354 x += button_width + 2;
355 }
356 }
357 }
358}