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

veeso / tui-realm-stdlib / 22147846375

18 Feb 2026 01:42PM UTC coverage: 76.863% (-0.4%) from 77.303%
22147846375

push

github

hasezoey
style(prop_ext): add module documentation

3940 of 5126 relevant lines covered (76.86%)

7.45 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>) {
234✔
37
        // Check if max length has been reached
156✔
38
        if self.input.len() < max_len.unwrap_or(usize::MAX) {
234✔
39
            // Check whether can push
40
            if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
228✔
41
                self.input.insert(self.cursor, ch);
201✔
42
                self.incr_cursor();
201✔
43
            }
201✔
44
        }
6✔
45
    }
234✔
46

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

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

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

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

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

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

36✔
94
            if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
54✔
95
                self.display_offset = self.display_offset.saturating_sub(1);
12✔
96
            }
42✔
97
        }
6✔
98
    }
60✔
99

100
    /// Move the cursor to the beginning of the input.
101
    pub fn cursor_at_begin(&mut self) {
6✔
102
        self.cursor = 0;
6✔
103
        self.display_offset = 0;
6✔
104
    }
6✔
105

106
    /// Move the cursor the the end of the input.
107
    pub fn cursor_at_end(&mut self) {
9✔
108
        self.cursor = self.input.len();
9✔
109
        self.display_offset = self.input.len().saturating_sub(
9✔
110
            usize::from(self.last_width.unwrap_or_default()).saturating_sub(PREVIEW_DISTANCE),
9✔
111
        );
9✔
112
    }
9✔
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) {
15✔
121
        let old_width = self.last_width;
15✔
122
        self.last_width = Some(new_width);
15✔
123

10✔
124
        // if the cursor would now be out-of-bounds, adjust the display offset to keep the cursor within bounds
10✔
125
        if self.cursor
15✔
126
            > (self.display_offset + usize::from(new_width)).saturating_sub(PREVIEW_DISTANCE)
15✔
127
        {
128
            let diff = if let Some(old_width) = old_width {
6✔
129
                usize::from(old_width.saturating_sub(new_width))
3✔
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))
3✔
135
            };
136
            self.display_offset += diff;
6✔
137
        }
9✔
138
    }
15✔
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 {
12✔
143
        self.render_value_chars(itype).iter().collect::<String>()
12✔
144
    }
12✔
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 {
90✔
151
        self.render_value_chars(itype)
90✔
152
            .iter()
90✔
153
            .skip(self.display_offset)
90✔
154
            .collect()
90✔
155
    }
90✔
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> {
102✔
162
        // TODO: can we return a iterator or something to prevent this intermediary Vec?
68✔
163
        match itype {
102✔
164
            InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
3✔
165
                (0..self.input.len()).map(|_| ch).collect()
7✔
166
            }
167
            _ => self.input.clone(),
99✔
168
        }
169
    }
102✔
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 {
105✔
174
        self.input.iter().collect()
105✔
175
    }
105✔
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 {
3✔
193
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
3✔
194
        self
3✔
195
    }
3✔
196

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

203
    /// Set the main text modifiers. This may get overwritten by individual text styles.
204
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
×
205
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
×
206
        self
×
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)!
212
    pub fn style(mut self, style: Style) -> Self {
×
213
        self.attr(Attribute::Style, AttrValue::Style(style));
×
214
        self
×
215
    }
×
216

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

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

229
    /// Add a title to the component.
230
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
3✔
231
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
3✔
232
        self
3✔
233
    }
3✔
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 {
3✔
237
        self.attr(Attribute::InputType, AttrValue::InputType(itype));
3✔
238
        self
3✔
239
    }
3✔
240

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

247
    /// Set the inital value of the Input.
248
    pub fn value<S: Into<String>>(mut self, s: S) -> Self {
3✔
249
        self.attr(Attribute::Value, AttrValue::String(s.into()));
3✔
250
        self
3✔
251
    }
3✔
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> {
27✔
274
        self.props
27✔
275
            .get(Attribute::InputLength)
27✔
276
            .map(|x| x.unwrap_length())
27✔
277
    }
27✔
278

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

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

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

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

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

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

325
        let mut area_for_bounds = area;
×
326

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

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

×
333
            area_for_bounds = block_inner_area;
×
334
        }
×
335

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

338
        let show_placeholder = text_to_display.is_empty();
×
339
        // Choose whether to show placeholder; if placeholder is unset, show nothing
