• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

veeso / tui-realm-stdlib / 20396078319

20 Dec 2025 03:02PM UTC coverage: 69.067% (+0.7%) from 68.377%
20396078319

Pull #37

github

web-flow
Merge 08a780d5b into 432d5e9ad
Pull Request #37: Add automatic scrolling for `Input` component (`InputStates`)

110 of 117 new or added lines in 1 file covered. (94.02%)

2 existing lines in 1 file now uncovered.

3146 of 4555 relevant lines covered (69.07%)

2.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.53
/src/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

6
use super::props::{INPUT_INVALID_STYLE, INPUT_PLACEHOLDER, INPUT_PLACEHOLDER_STYLE};
7
use crate::utils::calc_utf8_cursor_position;
8
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
9
use tuirealm::props::{
10
    Alignment, AttrValue, Attribute, Borders, Color, InputType, Props, Style, TextModifiers,
11
};
12
use tuirealm::ratatui::{layout::Rect, widgets::Paragraph};
13
use 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.
18
const PREVIEW_DISTANCE: usize = 2;
19

20
#[derive(Default, Debug)]
21
pub 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

34
impl 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>) {
78✔
39
        // Check if max length has been reached
40
        if self.input.len() < max_len.unwrap_or(usize::MAX) {
78✔
41
            // Check whether can push
42
            if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
76✔
43
                self.input.insert(self.cursor, ch);
67✔
44
                self.incr_cursor();
67✔
45
            }
67✔
46
        }
2✔
47
    }
78✔
48

49
    /// ### backspace
50
    ///
51
    /// Delete element at cursor -1; then decrement cursor by 1
52
    pub fn backspace(&mut self) {
3✔
53
        if self.cursor > 0 && !self.input.is_empty() {
3✔
54
            self.input.remove(self.cursor - 1);
2✔
55
            // Decrement cursor
56
            self.cursor -= 1;
2✔
57

58
            if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
2✔
59
                self.display_offset = self.display_offset.saturating_sub(1);
1✔
60
            }
1✔
61
        }
1✔
62
    }
3✔
63

64
    /// ### delete
65
    ///
66
    /// Delete element at cursor
67
    pub fn delete(&mut self) {
3✔
68
        if self.cursor < self.input.len() {
3✔
69
            self.input.remove(self.cursor);
1✔
70
        }
2✔
71
    }
3✔
72

73
    /// ### incr_cursor
74
    ///
75
    /// Increment cursor value by one if possible
76
    pub fn incr_cursor(&mut self) {
82✔
77
        if self.cursor < self.input.len() {
82✔
78
            self.cursor += 1;
82✔
79

80
            if let Some(last_width) = self.last_width {
82✔
81
                let input_with_width = self.input.len().saturating_sub(
11✔
82
                    usize::from(self.last_width.unwrap_or_default())
11✔
83
                        .saturating_sub(PREVIEW_DISTANCE),
11✔
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
11✔
88
                    > usize::from(last_width).saturating_sub(PREVIEW_DISTANCE) + self.display_offset
11✔
89
                    && self.display_offset < input_with_width
2✔
90
                {
2✔
91
                    self.display_offset += 1;
2✔
92
                }
9✔
93
            }
71✔
UNCOV
94
        }
×
95
    }
82✔
96

97
    /// ### cursoro_at_begin
98
    ///
99
    /// Place cursor at the begin of the input
100
    pub fn cursor_at_begin(&mut self) {
2✔
101
        self.cursor = 0;
2✔
102
        self.display_offset = 0;
2✔
103
    }
2✔
104

105
    /// ### cursor_at_end
106
    ///
107
    /// Place cursor at the end of the input
108
    pub fn cursor_at_end(&mut self) {
3✔
109
        self.cursor = self.input.len();
3✔
110
        self.display_offset = self.input.len().saturating_sub(
3✔
111
            usize::from(self.last_width.unwrap_or_default()).saturating_sub(PREVIEW_DISTANCE),
3✔
112
        );
3✔
113
    }
3✔
114

115
    /// ### decr_cursor
116
    ///
117
    /// Decrement cursor value by one if possible
118
    pub fn decr_cursor(&mut self) {
20✔
119
        if self.cursor > 0 {
20✔
120
            self.cursor -= 1;
18✔
121

122
            if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
18✔
123
                self.display_offset = self.display_offset.saturating_sub(1);
4✔
124
            }
14✔
125
        }
2✔
126
    }
