Skip to main content

mq_edit/ui/
dialog.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::{Alignment, Constraint, Layout, Rect},
4    style::{Color, Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, Clear, Paragraph, Widget},
7};
8
9/// Quit confirmation dialog widget
10pub struct QuitDialog;
11
12impl QuitDialog {
13    pub fn new() -> Self {
14        Self
15    }
16
17    /// Calculate the dialog area (centered in the given area)
18    fn dialog_area(area: Rect) -> Rect {
19        let dialog_width = 50.min(area.width.saturating_sub(4));
20        let dialog_height = 7.min(area.height.saturating_sub(2));
21
22        let x = (area.width.saturating_sub(dialog_width)) / 2;
23        let y = (area.height.saturating_sub(dialog_height)) / 2;
24
25        Rect::new(x, y, dialog_width, dialog_height)
26    }
27}
28
29impl Widget for QuitDialog {
30    fn render(self, area: Rect, buf: &mut Buffer) {
31        let dialog_area = Self::dialog_area(area);
32
33        // Clear the dialog area first
34        Clear.render(dialog_area, buf);
35
36        // Create the dialog block with warning colors
37        let block = Block::default()
38            .title(" Unsaved Changes ")
39            .title_alignment(Alignment::Center)
40            .borders(Borders::ALL)
41            .border_style(Style::default().fg(Color::Yellow))
42            .style(Style::default().bg(Color::Black));
43
44        let inner_area = block.inner(dialog_area);
45        block.render(dialog_area, buf);
46
47        // Create dialog content
48        let chunks = Layout::vertical([
49            Constraint::Length(1), // spacing
50            Constraint::Length(1), // message
51            Constraint::Length(1), // spacing
52            Constraint::Length(1), // buttons
53        ])
54        .split(inner_area);
55
56        // Warning message
57        let message = Paragraph::new("You have unsaved changes. Quit anyway?")
58            .alignment(Alignment::Center)
59            .style(Style::default().fg(Color::White));
60        message.render(chunks[1], buf);
61
62        // Button hints
63        let buttons = Line::from(vec![
64            Span::styled(
65                " [Y] ",
66                Style::default()
67                    .fg(Color::Black)
68                    .bg(Color::Red)
69                    .add_modifier(Modifier::BOLD),
70            ),
71            Span::raw(" Quit  "),
72            Span::styled(
73                " [N] ",
74                Style::default()
75                    .fg(Color::Black)
76                    .bg(Color::Green)
77                    .add_modifier(Modifier::BOLD),
78            ),
79            Span::raw(" Cancel "),
80        ]);
81        let buttons_para = Paragraph::new(buttons).alignment(Alignment::Center);
82        buttons_para.render(chunks[3], buf);
83    }
84}
85
86impl Default for QuitDialog {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92/// Save As dialog widget for entering a file name
93pub struct SaveAsDialog<'a> {
94    filename: &'a str,
95}
96
97impl<'a> SaveAsDialog<'a> {
98    pub fn new(filename: &'a str) -> Self {
99        Self { filename }
100    }
101
102    /// Calculate the dialog area (centered in the given area)
103    fn dialog_area(area: Rect) -> Rect {
104        let dialog_width = 60.min(area.width.saturating_sub(4));
105        let dialog_height = 7.min(area.height.saturating_sub(2));
106
107        let x = (area.width.saturating_sub(dialog_width)) / 2;
108        let y = (area.height.saturating_sub(dialog_height)) / 2;
109
110        Rect::new(x, y, dialog_width, dialog_height)
111    }
112}
113
114impl<'a> Widget for SaveAsDialog<'a> {
115    fn render(self, area: Rect, buf: &mut Buffer) {
116        let dialog_area = Self::dialog_area(area);
117
118        // Clear the dialog area first
119        Clear.render(dialog_area, buf);
120
121        // Create the dialog block
122        let block = Block::default()
123            .title(" Save As ")
124            .title_alignment(Alignment::Center)
125            .borders(Borders::ALL)
126            .border_style(Style::default().fg(Color::Cyan))
127            .style(Style::default().bg(Color::Black));
128
129        let inner_area = block.inner(dialog_area);
130        block.render(dialog_area, buf);
131
132        // Create dialog content
133        let chunks = Layout::vertical([
134            Constraint::Length(1), // spacing
135            Constraint::Length(1), // prompt
136            Constraint::Length(1), // input field
137            Constraint::Length(1), // hint
138        ])
139        .split(inner_area);
140
141        // Prompt
142        let prompt = Paragraph::new("Enter filename:").style(Style::default().fg(Color::White));
143        prompt.render(chunks[1], buf);
144
145        // Input field with current filename
146        let input = Paragraph::new(self.filename).style(
147            Style::default()
148                .fg(Color::Yellow)
149                .add_modifier(Modifier::BOLD),
150        );
151        input.render(chunks[2], buf);
152
153        // Hint
154        let hint = Paragraph::new("Press Enter to save, Esc to cancel")
155            .alignment(Alignment::Center)
156            .style(Style::default().fg(Color::DarkGray));
157        hint.render(chunks[3], buf);
158    }
159}
160
161/// Go to line dialog widget for entering a line number
162pub struct GotoLineDialog<'a> {
163    line_number: &'a str,
164    current_line: usize,
165    total_lines: usize,
166}
167
168impl<'a> GotoLineDialog<'a> {
169    pub fn new(line_number: &'a str, current_line: usize, total_lines: usize) -> Self {
170        Self {
171            line_number,
172            current_line,
173            total_lines,
174        }
175    }
176
177    /// Calculate the dialog area (centered in the given area)
178    fn dialog_area(area: Rect) -> Rect {
179        let dialog_width = 50.min(area.width.saturating_sub(4));
180        let dialog_height = 8.min(area.height.saturating_sub(2));
181
182        let x = (area.width.saturating_sub(dialog_width)) / 2;
183        let y = (area.height.saturating_sub(dialog_height)) / 2;
184
185        Rect::new(x, y, dialog_width, dialog_height)
186    }
187}
188
189impl<'a> Widget for GotoLineDialog<'a> {
190    fn render(self, area: Rect, buf: &mut Buffer) {
191        let dialog_area = Self::dialog_area(area);
192
193        // Clear the dialog area first
194        Clear.render(dialog_area, buf);
195
196        // Create the dialog block
197        let block = Block::default()
198            .title(" Go to Line ")
199            .title_alignment(Alignment::Center)
200            .borders(Borders::ALL)
201            .border_style(Style::default().fg(Color::Cyan))
202            .style(Style::default().bg(Color::Black));
203
204        let inner_area = block.inner(dialog_area);
205        block.render(dialog_area, buf);
206
207        // Create dialog content
208        let chunks = Layout::vertical([
209            Constraint::Length(1), // spacing
210            Constraint::Length(1), // info
211            Constraint::Length(1), // prompt
212            Constraint::Length(1), // input field
213            Constraint::Length(1), // hint
214        ])
215        .split(inner_area);
216
217        // Info about current position
218        let info = Paragraph::new(format!(
219            "Current: {} / {}",
220            self.current_line + 1,
221            self.total_lines
222        ))
223        .alignment(Alignment::Center)
224        .style(Style::default().fg(Color::DarkGray));
225        info.render(chunks[1], buf);
226
227        // Prompt
228        let prompt = Paragraph::new("Enter line number:").style(Style::default().fg(Color::White));
229        prompt.render(chunks[2], buf);
230
231        // Input field with current input
232        let input = Paragraph::new(self.line_number).style(
233            Style::default()
234                .fg(Color::Yellow)
235                .add_modifier(Modifier::BOLD),
236        );
237        input.render(chunks[3], buf);
238
239        // Hint
240        let hint = Paragraph::new("Press Enter to jump, Esc to cancel")
241            .alignment(Alignment::Center)
242            .style(Style::default().fg(Color::DarkGray));
243        hint.render(chunks[4], buf);
244    }
245}