termitype/tui/components/
modal_dialog.rs1use 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), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
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), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
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)"), ModalContext::CustomWordCount => (4, " word(s)"), ModalContext::CustomLineCount => (2, " line(s)"), _ => 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}