Skip to main content

tui_realm_stdlib/components/
checkbox.rs

1//! `Checkbox` component renders a checkbox group.
2
3/**
4 * MIT License
5 *
6 * termscp - Copyright (c) 2021 Christian Visintin
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a copy
9 * of this software and associated documentation files (the "Software"), to deal
10 * in the Software without restriction, including without limitation the rights
11 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 * copies of the Software, and to permit persons to whom the Software is
13 * furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in all
16 * copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 * SOFTWARE.
25 */
26use tuirealm::command::{Cmd, CmdResult, Direction};
27use tuirealm::component::Component;
28use tuirealm::props::{
29    AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, QueryResult, Style,
30    TextModifiers, Title,
31};
32use tuirealm::ratatui::Frame;
33use tuirealm::ratatui::layout::Rect;
34use tuirealm::ratatui::text::{Line, Span};
35use tuirealm::ratatui::widgets::Tabs;
36use tuirealm::state::{State, StateValue};
37
38use crate::prop_ext::{CommonHighlight, CommonProps};
39
40// -- states
41
42/// The state that needs to be kept for [`Checkbox`].
43#[derive(Default)]
44pub struct CheckboxStates {
45    /// Current hover option.
46    pub choice: usize,
47    /// Available choices.
48    pub choices: Vec<String>,
49    /// Enabled options.
50    pub selection: Vec<usize>,
51}
52
53impl CheckboxStates {
54    /// Move choice index to next choice.
55    pub fn next_choice(&mut self, rewind: bool) {
56        if rewind && self.choice + 1 >= self.choices.len() {
57            self.choice = 0;
58        } else if self.choice + 1 < self.choices.len() {
59            self.choice += 1;
60        }
61    }
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    /// Check or uncheck the option.
73    pub fn toggle(&mut self) {
74        let option = self.choice;
75        if self.selection.contains(&option) {
76            let target_index = self.selection.iter().position(|x| *x == option).unwrap();
77            self.selection.remove(target_index);
78        } else {
79            self.selection.push(option);
80        }
81    }
82
83    /// Select a specific option.
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    /// Determine if `options` is toggeled as selected.
91    #[must_use]
92    pub fn has(&self, option: usize) -> bool {
93        self.selection.contains(&option)
94    }
95
96    /// Overwrite the choices available with new ones.
97    ///
98    /// In addition resets current selection and keep index if possible or set it to the first value
99    /// available.
100    pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
101        self.choices = choices.into();
102        // Clear selection
103        self.selection.clear();
104        // Keep index if possible
105        if self.choice >= self.choices.len() {
106            self.choice = match self.choices.len() {
107                0 => 0,
108                l => l - 1,
109            };
110        }
111    }
112}
113
114// -- component
115
116/// The checkbox component is a multi-choice selector.
117///
118/// Use [`Radio`](crate::components::Radio) if a single-choice selector is wanted.
119#[derive(Default)]
120#[must_use]
121pub struct Checkbox {
122    common: CommonProps,
123    common_hg: CommonHighlight,
124    props: Props,
125    pub states: CheckboxStates,
126}
127
128impl Checkbox {
129    /// Set the main foreground color. This may get overwritten by individual text styles.
130    pub fn foreground(mut self, fg: Color) -> Self {
131        self.attr(Attribute::Foreground, AttrValue::Color(fg));
132        self
133    }
134
135    /// Set the main background color. This may get overwritten by individual text styles.
136    pub fn background(mut self, bg: Color) -> Self {
137        self.attr(Attribute::Background, AttrValue::Color(bg));
138        self
139    }
140
141    /// Set the main text modifiers. This may get overwritten by individual text styles.
142    pub fn modifiers(mut self, m: TextModifiers) -> Self {
143        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
144        self
145    }
146
147    /// Set the main style. This may get overwritten by individual text styles.
148    ///
149    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
150    pub fn style(mut self, style: Style) -> Self {
151        self.attr(Attribute::Style, AttrValue::Style(style));
152        self
153    }
154
155    /// Set a custom style for the border when the component is unfocused.
156    pub fn inactive(mut self, s: Style) -> Self {
157        self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
158        self
159    }
160
161    /// Add a border to the component.
162    pub fn borders(mut self, b: Borders) -> Self {
163        self.attr(Attribute::Borders, AttrValue::Borders(b));
164        self
165    }
166
167    /// Add a title to the component.
168    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
169        self.attr(Attribute::Title, AttrValue::Title(title.into()));
170        self
171    }
172
173    /// Set a custom highlight style that is patched on-top of the normal style.
174    ///
175    /// By default the highlight style is just `Style::new().add_modifier(Modifier::REVERSED)`.
176    pub fn highlight_style(mut self, s: Style) -> Self {
177        self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
178        self
179    }
180
181    /// Set a custom highlight style that is patched on-top of the highlight style when unfocused.
182    pub fn highlight_style_inactive(mut self, s: Style) -> Self {
183        self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
184        self
185    }
186
187    /// Set whether wraparound should be possible (down on the last choice wraps around to 0, and the other way around).
188    pub fn rewind(mut self, r: bool) -> Self {
189        self.attr(Attribute::Rewind, AttrValue::Flag(r));
190        self
191    }
192
193    /// Set the choices that should be possible.
194    pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
195        // TODO: we should consider using Spans or Lines
196        self.attr(
197            Attribute::Content,
198            AttrValue::Payload(PropPayload::Vec(
199                choices
200                    .into_iter()
201                    .map(|v| PropValue::Str(v.into()))
202                    .collect(),
203            )),
204        );
205        self
206    }
207
208    /// Set the initially selected choices.
209    pub fn values(mut self, selected: &[usize]) -> Self {
210        // Set state
211        self.attr(
212            Attribute::Value,
213            AttrValue::Payload(PropPayload::Vec(
214                selected.iter().map(|x| PropValue::Usize(*x)).collect(),
215            )),
216        );
217        self
218    }
219
220    /// Set the current component to be always active (show highligh even if unfocused)
221    pub fn always_active(mut self) -> Self {
222        self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
223        self
224    }
225
226    fn rewindable(&self) -> bool {
227        self.props
228            .get(Attribute::Rewind)
229            .and_then(AttrValue::as_flag)
230            .unwrap_or_default()
231    }
232}
233
234impl Component for Checkbox {
235    fn view(&mut self, render: &mut Frame, area: Rect) {
236        if !self.common.display {
237            return;
238        }
239
240        // Make choices
241        let choices: Vec<Line> = self
242            .states
243            .choices
244            .iter()
245            .enumerate()
246            .map(|(idx, x)| {
247                let checkbox: &str = if self.states.has(idx) { "☑ " } else { "☐ " };
248                // Make Lines
249                Line::from(vec![Span::raw(checkbox), Span::raw(x.to_string())])
250            })
251            .collect();
252        let mut widget: Tabs = Tabs::new(choices)
253            .select(self.states.choice)
254            .style(self.common.style)
255            .highlight_style(
256                self.common_hg
257                    .get_style_focus(self.common.style, self.common.is_active()),
258            );
259
260        if let Some(block) = self.common.get_block() {
261            widget = widget.block(block);
262        }
263
264        render.render_widget(widget, area);
265    }
266
267    fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
268        if let Some(value) = self
269            .common
270            .get_for_query(attr)
271            .or_else(|| self.common_hg.get_for_query(attr))
272        {
273            return Some(value);
274        }
275
276        self.props.get_for_query(attr)
277    }
278
279    fn attr(&mut self, attr: Attribute, value: AttrValue) {
280        if let Some(value) = self
281            .common
282            .set(attr, value)
283            .and_then(|value| self.common_hg.set(attr, value))
284        {
285            match attr {
286                Attribute::Content => {
287                    // Reset choices
288                    let current_selection = self.states.selection.clone();
289                    let choices: Vec<String> = value
290                        .unwrap_payload()
291                        .unwrap_vec()
292                        .iter()
293                        .cloned()
294                        .map(|x| x.unwrap_str())
295                        .collect();
296                    self.states.set_choices(choices);
297                    // Preserve selection if possible
298                    for c in current_selection {
299                        self.states.select(c);
300                    }
301                }
302                Attribute::Value => {
303                    // Clear section
304                    self.states.selection.clear();
305                    for c in value.unwrap_payload().unwrap_vec() {
306                        self.states.select(c.unwrap_usize());
307                    }
308                }
309                attr => {
310                    self.props.set(attr, value);
311                }
312            }
313        }
314    }
315
316    /// ### get_state
317    ///
318    /// Get current state from component
319    /// For this component returns the vec of selected items
320    fn state(&self) -> State {
321        State::Vec(
322            self.states
323                .selection
324                .iter()
325                .map(|x| StateValue::Usize(*x))
326                .collect(),
327        )
328    }
329
330    fn perform(&mut self, cmd: Cmd) -> CmdResult {
331        match cmd {
332            Cmd::Move(Direction::Right) => {
333                // Increment choice
334                self.states.next_choice(self.rewindable());
335                CmdResult::Visual
336            }
337            Cmd::Move(Direction::Left) => {
338                // Decrement choice
339                self.states.prev_choice(self.rewindable());
340                CmdResult::Visual
341            }
342            Cmd::Toggle => {
343                self.states.toggle();
344                CmdResult::Changed(self.state())
345            }
346            Cmd::Submit => {
347                // Return Submit
348                CmdResult::Submit(self.state())
349            }
350            _ => CmdResult::Invalid(cmd),
351        }
352    }
353}
354
355#[cfg(test)]
356mod test {
357
358    use pretty_assertions::{assert_eq, assert_ne};
359    use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
360
361    use super::*;
362
363    #[test]
364    fn test_components_checkbox_states() {
365        let mut states: CheckboxStates = CheckboxStates::default();
366        assert_eq!(states.choice, 0);
367        assert_eq!(states.choices.len(), 0);
368        assert_eq!(states.selection.len(), 0);
369        let choices: &[String] = &[
370            "lemon".to_string(),
371            "strawberry".to_string(),
372            "vanilla".to_string(),
373            "chocolate".to_string(),
374        ];
375        states.set_choices(choices);
376        assert_eq!(states.choice, 0);
377        assert_eq!(states.choices.len(), 4);
378        assert_eq!(states.selection.len(), 0);
379        // Select
380        states.toggle();
381        assert_eq!(states.selection, vec![0]);
382        // Move
383        states.prev_choice(false);
384        assert_eq!(states.choice, 0);
385        states.next_choice(false);
386        assert_eq!(states.choice, 1);
387        states.next_choice(false);
388        assert_eq!(states.choice, 2);
389        states.toggle();
390        assert_eq!(states.selection, vec![0, 2]);
391        // Forward overflow
392        states.next_choice(false);
393        states.next_choice(false);
394        assert_eq!(states.choice, 3);
395        states.prev_choice(false);
396        assert_eq!(states.choice, 2);
397        states.toggle();
398        assert_eq!(states.selection, vec![0]);
399        // has
400        assert_eq!(states.has(0), true);
401        assert_ne!(states.has(2), true);
402        // Update
403        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
404        states.set_choices(choices);
405        assert_eq!(states.choice, 1); // Move to first index available
406        assert_eq!(states.choices.len(), 2);
407        assert_eq!(states.selection.len(), 0);
408        let choices: &[String] = &[];
409        states.set_choices(choices);
410        assert_eq!(states.choice, 0); // Move to first index available
411        assert_eq!(states.choices.len(), 0);
412        assert_eq!(states.selection.len(), 0);
413        // Rewind
414        let choices: &[String] = &[
415            "lemon".to_string(),
416            "strawberry".to_string(),
417            "vanilla".to_string(),
418            "chocolate".to_string(),
419        ];
420        states.set_choices(choices);
421        assert_eq!(states.choice, 0);
422        states.prev_choice(true);
423        assert_eq!(states.choice, 3);
424        states.next_choice(true);
425        assert_eq!(states.choice, 0);
426        states.next_choice(true);
427        assert_eq!(states.choice, 1);
428        states.prev_choice(true);
429        assert_eq!(states.choice, 0);
430    }
431
432    #[test]
433    fn test_components_checkbox() {
434        // Make component
435        let mut component = Checkbox::default()
436            .background(Color::Blue)
437            .foreground(Color::Red)
438            .borders(Borders::default())
439            .title(Title::from("Which food do you prefer?").alignment(HorizontalAlignment::Center))
440            .choices(["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
441            .values(&[1, 4])
442            .rewind(false);
443        // Verify states
444        assert_eq!(component.states.selection, vec![1, 4]);
445        assert_eq!(component.states.choice, 0);
446        assert_eq!(component.states.choices.len(), 5);
447        component.attr(
448            Attribute::Content,
449            AttrValue::Payload(PropPayload::Vec(vec![
450                PropValue::Str(String::from("Pizza")),
451                PropValue::Str(String::from("Hummus")),
452                PropValue::Str(String::from("Ramen")),
453                PropValue::Str(String::from("Gyoza")),
454                PropValue::Str(String::from("Pasta")),
455                PropValue::Str(String::from("Falafel")),
456            ])),
457        );
458        assert_eq!(component.states.selection, vec![1, 4]);
459        assert_eq!(component.states.choices.len(), 6);
460        // Get value
461        component.attr(
462            Attribute::Value,
463            AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
464        );
465        assert_eq!(component.states.selection, vec![1]);
466        assert_eq!(component.states.choices.len(), 6);
467        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
468        // Handle events
469        assert_eq!(
470            component.perform(Cmd::Move(Direction::Left)),
471            CmdResult::Visual,
472        );
473        assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
474        // Toggle
475        assert_eq!(
476            component.perform(Cmd::Toggle),
477            CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
478        );
479        // Left again
480        assert_eq!(
481            component.perform(Cmd::Move(Direction::Left)),
482            CmdResult::Visual,
483        );
484        assert_eq!(component.states.choice, 0);
485        // Right
486        assert_eq!(
487            component.perform(Cmd::Move(Direction::Right)),
488            CmdResult::Visual,
489        );
490        // Toggle
491        assert_eq!(
492            component.perform(Cmd::Toggle),
493            CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
494        );
495        // Right again
496        assert_eq!(
497            component.perform(Cmd::Move(Direction::Right)),
498            CmdResult::Visual,
499        );
500        assert_eq!(component.states.choice, 2);
501        // Right again
502        assert_eq!(
503            component.perform(Cmd::Move(Direction::Right)),
504            CmdResult::Visual,
505        );
506        assert_eq!(component.states.choice, 3);
507        // Right again
508        assert_eq!(
509            component.perform(Cmd::Move(Direction::Right)),
510            CmdResult::Visual,
511        );
512        assert_eq!(component.states.choice, 4);
513        // Right again
514        assert_eq!(
515            component.perform(Cmd::Move(Direction::Right)),
516            CmdResult::Visual,
517        );
518        assert_eq!(component.states.choice, 5);
519        // Right again
520        assert_eq!(
521            component.perform(Cmd::Move(Direction::Right)),
522            CmdResult::Visual,
523        );
524        assert_eq!(component.states.choice, 5);
525        // Submit
526        assert_eq!(
527            component.perform(Cmd::Submit),
528            CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
529        );
530    }
531
532    #[test]
533    fn various_set_choice_types() {
534        // static array of strings
535        CheckboxStates::default().set_choices(&["hello".to_string()]);
536        // vector of strings
537        CheckboxStates::default().set_choices(vec!["hello".to_string()]);
538        // boxed array of strings
539        CheckboxStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
540    }
541
542    #[test]
543    fn various_choice_types() {
544        // static array of static strings
545        let _ = Checkbox::default().choices(["hello"]);
546        // static array of strings
547        let _ = Checkbox::default().choices(["hello".to_string()]);
548        // vec of static strings
549        let _ = Checkbox::default().choices(vec!["hello"]);
550        // vec of strings
551        let _ = Checkbox::default().choices(vec!["hello".to_string()]);
552        // boxed array of static strings
553        let _ = Checkbox::default().choices(vec!["hello"].into_boxed_slice());
554        // boxed array of strings
555        let _ = Checkbox::default().choices(vec!["hello".to_string()].into_boxed_slice());
556    }
557}