20✔
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) {
5✔
137
        let old_width = self.last_width;
5✔
138
        self.last_width = Some(new_width);
5✔
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
5✔
142
            > (self.display_offset + usize::from(new_width)).saturating_sub(PREVIEW_DISTANCE)
5✔
143
        {
144
            let diff = if let Some(old_width) = old_width {
2✔
145
                usize::from(old_width.saturating_sub(new_width))
1✔
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))
1✔
151
            };
152
            self.display_offset += diff;
2✔
153
        }
3✔
154
    }
5✔
155

156
    /// ### render_value
157
    ///
158
    /// Get value as string to render
159
    #[must_use]
160
    pub fn render_value(&self, itype: InputType) -> String {
4✔
161
        self.render_value_chars(itype).iter().collect::<String>()
4✔
162
    }
4✔
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 {
30✔
169
        self.render_value_chars(itype)
30✔
170
            .iter()
30✔
171
            .skip(self.display_offset)
30✔
172
            .collect()
30✔
173
    }
30✔
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> {
34✔
180
        match itype {
34✔
181
            InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
1✔
182
                (0..self.input.len()).map(|_| ch).collect()
1✔
183
            }
184
            _ => self.input.clone(),
33✔
185
        }
186
    }
34✔
187

188
    /// ### get_value
189
    ///
190
    /// Get value as string
191
    #[must_use]
192
    pub fn get_value(&self) -> String {
35✔
193
        self.input.iter().collect()
35✔
194
    }
35✔
195
}
196

197
// -- Component
198

199
/// ## Input
200
///
201
/// Input list component
202
#[derive(Default)]
203
#[must_use]
204
pub struct Input {
205
    props: Props,
206
    pub states: InputStates,
207
}
208

209
impl Input {
210
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
211
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
212
        self
1✔
213
    }
1✔
214

215
    pub fn background(mut self, bg: Color) -> Self {
1✔
216
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
217
        self
1✔
218
    }
1✔
219

220
    pub fn inactive(mut self, s: Style) -> Self {
1✔
221
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
1✔
222
        self
1✔
223
    }
1✔
224

225
    pub fn borders(mut self, b: Borders) -> Self {
1✔
226
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
227
        self
1✔
228
    }
1✔
229

230
    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
1✔
231
        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
1✔
232
        self
1✔
233
    }
1✔
234

235
    pub fn input_type(mut self, itype: InputType) -> Self {
1✔
236
        self.attr(Attribute::InputType, AttrValue::InputType(itype));
1✔
237
        self
1✔
238
    }
1✔
239

240
    pub fn input_len(mut self, ilen: usize) -> Self {
1✔
241
        self.attr(Attribute::InputLength, AttrValue::Length(ilen));
1✔
242
        self
1✔
243
    }
1✔
244

245
    pub fn value<S: Into<String>>(mut self, s: S) -> Self {
1✔
246
        self.attr(Attribute::Value, AttrValue::String(s.into()));
1✔
247
        self
1✔
248
    }
1✔
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> {
9✔
268
        self.props
9✔
269
            .get(Attribute::InputLength)
9✔
270
            .map(|x| x.unwrap_length())
9✔
271
    }
9✔
272

273
    fn get_input_type(&self) -> InputType {
27✔
274
        self.props
27✔
275
            .get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
27✔
276
            .unwrap_input_type()
27✔
277
    }
27✔
278

279
    /// ### is_valid
280
    ///
281
    /// Checks whether current input is valid
282
    fn is_valid(&self) -> bool {
18✔
283
        let value = self.states.get_value();
18✔
284
        self.get_input_type().validate(value.as_str())
18✔
285
    }
18✔
286
}
287

288
impl 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
NEW
340
            let block_inner_area = block.inner(area);
×
341

NEW
342
            self.states.update_width(block_inner_area.width);
×
343

NEW
344
            let text_to_display = self.states.render_value_offset(self.get_input_type());
×
345

UNCOV
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 {
×
NEW
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
NEW
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],
×
NEW
389
                    )
