tui_realm_stdlib/components/
input.rs

1//! ## Input
2//!
3//! `Input` represents a read-write input field. This component supports different input types, input length
4//! and handles input events related to cursor position, backspace, canc, ...
5
6use 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// -- states
16
17/// The number of characters [`InputStates::display_offset`] will keep in view in a single direction.
18const PREVIEW_DISTANCE: usize = 2;
19
20#[derive(Default, Debug)]
21pub struct InputStates {
22    /// The current input text
23    pub input: Vec<char>,
24    /// The cursor into "input", used as a index on where a character gets added next
25    pub cursor: usize,
26    /// The display offset for scrolling, always tries to keep the cursor within bounds
27    pub display_offset: usize,
28    /// The last drawn width of the component that displays "input".
29    ///
30    /// This is necessary to keep "display_offset" from jumping around on width changes.
31    pub last_width: Option<u16>,
32}
33
34impl InputStates {
35    /// ### append
36    ///
37    /// Append, if possible according to input type, the character to the input vec
38    pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
39        // Check if max length has been reached
40        if self.input.len() < max_len.unwrap_or(usize::MAX) {
41            // Check whether can push
42            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    /// ### backspace
50    ///
51    /// Delete element at cursor -1; then decrement cursor by 1
52    pub fn backspace(&mut self) {
53        if self.cursor > 0 && !self.input.is_empty() {
54            self.input.remove(self.cursor - 1);
55            // Decrement cursor
56            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    /// ### delete
65    ///
66    /// Delete element at cursor
67    pub fn delete(&mut self) {
68        if self.cursor < self.input.len() {
69            self.input.remove(self.cursor);
70        }
71    }
72
73    /// ### incr_cursor
74    ///
75    /// Increment cursor value by one if possible
76    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                // only increase the offset IF cursor is higher than last_width
86                // and the remaining text does not fit within the last_width
87                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    /// ### cursoro_at_begin
98    ///
99    /// Place cursor at the begin of the input
100    pub fn cursor_at_begin(&mut self) {
101        self.cursor = 0;
102        self.display_offset = 0;
103    }
104
105    /// ### cursor_at_end
106    ///
107    /// Place cursor at the end of the input
108    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    /// ### decr_cursor
116    ///
117    /// Decrement cursor value by one if possible
118    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    /// ### update_width
129    ///
130    /// Update the last width used to display [`InputStates::input`].
131    ///
132    /// This is necessary to update [`InputStates::display_offset`] correctly and keep it
133    /// from jumping around on width changes.
134    ///
135    /// Without using this function, no scrolling will effectively be applied.
136    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 the cursor would now be out-of-bounds, adjust the display offset to keep the cursor within bounds
141        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                // there was no previous width, use new_width minus cursor.
148                // this happens if "update_width" had never been called (like before the first draw)
149                // but the value is longer than the current display width and the cursor is not within bounds.
150                self.cursor.saturating_sub(usize::from(new_width))
151            };
152            self.display_offset += diff;
153        }
154    }
155
156    /// ### render_value
157    ///
158    /// Get value as string to render
159    #[must_use]
160    pub fn render_value(&self, itype: InputType) -> String {
161        self.render_value_chars(itype).iter().collect::<String>()
162    }
163
164    /// ### render_value_offset
165    ///
166    /// Get value as a string to render, with the [`InputStates::display_offset`] already skipped.
167    #[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    /// ### render_value_chars
176    ///
177    /// Render value as a vec of chars
178    #[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    /// ### get_value
189    ///
190    /// Get value as string
191    #[must_use]
192    pub fn get_value(&self) -> String {
193        self.input.iter().collect()
194    }
195}
196
197// -- Component
198
199/// ## Input
200///
201/// Input list component
202#[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    /// ### is_valid
280    ///
281    /// Checks whether current input is valid
282    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            // Apply invalid style
322            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            // Create input's area
340            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            // Choose whether to show placeholder; if placeholder is unset, show nothing
348            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            // Choose paragraph style based on whether is valid or not and if has focus and if should show placeholder
360            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            // Set cursor, if focus
385            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        // Check if new input
410        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        // Validate input
432        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                // Backspace and None
443                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                // Delete and None
453                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                // Push char to input
480                let prev_input = self.states.input.clone();
481                self.states
482                    .append(ch, &self.get_input_type(), self.get_input_len());
483                // Message on change
484                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        // Reached length
512        states.append('d', &InputType::Text, Some(3));
513        assert_eq!(states.input, vec!['a', 'b', 'c']);
514        // Push char to numbers
515        states.append('d', &InputType::Number, None);
516        assert_eq!(states.input, vec!['a', 'b', 'c']);
517        // move cursor
518        // decr cursor
519        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        // Incr
527        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        // Render value
534        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        // Instantiate Input with value
544        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        // Verify initial state
554        assert_eq!(component.states.cursor, 4);
555        assert_eq!(component.states.input.len(), 4);
556        // Get value
557        assert_eq!(
558            component.state(),
559            State::One(StateValue::String(String::from("home")))
560        );
561        // Character
562        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        // Verify max length (shouldn't push any character)
572        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        // Submit
579        assert_eq!(
580            component.perform(Cmd::Submit),
581            CmdResult::Submit(State::One(StateValue::String(String::from("home/"))))
582        );
583        // Backspace
584        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        // Check backspace at 0
594        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        // Another one...
606        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        // See del behaviour here
613        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        // Check del behaviour
620        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        // Another one (should do nothing)
632        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        // Move cursor right
639        component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
640        // Update length to 16
641        component.attr(Attribute::InputLength, AttrValue::Length(16));
642        component.states.cursor = 1;
643        assert_eq!(
644            component.perform(Cmd::Move(Direction::Right)), // between 'e' and 'l'
645            CmdResult::None
646        );
647        assert_eq!(component.states.cursor, 2);
648        // Put a character here
649        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        // Move left
659        assert_eq!(
660            component.perform(Cmd::Move(Direction::Left)),
661            CmdResult::None
662        );
663        assert_eq!(component.states.cursor, 2);
664        // Go at the end
665        component.states.cursor = 6;
666        // Move right
667        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
668        assert_eq!(component.states.cursor, 6);
669        // Move left
670        assert_eq!(
671            component.perform(Cmd::Move(Direction::Left)),
672            CmdResult::None
673        );
674        assert_eq!(component.states.cursor, 5);
675        // Go at the beginning
676        component.states.cursor = 0;
677        assert_eq!(
678            component.perform(Cmd::Move(Direction::Left)),
679            CmdResult::None
680        );
681        //assert_eq!(component.render().unwrap().cursor, 0); // Should stay
682        assert_eq!(component.states.cursor, 0);
683        // End - begin
684        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        // Update value
692        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        // Invalidate input type
698        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        // at first, without any "width" set, both functions should return the same
717        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        // the displayed text should not change until being in PREVIEW_STEP
731        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        // preview step space at the end
739        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        // the displayed text should not change until being in PREVIEW_STEP
765        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        // increasing width should not change display_offset
781        states.update_width(30);
782        assert_eq!(states.cursor, 10);
783        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
784
785        // reset to 10, should also not change
786        states.update_width(10);
787        assert_eq!(states.cursor, 10);
788        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
789
790        // should change display_offset by 1
791        states.update_width(9);
792        assert_eq!(states.cursor, 10);
793        assert_eq!(states.render_value_offset(InputType::Text), text[3..]);
794
795        // reset to end
796        states.update_width(10);
797        states.cursor_at_end();
798
799        // the displayed text should not change until being in PREVIEW_STEP
800        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        // note any width below PREVIEW_STEP * 2 + 1 is undefined behavior
816    }
817}