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

veeso / tui-realm-stdlib / 11316405772

13 Oct 2024 05:35PM UTC coverage: 73.26% (-0.2%) from 73.477%
11316405772

push

github

veeso
feat: tuirealm 2.x

0 of 7 new or added lines in 5 files covered. (0.0%)

16 existing lines in 6 files now uncovered.

2726 of 3721 relevant lines covered (73.26%)

1.98 hits per line

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

68.01
/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
    Alignment, AttrValue, Attribute, BorderSides, Borders, Color, PropPayload, PropValue, Props,
9
    Style, TextModifiers,
10
};
11
use tuirealm::ratatui::text::Line as Spans;
12
use tuirealm::ratatui::{
13
    layout::{Constraint, Direction as LayoutDirection, Layout, Rect},
14
    widgets::{Block, List, ListItem, ListState, Paragraph},
15
};
16
use tuirealm::{Frame, MockComponent, State, StateValue};
17

18
// -- states
19

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

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

48
    /// ### prev_choice
49
    ///
50
    /// Move choice index to previous choice
51
    pub fn prev_choice(&mut self, rewind: bool) {
11✔
52
        if self.tab_open {
11✔
53
            if rewind && self.selected == 0 && !self.choices.is_empty() {
8✔
54
                self.selected = self.choices.len() - 1;
1✔
55
            } else if self.selected > 0 {
7✔
56
                self.selected -= 1;
6✔
57
            }
58
        }
59
    }
11✔
60

61
    /// ### set_choices
62
    ///
63
    /// Set SelectStates choices from a vector of str
64
    /// In addition resets current selection and keep index if possible or set it to the first value
65
    /// available
66
    pub fn set_choices(&mut self, choices: &[String]) {
5✔
67
        self.choices = choices.to_vec();
5✔
68
        // Keep index if possible
69
        if self.selected >= self.choices.len() {
7✔
70
            self.selected = match self.choices.len() {
4✔
71
                0 => 0,
1✔
72
                l => l - 1,
1✔
73
            };
74
        }
75
    }
5✔
76

77
    pub fn select(&mut self, i: usize) {
3✔
78
        if i < self.choices.len() {
3✔
79
            self.selected = i;
3✔
80
        }
81
    }
3✔
82

83
    /// ### close_tab
84
    ///
85
    /// Close tab
86
    pub fn close_tab(&mut self) {
6✔
87
        self.tab_open = false;
6✔
88
    }
6✔
89

90
    /// ### open_tab
91
    ///
92
    /// Open tab
93
    pub fn open_tab(&mut self) {
6✔
94
        self.previously_selected = self.selected;
6✔
95
        self.tab_open = true;
6✔
96
    }
6✔
97

98
    /// Cancel tab open
99
    pub fn cancel_tab(&mut self) {
1✔
100
        self.close_tab();
1✔
101
        self.selected = self.previously_selected;
1✔
102
    }
1✔
103

104
    /// ### is_tab_open
105
    ///
106
    /// Returns whether the tab is open
107
    pub fn is_tab_open(&self) -> bool {
23✔
108
        self.tab_open
23✔
109
    }
23✔
110
}
111

112
// -- component
113

114
#[derive(Default)]
2✔
115
pub struct Select {
116
    props: Props,
1✔
117
    pub states: SelectStates,
1✔
118
    hg_str: Option<String>, // CRAP CRAP CRAP
1✔
119
}
120

121
impl Select {
122
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
123
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
124
        self
1✔
125
    }
1✔
126

127
    pub fn background(mut self, bg: Color) -> Self {
1✔
128
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
129
        self
1✔
130
    }
1✔
131

132
    pub fn borders(mut self, b: Borders) -> Self {
1✔
133
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
134
        self
1✔
135
    }
1✔
136

137
    pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
1✔
138
        self.attr(
1✔
139
            Attribute::Title,
1✔
140
            AttrValue::Title((t.as_ref().to_string(), a)),
1✔
141
        );
142
        self
1✔
143
    }
1✔
144

145
    pub fn highlighted_str<S: AsRef<str>>(mut self, s: S) -> Self {
1✔
146
        self.attr(
1✔
147
            Attribute::HighlightedStr,
1✔
148
            AttrValue::String(s.as_ref().to_string()),
1✔
149
        );
150
        self
1✔
151
    }
1✔
152

