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

veeso / tui-realm-stdlib / 22143900361

18 Feb 2026 02:31PM UTC coverage: 76.863% (+4.4%) from 72.486%
22143900361

Pull #58

github

web-flow
Merge 82269e219 into 237158c92
Pull Request #58: Remove unnecessary features

588 of 1352 new or added lines in 21 files covered. (43.49%)

40 existing lines in 15 files now uncovered.

3940 of 5126 relevant lines covered (76.86%)

4.91 hits per line

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

81.52
/src/components/input.rs
1
//! `Input` represents a read-write input field. This component supports different input types, input length
2
//! and handles input events related to cursor position, backspace, canc, ...
3

4
use super::props::{INPUT_INVALID_STYLE, INPUT_PLACEHOLDER, INPUT_PLACEHOLDER_STYLE};
5
use crate::prop_ext::CommonProps;
6
use crate::utils::calc_utf8_cursor_position;
7
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
8
use tuirealm::props::{
9
    AttrValue, Attribute, Borders, Color, InputType, Props, Style, TextModifiers, Title,
10
};
11
use tuirealm::ratatui::{layout::Rect, widgets::Paragraph};
12
use tuirealm::{Frame, MockComponent, State, StateValue};
13

14
// -- states
15

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

19
/// The state that needs to be kept for the [`Input`] component.
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 a character, if possible according to input type.
36
    pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
156✔
37
        // Check if max length has been reached
78✔
38
        if self.input.len() < max_len.unwrap_or(usize::MAX) {
156✔
39
            // Check whether can push
40
            if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
152✔
41
                self.input.insert(self.cursor, ch);
134✔
42
                self.incr_cursor();
134✔
43
            }
134✔
44
        }
4✔
45
    }
156✔
46

47
    /// Delete the element at `cursor - 1`, then decrement cursor by 1.
48
    pub fn backspace(&mut self) {
6✔
49
        if self.cursor > 0 && !self.input.is_empty() {
6✔
50
            self.input.remove(self.cursor - 1);
4✔
51
            // Decrement cursor
2✔
52
            self.cursor -= 1;
4✔
53

2✔
54
            if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
4✔
55
                self.display_offset = self.display_offset.saturating_sub(1);
2✔
56
            }
2✔
57
        }
2✔
58
    }
6✔
59

60
    /// Delete element at cursor position.
61
    pub fn delete(&mut self) {
6✔
62
        if self.cursor < self.input.len() {
6✔
63
            self.input.remove(self.cursor);
2✔
64
        }
4✔
65
    }
6✔
66

67
    /// Increment cursor by one if possible. (also known as moving RIGHT)
68
    pub fn incr_cursor(&mut self) {
164✔
69
        if self.cursor < self.input.len() {
164✔
70
            self.cursor += 1;
164✔
71

72
            if let Some(last_width) = self.last_width {
164✔
73
                let input_with_width = self.input.len().saturating_sub(
22✔
74
                    usize::from(self.last_width.unwrap_or_default())
22✔
75
                        .saturating_sub(PREVIEW_DISTANCE),
22✔
76
                );
11✔
77
                // only increase the offset IF cursor is higher than last_width
11✔
78
                // and the remaining text does not fit within the last_width
11✔
79
                if self.cursor
22✔
80
                    > usize::from(last_width).saturating_sub(PREVIEW_DISTANCE) + self.display_offset
22✔
81
                    && self.display_offset < input_with_width
4✔
82
                {
4✔
83
                    self.display_offset += 1;
4✔
84
                }
18✔
85
            }
142✔
86
        }
×
87
    }
164✔
88

89
    /// Decrement cursor value by one if possible. (also known as moving LEFT)
90
    pub fn decr_cursor(&mut self) {
40✔
91
        if self.cursor > 0 {
40✔
92
            self.cursor -= 1;
36✔
93

18✔
94
            if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
36✔
95
                self.display_offset = self.display_offset.saturating_sub(1);
8✔
96
            }
28✔
97
        }
4✔
98
    }
40✔
99

100
    /// Move the cursor to the beginning 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
    /// Move the cursor the the end of the input.
107
    pub fn cursor_at_end(&mut self) {
6✔
108
        self.cursor = self.input.len();
6✔
109
        self.display_offset = self.input.len().saturating_sub(
6✔
110
            usize::from(self.last_width.unwrap_or_default()).saturating_sub(PREVIEW_DISTANCE),
6✔
111
        );
6✔
112
    }
6✔
113

