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

veeso / tui-realm-stdlib / 22147789116

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

push

github

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

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

405 existing lines in 20 files now uncovered.

3944 of 5102 relevant lines covered (77.3%)

4.92 hits per line

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

77.35
/src/components/select.rs
1
//! ## Select
2
//!
3
//! `Select` represents a select field, like in HTML. The size for the component must be 3 (border + selected) + the quantity of rows
4
//! you want to display other options when opened (at least 3)
5

6
use tuirealm::command::{Cmd, CmdResult, Direction};
7
use tuirealm::props::{
8
    AttrValue, Attribute, Borders, Color, LineStatic, PropPayload, PropValue, Props, Style,
9
    TextModifiers, Title,
10
};
11
use tuirealm::ratatui::text::Line as Spans;
12
use tuirealm::ratatui::{
13
    layout::{Constraint, Direction as LayoutDirection, Layout, Rect},
14
    widgets::{List, ListItem, ListState, Paragraph},
15
};
16
use tuirealm::{Frame, MockComponent, State, StateValue};
17

18
use crate::prop_ext::CommonProps;
19
use crate::utils::borrow_clone_line;
20

21
// -- states
22

23
/// ## SelectStates
24
///
25
/// Component states
26
#[derive(Default)]
27
pub struct SelectStates {
28
    /// Available choices
29
    pub choices: Vec<String>,
30
    /// Currently selected choice
31
    pub selected: usize,
32
    /// Choice selected before opening the tab
33
    pub previously_selected: usize,
34
    pub tab_open: bool,
35
}
36

37
impl SelectStates {
38
    /// ### next_choice
39
    ///
40
    /// Move choice index to next choice
41
    pub fn next_choice(&mut self, rewind: bool) {
22✔
42
        if self.tab_open {
22✔
43
            if rewind && self.selected + 1 >= self.choices.len() {
18✔
44
                self.selected = 0;
2✔
45
            } else if self.selected + 1 < self.choices.len() {
16✔
46
                self.selected += 1;
12✔
47
            }
12✔
48
        }
4✔
49
    }
22✔
50

51
    /// ### prev_choice
52
    ///
53
    /// Move choice index to previous choice
54
    pub fn prev_choice(&mut self, rewind: bool) {
22✔
55
        if self.tab_open {
22✔
56
            if rewind && self.selected == 0 && !self.choices.is_empty() {
16✔
57
                self.selected = self.choices.len() - 1;
2✔
58
            } else if self.selected > 0 {
14✔
59
                self.selected -= 1;
12✔
60
            }
12✔
61
        }
6✔
62
    }
22✔
63

64
    /// ### set_choices
65
    ///
66
    /// Set SelectStates choices from a vector of str
67
    /// In addition resets current selection and keep index if possible or set it to the first value
68
    /// available
69
    pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
28✔
70
        self.choices = choices.into();
28✔
71
        // Keep index if possible
14✔
72
        if self.selected >= self.choices.len() {
28✔
73
            self.selected = match self.choices.len() {
4✔
74
                0 => 0,
2✔
75
                l => l - 1,
2✔
76
            };
77
        }
24✔
78
    }
28✔
79

80
    pub fn select(&mut self, i: usize) {
6✔
81
        if i < self.choices.len() {
6✔
82
            self.selected = i;
6✔
83
        }
6✔
84
    }
6✔
85

86
    /// ### close_tab
87
    ///
88
    /// Close tab
89
    pub fn close_tab(&mut self) {
12✔
90
        self.tab_open = false;
12✔
91
    }
12✔
92

93
    /// ### open_tab
94
    ///
95
    /// Open tab
96
    pub fn open_tab(&mut self) {
12✔
97
        self.previously_selected = self.selected;
12✔
98
        self.tab_open = true;
12✔
99
    }
12✔
100

101
    /// Cancel tab open
