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