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