102
    pub fn cancel_tab(&mut self) {
2✔
103
        self.close_tab();
2✔
104
        self.selected = self.previously_selected;
2✔
105
    }
2✔
106

107
    /// ### is_tab_open
108
    ///
109
    /// Returns whether the tab is open
110
    #[must_use]
111
    pub fn is_tab_open(&self) -> bool {
46✔
112
        self.tab_open
46✔
113
    }
46✔
114
}
115

116
// -- component
117

118
#[derive(Default)]
119
#[must_use]
120
pub struct Select {
121
    common: CommonProps,
122
    props: Props,
123
    pub states: SelectStates,
124
}
125

126
impl Select {
127
    /// Set the main foreground color. This may get overwritten by individual text styles.
128
    pub fn foreground(mut self, fg: Color) -> Self {
2✔
129
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
2✔
130
        self
2✔
131
    }
2✔
132

133
    /// Set the main background color. This may get overwritten by individual text styles.
134
    pub fn background(mut self, bg: Color) -> Self {
2✔
135
        self.attr(Attribute::Background, AttrValue::Color(bg));
2✔
136
        self
2✔
137
    }
2✔
138

139
    /// Set the main style. This may get overwritten by individual text styles.
140
    ///
141
    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
UNCOV
142
    pub fn style(mut self, style: Style) -> Self {
×
UNCOV
143
        self.attr(Attribute::Style, AttrValue::Style(style));
×
UNCOV
144
        self
×
UNCOV
145
    }
×
146

147
    /// Set a custom style for the border when the component is unfocused.
UNCOV
148
    pub fn inactive(mut self, s: Style) -> Self {
×
UNCOV
149
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
UNCOV
150
        self
×
UNCOV
151
    }
×
152

153
    /// Add a border to the component.
154
    pub fn borders(mut self, b: Borders) -> Self {
2✔
155
        self.attr(Attribute::Borders, AttrValue::Borders(b));
2✔
156
        self
2✔
157
    }
2✔
158

159
    /// Add a title to the component.
160
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
2✔
161
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
2✔
162
        self
2✔
163
    }
2✔
164

165
    /// Set whether wraparound should be possible (down on the last choice wraps around to 0, and the other way around).
166
    pub fn rewind(mut self, r: bool) -> Self {
2✔
167
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
2✔
168
        self
2✔
169
    }
2✔
170

171
    /// Set the Symbol and Style for the indicator of the current line.
172
    pub fn highlighted_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
2✔
173
        self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
2✔
174
        self
2✔
175
    }
2✔
176

177
    /// Set a custom foreground color for the currently highlighted item.
178
    pub fn highlighted_color(mut self, c: Color) -> Self {
2✔
179
        // TODO: shouldnt this be a highlight style instead?
1✔
180
        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
2✔
181
        self
2✔
182
    }
2✔
183

184
    /// Set the choices that should be possible.
185
    pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
14✔
186
        self.attr(
14✔
187
            Attribute::Content,
14✔
188
            AttrValue::Payload(PropPayload::Vec(
7✔
189
                choices
14✔
190
                    .into_iter()
14✔
191
                    .map(|v| PropValue::Str(v.into()))
18✔
192
                    .collect(),
14✔
193
            )),
7✔
194
        );
7✔
195
        self
14✔
196
    }
14✔
197

198
    /// Set the initially selected choice.
199
    pub fn value(mut self, i: usize) -> Self {
2✔
200
        // Set state
1✔
201
        self.attr(
2✔
202
            Attribute::Value,
2✔
203
            AttrValue::Payload(PropPayload::Single(PropValue::Usize(i))),
2✔
204
        );
1✔
205
        self
2✔
206
    }
2✔
207

208
    /// ### render_open_tab
209
    ///
210
    /// Render component when tab is open