153
    pub fn highlighted_color(mut self, c: Color) -> Self {
1✔
154
        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
1✔
155
        self
1✔
156
    }
1✔
157

158
    pub fn inactive(mut self, s: Style) -> Self {
×
159
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
160
        self
×
161
    }
×
162

163
    pub fn rewind(mut self, r: bool) -> Self {
1✔
164
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
1✔
165
        self
1✔
166
    }
1✔
167

168
    pub fn choices<S: AsRef<str>>(mut self, choices: &[S]) -> Self {
1✔
169
        self.attr(
1✔
170
            Attribute::Content,
1✔
171
            AttrValue::Payload(PropPayload::Vec(
1✔
172
                choices
1✔
173
                    .iter()
174
                    .map(|x| PropValue::Str(x.as_ref().to_string()))
3✔
175
                    .collect(),
176
            )),
177
        );
178
        self
1✔
179
    }
1✔
180

181
    pub fn value(mut self, i: usize) -> Self {
1✔
182
        // Set state
183
        self.attr(
1✔
184
            Attribute::Value,
1✔
185
            AttrValue::Payload(PropPayload::One(PropValue::Usize(i))),
1✔
186
        );
187
        self
1✔
188
    }
1✔
189

190
    /// ### render_open_tab
191
    ///
192
    /// Render component when tab is open
193
    fn render_open_tab(&mut self, render: &mut Frame, area: Rect) {
×
194
        // Make choices
195
        let choices: Vec<ListItem> = self
×
196
            .states
197
            .choices
198
            .iter()
199
            .map(|x| ListItem::new(Spans::from(x.clone())))
×
200
            .collect();
201
        let foreground = self
×
202
            .props
203
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
204
            .unwrap_color();
205
        let background = self
×
206
            .props
207
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
208
            .unwrap_color();
209
        let hg: Color = self
×
210
            .props
211
            .get_or(Attribute::HighlightedColor, AttrValue::Color(foreground))
×
212
            .unwrap_color();
213
        // Prepare layout
214
        let chunks = Layout::default()
×
215
            .direction(LayoutDirection::Vertical)
×
216
            .margin(0)
217
            .constraints([Constraint::Length(2), Constraint::Min(1)].as_ref())
×
218
            .split(area);
×
219
        // Render like "closed" tab in chunk 0
220
        let selected_text: String = match self.states.choices.get(self.states.selected) {
×
221
            None => String::default(),
×
222
            Some(s) => s.clone(),
×
223
        };
224
        let borders = self
×
225
            .props
226
            .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
227
            .unwrap_borders();
228
        let block: Block = Block::default()
×
229
            .borders(BorderSides::LEFT | BorderSides::TOP | BorderSides::RIGHT)
×
230
            .border_style(borders.style())
×
231
            .border_type(borders.modifiers)
×
232
            .style(Style::default().bg(background));
×
233
        let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
×
234
        let block = match title {
×
235
            Some((text, alignment)) => block.title(text).title_alignment(alignment),
×
236
            None => block,
×
237
        };
238
        let focus = self
×
239
            .props
240
            .get_or(Attribute::Focus, AttrValue::Flag(false))
×
241
            .unwrap_flag();
242
        let inactive_style = self
×
243
            .props
244
            .get(Attribute::FocusStyle)
×
245
            .map(|x| x.unwrap_style());
×
246
        let p: Paragraph = Paragraph::new(selected_text)
×
247
            .style(match focus {
×
248
                true => borders.style(),
×
249
                false => inactive_style.unwrap_or_default(),
×
250
            })
251
            .block(block);
×
252
        render.render_widget(p, chunks[0]);
×
253
        // Render the list of elements in chunks [1]
254
        // Make list
UNCOV
255
        let mut list = List::new(choices)
×
256
            .block(
UNCOV
257
                Block::default()
×
UNCOV
258
                    .borders(BorderSides::LEFT | BorderSides::BOTTOM | BorderSides::RIGHT)
×
UNCOV
259
                    .border_style(match focus {
×
UNCOV
260
                        true => borders.style(),
×
UNCOV
261
                        false => Style::default(),
×
262
                    })
UNCOV
263
                    .border_type(borders.modifiers)
×
UNCOV
264
                    .style(Style::default().bg(background)),
×
265
            )
NEW
266
            .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
×
UNCOV
267
            .style(Style::default().fg(foreground).bg(background))
×
268
            .highlight_style(
269
                Style::default()
×
270
                    .fg(hg)
271
                    .add_modifier(TextModifiers::REVERSED),
272
            );
273
        // Highlighted symbol
274
        self.hg_str = self
×
275
            .props
276
            .get(Attribute::HighlightedStr)
×
277
            .map(|x| x.unwrap_string());
×
278
        if let Some(hg_str) = &self.hg_str {
×
279
            list = list.highlight_symbol(hg_str);
×
280
        }
281
        let mut state: ListState = ListState::default();
×
282
        state.select(Some(self.states.selected));
×
283
        render.render_stateful_widget(list, chunks[1], &mut state);
×
284
    }