×
NEW
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) {
11✔
405
        let sanitize_input = matches!(
11✔
406
            attr,
11✔
407
            Attribute::InputLength | Attribute::InputType | Attribute::Value
408
        );
409
        // Check if new input
410
        let new_input = match attr {
11✔
411
            Attribute::Value => Some(value.clone().unwrap_string()),
2✔
412
            _ => None,
9✔
413
        };
414
        self.props.set(attr, value);
11✔
415
        if sanitize_input {
11✔
416
            let input = match new_input {
6✔
417
                None => self.states.input.clone(),
4✔
418
                Some(v) => v.chars().collect(),
2✔
419
            };
420
            self.states.input = Vec::new();
6✔
421
            self.states.cursor = 0;
6✔
422
            let itype = self.get_input_type();
6✔
423
            let max_len = self.get_input_len();
6✔
424
            for ch in input {
27✔
425
                self.states.append(ch, &itype, max_len);
27✔
426
            }
27✔
427
        }
5✔
428
    }
11✔
429

430
    fn state(&self) -> State {
18✔
431
        // Validate input
432
        if self.is_valid() {
18✔
433
            State::One(StateValue::String(self.states.get_value()))
17✔
434
        } else {
435
            State::None
1✔
436
        }
437
    }
18✔
438

439
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
17✔
440
        match cmd {
4✔
441
            Cmd::Delete => {
442
                // Backspace and None
443
                let prev_input = self.states.input.clone();
3✔
444
                self.states.backspace();
3✔
445
                if prev_input == self.states.input {
3✔
446
                    CmdResult::None
1✔
447
                } else {
448
                    CmdResult::Changed(self.state())
2✔
449
                }
450
            }
451
            Cmd::Cancel => {
452
                // Delete and None
453
                let prev_input = self.states.input.clone();
3✔
454
                self.states.delete();
3✔
455
                if prev_input == self.states.input {
3✔
456
                    CmdResult::None
2✔
457
                } else {
458
                    CmdResult::Changed(self.state())
1✔
459
                }
460
            }
461
            Cmd::Submit => CmdResult::Submit(self.state()),
1✔
462
            Cmd::Move(Direction::Left) => {
463
                self.states.decr_cursor();
3✔
464
                CmdResult::None
3✔
465
            }
466
            Cmd::Move(Direction::Right) => {
467
                self.states.incr_cursor();
1✔
468
                CmdResult::None
1✔
469
            }
470
            Cmd::GoTo(Position::Begin) => {
471
                self.states.cursor_at_begin();
1✔
472
                CmdResult::None
1✔
473
            }
474
            Cmd::GoTo(Position::End) => {
475
                self.states.cursor_at_end();
2✔
476
                CmdResult::None
2✔
477
            }
478
            Cmd::Type(ch) => {
3✔
479
                // Push char to input
480
                let prev_input = self.states.input.clone();
3✔
481
                self.states
3✔
482
                    .append(ch, &self.get_input_type(), self.get_input_len());
3✔
483
                // Message on change
484
                if prev_input == self.states.input {
3✔
485
                    CmdResult::None
1✔
486
                } else {
487
                    CmdResult::Changed(self.state())
2✔
488
                }
489
            }
490
            _ => CmdResult::None,
×
491
        }
492
    }
17✔
493
}
494

