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
15const PREVIEW_DISTANCE: usize = 2;
19
20#[derive(Default, Debug)]
21pub struct InputStates {
22 pub input: Vec<char>,
24 pub cursor: usize,
26 pub display_offset: usize,
28 pub last_width: Option<u16>,
32}
33
34impl InputStates {
35 pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
39 if self.input.len() < max_len.unwrap_or(usize::MAX) {
41 if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
43 self.input.insert(self.cursor, ch);
44 self.incr_cursor();
45 }
46 }
47 }
48
49 pub fn backspace(&mut self) {
53 if self.cursor > 0 && !self.input.is_empty() {
54 self.input.remove(self.cursor - 1);
55 self.cursor -= 1;
57
58 if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
59 self.display_offset = self.display_offset.saturating_sub(1);
60 }
61 }
62 }
63
64 pub fn delete(&mut self) {
68 if self.cursor < self.input.len() {
69 self.input.remove(self.cursor);
70 }
71 }
72
73 pub fn incr_cursor(&mut self) {
77 if self.cursor < self.input.len() {
78 self.cursor += 1;
79
80 if let Some(last_width) = self.last_width {
81 let input_with_width = self.input.len().saturating_sub(
82 usize::from(self.last_width.unwrap_or_default())
83 .saturating_sub(PREVIEW_DISTANCE),
84 );
85 if self.cursor
88 > usize::from(last_width).saturating_sub(PREVIEW_DISTANCE) + self.display_offset
89 && self.display_offset < input_with_width
90 {
91 self.display_offset += 1;
92 }
93 }
94 }
95 }
96
97 pub fn cursor_at_begin(&mut self) {
101 self.cursor = 0;
102 self.display_offset = 0;
103 }
104
105 pub fn cursor_at_end(&mut self) {
109 self.cursor = self.input.len();
110 self.display_offset = self.input.len().saturating_sub(
111 usize::from(self.last_width.unwrap_or_default()).saturating_sub(PREVIEW_DISTANCE),
112 );
113 }
114
115 pub fn decr_cursor(&mut self) {
119 if self.cursor > 0 {
120 self.cursor -= 1;
121
122 if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
123 self.display_offset = self.display_offset.saturating_sub(1);
124 }
125 }
126 }
127
128 pub fn update_width(&mut self, new_width: u16) {
137 let old_width = self.last_width;
138 self.last_width = Some(new_width);
139
140 if self.cursor
142 > (self.display_offset + usize::from(new_width)).saturating_sub(PREVIEW_DISTANCE)
143 {
144 let diff = if let Some(old_width) = old_width {
145 usize::from(old_width.saturating_sub(new_width))
146 } else {
147 self.cursor.saturating_sub(usize::from(new_width))
151 };
152 self.display_offset += diff;
153 }
154 }
155
156 #[must_use]
160 pub fn render_value(&self, itype: InputType) -> String {
161 self.render_value_chars(itype).iter().collect::<String>()
162 }
163
164 #[must_use]
168 pub fn render_value_offset(&self, itype: InputType) -> String {
169 self.render_value_chars(itype)
170 .iter()
171 .skip(self.display_offset)
172 .collect()
173 }
174
175 #[must_use]
179 pub fn render_value_chars(&self, itype: InputType) -> Vec<char> {
180 match itype {
181 InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
182 (0..self.input.len()).map(|_| ch).collect()
183 }
184 _ => self.input.clone(),
185 }
186 }
187
188 #[must_use]
192 pub fn get_value(&self) -> String {
193 self.input.iter().collect()
194 }
195}
196
197#[derive(Default)]
203#[must_use]
204pub struct Input {
205 props: Props,
206 pub states: InputStates,
207}
208
209impl Input {
210 pub fn foreground(mut self, fg: Color) -> Self {
211 self.attr(Attribute::Foreground, AttrValue::Color(fg));
212 self
213 }
214
215 pub fn background(mut self, bg: Color) -> Self {
216 self.attr(Attribute::Background, AttrValue::Color(bg));
217 self
218 }
219
220 pub fn inactive(mut self, s: Style) -> Self {
221 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
222 self
223 }
224
225 pub fn borders(mut self, b: Borders) -> Self {
226 self.attr(Attribute::Borders, AttrValue::Borders(b));
227 self
228 }
229
230 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
231 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
232 self
233 }
234
235 pub fn input_type(mut self, itype: InputType) -> Self {
236 self.attr(Attribute::InputType, AttrValue::InputType(itype));
237 self
238 }
239
240 pub fn input_len(mut self, ilen: usize) -> Self {
241 self.attr(Attribute::InputLength, AttrValue::Length(ilen));
242 self
243 }
244
245 pub fn value<S: Into<String>>(mut self, s: S) -> Self {
246 self.attr(Attribute::Value, AttrValue::String(s.into()));
247 self
248 }
249
250 pub fn invalid_style(mut self, s: Style) -> Self {
251 self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
252 self
253 }
254
255 pub fn placeholder<S: Into<String>>(mut self, placeholder: S, style: Style) -> Self {
256 self.attr(
257 Attribute::Custom(INPUT_PLACEHOLDER),
258 AttrValue::String(placeholder.into()),
259 );
260 self.attr(
261 Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
262 AttrValue::Style(style),
263 );
264 self
265 }
266
267 fn get_input_len(&self) -> Option<usize> {
268 self.props
269 .get(Attribute::InputLength)
270 .map(|x| x.unwrap_length())
271 }
272
273 fn get_input_type(&self) -> InputType {
274 self.props
275 .get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
276 .unwrap_input_type()
277 }
278
279 fn is_valid(&self) -> bool {
283 let value = self.states.get_value();
284 self.get_input_type().validate(value.as_str())
285 }
286}
287
288impl MockComponent for Input {
289 fn view(&mut self, render: &mut Frame, area: Rect) {
290 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
291 let mut foreground = self
292 .props
293 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
294 .unwrap_color();
295 let mut background = self
296 .props
297 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
298 .unwrap_color();
299 let modifiers = self
300 .props
301 .get_or(
302 Attribute::TextProps,
303 AttrValue::TextModifiers(TextModifiers::empty()),
304 )
305 .unwrap_text_modifiers();
306 let title = crate::utils::get_title_or_center(&self.props);
307 let borders = self
308 .props
309 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
310 .unwrap_borders();
311 let focus = self
312 .props
313 .get_or(Attribute::Focus, AttrValue::Flag(false))
314 .unwrap_flag();
315 let inactive_style = self
316 .props
317 .get(Attribute::FocusStyle)
318 .map(|x| x.unwrap_style());
319 let itype = self.get_input_type();
320 let mut block = crate::utils::get_block(borders, Some(&title), focus, inactive_style);
321 if focus && !self.is_valid() {
323 if let Some(style) = self
324 .props
325 .get(Attribute::Custom(INPUT_INVALID_STYLE))
326 .map(|x| x.unwrap_style())
327 {
328 let borders = self
329 .props
330 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
331 .unwrap_borders()
332 .color(style.fg.unwrap_or(Color::Reset));
333 block = crate::utils::get_block(borders, Some(&title), focus, None);
334 foreground = style.fg.unwrap_or(Color::Reset);
335 background = style.bg.unwrap_or(Color::Reset);
336 }
337 }
338
339 let block_inner_area = block.inner(area);
341
342 self.states.update_width(block_inner_area.width);
343
344 let text_to_display = self.states.render_value_offset(self.get_input_type());
345
346 let show_placeholder = text_to_display.is_empty();
347 let text_to_display = if show_placeholder {
349 self.states.cursor = 0;
350 self.props
351 .get_or(
352 Attribute::Custom(INPUT_PLACEHOLDER),
353 AttrValue::String(String::new()),
354 )
355 .unwrap_string()
356 } else {
357 text_to_display
358 };
359 let paragraph_style = if focus {
361 Style::default()
362 .fg(foreground)
363 .bg(background)
364 .add_modifier(modifiers)
365 } else {
366 inactive_style.unwrap_or_default()
367 };
368 let paragraph_style = if show_placeholder {
369 self.props
370 .get_or(
371 Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
372 AttrValue::Style(paragraph_style),
373 )
374 .unwrap_style()
375 } else {
376 paragraph_style
377 };
378
379 let p: Paragraph = Paragraph::new(text_to_display)
380 .style(paragraph_style)
381 .block(block);
382 render.render_widget(p, area);
383
384 if focus && !block_inner_area.is_empty() {
386 let x: u16 = block_inner_area.x
387 + calc_utf8_cursor_position(
388 &self.states.render_value_chars(itype)[0..self.states.cursor],
389 )
390 .saturating_sub(u16::try_from(self.states.display_offset).unwrap_or(u16::MAX));
391 let x = x.min(block_inner_area.x + block_inner_area.width);
392 render.set_cursor_position(tuirealm::ratatui::prelude::Position {
393 x,
394 y: block_inner_area.y,
395 });
396 }
397 }
398 }
399
400 fn query(&self, attr: Attribute) -> Option<AttrValue> {
401 self.props.get(attr)
402 }
403
404 fn attr(&mut self, attr: Attribute, value: AttrValue) {
405 let sanitize_input = matches!(
406 attr,
407 Attribute::InputLength | Attribute::InputType | Attribute::Value
408 );
409 let new_input = match attr {
411 Attribute::Value => Some(value.clone().unwrap_string()),
412 _ => None,
413 };
414 self.props.set(attr, value);
415 if sanitize_input {
416 let input = match new_input {
417 None => self.states.input.clone(),
418 Some(v) => v.chars().collect(),
419 };
420 self.states.input = Vec::new();
421 self.states.cursor = 0;
422 let itype = self.get_input_type();
423 let max_len = self.get_input_len();
424 for ch in input {
425 self.states.append(ch, &itype, max_len);
426 }
427 }
428 }
429
430 fn state(&self) -> State {
431 if self.is_valid() {
433 State::One(StateValue::String(self.states.get_value()))
434 } else {
435 State::None
436 }
437 }
438
439 fn perform(&mut self, cmd: Cmd) -> CmdResult {
440 match cmd {
441 Cmd::Delete => {
442 let prev_input = self.states.input.clone();
444 self.states.backspace();
445 if prev_input == self.states.input {
446 CmdResult::None
447 } else {
448 CmdResult::Changed(self.state())
449 }
450 }
451 Cmd::Cancel => {
452 let prev_input = self.states.input.clone();
454 self.states.delete();
455 if prev_input == self.states.input {
456 CmdResult::None
457 } else {
458 CmdResult::Changed(self.state())
459 }
460 }
461 Cmd::Submit => CmdResult::Submit(self.state()),
462 Cmd::Move(Direction::Left) => {
463 self.states.decr_cursor();
464 CmdResult::None
465 }
466 Cmd::Move(Direction::Right) => {
467 self.states.incr_cursor();
468 CmdResult::None
469 }
470 Cmd::GoTo(Position::Begin) => {
471 self.states.cursor_at_begin();
472 CmdResult::None
473 }
474 Cmd::GoTo(Position::End) => {
475 self.states.cursor_at_end();
476 CmdResult::None
477 }
478 Cmd::Type(ch) => {
479 let prev_input = self.states.input.clone();
481 self.states
482 .append(ch, &self.get_input_type(), self.get_input_len());
483 if prev_input == self.states.input {
485 CmdResult::None
486 } else {
487 CmdResult::Changed(self.state())
488 }
489 }
490 _ => CmdResult::None,
491 }
492 }
493}
494
495#[cfg(test)]
496mod tests {
497
498 use super::*;
499
500 use pretty_assertions::assert_eq;
501
502 #[test]
503 fn test_components_input_states() {
504 let mut states: InputStates = InputStates::default();
505 states.append('a', &InputType::Text, Some(3));
506 assert_eq!(states.input, vec!['a']);
507 states.append('b', &InputType::Text, Some(3));
508 assert_eq!(states.input, vec!['a', 'b']);
509 states.append('c', &InputType::Text, Some(3));
510 assert_eq!(states.input, vec!['a', 'b', 'c']);
511 states.append('d', &InputType::Text, Some(3));
513 assert_eq!(states.input, vec!['a', 'b', 'c']);
514 states.append('d', &InputType::Number, None);
516 assert_eq!(states.input, vec!['a', 'b', 'c']);
517 states.decr_cursor();
520 assert_eq!(states.cursor, 2);
521 states.cursor = 1;
522 states.decr_cursor();
523 assert_eq!(states.cursor, 0);
524 states.decr_cursor();
525 assert_eq!(states.cursor, 0);
526 states.incr_cursor();
528 assert_eq!(states.cursor, 1);
529 states.incr_cursor();
530 assert_eq!(states.cursor, 2);
531 states.incr_cursor();
532 assert_eq!(states.cursor, 3);
533 assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
535 assert_eq!(
536 states.render_value(InputType::Password('*')).as_str(),
537 "***"
538 );
539 }
540
541 #[test]
542 fn test_components_input_text() {
543 let mut component: Input = Input::default()
545 .background(Color::Yellow)
546 .borders(Borders::default())
547 .foreground(Color::Cyan)
548 .inactive(Style::default())
549 .input_len(5)
550 .input_type(InputType::Text)
551 .title("pippo", Alignment::Center)
552 .value("home");
553 assert_eq!(component.states.cursor, 4);
555 assert_eq!(component.states.input.len(), 4);
556 assert_eq!(
558 component.state(),
559 State::One(StateValue::String(String::from("home")))
560 );
561 assert_eq!(
563 component.perform(Cmd::Type('/')),
564 CmdResult::Changed(State::One(StateValue::String(String::from("home/"))))
565 );
566 assert_eq!(
567 component.state(),
568 State::One(StateValue::String(String::from("home/")))
569 );
570 assert_eq!(component.states.cursor, 5);
571 assert_eq!(component.perform(Cmd::Type('a')), CmdResult::None);
573 assert_eq!(
574 component.state(),
575 State::One(StateValue::String(String::from("home/")))
576 );
577 assert_eq!(component.states.cursor, 5);
578 assert_eq!(
580 component.perform(Cmd::Submit),
581 CmdResult::Submit(State::One(StateValue::String(String::from("home/"))))
582 );
583 assert_eq!(
585 component.perform(Cmd::Delete),
586 CmdResult::Changed(State::One(StateValue::String(String::from("home"))))
587 );
588 assert_eq!(
589 component.state(),
590 State::One(StateValue::String(String::from("home")))
591 );
592 assert_eq!(component.states.cursor, 4);
593 component.states.input = vec!['h'];
595 component.states.cursor = 1;
596 assert_eq!(
597 component.perform(Cmd::Delete),
598 CmdResult::Changed(State::One(StateValue::String(String::new())))
599 );
600 assert_eq!(
601 component.state(),
602 State::One(StateValue::String(String::new()))
603 );
604 assert_eq!(component.states.cursor, 0);
605 assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
607 assert_eq!(
608 component.state(),
609 State::One(StateValue::String(String::new()))
610 );
611 assert_eq!(component.states.cursor, 0);
612 assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
614 assert_eq!(
615 component.state(),
616 State::One(StateValue::String(String::new()))
617 );
618 assert_eq!(component.states.cursor, 0);
619 component.states.input = vec!['h', 'e'];
621 component.states.cursor = 1;
622 assert_eq!(
623 component.perform(Cmd::Cancel),
624 CmdResult::Changed(State::One(StateValue::String(String::from("h"))))
625 );
626 assert_eq!(
627 component.state(),
628 State::One(StateValue::String(String::from("h")))
629 );
630 assert_eq!(component.states.cursor, 1);
631 assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
633 assert_eq!(
634 component.state(),
635 State::One(StateValue::String(String::from("h")))
636 );
637 assert_eq!(component.states.cursor, 1);
638 component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
640 component.attr(Attribute::InputLength, AttrValue::Length(16));
642 component.states.cursor = 1;
643 assert_eq!(
644 component.perform(Cmd::Move(Direction::Right)), CmdResult::None
646 );
647 assert_eq!(component.states.cursor, 2);
648 assert_eq!(
650 component.perform(Cmd::Type('a')),
651 CmdResult::Changed(State::One(StateValue::String(String::from("heallo"))))
652 );
653 assert_eq!(
654 component.state(),
655 State::One(StateValue::String(String::from("heallo")))
656 );
657 assert_eq!(component.states.cursor, 3);
658 assert_eq!(
660 component.perform(Cmd::Move(Direction::Left)),
661 CmdResult::None
662 );
663 assert_eq!(component.states.cursor, 2);
664 component.states.cursor = 6;
666 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
668 assert_eq!(component.states.cursor, 6);
669 assert_eq!(
671 component.perform(Cmd::Move(Direction::Left)),
672 CmdResult::None
673 );
674 assert_eq!(component.states.cursor, 5);
675 component.states.cursor = 0;
677 assert_eq!(
678 component.perform(Cmd::Move(Direction::Left)),
679 CmdResult::None
680 );
681 assert_eq!(component.states.cursor, 0);
683 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
685 assert_eq!(component.states.cursor, 6);
686 assert_eq!(
687 component.perform(Cmd::GoTo(Position::Begin)),
688 CmdResult::None
689 );
690 assert_eq!(component.states.cursor, 0);
691 component.attr(Attribute::Value, AttrValue::String("new-value".to_string()));
693 assert_eq!(
694 component.state(),
695 State::One(StateValue::String(String::from("new-value")))
696 );
697 component.attr(
699 Attribute::InputType,
700 AttrValue::InputType(InputType::Number),
701 );
702 assert_eq!(component.state(), State::None);
703 }
704
705 #[test]
706 fn should_keep_cursor_within_bounds() {
707 let text = "The quick brown fox jumps over the lazy dog";
708 assert!(text.len() > 15);
709
710 let mut states = InputStates::default();
711
712 for ch in text.chars() {
713 states.append(ch, &InputType::Text, None);
714 }
715
716 assert_eq!(states.cursor, text.len());
718 assert_eq!(
719 states.render_value(InputType::Text),
720 states.render_value_offset(InputType::Text)
721 );
722
723 states.update_width(10);
724
725 assert_eq!(
726 states.render_value_offset(InputType::Text),
727 text[text.len() - 10..]
728 );
729
730 for i in 1..8 {
732 states.decr_cursor();
733 assert_eq!(states.cursor, text.len() - i);
734 let val = states.render_value_offset(InputType::Text);
735 assert_eq!(val, text[text.len() - 10..]);
736 }
737
738 states.decr_cursor();
740 assert_eq!(states.cursor, text.len() - 8);
741 assert_eq!(
742 states.render_value_offset(InputType::Text),
743 text[text.len() - 10..]
744 );
745
746 states.decr_cursor();
747 assert_eq!(states.cursor, text.len() - 9);
748 assert_eq!(
749 states.render_value_offset(InputType::Text),
750 text[text.len() - 11..]
751 );
752
753 states.decr_cursor();
754 assert_eq!(states.cursor, text.len() - 10);
755 assert_eq!(
756 states.render_value_offset(InputType::Text),
757 text[text.len() - 12..]
758 );
759
760 states.cursor_at_begin();
761 assert_eq!(states.cursor, 0);
762 assert_eq!(states.render_value(InputType::Text), text);
763
764 for i in 1..9 {
766 states.incr_cursor();
767 assert_eq!(states.cursor, i);
768 let val = states.render_value_offset(InputType::Text);
769 assert_eq!(val, text);
770 }
771
772 states.incr_cursor();
773 assert_eq!(states.cursor, 9);
774 assert_eq!(states.render_value_offset(InputType::Text), text[1..]);
775
776 states.incr_cursor();
777 assert_eq!(states.cursor, 10);
778 assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
779
780 states.update_width(30);
782 assert_eq!(states.cursor, 10);
783 assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
784
785 states.update_width(10);
787 assert_eq!(states.cursor, 10);
788 assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
789
790 states.update_width(9);
792 assert_eq!(states.cursor, 10);
793 assert_eq!(states.render_value_offset(InputType::Text), text[3..]);
794
795 states.update_width(10);
797 states.cursor_at_end();
798
799 for i in 1..=4 {
801 states.decr_cursor();
802 assert_eq!(states.cursor, text.len() - i);
803 let val = states.render_value_offset(InputType::Text);
804 assert_eq!(val, text[text.len() - 8..]);
805 }
806
807 assert_eq!(states.cursor, text.len() - 4);
808 states.incr_cursor();
809 assert_eq!(states.cursor, text.len() - 3);
810 assert_eq!(
811 states.render_value_offset(InputType::Text),
812 text[text.len() - 8..]
813 );
814
815 }
817}