×
285

286
    /// ### render_closed_tab
287
    ///
288
    /// Render component when tab is closed
289
    fn render_closed_tab(&self, render: &mut Frame, area: Rect) {
×
290
        let foreground = self
×
291
            .props
292
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
293
            .unwrap_color();
294
        let background = self
×
295
            .props
296
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
297
            .unwrap_color();
298
        let inactive_style = self
×
299
            .props
300
            .get(Attribute::FocusStyle)
×
301
            .map(|x| x.unwrap_style());
×
302
        let focus = self
×
303
            .props
304
            .get_or(Attribute::Focus, AttrValue::Flag(false))
×
305
            .unwrap_flag();
306
        let style = match focus {
×
307
            true => Style::default().bg(background).fg(foreground),
×
308
            false => inactive_style.unwrap_or_default(),
×
309
        };
310
        let borders = self
×
311
            .props
312
            .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
313
            .unwrap_borders();
314
        let borders_style = match focus {
×
315
            true => borders.style(),
×
316
            false => inactive_style.unwrap_or_default(),
×
317
        };
318
        let block: Block = Block::default()
×
319
            .borders(BorderSides::ALL)
320
            .border_style(borders_style)
×
321
            .border_type(borders.modifiers)
×
322
            .style(style);
×
323
        let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
×
324
        let block = match title {
×
325
            Some((text, alignment)) => block.title(text).title_alignment(alignment),
×
326
            None => block,
×
327
        };
328
        let selected_text: String = match self.states.choices.get(self.states.selected) {
×
329
            None => String::default(),
×
330
            Some(s) => s.clone(),
×
331
        };
332
        let p: Paragraph = Paragraph::new(selected_text).style(style).block(block);
×
333
        render.render_widget(p, area);
×
334
    }
×
335

336
    fn rewindable(&self) -> bool {
8✔
337
        self.props
16✔
338
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
8✔
339
            .unwrap_flag()
340
    }
8✔
341
}
342

343
impl MockComponent for Select {
344
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
345
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
346
            match self.states.is_tab_open() {
×
347
                true => self.render_open_tab(render, area),
×
348
                false => self.render_closed_tab(render, area),
×
349
            }
350
        }
351
    }
×
352

353
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
354
        self.props.get(attr)
×
355
    }
×
356

357
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
10✔
358
        match attr {
10✔
359
            Attribute::Content => {
360
                // Reset choices
361
                let choices: Vec<String> = value
1✔
362
                    .unwrap_payload()
363
                    .unwrap_vec()
364
                    .iter()
365
                    .map(|x| x.clone().unwrap_str())
3✔
366
                    .collect();
1✔
367
                self.states.set_choices(&choices);
1✔
368
            }
1✔
369
            Attribute::Value => {
370
                self.states
2✔
371
                    .select(value.unwrap_payload().unwrap_one().unwrap_usize());
2✔
372
            }
373
            Attribute::Focus if self.states.is_tab_open() => {
×
374
                if let AttrValue::Flag(false) = value {
×
375
                    self.states.cancel_tab();
×
376
                }
377
                self.props.set(attr, value);
×
378
            }
379
            attr => {
380
                self.props.set(attr, value);
7✔
381
            }
382
        }
383
    }
10✔
384

385
    fn state(&self) -> State {
3✔
386
        if self.states.is_tab_open() {
3✔
387
            State::None
×
388
        } else {
389
            State::One(StateValue::Usize(self.states.selected))
3✔
390
        }
391
    }
3✔
392

