1use crate::dialog::DialogData;
4use crate::theme::ThemeStyles;
5use ratatui::{
6 Frame,
7 layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
8 style::{Color, Modifier, Style},
9 text::Text,
10 widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},
11};
12
13pub type DialogButtonRenderer<'a> =
14 dyn FnMut(&mut Frame, Rect, &str, bool, &DialogTheme) + 'a;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct DialogTheme {
20 pub background: Color,
21 pub border: Color,
22 pub border_active: Color,
23 pub title: Color,
24 pub text: Color,
25 pub button: Color,
26 pub button_active: Color,
27}
28
29pub enum DialogPurposeClass {
31 Success,
32 Failure,
33 Neutral,
34}
35
36pub trait DialogPurposeStyle {
42 fn class(&self) -> DialogPurposeClass {
43 DialogPurposeClass::Neutral
44 }
45}
46
47impl DialogTheme {
48 pub fn themed<D: DialogPurposeStyle>(
55 text_color: Color,
56 error_color: Color,
57 success_color: Color,
58 background: Color,
59 button_active: Color,
60 purpose: Option<D>,
61 ) -> Self {
62 let fg = match purpose.as_ref().map(|p| p.class()) {
63 Some(DialogPurposeClass::Success) => success_color,
64 Some(DialogPurposeClass::Failure) => error_color,
65 _ => text_color,
66 };
67 Self {
68 background,
69 border: fg,
70 border_active: fg,
71 title: fg,
72 text: fg,
73 button: fg,
74 button_active,
75 }
76 }
77
78 pub fn from_theme_styles(styles: &ThemeStyles) -> Self {
84 let default = Self::default();
85 Self {
86 background: styles.background.bg.unwrap_or(default.background),
87 border: styles
88 .muted
89 .fg
90 .unwrap_or(styles.text.fg.unwrap_or(default.border)),
91 border_active: styles.text_focus.fg.unwrap_or(default.border_active),
92 title: styles.text_focus.fg.unwrap_or(default.title),
93 text: styles.text.fg.unwrap_or(default.text),
94 button: styles.text.fg.unwrap_or(default.button),
95 button_active: styles
96 .selection
97 .bg
98 .unwrap_or(styles.text_focus.fg.unwrap_or(default.button_active)),
99 }
100 }
101
102 pub fn error_styled(styles: &ThemeStyles) -> Self {
108 let default = Self::default();
109 let fg = styles.error.fg.unwrap_or(styles.text.fg.unwrap_or(default.text));
110 Self {
111 background: styles.background.bg.unwrap_or(default.background),
112 border: fg,
113 border_active: fg,
114 title: fg,
115 text: fg,
116 button: fg,
117 button_active: styles
118 .selection
119 .bg
120 .unwrap_or(styles.text_focus.fg.unwrap_or(default.button_active)),
121 }
122 }
123
124 pub fn success_styled(styles: &ThemeStyles) -> Self {
130 let default = Self::default();
131 let fg = styles
132 .success
133 .fg
134 .unwrap_or(styles.text.fg.unwrap_or(default.text));
135 Self {
136 background: styles.background.bg.unwrap_or(default.background),
137 border: fg,
138 border_active: fg,
139 title: fg,
140 text: fg,
141 button: fg,
142 button_active: styles
143 .selection
144 .bg
145 .unwrap_or(styles.text_focus.fg.unwrap_or(default.button_active)),
146 }
147 }
148
149}
150
151impl Default for DialogTheme {
152 fn default() -> Self {
153 Self {
154 background: Color::Reset,
155 border: Color::DarkGray,
156 border_active: Color::Cyan,
157 title: Color::Cyan,
158 text: Color::Reset,
159 button: Color::Gray,
160 button_active: Color::Cyan,
161 }
162 }
163}
164
165pub fn render_dialog<D>(
171 f: &mut Frame,
172 area: Rect,
173 data: &DialogData<D>,
174 active_button: usize,
175 theme: &DialogTheme,
176) {
177 render_dialog_with_button_renderer(
178 f,
179 area,
180 data,
181 active_button,
182 theme,
183 &mut render_default_dialog_button,
184 );
185}
186
187pub fn render_dialog_error<D>(
190 f: &mut Frame,
191 area: Rect,
192 data: &DialogData<D>,
193 active_button: usize,
194 styles: &ThemeStyles,
195) {
196 render_dialog(
197 f,
198 area,
199 data,
200 active_button,
201 &DialogTheme::error_styled(styles),
202 );
203}
204
205pub fn render_dialog_success<D>(
208 f: &mut Frame,
209 area: Rect,
210 data: &DialogData<D>,
211 active_button: usize,
212 styles: &ThemeStyles,
213) {
214 render_dialog(
215 f,
216 area,
217 data,
218 active_button,
219 &DialogTheme::success_styled(styles),
220 );
221}
222
223pub fn render_dialog_with_button_renderer<D>(
224 f: &mut Frame,
225 area: Rect,
226 data: &DialogData<D>,
227 active_button: usize,
228 theme: &DialogTheme,
229 render_button: &mut DialogButtonRenderer<'_>,
230) {
231 let message_height = data.message.lines().count().max(1) as u16;
232 let button_row_height = if data.buttons.is_empty() { 0 } else { 3 };
233 let total_height = (message_height + button_row_height + 4).min(area.height.max(3));
235
236 let width = (area.width * 60 / 100).clamp(20, area.width);
237 let x = area.x + (area.width.saturating_sub(width)) / 2;
238 let y = area.y + (area.height.saturating_sub(total_height)) / 2;
239 let dialog_area = Rect::new(x, y, width, total_height);
240
241 f.render_widget(Clear, dialog_area);
242 f.render_widget(
243 Block::default()
244 .borders(Borders::ALL)
245 .border_type(BorderType::Rounded)
246 .border_style(Style::default().fg(theme.border_active))
247 .title(format!(" {} ", data.title))
248 .title_style(
249 Style::default()
250 .fg(theme.title)
251 .add_modifier(Modifier::BOLD),
252 )
253 .style(Style::default().bg(theme.background)),
254 dialog_area,
255 );
256
257 let inner = dialog_area.inner(Margin {
258 horizontal: 2,
259 vertical: 1,
260 });
261
262 if data.is_loading {
263 f.render_widget(
264 Paragraph::new(data.message.as_str())
265 .style(
266 Style::default()
267 .fg(theme.text)
268 .add_modifier(Modifier::ITALIC),
269 )
270 .alignment(Alignment::Center)
271 .wrap(Wrap { trim: true }),
272 inner,
273 );
274 return;
275 }
276
277 let mut constraints = vec![Constraint::Min(message_height.max(1))];
278 if button_row_height > 0 {
279 constraints.push(Constraint::Length(button_row_height));
280 }
281 let chunks = Layout::default()
282 .direction(Direction::Vertical)
283 .constraints(constraints)
284 .split(inner);
285
286 f.render_widget(
287 Paragraph::new(Text::from(data.message.as_str()))
288 .style(Style::default().fg(theme.text))
289 .alignment(Alignment::Center)
290 .wrap(Wrap { trim: true }),
291 chunks[0],
292 );
293
294 if data.buttons.is_empty() || chunks.len() < 2 {
295 return;
296 }
297
298 let count = data.buttons.len();
299 let button_chunks = Layout::default()
300 .direction(Direction::Horizontal)
301 .constraints(vec![Constraint::Ratio(1, count as u32); count])
302 .horizontal_margin(1)
303 .split(chunks[1]);
304
305 for (i, label) in data.buttons.iter().enumerate() {
306 let active = i == active_button;
307 render_button(f, button_chunks[i], label.as_str(), active, theme);
308 }
309}
310
311pub fn render_dialog_error_with_button_renderer<D>(
315 f: &mut Frame,
316 area: Rect,
317 data: &DialogData<D>,
318 active_button: usize,
319 styles: &ThemeStyles,
320 render_button: &mut DialogButtonRenderer<'_>,
321) {
322 render_dialog_with_button_renderer(
323 f,
324 area,
325 data,
326 active_button,
327 &DialogTheme::error_styled(styles),
328 render_button,
329 );
330}
331
332pub fn render_dialog_success_with_button_renderer<D>(
336 f: &mut Frame,
337 area: Rect,
338 data: &DialogData<D>,
339 active_button: usize,
340 styles: &ThemeStyles,
341 render_button: &mut DialogButtonRenderer<'_>,
342) {
343 render_dialog_with_button_renderer(
344 f,
345 area,
346 data,
347 active_button,
348 &DialogTheme::success_styled(styles),
349 render_button,
350 );
351}
352
353fn render_default_dialog_button(
354 f: &mut Frame,
355 area: Rect,
356 label: &str,
357 active: bool,
358 theme: &DialogTheme,
359) {
360 let (text_style, border_style) = if active {
361 (
362 Style::default()
363 .fg(theme.button_active)
364 .add_modifier(Modifier::BOLD),
365 Style::default().fg(theme.border_active),
366 )
367 } else {
368 (
369 Style::default().fg(theme.button),
370 Style::default().fg(theme.border),
371 )
372 };
373 f.render_widget(
374 Paragraph::new(label)
375 .alignment(Alignment::Center)
376 .style(text_style)
377 .block(
378 Block::default()
379 .borders(Borders::ALL)
380 .border_style(border_style),
381 ),
382 area,
383 );
384}