211
    fn render_open_tab(&mut self, render: &mut Frame, mut area: Rect) {
×
212
        // Make choices
213
        let choices: Vec<ListItem> = self
×
214
            .states
×
215
            .choices
×
UNCOV
216
            .iter()
×
217
            .map(|x| ListItem::new(Spans::from(x.as_str())))
×
218
            .collect();
×
219

UNCOV
220
        let hg = self
×
221
            .props
×
222
            .get_ref(Attribute::HighlightedColor)
×
223
            .and_then(AttrValue::as_color);
×
224

225
        if let Some(block) = self.common.get_block() {
×
226
            let inner = block.inner(area);
×
227
            render.render_widget(block, area);
×
228
            area = inner;
×
UNCOV
229
        }
×
230

231
        // Prepare layout
232
        let [para_area, list_area] = Layout::default()
×
233
            .direction(LayoutDirection::Vertical)
×
234
            .margin(0)
×
235
            .constraints([Constraint::Length(2), Constraint::Min(1)])
×
236
            .areas(area);
×
237
        // Render like "closed" tab in chunk 0
238
        let selected_text: String = match self.states.choices.get(self.states.selected) {
×
239
            None => String::default(),
×
240
            Some(s) => s.clone(),
×
241
        };
242

243
        let para = Paragraph::new(selected_text).style(self.common.style);
×
UNCOV
244
        render.render_widget(para, para_area);
×
245

246
        let hg_style = if let Some(color) = hg {
×
247
            Style::new().fg(color)
×
248
        } else {
UNCOV
249
            Style::new()
×
250
        }
UNCOV
251
        .add_modifier(TextModifiers::REVERSED);
×
252

253
        // Render the list of elements in chunks [1]
254
        // Make list
255
        let mut list = List::new(choices)
×
256
            .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
×
257
            .style(self.common.style)
×
258
            .highlight_style(hg_style);
×
259
        // Highlighted symbol
UNCOV
260
        let hg_str = self
×
UNCOV
261
            .props
×
262
            .get_ref(Attribute::HighlightedStr)
×
263
            .and_then(|x| x.as_textline());
×
264
        if let Some(hg_str) = hg_str {
×
265
            list = list.highlight_symbol(borrow_clone_line(hg_str));
×
266
        }
×
267
        let mut state: ListState = ListState::default();
×
268
        state.select(Some(self.states.selected));
×
269

270
        render.render_stateful_widget(list, list_area, &mut state);
×
271
    }
×
272

273
    /// ### render_closed_tab
274
    ///
275
    /// Render component when tab is closed
UNCOV
276
    fn render_closed_tab(&self, render: &mut Frame, area: Rect) {
×
277
        let selected_text: String = match self.states.choices.get(self.states.selected) {
×
278
            None => String::default(),
×
279
            Some(s) => s.clone(),
×
280
        };
281
        let mut widget = Paragraph::new(selected_text).style(self.common.style);
×
282

283
        if let Some(block) = self.common.get_block() {
×
284
            widget = widget.block(block);
×
285
        }
×
286

287
        render.render_widget(widget, area);
×
288
    }
×
289

290
    fn rewindable(&self) -> bool {
16✔
291
        self.props
16✔
292
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
16✔
293
            .unwrap_flag()
16✔
294
    }
16✔
295
}
296

297
impl MockComponent for Select {
298
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
299
        if !self.common.display {
×
300
            return;
×
301
        }
×
302

303
        if self.states.is_tab_open() {
×
304
            self.render_open_tab(render, area);
×
305
        } else {
×
UNCOV
306
            self.render_closed_tab(render, area);
×
307
        }
×
308
    }
×
309

UNCOV
310
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
311
        if let Some(value) = self.common.get(attr) {
×
312
            return Some(value);
×
313
        }
×
314

315
        self.props.get(attr)
×
UNCOV
316
    }
×
317