114
    /// Update the last width used to display [`InputStates::input`].
115
    ///
116
    /// This is necessary to update [`InputStates::display_offset`] correctly and keep it
117
    /// from jumping around on width changes.
118
    ///
119
    /// Without using this function, no scrolling will effectively be applied.
120
    pub fn update_width(&mut self, new_width: u16) {
10✔
121
        let old_width = self.last_width;
10✔
122
        self.last_width = Some(new_width);
10✔
123

5✔
124
        // if the cursor would now be out-of-bounds, adjust the display offset to keep the cursor within bounds
5✔
125
        if self.cursor
10✔
126
            > (self.display_offset + usize::from(new_width)).saturating_sub(PREVIEW_DISTANCE)
10✔
127
        {
128
            let diff = if let Some(old_width) = old_width {
4✔
129
                usize::from(old_width.saturating_sub(new_width))
2✔
130
            } else {
131
                // there was no previous width, use new_width minus cursor.
132
                // this happens if "update_width" had never been called (like before the first draw)
133
                // but the value is longer than the current display width and the cursor is not within bounds.
134
                self.cursor.saturating_sub(usize::from(new_width))
2✔
135
            };
136
            self.display_offset += diff;
4✔
137
        }
6✔
138
    }
10✔
139

140
    /// Get the full text to render in the input, according to the [`InputType`].
141
    #[must_use]
142
    pub fn render_value(&self, itype: InputType) -> String {
8✔
143
        self.render_value_chars(itype).iter().collect::<String>()
8✔
144
    }
8✔
145

146
    /// Get the partial text to render, according to the [`InputType`].
147
    ///
148
    /// Unlike [`InputStates::render_value`], this will only collect the actually displayed text in the returned String.
149
    #[must_use]
150
    pub fn render_value_offset(&self, itype: InputType) -> String {
60✔
151
        self.render_value_chars(itype)
60✔
152
            .iter()
60✔
153
            .skip(self.display_offset)
60✔
154
            .collect()
60✔
155
    }
60✔
156

157
    /// Get the characters to render, according to [`InputType`].
158
    ///
159
    /// It is recommended to use [`render_value`](Self::render_value) or [`render_value_offset`](Self::render_value_offset) over this function.
160
    #[must_use]
161
    pub fn render_value_chars(&self, itype: InputType) -> Vec<char> {
68✔
162
        // TODO: can we return a iterator or something to prevent this intermediary Vec?
34✔
163
        match itype {
68✔
164
            InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
2✔
165
                (0..self.input.len()).map(|_| ch).collect()
4✔
166
            }
167
            _ => self.input.clone(),
66✔
168
        }
169
    }
68✔
170

171
    /// Get the current input as a String in full, without any [`InputType`] modifications.
172
    #[must_use]
173
    pub fn get_value(&self) -> String {
70✔
174
        self.input.iter().collect()
70✔
175
    }
70✔
176
}
177

178
// -- Component
179

180
/// `Input` represents a read-write input field. This component supports different input types, input length
181
/// and handles input events related to cursor position, backspace, canc, ...
182
#[derive(Default)]
183
#[must_use]
184
pub struct Input {
185
    common: CommonProps,
186
    props: Props,
187
    pub states: InputStates,
188
}
189

190
impl Input {
191
    /// Set the main foreground color. This may get overwritten by individual text styles.
192
    pub fn foreground(mut self, fg: Color) -> Self {
2✔
193
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
2✔
194
        self
2✔
195
    }
2✔
196

197
    /// Set the main background color. This may get overwritten by individual text styles.
198
    pub fn background(mut self, bg: Color) -> Self {
2✔
199
        self.attr(Attribute::Background, AttrValue::Color(bg));
2✔
200
        self
2✔
201
    }
2✔
202

203
    /// Set the main text modifiers. This may get overwritten by individual text styles.
NEW
204
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
×
NEW
205
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
×
NEW
206
        self
×
NEW
207
    }
×
208

209
    /// Set the main style. This may get overwritten by individual text styles.
210
    ///
211
    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
NEW
212
    pub fn style(mut self, style: Style) -> Self {
×
NEW
213
        self.attr(Attribute::Style, AttrValue::Style(style));
×
NEW
214
        self
×
NEW
215
    }
×
216

217
    /// Set a custom style for the border when the component is unfocused.
218
    pub fn inactive(mut self, s: Style) -> Self {
2✔
219
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
2✔
220
        self
2✔
221
    }
2✔
222

223
    /// Add a border to the component.
