1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4
5use std::mem;
6
7use crossterm::event::KeyCode;
8use ratatui::widgets::{Block, Borders, Clear, Paragraph};
9use ratatui_core::{
10 buffer::Buffer,
11 layout::{Alignment, Constraint, Layout, Rect},
12 style::{Color, Style, Stylize},
13 text::{Line, Span},
14 widgets::Widget,
15};
16
17pub const BOTTOM_TITLE: &str = "Press Enter to submit or Esc to abort";
19
20#[derive(PartialEq, Default, Clone)]
22pub struct Dialog {
23 pub open: bool,
25 pub submitted: bool,
27 pub working_input: String,
31 pub submitted_input: String,
34 cursor_position: usize,
35 title_top: Option<String>,
36 title_bottom: Option<String>,
37 borders: Option<Borders>,
38 style: Option<Style>,
39}
40
41impl Dialog {
42 pub fn key_action(&mut self, key_code: &KeyCode) {
44 self.submitted = false;
45 match key_code {
46 KeyCode::Char(to_insert) => self.insert_char(*to_insert),
47 KeyCode::Backspace => self.backspace(),
48 KeyCode::Delete => self.delete(),
49 KeyCode::End => self.end(),
50 KeyCode::Home => self.home(),
51 KeyCode::Left => self.move_cursor_left(),
52 KeyCode::Right => self.move_cursor_right(),
53 KeyCode::Enter => {
54 self.submitted_input = mem::take(&mut self.working_input);
57 self.submitted_input = self.submitted_input.trim().to_string();
58
59 self.submitted = true;
62 self.open = false;
63 self.cursor_position = 0;
64 }
65 KeyCode::Esc => {
66 self.working_input.clear();
67 self.open = false;
68 self.cursor_position = 0;
69 }
70 _ => (),
71 }
72 }
73
74 pub fn title_top(&mut self, title: &str) -> Self {
78 self.title_top = Some(title.to_owned());
79 self.clone()
80 }
81
82 pub fn title_bottom(&mut self, title: &str) -> Self {
86 self.title_bottom = Some(title.to_owned());
87 self.clone()
88 }
89
90 pub fn borders(&mut self, borders: Borders) -> Self {
94 self.borders = Some(borders);
95 self.clone()
96 }
97
98 pub fn style(&mut self, style: Style) -> Self {
103 self.style = Some(style);
104 self.clone()
105 }
106
107 fn render_working_input(&self) -> Line<'_> {
109 let text = format!("{} ", self.working_input);
111 let text_len = text.chars().count();
112 let text = text.chars();
113
114 let before_cursor = Span::raw(text.clone().take(self.cursor_position).collect::<String>());
116
117 let under_cursor = Span::raw(
118 text.clone()
119 .skip(self.cursor_position)
120 .take(1)
121 .collect::<String>(),
122 )
123 .reversed();
124
125 let after_cursor = if self.cursor_position != text_len {
126 Span::raw(
127 text.clone()
128 .skip(self.cursor_position + 1)
129 .collect::<String>(),
130 )
131 } else {
132 Span::raw("")
133 };
134
135 Line::from(vec![before_cursor, under_cursor, after_cursor])
136 }
137
138 fn move_cursor_left(&mut self) {
139 let cursor_moved_left = self.cursor_position.saturating_sub(1);
140 self.cursor_position = self.clamp_cursor(cursor_moved_left);
141 }
142
143 fn move_cursor_right(&mut self) {
144 let cursor_moved_right = self.cursor_position.saturating_add(1);
145 self.cursor_position = self.clamp_cursor(cursor_moved_right);
146 }
147
148 fn insert_char(&mut self, new_char: char) {
149 self.working_input.insert(self.cursor_position, new_char);
150 self.move_cursor_right();
151 }
152
153 fn backspace(&mut self) {
154 if self.cursor_position != 0 {
155 let current_index = self.cursor_position;
156 let from_left_to_current_index = current_index - 1;
157
158 let before_char_to_delete = self.working_input.chars().take(from_left_to_current_index);
160
161 let after_char_to_delete = self.working_input.chars().skip(current_index);
163
164 self.working_input = before_char_to_delete.chain(after_char_to_delete).collect();
166 self.move_cursor_left();
167 }
168 }
169
170 fn delete(&mut self) {
171 let current_index = self.cursor_position;
172
173 let before_char_to_delete = self.working_input.chars().take(current_index);
175
176 let after_char_to_delete = self.working_input.chars().skip(current_index + 1);
178
179 self.working_input = before_char_to_delete.chain(after_char_to_delete).collect();
181 }
182
183 fn end(&mut self) {
184 self.cursor_position = self.working_input.chars().count();
185 }
186
187 fn home(&mut self) {
188 self.cursor_position = 0;
189 }
190
191 fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
192 new_cursor_pos.clamp(0, self.working_input.len())
193 }
194}
195
196impl Widget for Dialog {
197 fn render(mut self, area: Rect, buf: &mut Buffer) {
198 if self.open {
199 Clear.render(area, buf);
200
201 let mut dialog_block = Block::default().title_alignment(Alignment::Center);
202
203 if let Some(ref mut v) = self.title_top {
204 dialog_block = dialog_block.title_top(mem::take(v))
205 }
206
207 dialog_block = if let Some(ref mut v) = self.title_bottom {
208 dialog_block.title_bottom(mem::take(v))
209 } else {
210 dialog_block.title_bottom(BOTTOM_TITLE)
211 };
212
213 dialog_block = if let Some(ref mut v) = self.borders {
214 dialog_block.borders(mem::take(v))
215 } else {
216 dialog_block.borders(Borders::ALL)
217 };
218
219 dialog_block = if let Some(ref mut v) = self.style {
220 dialog_block.style(mem::take(v))
221 } else {
222 dialog_block.style(Style::default().bg(Color::DarkGray))
223 };
224
225 Paragraph::new(self.render_working_input())
226 .block(dialog_block)
227 .render(area, buf)
228 }
229 }
230}
231
232pub fn centered_rect(
241 r: Rect,
242 width: u16,
243 height: u16,
244 horizontal_offset: i16,
245 vertical_offset: i16,
246) -> Rect {
247 let (dialog_layout, index) = if vertical_offset.is_negative() {
250 (
251 Layout::vertical([
252 Constraint::Fill(1),
253 Constraint::Length(height),
254 Constraint::Fill(1),
255 Constraint::Length(vertical_offset.unsigned_abs()),
256 ])
257 .split(r),
258 1,
259 )
260 } else {
261 (
262 Layout::vertical([
263 Constraint::Length(vertical_offset as u16),
264 Constraint::Fill(1),
265 Constraint::Length(height),
266 Constraint::Fill(1),
267 ])
268 .split(r),
269 2,
270 )
271 };
272
273 if horizontal_offset.is_negative() {
275 Layout::horizontal([
276 Constraint::Fill(1),
277 Constraint::Length(width),
278 Constraint::Fill(1),
279 Constraint::Length(horizontal_offset.unsigned_abs()),
280 ])
281 .split(dialog_layout[index])[1]
282 } else {
283 Layout::horizontal([
284 Constraint::Length(horizontal_offset as u16),
285 Constraint::Fill(1),
286 Constraint::Length(width),
287 Constraint::Fill(1),
288 ])
289 .split(dialog_layout[index])[2]
290 }
291}