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};
32use tuirealm::ratatui::text::Line as Spans;
33use tuirealm::ratatui::{layout::Rect, text::Span, widgets::Tabs};
34use tuirealm::{Frame, MockComponent, State, StateValue};
35
36// -- states
37
38/// ## CheckboxStates
39///
40/// CheckboxStates contains states for this component
41#[derive(Default)]
42pub struct CheckboxStates {
43    pub choice: usize,         // Selected option
44    pub choices: Vec<String>,  // Available choices
45    pub selection: Vec<usize>, // Selected options
46}
47
48impl CheckboxStates {
49    /// ### next_choice
50    ///
51    /// Move choice index to next choice
52    pub fn next_choice(&mut self, rewind: bool) {
53        if rewind && self.choice + 1 >= self.choices.len() {
54            self.choice = 0;
55        } else if self.choice + 1 < self.choices.len() {
56            self.choice += 1;
57        }
58    }
59
60    /// ### prev_choice
61    ///
62    /// Move choice index to previous choice
63    pub fn prev_choice(&mut self, rewind: bool) {
64        if rewind && self.choice == 0 && !self.choices.is_empty() {
65            self.choice = self.choices.len() - 1;
66        } else if self.choice > 0 {
67            self.choice -= 1;
68        }
69    }
70
71    /// ### toggle
72    ///
73    /// Check or uncheck the option
74    pub fn toggle(&mut self) {
75        let option = self.choice;
76        if self.selection.contains(&option) {
77            let target_index = self.selection.iter().position(|x| *x == option).unwrap();
78            self.selection.remove(target_index);
79        } else {
80            self.selection.push(option);
81        }
82    }
83
84    pub fn select(&mut self, i: usize) {
85        if i < self.choices.len() && !self.selection.contains(&i) {
86            self.selection.push(i);
87        }
88    }
89
90    /// ### has
91    ///
92    /// Returns whether selection contains option
93    pub fn has(&self, option: usize) -> bool {
94        self.selection.contains(&option)
95    }
96
97    /// ### set_choices
98    ///
99    /// Set CheckboxStates choices from a vector of str
100    /// In addition resets current selection and keep index if possible or set it to the first value
101    /// available
102    pub fn set_choices(&mut self, choices: &[String]) {
103        self.choices = choices.to_vec();
104        // Clear selection
105        self.selection.clear();
106        // Keep index if possible
107        if self.choice >= self.choices.len() {
108            self.choice = match self.choices.len() {
109                0 => 0,
110                l => l - 1,
111            };
112        }
113    }
114}
115
116// -- component
117
118/// ## Checkbox
119///
120/// Checkbox component represents a group of tabs to select from
121#[derive(Default)]
122pub struct Checkbox {
123    props: Props,
124    pub states: CheckboxStates,
125}
126
127impl Checkbox {
128    pub fn foreground(mut self, fg: Color) -> Self {
129        self.attr(Attribute::Foreground, AttrValue::Color(fg));
130        self
131    }
132
133    pub fn background(mut self, bg: Color) -> Self {
134        self.attr(Attribute::Background, AttrValue::Color(bg));
135        self
136    }
137
138    pub fn borders(mut self, b: Borders) -> Self {
139        self.attr(Attribute::Borders, AttrValue::Borders(b));
140        self
141    }
142
143    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
144        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
145        self
146    }
147
148    pub fn inactive(mut self, s: Style) -> Self {
149        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
150        self
151    }
152
153    pub fn rewind(mut self, r: bool) -> Self {
154        self.attr(Attribute::Rewind, AttrValue::Flag(r));
155        self
156    }
157
158    pub fn choices<S: AsRef<str>>(mut self, choices: &[S]) -> Self {
159        self.attr(
160            Attribute::Content,
161            AttrValue::Payload(PropPayload::Vec(
162                choices
163                    .iter()
164                    .map(|x| PropValue::Str(x.as_ref().to_string()))
165                    .collect(),
166            )),
167        );
168        self
169    }
170
171    pub fn values(mut self, selected: &[usize]) -> Self {
172        // Set state
173        self.attr(
174            Attribute::Value,
175            AttrValue::Payload(PropPayload::Vec(
176                selected.iter().map(|x| PropValue::Usize(*x)).collect(),
177            )),
178        );
179        self
180    }
181
182    fn rewindable(&self) -> bool {
183        self.props
184            .get_or(Attribute::Rewind, AttrValue::Flag(false))
185            .unwrap_flag()
186    }
187}
188
189impl MockComponent for Checkbox {
190    fn view(&mut self, render: &mut Frame, area: Rect) {
191        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
192            let foreground = self
193                .props
194                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
195                .unwrap_color();
196            let background = self
197                .props
198                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
199                .unwrap_color();
200            let borders = self
201                .props
202                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
203                .unwrap_borders();
204            let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
205            let focus = self
206                .props
207                .get_or(Attribute::Focus, AttrValue::Flag(false))
208                .unwrap_flag();
209            let inactive_style = self
210                .props
211                .get(Attribute::FocusStyle)
212                .map(|x| x.unwrap_style());
213            let div = crate::utils::get_block(borders, title, focus, inactive_style);
214            // Make colors
215            let (bg, fg, block_color): (Color, Color, Color) = match &focus {
216                true => (foreground, background, foreground),
217                false => (Color::Reset, foreground, Color::Reset),
218            };
219            // Make choices
220            let choices: Vec<Spans> = self
221                .states
222                .choices
223                .iter()
224                .enumerate()
225                .map(|(idx, x)| {
226                    let checkbox: &str = match self.states.has(idx) {
227                        true => "☑ ",
228                        false => "☐ ",
229                    };
230                    let (fg, bg) = match focus {
231                        true => match self.states.choice == idx {
232                            true => (fg, bg),
233                            false => (bg, fg),
234                        },
235                        false => (fg, bg),
236                    };
237                    // Make spans
238                    Spans::from(vec![
239                        Span::styled(checkbox, Style::default().fg(fg).bg(bg)),
240                        Span::styled(x.to_string(), Style::default().fg(fg).bg(bg)),
241                    ])
242                })
243                .collect();
244            let checkbox: Tabs = Tabs::new(choices)
245                .block(div)
246                .select(self.states.choice)
247                .style(Style::default().fg(block_color));
248            render.render_widget(checkbox, area);
249        }
250    }
251
252    fn query(&self, attr: Attribute) -> Option<AttrValue> {
253        self.props.get(attr)
254    }
255
256    fn attr(&mut self, attr: Attribute, value: AttrValue) {
257        match attr {
258            Attribute::Content => {
259                // Reset choices
260                let current_selection = self.states.selection.clone();
261                let choices: Vec<String> = value
262                    .unwrap_payload()
263                    .unwrap_vec()
264                    .iter()
265                    .cloned()
266                    .map(|x| x.unwrap_str())
267                    .collect();
268                self.states.set_choices(&choices);
269                // Preserve selection if possible
270                for c in current_selection.into_iter() {
271                    self.states.select(c);
272                }
273            }
274            Attribute::Value => {
275                // Clear section
276                self.states.selection.clear();
277                for c in value.unwrap_payload().unwrap_vec() {
278                    self.states.select(c.unwrap_usize());
279                }
280            }
281            attr => {
282                self.props.set(attr, value);
283            }
284        }
285    }
286
287    /// ### get_state
288    ///
289    /// Get current state from component
290    /// For this component returns the vec of selected items
291    fn state(&self) -> State {
292        State::Vec(
293            self.states
294                .selection
295                .iter()
296                .map(|x| StateValue::Usize(*x))
297                .collect(),
298        )
299    }
300
301    fn perform(&mut self, cmd: Cmd) -> CmdResult {
302        match cmd {
303            Cmd::Move(Direction::Right) => {
304                // Increment choice
305                self.states.next_choice(self.rewindable());
306                CmdResult::None
307            }
308            Cmd::Move(Direction::Left) => {
309                // Decrement choice
310                self.states.prev_choice(self.rewindable());
311                CmdResult::None
312            }
313            Cmd::Toggle => {
314                self.states.toggle();
315                CmdResult::Changed(self.state())
316            }
317            Cmd::Submit => {
318                // Return Submit
319                CmdResult::Submit(self.state())
320            }
321            _ => CmdResult::None,
322        }
323    }
324}
325
326#[cfg(test)]
327mod test {
328
329    use super::*;
330
331    use pretty_assertions::{assert_eq, assert_ne};
332    use tuirealm::props::{PropPayload, PropValue};
333
334    #[test]
335    fn test_components_checkbox_states() {
336        let mut states: CheckboxStates = CheckboxStates::default();
337        assert_eq!(states.choice, 0);
338        assert_eq!(states.choices.len(), 0);
339        assert_eq!(states.selection.len(), 0);
340        let choices: &[String] = &[
341            "lemon".to_string(),
342            "strawberry".to_string(),
343            "vanilla".to_string(),
344            "chocolate".to_string(),
345        ];
346        states.set_choices(choices);
347        assert_eq!(states.choice, 0);
348        assert_eq!(states.choices.len(), 4);
349        assert_eq!(states.selection.len(), 0);
350        // Select
351        states.toggle();
352        assert_eq!(states.selection, vec![0]);
353        // Move
354        states.prev_choice(false);
355        assert_eq!(states.choice, 0);
356        states.next_choice(false);
357        assert_eq!(states.choice, 1);
358        states.next_choice(false);
359        assert_eq!(states.choice, 2);
360        states.toggle();
361        assert_eq!(states.selection, vec![0, 2]);
362        // Forward overflow
363        states.next_choice(false);
364        states.next_choice(false);
365        assert_eq!(states.choice, 3);
366        states.prev_choice(false);
367        assert_eq!(states.choice, 2);
368        states.toggle();
369        assert_eq!(states.selection, vec![0]);
370        // has
371        assert_eq!(states.has(0), true);
372        assert_ne!(states.has(2), true);
373        // Update
374        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
375        states.set_choices(choices);
376        assert_eq!(states.choice, 1); // Move to first index available
377        assert_eq!(states.choices.len(), 2);
378        assert_eq!(states.selection.len(), 0);
379        let choices: &[String] = &[];
380        states.set_choices(choices);
381        assert_eq!(states.choice, 0); // Move to first index available
382        assert_eq!(states.choices.len(), 0);
383        assert_eq!(states.selection.len(), 0);
384        // Rewind
385        let choices: &[String] = &[
386            "lemon".to_string(),
387            "strawberry".to_string(),
388            "vanilla".to_string(),
389            "chocolate".to_string(),
390        ];
391        states.set_choices(choices);
392        assert_eq!(states.choice, 0);
393        states.prev_choice(true);
394        assert_eq!(states.choice, 3);
395        states.next_choice(true);
396        assert_eq!(states.choice, 0);
397        states.next_choice(true);
398        assert_eq!(states.choice, 1);
399        states.prev_choice(true);
400        assert_eq!(states.choice, 0);
401    }
402
403    #[test]
404    fn test_components_checkbox() {
405        // Make component
406        let mut component = Checkbox::default()
407            .background(Color::Blue)
408            .foreground(Color::Red)
409            .borders(Borders::default())
410            .title("Which food do you prefer?", Alignment::Center)
411            .choices(&["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
412            .values(&[1, 4])
413            .rewind(false);
414        // Verify states
415        assert_eq!(component.states.selection, vec![1, 4]);
416        assert_eq!(component.states.choice, 0);
417        assert_eq!(component.states.choices.len(), 5);
418        component.attr(
419            Attribute::Content,
420            AttrValue::Payload(PropPayload::Vec(vec![
421                PropValue::Str(String::from("Pizza")),
422                PropValue::Str(String::from("Hummus")),
423                PropValue::Str(String::from("Ramen")),
424                PropValue::Str(String::from("Gyoza")),
425                PropValue::Str(String::from("Pasta")),
426                PropValue::Str(String::from("Falafel")),
427            ])),
428        );
429        assert_eq!(component.states.selection, vec![1, 4]);
430        assert_eq!(component.states.choices.len(), 6);
431        // Get value
432        component.attr(
433            Attribute::Value,
434            AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
435        );
436        assert_eq!(component.states.selection, vec![1]);
437        assert_eq!(component.states.choices.len(), 6);
438        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
439        // Handle events
440        assert_eq!(
441            component.perform(Cmd::Move(Direction::Left)),
442            CmdResult::None,
443        );
444        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
445        // Toggle
446        assert_eq!(
447            component.perform(Cmd::Toggle),
448            CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
449        );
450        // Left again
451        assert_eq!(
452            component.perform(Cmd::Move(Direction::Left)),
453            CmdResult::None,
454        );
455        assert_eq!(component.states.choice, 0);
456        // Right
457        assert_eq!(
458            component.perform(Cmd::Move(Direction::Right)),
459            CmdResult::None,
460        );
461        // Toggle
462        assert_eq!(
463            component.perform(Cmd::Toggle),
464            CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
465        );
466        // Right again
467        assert_eq!(
468            component.perform(Cmd::Move(Direction::Right)),
469            CmdResult::None,
470        );
471        assert_eq!(component.states.choice, 2);
472        // Right again
473        assert_eq!(
474            component.perform(Cmd::Move(Direction::Right)),
475            CmdResult::None,
476        );
477        assert_eq!(component.states.choice, 3);
478        // Right again
479        assert_eq!(
480            component.perform(Cmd::Move(Direction::Right)),
481            CmdResult::None,
482        );
483        assert_eq!(component.states.choice, 4);
484        // Right again
485        assert_eq!(
486            component.perform(Cmd::Move(Direction::Right)),
487            CmdResult::None,
488        );
489        assert_eq!(component.states.choice, 5);
490        // Right again
491        assert_eq!(
492            component.perform(Cmd::Move(Direction::Right)),
493            CmdResult::None,
494        );
495        assert_eq!(component.states.choice, 5);
496        // Submit
497        assert_eq!(
498            component.perform(Cmd::Submit),
499            CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
500        );
501    }
502}