318
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
32✔
319
        if let Some(value) = self.common.set(attr, value) {
32✔
UNCOV
320
            match attr {
×
321
                Attribute::Content => {
7✔
322
                    // Reset choices
7✔
323
                    let choices: Vec<String> = value
14✔
324
                        .unwrap_payload()
14✔
325
                        .unwrap_vec()
14✔
326
                        .iter()
14✔
327
                        .map(|x| x.clone().unwrap_str())
18✔
328
                        .collect();
14✔
329
                    self.states.set_choices(choices);
14✔
330
                }
7✔
331
                Attribute::Value => {
4✔
332
                    self.states
4✔
333
                        .select(value.unwrap_payload().unwrap_single().unwrap_usize());
4✔
334
                }
4✔
335
                Attribute::Focus if self.states.is_tab_open() => {
×
336
                    if let AttrValue::Flag(false) = value {
×
337
                        self.states.cancel_tab();
×
UNCOV
338
                    }
×
UNCOV
339
                    self.props.set(attr, value);
×
340
                }
341
                attr => {
6✔
342
                    self.props.set(attr, value);
6✔
343
                }
6✔
344
            }
345
        }
8✔
346
    }
32✔
347

348
    fn state(&self) -> State {
6✔
349
        if self.states.is_tab_open() {
6✔
UNCOV
350
            State::None
×
351
        } else {
352
            State::Single(StateValue::Usize(self.states.selected))
6✔
353
        }
354
    }
6✔
355

356
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
22✔
357
        match cmd {
16✔
358
            Cmd::Move(Direction::Down) => {
359
                // Increment choice
360
                self.states.next_choice(self.rewindable());
8✔
361
                // Return CmdResult On Change or None if tab is closed
4✔
362
                if self.states.is_tab_open() {
8✔
363
                    CmdResult::Changed(State::Single(StateValue::Usize(self.states.selected)))
6✔
364
                } else {
365
                    CmdResult::None
2✔
366
                }
367
            }
368
            Cmd::Move(Direction::Up) => {
369
                // Increment choice
370
                self.states.prev_choice(self.rewindable());
8✔
371
                // Return CmdResult On Change or None if tab is closed
4✔
372
                if self.states.is_tab_open() {
8✔
373
                    CmdResult::Changed(State::Single(StateValue::Usize(self.states.selected)))
6✔
374
                } else {
375
                    CmdResult::None
2✔
376
                }
377
            }
378
            Cmd::Cancel => {
UNCOV
379
                self.states.cancel_tab();
×
UNCOV
380
                CmdResult::Changed(self.state())
×
381
            }
382
            Cmd::Submit => {
383
                // Open or close tab
384
                if self.states.is_tab_open() {
6✔
385
                    self.states.close_tab();
4✔
386
                    CmdResult::Submit(self.state())
4✔
387
                } else {
388
                    self.states.open_tab();
2✔
389
                    CmdResult::None
2✔
390
                }
391
            }
UNCOV
392
            _ => CmdResult::None,
×
393
        }
394
    }
22✔
395
}
396

