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

veeso / tui-realm-stdlib / 20192640301

13 Dec 2025 01:20PM UTC coverage: 68.439% (+0.4%) from 68.002%
20192640301

Pull #44

github

web-flow
Merge 1c77b17ae into 84017eb33
Pull Request #44: Fix Styling inconsistencies

6 of 83 new or added lines in 10 files covered. (7.23%)

6 existing lines in 5 files now uncovered.

3038 of 4439 relevant lines covered (68.44%)

1.84 hits per line

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

82.05
/src/components/checkbox.rs
1
//! ## Checkbox
2
//!
3
//! `Checkbox` component renders a checkbox group
4

5
/**
6
 * MIT License
7
 *
8
 * termscp - Copyright (c) 2021 Christian Visintin
9
 *
10
 * Permission is hereby granted, free of charge, to any person obtaining a copy
11
 * of this software and associated documentation files (the "Software"), to deal
12
 * in the Software without restriction, including without limitation the rights
13
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
 * copies of the Software, and to permit persons to whom the Software is
15
 * furnished to do so, subject to the following conditions:
16
 *
17
 * The above copyright notice and this permission notice shall be included in all
18
 * copies or substantial portions of the Software.
19
 *
20
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
 * SOFTWARE.
27
 */
28
use tuirealm::command::{Cmd, CmdResult, Direction};
29
use tuirealm::props::{
30
    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
31
    TextModifiers,
32
};
33
use tuirealm::ratatui::text::Line as Spans;
34
use tuirealm::ratatui::{layout::Rect, text::Span, widgets::Tabs};
35
use tuirealm::{Frame, MockComponent, State, StateValue};
36

37
// -- states
38

39
/// ## CheckboxStates
40
///
41
/// CheckboxStates contains states for this component
42
#[derive(Default)]
43
pub struct CheckboxStates {
44
    pub choice: usize,         // Selected option
45
    pub choices: Vec<String>,  // Available choices
46
    pub selection: Vec<usize>, // Selected options
47
}
48

49
impl CheckboxStates {
50
    /// ### next_choice
51
    ///
52
    /// Move choice index to next choice
53
    pub fn next_choice(&mut self, rewind: bool) {
12✔
54
        if rewind && self.choice + 1 >= self.choices.len() {
12✔
55
            self.choice = 0;
1✔
56
        } else if self.choice + 1 < self.choices.len() {
11✔
57
            self.choice += 1;
9✔
58
        }
9✔
59
    }
12✔
60

61
    /// ### prev_choice
62
    ///
63
    /// Move choice index to previous choice
64
    pub fn prev_choice(&mut self, rewind: bool) {
6✔
65
        if rewind && self.choice == 0 && !self.choices.is_empty() {
6✔
66
            self.choice = self.choices.len() - 1;
1✔
67
        } else if self.choice > 0 {
5✔
68
            self.choice -= 1;
2✔
69
        }
3✔
70
    }
6✔
71

72
    /// ### toggle
73
    ///
74
    /// Check or uncheck the option
75
    pub fn toggle(&mut self) {
5✔
76
        let option = self.choice;
5✔
77
        if self.selection.contains(&option) {
5✔
78
            let target_index = self.selection.iter().position(|x| *x == option).unwrap();
3✔
79
            self.selection.remove(target_index);
2✔
80
        } else {
3✔
81
            self.selection.push(option);
3✔
82
        }
3✔
83
    }
5✔
84

85
    pub fn select(&mut self, i: usize) {
5✔
86
        if i < self.choices.len() && !self.selection.contains(&i) {
5✔
87
            self.selection.push(i);
5✔
88
        }
5✔
89
    }
5✔
90

91
    /// ### has
92
    ///
93
    /// Returns whether selection contains option
94
    #[must_use]
95
    pub fn has(&self, option: usize) -> bool {
2✔
96
        self.selection.contains(&option)
2✔
97
    }
2✔
98

99
    /// ### set_choices
100
    ///
101
    /// Set CheckboxStates choices from a vector of str
102
    /// In addition resets current selection and keep index if possible or set it to the first value
103
    /// available
104
    pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
15✔
105
        self.choices = choices.into();
15✔
106
        // Clear selection
107
        self.selection.clear();
15✔
108
        // Keep index if possible
109
        if self.choice >= self.choices.len() {
15✔
110
            self.choice = match self.choices.len() {
2✔
111
                0 => 0,
1✔
112
                l => l - 1,
1✔
113
            };
114
        }
13✔
115
    }
