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

veeso / tui-realm-stdlib / 20435733121

22 Dec 2025 03:06PM UTC coverage: 70.048% (-0.1%) from 70.169%
20435733121

Pull #48

github

web-flow
Merge ad682c6d8 into 9528c3045
Pull Request #48: Apply changes for core `Title` changes

297 of 363 new or added lines in 17 files covered. (81.82%)

3 existing lines in 2 files now uncovered.

3204 of 4574 relevant lines covered (70.05%)

2.14 hits per line

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

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

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

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

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

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

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

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

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

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

122
            if self.cursor < self.display_offset.saturating_add(PREVIEW_DISTANCE) {
18✔
123
                self.display_offset = self.display_offset.saturating_sub(1);
4✔
124
            }
14✔
125
        }
2✔
126
    }
20✔
127

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

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

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

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

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

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

197
// -- Component
198

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

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

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

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

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

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

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

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

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

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

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

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

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

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

288
impl MockComponent for Input {
289
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
290
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
291
            let mut foreground = self
×
292
                .props
×
293
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
294
                .unwrap_color();
×
295
            let mut background = self
×
296
                .props
×
297
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
298
                .unwrap_color();
×
299
            let modifiers = self
×
300
                .props
×
301
                .get_or(
×
302
                    Attribute::TextProps,
×
303
                    AttrValue::TextModifiers(TextModifiers::empty()),
×
304
                )
305
                .unwrap_text_modifiers();
×
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) {
11✔
408
        let sanitize_input = matches!(
11✔
409
            attr,
11✔
410
            Attribute::InputLength | Attribute::InputType | Attribute::Value
411
        );
412
        // Check if new input
413
        let new_input = match attr {
11✔
414
            Attribute::Value => Some(value.clone().unwrap_string()),
2✔
415
            _ => None,
9✔
416
        };
417
        self.props.set(attr, value);
11✔
418
        if sanitize_input {
11✔
419
            let input = match new_input {
6✔
420
                None => self.states.input.clone(),
4✔
421
                Some(v) => v.chars().collect(),
2✔
422
            };
423
            self.states.input = Vec::new();
6✔
424
            self.states.cursor = 0;
6✔
425
            let itype = self.get_input_type();
6✔
426
            let max_len = self.get_input_len();
6✔
427
            for ch in input {
27✔
428
                self.states.append(ch, &itype, max_len);
27✔
429
            }
27✔
430
        }
5✔
431
    }
11✔
432

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

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

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

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

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

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

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

727
        states.update_width(10);
1✔
728

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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