340
        let text_to_display = if show_placeholder {
×
341
            self.states.cursor = 0;
×
342
            self.props
×
343
                .get_or(
×
344
                    Attribute::Custom(INPUT_PLACEHOLDER),
×
345
                    AttrValue::String(String::new()),
×
346
                )
347
                .unwrap_string()
×
348
        } else {
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
352
        let paragraph_style = if self.common.focused {
×
353
            normal_style
×
354
        } else {
355
            // TODO: this should likely be a different property
356
            self.common.border_unfocused_style
×
357
        };
358
        let paragraph_style = if show_placeholder {
×
359
            self.props
×
360
                .get_or(
×
361
                    Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
362
                    AttrValue::Style(paragraph_style),
×
363
                )
364
                .unwrap_style()
×
365
        } else {
366
            paragraph_style
×
367
        };
368

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

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

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

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

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

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

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

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

437
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
51✔
438
        match cmd {
12✔
439
            Cmd::Delete => {
440
                // Backspace and None
441
                let prev_input = self.states.input.clone();
9✔
442
                self.states.backspace();
9✔
443
                if prev_input == self.states.input {
9✔
444
                    CmdResult::None
3✔
445
                } else {
446
                    CmdResult::Changed(self.state())
6✔
447
                }
448
            }
449
            Cmd::Cancel => {
450
                // Delete and None
451
                let prev_input = self.states.input.clone();
9✔
452
                self.states.delete();
9✔
453
                if prev_input == self.states.input {
9✔
454
                    CmdResult::None
6✔
455
                } else {
456
                    CmdResult::Changed(self.state())
3✔
457
                }
458
            }
459
            Cmd::Submit => CmdResult::Submit(self.state()),
3✔
460
            Cmd::Move(Direction::Left) => {
461
                self.states.decr_cursor();
9✔
462
                CmdResult::None
9✔
463
            }
464
            Cmd::Move(Direction::Right) => {
465
                self.states.incr_cursor();
3✔
466
                CmdResult::None
3✔
467
            }
468
            Cmd::GoTo(Position::Begin) => {
469
                self.states.cursor_at_begin();
3✔
470
                CmdResult::None
3✔
471
            }
472
            Cmd::GoTo(Position::End) => {
473
                self.states.cursor_at_end();
6✔
474
                CmdResult::None
6✔
475
            }
476
            Cmd::Type(ch) => {
9✔
477
                // Push char to input
6✔
478
                let prev_input = self.states.input.clone();
9✔
479
                self.states
9✔
480
                    .append(ch, &self.get_input_type(), self.get_input_len());
9✔
481
                // Message on change
6✔
482
                if prev_input == self.states.input {
9✔
483
                    CmdResult::None
3✔
484
                } else {
485
                    CmdResult::Changed(self.state())
6✔
486
                }
487
            }
488
            _ => CmdResult::None,
×
489
        }
490
    }
51✔
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() {
3✔
503
        let mut states: InputStates = InputStates::default();
3✔
504
        states.append('a', &InputType::Text, Some(3));
3✔
505
        assert_eq!(states.input, vec!['a']);
3✔
506
        states.append('b', &InputType::Text, Some(3));
3✔
507
        assert_eq!(states.input, vec!['a', 'b']);
3✔
508
        states.append('c', &InputType::Text, Some(3));
3✔
509
        assert_eq!(states.input, vec!['a', 'b', 'c']);
3✔
510
        // Reached length
511
        states.append('d', &InputType::Text, Some(3));
3✔
512
        assert_eq!(states.input, vec!['a', 'b', 'c']);
3✔
513
        // Push char to numbers
514
        states.append('d', &InputType::Number, None);
3✔
515
        assert_eq!(states.input, vec!['a', 'b', 'c']);
3✔
516
        // move cursor
517
        // decr cursor
518
        states.decr_cursor();
3✔
519
        assert_eq!(states.cursor, 2);
3✔
520
        states.cursor = 1;
3✔
521
        states.decr_cursor();
3✔
522
        assert_eq!(states.cursor, 0);
3✔
523
        states.decr_cursor();
3✔
524
        assert_eq!(states.cursor, 0);
3✔
525
        // Incr
526
        states.incr_cursor();
3✔
527
        assert_eq!(states.cursor, 1);
3✔
528
        states.incr_cursor();
3✔
529
        assert_eq!(states.cursor, 2);
3✔
530
        states.incr_cursor();
3✔
531
        assert_eq!(states.cursor, 3);
3✔
532
        // Render value
533
        assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
3✔
534
        assert_eq!(
3✔
535
            states.render_value(InputType::Password('*')).as_str(),
3✔
536
            "***"
2✔
537
        );
2✔
538
    }
3✔
539

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

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

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

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

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

722
        states.update_width(10);
3✔
723

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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