Skip to main content

ratatui_toolkit/
dialog.rs

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/// Dialog type
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DialogType {
12    Info,
13    Success,
14    Warning,
15    Error,
16    Confirm,
17}
18
19/// A dialog/modal widget that overlays content
20pub struct Dialog<'a> {
21    /// Dialog title
22    title: &'a str,
23    /// Dialog message
24    message: &'a str,
25    /// Dialog type
26    dialog_type: DialogType,
27    /// Buttons to show
28    buttons: Vec<&'a str>,
29    /// Selected button index
30    selected_button: usize,
31    /// Width percentage (0.0 to 1.0)
32    width_percent: f32,
33    /// Height percentage (0.0 to 1.0)
34    height_percent: f32,
35    /// Style for the dialog
36    style: Style,
37    /// Style for selected button
38    button_selected_style: Style,
39    /// Style for unselected button
40    button_style: Style,
41    /// Areas for buttons (for click detection)
42    button_areas: Vec<Rect>,
43}
44
45impl<'a> Dialog<'a> {
46    /// Create a new dialog
47    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    /// Set dialog type
67    pub fn dialog_type(mut self, dialog_type: DialogType) -> Self {
68        self.dialog_type = dialog_type;
69        self
70    }
71
72    /// Set buttons
73    pub fn buttons(mut self, buttons: Vec<&'a str>) -> Self {
74        self.buttons = buttons;
75        self
76    }
77
78    /// Set width percentage
79    pub fn width_percent(mut self, percent: f32) -> Self {
80        self.width_percent = percent.clamp(0.1, 1.0);
81        self
82    }
83
84    /// Set height percentage
85    pub fn height_percent(mut self, percent: f32) -> Self {
86        self.height_percent = percent.clamp(0.1, 1.0);
87        self
88    }
89
90    /// Select next button
91    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    /// Select previous button
98    pub fn select_previous_button(&mut self) {
99        if self.selected_button > 0 {
100            self.selected_button -= 1;
101        }
102    }
103
104    /// Get selected button index
105    pub fn get_selected_button(&self) -> usize {
106        self.selected_button
107    }
108
109    /// Get selected button text
110    pub fn get_selected_button_text(&self) -> Option<&str> {
111        self.buttons.get(self.selected_button).copied()
112    }
113
114    /// Handle click on buttons
115    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    /// Create a confirmation dialog
129    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    /// Create an info dialog
136    pub fn info(title: &'a str, message: &'a str) -> Self {
137        Self::new(title, message).dialog_type(DialogType::Info)
138    }
139
140    /// Create a success dialog
141    pub fn success(title: &'a str, message: &'a str) -> Self {
142        Self::new(title, message).dialog_type(DialogType::Success)
143    }
144
145    /// Create a warning dialog
146    pub fn warning(title: &'a str, message: &'a str) -> Self {
147        Self::new(title, message).dialog_type(DialogType::Warning)
148    }
149
150    /// Create an error dialog
151    pub fn error(title: &'a str, message: &'a str) -> Self {
152        Self::new(title, message).dialog_type(DialogType::Error)
153    }
154
155    /// Get border color based on dialog type
156    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        // Calculate centered dialog area
170        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 the area behind the dialog
183        Clear.render(dialog_area, buf);
184
185        // Render dialog block
186        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        // Layout: message area and button area
197        let chunks = Layout::default()
198            .direction(Direction::Vertical)
199            .constraints([Constraint::Min(3), Constraint::Length(3)])
200            .split(inner);
201
202        // Render message
203        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        // Render buttons
210        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(); // +4 for spacing
214            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; // +2 for padding
226                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                // Fill button background
242                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; // +2 for spacing between buttons
253            }
254        }
255    }
256}
257
258/// Mutable widget variant
259pub 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        // Calculate centered dialog area
272        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 the area behind the dialog
285        Clear.render(dialog_area, buf);
286
287        // Render dialog block
288        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        // Layout: message area and button area
299        let chunks = Layout::default()
300            .direction(Direction::Vertical)
301            .constraints([Constraint::Min(3), Constraint::Length(3)])
302            .split(inner);
303
304        // Render message
305        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        // Render buttons
312        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                // Fill button background
344                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}