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

veeso / tui-realm-stdlib / 22147789116

09 Feb 2026 04:20PM UTC coverage: 77.303% (+4.8%) from 72.482%
22147789116

push

github

hasezoey
refactor(table): switch to use "CommonProps"

18 of 90 new or added lines in 1 file covered. (20.0%)

405 existing lines in 20 files now uncovered.

3944 of 5102 relevant lines covered (77.3%)

4.92 hits per line

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

81.49
/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::prop_ext::CommonProps;
8
use crate::utils::calc_utf8_cursor_position;
9
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
10
use tuirealm::props::{
11
    AttrValue, Attribute, Borders, Color, InputType, Props, Style, TextModifiers, Title,
12
};
13
use tuirealm::ratatui::{layout::Rect, widgets::Paragraph};
14
use tuirealm::{Frame, MockComponent, State, StateValue};
15

16
// -- states
17

18
/// The number of characters [`InputStates::display_offset`] will keep in view in a single direction.
19
const PREVIEW_DISTANCE: usize = 2;
20

21
#[derive(Default, Debug)]
22
pub struct InputStates {
23
    /// The current input text
24
    pub input: Vec<char>,
25
    /// The cursor into "input", used as a index on where a character gets added next
26
    pub cursor: usize,
27
    /// The display offset for scrolling, always tries to keep the cursor within bounds
28
    pub display_offset: usize,
29
    /// The last drawn width of the component that displays "input".
30
    ///
31
    /// This is necessary to keep "display_offset" from jumping around on width changes.
32
    pub last_width: Option<u16>,
33
}
34

35
impl InputStates {
36
    /// ### append
37
    ///
38
    /// Append, if possible according to input type, the character to the input vec
39
    pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
156✔
40
        // Check if max length has been reached
78✔
41
        if self.input.len() < max_len.unwrap_or(usize::MAX) {
156✔
42
            // Check whether can push
43
            if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
152✔
44
                self.input.insert(self.cursor, ch);
134✔
45
                self.incr_cursor();
134✔
46
            }
134✔
47
        }
4✔
48
    }
156✔
49

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

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

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

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

81
            if let Some(last_width) = self.last_width {
164✔
82
                let input_with_width = self.input.len().saturating_sub(
22✔
83
                    usize::from(self.last_width.unwrap_or_default())
22✔
84
                        .saturating_sub(PREVIEW_DISTANCE),
22✔
85
                );
11✔
86
                // only increase the offset IF cursor is higher than last_width
11✔
87
                // and the remaining text does not fit within the last_width
11✔
88
                if self.cursor
22✔
89
                    > usize::from(last_width).saturating_sub(PREVIEW_DISTANCE) + self.display_offset
22✔
90
                    && self.display_offset < input_with_width
4✔
91
                {
4✔
92
                    self.display_offset += 1;
4✔
93
                }
18✔
94
            }
142✔
UNCOV
95
        }
×
96
    }
164✔
97

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

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

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

18✔
123
            if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
36✔
124
                self.display_offset = self.display_offset.saturating_sub(1);
8✔
125
            }
28✔
126
        }
4✔
127
    }
40✔
128

129
    /// ### update_width
130
    ///
131
    /// Update the last width used to display [`InputStates::input`].
132
    ///
133
    /// This is necessary to update [`InputStates::display_offset`] correctly and keep it
134
    /// from jumping around on width changes.
135
    ///
136
    /// Without using this function, no scrolling will effectively be applied.
137
    pub fn update_width(&mut self, new_width: u16) {
10✔
138
        let old_width = self.last_width;
10✔
139
        self.last_width = Some(new_width);
10✔
140

5✔
141
        // if the cursor would now be out-of-bounds, adjust the display offset to keep the cursor within bounds
5✔
142
        if self.cursor
10✔
143
            > (self.display_offset + usize::from(new_width)).saturating_sub(PREVIEW_DISTANCE)
10✔
144
        {
145
            let diff = if let Some(old_width) = old_width {
4✔
146
                usize::from(old_width.saturating_sub(new_width))
2✔
147
            } else {
148
                // there was no previous width, use new_width minus cursor.
149
                // this happens if "update_width" had never been called (like before the first draw)
150
                // but the value is longer than the current display width and the cursor is not within bounds.
151
                self.cursor.saturating_sub(usize::from(new_width))
2✔
152
            };
153
            self.display_offset += diff;
4✔
154
        }
6✔
155
    }
