1use super::props::{INPUT_INVALID_STYLE, INPUT_PLACEHOLDER, INPUT_PLACEHOLDER_STYLE};
7use crate::utils::calc_utf8_cursor_position;
8use tuirealm::command::{Cmd, CmdResult, Direction, Position};
9use tuirealm::props::{
10 Alignment, AttrValue, Attribute, Borders, Color, InputType, Props, Style, TextModifiers,
11};
12use tuirealm::ratatui::{layout::Rect, widgets::Paragraph};
13use tuirealm::{Frame, MockComponent, State, StateValue};
14
15#[derive(Default)]
18pub struct InputStates {
19 pub input: Vec<char>, pub cursor: usize, }
22
23impl InputStates {
24 pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
28 if self.input.len() < max_len.unwrap_or(usize::MAX) {
30 if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
32 self.input.insert(self.cursor, ch);
33 self.incr_cursor();
34 }
35 }
36 }
37
38 pub fn backspace(&mut self) {
42 if self.cursor > 0 && !self.input.is_empty() {
43 self.input.remove(self.cursor - 1);
44 self.cursor -= 1;
46 }
47 }
48
49 pub fn delete(&mut self) {
53 if self.cursor < self.input.len() {
54 self.input.remove(self.cursor);
55 }
56 }
57
58 pub fn incr_cursor(&mut self) {
62 if self.cursor < self.input.len() {
63 self.cursor += 1;
64 }
65 }
66
67 pub fn cursor_at_begin(&mut self) {
71 self.cursor = 0;
72 }
73
74 pub fn cursor_at_end(&mut self) {
78 self.cursor = self.input.len();
79 }
80
81 pub fn decr_cursor(&mut self) {
85 if self.cursor > 0 {
86 self.cursor -= 1;
87 }
88 }
89
90 #[must_use]
94 pub fn render_value(&self, itype: InputType) -> String {
95 self.render_value_chars(itype).iter().collect::<String>()
96 }
97
98 #[must_use]
102 pub fn render_value_chars(&self, itype: InputType) -> Vec<char> {
103 match itype {
104 InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
105 (0..self.input.len()).map(|_| ch).collect()
106 }
107 _ => self.input.clone(),
108 }
109 }
110
111 #[must_use]
115 pub fn get_value(&self) -> String {
116 self.input.iter().collect()
117 }
118}
119
120#[derive(Default)]
126#[must_use]
127pub struct Input {
128 props: Props,
129 pub states: InputStates,
130}
131
132impl Input {
133 pub fn foreground(mut self, fg: Color) -> Self {
134 self.attr(Attribute::Foreground, AttrValue::Color(fg));
135 self
136 }
137
138 pub fn background(mut self, bg: Color) -> Self {
139 self.attr(Attribute::Background, AttrValue::Color(bg));
140 self
141 }
142
143 pub fn inactive(mut self, s: Style) -> Self {
144 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
145 self
146 }
147
148 pub fn borders(mut self, b: Borders) -> Self {
149 self.attr(Attribute::Borders, AttrValue::Borders(b));
150 self
151 }
152
153 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
154 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
155 self
156 }
157
158 pub fn input_type(mut self, itype: InputType) -> Self {
159 self.attr(Attribute::InputType, AttrValue::InputType(itype));
160 self
161 }
162
163 pub fn input_len(mut self, ilen: usize) -> Self {
164 self.attr(Attribute::InputLength, AttrValue::Length(ilen));
165 self
166 }
167
168 pub fn value<S: Into<String>>(mut self, s: S) -> Self {
169 self.attr(Attribute::Value, AttrValue::String(s.into()));
170 self
171 }
172
173 pub fn invalid_style(mut self, s: Style) -> Self {
174 self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
175 self
176 }
177
178 pub fn placeholder<S: Into<String>>(mut self, placeholder: S, style: Style) -> Self {
179 self.attr(
180 Attribute::Custom(INPUT_PLACEHOLDER),
181 AttrValue::String(placeholder.into()),
182 );
183 self.attr(
184 Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
185 AttrValue::Style(style),
186 );
187 self
188 }
189
190 fn get_input_len(&self) -> Option<usize> {
191 self.props
192 .get(Attribute::InputLength)
193 .map(|x| x.unwrap_length())
194 }
195
196 fn get_input_type(&self) -> InputType {
197 self.props
198 .get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
199 .unwrap_input_type()
200 }
201
202 fn is_valid(&self) -> bool {
206 let value = self.states.get_value();
207 self.get_input_type().validate(value.as_str())
208 }
209}
210
211impl MockComponent for Input {
212 fn view(&mut self, render: &mut Frame, area: Rect) {
213 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
214 let mut foreground = self
215 .props
216 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
217 .unwrap_color();
218 let mut background = self
219 .props
220 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
221 .unwrap_color();
222 let modifiers = self
223 .props
224 .get_or(
225 Attribute::TextProps,
226 AttrValue::TextModifiers(TextModifiers::empty()),
227 )
228 .unwrap_text_modifiers();
229 let title = crate::utils::get_title_or_center(&self.props);
230 let borders = self
231 .props
232 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
233 .unwrap_borders();
234 let focus = self
235 .props
236 .get_or(Attribute::Focus, AttrValue::Flag(false))
237 .unwrap_flag();
238 let inactive_style = self
239 .props
240 .get(Attribute::FocusStyle)
241 .map(|x| x.unwrap_style());
242 let itype = self.get_input_type();
243 let mut block = crate::utils::get_block(borders, Some(&title), focus, inactive_style);
244 if focus && !self.is_valid() {
246 if let Some(style) = self
247 .props
248 .get(Attribute::Custom(INPUT_INVALID_STYLE))
249 .map(|x| x.unwrap_style())
250 {
251 let borders = self
252 .props
253 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
254 .unwrap_borders()
255 .color(style.fg.unwrap_or(Color::Reset));
256 block = crate::utils::get_block(borders, Some(&title), focus, None);
257 foreground = style.fg.unwrap_or(Color::Reset);
258 background = style.bg.unwrap_or(Color::Reset);
259 }
260 }
261 let text_to_display = self.states.render_value(self.get_input_type());
262 let show_placeholder = text_to_display.is_empty();
263 let text_to_display = if show_placeholder {
265 self.props
266 .get_or(
267 Attribute::Custom(INPUT_PLACEHOLDER),
268 AttrValue::String(String::new()),
269 )
270 .unwrap_string()
271 } else {
272 text_to_display
273 };
274 let paragraph_style = if focus {
276 Style::default()
277 .fg(foreground)
278 .bg(background)
279 .add_modifier(modifiers)
280 } else {
281 inactive_style.unwrap_or_default()
282 };
283 let paragraph_style = if show_placeholder {
284 self.props
285 .get_or(
286 Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
287 AttrValue::Style(paragraph_style),
288 )
289 .unwrap_style()
290 } else {
291 paragraph_style
292 };
293 let block_inner_area = block.inner(area);
295 let p: Paragraph = Paragraph::new(text_to_display)
296 .style(paragraph_style)
297 .block(block);
298 render.render_widget(p, area);
299 if focus {
301 let x: u16 = block_inner_area.x
302 + calc_utf8_cursor_position(
303 &self.states.render_value_chars(itype)[0..self.states.cursor],
304 );
305 render
306 .set_cursor_position(tuirealm::ratatui::prelude::Position { x, y: area.y + 1 });
307 }
308 }
309 }
310
311 fn query(&self, attr: Attribute) -> Option<AttrValue> {
312 self.props.get(attr)
313 }
314
315 fn attr(&mut self, attr: Attribute, value: AttrValue) {
316 let sanitize_input = matches!(
317 attr,
318 Attribute::InputLength | Attribute::InputType | Attribute::Value
319 );
320 let new_input = match attr {
322 Attribute::Value => Some(value.clone().unwrap_string()),
323 _ => None,
324 };
325 self.props.set(attr, value);
326 if sanitize_input {
327 let input = match new_input {
328 None => self.states.input.clone(),
329 Some(v) => v.chars().collect(),
330 };
331 self.states.input = Vec::new();
332 self.states.cursor = 0;
333 let itype = self.get_input_type();
334 let max_len = self.get_input_len();
335 for ch in input {
336 self.states.append(ch, &itype, max_len);
337 }
338 }
339 }
340
341 fn state(&self) -> State {
342 if self.is_valid() {
344 State::One(StateValue::String(self.states.get_value()))
345 } else {
346 State::None
347 }
348 }
349
350 fn perform(&mut self, cmd: Cmd) -> CmdResult {
351 match cmd {
352 Cmd::Delete => {
353 let prev_input = self.states.input.clone();
355 self.states.backspace();
356 if prev_input == self.states.input {
357 CmdResult::None
358 } else {
359 CmdResult::Changed(self.state())
360 }
361 }
362 Cmd::Cancel => {
363 let prev_input = self.states.input.clone();
365 self.states.delete();
366 if prev_input == self.states.input {
367 CmdResult::None
368 } else {
369 CmdResult::Changed(self.state())
370 }
371 }
372 Cmd::Submit => CmdResult::Submit(self.state()),
373 Cmd::Move(Direction::Left) => {
374 self.states.decr_cursor();
375 CmdResult::None
376 }
377 Cmd::Move(Direction::Right) => {
378 self.states.incr_cursor();
379 CmdResult::None
380 }
381 Cmd::GoTo(Position::Begin) => {
382 self.states.cursor_at_begin();
383 CmdResult::None
384 }
385 Cmd::GoTo(Position::End) => {
386 self.states.cursor_at_end();
387 CmdResult::None
388 }
389 Cmd::Type(ch) => {
390 let prev_input = self.states.input.clone();
392 self.states
393 .append(ch, &self.get_input_type(), self.get_input_len());
394 if prev_input == self.states.input {
396 CmdResult::None
397 } else {
398 CmdResult::Changed(self.state())
399 }
400 }
401 _ => CmdResult::None,
402 }
403 }
404}
405
406#[cfg(test)]
407mod tests {
408
409 use super::*;
410
411 use pretty_assertions::assert_eq;
412
413 #[test]
414 fn test_components_input_states() {
415 let mut states: InputStates = InputStates::default();
416 states.append('a', &InputType::Text, Some(3));
417 assert_eq!(states.input, vec!['a']);
418 states.append('b', &InputType::Text, Some(3));
419 assert_eq!(states.input, vec!['a', 'b']);
420 states.append('c', &InputType::Text, Some(3));
421 assert_eq!(states.input, vec!['a', 'b', 'c']);
422 states.append('d', &InputType::Text, Some(3));
424 assert_eq!(states.input, vec!['a', 'b', 'c']);
425 states.append('d', &InputType::Number, None);
427 assert_eq!(states.input, vec!['a', 'b', 'c']);
428 states.decr_cursor();
431 assert_eq!(states.cursor, 2);
432 states.cursor = 1;
433 states.decr_cursor();
434 assert_eq!(states.cursor, 0);
435 states.decr_cursor();
436 assert_eq!(states.cursor, 0);
437 states.incr_cursor();
439 assert_eq!(states.cursor, 1);
440 states.incr_cursor();
441 assert_eq!(states.cursor, 2);
442 states.incr_cursor();
443 assert_eq!(states.cursor, 3);
444 assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
446 assert_eq!(
447 states.render_value(InputType::Password('*')).as_str(),
448 "***"
449 );
450 }
451
452 #[test]
453 fn test_components_input_text() {
454 let mut component: Input = Input::default()
456 .background(Color::Yellow)
457 .borders(Borders::default())
458 .foreground(Color::Cyan)
459 .inactive(Style::default())
460 .input_len(5)
461 .input_type(InputType::Text)
462 .title("pippo", Alignment::Center)
463 .value("home");
464 assert_eq!(component.states.cursor, 4);
466 assert_eq!(component.states.input.len(), 4);
467 assert_eq!(
469 component.state(),
470 State::One(StateValue::String(String::from("home")))
471 );
472 assert_eq!(
474 component.perform(Cmd::Type('/')),
475 CmdResult::Changed(State::One(StateValue::String(String::from("home/"))))
476 );
477 assert_eq!(
478 component.state(),
479 State::One(StateValue::String(String::from("home/")))
480 );
481 assert_eq!(component.states.cursor, 5);
482 assert_eq!(component.perform(Cmd::Type('a')), CmdResult::None);
484 assert_eq!(
485 component.state(),
486 State::One(StateValue::String(String::from("home/")))
487 );
488 assert_eq!(component.states.cursor, 5);
489 assert_eq!(
491 component.perform(Cmd::Submit),
492 CmdResult::Submit(State::One(StateValue::String(String::from("home/"))))
493 );
494 assert_eq!(
496 component.perform(Cmd::Delete),
497 CmdResult::Changed(State::One(StateValue::String(String::from("home"))))
498 );
499 assert_eq!(
500 component.state(),
501 State::One(StateValue::String(String::from("home")))
502 );
503 assert_eq!(component.states.cursor, 4);
504 component.states.input = vec!['h'];
506 component.states.cursor = 1;
507 assert_eq!(
508 component.perform(Cmd::Delete),
509 CmdResult::Changed(State::One(StateValue::String(String::new())))
510 );
511 assert_eq!(
512 component.state(),
513 State::One(StateValue::String(String::new()))
514 );
515 assert_eq!(component.states.cursor, 0);
516 assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
518 assert_eq!(
519 component.state(),
520 State::One(StateValue::String(String::new()))
521 );
522 assert_eq!(component.states.cursor, 0);
523 assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
525 assert_eq!(
526 component.state(),
527 State::One(StateValue::String(String::new()))
528 );
529 assert_eq!(component.states.cursor, 0);
530 component.states.input = vec!['h', 'e'];
532 component.states.cursor = 1;
533 assert_eq!(
534 component.perform(Cmd::Cancel),
535 CmdResult::Changed(State::One(StateValue::String(String::from("h"))))
536 );
537 assert_eq!(
538 component.state(),
539 State::One(StateValue::String(String::from("h")))
540 );
541 assert_eq!(component.states.cursor, 1);
542 assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
544 assert_eq!(
545 component.state(),
546 State::One(StateValue::String(String::from("h")))
547 );
548 assert_eq!(component.states.cursor, 1);
549 component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
551 component.attr(Attribute::InputLength, AttrValue::Length(16));
553 component.states.cursor = 1;
554 assert_eq!(
555 component.perform(Cmd::Move(Direction::Right)), CmdResult::None
557 );
558 assert_eq!(component.states.cursor, 2);
559 assert_eq!(
561 component.perform(Cmd::Type('a')),
562 CmdResult::Changed(State::One(StateValue::String(String::from("heallo"))))
563 );
564 assert_eq!(
565 component.state(),
566 State::One(StateValue::String(String::from("heallo")))
567 );
568 assert_eq!(component.states.cursor, 3);
569 assert_eq!(
571 component.perform(Cmd::Move(Direction::Left)),
572 CmdResult::None
573 );
574 assert_eq!(component.states.cursor, 2);
575 component.states.cursor = 6;
577 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
579 assert_eq!(component.states.cursor, 6);
580 assert_eq!(
582 component.perform(Cmd::Move(Direction::Left)),
583 CmdResult::None
584 );
585 assert_eq!(component.states.cursor, 5);
586 component.states.cursor = 0;
588 assert_eq!(
589 component.perform(Cmd::Move(Direction::Left)),
590 CmdResult::None
591 );
592 assert_eq!(component.states.cursor, 0);
594 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
596 assert_eq!(component.states.cursor, 6);
597 assert_eq!(
598 component.perform(Cmd::GoTo(Position::Begin)),
599 CmdResult::None
600 );
601 assert_eq!(component.states.cursor, 0);
602 component.attr(Attribute::Value, AttrValue::String("new-value".to_string()));
604 assert_eq!(
605 component.state(),
606 State::One(StateValue::String(String::from("new-value")))
607 );
608 component.attr(
610 Attribute::InputType,
611 AttrValue::InputType(InputType::Number),
612 );
613 assert_eq!(component.state(), State::None);
614 }
615}