224
    pub fn borders(mut self, b: Borders) -> Self {
2✔
225
        self.attr(Attribute::Borders, AttrValue::Borders(b));
2✔
226
        self
2✔
227
    }
2✔
228

229
    /// Add a title to the component.
230
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
2✔
231
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
2✔
232
        self
2✔
233
    }
2✔
234

235
    /// Set the type of input this Input Component is for. Specific types may have different display or validate methods.
236
    pub fn input_type(mut self, itype: InputType) -> Self {
2✔
237
        self.attr(Attribute::InputType, AttrValue::InputType(itype));
2✔
238
        self
2✔
239
    }
2✔
240

241
    /// Set the max length of the input.
242
    pub fn input_len(mut self, ilen: usize) -> Self {
2✔
243
        self.attr(Attribute::InputLength, AttrValue::Length(ilen));
2✔
244
        self
2✔
245
    }
2✔
246

247
    /// Set the inital value of the Input.
248
    pub fn value<S: Into<String>>(mut self, s: S) -> Self {
2✔
249
        self.attr(Attribute::Value, AttrValue::String(s.into()));
2✔
250
        self
2✔
251
    }
2✔
252

253
    /// Set a style for when the input fails validation.
254
    pub fn invalid_style(mut self, s: Style) -> Self {
×
255
        self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
×
256
        self
×
257
    }
×
258

259
    /// Set a placeholder text and stylew for when the Input is empty.
260
    pub fn placeholder<S: Into<String>>(mut self, placeholder: S, style: Style) -> Self {
×
261
        // TODO: Span / Line?
262
        self.attr(
×
263
            Attribute::Custom(INPUT_PLACEHOLDER),
×
264
            AttrValue::String(placeholder.into()),
×
265
        );
266
        self.attr(
×
267
            Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
268
            AttrValue::Style(style),
×
269
        );
270
        self
×
271
    }
×
272

273
    fn get_input_len(&self) -> Option<usize> {
18✔
274
        self.props
18✔
275
            .get(Attribute::InputLength)
18✔
276
            .map(|x| x.unwrap_length())
18✔
277
    }
18✔
278

279
    fn get_input_type(&self) -> InputType {
54✔
280
        self.props
54✔
281
            .get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
54✔
282
            .unwrap_input_type()
54✔
283
    }
54✔
284

285
    /// Checks whether current input is valid according to the set [`InputType`].
286
    fn is_valid(&self) -> bool {
36✔
287
        let value = self.states.get_value();
36✔
288
        self.get_input_type().validate(value.as_str())
36✔
289
    }
36✔
290
}
291