10✔
156

157
    /// ### render_value
158
    ///
159
    /// Get value as string to render
160
    #[must_use]
161
    pub fn render_value(&self, itype: InputType) -> String {
8✔
162
        self.render_value_chars(itype).iter().collect::<String>()
8✔
163
    }
8✔
164

165
    /// ### render_value_offset
166
    ///
167
    /// Get value as a string to render, with the [`InputStates::display_offset`] already skipped.
168
    #[must_use]
169
    pub fn render_value_offset(&self, itype: InputType) -> String {
60✔
170
        self.render_value_chars(itype)
60✔
171
            .iter()
60✔
172
            .skip(self.display_offset)
60✔
173
            .collect()
60✔
174
    }
60✔
175

176
    /// ### render_value_chars
177
    ///
178
    /// Render value as a vec of chars
179
    #[must_use]
180
    pub fn render_value_chars(&self, itype: InputType) -> Vec<char> {
68✔
181
        match itype {
68✔
182
            InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
2✔
183
                (0..self.input.len()).map(|_| ch).collect()
4✔
184
            }
185
            _ => self.input.clone(),
66✔
186
        }
187
    }
68✔
188

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

198
// -- Component
199

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

211
impl Input {
212
    /// Set the main foreground color. This may get overwritten by individual text styles.
213
    pub fn foreground(mut self, fg: Color) -> Self {
2✔
214
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
2✔
215
        self
2✔
216
    }
2✔
217

218
    /// Set the main background color. This may get overwritten by individual text styles.
219
    pub fn background(mut self, bg: Color) -> Self {
2✔
220
        self.attr(Attribute::Background, AttrValue::Color(bg));
2✔
221
        self
2✔
222
    }
2✔
223

224
    /// Set the main text modifiers. This may get overwritten by individual text styles.
UNCOV
225
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
×
UNCOV
226
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
×
UNCOV
227
        self
×
UNCOV
228
    }
×
229

230
    /// Set the main style. This may get overwritten by individual text styles.
231
    ///
232
    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
UNCOV
233
    pub fn style(mut self, style: Style) -> Self {
×
UNCOV
234
        self.attr(Attribute::Style, AttrValue::Style(style));
×
UNCOV
235
        self
×
UNCOV
236
    }
×
237

238
    /// Set a custom style for the border when the component is unfocused.
239
    pub fn inactive(mut self, s: Style) -> Self {
2✔
240
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
2✔
241
        self
2✔
242
    }
2✔
243

244
    /// Add a border to the component.
245
    pub fn borders(mut self, b: Borders) -> Self {
2✔
246
        self.attr(Attribute::Borders, AttrValue::Borders(b));
2✔
247
        self
2✔
248
    }
2✔
249

250
    /// Add a title to the component.
251
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
2✔
252
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
2✔
253
        self
2✔
254
    }
2✔
255

256
    /// Set the type of input this Input Component is for. Specific types may have different display or validate methods.
257
    pub fn input_type(mut self, itype: InputType) -> Self {
2✔
258
        self.attr(Attribute::InputType, AttrValue::InputType(itype));
2✔
259
        self
2✔
260
    }
2✔
261

262
    /// Set the max length of the input.
263
    pub fn input_len(mut self, ilen: usize) -> Self {
2✔
264
        self.attr(Attribute::InputLength, AttrValue::Length(ilen));
2✔
265
        self
2✔
266
    }
2✔
267

268
    /// Set the inital value of the Input.
269
    pub fn value<S: Into<String>>(mut self, s: S) -> Self {
2✔
270
        self.attr(Attribute::Value, AttrValue::String(s.into()));
2✔
271
        self
2✔
272
    }
