Skip to main content

termitype/tui/components/
modal_dialog.rs

1use crate::{
2    modal::{Modal, ModalContext, ModalKind},
3    theme::Theme,
4    tui::helpers,
5};
6use ratatui::{
7    Frame,
8    layout::{Alignment, Constraint, Direction, Layout, Rect},
9    style::{Modifier, Style},
10    text::{Line, Span},
11    widgets::{Block, BorderType, Borders, Clear, Paragraph},
12};
13
14pub struct ModalDialog;
15
16impl ModalDialog {
17    pub fn render(f: &mut Frame, modal: &Modal, theme: &Theme, area: Rect) {
18        let (w, h) = match modal.kind {
19            ModalKind::Input => (60, 12),
20            ModalKind::Confirmation => (60, 10),
21        };
22
23        let modal_area = helpers::centered_fixed_rect(w, h, area);
24        f.render_widget(Clear, modal_area);
25
26        let border_style = Style::default()
27            .fg(theme.border())
28            .add_modifier(Modifier::DIM);
29
30        let block = Block::default()
31            .borders(Borders::ALL)
32            .border_type(BorderType::Rounded)
33            .border_style(border_style)
34            .style(Style::default().bg(theme.bg()));
35
36        let inner_area = block.inner(modal_area);
37        f.render_widget(block, modal_area);
38        Self::render_modal_dialog(f, modal, theme, inner_area);
39    }
40
41    fn render_modal_dialog(f: &mut Frame, modal: &Modal, theme: &Theme, area: Rect) {
42        match modal.kind {
43            ModalKind::Input => Self::render_input_kind_modal(f, modal, theme, area),
44            ModalKind::Confirmation => Self::render_confirmation_kind_modal(f, modal, theme, area),
45        }
46    }
47
48    fn render_input_kind_modal(f: &mut Frame, modal: &Modal, theme: &Theme, area: Rect) {
49        let layout = Layout::default()
50            .direction(Direction::Vertical)
51            .margin(1)
52            .constraints(vec![
53                Constraint::Length(1), // title
54                Constraint::Length(1), // space
55                Constraint::Length(1), // desc
56                Constraint::Length(1), // gap
57                Constraint::Length(1), // input
58                Constraint::Length(1), // gap/error
59                Constraint::Length(1), // gap
60                Constraint::Length(1), // ok button
61            ])
62            .split(area);
63
64        Self::render_modal_title(f, &modal.title, theme, layout[0]);
65        Self::render_modal_description(f, &modal.description, theme, layout[2]);
66        Self::render_modal_input_field(f, modal, theme, layout[4]);
67        Self::render_modal_error_message(f, modal, theme, layout[5]);
68        Self::render_button(f, &modal.kind, theme, layout[7]);
69    }
70
71    fn render_confirmation_kind_modal(f: &mut Frame, modal: &Modal, theme: &Theme, area: Rect) {
72        let layout = Layout::default()
73            .direction(Direction::Vertical)
74            .margin(1)
75            .constraints(vec![
76                Constraint::Length(1), // title
77                Constraint::Length(1), // gap
78                Constraint::Length(1), // desc
79                Constraint::Length(1), // gap
80                Constraint::Length(1), // gap
81                Constraint::Length(1), // yes button
82            ])
83            .split(area);
84
85        Self::render_modal_title(f, &modal.title, theme, layout[0]);
86        Self::render_modal_description(f, &modal.description, theme, layout[2]);
87        Self::render_button(f, &modal.kind, theme, layout[5]);
88    }
89
90    fn render_modal_title(f: &mut Frame, title: &str, theme: &Theme, area: Rect) {
91        let title = Paragraph::new(title)
92            .style(Style::default().fg(theme.highlight()))
93            .alignment(Alignment::Center);
94        f.render_widget(title, area);
95    }
96
97    fn render_modal_description(frame: &mut Frame, description: &str, theme: &Theme, area: Rect) {
98        let desc = Paragraph::new(description)
99            .style(Style::default().fg(theme.fg()))
100            .alignment(Alignment::Center);
101        frame.render_widget(desc, area);
102    }
103
104    fn render_modal_input_field(f: &mut Frame, modal: &Modal, theme: &Theme, area: Rect) {
105        let input_style = Style::default().fg(theme.fg());
106        let cursor_style = Style::default().fg(theme.cursor_text()).bg(theme.cursor());
107        let suffix_style = Style::default().fg(theme.fg()).add_modifier(Modifier::DIM);
108
109        let (width, suffix) = match modal.ctx {
110            ModalContext::CustomTime => (3, " second(s)"), // 300 is the max custom time
111            ModalContext::CustomWordCount => (4, " word(s)"), // 5000 is the max custom word count
112            ModalContext::CustomLineCount => (2, " line(s)"), // 10 is the max custom line
113            _ => unreachable!(),
114        };
115
116        if let Some(buffer) = modal.buffer.clone() {
117            let input_text = buffer.input;
118
119            let input_str = &input_text[..input_text.len().min(width)];
120            let cursor_pos = buffer.cursor_pos.min(width);
121
122            let total_width = width + suffix.len();
123            let left_padding = (area.width as usize).saturating_sub(total_width) / 2;
124
125            let input_spans = vec![
126                Span::raw(" ".repeat(left_padding)),
127                Span::styled(&input_str[..cursor_pos], input_style),
128                Span::styled(" ", cursor_style),
129                Span::styled(&input_str[cursor_pos..], input_style),
130                Span::raw(" ".repeat(width.saturating_sub(input_str.len()))),
131                Span::styled(suffix, suffix_style),
132            ];
133
134            let input_field = Paragraph::new(Line::from(input_spans));
135            f.render_widget(input_field, area);
136        }
137    }
138
139    fn render_modal_error_message(frame: &mut Frame, modal: &Modal, theme: &Theme, area: Rect) {
140        if let Some(buffer) = &modal.buffer {
141            if let Some(error_msg) = &buffer.error {
142                let error_text = Paragraph::new(error_msg.as_str())
143                    .style(Style::default().fg(theme.error()))
144                    .alignment(Alignment::Center);
145                frame.render_widget(error_text, area);
146            }
147        }
148    }
149
150    fn render_button(frame: &mut Frame, kind: &ModalKind, theme: &Theme, area: Rect) {
151        let text = match kind {
152            ModalKind::Input => "<OK>",
153            ModalKind::Confirmation => "<Yes>",
154        };
155        let style = Style::default()
156            .fg(theme.bg())
157            .bg(theme.highlight())
158            .add_modifier(Modifier::BOLD);
159        let padding = (area.width.saturating_sub(text.len() as u16)) / 2;
160        let line = Line::from(vec![
161            Span::raw(" ".repeat(padding as usize)),
162            Span::styled(text, style),
163        ]);
164        frame.render_widget(Paragraph::new(line), area);
165    }
166}