15✔
116
}
117

118
// -- component
119

120
/// ## Checkbox
121
///
122
/// Checkbox component represents a group of tabs to select from
123
#[derive(Default)]
124
#[must_use]
125
pub struct Checkbox {
126
    props: Props,
127
    pub states: CheckboxStates,
128
}
129

130
impl Checkbox {
131
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
132
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
133
        self
1✔
134
    }
1✔
135

136
    pub fn background(mut self, bg: Color) -> Self {
1✔
137
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
138
        self
1✔
139
    }
1✔
140

141
    pub fn borders(mut self, b: Borders) -> Self {
1✔
142
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
143
        self
1✔
144
    }
1✔
145

146
    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
1✔
147
        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
1✔
148
        self
1✔
149
    }
1✔
150

151
    pub fn inactive(mut self, s: Style) -> Self {
×
152
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
153
        self
×
154
    }
×
155

156
    pub fn rewind(mut self, r: bool) -> Self {
1✔
157
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
1✔
158
        self
1✔
159
    }
1✔
160

161
    pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
7✔
162
        self.attr(
7✔
163
            Attribute::Content,
7✔
164
            AttrValue::Payload(PropPayload::Vec(
165
                choices
7✔
166
                    .into_iter()
7✔
167
                    .map(|v| PropValue::Str(v.into()))
11✔
168
                    .collect(),
7✔
169
            )),
170
        );
171
        self
7✔
172
    }
7✔
173

174
    pub fn values(mut self, selected: &[usize]) -> Self {
1✔
175
        // Set state
176
        self.attr(
1✔
177
            Attribute::Value,
1✔
178
            AttrValue::Payload(PropPayload::Vec(
179
                selected.iter().map(|x| PropValue::Usize(*x)).collect(),
2✔
180
            )),
181
        );
182
        self
1✔
183
    }
1✔
184

185
    fn rewindable(&self) -> bool {
8✔
186
        self.props
8✔
187
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
8✔
188
            .unwrap_flag()
8✔
189
    }
8✔
190
}
191

192
impl MockComponent for Checkbox {
193
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
194
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
195
            let foreground = self
×
196
                .props
×
197
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
198
                .unwrap_color();
×
199
            let background = self
×
200
                .props
×
201
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
202
                .unwrap_color();
×
203
            let borders = self
×
204
                .props
×
205
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
206
                .unwrap_borders();
×
207
            let title = self
×
208
                .props
×
209
                .get_ref(Attribute::Title)
×
210
                .and_then(|x| x.as_title());
×
211
            let focus = self
×
212
                .props
×
213
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
214
                .unwrap_flag();
×
215
            let inactive_style = self
×
216
                .props
×
217
                .get(Attribute::FocusStyle)
×
218
                .map(|x| x.unwrap_style());
×
219

NEW
220
            let normal_style = Style::default().fg(foreground).bg(background);
×
221

UNCOV
222
            let div = crate::utils::get_block(borders, title, focus, inactive_style);
×
223
            // Make choices
224
            let choices: Vec<Spans> = self
×
225
                .states
×
226
                .choices
×
227
                .iter()
×
228
                .enumerate()
×
229
                .map(|(idx, x)| {
×
230
                    let checkbox: &str = if self.states.has(idx) { "☑ " } else { "☐ " };
×
231
                    // Make spans
NEW
232
                    Spans::from(vec![Span::raw(checkbox), Span::raw(x.to_string())])
×
233
                })
×
234
                .collect();
×
235
            let checkbox: Tabs = Tabs::new(choices)
×
236
                .block(div)
×
237
                .select(self.states.choice)
×
NEW
238
                .style(normal_style)
×
NEW
239
                .highlight_style(Style::default().fg(foreground).add_modifier(if focus {
×
NEW
240
                    TextModifiers::REVERSED
×
241
                } else {
NEW
242
                    TextModifiers::empty()
×
243
                }));
244

245
            render.render_widget(checkbox, area);
×
246
        }