2✔
273

274
    /// Set a style for when the input fails validation.
UNCOV
275
    pub fn invalid_style(mut self, s: Style) -> Self {
×
UNCOV
276
        self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
×
UNCOV
277
        self
×
UNCOV
278
    }
×
279

280
    /// Set a placeholder text and stylew for when the Input is empty.
UNCOV
281
    pub fn placeholder<S: Into<String>>(mut self, placeholder: S, style: Style) -> Self {
×
282
        // TODO: Span / Line?
UNCOV
283
        self.attr(
×
UNCOV
284
            Attribute::Custom(INPUT_PLACEHOLDER),
×
UNCOV
285
            AttrValue::String(placeholder.into()),
×
286
        );
UNCOV
287
        self.attr(
×
UNCOV
288
            Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
289
            AttrValue::Style(style),
×
290
        );
291
        self
×
292
    }
×
293

294
    fn get_input_len(&self) -> Option<usize> {
18✔
295
        self.props
18✔
296
            .get(Attribute::InputLength)
18✔
297
            .map(|x| x.unwrap_length())
18✔
298
    }
18✔
299

300
    fn get_input_type(&self) -> InputType {
54✔
301
        self.props
54✔
302
            .get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
54✔
303
            .unwrap_input_type()
54✔
304
    }
54✔
305

306
    /// ### is_valid
307
    ///
308
    /// Checks whether current input is valid
309
    fn is_valid(&self) -> bool {
36✔
310
        let value = self.states.get_value();
36✔
311
        self.get_input_type().validate(value.as_str())
36✔
312
    }
36✔
313
}
314

315
impl MockComponent for Input {
316
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
317
        if !self.common.display {
×
318
            return;
×
319
        }
×
320

321
        let mut normal_style = self.common.style;
×
322

323
        let itype = self.get_input_type();
×
UNCOV
324
        let mut block = self.common.get_block();
×
325
        // Apply invalid style
326
        // TODO: invalid style should likely still be applied even if unfocused
327
        if self.common.focused && !self.is_valid() {
×
328
            if let Some(invalid_style) = self
×
329
                .props
×
330
                .get(Attribute::Custom(INPUT_INVALID_STYLE))
×
331
                .map(|x| x.unwrap_style())
×
332
            {
333
                if let Some(block) = &mut block {
×
334
                    let border_style = self
×
335
                        .common
×
336
                        .border
×
337
                        .unwrap_or_default()
×
338
                        .style()
×
339
                        .patch(invalid_style);
×
340
                    // i dont like this, but ratatui does not offer a non-self taking method to change the style
×
UNCOV
341
                    *block = std::mem::take(block).border_style(border_style);
×
UNCOV
342
                }
×
343

UNCOV
344
                normal_style = normal_style.patch(invalid_style);
×
345
            }
×
UNCOV
346
        }
×
347

UNCOV
348
        let mut area_for_bounds = area;
×
349

UNCOV
350
        if let Some(block) = &block {
×
351
            // Create input's area
×
352
            let block_inner_area = block.inner(area);
×
353

×
354
            self.states.update_width(block_inner_area.width);
×
355

×
356
            area_for_bounds = block_inner_area;
×
UNCOV
357
        }
×
358

UNCOV
359
        let text_to_display = self.states.render_value_offset(self.get_input_type());
×
360

UNCOV
361
        let show_placeholder = text_to_display.is_empty();
×
362
        // Choose whether to show placeholder; if placeholder is unset, show nothing
363
        let text_to_display = if show_placeholder {
×
364
            self.states.cursor = 0;
×
365
            self.props
×
366
                .get_or(
×
367
                    Attribute::Custom(INPUT_PLACEHOLDER),
×
UNCOV
368
                    AttrValue::String(String::new()),
×
369
                )
UNCOV
370
                .unwrap_string()
×
371
        } else {
372
            text_to_display
×
373
        };
374
        // Choose paragraph style based on whether is valid or not and if has focus and if should show placeholder
375
        let paragraph_style = if self.common.focused {
×
UNCOV
376
            normal_style
×
377
        } else {
378
            // TODO: this should likely be a different property
379
            self.common.border_unfocused_style
×
380
        };
UNCOV
381
        let paragraph_style = if show_placeholder {
×
382
            self.props
×
383
                .get_or(
×
384
                    Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
385
                    AttrValue::Style(paragraph_style),
×
386
                )
UNCOV
387
                .unwrap_style()
×
388
        } else {
389
            paragraph_style
×
390
        };
391

392
        let mut widget = Paragraph::new(text_to_display).style(paragraph_style);
×
393

394
        if let Some(block) = block {
×
395
            widget = widget.block(block);
×
396
        }
×
397

398
        render.render_widget(widget, area);
×
399

400
        // Set cursor, if focus
401
        if self.common.focused && !area_for_bounds.is_empty() {
×
UNCOV
402
            let x: u16 = area_for_bounds.x
×
403
                + calc_utf8_cursor_position(
×
404
                    &self.states.render_value_chars(itype)[0..self.states.cursor],
×
405
                )
×
UNCOV
406
                .saturating_sub(u16::try_from(self.states.display_offset).unwrap_or(u16::MAX));
×
UNCOV
407
            let x = x.min(area_for_bounds.x + area_for_bounds.width);
×
UNCOV
408
            render.set_cursor_position(tuirealm::ratatui::prelude::Position {
×
UNCOV
409
                x,
×
UNCOV
410
                y: area_for_bounds.y,
×
UNCOV
411
            });
×
UNCOV
412
        }
×
UNCOV
413
    }
