tongo/components/input/
input_modal.rs1use crate::{
2 components::{
3 tab::{CloneWithFocus, TabFocus},
4 Component,
5 },
6 config::Config,
7 system::{
8 command::{Command, CommandCategory, CommandGroup},
9 event::Event,
10 message::{AppAction, Message},
11 signal::SignalQueue,
12 },
13};
14use ratatui::prelude::*;
15use std::{cell::Cell, rc::Rc};
16
17use super::{DefaultFormatter, InnerInput};
18
19const INPUT_MODAL_WIDTH: u16 = 40;
20const INPUT_MODAL_HEIGHT: u16 = 1;
21
22#[derive(Debug, Clone, Copy)]
23pub enum InputKind {
24 NewCollectionName,
25 NewDatabaseName,
26}
27
28impl InputKind {
29 const fn modal_title(self) -> &'static str {
30 match self {
31 Self::NewCollectionName => "New Connection's Name",
32 Self::NewDatabaseName => "New Database's Name",
33 }
34 }
35}
36
37#[derive(Debug, Default, Clone)]
38pub struct InputModal {
39 focus: Rc<Cell<TabFocus>>,
40 kind: Option<InputKind>,
41 input: InnerInput<DefaultFormatter>,
42}
43
44impl CloneWithFocus for InputModal {
45 fn clone_with_focus(&self, focus: Rc<Cell<TabFocus>>) -> Self {
46 Self {
47 focus,
48 ..self.clone()
49 }
50 }
51}
52
53impl InputModal {
54 pub fn new(
55 focus: Rc<Cell<TabFocus>>,
56 cursor_pos: Rc<Cell<(u16, u16)>>,
57 config: Config,
58 ) -> Self {
59 let mut input = InnerInput::new("", cursor_pos, config, DefaultFormatter::default());
60 input.start_editing();
61
62 Self {
63 focus,
64 input,
65 ..Default::default()
66 }
67 }
68
69 pub fn show_with(&mut self, kind: InputKind) {
70 self.input.set_title(kind.modal_title());
73
74 self.kind = Some(kind);
75 self.focus();
76 }
77}
78
79impl Component for InputModal {
80 fn is_focused(&self) -> bool {
81 self.focus.get() == TabFocus::InputModal
82 }
83
84 fn focus(&self) {
85 self.focus.set(TabFocus::InputModal);
86 }
87
88 fn render(&mut self, frame: &mut Frame, area: Rect) {
89 let layout = Layout::vertical(vec![
90 Constraint::Fill(1),
91 Constraint::Length(INPUT_MODAL_HEIGHT + 2),
92 Constraint::Fill(1),
93 ])
94 .split(area);
95 let layout = Layout::horizontal(vec![
96 Constraint::Fill(1),
97 Constraint::Length(INPUT_MODAL_WIDTH + 2),
98 Constraint::Fill(1),
99 ])
100 .split(layout[1]);
101
102 self.input.render(frame, layout[1], true);
103 }
104
105 fn commands(&self) -> Vec<CommandGroup> {
106 match self.kind {
107 Some(InputKind::NewCollectionName) => vec![
108 CommandGroup::new(vec![Command::Confirm], "create collection")
109 .in_cat(CommandCategory::StatusBarOnly),
110 CommandGroup::new(vec![Command::Back], "cancel")
111 .in_cat(CommandCategory::StatusBarOnly),
112 ],
113 Some(InputKind::NewDatabaseName) => vec![
114 CommandGroup::new(vec![Command::Confirm], "create database")
115 .in_cat(CommandCategory::StatusBarOnly),
116 CommandGroup::new(vec![Command::Back], "cancel")
117 .in_cat(CommandCategory::StatusBarOnly),
118 ],
119 _ => vec![],
120 }
121 }
122
123 fn handle_raw_event(&mut self, event: &crossterm::event::Event, queue: &mut SignalQueue) {
124 self.input.handle_raw_event(event, queue);
125 }
126
127 fn handle_command(&mut self, command: &Command, queue: &mut SignalQueue) {
128 match command {
129 Command::Confirm => {
130 let value = self.input.value().to_string();
131 self.input.set_value("");
132 queue.push(Event::InputConfirmed(
133 self.kind.expect("input should not be shown without a kind"),
134 value,
135 ));
136 queue.push(Message::to_app(AppAction::ExitRawMode));
137 }
138 Command::Back => {
139 self.input.set_value("");
140 queue.push(Event::InputCanceled);
141 queue.push(Message::to_app(AppAction::ExitRawMode));
142 }
143 _ => {}
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::testing::ComponentTestHarness;
152
153 #[test]
154 fn enter_text_then_confirm() {
155 let mut test = ComponentTestHarness::new(InputModal::default());
156
157 test.component_mut().input.start_editing();
158 test.component_mut().show_with(InputKind::NewCollectionName);
159
160 test.given_string("text!");
161 test.given_command(Command::Confirm);
162
163 test.expect_event(
164 |e| matches!(e, Event::InputConfirmed(InputKind::NewCollectionName, s) if s == "text!"),
165 );
166 test.expect_message(|m| matches!(m.read_as_app(), Some(AppAction::ExitRawMode)));
167 assert_eq!(test.component_mut().input.value(), "");
168 }
169
170 #[test]
171 fn enter_text_then_cancel() {
172 let mut test = ComponentTestHarness::new(InputModal::default());
173
174 test.component_mut().input.start_editing();
175 test.component_mut().show_with(InputKind::NewCollectionName);
176
177 test.given_string("text!");
178 test.given_command(Command::Back);
179
180 test.expect_event(|e| matches!(e, Event::InputCanceled));
181 test.expect_message(|m| matches!(m.read_as_app(), Some(AppAction::ExitRawMode)));
182 assert_eq!(test.component_mut().input.value(), "");
183 }
184}