397
#[cfg(test)]
398
mod test {
399

400
    use super::*;
401

402
    use pretty_assertions::assert_eq;
403

404
    use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
405

406
    #[test]
407
    fn test_components_select_states() {
2✔
408
        let mut states: SelectStates = SelectStates::default();
2✔
409
        assert_eq!(states.selected, 0);
2✔
410
        assert_eq!(states.choices.len(), 0);
2✔
411
        assert_eq!(states.tab_open, false);
2✔
412
        let choices: &[String] = &[
2✔
413
            "lemon".to_string(),
2✔
414
            "strawberry".to_string(),
2✔
415
            "vanilla".to_string(),
2✔
416
            "chocolate".to_string(),
2✔
417
        ];
2✔
418
        states.set_choices(choices);
2✔
419
        assert_eq!(states.selected, 0);
2✔
420
        assert_eq!(states.choices.len(), 4);
2✔
421
        // Move
422
        states.prev_choice(false);
2✔
423
        assert_eq!(states.selected, 0);
2✔
424
        states.next_choice(false);
2✔
425
        // Tab is closed!!!
1✔
426
        assert_eq!(states.selected, 0);
2✔
427
        states.open_tab();
2✔
428
        assert_eq!(states.is_tab_open(), true);
2✔
429
        // Now we can move
430
        states.next_choice(false);
2✔
431
        assert_eq!(states.selected, 1);
2✔
432
        states.next_choice(false);
2✔
433
        assert_eq!(states.selected, 2);
2✔
434
        // Forward overflow
435
        states.next_choice(false);
2✔
436
        states.next_choice(false);
2✔
437
        assert_eq!(states.selected, 3);
2✔
438
        states.prev_choice(false);
2✔
439
        assert_eq!(states.selected, 2);
2✔
440
        // Close tab
441
        states.close_tab();
2✔
442
        assert_eq!(states.is_tab_open(), false);
2✔
443
        states.prev_choice(false);
2✔
444
        assert_eq!(states.selected, 2);
2✔
445
        // Update
446
        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
2✔
447
        states.set_choices(choices);
2✔
448
        assert_eq!(states.selected, 1); // Move to first index available
2✔
449
        assert_eq!(states.choices.len(), 2);
2✔
450
        let choices = vec![];
2✔
451
        states.set_choices(choices);
2✔
452
        assert_eq!(states.selected, 0); // Move to first index available
2✔
453
        assert_eq!(states.choices.len(), 0);
2✔
454
        // Rewind
455
        let choices: &[String] = &[
2✔
456
            "lemon".to_string(),
2✔
457
            "strawberry".to_string(),
2✔
458
            "vanilla".to_string(),
2✔
459
            "chocolate".to_string(),
2✔
460
        ];
2✔
461
        states.set_choices(choices);
2✔
462
        states.open_tab();
2✔
463
        assert_eq!(states.selected, 0);
2✔
464
        states.prev_choice(true);
2✔
465
        assert_eq!(states.selected, 3);
2✔
466
        states.next_choice(true);
2✔
467
        assert_eq!(states.selected, 0);
2✔
468
        states.next_choice(true);
2✔
469
        assert_eq!(states.selected, 1);
2✔
470
        states.prev_choice(true);
2✔
471
        assert_eq!(states.selected, 0);
2✔
472
        // Cancel tab
473
        states.close_tab();
2✔
474
        states.select(2);
2✔
475
        states.open_tab();
2✔
476
        states.prev_choice(true);
2✔
477
        states.prev_choice(true);
2✔
478
        assert_eq!(states.selected, 0);
2✔
479
        states.cancel_tab();
2✔
480
        assert_eq!(states.selected, 2);
2✔
481
        assert_eq!(states.is_tab_open(), false);
2✔
482
    }
2✔
483

484
    #[test]
485
    fn test_components_select() {
2✔
486
        // Make component
1✔
487
        let mut component = Select::default()
2✔
488
            .foreground(Color::Red)
2✔
489
            .background(Color::Black)
2✔
490
            .borders(Borders::default())
2✔
491
            .highlighted_color(Color::Red)
2✔
492
            .highlighted_str(">>")
2✔
493
            .title(
2✔
494
                Title::from("C'est oui ou bien c'est non?").alignment(HorizontalAlignment::Center),
2✔
495
            )
1✔
496
            .choices(["Oui!", "Non", "Peut-ĂȘtre"])
2✔
497
            .value(1)
2✔
498
            .rewind(false);
2✔
499
        assert_eq!(component.states.is_tab_open(), false);
2✔
500
        component.states.open_tab();
2✔
501
        assert_eq!(component.states.is_tab_open(), true);
2✔
502
        component.states.close_tab();
2✔
503
        assert_eq!(component.states.is_tab_open(), false);
2✔
504
        // Update
505
        component.attr(
2✔
506
            Attribute::Value,
2✔
507
            AttrValue::Payload(PropPayload::Single(PropValue::Usize(2))),
2✔
508
        );
1✔
509
        // Get value
1✔
510
        assert_eq!(component.state(), State::Single(StateValue::Usize(2)));
2✔
511
        // Open tab
512
        component.states.open_tab();
2✔
513
        // Events
1✔
514
        // Move cursor
1✔
515
        assert_eq!(
2✔
516
            component.perform(Cmd::Move(Direction::Up)),
2✔
517
            CmdResult::Changed(State::Single(StateValue::Usize(1))),
1✔
518
        );
1✔
519
        assert_eq!(
2✔
520
            component.perform(Cmd::Move(Direction::Up)),
2✔
521
            CmdResult::Changed(State::Single(StateValue::Usize(0))),
1✔
522
        );
1✔
523
        // Upper boundary
524
        assert_eq!(
2✔
525
            component.perform(Cmd::Move(Direction::Up)),
2✔
526
            CmdResult::Changed(State::Single(StateValue::Usize(0))),
1✔
527
        );
1✔
528
        // Move down
529
        assert_eq!(
2✔
530
            component.perform(Cmd::Move(Direction::Down)),
2✔
531
            CmdResult::Changed(State::Single(StateValue::Usize(1))),
1✔
532
        );
1✔
533
        assert_eq!(
2✔
534
            component.perform(Cmd::Move(Direction::Down)),
2✔
535
            CmdResult::Changed(State::Single(StateValue::Usize(2))),
1✔
536
        );
1✔
537
        // Lower boundary
538
        assert_eq!(
2✔
539
            component.perform(Cmd::Move(Direction::Down)),
2✔
540
            CmdResult::Changed(State::Single(StateValue::Usize(2))),
1✔
541
        );
1✔
542
        // Press enter
543
        assert_eq!(
2✔
544
            component.perform(Cmd::Submit),
2✔
545
            CmdResult::Submit(State::Single(StateValue::Usize(2))),
1✔
546
        );
1✔
547
        // Tab should be closed
548
        assert_eq!(component.states.is_tab_open(), false);
2✔
549
        // Re open
550
        assert_eq!(component.perform(Cmd::Submit), CmdResult::None);
2✔
551
        assert_eq!(component.states.is_tab_open(), true);
2✔
552
        // Move arrows
553
        assert_eq!(
2✔
554
            component.perform(Cmd::Submit),
2✔
555
            CmdResult::Submit(State::Single(StateValue::Usize(2))),
1✔
556
        );
1✔
557
        assert_eq!(component.states.is_tab_open(), false);
2✔
558
        assert_eq!(
2✔
559
            component.perform(Cmd::Move(Direction::Down)),
2✔
560
            CmdResult::None
1✔
561
        );
1✔
562
        assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
2✔
563
    }
2✔
564

565
    #[test]
566
    fn various_set_choice_types() {
2✔
567
        // static array of strings
1✔
568
        SelectStates::default().set_choices(&["hello".to_string()]);
2✔
569
        // vector of strings
1✔
570
        SelectStates::default().set_choices(vec!["hello".to_string()]);
2✔
571
        // boxed array of strings
1✔
572
        SelectStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
2✔
573
    }
2✔
574

575
    #[test]
576
    fn various_choice_types() {
2✔
577
        // static array of static strings
1✔
578
        let _ = Select::default().choices(["hello"]);
2✔
579
        // static array of strings
1✔
580
        let _ = Select::default().choices(["hello".to_string()]);
2✔
581
        // vec of static strings
1✔
582
        let _ = Select::default().choices(vec!["hello"]);
2✔
583
        // vec of strings
1✔
584
        let _ = Select::default().choices(vec!["hello".to_string()]);
2✔
585
        // boxed array of static strings
1✔
586
        let _ = Select::default().choices(vec!["hello"].into_boxed_slice());
2✔
587
        // boxed array of strings
1✔
588
        let _ = Select::default().choices(vec!["hello".to_string()].into_boxed_slice());
2✔
589
    }
2✔
590
}
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