Skip to main content

tui_realm_stdlib/components/
input.rs

1//! `Input` represents a read-write input field. This component supports different input types, input length
2//! and handles input events related to cursor position, backspace, canc, ...
3
4use 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
20// -- states
21
22/// The number of characters [`InputStates::display_offset`] will keep in view in a single direction.
23const PREVIEW_DISTANCE: usize = 2;
24
25/// The state that needs to be kept for the [`Input`] component.
26#[derive(Default, Debug)]
27pub struct InputStates {
28    /// The current input text
29    pub input: Vec<char>,
30    /// The cursor into "input", used as a index on where a character gets added next
31    pub cursor: usize,
32    /// The display offset for scrolling, always tries to keep the cursor within bounds
33    pub display_offset: usize,
34    /// The last drawn width of the component that displays "input".
35    ///
36    /// This is necessary to keep "display_offset" from jumping around on width changes.
37    pub last_width: Option<u16>,
38}
39
40impl InputStates {
41    /// Append a character, if possible according to input type.
42    pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
43        // Check if max length has been reached
44        if self.input.len() < max_len.unwrap_or(usize::MAX) {
45            // Check whether can push
46            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    /// Delete the element at `cursor - 1`, then decrement cursor by 1.
54    pub fn backspace(&mut self) {
55        if self.cursor > 0 && !self.input.is_empty() {
56            self.input.remove(self.cursor - 1);
57            // Decrement cursor
58            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    /// Delete element at cursor position.
67    pub fn delete(&mut self) {
68        if self.cursor < self.input.len() {
69            self.input.remove(self.cursor);
70        }
71    }
72
73    /// Increment cursor by one if possible. (also known as moving RIGHT)
74    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                // only increase the offset IF cursor is higher than last_width
84                // and the remaining text does not fit within the last_width
85                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    /// Decrement cursor value by one if possible. (also known as moving LEFT)
96    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    /// Move the cursor to the beginning of the input.
107    pub fn cursor_at_begin(&mut self) {
108        self.cursor = 0;
109        self.display_offset = 0;
110    }
111
112    /// Move the cursor the the end of the input.
113    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    /// Update the last width used to display [`InputStates::input`].
121    ///
122    /// This is necessary to update [`InputStates::display_offset`] correctly and keep it
123    /// from jumping around on width changes.
124    ///
125    /// Without using this function, no scrolling will effectively be applied.
126    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 the cursor would now be out-of-bounds, adjust the display offset to keep the cursor within bounds
131        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                // there was no previous width, use new_width minus cursor.
138                // this happens if "update_width" had never been called (like before the first draw)
139                // but the value is longer than the current display width and the cursor is not within bounds.
140                self.cursor.saturating_sub(usize::from(new_width))
141            };
142            self.display_offset += diff;
143        }
144    }
145
146    /// Get the full text to render in the input, according to the [`InputType`].
147    #[must_use]
148    pub fn render_value(&self, itype: &InputType) -> String {
149        self.render_value_chars(itype).iter().collect::<String>()
150    }
151
152    /// Get the partial text to render, according to the [`InputType`].
153    ///
154    /// Unlike [`InputStates::render_value`], this will only collect the actually displayed text in the returned String.
155    #[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    /// Get the characters to render, according to [`InputType`].
164    ///
165    /// It is recommended to use [`render_value`](Self::render_value) or [`render_value_offset`](Self::render_value_offset) over this function.
166    #[must_use]
167    pub fn render_value_chars(&self, itype: &InputType) -> Vec<char> {
168        // TODO: can we return a iterator or something to prevent this intermediary Vec?
169        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    /// Get the current input as a String in full, without any [`InputType`] modifications.
178    #[must_use]
179    pub fn get_value(&self) -> String {
180        self.input.iter().collect()
181    }
182
183    /// Get whether the input is empty or has characters typed.
184    #[inline]
185    pub fn is_empty(&self) -> bool {
186        self.input.is_empty()
187    }
188}
189
190// -- Component
191
192/// `Input` represents a read-write input field. This component supports different input types, input length
193/// and handles input events related to cursor position, backspace, canc, ...
194#[derive(Default)]
195#[must_use]
196pub struct Input {
197    common: CommonProps,
198    props: Props,
199    pub states: InputStates,
200}
201
202impl Input {
203    /// Set the main foreground color. This may get overwritten by individual text styles.
204    pub fn foreground(mut self, fg: Color) -> Self {
205        self.attr(Attribute::Foreground, AttrValue::Color(fg));
206        self
207    }
208
209    /// Set the main background color. This may get overwritten by individual text styles.
210    pub fn background(mut self, bg: Color) -> Self {
211        self.attr(Attribute::Background, AttrValue::Color(bg));
212        self
213    }
214
215    /// Set the main text modifiers. This may get overwritten by individual text styles.
216    pub fn modifiers(mut self, m: TextModifiers) -> Self {
217        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
218        self
219    }
220
221    /// Set the main style. This may get overwritten by individual text styles.
222    ///
223    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
224    pub fn style(mut self, style: Style) -> Self {
225        self.attr(Attribute::Style, AttrValue::Style(style));
226        self
227    }
228
229    /// Set a custom style for the border when the component is unfocused.
230    pub fn inactive(mut self, s: Style) -> Self {
231        self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
232        self
233    }
234
235    /// Add a border to the component.
236    pub fn borders(mut self, b: Borders) -> Self {
237        self.attr(Attribute::Borders, AttrValue::Borders(b));
238        self
239    }
240
241    /// Add a title to the component.
242    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    /// Set the type of input this Input Component is for. Specific types may have different display or validate methods.
248    pub fn input_type(mut self, itype: InputType) -> Self {
249        self.attr(Attribute::InputType, AttrValue::InputType(itype));
250        self
251    }
252
253    /// Set the max length of the input.
254    pub fn input_len(mut self, ilen: usize) -> Self {
255        self.attr(Attribute::InputLength, AttrValue::Length(ilen));
256        self
257    }
258
259    /// Set the inital value of the Input.
260    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    /// Set a style for when the input fails validation.
266    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    /// Set a placeholder text for when the Input is empty.
272    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    /// Checks whether current input is valid according to the set [`InputType`].
294    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        // Apply invalid style
310        // TODO: invalid style should likely still be applied even if unfocused
311        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                // i dont like this, but ratatui does not offer a non-self taking method to change the style
326                *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            // Create input's area
336            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        // Choose whether to show placeholder; if placeholder is unset, show nothing
344        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        // Choose paragraph style based on whether is valid or not and if has focus and if should show placeholder
355        let paragraph_style = if self.common.is_active() {
356            normal_style
357        } else {
358            // TODO: this should likely be a different property
359            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        // Set cursor, if focus
371        // not using "common.is_active" here as cursor position should *only* be set for the actually focused component (as there can only be one cursor position)
372        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            // Check if new input
401            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        // Validate input
424        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                // Backspace and None
435                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                // Delete and None
445                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                // Push char to input
472                let prev_input = self.states.input.clone();
473                self.states
474                    .append(ch, &self.get_input_type().clone(), self.get_input_len());
475                // Message on change
476                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        // Reached length
505        states.append('d', &InputType::Text, Some(3));
506        assert_eq!(states.input, vec!['a', 'b', 'c']);
507        // Push char to numbers
508        states.append('d', &InputType::Number, None);
509        assert_eq!(states.input, vec!['a', 'b', 'c']);
510        // move cursor
511        // decr cursor
512        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        // Incr
520        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        // Render value
527        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        // Instantiate Input with value
537        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        // Verify initial state
547        assert_eq!(component.states.cursor, 4);
548        assert_eq!(component.states.input.len(), 4);
549        // Get value
550        assert_eq!(
551            component.state(),
552            State::Single(StateValue::String(String::from("home")))
553        );
554        // Character
555        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        // Verify max length (shouldn't push any character)
565        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        // Submit
572        assert_eq!(
573            component.perform(Cmd::Submit),
574            CmdResult::Submit(State::Single(StateValue::String(String::from("home/"))))
575        );
576        // Backspace
577        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        // Check backspace at 0
587        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        // Another one...
599        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        // See del behaviour here
606        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        // Check del behaviour
613        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        // Another one (should do nothing)
625        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        // Move cursor right
632        component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
633        // Update length to 16
634        component.attr(Attribute::InputLength, AttrValue::Length(16));
635        component.states.cursor = 1;
636        assert_eq!(
637            component.perform(Cmd::Move(Direction::Right)), // between 'e' and 'l'
638            CmdResult::Visual
639        );
640        assert_eq!(component.states.cursor, 2);
641        // Put a character here
642        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        // Move left
652        assert_eq!(
653            component.perform(Cmd::Move(Direction::Left)),
654            CmdResult::Visual
655        );
656        assert_eq!(component.states.cursor, 2);
657        // Go at the end
658        component.states.cursor = 6;
659        // Move right
660        assert_eq!(
661            component.perform(Cmd::GoTo(Position::End)),
662            CmdResult::Visual
663        );
664        assert_eq!(component.states.cursor, 6);
665        // Move left
666        assert_eq!(
667            component.perform(Cmd::Move(Direction::Left)),
668            CmdResult::Visual
669        );
670        assert_eq!(component.states.cursor, 5);
671        // Go at the beginning
672        component.states.cursor = 0;
673        assert_eq!(
674            component.perform(Cmd::Move(Direction::Left)),
675            CmdResult::Visual
676        );
677        //assert_eq!(component.render().unwrap().cursor, 0); // Should stay
678        assert_eq!(component.states.cursor, 0);
679        // End - begin
680        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        // Update value
691        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        // Invalidate input type
697        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        // at first, without any "width" set, both functions should return the same
716        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        // the displayed text should not change until being in PREVIEW_STEP
730        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        // preview step space at the end
738        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        // the displayed text should not change until being in PREVIEW_STEP
764        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        // increasing width should not change display_offset
780        states.update_width(30);
781        assert_eq!(states.cursor, 10);
782        assert_eq!(states.render_value_offset(&InputType::Text), text[2..]);
783
784        // reset to 10, should also not change
785        states.update_width(10);
786        assert_eq!(states.cursor, 10);
787        assert_eq!(states.render_value_offset(&InputType::Text), text[2..]);
788
789        // should change display_offset by 1
790        states.update_width(9);
791        assert_eq!(states.cursor, 10);
792        assert_eq!(states.render_value_offset(&InputType::Text), text[3..]);
793
794        // reset to end
795        states.update_width(10);
796        states.cursor_at_end();
797
798        // the displayed text should not change until being in PREVIEW_STEP
799        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        // note any width below PREVIEW_STEP * 2 + 1 is undefined behavior
815    }
816}