×
414

UNCOV
415
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
UNCOV
416
        if let Some(value) = self.common.get(attr) {
×
UNCOV
417
            return Some(value);
×
UNCOV
418
        }
×
419

UNCOV
420
        self.props.get(attr)
×
UNCOV
421
    }
×
422

423
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
22✔
424
        if let Some(value) = self.common.set(attr, value) {
22✔
425
            let sanitize_input = matches!(
12✔
426
                attr,
12✔
427
                Attribute::InputLength | Attribute::InputType | Attribute::Value
428
            );
429
            // Check if new input
430
            let new_input = match attr {
12✔
431
                Attribute::Value => Some(value.clone().unwrap_string()),
4✔
432
                _ => None,
8✔
433
            };
434
            self.props.set(attr, value);
12✔
435
            if sanitize_input {
12✔
436
                let input = match new_input {
12✔
437
                    None => self.states.input.clone(),
8✔
438
                    Some(v) => v.chars().collect(),
4✔
439
                };
440
                self.states.input = Vec::new();
12✔
441
                self.states.cursor = 0;
12✔
442
                let itype = self.get_input_type();
12✔
443
                let max_len = self.get_input_len();
12✔
444
                for ch in input {
60✔
445
                    self.states.append(ch, &itype, max_len);
54✔
446
                }
54✔
UNCOV
447
            }
×
448
        }
10✔
449
    }
22✔
450

451
    fn state(&self) -> State {
36✔
452
        // Validate input
18✔
453
        if self.is_valid() {
36✔
454
            State::Single(StateValue::String(self.states.get_value()))
34✔
455
        } else {
456
            State::None
2✔
457
        }
458
    }
36✔
459