495
#[cfg(test)]
496
mod tests {
497

498
    use super::*;
499

500
    use pretty_assertions::assert_eq;
501

502
    #[test]
503
    fn test_components_input_states() {
1✔
504
        let mut states: InputStates = InputStates::default();
1✔
505
        states.append('a', &InputType::Text, Some(3));
1✔
506
        assert_eq!(states.input, vec!['a']);
1✔
507
        states.append('b', &InputType::Text, Some(3));
1✔
508
        assert_eq!(states.input, vec!['a', 'b']);
1✔
509
        states.append('c', &InputType::Text, Some(3));
1✔
510
        assert_eq!(states.input, vec!['a', 'b', 'c']);
1✔
511
        // Reached length
512
        states.append('d', &InputType::Text, Some(3));
1✔
513
        assert_eq!(states.input, vec!['a', 'b', 'c']);
1✔
514
        // Push char to numbers
515
        states.append('d', &InputType::Number, None);
1✔
516
        assert_eq!(states.input, vec!['a', 'b', 'c']);
1✔
517
        // move cursor
518
        // decr cursor
519
        states.decr_cursor();
1✔
520
        assert_eq!(states.cursor, 2);
1✔
521
        states.cursor = 1;
1✔
522
        states.decr_cursor();
1✔
523
        assert_eq!(states.cursor, 0);
1✔
524
        states.decr_cursor();
1✔
525
        assert_eq!(states.cursor, 0);
1✔
526
        // Incr
527
        states.incr_cursor();
1✔
528
        assert_eq!(states.cursor, 1);
1✔
529
        states.incr_cursor();
1✔
530
        assert_eq!(states.cursor, 2);
1✔
531
        states.incr_cursor();
1✔
532
        assert_eq!(states.cursor, 3);
1✔
533
        // Render value
534
        assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
1✔
535
        assert_eq!(
1✔
536
            states.render_value(InputType::Password('*')).as_str(),
1✔
537
            "***"
538
        );
539
    }
1✔
540

541
    #[test]
542
    fn test_components_input_text() {
1✔
543
        // Instantiate Input with value
544
        let mut component: Input = Input::default()
1✔
545
            .background(Color::Yellow)
1✔
546
            .borders(Borders::default())
1✔
547
            .foreground(Color::Cyan)
1✔
548
            .inactive(Style::default())
1✔
549
            .input_len(5)
1✔
550
            .input_type(InputType::Text)
1✔
551
            .title("pippo", Alignment::Center)
1✔
552
            .value("home");
1✔
553
        // Verify initial state
554
        assert_eq!(component.states.cursor, 4);
1✔
555
        assert_eq!(component.states.input.len(), 4);
1✔
556
        // Get value
557
        assert_eq!(
1✔
558
            component.state(),
1✔
559
            State::One(StateValue::String(String::from("home")))
1✔
560
        );
561
        // Character
562
        assert_eq!(
1✔
563
            component.perform(Cmd::Type('/')),
1✔
564
            CmdResult::Changed(State::One(StateValue::String(String::from("home/"))))
1✔
565
        );
566
        assert_eq!(
1✔
567
            component.state(),
1✔
568
            State::One(StateValue::String(String::from("home/")))
1✔
569
        );
570
        assert_eq!(component.states.cursor, 5);
1✔
571
        // Verify max length (shouldn't push any character)
572
        assert_eq!(component.perform(Cmd::Type('a')), CmdResult::None);
1✔
573
        assert_eq!(
1✔
574
            component.state(),
1✔
575
            State::One(StateValue::String(String::from("home/")))
1✔
576
        );
577
        assert_eq!(component.states.cursor, 5);
1✔
578
        // Submit
579
        assert_eq!(
1✔
580
            component.perform(Cmd::Submit),
1✔
581
            CmdResult::Submit(State::One(StateValue::String(String::from("home/"))))
1✔
582
        );
583
        // Backspace
584
        assert_eq!(
1✔
585
            component.perform(Cmd::Delete),
1✔
586
            CmdResult::Changed(State::One(StateValue::String(String::from("home"))))
1✔
587
        );
588
        assert_eq!(
1✔
589
            component.state(),
1✔
590
            State::One(StateValue::String(String::from("home")))
1✔
591
        );
592
        assert_eq!(component.states.cursor, 4);
1✔
593
        // Check backspace at 0
594
        component.states.input = vec!['h'];
1✔
595
        component.states.cursor = 1;
1✔
596
        assert_eq!(
1✔
597
            component.perform(Cmd::Delete),
1✔
598
            CmdResult::Changed(State::One(StateValue::String(String::new())))
1✔
599
        );
600
        assert_eq!(
1✔
601
            component.state(),
1✔
602
            State::One(StateValue::String(String::new()))
1✔
603
        );
604
        assert_eq!(component.states.cursor, 0);
1✔
605
        // Another one...
606
        assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
1✔
607
        assert_eq!(
1✔
608
            component.state(),
1✔
609
            State::One(StateValue::String(String::new()))
1✔
610
        );
611
        assert_eq!(component.states.cursor, 0);
1✔
612
        // See del behaviour here
613
        assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
1✔
614
        assert_eq!(
1✔
615
            component.state(),
1✔
616
            State::One(StateValue::String(String::new()))
1✔
617
        );
618
        assert_eq!(component.states.cursor, 0);
1✔
619
        // Check del behaviour
620
        component.states.input = vec!['h', 'e'];
1✔
621
        component.states.cursor = 1;
1✔
622
        assert_eq!(
1✔
623
            component.perform(Cmd::Cancel),
1✔
624
            CmdResult::Changed(State::One(StateValue::String(String::from("h"))))
1✔
625
        );
626
        assert_eq!(
1✔
627
            component.state(),
1✔
628
            State::One(StateValue::String(String::from("h")))
1✔
629
        );
630
        assert_eq!(component.states.cursor, 1);
1✔
631
        // Another one (should do nothing)
632
        assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
1✔
633
        assert_eq!(
1✔
634
            component.state(),
1✔
635
            State::One(StateValue::String(String::from("h")))
1✔
636
        );
637
        assert_eq!(component.states.cursor, 1);
1✔
638
        // Move cursor right
639
        component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
1✔
640
        // Update length to 16
641
        component.attr(Attribute::InputLength, AttrValue::Length(16));
1✔
642
        component.states.cursor = 1;
1✔
643
        assert_eq!(
1✔
644
            component.perform(Cmd::Move(Direction::Right)), // between 'e' and 'l'
1✔
645
            CmdResult::None
646
        );
647
        assert_eq!(component.states.cursor, 2);
1✔
648
        // Put a character here
649
        assert_eq!(
1✔
650
            component.perform(Cmd::Type('a')),
1✔
651
            CmdResult::Changed(State::One(StateValue::String(String::from("heallo"))))
1✔
652
        );
653
        assert_eq!(
1✔
654
            component.state(),
1✔
655
            State::One(StateValue::String(String::from("heallo")))
1✔
656
        );
657
        assert_eq!(component.states.cursor, 3);
1✔
658
        // Move left
659
        assert_eq!(
1✔
660
            component.perform(Cmd::Move(Direction::Left)),
1✔
661
            CmdResult::None
662
        );
663
        assert_eq!(component.states.cursor, 2);
1✔
664
        // Go at the end
665
        component.states.cursor = 6;
1✔
666
        // Move right
667
        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
1✔
668
        assert_eq!(component.states.cursor, 6);
1✔
669
        // Move left
670
        assert_eq!(
1✔
671
            component.perform(Cmd::Move(Direction::Left)),
1✔
672
            CmdResult::None
673
        );
674
        assert_eq!(component.states.cursor, 5);
1✔
675
        // Go at the beginning
676
        component.states.cursor = 0;
1✔
677
        assert_eq!(
1✔
678
            component.perform(Cmd::Move(Direction::Left)),
1✔
679
            CmdResult::None
680
        );
681
        //assert_eq!(component.render().unwrap().cursor, 0); // Should stay
682
        assert_eq!(component.states.cursor, 0);
1✔
683
        // End - begin
684
        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
1✔
685
        assert_eq!(component.states.cursor, 6);
1✔
686
        assert_eq!(
1✔
687
            component.perform(Cmd::GoTo(Position::Begin)),
1✔
688
            CmdResult::None
689
        );
690
        assert_eq!(component.states.cursor, 0);
1✔
691
        // Update value
692
        component.attr(Attribute::Value, AttrValue::String("new-value".to_string()));
1✔
693
        assert_eq!(
1✔
694
            component.state(),
1✔
695
            State::One(StateValue::String(String::from("new-value")))
1✔
696
        );
697
        // Invalidate input type
698
        component.attr(
1✔
699
            Attribute::InputType,
1✔
700
            AttrValue::InputType(InputType::Number),
1✔
701
        );
702
        assert_eq!(component.state(), State::None);
1✔
703
    }
1✔
704

705
    #[test]
706
    fn should_keep_cursor_within_bounds() {
1✔
707
        let text = "The quick brown fox jumps over the lazy dog";
1✔
708
        assert!(text.len() > 15);
1✔
709

710
        let mut states = InputStates::default();
1✔
711

712
        for ch in text.chars() {
43✔
713
            states.append(ch, &InputType::Text, None);
43✔
714
        }
43✔
715

716
        // at first, without any "width" set, both functions should return the same
717
        assert_eq!(states.cursor, text.len());
1✔
718
        assert_eq!(
1✔
719
            states.render_value(InputType::Text),
1✔
720
            states.render_value_offset(InputType::Text)
1✔
721
        );
722

723
        states.update_width(10);
1✔
724

725
        assert_eq!(
1✔
726
            states.render_value_offset(InputType::Text),
1✔
727
            text[text.len() - 10..]
1✔
728
        );
729

730
        // the displayed text should not change until being in PREVIEW_STEP
731
        for i in 1..8 {
7✔
732
            states.decr_cursor();
7✔
733
            assert_eq!(states.cursor, text.len() - i);
7✔
734
            let val = states.render_value_offset(InputType::Text);
7✔
735
            assert_eq!(val, text[text.len() - 10..]);
7✔
736
        }
737

738
        // preview step space at the end
739
        states.decr_cursor();
1✔
740
        assert_eq!(states.cursor, text.len() - 8);
1✔
741
        assert_eq!(
1✔
742
            states.render_value_offset(InputType::Text),
1✔
743
            text[text.len() - 10..]
1✔
744
        );
745

746
        states.decr_cursor();
1✔
747
        assert_eq!(states.cursor, text.len() - 9);
1✔
748
        assert_eq!(
1✔
749
            states.render_value_offset(InputType::Text),
1✔
750
            text[text.len() - 11..]
1✔
751
        );
752

753
        states.decr_cursor();
1✔
754
        assert_eq!(states.cursor, text.len() - 10);
1✔
755
        assert_eq!(
1✔
756
            states.render_value_offset(InputType::Text),
1✔
757
            text[text.len() - 12..]
1✔
758
        );
759

760
        states.cursor_at_begin();
1✔
761
        assert_eq!(states.cursor, 0);
1✔
762
        assert_eq!(states.render_value(InputType::Text), text);
1✔
763

764
        // the displayed text should not change until being in PREVIEW_STEP
765
        for i in 1..9 {
8✔
766
            states.incr_cursor();
8✔
767
            assert_eq!(states.cursor, i);
8✔
768
            let val = states.render_value_offset(InputType::Text);
8✔
769
            assert_eq!(val, text);
8✔
770
        }
771

772
        states.incr_cursor();
1✔
773
        assert_eq!(states.cursor, 9);
1✔
774
        assert_eq!(states.render_value_offset(InputType::Text), text[1..]);
1✔
775

776
        states.incr_cursor();
1✔
777
        assert_eq!(states.cursor, 10);
1✔
778
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
1✔
779

780
        // increasing width should not change display_offset
781
        states.update_width(30);
1✔
782
        assert_eq!(states.cursor, 10);
1✔
783
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
1✔
784

785
        // reset to 10, should also not change
786
        states.update_width(10);
1✔
787
        assert_eq!(states.cursor, 10);
1✔
788
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
1✔
789

790
        // should change display_offset by 1
791
        states.update_width(9);
1✔
792
        assert_eq!(states.cursor, 10);
1✔
793
        assert_eq!(states.render_value_offset(InputType::Text), text[3..]);
1✔
794

795
        // reset to end
796
        states.update_width(10);
1✔
797
        states.cursor_at_end();
1✔
798

799
        // the displayed text should not change until being in PREVIEW_STEP
800
        for i in 1..=4 {
4✔
801
            states.decr_cursor();
4✔
802
            assert_eq!(states.cursor, text.len() - i);
4✔
803
            let val = states.render_value_offset(InputType::Text);
4✔
804
            assert_eq!(val, text[text.len() - 8..]);
4✔
805
        }
806

807
        assert_eq!(states.cursor, text.len() - 4);
1✔
808
        states.incr_cursor();
1✔
809
        assert_eq!(states.cursor, text.len() - 3);
1✔
810
        assert_eq!(
1✔
811
            states.render_value_offset(InputType::Text),
1✔
812
            text[text.len() - 8..]
1✔
813
        );
814

815
        // note any width below PREVIEW_STEP * 2 + 1 is undefined behavior
816
    }
1✔
817
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc