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

veeso / tui-realm-stdlib / 20922869537

12 Jan 2026 02:26PM UTC coverage: 72.217% (+0.2%) from 72.047%
20922869537

Pull #48

github

web-flow
Merge 4adcb7d6b into b7d42e30d
Pull Request #48: Apply changes for core `Title` changes

64 of 92 new or added lines in 16 files covered. (69.57%)

13 existing lines in 1 file now uncovered.

3561 of 4931 relevant lines covered (72.22%)

4.14 hits per line

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

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

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

15
// -- states
16

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

197
// -- Component
198

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

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

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

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

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

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
    pub fn input_type(mut self, itype: InputType) -> Self {
2✔
236
        self.attr(Attribute::InputType, AttrValue::InputType(itype));
2✔
237
        self
2✔
238
    }
2✔
239

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

245
    pub fn value<S: Into<String>>(mut self, s: S) -> Self {
2✔
246
        self.attr(Attribute::Value, AttrValue::String(s.into()));
2✔
247
        self
2✔
248
    }
2✔
249

250
    pub fn invalid_style(mut self, s: Style) -> Self {
×
251
        self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
×
252
        self
×
253
    }
×
254

255
    pub fn placeholder<S: Into<String>>(mut self, placeholder: S, style: Style) -> Self {
×
256
        self.attr(
×
257
            Attribute::Custom(INPUT_PLACEHOLDER),
×
258
            AttrValue::String(placeholder.into()),
×
259
        );
260
        self.attr(
×
261
            Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
262
            AttrValue::Style(style),
×
263
        );
264
        self
×
265
    }
×
266

267
    fn get_input_len(&self) -> Option<usize> {
18✔
268
        self.props
18✔
269
            .get(Attribute::InputLength)
18✔
270
            .map(|x| x.unwrap_length())
18✔
271
    }
18✔
272

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

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

288
impl MockComponent for Input {
289
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
290
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
291
            let mut foreground = self
×
292
                .props
×
293
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
294
                .unwrap_color();
×
295
            let mut background = self
×
296
                .props
×
297
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
298
                .unwrap_color();
×
299
            let modifiers = self
×
300
                .props
×
301
                .get_or(
×
302
                    Attribute::TextProps,
×
303
                    AttrValue::TextModifiers(TextModifiers::empty()),
×
304
                )
305
                .unwrap_text_modifiers();
×
NEW
306
            let title = self
×
NEW
307
                .props
×
NEW
308
                .get_ref(Attribute::Title)
×
NEW
309
                .and_then(|v| v.as_title());
×
310
            let borders = self
×
311
                .props
×
312
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
313
                .unwrap_borders();
×
314
            let focus = self
×
315
                .props
×
316
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
317
                .unwrap_flag();
×
318
            let inactive_style = self
×
319
                .props
×
320
                .get(Attribute::FocusStyle)
×
321
                .map(|x| x.unwrap_style());
×
322
            let itype = self.get_input_type();
×
NEW
323
            let mut block = crate::utils::get_block(borders, title, focus, inactive_style);
×
324
            // Apply invalid style
325
            if focus && !self.is_valid() {
×
326
                if let Some(style) = self
×
327
                    .props
×
328
                    .get(Attribute::Custom(INPUT_INVALID_STYLE))
×
329
                    .map(|x| x.unwrap_style())
×
330
                {
×
331
                    let borders = self
×
332
                        .props
×
333
                        .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
334
                        .unwrap_borders()
×
335
                        .color(style.fg.unwrap_or(Color::Reset));
×
NEW
336
                    block = crate::utils::get_block(borders, title, focus, None);
×
337
                    foreground = style.fg.unwrap_or(Color::Reset);
×
338
                    background = style.bg.unwrap_or(Color::Reset);
×
339
                }
×
340
            }
×
341

342
            // Create input's area
343
            let block_inner_area = block.inner(area);
×
344

345
            self.states.update_width(block_inner_area.width);
×
346

347
            let text_to_display = self.states.render_value_offset(self.get_input_type());
×
348

349
            let show_placeholder = text_to_display.is_empty();
×
350
            // Choose whether to show placeholder; if placeholder is unset, show nothing
351
            let text_to_display = if show_placeholder {
×
352
                self.states.cursor = 0;
×
353
                self.props
×
354
                    .get_or(
×
355
                        Attribute::Custom(INPUT_PLACEHOLDER),
×
356
                        AttrValue::String(String::new()),
×
357
                    )
358
                    .unwrap_string()
×
359
            } else {
360
                text_to_display
×
361
            };
362
            // Choose paragraph style based on whether is valid or not and if has focus and if should show placeholder
363
            let paragraph_style = if focus {
×
364
                Style::default()
×
365
                    .fg(foreground)
×
366
                    .bg(background)
×
367
                    .add_modifier(modifiers)
×
368
            } else {
369
                inactive_style.unwrap_or_default()
×
370
            };
371
            let paragraph_style = if show_placeholder {
×
372
                self.props
×
373
                    .get_or(
×
374
                        Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
375
                        AttrValue::Style(paragraph_style),
×
376
                    )
377
                    .unwrap_style()
×
378
            } else {
379
                paragraph_style
×
380
            };
381

382
            let p: Paragraph = Paragraph::new(text_to_display)
×
383
                .style(paragraph_style)
×
384
                .block(block);
×
385
            render.render_widget(p, area);
×
386

387
            // Set cursor, if focus
388
            if focus && !block_inner_area.is_empty() {
×
389
                let x: u16 = block_inner_area.x
×
390
                    + calc_utf8_cursor_position(
×
391
                        &self.states.render_value_chars(itype)[0..self.states.cursor],
×
392
                    )
×
393
                    .saturating_sub(u16::try_from(self.states.display_offset).unwrap_or(u16::MAX));
×
394
                let x = x.min(block_inner_area.x + block_inner_area.width);
×
395
                render.set_cursor_position(tuirealm::ratatui::prelude::Position {
×
396
                    x,
×
397
                    y: block_inner_area.y,
×
398
                });
×
399
            }
×
400
        }
