tui_realm_stdlib/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 */
28use tuirealm::command::{Cmd, CmdResult, Direction};
29use tuirealm::props::{
30    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
31    TextModifiers,
32};
33use tuirealm::ratatui::text::Line as Spans;
34use tuirealm::ratatui::{layout::Rect, text::Span, widgets::Tabs};
35use tuirealm::{Frame, MockComponent, State, StateValue};
36
37// -- states
38
39/// ## CheckboxStates
40///
41/// CheckboxStates contains states for this component
42#[derive(Default)]
43pub struct CheckboxStates {
44    pub choice: usize,         // Selected option
45    pub choices: Vec<String>,  // Available choices
46    pub selection: Vec<usize>, // Selected options
47}
48
49impl CheckboxStates {
50    /// ### next_choice
51    ///
52    /// Move choice index to next choice
53    pub fn next_choice(&mut self, rewind: bool) {
54        if rewind && self.choice + 1 >= self.choices.len() {
55            self.choice = 0;
56        } else if self.choice + 1 < self.choices.len() {
57            self.choice += 1;
58        }
59    }
60
61    /// ### prev_choice
62    ///
63    /// Move choice index to previous choice
64    pub fn prev_choice(&mut self, rewind: bool) {
65        if rewind && self.choice == 0 && !self.choices.is_empty() {
66            self.choice = self.choices.len() - 1;
67        } else if self.choice > 0 {
68            self.choice -= 1;
69        }
70    }
71
72    /// ### toggle
73    ///
74    /// Check or uncheck the option
75    pub fn toggle(&mut self) {
76        let option = self.choice;
77        if self.selection.contains(&option) {
78            let target_index = self.selection.iter().position(|x| *x == option).unwrap();
79            self.selection.remove(target_index);
80        } else {
81            self.selection.push(option);
82        }
83    }
84
85    pub fn select(&mut self, i: usize) {
86        if i < self.choices.len() && !self.selection.contains(&i) {
87            self.selection.push(i);
88        }
89    }
90
91    /// ### has
92    ///
93    /// Returns whether selection contains option
94    #[must_use]
95    pub fn has(&self, option: usize) -> bool {
96        self.selection.contains(&option)
97    }
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>>) {
105        self.choices = choices.into();
106        // Clear selection
107        self.selection.clear();
108        // Keep index if possible
109        if self.choice >= self.choices.len() {
110            self.choice = match self.choices.len() {
111                0 => 0,
112                l => l - 1,
113            };
114        }
115    }
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]
125pub struct Checkbox {
126    props: Props,
127    pub states: CheckboxStates,
128}
129
130impl Checkbox {
131    pub fn foreground(mut self, fg: Color) -> Self {
132        self.attr(Attribute::Foreground, AttrValue::Color(fg));
133        self
134    }
135
136    pub fn background(mut self, bg: Color) -> Self {
137        self.attr(Attribute::Background, AttrValue::Color(bg));
138        self
139    }
140
141    pub fn borders(mut self, b: Borders) -> Self {
142        self.attr(Attribute::Borders, AttrValue::Borders(b));
143        self
144    }
145
146    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
147        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
148        self
149    }
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 {
157        self.attr(Attribute::Rewind, AttrValue::Flag(r));
158        self
159    }
160
161    pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
162        self.attr(
163            Attribute::Content,
164            AttrValue::Payload(PropPayload::Vec(
165                choices
166                    .into_iter()
167                    .map(|v| PropValue::Str(v.into()))
168                    .collect(),
169            )),
170        );
171        self
172    }
173
174    pub fn values(mut self, selected: &[usize]) -> Self {
175        // Set state
176        self.attr(
177            Attribute::Value,
178            AttrValue::Payload(PropPayload::Vec(
179                selected.iter().map(|x| PropValue::Usize(*x)).collect(),
180            )),
181        );
182        self
183    }
184
185    fn rewindable(&self) -> bool {
186        self.props
187            .get_or(Attribute::Rewind, AttrValue::Flag(false))
188            .unwrap_flag()
189    }
190}
191
192impl 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
220            let normal_style = Style::default().fg(foreground).bg(background);
221
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
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)
238                .style(normal_style)
239                .highlight_style(Style::default().fg(foreground).add_modifier(if focus {
240                    TextModifiers::REVERSED
241                } else {
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) {
254        match attr {
255            Attribute::Content => {
256                // Reset choices
257                let current_selection = self.states.selection.clone();
258                let choices: Vec<String> = value
259                    .unwrap_payload()
260                    .unwrap_vec()
261                    .iter()
262                    .cloned()
263                    .map(|x| x.unwrap_str())
264                    .collect();
265                self.states.set_choices(choices);
266                // Preserve selection if possible
267                for c in current_selection {
268                    self.states.select(c);
269                }
270            }
271            Attribute::Value => {
272                // Clear section
273                self.states.selection.clear();
274                for c in value.unwrap_payload().unwrap_vec() {
275                    self.states.select(c.unwrap_usize());
276                }
277            }
278            attr => {
279                self.props.set(attr, value);
280            }
281        }
282    }
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 {
289        State::Vec(
290            self.states
291                .selection
292                .iter()
293                .map(|x| StateValue::Usize(*x))
294                .collect(),
295        )
296    }
297
298    fn perform(&mut self, cmd: Cmd) -> CmdResult {
299        match cmd {
300            Cmd::Move(Direction::Right) => {
301                // Increment choice
302                self.states.next_choice(self.rewindable());
303                CmdResult::None
304            }
305            Cmd::Move(Direction::Left) => {
306                // Decrement choice
307                self.states.prev_choice(self.rewindable());
308                CmdResult::None
309            }
310            Cmd::Toggle => {
311                self.states.toggle();
312                CmdResult::Changed(self.state())
313            }
314            Cmd::Submit => {
315                // Return Submit
316                CmdResult::Submit(self.state())
317            }
318            _ => CmdResult::None,
319        }
320    }
321}
322
323#[cfg(test)]
324mod 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() {
333        let mut states: CheckboxStates = CheckboxStates::default();
334        assert_eq!(states.choice, 0);
335        assert_eq!(states.choices.len(), 0);
336        assert_eq!(states.selection.len(), 0);
337        let choices: &[String] = &[
338            "lemon".to_string(),
339            "strawberry".to_string(),
340            "vanilla".to_string(),
341            "chocolate".to_string(),
342        ];
343        states.set_choices(choices);
344        assert_eq!(states.choice, 0);
345        assert_eq!(states.choices.len(), 4);
346        assert_eq!(states.selection.len(), 0);
347        // Select
348        states.toggle();
349        assert_eq!(states.selection, vec![0]);
350        // Move
351        states.prev_choice(false);
352        assert_eq!(states.choice, 0);
353        states.next_choice(false);
354        assert_eq!(states.choice, 1);
355        states.next_choice(false);
356        assert_eq!(states.choice, 2);
357        states.toggle();
358        assert_eq!(states.selection, vec![0, 2]);
359        // Forward overflow
360        states.next_choice(false);
361        states.next_choice(false);
362        assert_eq!(states.choice, 3);
363        states.prev_choice(false);
364        assert_eq!(states.choice, 2);
365        states.toggle();
366        assert_eq!(states.selection, vec![0]);
367        // has
368        assert_eq!(states.has(0), true);
369        assert_ne!(states.has(2), true);
370        // Update
371        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
372        states.set_choices(choices);
373        assert_eq!(states.choice, 1); // Move to first index available
374        assert_eq!(states.choices.len(), 2);
375        assert_eq!(states.selection.len(), 0);
376        let choices: &[String] = &[];
377        states.set_choices(choices);
378        assert_eq!(states.choice, 0); // Move to first index available
379        assert_eq!(states.choices.len(), 0);
380        assert_eq!(states.selection.len(), 0);
381        // Rewind
382        let choices: &[String] = &[
383            "lemon".to_string(),
384            "strawberry".to_string(),
385            "vanilla".to_string(),
386            "chocolate".to_string(),
387        ];
388        states.set_choices(choices);
389        assert_eq!(states.choice, 0);
390        states.prev_choice(true);
391        assert_eq!(states.choice, 3);
392        states.next_choice(true);
393        assert_eq!(states.choice, 0);
394        states.next_choice(true);
395        assert_eq!(states.choice, 1);
396        states.prev_choice(true);
397        assert_eq!(states.choice, 0);
398    }
399
400    #[test]
401    fn test_components_checkbox() {
402        // Make component
403        let mut component = Checkbox::default()
404            .background(Color::Blue)
405            .foreground(Color::Red)
406            .borders(Borders::default())
407            .title("Which food do you prefer?", Alignment::Center)
408            .choices(["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
409            .values(&[1, 4])
410            .rewind(false);
411        // Verify states
412        assert_eq!(component.states.selection, vec![1, 4]);
413        assert_eq!(component.states.choice, 0);
414        assert_eq!(component.states.choices.len(), 5);
415        component.attr(
416            Attribute::Content,
417            AttrValue::Payload(PropPayload::Vec(vec![
418                PropValue::Str(String::from("Pizza")),
419                PropValue::Str(String::from("Hummus")),
420                PropValue::Str(String::from("Ramen")),
421                PropValue::Str(String::from("Gyoza")),
422                PropValue::Str(String::from("Pasta")),
423                PropValue::Str(String::from("Falafel")),
424            ])),
425        );
426        assert_eq!(component.states.selection, vec![1, 4]);
427        assert_eq!(component.states.choices.len(), 6);
428        // Get value
429        component.attr(
430            Attribute::Value,
431            AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
432        );
433        assert_eq!(component.states.selection, vec![1]);
434        assert_eq!(component.states.choices.len(), 6);
435        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
436        // Handle events
437        assert_eq!(
438            component.perform(Cmd::Move(Direction::Left)),
439            CmdResult::None,
440        );
441        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
442        // Toggle
443        assert_eq!(
444            component.perform(Cmd::Toggle),
445            CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
446        );
447        // Left again
448        assert_eq!(
449            component.perform(Cmd::Move(Direction::Left)),
450            CmdResult::None,
451        );
452        assert_eq!(component.states.choice, 0);
453        // Right
454        assert_eq!(
455            component.perform(Cmd::Move(Direction::Right)),
456            CmdResult::None,
457        );
458        // Toggle
459        assert_eq!(
460            component.perform(Cmd::Toggle),
461            CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
462        );
463        // Right again
464        assert_eq!(
465            component.perform(Cmd::Move(Direction::Right)),
466            CmdResult::None,
467        );
468        assert_eq!(component.states.choice, 2);
469        // Right again
470        assert_eq!(
471            component.perform(Cmd::Move(Direction::Right)),
472            CmdResult::None,
473        );
474        assert_eq!(component.states.choice, 3);
475        // Right again
476        assert_eq!(
477            component.perform(Cmd::Move(Direction::Right)),
478            CmdResult::None,
479        );
480        assert_eq!(component.states.choice, 4);
481        // Right again
482        assert_eq!(
483            component.perform(Cmd::Move(Direction::Right)),
484            CmdResult::None,
485        );
486        assert_eq!(component.states.choice, 5);
487        // Right again
488        assert_eq!(
489            component.perform(Cmd::Move(Direction::Right)),
490            CmdResult::None,
491        );
492        assert_eq!(component.states.choice, 5);
493        // Submit
494        assert_eq!(
495            component.perform(Cmd::Submit),
496            CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
497        );
498    }
499
500    #[test]
501    fn various_set_choice_types() {
502        // static array of strings
503        CheckboxStates::default().set_choices(&["hello".to_string()]);
504        // vector of strings
505        CheckboxStates::default().set_choices(vec!["hello".to_string()]);
506        // boxed array of strings
507        CheckboxStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
508    }
509
510    #[test]
511    fn various_choice_types() {
512        // static array of static strings
513        let _ = Checkbox::default().choices(["hello"]);
514        // static array of strings
515        let _ = Checkbox::default().choices(["hello".to_string()]);
516        // vec of static strings
517        let _ = Checkbox::default().choices(vec!["hello"]);
518        // vec of strings
519        let _ = Checkbox::default().choices(vec!["hello".to_string()]);
520        // boxed array of static strings
521        let _ = Checkbox::default().choices(vec!["hello"].into_boxed_slice());
522        // boxed array of strings
523        let _ = Checkbox::default().choices(vec!["hello".to_string()].into_boxed_slice());
524    }
525}