×
247
    }
×
248

249
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
250
        self.props.get(attr)
×
251
    }
×
252

253
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
15✔
254
        match attr {
15✔
255
            Attribute::Content => {
256
                // Reset choices
257
                let current_selection = self.states.selection.clone();
8✔
258
                let choices: Vec<String> = value
8✔
259
                    .unwrap_payload()
8✔
260
                    .unwrap_vec()
8✔
261
                    .iter()
8✔
262
                    .cloned()
8✔
263
                    .map(|x| x.unwrap_str())
17✔
264
                    .collect();
8✔
265
                self.states.set_choices(choices);
8✔
266
                // Preserve selection if possible
267
                for c in current_selection {
8✔
268
                    self.states.select(c);
2✔
269
                }
2✔
270
            }
271
            Attribute::Value => {
272
                // Clear section
273
                self.states.selection.clear();
2✔
274
                for c in value.unwrap_payload().unwrap_vec() {
3✔
275
                    self.states.select(c.unwrap_usize());
3✔
276
                }
3✔
277
            }
278
            attr => {
5✔
279
                self.props.set(attr, value);
5✔
280
            }
5✔
281
        }
282
    }
15✔
283

284
    /// ### get_state
285
    ///
286
    /// Get current state from component
287
    /// For this component returns the vec of selected items
288
    fn state(&self) -> State {
5✔
289
        State::Vec(
290
            self.states
5✔
291
                .selection
5✔
292
                .iter()
5✔
293
                .map(|x| StateValue::Usize(*x))
6✔
294
                .collect(),
5✔
295
        )
296
    }
5✔
297

298
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
11✔
299
        match cmd {
8✔
300
            Cmd::Move(Direction::Right) => {
301
                // Increment choice
302
                self.states.next_choice(self.rewindable());
6✔
303
                CmdResult::None
6✔
304
            }
305
            Cmd::Move(Direction::Left) => {
306
                // Decrement choice
307
                self.states.prev_choice(self.rewindable());
2✔
308
                CmdResult::None
2✔
309
            }
310
            Cmd::Toggle => {
311
                self.states.toggle();
2✔
312
                CmdResult::Changed(self.state())
2✔
313
            }
314
            Cmd::Submit => {
315
                // Return Submit
316
                CmdResult::Submit(self.state())
1✔
317
            }
318
            _ => CmdResult::None,
×
319
        }
320
    }
11✔
321
}
322