393
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
11✔
394
        match cmd {
11✔
395
            Cmd::Move(Direction::Down) => {
396
                // Increment choice
397
                self.states.next_choice(self.rewindable());
4✔
398
                // Return CmdResult On Change or None if tab is closed
399
                match self.states.is_tab_open() {
4✔
400
                    false => CmdResult::None,
1✔
401
                    true => CmdResult::Changed(State::One(StateValue::Usize(self.states.selected))),
3✔
402
                }
403
            }
404
            Cmd::Move(Direction::Up) => {
405
                // Increment choice
406
                self.states.prev_choice(self.rewindable());
4✔
407
                // Return CmdResult On Change or None if tab is closed
408
                match self.states.is_tab_open() {
4✔
409
                    false => CmdResult::None,
1✔
410
                    true => CmdResult::Changed(State::One(StateValue::Usize(self.states.selected))),
3✔
411
                }
412
            }
413
            Cmd::Cancel => {
414
                self.states.cancel_tab();
×
415
                CmdResult::Changed(self.state())
×
416
            }
417
            Cmd::Submit => {
418
                // Open or close tab
419
                if self.states.is_tab_open() {
3✔
420
                    self.states.close_tab();
2✔
421
                    CmdResult::Submit(self.state())
2✔
422
                } else {
423
                    self.states.open_tab();
1✔
424
                    CmdResult::None
1✔
425
                }
426
            }
427
            _ => CmdResult::None,
×
428
        }
429
    }
11✔
430
}
431