460
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
34✔
461
        match cmd {
8✔
462
            Cmd::Delete => {
463
                // Backspace and None
464
                let prev_input = self.states.input.clone();
6✔
465
                self.states.backspace();
6✔
466
                if prev_input == self.states.input {
6✔
467
                    CmdResult::None
2✔
468
                } else {
469
                    CmdResult::Changed(self.state())
4✔
470
                }
471
            }
472
            Cmd::Cancel => {
473
                // Delete and None
474
                let prev_input = self.states.input.clone();
6✔
475
                self.states.delete();
6✔
476
                if prev_input == self.states.input {
6✔
477
                    CmdResult::None
4✔
478
                } else {
479
                    CmdResult::Changed(self.state())
2✔
480
                }
481
            }
482
            Cmd::Submit => CmdResult::Submit(self.state()),
2✔
483
            Cmd::Move(Direction::Left) => {
484
                self.states.decr_cursor();
6✔
485
                CmdResult::None
6✔
486
            }
487
            Cmd::Move(Direction::Right) => {
488
                self.states.incr_cursor();
2✔
489
                CmdResult::None
2✔
490
            }
491
            Cmd::GoTo(Position::Begin) => {
492
                self.states.cursor_at_begin();
2✔
493
                CmdResult::None
2✔
494
            }
495
            Cmd::GoTo(Position::End) => {
496
                self.states.cursor_at_end();
4✔
497
                CmdResult::None
4✔
498
            }
499
            Cmd::Type(ch) => {
6✔
500
                // Push char to input
3✔
501
                let prev_input = self.states.input.clone();
6✔
502
                self.states
6✔
503
                    .append(ch, &self.get_input_type(), self.get_input_len());
6✔
504
                // Message on change
3✔
505
                if prev_input == self.states.input {
6✔
506
                    CmdResult::None
2✔
507
                } else {
508
                    CmdResult::Changed(self.state())
4✔
509
                }
510
            }
UNCOV
511
            _ => CmdResult::None,
×
512
        }
513
    }
34✔
514
}
515

