• 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

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

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

16
use crate::prop_ext::CommonProps;
17
use crate::utils::borrow_clone_line;
18

19
// -- states
20

21
/// The states that need to be kept for the [`Select`] component.
22
#[derive(Default)]
23
pub struct SelectStates {
24
    /// Available choices
25
    pub choices: Vec<String>,
26
    /// Currently selected choice
27
    pub selected: usize,
28
    /// Choice selected before opening the tab
29
    pub previously_selected: usize,
30
    pub tab_open: bool,
31
}
32

33
impl SelectStates {
34
    /// Move choice index to next choice.
35
    pub fn next_choice(&mut self, rewind: bool) {
33✔
36
        if self.tab_open {
33✔
37
            if rewind && self.selected + 1 >= self.choices.len() {
27✔
38
                self.selected = 0;
3✔
39
            } else if self.selected + 1 < self.choices.len() {
24✔
40
                self.selected += 1;
18✔
41
            }
18✔
42
        }
6✔
43
    }
33✔
44

45
    /// Move choice index to previous choice.
46
    pub fn prev_choice(&mut self, rewind: bool) {
33✔
47
        if self.tab_open {
33✔
48
            if rewind && self.selected == 0 && !self.choices.is_empty() {
24✔
49
                self.selected = self.choices.len() - 1;
3✔
50
            } else if self.selected > 0 {
21✔
51
                self.selected -= 1;
18✔
52
            }
18✔
53
        }
9✔
54
    }
33✔
55

56
    /// Overwrite the choices available with new ones.
57
    ///
58
    /// In addition resets current selection and keep index if possible or set it to the first value
59
    /// available.
60
    pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
42✔
61
        self.choices = choices.into();
42✔
62
        // Keep index if possible
28✔
63
        if self.selected >= self.choices.len() {
42✔
64
            self.selected = match self.choices.len() {
6✔
65
                0 => 0,
3✔
66
                l => l - 1,
3✔
67
            };
68
        }
36✔
69
    }
42✔
70

71
    pub fn select(&mut self, i: usize) {
9✔
72
        if i < self.choices.len() {
9✔
73
            self.selected = i;
9✔
74
        }
9✔
75
    }
9✔
76

77
    /// Close tab.
78
    pub fn close_tab(&mut self) {
18✔
79
        self.tab_open = false;
18✔
80
    }
18✔
81

82
    /// Open tab.
83
    pub fn open_tab(&mut self) {
18✔
84
        self.previously_selected = self.selected;
18✔
85
        self.tab_open = true;
18✔
86
    }
18✔
87

88
    /// Cancel tab open.
89
    pub fn cancel_tab(&mut self) {
3✔
90
        self.close_tab();
3✔
91
        self.selected = self.previously_selected;
3✔
92
    }
3✔
93

94
    /// Returns whether the tab is open.
95
    #[must_use]
96
    pub fn is_tab_open(&self) -> bool {
69✔
97
        self.tab_open
69✔
98
    }
69✔
99
}
100

101
// -- component
102

103
/// `Select` represents a select field, like in HTML. The size for the component must be 3 (border + selected) + the quantity of rows
104
/// you want to display other options when opened (at least 3).
105
///
106
/// Similar to [`Radio`](crate::Radio), [`Select`] is a single-choice selector, but the difference is that it does not show the selector
107
/// unless the "Tab" is open, and only shows the currently selected choice.
108
#[derive(Default)]
109
#[must_use]
110
pub struct Select {
111
    common: CommonProps,
112
    props: Props,
113
    pub states: SelectStates,
114
}
115

116
impl Select {
117
    /// Set the main foreground color. This may get overwritten by individual text styles.
118
    pub fn foreground(mut self, fg: Color) -> Self {
3✔
119
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
3✔
120
        self
3✔
121
    }
3✔
122

123
    /// Set the main background color. This may get overwritten by individual text styles.
124
    pub fn background(mut self, bg: Color) -> Self {
3✔
125
        self.attr(Attribute::Background, AttrValue::Color(bg));
3✔
126
        self
3✔
127
    }
3✔
128

129
    /// Set the main text modifiers. This may get overwritten by individual text styles.
130
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
×
131
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
×
132
        self
×
133
    }
×
134

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

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

149
    /// Add a border to the component.
150
    pub fn borders(mut self, b: Borders) -> Self {
3✔
151
        self.attr(Attribute::Borders, AttrValue::Borders(b));
3✔
152
        self
3✔
153
    }
3✔
154

155
    /// Add a title to the component.
156
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
3✔
157
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
3✔
158
        self
3✔
159
    }
3✔
160

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

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

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

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

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

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

216
        let hg = self
×
217
            .props
×
218
            .get_ref(Attribute::HighlightedColor)
×
219
            .and_then(AttrValue::as_color);
×
220

221
        if let Some(block) = self.common.get_block() {
×
222
            let inner = block.inner(area);
×
223
            render.render_widget(block, area);
×
224
            area = inner;
×
225
        }
×
226

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

239
        let para = Paragraph::new(selected_text).style(self.common.style);
×
240
        render.render_widget(para, para_area);
×
241

242
        let hg_style = if let Some(color) = hg {
×
243
            Style::new().fg(color)
×
244
        } else {
245
            Style::new()
×
246
        }
247
        .add_modifier(TextModifiers::REVERSED);
×
248

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

266
        render.render_stateful_widget(list, list_area, &mut state);
×
267
    }
×
268

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

279
        if let Some(block) = self.common.get_block() {
×
280
            widget = widget.block(block);
×
281
        }
×
282

283
        render.render_widget(widget, area);
×
284
    }
×
285

286
    fn rewindable(&self) -> bool {
24✔
287
        self.props
24✔
288
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
24✔
289
            .unwrap_flag()
24✔
290
    }
24✔
291
}
292

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

299
        if self.states.is_tab_open() {
×
300
            self.render_open_tab(render, area);
×
301
        } else {
×
302
            self.render_closed_tab(render, area);
×
303
        }
×
304
    }
×
305

306
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
307
        if let Some(value) = self.common.get(attr) {
×
308
            return Some(value);
×
309
        }
×
310

311
        self.props.get(attr)
×
312
    }
×
313

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

344
    fn state(&self) -> State {
9✔
345
        if self.states.is_tab_open() {
9✔
346
            State::None
×
347
        } else {
348
            State::Single(StateValue::Usize(self.states.selected))
9✔
349
        }
350
    }
9✔
351

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

393
#[cfg(test)]
394
mod test {
395

396
    use super::*;
397

398
    use pretty_assertions::assert_eq;
399

400
    use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
401

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

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

561
    #[test]
562
    fn various_set_choice_types() {
3✔
563
        // static array of strings
2✔
564
        SelectStates::default().set_choices(&["hello".to_string()]);
3✔
565
        // vector of strings
2✔
566
        SelectStates::default().set_choices(vec!["hello".to_string()]);
3✔
567
        // boxed array of strings
2✔
568
        SelectStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
3✔
569
    }
3✔
570

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