×
401
    }
×
402

403
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
404
        self.props.get(attr)
×
405
    }
×
406

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

433
    fn state(&self) -> State {
36✔
434
        // Validate input
18✔
435
        if self.is_valid() {
36✔
436
            State::One(StateValue::String(self.states.get_value()))
34✔
437
        } else {
438
            State::None
2✔
439
        }
440
    }
36✔
441

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

498
#[cfg(test)]
499
mod tests {
500

501
    use super::*;
502

503
    use pretty_assertions::assert_eq;
504
    use tuirealm::props::Alignment;
505

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

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

709
    #[test]
710
    fn should_keep_cursor_within_bounds() {
2✔
711
        let text = "The quick brown fox jumps over the lazy dog";
2✔
712
        assert!(text.len() > 15);
2✔
713

714
        let mut states = InputStates::default();
2✔
715

716
        for ch in text.chars() {
86✔
717
            states.append(ch, &InputType::Text, None);
86✔
718
        }
86✔
719

720
        // at first, without any "width" set, both functions should return the same
721
        assert_eq!(states.cursor, text.len());
2✔
722
        assert_eq!(
2✔
723
            states.render_value(InputType::Text),
2✔
724
            states.render_value_offset(InputType::Text)
2✔
725
        );
1✔
726

727
        states.update_width(10);
2✔
728

1✔
729
        assert_eq!(
2✔
730
            states.render_value_offset(InputType::Text),
2✔
731
            text[text.len() - 10..]
2✔
732
        );
1✔
733

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

742
        // preview step space at the end
743
        states.decr_cursor();
2✔
744
        assert_eq!(states.cursor, text.len() - 8);
2✔
745
        assert_eq!(
2✔
746
            states.render_value_offset(InputType::Text),
2✔
747
            text[text.len() - 10..]
2✔
748
        );
1✔
749

750
        states.decr_cursor();
2✔
751
        assert_eq!(states.cursor, text.len() - 9);
2✔
752
        assert_eq!(
2✔
753
            states.render_value_offset(InputType::Text),
2✔
754
            text[text.len() - 11..]
2✔
755
        );
1✔
756

757
        states.decr_cursor();
2✔
758
        assert_eq!(states.cursor, text.len() - 10);
2✔
759
        assert_eq!(
2✔
760
            states.render_value_offset(InputType::Text),
2✔
761
            text[text.len() - 12..]
2✔
762
        );
1✔
763

764
        states.cursor_at_begin();
2✔
765
        assert_eq!(states.cursor, 0);
2✔
766
        assert_eq!(states.render_value(InputType::Text), text);
2✔
767

768
        // the displayed text should not change until being in PREVIEW_STEP
769
        for i in 1..9 {
18✔
770
            states.incr_cursor();
16✔
771
            assert_eq!(states.cursor, i);
16✔
772
            let val = states.render_value_offset(InputType::Text);
16✔
773
            assert_eq!(val, text);
16✔
774
        }
775

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

780
        states.incr_cursor();
2✔
781
        assert_eq!(states.cursor, 10);
2✔
782
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
2✔
783

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

789
        // reset to 10, should also not change
790
        states.update_width(10);
2✔
791
        assert_eq!(states.cursor, 10);
2✔
792
        assert_eq!(states.render_value_offset(InputType::Text), text[2..]);
2✔
793

794
        // should change display_offset by 1
795
        states.update_width(9);
2✔
796
        assert_eq!(states.cursor, 10);
2✔
797
        assert_eq!(states.render_value_offset(InputType::Text), text[3..]);
2✔
798

799
        // reset to end
800
        states.update_width(10);
2✔
801
        states.cursor_at_end();
2✔
802

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

811
        assert_eq!(states.cursor, text.len() - 4);
2✔
812
        states.incr_cursor();
2✔
813
        assert_eq!(states.cursor, text.len() - 3);
2✔
814
        assert_eq!(
2✔
815
            states.render_value_offset(InputType::Text),
2✔
816
            text[text.len() - 8..]
2✔
817
        );
1✔
818

819
        // note any width below PREVIEW_STEP * 2 + 1 is undefined behavior
820
    }
2✔
821
}
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