323
#[cfg(test)]
324
mod test {
325

326
    use super::*;
327

328
    use pretty_assertions::{assert_eq, assert_ne};
329
    use tuirealm::props::{PropPayload, PropValue};
330

331
    #[test]
332
    fn test_components_checkbox_states() {
1✔
333
        let mut states: CheckboxStates = CheckboxStates::default();
1✔
334
        assert_eq!(states.choice, 0);
1✔
335
        assert_eq!(states.choices.len(), 0);
1✔
336
        assert_eq!(states.selection.len(), 0);
1✔
337
        let choices: &[String] = &[
1✔
338
            "lemon".to_string(),
1✔
339
            "strawberry".to_string(),
1✔
340
            "vanilla".to_string(),
1✔
341
            "chocolate".to_string(),
1✔
342
        ];
1✔
343
        states.set_choices(choices);
1✔
344
        assert_eq!(states.choice, 0);
1✔
345
        assert_eq!(states.choices.len(), 4);
1✔
346
        assert_eq!(states.selection.len(), 0);
1✔
347
        // Select
348
        states.toggle();
1✔
349
        assert_eq!(states.selection, vec![0]);
1✔
350
        // Move
351
        states.prev_choice(false);
1✔
352
        assert_eq!(states.choice, 0);
1✔
353
        states.next_choice(false);
1✔
354
        assert_eq!(states.choice, 1);
1✔
355
        states.next_choice(false);
1✔
356
        assert_eq!(states.choice, 2);
1✔
357
        states.toggle();
1✔
358
        assert_eq!(states.selection, vec![0, 2]);
1✔
359
        // Forward overflow
360
        states.next_choice(false);
1✔
361
        states.next_choice(false);
1✔
362
        assert_eq!(states.choice, 3);
1✔
363
        states.prev_choice(false);
1✔
364
        assert_eq!(states.choice, 2);
1✔
365
        states.toggle();
1✔
366
        assert_eq!(states.selection, vec![0]);
1✔
367
        // has
368
        assert_eq!(states.has(0), true);
1✔
369
        assert_ne!(states.has(2), true);
1✔
370
        // Update
371
        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
1✔
372
        states.set_choices(choices);
1✔
373
        assert_eq!(states.choice, 1); // Move to first index available
1✔
374
        assert_eq!(states.choices.len(), 2);
1✔
375
        assert_eq!(states.selection.len(), 0);
1✔
376
        let choices: &[String] = &[];
1✔
377
        states.set_choices(choices);
1✔
378
        assert_eq!(states.choice, 0); // Move to first index available
1✔
379
        assert_eq!(states.choices.len(), 0);
1✔
380
        assert_eq!(states.selection.len(), 0);
1✔
381
        // Rewind
382
        let choices: &[String] = &[
1✔
383
            "lemon".to_string(),
1✔
384
            "strawberry".to_string(),
1✔
385
            "vanilla".to_string(),
1✔
386
            "chocolate".to_string(),
1✔
387
        ];
1✔
388
        states.set_choices(choices);
1✔
389
        assert_eq!(states.choice, 0);
1✔
390
        states.prev_choice(true);
1✔
391
        assert_eq!(states.choice, 3);
1✔
392
        states.next_choice(true);
1✔
393
        assert_eq!(states.choice, 0);
1✔
394
        states.next_choice(true);
1✔
395
        assert_eq!(states.choice, 1);
1✔
396
        states.prev_choice(true);
1✔
397
        assert_eq!(states.choice, 0);
1✔
398
    }
1✔
399

400
    #[test]
401
    fn test_components_checkbox() {
1✔
402
        // Make component
403
        let mut component = Checkbox::default()
1✔
404
            .background(Color::Blue)
1✔
405
            .foreground(Color::Red)
1✔
406
            .borders(Borders::default())
1✔
407
            .title("Which food do you prefer?", Alignment::Center)
1✔
408
            .choices(["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
1✔
409
            .values(&[1, 4])
1✔
410
            .rewind(false);
1✔
411
        // Verify states
412
        assert_eq!(component.states.selection, vec![1, 4]);
1✔
413
        assert_eq!(component.states.choice, 0);
1✔
414
        assert_eq!(component.states.choices.len(), 5);
1✔
415
        component.attr(
1✔
416
            Attribute::Content,
1✔
417
            AttrValue::Payload(PropPayload::Vec(vec![
1✔
418
                PropValue::Str(String::from("Pizza")),
1✔
419
                PropValue::Str(String::from("Hummus")),
1✔
420
                PropValue::Str(String::from("Ramen")),
1✔
421
                PropValue::Str(String::from("Gyoza")),
1✔
422
                PropValue::Str(String::from("Pasta")),
1✔
423
                PropValue::Str(String::from("Falafel")),
1✔
424
            ])),
1✔
425
        );
426
        assert_eq!(component.states.selection, vec![1, 4]);
1✔
427
        assert_eq!(component.states.choices.len(), 6);
1✔
428
        // Get value
429
        component.attr(
1✔
430
            Attribute::Value,
1✔
431
            AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
1✔
432
        );
433
        assert_eq!(component.states.selection, vec![1]);
1✔
434
        assert_eq!(component.states.choices.len(), 6);
1✔
435
        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
1✔
436
        // Handle events
437
        assert_eq!(
1✔
438
            component.perform(Cmd::Move(Direction::Left)),
1✔
439
            CmdResult::None,
440
        );
441
        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
1✔
442
        // Toggle
443
        assert_eq!(
1✔
444
            component.perform(Cmd::Toggle),
1✔
445
            CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
1✔
446
        );
447
        // Left again
448
        assert_eq!(
1✔
449
            component.perform(Cmd::Move(Direction::Left)),
1✔
450
            CmdResult::None,
451
        );
452
        assert_eq!(component.states.choice, 0);
1✔
453
        // Right
454
        assert_eq!(
1✔
455
            component.perform(Cmd::Move(Direction::Right)),
1✔
456
            CmdResult::None,
457
        );
458
        // Toggle
459
        assert_eq!(
1✔
460
            component.perform(Cmd::Toggle),
1✔
461
            CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
1✔
462
        );
463
        // Right again
464
        assert_eq!(
1✔
465
            component.perform(Cmd::Move(Direction::Right)),
1✔
466
            CmdResult::None,
467
        );
468
        assert_eq!(component.states.choice, 2);
1✔
469
        // Right again
470
        assert_eq!(
1✔
471
            component.perform(Cmd::Move(Direction::Right)),
1✔
472
            CmdResult::None,
473
        );
474
        assert_eq!(component.states.choice, 3);
1✔
475
        // Right again
476
        assert_eq!(
1✔
477
            component.perform(Cmd::Move(Direction::Right)),
1✔
478
            CmdResult::None,
479
        );
480
        assert_eq!(component.states.choice, 4);
1✔
481
        // Right again
482
        assert_eq!(
1✔
483
            component.perform(Cmd::Move(Direction::Right)),
1✔
484
            CmdResult::None,
485
        );
486
        assert_eq!(component.states.choice, 5);
1✔
487
        // Right again
488
        assert_eq!(
1✔
489
            component.perform(Cmd::Move(Direction::Right)),
1✔
490
            CmdResult::None,
491
        );
492
        assert_eq!(component.states.choice, 5);
1✔
493
        // Submit
494
        assert_eq!(
1✔
495
            component.perform(Cmd::Submit),
1✔
496
            CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
1✔
497
        );
498
    }
1✔
499

500
    #[test]
501
    fn various_set_choice_types() {
1✔
502
        // static array of strings
503
        CheckboxStates::default().set_choices(&["hello".to_string()]);
1✔
504
        // vector of strings
505
        CheckboxStates::default().set_choices(vec!["hello".to_string()]);
1✔
506
        // boxed array of strings
507
        CheckboxStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
1✔
508
    }
1✔
509

510
    #[test]
511
    fn various_choice_types() {
1✔
512
        // static array of static strings
513
        let _ = Checkbox::default().choices(["hello"]);
1✔
514
        // static array of strings
515
        let _ = Checkbox::default().choices(["hello".to_string()]);
1✔
516
        // vec of static strings
517
        let _ = Checkbox::default().choices(vec!["hello"]);
1✔
518
        // vec of strings
519
        let _ = Checkbox::default().choices(vec!["hello".to_string()]);
1✔
520
        // boxed array of static strings
521
        let _ = Checkbox::default().choices(vec!["hello"].into_boxed_slice());
1✔
522
        // boxed array of strings
523
        let _ = Checkbox::default().choices(vec!["hello".to_string()].into_boxed_slice());
1✔
524
    }
1✔
525
}
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