292
impl MockComponent for Input {
293
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
NEW
294
        if !self.common.display {
×
NEW
295
            return;
×
NEW
296
        }
×
297

NEW
298
        let mut normal_style = self.common.style;
×
299

NEW
300
        let itype = self.get_input_type();
×
NEW
301
        let mut block = self.common.get_block();
×
302
        // Apply invalid style
303
        // TODO: invalid style should likely still be applied even if unfocused
NEW
304
        if self.common.focused && !self.is_valid() {
×
NEW
305
            if let Some(invalid_style) = self
×
306
                .props
×
NEW
307
                .get(Attribute::Custom(INPUT_INVALID_STYLE))
×
NEW
308
                .map(|x| x.unwrap_style())
×
309
            {
NEW
310
                if let Some(block) = &mut block {
×
NEW
311
                    let border_style = self
×
NEW
312
                        .common
×
NEW
313
                        .border
×
NEW
314
                        .unwrap_or_default()
×
NEW
315
                        .style()
×
NEW
316
                        .patch(invalid_style);
×
NEW
317
                    // i dont like this, but ratatui does not offer a non-self taking method to change the style
×
NEW
318
                    *block = std::mem::take(block).border_style(border_style);
×
319
                }
×
320

NEW
321
                normal_style = normal_style.patch(invalid_style);
×
UNCOV
322
            }
×
NEW
323
        }
×
324

NEW
325
        let mut area_for_bounds = area;
×
326

NEW
327
        if let Some(block) = &block {
×
328
            // Create input's area
×
329
            let block_inner_area = block.inner(area);
×
UNCOV
330

×
331
            self.states.update_width(block_inner_area.width);
×
UNCOV
332

×
NEW
333
            area_for_bounds = block_inner_area;
×
NEW
334
        }
×
335

NEW
336
        let text_to_display = self.states.render_value_offset(self.get_input_type());
×
337

NEW
338
        let show_placeholder = text_to_display.is_empty();
×
339
        // Choose whether to show placeholder; if placeholder is unset, show nothing
NEW
340
        let text_to_display = if show_placeholder {
×
NEW
341
            self.states.cursor = 0;
×
NEW
342
            self.props
×
NEW
343
                .get_or(
×
NEW
344
                    Attribute::Custom(INPUT_PLACEHOLDER),
×
NEW
345
                    AttrValue::String(String::new()),
×
346
                )
NEW
347
                .unwrap_string()
×
348
        } else {
NEW
349
            text_to_display
×
350
        };
351
        // Choose paragraph style based on whether is valid or not and if has focus and if should show placeholder
NEW
352
        let paragraph_style = if self.common.focused {
×
NEW
353
            normal_style
×
354
        } else {
355
            // TODO: this should likely be a different property
NEW
356
            self.common.border_unfocused_style
×
357
        };
NEW
358
        let paragraph_style = if show_placeholder {
×
NEW
359
            self.props
×
NEW
360
                .get_or(
×
NEW
361
                    Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
NEW
362
                    AttrValue::Style(paragraph_style),
×
363
                )
NEW
364
                .unwrap_style()
×
365
        } else {
NEW
366
            paragraph_style
×
367
        };
368

NEW
369
        let mut widget = Paragraph::new(text_to_display).style(paragraph_style);
×
370

NEW
371
        if let Some(block) = block {
×
NEW
372
            widget = widget.block(block);
×
NEW
373
        }
×
374

NEW
375
        render.render_widget(widget, area);
×
376

377
        // Set cursor, if focus
NEW
378
        if self.common.focused && !area_for_bounds.is_empty() {
×
NEW
379
            let x: u16 = area_for_bounds.x
×
NEW
380
                + calc_utf8_cursor_position(
×
NEW
381
                    &self.states.render_value_chars(itype)[0..self.states.cursor],
×
NEW
382
                )
×
NEW
383
                .saturating_sub(u16::try_from(self.states.display_offset).unwrap_or(u16::MAX));
×
NEW
384
            let x = x.min(area_for_bounds.x + area_for_bounds.width);
×
NEW
385
            render.set_cursor_position(tuirealm::ratatui::prelude::Position {
×
NEW
386
                x,
×
NEW
387
                y: area_for_bounds.y,
×
NEW
388
            });
×
389
        }
×
390
    }
×
391

392
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
NEW
393
        if let Some(value) = self.common.get(attr) {
×
NEW
394
            return Some(value);
×
NEW
395
        }
×
396

397
        self.props.get(attr)
×
398
    }
×
399

400
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
22✔
401
        if let Some(value) = self.common.set(attr, value) {
22✔
402
            let sanitize_input = matches!(
12✔
403
                attr,
12✔
404
                Attribute::InputLength | Attribute::InputType | Attribute::Value
405
            );
406
            // Check if new input
407
            let new_input = match attr {
12✔
408
                Attribute::Value => Some(value.clone().unwrap_string()),
4✔
409
                _ => None,
8✔
410
            };
411
            self.props.set(attr, value);
12✔
412
            if sanitize_input {
12✔
413
                let input = match new_input {
12✔
414
                    None => self.states.input.clone(),
8✔
415
                    Some(v) => v.chars().collect(),
4✔
416
                };
417
                self.states.input = Vec::new();
12✔
418
                self.states.cursor = 0;
12✔
419
                let itype = self.get_input_type();
12✔
420
                let max_len = self.get_input_len();
12✔
421
                for ch in input {
60✔
422
                    self.states.append(ch, &itype, max_len);
54✔
423
                }
54✔
UNCOV
424
            }
×
425
        }
10✔
426
    }
22✔
427

428
    fn state(&self) -> State {
36✔
429
        // Validate input
18✔
430
        if self.is_valid() {
36✔
431
            State::Single(StateValue::String(self.states.get_value()))
34✔
432
        } else {
433
            State::None
2✔
434
        }
435
    }
36✔
436

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

493
#[cfg(test)]
494
mod tests {
495

496
    use super::*;
497

498
    use pretty_assertions::assert_eq;
499
    use tuirealm::props::HorizontalAlignment;
500

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

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

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

709
        let mut states = InputStates::default();
2✔
710

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

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

722
        states.update_width(10);
2✔
723

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

814
        // note any width below PREVIEW_STEP * 2 + 1 is undefined behavior
815
    }
2✔
816
}
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