516
#[cfg(test)]
517
mod tests {
518

519
    use super::*;
520

521
    use pretty_assertions::assert_eq;
522
    use tuirealm::props::HorizontalAlignment;
523

524
    #[test]
525
    fn test_components_input_states() {
2✔
526
        let mut states: InputStates = InputStates::default();
2✔
527
        states.append('a', &InputType::Text, Some(3));
2✔
528
        assert_eq!(states.input, vec!['a']);
2✔
529
        states.append('b', &InputType::Text, Some(3));
2✔
530
        assert_eq!(states.input, vec!['a', 'b']);
2✔
531
        states.append('c', &InputType::Text, Some(3));
2✔
532
        assert_eq!(states.input, vec!['a', 'b', 'c']);
2✔
533
        // Reached length
534
        states.append('d', &InputType::Text, Some(3));
2✔
535
        assert_eq!(states.input, vec!['a', 'b', 'c']);
2✔
536
        // Push char to numbers
537
        states.append('d', &InputType::Number, None);
2✔
538
        assert_eq!(states.input, vec!['a', 'b', 'c']);
2✔
539
        // move cursor
540
        // decr cursor
541
        states.decr_cursor();
2✔
542
        assert_eq!(states.cursor, 2);
2✔
543
        states.cursor = 1;
2✔
544
        states.decr_cursor();
2✔
545
        assert_eq!(states.cursor, 0);
2✔
546
        states.decr_cursor();
2✔
547
        assert_eq!(states.cursor, 0);
2✔
548
        // Incr
549
        states.incr_cursor();
2✔
550
        assert_eq!(states.cursor, 1);
2✔
551
        states.incr_cursor();
2✔
552
        assert_eq!(states.cursor, 2);
2✔
553
        states.incr_cursor();
2✔
554
        assert_eq!(states.cursor, 3);
2✔
555
        // Render value
556
        assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
2✔
557
        assert_eq!(
2✔
558
            states.render_value(InputType::Password('*')).as_str(),
2✔
559
            "***"
1✔
560
        );
1✔
561
    }
2✔
562

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

727
    #[test]
728
    fn should_keep_cursor_within_bounds() {
2✔
729
        let text = "The quick brown fox jumps over the lazy dog";
2✔
730
        assert!(text.len() > 15);
2✔
731

732
        let mut states = InputStates::default();
2✔
733

734
        for ch in text.chars() {
86✔
735
            states.append(ch, &InputType::Text, None);
86✔
736
        }
86✔
737

738
        // at first, without any "width" set, both functions should return the same
739
        assert_eq!(states.cursor, text.len());
2✔
740
        assert_eq!(
2✔
741
            states.render_value(InputType::Text),
2✔
742
            states.render_value_offset(InputType::Text)
2✔
743
        );
1✔
744

745
        states.update_width(10);
2✔
746

1✔
747
        assert_eq!(
2✔
748
            states.render_value_offset(InputType::Text),
2✔
749
            text[text.len() - 10..]
2✔
750
        );
1✔
751

752
        // the displayed text should not change until being in PREVIEW_STEP
753
        for i in 1..8 {
15✔
754
            states.decr_cursor();
14✔
755
            assert_eq!(states.cursor, text.len() - i);
14✔
756
            let val = states.render_value_offset(InputType::Text);
14✔
757
            assert_eq!(val, text[text.len() - 10..]);
14✔
758
        }
759

760
        // preview step space at the end
761
        states.decr_cursor();
2✔
762
        assert_eq!(states.cursor, text.len() - 8);
2✔
763
        assert_eq!(
2✔
764
            states.render_value_offset(InputType::Text),
2✔
765
            text[text.len() - 10..]
2✔
766
        );
1✔
767

768
        states.decr_cursor();
2✔
769
        assert_eq!(states.cursor, text.len() - 9);
2✔
770
        assert_eq!(
2✔
771
            states.render_value_offset(InputType::Text),
2✔
772
            text[text.len() - 11..]
2✔
773
        );
1✔
774

775
        states.decr_cursor();
2✔
776
        assert_eq!(states.cursor, text.len() - 10);
2✔
777
        assert_eq!(
2✔
778
            states.render_value_offset(InputType::Text),
2✔
779
            text[text.len() - 12..]
2✔
780
        );
1✔
781

782
        states.cursor_at_begin();
2✔
783
        assert_eq!(states.cursor, 0);
2✔
784
        assert_eq!(states.render_value(InputType::Text), text);
2✔
785

786
        // the displayed text should not change until being in PREVIEW_STEP
787
        for i in 1..9 {
17✔
788
            states.incr_cursor();
16✔
789
            assert_eq!(states.cursor, i);
16✔
790
            let val = states.render_value_offset(InputType::Text);
16✔
791
            assert_eq!(val, text);
16✔
792
        }
793

794
        states.incr_cursor();
2✔
795
        assert_eq!(states.cursor, 9);
2✔
796
        assert_eq!(states.render_value_offset(InputType::Text), text[1..]);
2✔
797

798
        states.incr_cursor();
2✔
799
        assert_eq!(states.cursor, 10);
2✔
800
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
2✔
801

802
        // increasing width should not change display_offset
803
        states.update_width(30);
2✔
804
        assert_eq!(states.cursor, 10);
2✔
805
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
2✔
806

807
        // reset to 10, should also not change
808
        states.update_width(10);
2✔
809
        assert_eq!(states.cursor, 10);
2✔
810
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
2✔
811

812
        // should change display_offset by 1
813
        states.update_width(9);
2✔
814
        assert_eq!(states.cursor, 10);
2✔
815
        assert_eq!(states.render_value_offset(InputType::Text), text[3..]);
2✔
816

817
        // reset to end
818
        states.update_width(10);
2✔
819
        states.cursor_at_end();
2✔
820

821
        // the displayed text should not change until being in PREVIEW_STEP
822
        for i in 1..=4 {
9✔
823
            states.decr_cursor();
8✔
824
            assert_eq!(states.cursor, text.len() - i);
8✔
825
            let val = states.render_value_offset(InputType::Text);
8✔
826
            assert_eq!(val, text[text.len() - 8..]);
8✔
827
        }
828

829
        assert_eq!(states.cursor, text.len() - 4);
2✔
830
        states.incr_cursor();
2✔
831
        assert_eq!(states.cursor, text.len() - 3);
2✔
832
        assert_eq!(
2✔
833
            states.render_value_offset(InputType::Text),
2✔
834
            text[text.len() - 8..]
2✔
835
        );
1✔
836

837
        // note any width below PREVIEW_STEP * 2 + 1 is undefined behavior
838
    }
2✔
839
}
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