432
#[cfg(test)]
433
mod test {
434

435
    use super::*;
436

437
    use pretty_assertions::assert_eq;
438

439
    use tuirealm::props::{PropPayload, PropValue};
440

441
    #[test]
442
    fn test_components_select_states() {
2✔
443
        let mut states: SelectStates = SelectStates::default();
1✔
444
        assert_eq!(states.selected, 0);
1✔
445
        assert_eq!(states.choices.len(), 0);
1✔
446
        assert_eq!(states.tab_open, false);
1✔
447
        let choices: &[String] = &[
1✔
448
            "lemon".to_string(),
1✔
449
            "strawberry".to_string(),
1✔
450
            "vanilla".to_string(),
1✔
451
            "chocolate".to_string(),
1✔
452
        ];
453
        states.set_choices(&choices);
1✔
454
        assert_eq!(states.selected, 0);
1✔
455
        assert_eq!(states.choices.len(), 4);
1✔
456
        // Move
457
        states.prev_choice(false);
1✔
458
        assert_eq!(states.selected, 0);
1✔
459
        states.next_choice(false);
1✔
460
        // Tab is closed!!!
461
        assert_eq!(states.selected, 0);
1✔
462
        states.open_tab();
1✔
463
        assert_eq!(states.is_tab_open(), true);
1✔
464
        // Now we can move
465
        states.next_choice(false);
1✔
466
        assert_eq!(states.selected, 1);
1✔
467
        states.next_choice(false);
1✔
468
        assert_eq!(states.selected, 2);
1✔
469
        // Forward overflow
470
        states.next_choice(false);
1✔
471
        states.next_choice(false);
1✔
472
        assert_eq!(states.selected, 3);
1✔
473
        states.prev_choice(false);
1✔
474
        assert_eq!(states.selected, 2);
1✔
475
        // Close tab
476
        states.close_tab();
1✔
477
        assert_eq!(states.is_tab_open(), false);
1✔
478
        states.prev_choice(false);
1✔
479
        assert_eq!(states.selected, 2);
1✔
480
        // Update
481
        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
1✔
482
        states.set_choices(&choices);
1✔
483
        assert_eq!(states.selected, 1); // Move to first index available
1✔
484
        assert_eq!(states.choices.len(), 2);
1✔
485
        let choices = vec![];
1✔
486
        states.set_choices(&choices);
1✔
487
        assert_eq!(states.selected, 0); // Move to first index available
1✔
488
        assert_eq!(states.choices.len(), 0);
1✔
489
        // Rewind
490
        let choices: &[String] = &[
1✔
491
            "lemon".to_string(),
1✔
492
            "strawberry".to_string(),
1✔
493
            "vanilla".to_string(),
1✔
494
            "chocolate".to_string(),
1✔
495
        ];
496
        states.set_choices(choices);
1✔
497
        states.open_tab();
1✔
498
        assert_eq!(states.selected, 0);
1✔
499
        states.prev_choice(true);
1✔
500
        assert_eq!(states.selected, 3);
1✔
501
        states.next_choice(true);
1✔
502
        assert_eq!(states.selected, 0);
1✔
503
        states.next_choice(true);
1✔
504
        assert_eq!(states.selected, 1);
1✔
505
        states.prev_choice(true);
1✔
506
        assert_eq!(states.selected, 0);
1✔
507
        // Cancel tab
508
        states.close_tab();
1✔
509
        states.select(2);
1✔
510
        states.open_tab();
1✔
511
        states.prev_choice(true);
1✔
512
        states.prev_choice(true);
1✔
513
        assert_eq!(states.selected, 0);
1✔
514
        states.cancel_tab();
1✔
515
        assert_eq!(states.selected, 2);
1✔
516
        assert_eq!(states.is_tab_open(), false);
1✔
517
    }
2✔
518

519
    #[test]
520
    fn test_components_select() {
2✔
521
        // Make component
522
        let mut component = Select::default()
6✔
523
            .foreground(Color::Red)
1✔
524
            .background(Color::Black)
1✔
525
            .borders(Borders::default())
1✔
526
            .highlighted_color(Color::Red)
1✔
527
            .highlighted_str(">>")
528
            .title("C'est oui ou bien c'est non?", Alignment::Center)
1✔
529
            .choices(&["Oui!", "Non", "Peut-ĂȘtre"])
530
            .value(1)
531
            .rewind(false);
532
        assert_eq!(component.states.is_tab_open(), false);
1✔
533
        component.states.open_tab();
1✔
534
        assert_eq!(component.states.is_tab_open(), true);
1✔
535
        component.states.close_tab();
1✔
536
        assert_eq!(component.states.is_tab_open(), false);
1✔
537
        // Update
538
        component.attr(
1✔
539
            Attribute::Value,
1✔
540
            AttrValue::Payload(PropPayload::One(PropValue::Usize(2))),
1✔
541
        );
542
        // Get value
543
        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
1✔
544
        // Open tab
545
        component.states.open_tab();
1✔
546
        // Events
547
        // Move cursor
548
        assert_eq!(
1✔
549
            component.perform(Cmd::Move(Direction::Up)),
1✔
550
            CmdResult::Changed(State::One(StateValue::Usize(1))),
551
        );
552
        assert_eq!(
1✔
553
            component.perform(Cmd::Move(Direction::Up)),
1✔
554
            CmdResult::Changed(State::One(StateValue::Usize(0))),
555
        );
556
        // Upper boundary
557
        assert_eq!(
1✔
558
            component.perform(Cmd::Move(Direction::Up)),
1✔
559
            CmdResult::Changed(State::One(StateValue::Usize(0))),
560
        );
561
        // Move down
562
        assert_eq!(
1✔
563
            component.perform(Cmd::Move(Direction::Down)),
1✔
564
            CmdResult::Changed(State::One(StateValue::Usize(1))),
565
        );
566
        assert_eq!(
1✔
567
            component.perform(Cmd::Move(Direction::Down)),
1✔
568
            CmdResult::Changed(State::One(StateValue::Usize(2))),
569
        );
570
        // Lower boundary
571
        assert_eq!(
1✔
572
            component.perform(Cmd::Move(Direction::Down)),
1✔
573
            CmdResult::Changed(State::One(StateValue::Usize(2))),
574
        );
575
        // Press enter
576
        assert_eq!(
1✔
577
            component.perform(Cmd::Submit),
1✔
578
            CmdResult::Submit(State::One(StateValue::Usize(2))),
579
        );
580
        // Tab should be closed
581
        assert_eq!(component.states.is_tab_open(), false);
1✔
582
        // Re open
583
        assert_eq!(component.perform(Cmd::Submit), CmdResult::None);
1✔
584
        assert_eq!(component.states.is_tab_open(), true);
1✔
585
        // Move arrows
586
        assert_eq!(
1✔
587
            component.perform(Cmd::Submit),
1✔
588
            CmdResult::Submit(State::One(StateValue::Usize(2))),
589
        );
590
        assert_eq!(component.states.is_tab_open(), false);
1✔
591
        assert_eq!(
1✔
592
            component.perform(Cmd::Move(Direction::Down)),
1✔
593
            CmdResult::None
594
        );
595
        assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
1✔
596
    }
2✔
597
}
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