1use tuirealm::command::{Cmd, CmdResult, Direction, Position};
5use tuirealm::component::Component;
6use tuirealm::props::{
7 AttrValue, Attribute, Borders, Color, InputType, LineStatic, Props, QueryResult, Style,
8 TextModifiers, Title,
9};
10use tuirealm::ratatui::Frame;
11use tuirealm::ratatui::layout::Rect;
12use tuirealm::ratatui::text::Line;
13use tuirealm::ratatui::widgets::Paragraph;
14use tuirealm::state::{State, StateValue};
15
16use super::props::{INPUT_INVALID_STYLE, INPUT_PLACEHOLDER};
17use crate::prop_ext::CommonProps;
18use crate::utils::{borrow_clone_line, calc_utf8_cursor_position};
19
20const PREVIEW_DISTANCE: usize = 2;
24
25#[derive(Default, Debug)]
27pub struct InputStates {
28 pub input: Vec<char>,
30 pub cursor: usize,
32 pub display_offset: usize,
34 pub last_width: Option<u16>,
38}
39
40impl InputStates {
41 pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
43 if self.input.len() < max_len.unwrap_or(usize::MAX) {
45 if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
47 self.input.insert(self.cursor, ch);
48 self.incr_cursor();
49 }
50 }
51 }
52
53 pub fn backspace(&mut self) {
55 if self.cursor > 0 && !self.input.is_empty() {
56 self.input.remove(self.cursor - 1);
57 self.cursor -= 1;
59
60 if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
61 self.display_offset = self.display_offset.saturating_sub(1);
62 }
63 }
64 }
65
66 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) {
75 if self.cursor < self.input.len() {
76 self.cursor += 1;
77
78 if let Some(last_width) = self.last_width {
79 let input_with_width = self.input.len().saturating_sub(
80 usize::from(self.last_width.unwrap_or_default())
81 .saturating_sub(PREVIEW_DISTANCE),
82 );
83 if self.cursor
86 > usize::from(last_width).saturating_sub(PREVIEW_DISTANCE) + self.display_offset
87 && self.display_offset < input_with_width
88 {
89 self.display_offset += 1;
90 }
91 }
92 }
93 }
94
95 pub fn decr_cursor(&mut self) {
97 if self.cursor > 0 {
98 self.cursor -= 1;
99
100 if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
101 self.display_offset = self.display_offset.saturating_sub(1);
102 }
103 }
104 }
105
106 pub fn cursor_at_begin(&mut self) {
108 self.cursor = 0;
109 self.display_offset = 0;
110 }
111
112 pub fn cursor_at_end(&mut self) {
114 self.cursor = self.input.len();
115 self.display_offset = self.input.len().saturating_sub(
116 usize::from(self.last_width.unwrap_or_default()).saturating_sub(PREVIEW_DISTANCE),
117 );
118 }
119
120 pub fn update_width(&mut self, new_width: u16) {
127 let old_width = self.last_width;
128 self.last_width = Some(new_width);
129
130 if self.cursor
132 > (self.display_offset + usize::from(new_width)).saturating_sub(PREVIEW_DISTANCE)
133 {
134 let diff = if let Some(old_width) = old_width {
135 usize::from(old_width.saturating_sub(new_width))
136 } else {
137 self.cursor.saturating_sub(usize::from(new_width))
141 };
142 self.display_offset += diff;
143 }
144 }
145
146 #[must_use]
148 pub fn render_value(&self, itype: &InputType) -> String {
149 self.render_value_chars(itype).iter().collect::<String>()
150 }
151
152 #[must_use]
156 pub fn render_value_offset(&self, itype: &InputType) -> String {
157 self.render_value_chars(itype)
158 .iter()
159 .skip(self.display_offset)
160 .collect()
161 }
162
163 #[must_use]
167 pub fn render_value_chars(&self, itype: &InputType) -> Vec<char> {
168 match itype {
170 InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
171 (0..self.input.len()).map(|_| *ch).collect()
172 }
173 _ => self.input.clone(),
174 }
175 }
176
177 #[must_use]
179 pub fn get_value(&self) -> String {
180 self.input.iter().collect()
181 }
182
183 #[inline]
185 pub fn is_empty(&self) -> bool {
186 self.input.is_empty()
187 }
188}
189
190#[derive(Default)]
195#[must_use]
196pub struct Input {
197 common: CommonProps,
198 props: Props,
199 pub states: InputStates,
200}
201
202impl Input {
203 pub fn foreground(mut self, fg: Color) -> Self {
205 self.attr(Attribute::Foreground, AttrValue::Color(fg));
206 self
207 }
208
209 pub fn background(mut self, bg: Color) -> Self {
211 self.attr(Attribute::Background, AttrValue::Color(bg));
212 self
213 }
214
215 pub fn modifiers(mut self, m: TextModifiers) -> Self {
217 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
218 self
219 }
220
221 pub fn style(mut self, style: Style) -> Self {
225 self.attr(Attribute::Style, AttrValue::Style(style));
226 self
227 }
228
229 pub fn inactive(mut self, s: Style) -> Self {
231 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
232 self
233 }
234
235 pub fn borders(mut self, b: Borders) -> Self {
237 self.attr(Attribute::Borders, AttrValue::Borders(b));
238 self
239 }
240
241 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
243 self.attr(Attribute::Title, AttrValue::Title(title.into()));
244 self
245 }
246
247 pub fn input_type(mut self, itype: InputType) -> Self {
249 self.attr(Attribute::InputType, AttrValue::InputType(itype));
250 self
251 }
252
253 pub fn input_len(mut self, ilen: usize) -> Self {
255 self.attr(Attribute::InputLength, AttrValue::Length(ilen));
256 self
257 }
258
259 pub fn value<S: Into<String>>(mut self, s: S) -> Self {
261 self.attr(Attribute::Value, AttrValue::String(s.into()));
262 self
263 }
264
265 pub fn invalid_style(mut self, s: Style) -> Self {
267 self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
268 self
269 }
270
271 pub fn placeholder<S: Into<LineStatic>>(mut self, placeholder: S) -> Self {
273 self.attr(
274 Attribute::Custom(INPUT_PLACEHOLDER),
275 AttrValue::TextLine(placeholder.into()),
276 );
277 self
278 }
279
280 fn get_input_len(&self) -> Option<usize> {
281 self.props
282 .get(Attribute::InputLength)
283 .and_then(AttrValue::as_length)
284 }
285
286 fn get_input_type(&self) -> &InputType {
287 self.props
288 .get(Attribute::InputType)
289 .and_then(AttrValue::as_input_type)
290 .unwrap_or(&InputType::Text)
291 }
292
293 fn is_valid(&self) -> bool {
295 let value = self.states.get_value();
296 self.get_input_type().validate(value.as_str())
297 }
298}
299
300impl Component for Input {
301 fn view(&mut self, render: &mut Frame, area: Rect) {
302 if !self.common.display {
303 return;
304 }
305
306 let mut normal_style = self.common.style;
307
308 let mut block = self.common.get_block();
309 if self.common.is_active()
312 && !self.is_valid()
313 && let Some(invalid_style) = self
314 .props
315 .get(Attribute::Custom(INPUT_INVALID_STYLE))
316 .and_then(AttrValue::as_style)
317 {
318 if let Some(block) = &mut block {
319 let border_style = self
320 .common
321 .border
322 .unwrap_or_default()
323 .style()
324 .patch(invalid_style);
325 *block = std::mem::take(block).border_style(border_style);
327 }
328
329 normal_style = normal_style.patch(invalid_style);
330 }
331
332 let mut area_for_bounds = area;
333
334 if let Some(block) = &block {
335 let block_inner_area = block.inner(area);
337
338 self.states.update_width(block_inner_area.width);
339
340 area_for_bounds = block_inner_area;
341 }
342
343 let text_to_display = if self.states.is_empty() {
345 self.states.cursor = 0;
346 self.props
347 .get(Attribute::Custom(INPUT_PLACEHOLDER))
348 .and_then(AttrValue::as_textline)
349 .map(borrow_clone_line)
350 .unwrap_or_default()
351 } else {
352 Line::from(self.states.render_value_offset(self.get_input_type()))
353 };
354 let paragraph_style = if self.common.is_active() {
356 normal_style
357 } else {
358 self.common.border_unfocused_style
360 };
361
362 let mut widget = Paragraph::new(text_to_display).style(paragraph_style);
363
364 if let Some(block) = block {
365 widget = widget.block(block);
366 }
367
368 render.render_widget(widget, area);
369
370 if self.common.focused && !area_for_bounds.is_empty() {
373 let x: u16 = area_for_bounds.x
374 + calc_utf8_cursor_position(
375 &self.states.render_value_chars(self.get_input_type())[0..self.states.cursor],
376 )
377 .saturating_sub(u16::try_from(self.states.display_offset).unwrap_or(u16::MAX));
378 let x = x.min(area_for_bounds.x + area_for_bounds.width);
379 render.set_cursor_position(tuirealm::ratatui::prelude::Position {
380 x,
381 y: area_for_bounds.y,
382 });
383 }
384 }
385
386 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
387 if let Some(value) = self.common.get_for_query(attr) {
388 return Some(value);
389 }
390
391 self.props.get_for_query(attr)
392 }
393
394 fn attr(&mut self, attr: Attribute, value: AttrValue) {
395 if let Some(value) = self.common.set(attr, value) {
396 let sanitize_input = matches!(
397 attr,
398 Attribute::InputLength | Attribute::InputType | Attribute::Value
399 );
400 let new_input = match attr {
402 Attribute::Value => Some(value.clone().unwrap_string()),
403 _ => None,
404 };
405 self.props.set(attr, value);
406 if sanitize_input {
407 let input = match new_input {
408 None => self.states.input.clone(),
409 Some(v) => v.chars().collect(),
410 };
411 self.states.input = Vec::new();
412 self.states.cursor = 0;
413 let itype = self.get_input_type().clone();
414 let max_len = self.get_input_len();
415 for ch in input {
416 self.states.append(ch, &itype, max_len);
417 }
418 }
419 }
420 }
421
422 fn state(&self) -> State {
423 if self.is_valid() {
425 State::Single(StateValue::String(self.states.get_value()))
426 } else {
427 State::None
428 }
429 }
430
431 fn perform(&mut self, cmd: Cmd) -> CmdResult {
432 match cmd {
433 Cmd::Delete => {
434 let prev_input = self.states.input.clone();
436 self.states.backspace();
437 if prev_input == self.states.input {
438 CmdResult::NoChange
439 } else {
440 CmdResult::Changed(self.state())
441 }
442 }
443 Cmd::Cancel => {
444 let prev_input = self.states.input.clone();
446 self.states.delete();
447 if prev_input == self.states.input {
448 CmdResult::NoChange
449 } else {
450 CmdResult::Changed(self.state())
451 }
452 }
453 Cmd::Submit => CmdResult::Submit(self.state()),
454 Cmd::Move(Direction::Left) => {
455 self.states.decr_cursor();
456 CmdResult::Visual
457 }
458 Cmd::Move(Direction::Right) => {
459 self.states.incr_cursor();
460 CmdResult::Visual
461 }
462 Cmd::GoTo(Position::Begin) => {
463 self.states.cursor_at_begin();
464 CmdResult::Visual
465 }
466 Cmd::GoTo(Position::End) => {
467 self.states.cursor_at_end();
468 CmdResult::Visual
469 }
470 Cmd::Type(ch) => {
471 let prev_input = self.states.input.clone();
473 self.states
474 .append(ch, &self.get_input_type().clone(), self.get_input_len());
475 if prev_input == self.states.input {
477 CmdResult::NoChange
478 } else {
479 CmdResult::Changed(self.state())
480 }
481 }
482 _ => CmdResult::Invalid(cmd),
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489
490 use pretty_assertions::assert_eq;
491 use tuirealm::props::HorizontalAlignment;
492
493 use super::*;
494
495 #[test]
496 fn test_components_input_states() {
497 let mut states: InputStates = InputStates::default();
498 states.append('a', &InputType::Text, Some(3));
499 assert_eq!(states.input, vec!['a']);
500 states.append('b', &InputType::Text, Some(3));
501 assert_eq!(states.input, vec!['a', 'b']);
502 states.append('c', &InputType::Text, Some(3));
503 assert_eq!(states.input, vec!['a', 'b', 'c']);
504 states.append('d', &InputType::Text, Some(3));
506 assert_eq!(states.input, vec!['a', 'b', 'c']);
507 states.append('d', &InputType::Number, None);
509 assert_eq!(states.input, vec!['a', 'b', 'c']);
510 states.decr_cursor();
513 assert_eq!(states.cursor, 2);
514 states.cursor = 1;
515 states.decr_cursor();
516 assert_eq!(states.cursor, 0);
517 states.decr_cursor();
518 assert_eq!(states.cursor, 0);
519 states.incr_cursor();
521 assert_eq!(states.cursor, 1);
522 states.incr_cursor();
523 assert_eq!(states.cursor, 2);
524 states.incr_cursor();
525 assert_eq!(states.cursor, 3);
526 assert_eq!(states.render_value(&InputType::Text).as_str(), "abc");
528 assert_eq!(
529 states.render_value(&InputType::Password('*')).as_str(),
530 "***"
531 );
532 }
533
534 #[test]
535 fn test_components_input_text() {
536 let mut component: Input = Input::default()
538 .background(Color::Yellow)
539 .borders(Borders::default())
540 .foreground(Color::Cyan)
541 .inactive(Style::default())
542 .input_len(5)
543 .input_type(InputType::Text)
544 .title(Title::from("pippo").alignment(HorizontalAlignment::Center))
545 .value("home");
546 assert_eq!(component.states.cursor, 4);
548 assert_eq!(component.states.input.len(), 4);
549 assert_eq!(
551 component.state(),
552 State::Single(StateValue::String(String::from("home")))
553 );
554 assert_eq!(
556 component.perform(Cmd::Type('/')),
557 CmdResult::Changed(State::Single(StateValue::String(String::from("home/"))))
558 );
559 assert_eq!(
560 component.state(),
561 State::Single(StateValue::String(String::from("home/")))
562 );
563 assert_eq!(component.states.cursor, 5);
564 assert_eq!(component.perform(Cmd::Type('a')), CmdResult::NoChange);
566 assert_eq!(
567 component.state(),
568 State::Single(StateValue::String(String::from("home/")))
569 );
570 assert_eq!(component.states.cursor, 5);
571 assert_eq!(
573 component.perform(Cmd::Submit),
574 CmdResult::Submit(State::Single(StateValue::String(String::from("home/"))))
575 );
576 assert_eq!(
578 component.perform(Cmd::Delete),
579 CmdResult::Changed(State::Single(StateValue::String(String::from("home"))))
580 );
581 assert_eq!(
582 component.state(),
583 State::Single(StateValue::String(String::from("home")))
584 );
585 assert_eq!(component.states.cursor, 4);
586 component.states.input = vec!['h'];
588 component.states.cursor = 1;
589 assert_eq!(
590 component.perform(Cmd::Delete),
591 CmdResult::Changed(State::Single(StateValue::String(String::new())))
592 );
593 assert_eq!(
594 component.state(),
595 State::Single(StateValue::String(String::new()))
596 );
597 assert_eq!(component.states.cursor, 0);
598 assert_eq!(component.perform(Cmd::Delete), CmdResult::NoChange);
600 assert_eq!(
601 component.state(),
602 State::Single(StateValue::String(String::new()))
603 );
604 assert_eq!(component.states.cursor, 0);
605 assert_eq!(component.perform(Cmd::Cancel), CmdResult::NoChange);
607 assert_eq!(
608 component.state(),
609 State::Single(StateValue::String(String::new()))
610 );
611 assert_eq!(component.states.cursor, 0);
612 component.states.input = vec!['h', 'e'];
614 component.states.cursor = 1;
615 assert_eq!(
616 component.perform(Cmd::Cancel),
617 CmdResult::Changed(State::Single(StateValue::String(String::from("h"))))
618 );
619 assert_eq!(
620 component.state(),
621 State::Single(StateValue::String(String::from("h")))
622 );
623 assert_eq!(component.states.cursor, 1);
624 assert_eq!(component.perform(Cmd::Cancel), CmdResult::NoChange);
626 assert_eq!(
627 component.state(),
628 State::Single(StateValue::String(String::from("h")))
629 );
630 assert_eq!(component.states.cursor, 1);
631 component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
633 component.attr(Attribute::InputLength, AttrValue::Length(16));
635 component.states.cursor = 1;
636 assert_eq!(
637 component.perform(Cmd::Move(Direction::Right)), CmdResult::Visual
639 );
640 assert_eq!(component.states.cursor, 2);
641 assert_eq!(
643 component.perform(Cmd::Type('a')),
644 CmdResult::Changed(State::Single(StateValue::String(String::from("heallo"))))
645 );
646 assert_eq!(
647 component.state(),
648 State::Single(StateValue::String(String::from("heallo")))
649 );
650 assert_eq!(component.states.cursor, 3);
651 assert_eq!(
653 component.perform(Cmd::Move(Direction::Left)),
654 CmdResult::Visual
655 );
656 assert_eq!(component.states.cursor, 2);
657 component.states.cursor = 6;
659 assert_eq!(
661 component.perform(Cmd::GoTo(Position::End)),
662 CmdResult::Visual
663 );
664 assert_eq!(component.states.cursor, 6);
665 assert_eq!(
667 component.perform(Cmd::Move(Direction::Left)),
668 CmdResult::Visual
669 );
670 assert_eq!(component.states.cursor, 5);
671 component.states.cursor = 0;
673 assert_eq!(
674 component.perform(Cmd::Move(Direction::Left)),
675 CmdResult::Visual
676 );
677 assert_eq!(component.states.cursor, 0);
679 assert_eq!(
681 component.perform(Cmd::GoTo(Position::End)),
682 CmdResult::Visual
683 );
684 assert_eq!(component.states.cursor, 6);
685 assert_eq!(
686 component.perform(Cmd::GoTo(Position::Begin)),
687 CmdResult::Visual
688 );
689 assert_eq!(component.states.cursor, 0);
690 component.attr(Attribute::Value, AttrValue::String("new-value".to_string()));
692 assert_eq!(
693 component.state(),
694 State::Single(StateValue::String(String::from("new-value")))
695 );
696 component.attr(
698 Attribute::InputType,
699 AttrValue::InputType(InputType::Number),
700 );
701 assert_eq!(component.state(), State::None);
702 }
703
704 #[test]
705 fn should_keep_cursor_within_bounds() {
706 let text = "The quick brown fox jumps over the lazy dog";
707 assert!(text.len() > 15);
708
709 let mut states = InputStates::default();
710
711 for ch in text.chars() {
712 states.append(ch, &InputType::Text, None);
713 }
714
715 assert_eq!(states.cursor, text.len());
717 assert_eq!(
718 states.render_value(&InputType::Text),
719 states.render_value_offset(&InputType::Text)
720 );
721
722 states.update_width(10);
723
724 assert_eq!(
725 states.render_value_offset(&InputType::Text),
726 text[text.len() - 10..]
727 );
728
729 for i in 1..8 {
731 states.decr_cursor();
732 assert_eq!(states.cursor, text.len() - i);
733 let val = states.render_value_offset(&InputType::Text);
734 assert_eq!(val, text[text.len() - 10..]);
735 }
736
737 states.decr_cursor();
739 assert_eq!(states.cursor, text.len() - 8);
740 assert_eq!(
741 states.render_value_offset(&InputType::Text),
742 text[text.len() - 10..]
743 );
744
745 states.decr_cursor();
746 assert_eq!(states.cursor, text.len() - 9);
747 assert_eq!(
748 states.render_value_offset(&InputType::Text),
749 text[text.len() - 11..]
750 );
751
752 states.decr_cursor();
753 assert_eq!(states.cursor, text.len() - 10);
754 assert_eq!(
755 states.render_value_offset(&InputType::Text),
756 text[text.len() - 12..]
757 );
758
759 states.cursor_at_begin();
760 assert_eq!(states.cursor, 0);
761 assert_eq!(states.render_value(&InputType::Text), text);
762
763 for i in 1..9 {
765 states.incr_cursor();
766 assert_eq!(states.cursor, i);
767 let val = states.render_value_offset(&InputType::Text);
768 assert_eq!(val, text);
769 }
770
771 states.incr_cursor();
772 assert_eq!(states.cursor, 9);
773 assert_eq!(states.render_value_offset(&InputType::Text), text[1..]);
774
775 states.incr_cursor();
776 assert_eq!(states.cursor, 10);
777 assert_eq!(states.render_value_offset(&InputType::Text), text[2..]);
778
779 states.update_width(30);
781 assert_eq!(states.cursor, 10);
782 assert_eq!(states.render_value_offset(&InputType::Text), text[2..]);
783
784 states.update_width(10);
786 assert_eq!(states.cursor, 10);
787 assert_eq!(states.render_value_offset(&InputType::Text), text[2..]);
788
789 states.update_width(9);
791 assert_eq!(states.cursor, 10);
792 assert_eq!(states.render_value_offset(&InputType::Text), text[3..]);
793
794 states.update_width(10);
796 states.cursor_at_end();
797
798 for i in 1..=4 {
800 states.decr_cursor();
801 assert_eq!(states.cursor, text.len() - i);
802 let val = states.render_value_offset(&InputType::Text);
803 assert_eq!(val, text[text.len() - 8..]);
804 }
805
806 assert_eq!(states.cursor, text.len() - 4);
807 states.incr_cursor();
808 assert_eq!(states.cursor, text.len() - 3);
809 assert_eq!(
810 states.render_value_offset(&InputType::Text),
811 text[text.len() - 8..]
812 );
813
814 }
816}